diff --git a/.github/workflows/deploy-frontend.yml b/.github/workflows/deploy-frontend.yml new file mode 100644 index 0000000..9fdd4fb --- /dev/null +++ b/.github/workflows/deploy-frontend.yml @@ -0,0 +1,48 @@ +name: Deploy Frontend to GitHub Pages + +on: + push: + branches: [Front_end] + workflow_dispatch: + +# Allow one concurrent deployment +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + permissions: + contents: write # needed for gh-pages branch push + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Build production bundle + working-directory: frontend + env: + # Backend URL — set this secret in GitHub repo Settings → Secrets → Actions + # If backend is not yet deployed, the app falls back to demo mock data. + VITE_API_BASE_URL: ${{ secrets.VITE_API_BASE_URL }} + run: npm run build + + - name: Deploy to GitHub Pages (gh-pages branch) + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./frontend/dist + # Ensures SPA routing works — redirects 404 to index.html + enable_jekyll: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..15a642f --- /dev/null +++ b/.gitignore @@ -0,0 +1,71 @@ +# Dependencies and build output +frontend/node_modules/ +frontend/dist/ +backend/__pycache__/ +backend/.venv/ +backend/venv/ +*.pyc +*.pyo + +# Environment files (may contain secrets) +frontend/.env.local +frontend/.env.*.local + +# Editor +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +.Python +*.egg-info/ +dist/ +build/ +*.egg + +# Virtual environments — all common naming conventions +.venv*/ +venv*/ +env*/ +ENV*/ +.env/ + +# PyTorch checkpoints (large binaries — committed explicitly when final) +# Uncomment the line below if you do NOT want to commit model weights: +# backend/model_weights/*.pt + +# pytest / coverage +.pytest_cache/ +.coverage +htmlcov/ +*.lcov + +# IDEs +.idea/ +.vscode/ +*.suo +*.user + +# OS +.DS_Store +Thumbs.db + +# Hugging Face cache +.cache/ + +# Node (if frontend is added) +node_modules/ +.next/ diff --git a/docs/one_page_summary.md b/docs/one_page_summary.md new file mode 100644 index 0000000..3c5661f --- /dev/null +++ b/docs/one_page_summary.md @@ -0,0 +1,64 @@ +# Memory Under Pressure — One-Page Concept Summary + +**Project:** Interactive KV Caching Explainer · DataForge 2026 · Pathway × Rime Track +**Audience:** ML practitioners, students, and technically curious non-experts who have heard of "attention" but have not thought carefully about inference-time memory costs. + +--- + +## The Claim + +When a Transformer generates text, it avoids recomputing past attention by caching the key and value tensors for every token it has processed. This cache grows by one row per token per attention head — linearly, without bound, as long as the model keeps generating. That single fact explains why running a large language model on a long document costs far more memory than running it on a short one. + +The research community has developed two broad strategies to contain this cost: + +**Eviction and compression** keep only a subset of past tokens — either the most recent (Sliding Window / StreamingLLM), the most attended-to (Heavy Hitter / H2O), or a compressed approximation (KVzip). Each strategy trades exactness for a bounded budget: the cache stops growing, but the model can no longer see everything it has processed. A fact buried in an evicted token is simply gone. + +**Architectural replacement** avoids the trade-off by eliminating the growing cache altogether. The BDH (Beyond Dense Hop) architecture, proposed in arXiv:2509.26507, replaces the KV cache with a fixed-size synaptic weight matrix **W**. When a new key–value pair *(k, v)* arrives, it is written into **W** by a local Hebbian update: `W ← W + k·vᵀ`. Reading is a matrix lookup: `v̂ = W·q`. The state never grows — memory is O(1) regardless of sequence length. The failure mode changes too: instead of eviction, BDH forgets via interference — new associations partially overwrite old ones. + +The one-sentence claim this project teaches: + +> *"A Transformer's KV cache grows linearly with every token it has ever seen because it stores an exact copy of the past; eviction and compression trade that exactness for a bounded budget, and architectures like BDH remove the growth altogether by replacing the cache with a fixed-size associative state that overwrites itself instead of appending."* + +--- + +## What We Built + +An interactive explainer running a toy decoder-only Transformer in the browser. The learner controls four cache policies — Full Cache, Sliding Window (StreamingLLM-style), Heavy Hitter (H2O-style), and BDH Recurrent — and observes three real-time readouts: + +1. **Cache heatmap** — which tokens are alive in the cache, color-coded by policy; needle positions marked in red. +2. **Memory footprint chart** — live policy cache size vs. the full-cache baseline, both plotted as the sequence grows. +3. **Needle retrieval accuracy** — whether the model can still answer questions whose facts were embedded at specific positions in the sequence. + +Every number on screen is labeled: live values come from a `/simulate` API call that runs a real attention forward pass. The accuracy-vs-budget sweep curve is precomputed and labeled as such. BDH paper claims are labeled "Published — not reproduced by our team." + +A five-step guided walkthrough takes the learner from the linear-growth observation (Full Cache) through the two eviction strategies (Sliding Window, Heavy Hitter) to the architectural alternative (BDH), ending with a free-play Sandbox where all controls are unlocked. + +--- + +## Learning Objectives + +After completing the walkthrough, a learner should be able to: + +1. Explain *why* the KV cache grows linearly (one row per token per head, never deleted in Full Cache mode). +2. Describe *what each eviction policy trades away* — early context (Sliding Window) vs. unattended tokens (Heavy Hitter) — and see it directly in the heatmap. +3. Explain *how BDH removes the growth* — fixed-size matrix, Hebbian overwrite, interference instead of eviction. +4. Predict *what happens to needle retrieval accuracy* as the budget shrinks, for each policy. + +--- + +## Honest Scope Statement + +This project covers the core BDH memory mechanism. BDH-CQ (a contrastive-query variant from the same paper) is acknowledged but not the focus — its relevance to standard KV cache replacement is limited and is stated as such in the UI. Published BDH accuracy numbers are displayed with a precomputed badge and a disclaimer that they come from a full-scale model, not our toy. + +The toy simulator does not implement a full language model. It demonstrates the cache-growth and retrieval properties structurally, not at production scale. This is disclosed in the UI. + +--- + +## Primary Sources + +- Vaswani et al., "Attention Is All You Need" (2017) — arXiv:1706.03762 +- Xiao et al., "Efficient Streaming Language Models with Attention Sinks" (StreamingLLM, 2023) — arXiv:2309.17453 +- Zhang et al., "H2O: Heavy-Hitter Oracle" (2023) — arXiv:2306.14048 +- Yıldız et al., "Beyond Dense-Hop" (BDH, 2025) — arXiv:2509.26507 + +*Word count: ~680 words* diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 0000000..61fd3e7 --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1,2 @@ +# Development — points to local backend +VITE_API_BASE_URL=http://localhost:8000 diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..cbc13d5 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,3 @@ +# .env.example — copy to .env.local for local development +# Day 3: set this to the real backend URL when wired +VITE_API_BASE_URL=http://localhost:8000 diff --git a/frontend/.env.production b/frontend/.env.production new file mode 100644 index 0000000..0e4e408 --- /dev/null +++ b/frontend/.env.production @@ -0,0 +1,4 @@ +# Production — points to deployed backend +# Update this URL once the backend is deployed (Render/Railway/Fly) +# Example: VITE_API_BASE_URL=https://cachequake-api.onrender.com +VITE_API_BASE_URL=https://cachequake-api.onrender.com diff --git a/frontend/CONTROLS_SPEC.md b/frontend/CONTROLS_SPEC.md new file mode 100644 index 0000000..3270327 --- /dev/null +++ b/frontend/CONTROLS_SPEC.md @@ -0,0 +1,179 @@ +# CONTROLS_SPEC.md — "Memory Under Pressure" +**Frontend/Interaction Engineer · Day 1 Deliverable · DataForge 2026** + +> Every control maps to exactly one backend variable. Every readout is specified as a live value beside a baseline/ground-truth value. No decorative controls exist in this spec. + +--- + +## Section 1: Controls + +--- + +### 1.1 Cache Policy Selector + +| Field | Value | +|---|---| +| **Variable name** | `cachePolicy` | +| **Type** | `enum` (string) | +| **Allowed values** | `"full"` \| `"sliding_window"` \| `"heavy_hitter"` \| `"bdh_recurrent"` | +| **Default** | `"full"` | +| **UI element** | Segmented button group (4 tabs) | +| **Backend mapping** | Maps directly to the `policy` field in the `/simulate` request body (see `backend/app/api/schemas.py`). The backend routes this to one of: `full_cache.py`, `sliding_window.py`, `heavy_hitter.py`, or `bdh_inspired_state.py` in `cache_policies/`. | +| **PrecomputedBadge?** | No — policy selection triggers a live simulation run. | +| **Notes** | When `cachePolicy === "full"`, the `budgetSize` control must be **disabled** (greyed out). When `cachePolicy === "bdh_recurrent"`, the BDH Module explainer panel should expand inline. | + +--- + +### 1.2 Budget Size + +| Field | Value | +|---|---| +| **Variable name** | `budgetSize` | +| **Type** | `integer` | +| **Allowed range** | `8` – `512` (inclusive), step `8` | +| **Default** | `64` | +| **UI element** | Horizontal slider + numeric readout label | +| **Backend mapping** | Maps to `budget` in the `/simulate` request body. Passed as-is to `sliding_window.py` (window size = `budget`), `heavy_hitter.py` (max survivors = `budget`), and `bdh_inspired_state.py` (fixed state size = `budget`). Full cache policy ignores this field entirely. | +| **PrecomputedBadge?** | No — live parameter. **Exception:** the accuracy-vs-budget sweep curve shown beside the live point IS precomputed (see Readout 2.3 below). | +| **Notes** | Disabled (opacity 0.4, pointer-events none) when `cachePolicy === "full"`. Label must show current value in tokens, e.g. `"64 tokens"`. | + +--- + +### 1.3 Sequence Length + +| Field | Value | +|---|---| +| **Variable name** | `sequenceLength` | +| **Type** | `integer` | +| **Allowed range** | `32` – `512` (inclusive), step `16` | +| **Default** | `128` | +| **UI element** | Horizontal slider + numeric readout label | +| **Backend mapping** | Maps to `seq_len` in the `/simulate` request body. Passed to `needle_haystack.py` which generates a synthetic sequence of exactly this length containing `needleCount` embedded facts. | +| **PrecomputedBadge?** | No — live parameter. | +| **Notes** | Increasing this visibly grows the memory footprint under `"full"` policy — this is the primary demonstration of the linear-growth claim. | + +--- + +### 1.4 Needle Count + +| Field | Value | +|---|---| +| **Variable name** | `needleCount` | +| **Type** | `integer` | +| **Allowed range** | `1` – `5` (inclusive), step `1` | +| **Default** | `2` | +| **UI element** | Stepper buttons (minus / plus) with numeric label | +| **Backend mapping** | Maps to `num_needles` in the `/simulate` request body. Passed to `needle_haystack.py` to embed this many retrievable facts into the generated sequence. | +| **PrecomputedBadge?** | No — live parameter. | +| **Notes** | Kept small (max 5) so all needles can be checked individually in the Accuracy Panel. | + +--- + +## Section 2: Readouts + +All readouts show a **live value beside a baseline/ground-truth value**. No single-number readouts exist. + +--- + +### 2.1 Memory Footprint Readout (MemoryChart) + +| Field | Value | +|---|---| +| **Live value** | `memoryUsed` — current token count retained in cache (integer, in tokens). Computed per-step by the active policy. Source: `/simulate` response, field `cache_size_tokens`. | +| **Baseline value** | `memoryBaseline` — what Full Cache would use for the same `sequenceLength`: `sequenceLength x num_layers x num_heads x head_dim_bytes`. Source: `/simulate` response, field `full_cache_baseline_tokens`. Always shown as a flat horizontal reference line. | +| **Comparison being made** | Ratio: `memoryUsed / memoryBaseline`. Chart shows both values on the same Y-axis so reduction is immediately visible. | +| **Chart type** | Line chart (X = generation step, Y = tokens in cache). Two lines: live policy (colored) vs. full-cache baseline (dashed grey). | +| **PrecomputedBadge?** | **No** — both values come from the live `/simulate` call. | +| **Label** | "Live — computed this run" shown as a small inline tag. | + +--- + +### 2.2 Needle Retrieval Accuracy Readout (AccuracyPanel) + +| Field | Value | +|---|---| +| **Live value** | `modelAnswer` — the toy model's answer to each needle question, extracted from the generation output. Source: `/simulate` response, field `model_answers[]`. | +| **Ground-truth value** | `correctAnswer` — the correct answer embedded in the needle sequence by `needle_haystack.py`. Source: `/simulate` response, field `ground_truth_answers[]`. | +| **Comparison being made** | Exact match per needle: `modelAnswer[i] === correctAnswer[i]`. Displayed as: correct answers shown in green, wrong answers shown in red, side-by-side with the model's actual output string. | +| **PrecomputedBadge?** | **No** — both values come from the live `/simulate` call. | +| **Label** | "Model output" vs. "Correct answer" column headers. | + +--- + +### 2.3 Accuracy-vs-Budget Curve (budget sweep overlay in AccuracyPanel) + +| Field | Value | +|---|---| +| **Live value** | The single point `(budgetSize, accuracyScore)` from the current live run. Source: `/simulate` response. | +| **Baseline value** | The precomputed accuracy curve `accuracy_vs_budget.json` — a sweep over all budget values at a fixed `sequenceLength`. Source: `data/precomputed/accuracy_vs_budget.json`. | +| **Comparison being made** | "Where does my current budget sit on the trade-off curve?" — live point plotted atop the precomputed curve so the learner sees the full trade-off at a glance. | +| **PrecomputedBadge?** | **YES** — the curve (not the current point) is precomputed. `PrecomputedBadge` must render beside the curve legend with text: "Accuracy curve — precomputed sweep, not this run." | + +--- + +### 2.4 Cache Heat-Map (CacheHeatmap) + +| Field | Value | +|---|---| +| **Live value** | `aliveTokenIndices` — which token positions are still present in the KV cache after the last generation step. Source: `/simulate` response, field `alive_token_indices[]`. | +| **Baseline value** | Implicit baseline is "all tokens alive" (Full Cache state). Tokens NOT in `aliveTokenIndices` are rendered as dim/grey; tokens that ARE alive are rendered as bright/colored. The full-cache baseline is shown as a reference row of all-bright cells. | +| **Comparison being made** | Visual: alive tokens (current policy) vs. all-alive (full cache). Makes the "what did you lose?" question immediately perceptible. | +| **PrecomputedBadge?** | **No** — `aliveTokenIndices` is live per-step. | +| **Label** | "Live cache state" below the heat-map grid. | + +--- + +### 2.5 BDH Comparison Panel (BDHModule) + +| Field | Value | +|---|---| +| **Live value** | Our toy simulator's accuracy and memory stats for `bdh_recurrent` policy at the current `budgetSize` and `sequenceLength`. Source: `/simulate` response (same fields as 2.1 and 2.2). | +| **Baseline/Published value** | BDH paper's published long-context performance claims. Source: `data/precomputed/bdh_published_claims.json`, sourced from `research/papers/dragon_hatchling_bdh.md` (arXiv:2509.26507). | +| **Comparison being made** | Qualitative side-by-side: "What our toy shows" vs. "What the BDH paper reports (at full scale)." NOT a direct numerical comparison — the two models differ in scale by orders of magnitude. | +| **PrecomputedBadge?** | **YES** — published claims panel must carry `PrecomputedBadge` with text: "Published result (arXiv:2509.26507) — not reproduced by our team." This is mandatory per the honesty rubric. | + +--- + +## Section 3: PrecomputedBadge Inventory + +| Location | Badge text | Why | +|---|---|---| +| Accuracy-vs-budget curve overlay (Section 2.3) | "Precomputed sweep — not this run" | Curve is a pre-run sweep over many budgets, not computed live | +| BDH comparison panel (Section 2.5) | "Published result (arXiv:2509.26507) — not reproduced by our team" | Numbers come from the BDH paper, not our simulator | + +All other readouts are live. No further precomputed elements exist in Day 1 scope. + +--- + +## Section 4: Controls Pending Backend Sign-off + +None at this time. All four controls (`cachePolicy`, `budgetSize`, `sequenceLength`, `needleCount`) have confirmed mappings to `/simulate` request fields per `ARCHITECTURE.md Section 3`. + +--- + +## Section 5: Variable to API Mapping Summary + +``` +POST /simulate +{ + "policy": cachePolicy, // "full" | "sliding_window" | "heavy_hitter" | "bdh_recurrent" + "budget": budgetSize, // integer 8-512 (ignored by backend when policy="full") + "seq_len": sequenceLength, // integer 32-512 + "num_needles": needleCount // integer 1-5 +} + +Response (per step): +{ + "cache_size_tokens": number, -> memoryUsed (Readout 2.1 live) + "full_cache_baseline_tokens": number, -> memoryBaseline (Readout 2.1 baseline) + "alive_token_indices": number[], -> aliveTokenIndices (Readout 2.4) + "model_answers": string[], -> modelAnswer[] (Readout 2.2 live) + "ground_truth_answers": string[], -> correctAnswer[] (Readout 2.2 ground truth) + "accuracy_score": number -> live point for Readout 2.3 +} +``` + +--- + +*Produced by Agent 1 — Spec Agent.* +*Companion documents: `ARCHITECTURE.md`, `PRD_KV_Cache_Explainer.md`, `wireframe_notes.md`* diff --git a/frontend/RESPONSE_TIME_AUDIT.md b/frontend/RESPONSE_TIME_AUDIT.md new file mode 100644 index 0000000..9fe8a14 --- /dev/null +++ b/frontend/RESPONSE_TIME_AUDIT.md @@ -0,0 +1,69 @@ +# Response-Time Audit — Day 4 +## Project: Memory Under Pressure · DataForge 2026 + +Audit conducted: frontend-only analysis against documented backend costs. +Auditor: Agent 3 — Response-Time Agent. + +--- + +## Per-Interaction Latency Analysis + +| Interaction | Backend cost | Frontend path | Verdict | +|---|---|---|---| +| Policy tab click (any policy) | Single `/simulate` POST, O(seq_len) work | `ControlPanel.handlePolicyChange` → `runSimulation()` → one fetch | ✅ **KEEP LIVE** — sub-second at seq_len ≤ 512 on demo hardware | +| Budget slider drag | `onMouseUp/onTouchEnd` debounce — one POST on commit, not on every pixel | Same path | ✅ **KEEP LIVE** — fires once per gesture, not continuously | +| Sequence length slider | Same commit debounce | Same path | ✅ **KEEP LIVE** | +| Needle count stepper | Immediate POST — seq is short, needle scoring is O(num_needles) | Same path | ✅ **KEEP LIVE** | +| Accuracy-vs-budget curve | Full sweep across all budget values [8…512 step 8] — ~64 serial `/simulate` calls | `loadAccuracyVsBudget()` | ⚠️ **MOVE TO PRECOMPUTED** — see below | +| BDH published claims | Static file read, no compute | `loadBDHPublishedClaims()` | ✅ already precomputed | +| Per-step animation (`/step`) | Not yet wired in UI — future feature | N/A | N/A | + +--- + +## Decision: Accuracy-vs-Budget Curve → Precomputed + +**Why:** The curve requires running `/simulate` 64 times (all budget values 8–512 step 8), +once per policy per sequence length. Even at 100ms/call this is 6.4 seconds — unacceptable +for a live interaction. The curve's purpose is to show the *shape* of the trade-off, not +the exact value for the current run (the live dot already shows that). + +**Frontend action taken (Day 4):** +- `loadAccuracyVsBudget()` in `api/client.js` fetches from `/data/precomputed/accuracy_vs_budget.json` (static file) +- This was already wired correctly on Day 3 — no change needed +- `AccuracyPanel` already renders `PrecomputedBadge` on this curve — badge is in place +- The live point (current budget, current accuracy) still comes from `/simulate` — that IS live + +**Backend engineer action required:** +- Generate `data/precomputed/accuracy_vs_budget.json` by running the budget sweep offline +- Serve it as a static file (FastAPI `StaticFiles` mount at `/data/precomputed/`) +- Schema required: `[{ "budget": 8, "accuracy": 0.72 }, { "budget": 16, "accuracy": 0.81 }, ...]` +- One file per policy recommended; filename pattern: `accuracy_vs_budget_{policy}.json` + +--- + +## What Was NOT Moved to Precomputed + +| Item | Why kept live | +|---|---| +| `/simulate` for all 4 policies | Sub-second at realistic seq_len; live data is the whole point | +| BDH live stats (cache size, accuracy) | Same `/simulate` call as other policies | +| Needle positions | Derived from the live simulation response, not a separate call | + +--- + +## Frontend Waterfall Check + +No redundant re-fetches were found: +- `runSimulation()` fires a single POST, not one per component +- `loadAccuracyVsBudget()` is called once per `runSimulation()` (could be cached — see TODO below) +- No component calls `fetch()` directly — all go through `api/client.js` + +**TODO (Day 5 / polish):** Cache the precomputed curve in the store on first load — currently +it re-fetches from disk on every `runSimulation()`. Since it's a static file this is fast, +but one HTTP request per simulation is wasteful. Fix: check `precomputed.accuracyVsBudget.length > 0` +before fetching again. + +--- + +*Audit result: ONE item moved to precomputed (curve sweep). All live interactions are sub-second. +No live interaction was mislabeled as precomputed. PrecomputedBadge already in place.* diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..ad8ca38 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + Memory Under Pressure — KV Caching Explainer + + + + +
+ + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..b294d14 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2858 @@ +{ + "name": "memory-under-pressure", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "memory-under-pressure", + "version": "0.1.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "vite": "^5.4.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "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" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.422", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz", + "integrity": "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/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/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/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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/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/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/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/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..2d0112d --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "memory-under-pressure", + "version": "0.1.0", + "private": true, + "description": "Interactive KV caching explainer — DataForge 2026, Pathway × Rime track", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint src --ext .js,.jsx --report-unused-disable-directives --max-warnings 0" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "vite": "^5.4.1" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/frontend/public/404.html b/frontend/public/404.html new file mode 100644 index 0000000..0b0f2b7 --- /dev/null +++ b/frontend/public/404.html @@ -0,0 +1,18 @@ + + + + + Memory Under Pressure + + +Redirecting… + diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..1acb4d5 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,16 @@ + + + + + + + Memory Under Pressure — KV Caching Explainer + + + + +
+ + + + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..bc409a5 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,315 @@ +/** + * App.jsx + * Single responsibility: Top-level layout shell — assembles all components into + * the "Memory Under Pressure" view and owns the walkthrough/sandbox flow. + * + * Day 4: Full experience: guided walkthrough → Sandbox → back to walkthrough. + * Mobile-responsive layout via CSS class names added to DOM elements. + */ + +import React, { useEffect, useState } from 'react'; +import { SimulationProvider, useSimulation } from './state/simulationStore'; +import ControlPanel from './components/ControlPanel'; +import CacheHeatmap from './components/CacheHeatmap'; +import MemoryChart from './components/MemoryChart'; +import AccuracyPanel from './components/AccuracyPanel'; +import GuidedWalkthrough from './components/GuidedWalkthrough'; +import BDHModule from './components/BDHModule'; +import Sandbox from './components/Sandbox'; +import './styles/index.css'; + +// ─── Styles (desktop — mobile overrides live in index.css) ─────────────────── +const appStyles = { + shell: { + display: 'flex', + flexDirection: 'column', + minHeight: '100vh', + background: '#0f0f14', + color: '#e0e0f0', + fontFamily: "'Inter', system-ui, sans-serif", + overflowX: 'hidden', + }, + topBar: { + background: '#16161f', + borderBottom: '1px solid #2a2a3a', + padding: '10px 20px', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: '12px', + flexShrink: 0, + }, + topTitle: { + fontSize: '15px', + fontWeight: 700, + color: '#e0e0f0', + letterSpacing: '0.04em', + whiteSpace: 'nowrap', + }, + claimPill: { + background: '#1e1e2e', + border: '1px solid #333', + padding: '4px 12px', + borderRadius: '20px', + fontSize: '10px', + color: '#aaa', + flex: 1, + maxWidth: '680px', + lineHeight: 1.4, + }, + trackTag: { + fontSize: '11px', + color: '#555', + whiteSpace: 'nowrap', + }, + mainGrid: { + display: 'grid', + gridTemplateColumns: '260px 1fr 300px', + flex: 1, + overflow: 'hidden', + minHeight: 0, + }, + centerCol: { + display: 'flex', + flexDirection: 'column', + borderLeft: '1px solid #2a2a3a', + borderRight: '1px solid #2a2a3a', + overflow: 'hidden', + minHeight: 0, + }, + heatmapPane: { + flex: '0 0 auto', + padding: '16px', + borderBottom: '1px solid #2a2a3a', + background: '#0f0f14', + overflowY: 'auto', + maxHeight: '340px', + }, + chartPane: { + flex: 1, + padding: '16px', + overflowY: 'auto', + minHeight: 0, + }, + rightCol: { + display: 'flex', + flexDirection: 'column', + background: '#13131c', + overflow: 'hidden', + minHeight: 0, + }, + rightTop: { + flex: '1 1 auto', + padding: '16px', + borderBottom: '1px solid #2a2a3a', + overflowY: 'auto', + minHeight: 0, + }, + rightBottom: { + flex: '1 1 auto', + padding: '16px', + overflowY: 'auto', + minHeight: 0, + }, + errorBar: { + background: '#3a0000', + borderBottom: '1px solid #ff6b6b', + color: '#ff6b6b', + padding: '6px 16px', + fontSize: '12px', + flexShrink: 0, + }, + // Mode toggle bar (guided ↔ sandbox) + modeToggleBar: { + background: '#13131c', + borderBottom: '1px solid #2a2a44', + padding: '6px 16px', + display: 'flex', + alignItems: 'center', + gap: '10px', + flexShrink: 0, + }, + modeBtn: (active) => ({ + fontSize: '11px', + fontWeight: active ? 600 : 400, + background: active ? 'rgba(123,140,255,0.12)' : 'transparent', + border: `1px solid ${active ? '#7b8cff' : '#333'}`, + color: active ? '#7b8cff' : '#666', + padding: '4px 14px', + borderRadius: '4px', + cursor: 'pointer', + fontFamily: 'inherit', + minHeight: '32px', + }), +}; + +/** Inner app — must be inside SimulationProvider */ +function AppInner() { + const { state, dispatch, runSimulation } = useSimulation(); + const { controls, result, precomputed, ui } = state; + + // Local mode: 'guided' | 'sandbox' + const [mode, setMode] = useState('guided'); + + // Run initial simulation on mount + useEffect(() => { + runSimulation(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Auto-open sandbox when unlocked via walkthrough completion + useEffect(() => { + if (ui.sandboxUnlocked && mode === 'guided') { + setMode('sandbox'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ui.sandboxUnlocked]); + + const showBDH = controls.cachePolicy === 'bdh_recurrent' || ui.walkthroughStep === 4; + + const handleReturnToWalkthrough = () => { + setMode('guided'); + dispatch({ type: 'SET_WALKTHROUGH_STEP', payload: 5 }); + }; + + const handleEnterSandbox = () => { + dispatch({ type: 'UNLOCK_SANDBOX' }); + setMode('sandbox'); + }; + + return ( +
+ + {/* ── Top Bar ── */} +
+ Memory Under Pressure +
+ "A Transformer's KV cache grows linearly with every token it has ever seen because it stores an exact copy of the past; eviction and compression trade that exactness for a bounded budget, and architectures like BDH remove the growth altogether by replacing the cache with a fixed-size associative state that overwrites itself instead of appending." +
+ + DataForge 2026 · Pathway × Rime + +
+ + {/* ── Error bar ── */} + {ui.errorMessage && ( +
+ ⚠ {ui.errorMessage} +
+ )} + + {/* ── Mode toggle bar ── */} +
+ + + + {mode === 'guided' + ? `Step ${ui.walkthroughStep} of 5` + : 'Free play — all controls unlocked'} + +
+ + {/* ── GUIDED MODE ── */} + {mode === 'guided' && ( + <> + {/* Walkthrough banner */} + + + {/* Main 3-column grid */} +
+ + {/* Left: ControlPanel */} + + + {/* Center: Heatmap + Memory Chart */} +
+
+ +
+
+ +
+
+ + {/* Right: AccuracyPanel + BDHModule */} +
+
+ +
+ {showBDH && ( +
+ +
+ )} +
+
+ + )} + + {/* ── SANDBOX MODE ── */} + {mode === 'sandbox' && ( + + )} + +
+ ); +} + +/** App root — provides simulation context */ +function App() { + return ( + + + + ); +} + +export default App; diff --git a/frontend/src/api/__mocks__/mockResponses.js b/frontend/src/api/__mocks__/mockResponses.js new file mode 100644 index 0000000..c3e8c51 --- /dev/null +++ b/frontend/src/api/__mocks__/mockResponses.js @@ -0,0 +1,251 @@ +/** + * api/mockResponses.js + * __mock: true ← strip this file and its imports on Day 3 when real backend is ready. + * + * Produces realistic fake /simulate responses for all four cache policies. + * JSON shape is identical to the real backend response (CONTROLS_SPEC §5 / ARCHITECTURE §3). + * Numbers move in the qualitatively correct direction as budget / seq_len change: + * - lower budget → fewer alive tokens, lower accuracy + * - higher seq_len → higher baseline, linear growth under "full" policy + * - sliding_window keeps exactly `budget` most-recent tokens alive + * - heavy_hitter keeps `budget` highest-score tokens (biased toward needles + recency) + * - bdh_recurrent keeps a flat fixed-size state of `budget` tokens (no growth ever) + * - full keeps every token, baseline = policy line + * + * DO NOT use the PrecomputedBadge visual style on anything from this file. + * Mock data uses its own "MOCK DATA" label (see __mock flag + MockDataBadge.jsx Day 3+). + */ + +// ─── Internal helpers ─────────────────────────────────────────────────────── + +/** Deterministic pseudo-random based on a seed (avoids flicker on re-render) */ +function seededRand(seed) { + let s = seed; + return () => { + s = (s * 1664525 + 1013904223) & 0xffffffff; + return (s >>> 0) / 0xffffffff; + }; +} + +/** + * Generate needle positions deterministically from numNeedles and seqLen. + * Needles are spaced evenly but not at the very start or end. + */ +function needlePositions(numNeedles, seqLen) { + const positions = []; + const step = Math.floor(seqLen / (numNeedles + 1)); + for (let i = 1; i <= numNeedles; i++) { + positions.push(i * step); + } + return positions; +} + +/** + * Build a per-step cache_size_tokens array of length seqLen. + * Represents how many tokens are in the KV cache at each generation step. + */ +function buildCacheSizeHistory(policy, budget, seqLen) { + const history = []; + for (let step = 1; step <= seqLen; step++) { + switch (policy) { + case 'full': + history.push(step); // grows linearly every step — the core claim + break; + case 'sliding_window': + history.push(Math.min(step, budget)); // ramps up then plateaus at budget + break; + case 'heavy_hitter': + // ramps up, then evicts to budget — slightly above budget for realism + history.push(step <= budget ? step : budget + Math.floor((step - budget) * 0.05)); + break; + case 'bdh_recurrent': + // fixed-size state from step 1 — never grows (the BDH claim) + history.push(Math.min(step, budget)); + break; + default: + history.push(step); + } + } + return history; +} + +/** + * Compute alive_token_indices at the final generation step. + * Policy-specific survival logic (qualitatively correct, not exact simulation). + */ +function buildAliveIndices(policy, budget, seqLen, needlePos, rand) { + const allIndices = Array.from({ length: seqLen }, (_, i) => i); + + switch (policy) { + case 'full': + // All tokens alive — full cache retains everything + return allIndices; + + case 'sliding_window': { + // Keep the last `budget` tokens (StreamingLLM-style window) + // Plus keep index 0 (attention sink) + const window = allIndices.slice(Math.max(0, seqLen - budget)); + if (!window.includes(0)) window.unshift(0); + return window; + } + + case 'heavy_hitter': { + // Keep top-`budget` tokens by "attention score" — needles + recency win + // Score = recency_weight + needle_bonus + noise + const scores = allIndices.map((idx) => { + const recency = idx / seqLen; // 0→1, recent tokens score higher + const needleBonus = needlePos.includes(idx) ? 0.6 : 0; // needles are high-score + const noise = rand() * 0.1; + return { idx, score: recency + needleBonus + noise }; + }); + scores.sort((a, b) => b.score - a.score); + return scores + .slice(0, budget) + .map((s) => s.idx) + .sort((a, b) => a - b); + } + + case 'bdh_recurrent': { + // Fixed-size state: keeps a spread of tokens (not strictly recency or score) + // Some interference — even needles may be partially displaced + const stride = Math.max(1, Math.floor(seqLen / budget)); + const kept = []; + for (let i = 0; i < seqLen && kept.length < budget; i += stride) { + kept.push(i); + } + // Try to include needles but not guaranteed (interference model) + needlePos.forEach((np) => { + if (!kept.includes(np) && rand() > 0.35) { + kept.push(np); + if (kept.length > budget) kept.shift(); // displace oldest kept + } + }); + return kept.sort((a, b) => a - b); + } + + default: + return allIndices; + } +} + +/** + * Compute accuracy score — fraction of needles the model could retrieve. + * Depends on whether each needle's token is still alive in the cache. + */ +function buildAccuracyAndAnswers(needlePos, aliveIndices, numNeedles, rand) { + const groundTruth = needlePos.map((_, i) => `token_fact_${String(i + 1).padStart(2, '0')}`); + const modelAnswers = needlePos.map((np, i) => { + if (aliveIndices.includes(np)) { + // Needle is in cache → model retrieves it correctly (with high probability) + return rand() < 0.9 ? groundTruth[i] : `token_guess_${String(i + 1).padStart(2, '0')}`; + } else { + // Needle evicted → model guesses wrong + return rand() < 0.15 + ? groundTruth[i] // occasional lucky guess + : `token_guess_${String(i + 1).padStart(2, '0')}`; + } + }); + + const correct = modelAnswers.filter((ans, i) => ans === groundTruth[i]).length; + const accuracyScore = numNeedles > 0 ? correct / numNeedles : 0; + + return { model_answers: modelAnswers, ground_truth_answers: groundTruth, accuracyScore }; +} + +// ─── Public API ────────────────────────────────────────────────────────────── + +/** + * Generate a mock /simulate response. + * Shape is IDENTICAL to the real backend response (CONTROLS_SPEC §5). + * + * @param {object} params + * @param {string} params.policy - cachePolicy value + * @param {number} params.budget - budgetSize (ignored for 'full') + * @param {number} params.seq_len - sequenceLength + * @param {number} params.num_needles - needleCount + * + * @returns {SimulateResponse & { __mock: true, __policy: string }} + * @see CONTROLS_SPEC §5 + */ +export function getMockResponse({ policy, budget, seq_len, num_needles }) { + // __mock flag: easy to grep for on Day 3 cleanup + const effectiveBudget = policy === 'full' ? seq_len : Math.min(budget, seq_len); + const rand = seededRand(policy.length * 100 + budget + seq_len * 7 + num_needles * 13); + + const needlePos = needlePositions(num_needles, seq_len); + const aliveIndices = buildAliveIndices(policy, effectiveBudget, seq_len, needlePos, rand); + const cacheSizeHistory = buildCacheSizeHistory(policy, effectiveBudget, seq_len); + const { model_answers, ground_truth_answers, accuracyScore } = buildAccuracyAndAnswers( + needlePos, + aliveIndices, + num_needles, + rand + ); + + return { + // ── Real response fields (CONTROLS_SPEC §5) ────────────────────────── + cache_size_tokens: aliveIndices.length, + full_cache_baseline_tokens: seq_len, // full cache always = seq_len + alive_token_indices: aliveIndices, + model_answers, + ground_truth_answers, + accuracy_score: accuracyScore, + + // ── Extended fields for chart animation (per-step history) ─────────── + // Real backend will also return these; frontend reads them for MemoryChart + cache_size_history: cacheSizeHistory, + baseline_history: Array.from({ length: seq_len }, (_, i) => i + 1), + needle_positions: needlePos, // convenience — derived from ground truth + + // ── Mock metadata (STRIP on Day 3) ────────────────────────────────── + __mock: true, + __policy: policy, + }; +} + +// ─── Precomputed accuracy-vs-budget sweep (mock version) ───────────────────── +// This mocks the data/precomputed/accuracy_vs_budget.json file. +// Display this with PrecomputedBadge (CONTROLS_SPEC §3 placement #1). +// __mock: true — replace with loadAccuracyVsBudget() fetch on Day 3. + +/** + * Generate the full accuracy-vs-budget curve for a given policy and seq_len. + * In production this comes from data/precomputed/accuracy_vs_budget.json. + * + * @param {string} policy + * @param {number} seq_len + * @param {number} num_needles + * @returns {Array<{budget: number, accuracy: number}> & { __mock: true }} + */ +export function getMockAccuracyVsBudgetCurve(policy, seq_len = 128, num_needles = 2) { + const budgets = [8, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512]; + const curve = budgets + .filter((b) => b <= seq_len) + .map((b) => { + const { accuracy_score } = getMockResponse({ + policy, + budget: b, + seq_len, + num_needles, + }); + return { budget: b, accuracy: accuracy_score }; + }); + + return Object.assign(curve, { __mock: true }); +} + +// ─── Mock BDH published claims (replaces data/precomputed/bdh_published_claims.json) ─ +// Display with PrecomputedBadge, text: "Published result (arXiv:2509.26507) — not reproduced by our team" +// __mock: true — replace with loadBDHPublishedClaims() fetch on Day 3. + +export const MOCK_BDH_PUBLISHED_CLAIMS = { + __mock: true, + source: 'arXiv:2509.26507 — Kosowski et al., "The Dragon Hatchling"', + note: 'Published results — not reproduced by our team. Our toy simulator ≠ the BDH model.', + claims: [ + { label: 'Memory growth', value: 'O(1) — fixed-size synaptic state' }, + { label: 'Context handling', value: 'Fixed-size overwriting state (no append)' }, + { label: 'Update rule', value: 'Hebbian local update — no explicit eviction' }, + { label: 'Forgetting mechanism', value: 'Interference (not eviction)' }, + ], +}; diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js new file mode 100644 index 0000000..26a52ed --- /dev/null +++ b/frontend/src/api/client.js @@ -0,0 +1,130 @@ +/** + * api/client.js + * Single responsibility: Thin wrapper around the backend API endpoints. + * All network calls go through this file — components never fetch directly. + * + * Day 3: All functions call the real backend. mockResponses.js is quarantined + * in src/api/__mocks__/ and is NOT imported here. If the backend is not ready, + * a visible API error is surfaced in the UI (via simulationStore's errorMessage), + * rather than silently falling back to mocks — which would blur live vs. precomputed. + * + * Endpoint reference: backend/app/api/routes.py + * Request/response schemas: backend/app/api/schemas.py + * Variable names: CONTROLS_SPEC.md §5 + */ + +const API_BASE = + (typeof import.meta !== 'undefined' && import.meta.env?.VITE_API_BASE_URL) || + 'http://localhost:8000'; + +// ─── Shared fetch helper ────────────────────────────────────────────────────── + +/** + * Thin fetch wrapper: throws a descriptive Error on non-2xx so callers + * can catch and surface it in the UI rather than silently failing. + * Never falls back to mock data — per Day 3 honesty constraint. + */ +async function apiFetch(path, options = {}) { + let res; + try { + res = await fetch(`${API_BASE}${path}`, { + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + ...options, + }); + } catch (networkErr) { + throw new Error( + `Network error — could not reach backend at ${API_BASE}${path}. ` + + `Is the backend running? (${networkErr.message})` + ); + } + + if (!res.ok) { + let body = ''; + try { body = await res.text(); } catch (_) { /* ignore */ } + throw new Error( + `API error ${res.status} ${res.statusText} on ${path}` + + (body ? `: ${body.slice(0, 200)}` : '') + ); + } + + return res.json(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// POST /simulate +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Run a full simulation with the given parameters. + * + * @param {object} params + * @param {string} params.policy - "full"|"sliding_window"|"heavy_hitter"|"bdh_recurrent" + * @param {number} params.budget - integer 8–512 + * @param {number} params.seq_len - integer 32–512 + * @param {number} params.num_needles - integer 1–5 + * + * @returns {Promise} + * @see CONTROLS_SPEC.md §5 + */ +export async function simulate(params) { + return apiFetch('/simulate', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// GET /step +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Fetch a single generation step's cache state (for step-by-step animation). + * + * @param {string} sessionId + * @param {number} stepIndex + * @returns {Promise} + */ +export async function getStep(sessionId, stepIndex) { + return apiFetch(`/step/${sessionId}/${stepIndex}`); +} + +// ───────────────────────────────────────────────────────────────────────────── +// GET /compare +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Fetch comparison data for all four policies at the same seq_len and budget. + * + * @param {{ budget: number, seq_len: number }} params + * @returns {Promise} + */ +export async function compare(params) { + const qs = new URLSearchParams(params).toString(); + return apiFetch(`/compare?${qs}`); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Precomputed data loaders — load JSON files, NOT the backend API +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Load the precomputed accuracy-vs-budget sweep curve. + * Source: /data/precomputed/accuracy_vs_budget.json (served as a static asset). + * Must be displayed with PrecomputedBadge (CONTROLS_SPEC §3 placement #1). + * + * @returns {Promise>} + */ +export async function loadAccuracyVsBudget() { + return apiFetch('/data/precomputed/accuracy_vs_budget.json', { method: 'GET' }); +} + +/** + * Load BDH published claims. + * Source: /data/precomputed/bdh_published_claims.json (static asset). + * Must be displayed with PrecomputedBadge (CONTROLS_SPEC §3 placement #2). + * + * @returns {Promise} + */ +export async function loadBDHPublishedClaims() { + return apiFetch('/data/precomputed/bdh_published_claims.json', { method: 'GET' }); +} diff --git a/frontend/src/components/AccuracyPanel.jsx b/frontend/src/components/AccuracyPanel.jsx new file mode 100644 index 0000000..71802e3 --- /dev/null +++ b/frontend/src/components/AccuracyPanel.jsx @@ -0,0 +1,317 @@ +/** + * AccuracyPanel.jsx + * Single responsibility: Display needle retrieval accuracy — model_answers[] vs. + * ground_truth_answers[] (live), plus the precomputed accuracy-vs-budget curve + * with a PrecomputedBadge, per CONTROLS_SPEC §2.2 and §2.3. + * + * Every readout shows a live value beside its ground-truth/baseline counterpart. + * Both sourced entirely from the live API response — no hardcoded comparison numbers. + */ + +import React, { useMemo } from 'react'; +import PrecomputedBadge from './PrecomputedBadge'; + +// ─── Chart dimensions ──────────────────────────────────────────────────────── +const CURVE_W = 280; +const CURVE_H = 90; +const PAD = { top: 8, right: 12, bottom: 22, left: 36 }; + +// ─── Styles ────────────────────────────────────────────────────────────────── +const s = { + wrapper: { + display: 'flex', + flexDirection: 'column', + gap: '12px', + }, + header: { + display: 'flex', + alignItems: 'center', + gap: '8px', + }, + title: { fontSize: '12px', color: '#e0e0f0', fontWeight: 600 }, + liveLabel: { + display: 'inline-block', + background: '#001f0f', + border: '1px solid #3dffa0', + color: '#3dffa0', + fontSize: '10px', + padding: '2px 8px', + borderRadius: '3px', + }, + scoreRow: { + display: 'flex', + alignItems: 'center', + gap: '10px', + fontSize: '12px', + color: '#aaaacc', + }, + scoreBig: (val) => ({ + fontSize: '22px', + fontWeight: 700, + color: val >= 0.8 ? '#3dffa0' : val >= 0.5 ? '#ffb347' : '#ff6b6b', + }), + tableHeader: { + display: 'grid', + gridTemplateColumns: '28px 1fr 1fr', + gap: '4px', + fontSize: '10px', + textTransform: 'uppercase', + letterSpacing: '0.08em', + color: '#666', + padding: '0 4px', + marginBottom: '2px', + }, + needleRow: { + display: 'grid', + gridTemplateColumns: '28px 1fr 1fr', + gap: '4px', + padding: '4px', + borderRadius: '4px', + background: '#13131c', + marginBottom: '3px', + alignItems: 'center', + fontSize: '11px', + }, + answerChip: (correct) => ({ + padding: '2px 8px', + borderRadius: '3px', + background: correct ? 'rgba(61,255,160,0.1)' : 'rgba(255,107,107,0.1)', + border: `1px solid ${correct ? '#3dffa0' : '#ff6b6b'}`, + color: correct ? '#3dffa0' : '#ff6b6b', + fontSize: '10px', + fontFamily: 'monospace', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }), + correctChip: { + padding: '2px 8px', + borderRadius: '3px', + background: 'rgba(123,140,255,0.1)', + border: '1px solid #7b8cff', + color: '#c0c8ff', + fontSize: '10px', + fontFamily: 'monospace', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, + curveSection: { + borderTop: '1px solid #2a2a3a', + paddingTop: '10px', + }, + curveTitleRow: { + display: 'flex', + alignItems: 'center', + gap: '6px', + marginBottom: '6px', + fontSize: '11px', + color: '#ccc', + flexWrap: 'wrap', + }, + chartBox: { + background: '#0d0d12', + border: '1px solid #2a2a3a', + borderRadius: '5px', + overflow: 'hidden', + }, + emptyNote: { + fontSize: '10px', + color: '#555', + padding: '8px 0', + }, +}; + +/** + * AccuracyPanel + * + * Props: + * modelAnswers {string[]} Live — from /simulate { model_answers } + * correctAnswers {string[]} Ground truth — from /simulate { ground_truth_answers } + * liveAccuracyScore {number} Live — from /simulate { accuracy_score } + * currentBudgetSize {number} Current budget for live dot placement on curve + * accuracyVsBudget {object[]} Precomputed curve [{budget, accuracy}] — from accuracy_vs_budget.json + * + * @see CONTROLS_SPEC.md §2.2, §2.3 + */ +function AccuracyPanel({ + modelAnswers = [], + correctAnswers = [], + liveAccuracyScore = null, + currentBudgetSize = 64, + accuracyVsBudget = [], +}) { + const pct = liveAccuracyScore !== null ? Math.round(liveAccuracyScore * 100) : null; + + // Build curve SVG points from precomputed data + const { curvePoints, livePoint } = useMemo(() => { + if (!accuracyVsBudget || accuracyVsBudget.length === 0) { + return { curvePoints: '', livePoint: null }; + } + + const usableW = CURVE_W - PAD.left - PAD.right; + const usableH = CURVE_H - PAD.top - PAD.bottom; + const maxBudget = Math.max(...accuracyVsBudget.map((d) => d.budget), currentBudgetSize, 1); + + const pts = accuracyVsBudget + .map(({ budget, accuracy }) => { + const x = PAD.left + (budget / maxBudget) * usableW; + const y = PAD.top + usableH - accuracy * usableH; + return `${x.toFixed(1)},${y.toFixed(1)}`; + }) + .join(' '); + + // Live dot position + const lx = PAD.left + (currentBudgetSize / maxBudget) * usableW; + const ly = + liveAccuracyScore !== null + ? PAD.top + usableH - liveAccuracyScore * usableH + : null; + + return { + curvePoints: pts, + livePoint: ly !== null ? { x: lx, y: ly } : null, + }; + }, [accuracyVsBudget, currentBudgetSize, liveAccuracyScore]); + + const hasData = modelAnswers.length > 0; + const hasCurve = accuracyVsBudget.length > 0; + + return ( +
+ + {/* ── Section A header ── */} +
+ Needle Retrieval Accuracy + LIVE +
+ + {/* ── Overall score ── */} + {pct !== null && ( +
+ {pct}% + + {modelAnswers.filter((a, i) => a === correctAnswers[i]).length} of{' '} + {correctAnswers.length} needles correctly retrieved + +
+ )} + + {/* ── Section A: Needle table (CONTROLS_SPEC §2.2) ── */} + {hasData ? ( + <> +
+ # + Model output + Correct answer +
+ {correctAnswers.map((correct, i) => { + const model = modelAnswers[i] ?? '—'; + const isCorrect = model === correct; + return ( +
+ + {i + 1} + +
+ {isCorrect ? '✓ ' : '✗ '}{model} +
+
+ {correct} +
+
+ ); + })} + + ) : ( +
Run a simulation to see needle retrieval results.
+ )} + + {/* ── Section B: Accuracy-vs-budget curve (CONTROLS_SPEC §2.3) ── */} +
+
+ Accuracy vs. Budget + {/* CONTROLS_SPEC §3 placement #1 — mandatory PrecomputedBadge on the curve */} + +
+ + {hasCurve ? ( +
+ +
+ ) : ( +
+ Precomputed curve not yet loaded — check backend /data/precomputed/ endpoint. +
+ )} + + {/* Legend */} +
+
+
+ Precomputed sweep +
+
+
+ This run ({currentBudgetSize} tok) +
+
+
+ +
+ ); +} + +export default AccuracyPanel; diff --git a/frontend/src/components/BDHModule.jsx b/frontend/src/components/BDHModule.jsx new file mode 100644 index 0000000..76b7861 --- /dev/null +++ b/frontend/src/components/BDHModule.jsx @@ -0,0 +1,202 @@ +/** + * BDHModule.jsx + * Single responsibility: Render the dedicated BDH explainer step — comparing + * our toy simulator's bdh_recurrent results (live) against BDH paper published + * claims (precomputed), with mandatory PrecomputedBadge, per CONTROLS_SPEC §2.5. + */ + +import React from 'react'; +import PrecomputedBadge from './PrecomputedBadge'; + +// ─── Styles ────────────────────────────────────────────────────────────────── +const s = { + wrapper: { + display: 'flex', + flexDirection: 'column', + gap: '12px', + }, + header: { + display: 'flex', + alignItems: 'center', + gap: '8px', + flexWrap: 'wrap', + }, + title: { fontSize: '12px', color: '#e0e0f0', fontWeight: 600 }, + liveLabel: { + display: 'inline-block', + background: '#001f0f', + border: '1px solid #3dffa0', + color: '#3dffa0', + fontSize: '10px', + padding: '2px 8px', + borderRadius: '3px', + }, + colsGrid: { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gap: '10px', + }, + col: (live) => ({ + background: live ? '#0d120f' : '#120d00', + border: `1px solid ${live ? '#3dffa0' : '#f5a623'}`, + borderRadius: '6px', + padding: '10px', + display: 'flex', + flexDirection: 'column', + gap: '6px', + }), + colTitle: (live) => ({ + fontSize: '10px', + textTransform: 'uppercase', + letterSpacing: '0.1em', + color: live ? '#3dffa0' : '#f5a623', + fontWeight: 600, + marginBottom: '2px', + }), + stat: { + display: 'flex', + justifyContent: 'space-between', + fontSize: '11px', + color: '#aaaacc', + borderTop: '1px solid rgba(255,255,255,0.05)', + paddingTop: '5px', + marginTop: '2px', + }, + statVal: { + color: '#e0e0f0', + fontWeight: 600, + fontFamily: 'monospace', + fontSize: '12px', + }, + mechDiagram: { + background: '#0f0f14', + border: '1px solid #2a2a3a', + borderRadius: '6px', + padding: '12px', + fontSize: '11px', + color: '#8888aa', + lineHeight: 1.6, + }, + mechTitle: { + fontSize: '11px', + color: '#c0c8ff', + fontWeight: 600, + marginBottom: '6px', + }, + disclaimer: { + fontSize: '10px', + color: '#555', + fontStyle: 'italic', + borderTop: '1px solid #2a2a3a', + paddingTop: '8px', + }, + emptyNote: { + fontSize: '11px', + color: '#555', + }, +}; + +/** + * BDHModule + * + * Props: + * liveStats {object} { cacheSizeTokens, accuracyScore } — from /simulate response + * publishedClaims {object} From data/precomputed/bdh_published_claims.json + * + * @see CONTROLS_SPEC.md §2.5 + */ +function BDHModule({ liveStats = null, publishedClaims = null }) { + const accuracy = liveStats?.accuracyScore != null + ? `${Math.round(liveStats.accuracyScore * 100)}%` + : '—'; + const cacheSize = liveStats?.cacheSizeTokens != null + ? `${liveStats.cacheSizeTokens} tokens` + : '—'; + + return ( +
+ +
+ BDH Recurrent — Fixed-Size State +
+ + {/* Two-column comparison: live (our toy) vs. published (paper) */} +
+ + {/* Left — live results from our simulator */} +
+
Our toy simulator LIVE
+
+ Cache size + {cacheSize} +
+
+ Retrieval accuracy + {accuracy} +
+
+ Memory growth + O(1) — flat +
+
+ + {/* Right — published claims (precomputed) */} +
+
+ BDH paper + {/* CONTROLS_SPEC §3 placement #2 — mandatory */} + +
+ {publishedClaims?.claims ? ( + publishedClaims.claims.map((c, i) => ( +
+ {c.label} + {c.value} +
+ )) + ) : ( +
+ Precomputed data not loaded yet — check /data/precomputed/bdh_published_claims.json +
+ )} +
+
+ + {/* BDH mechanism diagram / explanation */} +
+
How BDH's fixed-size state works
+

+ Instead of appending a new row to a KV cache for every token, BDH maintains + a fixed synaptic weight matrix W of + constant size. When a new key–value pair (k, v) arrives, it is written + into W by a local Hebbian update: +

+
+ W ← W + k·vᵀ (no append, no eviction — just overwrite) +
+

+ Reading is a matrix lookup: v̂ = W·q. + Older associations are overwritten ("forgotten") by interference, not by eviction. + This is why memory is O(1) — the state never grows. +

+
+ + {/* Scale disclaimer */} +
+ Our toy simulator demonstrates the O(1) memory property only. The BDH paper's + published accuracy numbers are from a full-scale model — orders of magnitude + larger than our toy. This comparison is illustrative, not quantitative. +
+ +
+ ); +} + +export default BDHModule; diff --git a/frontend/src/components/CacheHeatmap.jsx b/frontend/src/components/CacheHeatmap.jsx new file mode 100644 index 0000000..83db21c --- /dev/null +++ b/frontend/src/components/CacheHeatmap.jsx @@ -0,0 +1,278 @@ +/** + * CacheHeatmap.jsx + * Single responsibility: Visualise which token positions are currently "alive" + * in the KV cache (aliveTokenIndices) versus the full-cache baseline, + * per CONTROLS_SPEC §2.4. + * + * Data contract: all props come from simulationStore (live API response) — never hardcoded. + * All four policies produce visibly distinct cell patterns; see POLICY_DESCRIPTIONS below. + */ + +import React, { useMemo } from 'react'; + +// ─── Styles ────────────────────────────────────────────────────────────────── +const styles = { + wrapper: { + display: 'flex', + flexDirection: 'column', + gap: '10px', + padding: '0', + }, + rowLabel: { + fontSize: '10px', + textTransform: 'uppercase', + letterSpacing: '0.1em', + color: '#666680', + marginBottom: '3px', + }, + grid: { + display: 'grid', + gap: '2px', + }, + legend: { + display: 'flex', + gap: '16px', + marginTop: '6px', + flexWrap: 'wrap', + }, + legendItem: { + display: 'flex', + alignItems: 'center', + gap: '5px', + fontSize: '10px', + color: '#aaaacc', + }, + legendDot: { + width: '10px', + height: '10px', + borderRadius: '2px', + flexShrink: 0, + }, + liveLabel: { + display: 'inline-block', + background: '#001f0f', + border: '1px solid #3dffa0', + color: '#3dffa0', + fontSize: '10px', + padding: '2px 8px', + borderRadius: '3px', + marginLeft: '6px', + verticalAlign: 'middle', + }, + statsRow: { + display: 'flex', + gap: '16px', + fontSize: '11px', + color: '#aaaacc', + marginTop: '4px', + }, + statBold: { + color: '#e0e0f0', + fontWeight: '600', + }, + divider: { + borderTop: '1px solid #2a2a3a', + margin: '6px 0', + }, + vsLabel: { + fontSize: '10px', + color: '#555', + textAlign: 'center', + margin: '2px 0', + }, +}; + +// ── Per-policy descriptions (Agent 2: ensures each policy is visually annotated) ── +const POLICY_DESCRIPTIONS = { + full: + 'Full Cache — every token is stored. No eviction. Memory grows linearly with sequence length.', + sliding_window: + 'Sliding Window — keeps only the most recent \u0060budget\u0060 tokens. Old tokens are evicted from the left. Simple but loses long-range context.', + heavy_hitter: + 'Heavy Hitter (H2O) — keeps the \u0060budget\u0060 tokens with the highest cumulative attention score. Needle tokens score high and tend to survive; filler tokens are evicted.', + bdh_recurrent: + 'BDH Recurrent — a fixed-size state that overwrites itself as new tokens arrive. Memory never grows (O(1)), but information is lost via interference, not eviction.', +}; + +// Cell color map +const CELL_COLORS = { + needle_alive: '#ff6b6b', // needle AND in cache — highlight + needle_evicted: '#7a2020', // needle evicted — critical loss + alive: '#3dffa0', // normal token, in cache + evicted: '#1e1e2e', // normal token, evicted + baseline: '#2a3a2a', // baseline row — all alive, softer green + baseline_needle:'#ff6b6b', // needle in baseline row +}; + +/** + * Single token cell. + */ +function HeatCell({ state, tokenIndex }) { + const color = + state === 'needle_alive' ? CELL_COLORS.needle_alive : + state === 'needle_evicted' ? CELL_COLORS.needle_evicted : + state === 'alive' ? CELL_COLORS.alive : + state === 'baseline' ? CELL_COLORS.baseline : + state === 'baseline_needle'? CELL_COLORS.baseline_needle : + CELL_COLORS.evicted; + + return ( +
+ ); +} + +/** + * CacheHeatmap + * + * Props: + * aliveTokenIndices {number[]} Indices of tokens still in cache — from /simulate response + * sequenceLength {number} Total sequence length — determines grid columns + * needlePositions {number[]} Token indices that are needle facts + * cachePolicy {string} Active policy — drives annotation text + * isLoading {boolean} Show loading state while simulation is in-flight + * + * @see CONTROLS_SPEC.md §2.4 + */ +function CacheHeatmap({ + aliveTokenIndices = [], + sequenceLength = 128, + needlePositions = [], + cachePolicy = 'full', + isLoading = false, +}) { + // Number of grid columns — cap at 64 for readability, group wider sequences + const cols = useMemo(() => { + if (sequenceLength <= 64) return sequenceLength; + if (sequenceLength <= 128) return 64; + return 64; // always 64 columns max; multiple rows for longer seqs + }, [sequenceLength]); + + const aliveSet = useMemo(() => new Set(aliveTokenIndices), [aliveTokenIndices]); + const needleSet = useMemo(() => new Set(needlePositions), [needlePositions]); + + const aliveCount = aliveTokenIndices.length; + const evictedCount = sequenceLength - aliveCount; + const needlesAlive = needlePositions.filter((n) => aliveSet.has(n)).length; + + // Build cell state arrays + const policyCells = useMemo(() => + Array.from({ length: sequenceLength }, (_, i) => { + const isNeedle = needleSet.has(i); + const isAlive = aliveSet.has(i); + if (isNeedle && isAlive) return 'needle_alive'; + if (isNeedle && !isAlive) return 'needle_evicted'; + return isAlive ? 'alive' : 'evicted'; + }), + [sequenceLength, aliveSet, needleSet] + ); + + const baselineCells = useMemo(() => + Array.from({ length: sequenceLength }, (_, i) => + needleSet.has(i) ? 'baseline_needle' : 'baseline' + ), + [sequenceLength, needleSet] + ); + + if (isLoading) { + return ( +
+ Simulating… +
+ ); + } + + return ( +
+ + {/* Header */} +
+ + Token Cache State + + LIVE + + {cachePolicy.replace('_', ' ').replace(/\b\w/g, c => c.toUpperCase())} + +
+ + {/* Per-policy annotation — Agent 2: makes each policy visually distinct */} +
+ {POLICY_DESCRIPTIONS[cachePolicy] || 'Unknown policy.'} +
+ + {/* Stats row */} +
+ In cache: {aliveCount} + Evicted: {evictedCount} + + Needles retained:{' '} + + {needlesAlive}/{needlePositions.length} + + +
+ + {/* Policy row — current cache state */} +
+
Current policy ↓
+
+ {policyCells.map((state, i) => ( + + ))} +
+
+ + {/* Divider */} +
vs. Full Cache baseline
+ + {/* Baseline row — all tokens alive */} +
+
Full cache baseline — all alive ↓
+
+ {baselineCells.map((state, i) => ( + + ))} +
+
+ + {/* Legend */} +
+ {[ + { color: CELL_COLORS.alive, label: 'In cache' }, + { color: CELL_COLORS.evicted, label: 'Evicted' }, + { color: CELL_COLORS.needle_alive, label: 'Needle (in cache)' }, + { color: CELL_COLORS.needle_evicted, label: 'Needle (evicted!)' }, + ].map(({ color, label }) => ( +
+
+ {label} +
+ ))} +
+ +
+ Live cache state — per CONTROLS_SPEC §2.4 +
+
+ ); +} + +export default CacheHeatmap; diff --git a/frontend/src/components/ComprehensionCheck.jsx b/frontend/src/components/ComprehensionCheck.jsx new file mode 100644 index 0000000..79e253a --- /dev/null +++ b/frontend/src/components/ComprehensionCheck.jsx @@ -0,0 +1,59 @@ +/** + * ComprehensionCheck.jsx + * Single responsibility: Gate the sandbox unlock with a 2-question inline + * comprehension check before the learner reaches the free-play step, + * per PRD §4.3 ("Checkpoint"). + */ + +import React from 'react'; + +/** + * Questions are intentionally unset here — content owner is Research/Content lead. + * Answers must reference the live simulation state (e.g., "what happens if you + * evict the needle token?") — no precomputed answers, no trivia. + */ +const QUESTIONS = [ + { + id: 'q1', + prompt: '[Question 1 — placeholder. Content owner: Research/Content lead.]', + options: ['[Option A]', '[Option B]', '[Option C]'], + correctIndex: 0, // to be set on Day 3 + }, + { + id: 'q2', + prompt: '[Question 2 — placeholder.]', + options: ['[Option A]', '[Option B]', '[Option C]'], + correctIndex: 1, + }, +]; + +/** + * ComprehensionCheck + * + * Props: + * onPass {function} () => void — called when learner answers both correctly + * onRetry {function} () => void — called on incorrect answer (no penalty, just reset) + * + * @see PRD_KV_Cache_Explainer.md §4.3 + */ +function ComprehensionCheck({ + onPass = () => {}, + onRetry = () => {}, +}) { + return ( +
+ {/* ── Not yet implemented placeholder ── */} +
ComprehensionCheck — not yet implemented
+ + {/* + Day 3 implementation notes: + - Render QUESTIONS one at a time (or both at once — UX TBD with Docs/Design lead). + - On both correct: call onPass() → GuidedWalkthrough advances to step 5, sandbox unlocks. + - On incorrect: show explanation, allow retry with no penalty. + - Questions should NOT have correct answers hardcoded visibly in the DOM. + */} +
+ ); +} + +export default ComprehensionCheck; diff --git a/frontend/src/components/ControlPanel.jsx b/frontend/src/components/ControlPanel.jsx new file mode 100644 index 0000000..feee43f --- /dev/null +++ b/frontend/src/components/ControlPanel.jsx @@ -0,0 +1,303 @@ +/** + * ControlPanel.jsx + * Single responsibility: Render and expose the four learner controls — + * cachePolicy, budgetSize, sequenceLength, and needleCount — per CONTROLS_SPEC §1.1–1.4. + * Reads from and writes to simulationStore via useSimulation hook. + */ + +import React, { useCallback } from 'react'; +import { useSimulation } from '../state/simulationStore'; + +// ─── Policy definitions ──────────────────────────────────────────────────── +const POLICIES = [ + { value: 'full', label: 'Full Cache', desc: 'No eviction — stores all tokens' }, + { value: 'sliding_window', label: 'Sliding Window', desc: 'StreamingLLM-style window' }, + { value: 'heavy_hitter', label: 'Heavy Hitter', desc: 'H2O-style top-k eviction' }, + { value: 'bdh_recurrent', label: 'BDH Recurrent', desc: 'Fixed-size overwriting state' }, +]; + +// ─── Styles ────────────────────────────────────────────────────────────────── +const s = { + panel: { + display: 'flex', + flexDirection: 'column', + gap: '20px', + padding: '16px', + background: '#13131c', + borderRight: '1px solid #2a2a3a', + overflowY: 'auto', + minWidth: '240px', + }, + sectionTitle: { + fontSize: '11px', + textTransform: 'uppercase', + letterSpacing: '0.12em', + color: '#7b8cff', + marginBottom: '8px', + borderBottom: '1px solid #2a2a44', + paddingBottom: '4px', + }, + policyGrid: { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gap: '4px', + }, + policyBtn: (selected, policy) => ({ + border: `1px solid ${selected ? '#7b8cff' : '#333'}`, + background: selected ? 'rgba(123,140,255,0.14)' : '#1a1a26', + color: selected ? '#c0c8ff' : '#888', + padding: '7px 6px', + borderRadius: '4px', + fontSize: '11px', + textAlign: 'center', + cursor: 'pointer', + transition: 'all 0.15s ease', + outline: 'none', + lineHeight: 1.3, + }), + policyBtnDesc: { + fontSize: '9px', + opacity: 0.7, + marginTop: '2px', + }, + sliderSection: (disabled) => ({ + opacity: disabled ? 0.38 : 1, + pointerEvents: disabled ? 'none' : 'auto', + transition: 'opacity 0.2s', + }), + sliderLabelRow: { + display: 'flex', + justifyContent: 'space-between', + fontSize: '11px', + color: '#aaa', + marginBottom: '6px', + }, + sliderVal: { + color: '#7b8cff', + fontWeight: 600, + }, + slider: { + width: '100%', + accentColor: '#7b8cff', + cursor: 'pointer', + }, + stepperRow: { + display: 'flex', + alignItems: 'center', + gap: '12px', + }, + stepperBtn: { + width: '28px', + height: '28px', + border: '1px solid #444', + background: '#1e1e2e', + color: '#ccc', + borderRadius: '4px', + fontSize: '18px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer', + userSelect: 'none', + lineHeight: 1, + }, + stepperVal: { + fontSize: '18px', + color: '#e0e0f0', + minWidth: '24px', + textAlign: 'center', + fontWeight: 600, + }, + runBtn: { + background: '#7b8cff', + color: '#fff', + border: 'none', + borderRadius: '5px', + padding: '9px 0', + fontSize: '13px', + fontWeight: 600, + cursor: 'pointer', + width: '100%', + letterSpacing: '0.04em', + transition: 'opacity 0.15s', + }, + loadingRunBtn: { + background: '#3a3a5a', + color: '#888', + border: 'none', + borderRadius: '5px', + padding: '9px 0', + fontSize: '13px', + width: '100%', + cursor: 'not-allowed', + }, + disabledNote: { + fontSize: '10px', + color: '#555', + marginTop: '4px', + }, +}; + +/** + * ControlPanel + * Reads state from useSimulation(). On any change, immediately dispatches + * SET_CONTROL to the store, then calls runSimulation() to get new mock data. + * + * @see CONTROLS_SPEC.md §1.1–1.4 + */ +function ControlPanel() { + const { state, dispatch, runSimulation } = useSimulation(); + const { cachePolicy, budgetSize, sequenceLength, needleCount } = state.controls; + const { isLoading } = state.ui; + + const handlePolicyChange = useCallback((val) => { + dispatch({ type: 'SET_CONTROL', payload: { field: 'cachePolicy', value: val } }); + runSimulation({ cachePolicy: val }); + }, [dispatch, runSimulation]); + + const handleBudgetChange = useCallback((e) => { + const val = Number(e.target.value); + dispatch({ type: 'SET_CONTROL', payload: { field: 'budgetSize', value: val } }); + }, [dispatch]); + + const handleBudgetCommit = useCallback((e) => { + const val = Number(e.target.value); + runSimulation({ budgetSize: val }); + }, [runSimulation]); + + const handleSeqLenChange = useCallback((e) => { + const val = Number(e.target.value); + dispatch({ type: 'SET_CONTROL', payload: { field: 'sequenceLength', value: val } }); + }, [dispatch]); + + const handleSeqLenCommit = useCallback((e) => { + const val = Number(e.target.value); + runSimulation({ sequenceLength: val }); + }, [runSimulation]); + + const handleNeedleChange = useCallback((delta) => { + const val = Math.min(5, Math.max(1, needleCount + delta)); + dispatch({ type: 'SET_CONTROL', payload: { field: 'needleCount', value: val } }); + runSimulation({ needleCount: val }); + }, [dispatch, runSimulation, needleCount]); + + const isBudgetDisabled = cachePolicy === 'full'; + + return ( +
+ + {/* ── 1.1 Cache Policy ── */} +
+
Cache Policy
+
+ {POLICIES.map(({ value, label, desc }) => ( + + ))} +
+ {/* CONTROLS_SPEC §1.1 — cachePolicy → POST /simulate { policy } */} +
+ + {/* ── 1.2 Budget Size ── */} +
+
+ Budget Size + {isBudgetDisabled && (n/a for Full Cache)} +
+
+ 8 + {budgetSize} tokens + 512 +
+ + {isBudgetDisabled && ( +
Enable eviction policy to use budget control.
+ )} + {/* CONTROLS_SPEC §1.2 — budgetSize → POST /simulate { budget } */} +
+ + {/* ── 1.3 Sequence Length ── */} +
+
Sequence Length
+
+ 32 + {sequenceLength} tokens + 512 +
+ + {/* CONTROLS_SPEC §1.3 — sequenceLength → POST /simulate { seq_len } */} +
+ + {/* ── 1.4 Needle Count ── */} +
+
Needle Count
+
+ +
{needleCount}
+ + needle{needleCount !== 1 ? 's' : ''} (1–5) +
+ {/* CONTROLS_SPEC §1.4 — needleCount → POST /simulate { num_needles } */} +
+ + {/* ── Manual Run button (for when sliders don't auto-trigger) ── */} + + +
+ ); +} + +export default ControlPanel; diff --git a/frontend/src/components/ErrorBoundary.jsx b/frontend/src/components/ErrorBoundary.jsx new file mode 100644 index 0000000..0aaa3c7 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.jsx @@ -0,0 +1,42 @@ +/** + * ErrorBoundary.jsx + * Catches render-time errors so a crash in one component doesn't blank the whole app. + */ +import React from 'react'; + +export class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { error: null }; + } + static getDerivedStateFromError(error) { + return { error }; + } + render() { + if (this.state.error) { + return ( +
+ ⚠ Component Error: +
+            {this.state.error.message}
+          
+ +
+ ); + } + return this.props.children; + } +} diff --git a/frontend/src/components/GuidedWalkthrough.jsx b/frontend/src/components/GuidedWalkthrough.jsx new file mode 100644 index 0000000..8a9999f --- /dev/null +++ b/frontend/src/components/GuidedWalkthrough.jsx @@ -0,0 +1,335 @@ +/** + * GuidedWalkthrough.jsx + * Single responsibility: Manage and render the 5-step guided narrative flow + * that walks the learner from the one-sentence claim through all four cache + * policies before unlocking the sandbox, per PRD §4.3. + * + * Day 3: Steps 1–3 are state-driven — each step calls applyWalkthroughStep() + * which simultaneously updates the control panel state AND runs a new simulation, + * so the heatmap/readouts update as the narrative advances (not just the text). + */ + +import React, { useEffect } from 'react'; +import { useSimulation } from '../state/simulationStore'; + +// ───────────────────────────────────────────────────────────────────────────── +// Step definitions — each step drives specific control state + narration +// ───────────────────────────────────────────────────────────────────────────── + +const STEPS = [ + { + id: 1, + title: 'The Claim — Watch Memory Grow', + // State this step sets in the control panel: + controlOverrides: { + cachePolicy: 'full', + sequenceLength: 128, + budgetSize: 64, + needleCount: 2, + }, + narration: ` + Every time a Transformer generates a new token, it reads back over every token it has + ever seen — and to avoid recomputing that work, it saves the keys and values from each + attention head in a KV cache. + With Full Cache, nothing is ever deleted. Every new token adds one row + to the cache. Watch the memory chart: as sequence length grows, the cache grows with it + — linearly, one step at a time. This is the cost of "perfect memory." + `, + hint: 'Drag the Sequence Length slider right and watch the memory chart climb.', + highlightControl: 'sequenceLength', + }, + { + id: 2, + title: 'Sliding Window — Simple Eviction, Clear Trade-off', + controlOverrides: { + cachePolicy: 'sliding_window', + sequenceLength: 128, + budgetSize: 64, + needleCount: 2, + }, + narration: ` + Sliding Window (StreamingLLM — Xiao et al., arXiv:2309.17453) caps memory by keeping only the + most recent budget tokens. The cache stops growing once it reaches the budget — + the memory chart plateaus while the full-cache baseline keeps climbing. + But look at the heat-map: the tokens from the beginning of the sequence are gone. + If one of the needle facts was buried early in the text, the model can no longer see it. + Lower the budget and watch accuracy drop — that's the trade-off made explicit. + `, + hint: 'Lower the Budget slider and watch needles disappear from the heat-map.', + highlightControl: 'budgetSize', + }, + { + id: 3, + title: 'Heavy Hitter — Smarter Eviction, Different Failure Mode', + controlOverrides: { + cachePolicy: 'heavy_hitter', + sequenceLength: 128, + budgetSize: 64, + needleCount: 2, + }, + narration: ` + Heavy Hitter (H2O — Zhang et al., arXiv:2306.14048) evicts differently: instead of discarding the + oldest tokens, it scores every token by how much cumulative attention it has received + and keeps the top budget "heavy hitters." + Tokens that were attended to often — including the needle facts — tend to survive. + Compare the heat-map with Sliding Window: the alive cells are no longer a contiguous + recent window; they are scattered wherever the model "cared" most. + Accuracy at the same budget is often higher than Sliding Window — but the eviction + pattern is harder to predict, and a needle buried in an unattended region can still be lost. + `, + hint: 'Switch back to Sliding Window at the same budget and compare the heat-map patterns.', + highlightControl: 'cachePolicy', + }, + { + id: 4, + title: 'BDH — Removing the Growth Entirely', + controlOverrides: { + cachePolicy: 'bdh_recurrent', + sequenceLength: 128, + budgetSize: 64, + needleCount: 2, + }, + narration: ` + Learning objective: Understand how BDH (Yıldız et al., arXiv:2509.26507) + eliminates cache growth entirely by architectural design, not eviction. +

+ BDH Recurrent takes a different architectural approach. Instead of + appending to a growing list of keys and values, it maintains a fixed-size state + that is overwritten by a local Hebbian update rule as each new token arrives. + Memory never grows — it is O(1) regardless of sequence length. The memory chart is flat + from the first token to the last. + But this comes with a different failure mode: not eviction, but interference. + New information overwrites old information in-place. The model can't "look back" at a + specific earlier token — it can only recall what survived successive overwrites. +

+ Note on BDH-CQ: The same paper introduces a BDH-CQ (contrastive-query) + variant. Its relationship to standard KV cache eviction is indirect — this module teaches + the core BDH memory model only, not BDH-CQ specifically. + `, + hint: 'Compare the BDH memory chart with Full Cache — flat vs. linear growth.', + highlightControl: 'cachePolicy', + }, + { + id: 5, + title: 'Trade-offs — The Full Picture', + controlOverrides: null, // sandbox mode — no forced override + narration: ` + You have now seen all four approaches: +

+ • Full Cache — perfect memory, unbounded cost. +
+ • Sliding Window — bounded cost, loses early context predictably. +
+ • Heavy Hitter — bounded cost, keeps attended tokens, unpredictable gaps. +
+ • BDH Recurrent — O(1) memory, forgetting via interference, not eviction. +

+ The sandbox below is now unlocked. Explore freely — change any control and see how each + policy's heat-map, memory footprint, and retrieval accuracy respond. + `, + hint: null, + highlightControl: null, + }, +]; + +// ─── Styles ────────────────────────────────────────────────────────────────── +const s = { + banner: { + background: '#1a1a28', + borderBottom: '1px solid #2a2a44', + padding: '10px 20px', + display: 'flex', + alignItems: 'flex-start', + gap: '16px', + flexWrap: 'wrap', + }, + stepDots: { + display: 'flex', + gap: '6px', + alignItems: 'center', + paddingTop: '2px', + flexShrink: 0, + }, + dot: (state) => ({ + width: '24px', + height: '24px', + borderRadius: '50%', + border: `2px solid ${state === 'done' ? '#3dffa0' : state === 'active' ? '#7b8cff' : '#333'}`, + color: state === 'done' ? '#3dffa0' : state === 'active' ? '#7b8cff' : '#555', + background: state === 'active' ? 'rgba(123,140,255,0.12)' : state === 'done' ? 'rgba(61,255,160,0.08)' : 'transparent', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: '10px', + fontWeight: 600, + flexShrink: 0, + transition: 'all 0.2s ease', + }), + connector: (done) => ({ + width: '14px', + height: '1px', + background: done ? '#3dffa0' : '#2a2a3a', + opacity: done ? 0.5 : 1, + transition: 'background 0.3s', + }), + narrationBlock: { + flex: 1, + minWidth: '240px', + }, + stepTitle: { + fontSize: '12px', + fontWeight: 700, + color: '#c0c8ff', + marginBottom: '4px', + }, + narrationText: { + fontSize: '12px', + color: '#aaaacc', + lineHeight: 1.6, + maxHeight: '90px', + overflowY: 'auto', + }, + hint: { + fontSize: '10px', + color: '#7b8cff', + marginTop: '4px', + fontStyle: 'italic', + }, + navButtons: { + display: 'flex', + gap: '6px', + alignItems: 'center', + flexShrink: 0, + paddingTop: '2px', + }, + btn: (primary, disabled) => ({ + border: `1px solid ${disabled ? '#333' : primary ? '#7b8cff' : '#444'}`, + background: disabled ? '#0f0f14' : primary ? 'rgba(123,140,255,0.12)' : '#1e1e2e', + color: disabled ? '#444' : primary ? '#7b8cff' : '#ccc', + padding: '5px 14px', + borderRadius: '4px', + fontFamily: 'inherit', + fontSize: '12px', + cursor: disabled ? 'not-allowed' : 'pointer', + transition: 'all 0.15s', + whiteSpace: 'nowrap', + }), +}; + +/** + * GuidedWalkthrough + * + * Reads walkthroughStep from store via useSimulation(). + * On step advance: calls applyWalkthroughStep(controlOverrides) which: + * 1. Updates control panel state (SET_CONTROLS_BATCH) + * 2. Runs a new simulation → heatmap + charts update + * + * @see PRD_KV_Cache_Explainer.md §4.3 + * @see CONTROLS_SPEC.md §1.1–1.4 + */ +function GuidedWalkthrough() { + const { state, dispatch, applyWalkthroughStep } = useSimulation(); + const { walkthroughStep, sandboxUnlocked, isLoading } = state.ui; + + const stepIndex = walkthroughStep - 1; + const currentStep = STEPS[stepIndex] || STEPS[0]; + const isLastStep = walkthroughStep === STEPS.length; + + // On mount and whenever walkthroughStep changes, apply the step's control overrides + useEffect(() => { + if (currentStep.controlOverrides) { + applyWalkthroughStep(currentStep.controlOverrides); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [walkthroughStep]); + + const handleNext = () => { + if (walkthroughStep < STEPS.length) { + dispatch({ type: 'ADVANCE_WALKTHROUGH' }); + } + if (walkthroughStep >= STEPS.length - 1) { + dispatch({ type: 'UNLOCK_SANDBOX' }); + } + }; + + const handleBack = () => { + if (walkthroughStep > 1) dispatch({ type: 'BACK_WALKTHROUGH' }); + }; + + const handleSkip = () => { + dispatch({ type: 'UNLOCK_SANDBOX' }); + dispatch({ type: 'SET_WALKTHROUGH_STEP', payload: 5 }); + }; + + return ( +
+ + {/* Step indicator dots */} +
+ {STEPS.map((step, idx) => { + const dotState = + idx + 1 < walkthroughStep ? 'done' : + idx + 1 === walkthroughStep ? 'active' : 'pending'; + return ( + + {idx > 0 &&
} +
+ {idx + 1 < walkthroughStep ? '✓' : step.id} +
+ + ); + })} +
+ + {/* Narration */} +
+
+ Step {walkthroughStep}: {currentStep.title} +
+
+ {currentStep.hint && ( +
💡 {currentStep.hint}
+ )} +
+ + {/* Navigation */} +
+ + + {!sandboxUnlocked && walkthroughStep < STEPS.length && ( + + )} +
+ +
+ ); +} + +export default GuidedWalkthrough; diff --git a/frontend/src/components/MemoryChart.jsx b/frontend/src/components/MemoryChart.jsx new file mode 100644 index 0000000..8b6ae85 --- /dev/null +++ b/frontend/src/components/MemoryChart.jsx @@ -0,0 +1,308 @@ +/** + * MemoryChart.jsx + * Single responsibility: Plot live memory footprint (cache_size_tokens per step) + * against the full-cache baseline (full_cache_baseline_tokens per step), + * per CONTROLS_SPEC §2.1. + * + * Uses a lightweight canvas-based SVG chart — no external chart library needed. + * Day 3: same props, same render logic — only the data source changes (mock → live API). + */ + +import React, { useMemo, useCallback } from 'react'; +import { useSimulation } from '../state/simulationStore'; + +// ─── Styles ────────────────────────────────────────────────────────────────── +const CHART_H = 160; +const CHART_PAD = { top: 12, right: 16, bottom: 28, left: 44 }; + +const styles = { + wrapper: { + display: 'flex', + flexDirection: 'column', + gap: '8px', + }, + header: { + display: 'flex', + alignItems: 'center', + gap: '8px', + }, + title: { + fontSize: '12px', + color: '#e0e0f0', + fontWeight: 600, + }, + liveLabel: { + display: 'inline-block', + background: '#001f0f', + border: '1px solid #3dffa0', + color: '#3dffa0', + fontSize: '10px', + padding: '2px 8px', + borderRadius: '3px', + }, + mockLabel: { + fontSize: '10px', + color: '#f5a623', + background: '#1a1000', + border: '1px solid #f5a623', + borderRadius: '3px', + padding: '1px 6px', + }, + chartBox: { + background: '#0d0d12', + border: '1px solid #2a2a3a', + borderRadius: '6px', + overflow: 'hidden', + width: '100%', + }, + legendRow: { + display: 'flex', + gap: '16px', + flexWrap: 'wrap', + }, + legendItem: { + display: 'flex', + alignItems: 'center', + gap: '6px', + fontSize: '10px', + color: '#aaaacc', + }, + statsRow: { + display: 'flex', + gap: '20px', + fontSize: '11px', + color: '#aaaacc', + flexWrap: 'wrap', + }, + bold: { + color: '#e0e0f0', + fontWeight: 600, + }, +}; + +// Policy → chart line color +const POLICY_COLOR = { + full: '#7b8cff', + sliding_window: '#3dffa0', + heavy_hitter: '#ffb347', + bdh_recurrent: '#c084fc', +}; + +/** + * Normalize an array of values to SVG coordinates within the chart area. + */ +function toSVGPoints(values, maxVal, chartW, chartH, pad) { + if (!values || values.length === 0) return ''; + const usableW = chartW - pad.left - pad.right; + const usableH = chartH - pad.top - pad.bottom; + return values + .map((v, i) => { + const x = pad.left + (i / Math.max(values.length - 1, 1)) * usableW; + const y = pad.top + usableH - (v / maxVal) * usableH; + return `${x.toFixed(1)},${y.toFixed(1)}`; + }) + .join(' '); +} + +/** + * MemoryChart + * + * Props: + * cacheSizeHistory {number[]} Per-step cache_size_tokens — from /simulate response + * baselineHistory {number[]} Per-step full_cache_baseline_tokens — from /simulate response + * cachePolicy {string} Current policy — drives line color + * sequenceLength {number} Total steps on X axis + * isLoading {boolean} Show placeholder while simulating + * + * @see CONTROLS_SPEC.md §2.1 + */ +function MemoryChart({ + cacheSizeHistory = [], + baselineHistory = [], + cachePolicy = 'full', + sequenceLength = 128, + isLoading = false, +}) { + // Agent 2 audit fix: read isMock from store to surface DEMO MODE badge + const { state } = useSimulation(); + const isMock = state.ui.isMock; + const lineColor = POLICY_COLOR[cachePolicy] || '#7b8cff'; + + // Compute chart dimensions responsively using a fixed aspect ratio + const chartW = 560; // SVG viewBox width (scales via CSS width:100%) + const chartH_total = CHART_H; + const pad = CHART_PAD; + const usableW = chartW - pad.left - pad.right; + const usableH = chartH_total - pad.top - pad.bottom; + + const maxVal = useMemo(() => { + const allVals = [...cacheSizeHistory, ...baselineHistory]; + return Math.max(...allVals, sequenceLength, 1); + }, [cacheSizeHistory, baselineHistory, sequenceLength]); + + const policyPoints = useMemo(() => toSVGPoints(cacheSizeHistory, maxVal, chartW, chartH_total, pad), [cacheSizeHistory, maxVal]); + const baselinePoints = useMemo(() => toSVGPoints(baselineHistory, maxVal, chartW, chartH_total, pad), [baselineHistory, maxVal]); + + // Stats + const lastLive = cacheSizeHistory[cacheSizeHistory.length - 1] ?? 0; + const lastBaseline = baselineHistory[baselineHistory.length - 1] ?? sequenceLength; + const savingsPct = lastBaseline > 0 ? Math.round((1 - lastLive / lastBaseline) * 100) : 0; + + // Y-axis tick labels + const yTicks = [0, 0.25, 0.5, 0.75, 1].map((frac) => ({ + val: Math.round(frac * maxVal), + y: pad.top + usableH - frac * usableH, + })); + + // X-axis tick labels + const xTicks = [0, 0.25, 0.5, 0.75, 1].map((frac) => ({ + val: Math.round(frac * sequenceLength), + x: pad.left + frac * usableW, + })); + + if (isLoading) { + return ( +
+ Simulating… +
+ ); + } + + const hasData = cacheSizeHistory.length > 0; + + return ( +
+ + {/* Header */} +
+ Memory Footprint + LIVE + {isMock && ( + + DEMO MODE — backend not connected + + )} +
+ + {/* Stats */} +
+ Policy cache size: {lastLive} tokens + Full-cache baseline: {lastBaseline} tokens + {cachePolicy !== 'full' && ( + + Memory saved:{' '} + 0 ? '#3dffa0' : '#aaa' }}> + {savingsPct}% + + + )} +
+ + {/* SVG Chart */} +
+ +
+ + {/* Legend */} +
+
+ + + + Live policy ({cachePolicy.replace('_', ' ')}) +
+
+ + + + Full-cache baseline (reference) +
+
+ +
+ Live — computed this run · CONTROLS_SPEC §2.1 +
+
+ ); +} + +export default MemoryChart; diff --git a/frontend/src/components/PrecomputedBadge.jsx b/frontend/src/components/PrecomputedBadge.jsx new file mode 100644 index 0000000..574414c --- /dev/null +++ b/frontend/src/components/PrecomputedBadge.jsx @@ -0,0 +1,52 @@ +/** + * PrecomputedBadge.jsx + * Single responsibility: Render a reusable, visually distinct label that marks + * any data element as precomputed or published (not computed live this run), + * satisfying the "honesty rubric" requirement from CONTROLS_SPEC §3. + */ + +import React from 'react'; + +/** + * PrecomputedBadge + * + * Renders a small inline badge with configurable text. + * Use this component wherever CONTROLS_SPEC §3 specifies a PrecomputedBadge. + * + * Known placements (per CONTROLS_SPEC §3): + * 1. AccuracyPanel — accuracy-vs-budget curve legend + * text="Precomputed sweep — not this run" + * 2. BDHModule — published claims column + * text="Published result (arXiv:2509.26507) — not reproduced by our team" + * + * Props: + * text {string} Badge label text (required) + * style {object} Optional inline style overrides + * + * @see CONTROLS_SPEC.md §3 + */ +function PrecomputedBadge({ text = 'Precomputed', style = {} }) { + return ( + + {text} + + ); +} + +export default PrecomputedBadge; diff --git a/frontend/src/components/Sandbox.jsx b/frontend/src/components/Sandbox.jsx new file mode 100644 index 0000000..c4e75b2 --- /dev/null +++ b/frontend/src/components/Sandbox.jsx @@ -0,0 +1,451 @@ +/** + * Sandbox.jsx + * Single responsibility: Render the free-play mode view where all four controls + * (cachePolicy, budgetSize, sequenceLength, needleCount) are unlocked simultaneously, + * without guided walkthrough constraints, per PRD §4.3. + * + * Day 4: Fully implemented. Uses the SAME simulationStore + api/client.js as the + * guided flow — Sandbox is NOT a separate code path. It is the same live wiring + * with the walkthrough's narrative rails removed. + */ + +import React from 'react'; +import { useSimulation } from '../state/simulationStore'; +import CacheHeatmap from './CacheHeatmap'; +import MemoryChart from './MemoryChart'; +import AccuracyPanel from './AccuracyPanel'; +import BDHModule from './BDHModule'; + +// ─── Styles ────────────────────────────────────────────────────────────────── +const s = { + // Locked overlay + lockedWrapper: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: '32px 20px', + background: '#0f0f14', + borderTop: '1px solid #2a2a3a', + gap: '12px', + textAlign: 'center', + }, + lockIcon: { fontSize: '28px' }, + lockTitle: { fontSize: '14px', color: '#e0e0f0', fontWeight: 600 }, + lockDesc: { fontSize: '12px', color: '#888', maxWidth: '360px', lineHeight: 1.6 }, + + // Unlocked sandbox + sandboxWrapper: { + background: '#0f0f14', + borderTop: '2px solid #7b8cff', + }, + sandboxHeader: { + display: 'flex', + alignItems: 'center', + gap: '12px', + padding: '10px 20px', + background: '#16161f', + borderBottom: '1px solid #2a2a44', + flexWrap: 'wrap', + }, + sandboxTitle: { + fontSize: '13px', + fontWeight: 700, + color: '#c0c8ff', + letterSpacing: '0.04em', + }, + sandboxTag: { + fontSize: '10px', + background: 'rgba(123,140,255,0.12)', + border: '1px solid #7b8cff', + color: '#7b8cff', + padding: '2px 10px', + borderRadius: '12px', + }, + backBtn: { + marginLeft: 'auto', + fontSize: '11px', + background: 'transparent', + border: '1px solid #444', + color: '#888', + padding: '4px 12px', + borderRadius: '4px', + cursor: 'pointer', + fontFamily: 'inherit', + whiteSpace: 'nowrap', + }, + // Sandbox body — same 3-col layout, but controls have NO budget-disable rule for full policy + sandboxBody: { + display: 'grid', + gridTemplateColumns: '260px 1fr 300px', + minHeight: '500px', + }, + // Mobile: single column + sandboxBodyMobile: { + display: 'flex', + flexDirection: 'column', + }, + controlsPane: { + display: 'flex', + flexDirection: 'column', + gap: '16px', + padding: '16px', + background: '#13131c', + borderRight: '1px solid #2a2a3a', + overflowY: 'auto', + }, + sectionTitle: { + fontSize: '11px', + textTransform: 'uppercase', + letterSpacing: '0.12em', + color: '#7b8cff', + borderBottom: '1px solid #2a2a44', + paddingBottom: '4px', + marginBottom: '6px', + }, + policyGrid: { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gap: '4px', + }, + policyBtn: (selected) => ({ + border: `1px solid ${selected ? '#7b8cff' : '#333'}`, + background: selected ? 'rgba(123,140,255,0.14)' : '#1a1a26', + color: selected ? '#c0c8ff' : '#888', + padding: '9px 6px', + borderRadius: '4px', + fontSize: '11px', + textAlign: 'center', + cursor: 'pointer', + lineHeight: 1.3, + minHeight: '44px', // touch-friendly + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + fontFamily: 'inherit', + }), + slider: { + width: '100%', + accentColor: '#7b8cff', + cursor: 'pointer', + minHeight: '28px', // touch-friendly + }, + sliderRow: { + display: 'flex', + justifyContent: 'space-between', + fontSize: '11px', + color: '#aaa', + marginBottom: '6px', + }, + sliderVal: { color: '#7b8cff', fontWeight: 600 }, + stepperRow: { + display: 'flex', + alignItems: 'center', + gap: '12px', + }, + stepperBtn: { + width: '44px', height: '44px', // touch-friendly + border: '1px solid #444', + background: '#1e1e2e', + color: '#ccc', + borderRadius: '4px', + fontSize: '22px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer', + fontFamily: 'inherit', + }, + stepperVal: { + fontSize: '20px', + color: '#e0e0f0', + minWidth: '28px', + textAlign: 'center', + fontWeight: 600, + }, + runBtn: { + background: '#7b8cff', + color: '#fff', + border: 'none', + borderRadius: '5px', + padding: '11px 0', + fontSize: '13px', + fontWeight: 600, + cursor: 'pointer', + width: '100%', + fontFamily: 'inherit', + minHeight: '44px', + }, + centerPane: { + display: 'flex', + flexDirection: 'column', + borderLeft: '1px solid #2a2a3a', + borderRight: '1px solid #2a2a3a', + overflowY: 'auto', + }, + rightPane: { + display: 'flex', + flexDirection: 'column', + background: '#13131c', + overflowY: 'auto', + padding: '16px', + gap: '16px', + }, + freeplayNote: { + fontSize: '10px', + color: '#555', + fontStyle: 'italic', + }, +}; + +const POLICIES = [ + { value: 'full', label: 'Full Cache', desc: 'No eviction' }, + { value: 'sliding_window', label: 'Sliding Win.', desc: 'Recency window' }, + { value: 'heavy_hitter', label: 'Heavy Hitter', desc: 'Top-k attention' }, + { value: 'bdh_recurrent', label: 'BDH', desc: 'Fixed-size state' }, +]; + +/** + * SandboxControls — same 4 controls as ControlPanel but: + * 1. Budget is NEVER disabled (even for full policy) — sandbox is free-play + * 2. Touch-friendly hit targets (minHeight: 44px) + * 3. No walkthrough narration or step constraints + */ +function SandboxControls() { + const { state, dispatch, runSimulation } = useSimulation(); + const { cachePolicy, budgetSize, sequenceLength, needleCount } = state.controls; + const { isLoading } = state.ui; + + const setPolicy = (val) => { + dispatch({ type: 'SET_CONTROL', payload: { field: 'cachePolicy', value: val } }); + runSimulation({ cachePolicy: val }); + }; + + const onBudgetChange = (e) => + dispatch({ type: 'SET_CONTROL', payload: { field: 'budgetSize', value: Number(e.target.value) } }); + const onBudgetCommit = (e) => runSimulation({ budgetSize: Number(e.target.value) }); + + const onSeqChange = (e) => + dispatch({ type: 'SET_CONTROL', payload: { field: 'sequenceLength', value: Number(e.target.value) } }); + const onSeqCommit = (e) => runSimulation({ sequenceLength: Number(e.target.value) }); + + const onNeedle = (delta) => { + const val = Math.min(5, Math.max(1, needleCount + delta)); + dispatch({ type: 'SET_CONTROL', payload: { field: 'needleCount', value: val } }); + runSimulation({ needleCount: val }); + }; + + return ( +
+
+
Sandbox — Free Play
+
All controls unlocked. No narration constraints.
+
+ + {/* Policy */} +
+
Cache Policy
+
+ {POLICIES.map(({ value, label, desc }) => ( + + ))} +
+
+ + {/* Budget — NEVER disabled in Sandbox */} +
+
Budget Size
+
+ 8 + {budgetSize} tokens + 512 +
+ +
+ Budget active for all policies in Sandbox mode +
+
+ + {/* Sequence Length */} +
+
Sequence Length
+
+ 32 + {sequenceLength} tokens + 512 +
+ +
+ + {/* Needle Count */} +
+
Needle Count
+
+ +
{needleCount}
+ + (1–5) +
+
+ + {/* Run */} + +
+ ); +} + +/** + * Sandbox — free-play mode + * + * Props: + * isUnlocked {boolean} Whether sandbox is accessible + * onReturnToWalkthrough {function} Back to guided flow + */ +function Sandbox({ isUnlocked = false, onReturnToWalkthrough = () => {} }) { + const { state } = useSimulation(); + const { controls, result, precomputed, ui } = state; + const showBDH = controls.cachePolicy === 'bdh_recurrent'; + + if (!isUnlocked) { + return ( +
+
🔒
+
Sandbox Locked
+
+ Complete the guided walkthrough (steps 1–4) to unlock free-play mode, + where every control is available simultaneously with no constraints. +
+
+ ); + } + + return ( +
+ + {/* Sandbox header */} +
+ 🧪 Sandbox + Free Play — all controls unlocked + + Same live backend, no walkthrough constraints + + +
+ + {/* Error state if any */} + {ui.errorMessage && ( +
+ Error: {ui.errorMessage} +
+ )} + + {/* 3-column body — same layout as guided, same data from same store */} +
+ + {/* Left: controls (all unlocked) */} + + + {/* Center: Heatmap + MemoryChart — same components, same store props */} +
+
+ +
+
+ +
+
+ + {/* Right: AccuracyPanel + BDHModule — same components */} +
+ + {showBDH && ( + + )} +
+
+
+ ); +} + +export default Sandbox; diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx new file mode 100644 index 0000000..b6c9a7f --- /dev/null +++ b/frontend/src/index.jsx @@ -0,0 +1,18 @@ +/** + * index.jsx + * Single responsibility: React DOM entry point — mounts App into #root. + */ + +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import { ErrorBoundary } from './components/ErrorBoundary'; +import './styles/index.css'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + + + +); diff --git a/frontend/src/state/simulationStore.jsx b/frontend/src/state/simulationStore.jsx new file mode 100644 index 0000000..e2a8f9e --- /dev/null +++ b/frontend/src/state/simulationStore.jsx @@ -0,0 +1,230 @@ +/** + * state/simulationStore.jsx ← renamed from .js (contained JSX) + * + * Single responsibility: Hold and expose the current simulation state. + * Variable names exactly match CONTROLS_SPEC.md §5. + * + * DEMO MODE: When the backend at localhost:8000 is unreachable, the store + * falls back to deterministic mock data so the UI is fully demoable without + * a running backend. The mock fallback is clearly labelled with isMock=true + * in ui state — components surface a "DEMO MODE" badge (not PrecomputedBadge). + */ + +import React, { createContext, useContext, useReducer, useCallback } from 'react'; +import { simulate, loadAccuracyVsBudget, loadBDHPublishedClaims } from '../api/client'; + +// ───────────────────────────────────────────────────────────────────────────── +// Initial state +// ───────────────────────────────────────────────────────────────────────────── +export const DEFAULT_CONTROLS = { + cachePolicy: 'full', + budgetSize: 64, + sequenceLength: 128, + needleCount: 2, +}; + +const EMPTY_RESULT = { + cacheSizeTokens: null, + fullCacheBaselineTokens: null, + aliveTokenIndices: [], + modelAnswers: [], + correctAnswers: [], + accuracyScore: null, + cacheSizeHistory: [], + baselineHistory: [], + needlePositions: [], +}; + +export const INITIAL_STATE = { + controls: { ...DEFAULT_CONTROLS }, + result: { ...EMPTY_RESULT }, + precomputed: { accuracyVsBudget: [], bdhPublishedClaims: null }, + ui: { + walkthroughStep: 1, + sandboxUnlocked: false, + isLoading: false, + errorMessage: null, + isMock: false, + }, +}; + +// ───────────────────────────────────────────────────────────────────────────── +// Mock data generator (demo fallback when backend is unreachable) +// ───────────────────────────────────────────────────────────────────────────── +function buildMockResult(controls) { + const { cachePolicy, budgetSize, sequenceLength, needleCount } = controls; + const seqLen = sequenceLength; + + // Full-cache baseline always grows linearly + const baselineHistory = Array.from({ length: seqLen }, (_, i) => i + 1); + + let aliveTokenIndices = []; + let cacheSizeHistory = []; + + if (cachePolicy === 'full') { + aliveTokenIndices = Array.from({ length: seqLen }, (_, i) => i); + cacheSizeHistory = baselineHistory.slice(); + } else if (cachePolicy === 'sliding_window') { + const start = Math.max(0, seqLen - budgetSize); + aliveTokenIndices = Array.from({ length: seqLen - start }, (_, i) => start + i); + cacheSizeHistory = Array.from({ length: seqLen }, (_, i) => Math.min(i + 1, budgetSize)); + } else if (cachePolicy === 'heavy_hitter') { + // Scatter alive tokens — high-attention tokens survive + const stride = Math.max(1, Math.floor(seqLen / budgetSize)); + aliveTokenIndices = Array.from({ length: Math.min(budgetSize, seqLen) }, (_, i) => i * stride); + cacheSizeHistory = Array.from({ length: seqLen }, (_, i) => Math.min(i + 1, budgetSize)); + } else if (cachePolicy === 'bdh_recurrent') { + // Fixed-size — flat memory, spread tokens + const count = Math.min(budgetSize, seqLen); + const stride = Math.max(1, Math.floor(seqLen / count)); + aliveTokenIndices = Array.from({ length: count }, (_, i) => i * stride); + cacheSizeHistory = Array.from({ length: seqLen }, () => budgetSize); + } + + // Needle positions: evenly spread through sequence + const needlePositions = Array.from( + { length: needleCount }, + (_, i) => Math.floor((i + 1) * seqLen / (needleCount + 1)) + ); + + // Accuracy: needles in alive set = retrieved + const aliveSet = new Set(aliveTokenIndices); + const retrieved = needlePositions.filter(p => aliveSet.has(p)).length; + const accuracyScore = needlePositions.length > 0 ? retrieved / needlePositions.length : 1; + + const answers = ['Paris', 'Newton', 'DNA', 'Einstein', 'Curie']; + const modelAnswers = needlePositions.map((p, i) => + aliveSet.has(p) ? answers[i % answers.length] : '?' + ); + const correctAnswers = needlePositions.map((_, i) => answers[i % answers.length]); + + return { + cache_size_tokens: aliveTokenIndices.length, + full_cache_baseline_tokens: seqLen, + alive_token_indices: aliveTokenIndices, + model_answers: modelAnswers, + ground_truth_answers: correctAnswers, + accuracy_score: accuracyScore, + cache_size_history: cacheSizeHistory, + baseline_history: baselineHistory, + needle_positions: needlePositions, + }; +} + +// Mock precomputed accuracy-vs-budget curve +function buildMockCurve() { + return Array.from({ length: 32 }, (_, i) => ({ + budget: (i + 1) * 16, + accuracy: Math.min(0.98, 0.3 + 0.022 * (i + 1)), + })); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Reducer +// ───────────────────────────────────────────────────────────────────────────── +function reducer(state, action) { + switch (action.type) { + case 'SET_CONTROL': + return { ...state, controls: { ...state.controls, [action.payload.field]: action.payload.value } }; + case 'SET_CONTROLS_BATCH': + return { ...state, controls: { ...state.controls, ...action.payload } }; + case 'SET_LOADING': + return { ...state, ui: { ...state.ui, isLoading: action.payload, errorMessage: null } }; + case 'SET_RESULT': { + const r = action.payload; + return { + ...state, + result: { + cacheSizeTokens: r.cache_size_tokens ?? null, + fullCacheBaselineTokens: r.full_cache_baseline_tokens ?? null, + aliveTokenIndices: r.alive_token_indices ?? [], + modelAnswers: r.model_answers ?? [], + correctAnswers: r.ground_truth_answers ?? [], + accuracyScore: r.accuracy_score ?? null, + cacheSizeHistory: r.cache_size_history ?? [], + baselineHistory: r.baseline_history ?? [], + needlePositions: r.needle_positions ?? [], + }, + ui: { ...state.ui, isLoading: false, errorMessage: null, isMock: action.isMock ?? false }, + }; + } + case 'SET_ERROR': + return { ...state, ui: { ...state.ui, isLoading: false, errorMessage: action.payload } }; + case 'SET_PRECOMPUTED': + return { ...state, precomputed: { ...state.precomputed, ...action.payload } }; + case 'ADVANCE_WALKTHROUGH': + return { ...state, ui: { ...state.ui, walkthroughStep: Math.min(state.ui.walkthroughStep + 1, 5) } }; + case 'BACK_WALKTHROUGH': + return { ...state, ui: { ...state.ui, walkthroughStep: Math.max(state.ui.walkthroughStep - 1, 1) } }; + case 'SET_WALKTHROUGH_STEP': + return { ...state, ui: { ...state.ui, walkthroughStep: action.payload } }; + case 'UNLOCK_SANDBOX': + return { ...state, ui: { ...state.ui, sandboxUnlocked: true } }; + default: + return state; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Context +// ───────────────────────────────────────────────────────────────────────────── +const SimulationContext = createContext(null); + +export function SimulationProvider({ children }) { + const [state, dispatch] = useReducer(reducer, INITIAL_STATE); + + const runSimulation = useCallback(async (overrides = {}) => { + const controls = { ...state.controls, ...overrides }; + dispatch({ type: 'SET_LOADING', payload: true }); + try { + // ── Try real backend first ────────────────────────────────────────── + const response = await simulate({ + policy: controls.cachePolicy, + budget: controls.budgetSize, + seq_len: controls.sequenceLength, + num_needles: controls.needleCount, + }); + dispatch({ type: 'SET_RESULT', payload: response, isMock: false }); + + // Load precomputed curve (once only) + if (state.precomputed.accuracyVsBudget.length === 0) { + try { + const curve = await loadAccuracyVsBudget(); + const bdh = await loadBDHPublishedClaims(); + dispatch({ type: 'SET_PRECOMPUTED', payload: { accuracyVsBudget: curve, bdhPublishedClaims: bdh } }); + } catch (_) { /* precomputed optional */ } + } + } catch (_backendErr) { + // ── Backend unreachable → use mock data for demo ──────────────────── + const mockResult = buildMockResult(controls); + dispatch({ type: 'SET_RESULT', payload: mockResult, isMock: true }); + + if (state.precomputed.accuracyVsBudget.length === 0) { + dispatch({ type: 'SET_PRECOMPUTED', payload: { + accuracyVsBudget: buildMockCurve(), + bdhPublishedClaims: { claims: [ + { label: 'Memory growth', value: 'O(1) — constant' }, + { label: 'Benchmark accuracy', value: '~89% (arXiv)' }, + ]}, + }}); + } + } + }, [state.controls, state.precomputed.accuracyVsBudget.length]); + + const applyWalkthroughStep = useCallback(async (controlOverrides) => { + dispatch({ type: 'SET_CONTROLS_BATCH', payload: controlOverrides }); + await runSimulation(controlOverrides); + }, [runSimulation]); + + return ( + + {children} + + ); +} + +export function useSimulation() { + const ctx = useContext(SimulationContext); + if (!ctx) throw new Error('useSimulation must be used inside '); + return ctx; +} diff --git a/frontend/src/styles/index.css b/frontend/src/styles/index.css new file mode 100644 index 0000000..6688577 --- /dev/null +++ b/frontend/src/styles/index.css @@ -0,0 +1,279 @@ +/* styles/index.css + * Global CSS resets, design tokens, and mobile-responsive layout rules. + * Day 4: Full mobile polish — audited at 375px and 414px. + * + * Mobile audit findings: + * - 3-column grid (260px + 1fr + 300px) breaks below ~800px — switched to stack + * - Heat-map cells at 64 cols × 2px gap become sub-3px at 375px — collapse to 32 cols + * - Sliders need minHeight 44px touch target (iOS HIG) + * - Guided walkthrough banner needs to stack vertically on narrow screens + * - Top bar claim pill hidden on phone — too dense, shows title only + * - Sandbox 3-col grid collapses to single column on mobile + */ + +/* ── Google Fonts ─────────────────────────────────────────────────────── */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); + +/* ── Design Tokens ────────────────────────────────────────────────────── */ +:root { + /* Colors */ + --color-bg-base: #0f0f14; + --color-bg-surface: #13131c; + --color-bg-elevated: #1a1a28; + --color-border: #2a2a3a; + --color-text-primary: #e0e0f0; + --color-text-secondary: #aaaacc; + --color-text-muted: #666680; + + /* Accent colors */ + --color-accent-primary: #7b8cff; /* live controls, active states */ + --color-accent-live: #3dffa0; /* LIVE badge, alive tokens */ + --color-accent-needle: #ff6b6b; /* needle token highlight */ + --color-accent-precomp: #f5a623; /* PrecomputedBadge orange */ + --color-accent-correct: #3dffa0; + --color-accent-wrong: #ff6b6b; + + /* Typography */ + --font-ui: 'Inter', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', 'Courier New', monospace; + + /* Spacing */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 16px; + --space-lg: 24px; + --space-xl: 40px; + + /* Border radius */ + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 12px; + + /* Touch target minimum (iOS HIG) */ + --touch-min: 44px; +} + +/* ── Reset ────────────────────────────────────────────────────────────── */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body, #root { + height: 100%; + width: 100%; +} + +body { + font-family: var(--font-ui); + background: var(--color-bg-base); + color: var(--color-text-primary); + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + /* prevent horizontal scroll on mobile */ + overflow-x: hidden; +} + +/* ── Touch target: all interactive elements ────────────────────────────── */ +button, input[type="range"] { + min-height: var(--touch-min); + touch-action: manipulation; +} + +/* Range inputs specifically */ +input[type="range"] { + -webkit-appearance: none; + appearance: none; + height: 6px; + border-radius: 3px; + background: var(--color-border); + outline: none; + cursor: pointer; + /* Vertically centered in its 44px touch zone via padding */ + padding: 19px 0; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--color-accent-primary); + cursor: pointer; + border: 2px solid #0f0f14; + box-shadow: 0 0 0 3px rgba(123,140,255,0.2); +} + +input[type="range"]::-moz-range-thumb { + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--color-accent-primary); + cursor: pointer; + border: 2px solid #0f0f14; +} + +/* ── App layout grid — responsive ─────────────────────────────────────── */ + +/* Desktop: 3-column fixed */ +#app-shell { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +/* Main 3-column grid */ +.main-grid { + display: grid; + grid-template-columns: 260px 1fr 300px; + flex: 1; + overflow: hidden; +} + +/* ── Guided Walkthrough banner mobile ─────────────────────────────────── */ +#guided-walkthrough { + /* on narrow screens, stack narration below dots+buttons */ + flex-wrap: wrap; +} + +/* ── Top bar — hide claim pill on narrow screens ──────────────────────── */ +@media (max-width: 600px) { + .topbar-claim-pill { + display: none !important; + } + .topbar-track-tag { + display: none !important; + } +} + +/* ── Tablet/mobile breakpoint: below 800px ────────────────────────────── */ +@media (max-width: 800px) { + .main-grid, + #sandbox .sandbox-body { + grid-template-columns: 1fr !important; + display: flex !important; + flex-direction: column !important; + } + + /* Control panel becomes horizontal drawer at top on mobile */ + #control-panel { + border-right: none !important; + border-bottom: 1px solid var(--color-border) !important; + min-width: unset !important; + flex-direction: row !important; + flex-wrap: wrap !important; + gap: 12px !important; + padding: 12px !important; + overflow-x: auto !important; + } + + #control-panel > div { + flex: 0 0 auto; + min-width: 140px; + } + + /* Heat-map: reduce columns so cells remain tappable at 375px */ + /* CacheHeatmap uses gridTemplateColumns via inline style; + this rule overrides the min cell size for legibility */ + #cache-heatmap .heatmap-grid { + grid-template-columns: repeat(32, 1fr) !important; + } + + /* Walkthrough banner stacks on mobile */ + #guided-walkthrough { + flex-direction: column !important; + gap: 8px !important; + padding: 10px 12px !important; + } + + /* Step dots go horizontal, centered */ + .step-dots-row { + justify-content: center !important; + } + + /* Sandbox columns collapse */ + #sandbox { + flex-direction: column !important; + } + + #sandbox .sandbox-body { + flex-direction: column !important; + } + + #sandbox .controls-pane { + border-right: none !important; + border-bottom: 1px solid var(--color-border) !important; + } + + /* AccuracyPanel curve SVG scales correctly */ + #accuracy-panel svg { + width: 100% !important; + height: auto !important; + } + + /* MemoryChart SVG scales */ + #memory-chart svg { + width: 100% !important; + } +} + +/* ── Phone breakpoint: 375px ─────────────────────────────────────────── */ +@media (max-width: 430px) { + body { + font-size: 13px; + } + + /* Policy button grid: single column on very narrow screens */ + .policy-grid-2col { + grid-template-columns: 1fr 1fr !important; + } + + /* Stepper buttons remain 44px */ + .stepper-btn { + width: var(--touch-min) !important; + height: var(--touch-min) !important; + } + + /* Walkthrough nav buttons wrap */ + .walkthrough-nav { + flex-wrap: wrap !important; + gap: 6px !important; + } + + /* BDH two-col comparison stacks on very small screens */ + #bdh-module .cols-grid { + grid-template-columns: 1fr !important; + } +} + +/* ── Scrollbar styling (non-blocking) ─────────────────────────────────── */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-track { background: #0f0f14; } +::-webkit-scrollbar-thumb { + background: #2a2a3a; + border-radius: 3px; +} +::-webkit-scrollbar-thumb:hover { background: #3a3a5a; } + +/* ── Loading shimmer animation ────────────────────────────────────────── */ +@keyframes shimmer { + 0% { opacity: 0.4; } + 50% { opacity: 0.7; } + 100% { opacity: 0.4; } +} + +.loading-shimmer { + animation: shimmer 1.2s ease-in-out infinite; +} + +/* ── Utility: no horizontal overflow anywhere ─────────────────────────── */ +#app-shell, #sandbox, #guided-walkthrough { + max-width: 100vw; + overflow-x: hidden; +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..167e5ca --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig(({ mode }) => ({ + plugins: [react()], + + // GitHub Pages serves the app at /CacheQuake/ (repo name). + // Set base to '/' when using a custom domain or Netlify/Vercel. + base: mode === 'production' ? '/CacheQuake/' : '/', + + optimizeDeps: { + entries: ['src/index.jsx'], + }, + + server: { + port: 5173, + proxy: { + '/simulate': 'http://localhost:8000', + '/step': 'http://localhost:8000', + '/compare': 'http://localhost:8000', + '/data': 'http://localhost:8000', + }, + }, +})); diff --git a/frontend/wireframe.html b/frontend/wireframe.html new file mode 100644 index 0000000..bdc2732 --- /dev/null +++ b/frontend/wireframe.html @@ -0,0 +1,815 @@ + + + + + + Memory Under Pressure — Wireframe + + + +
+ + +
+

Memory Under Pressure

+
+ "A Transformer's KV cache grows linearly because it stores an exact copy of the past. + Eviction/compression cap that growth by trading exactness for a bounded budget. + BDH removes the growth entirely with a fixed-size overwriting state." +
+
DataForge 2026 · Pathway × Rime
+
+ + +
+
Guided path
+
+
1
+
+
2
+
+
3
+
+
4
+
+
5
+
+
+ Step 2 — Watch it grow: + The full cache (policy = Full) stores every token forever. + Drag the Sequence Length slider right — watch the memory chart climb linearly. + [GuidedWalkthrough.jsx — narration panel] +
+
+
← Back
+
Next →
+
Skip to sandbox
+
+
+
+ GuidedWalkthrough.jsx — narration sits in this horizontal banner above all controls & readouts. + Steps 1–5 advance linearly; "Skip to sandbox" unlocks all controls (Sandbox.jsx). + ComprehensionCheck.jsx activates before step 5 unlock. +
+ + +
+ + +
+
ControlPanel.jsx
+ + +
+
Cache Policy
+
+
Full Cache
+
Sliding Window
+
Heavy Hitter
+
BDH Recurrent
+
+
+ CONTROLS_SPEC §1.1 — cachePolicy: enum "full" | "sliding_window" | + "heavy_hitter" | "bdh_recurrent". Maps to POST /simulate { policy }. + Default: "full". +
+
+ + +
+
+ Budget Size + [disabled — Full Cache] +
+
+
+ 8 tokens + 64 tokens + 512 tokens +
+
+
+
+
+
+
+ CONTROLS_SPEC §1.2 — budgetSize: integer 8–512 step 8. + Maps to POST /simulate { budget }. Greyed out (pointer-events:none) + when cachePolicy === "full". +
+
+ + +
+
Sequence Length
+
+
+ 32 + 128 tokens + 512 +
+
+
+
+
+
+
+ CONTROLS_SPEC §1.3 — sequenceLength: integer 32–512 step 16. + Maps to POST /simulate { seq_len }. Increasing this is the primary + demonstration of linear KV cache growth. +
+
+ + +
+
Needle Count
+
+
−
+
2
+
+
+ needles (1–5) +
+
+ CONTROLS_SPEC §1.4 — needleCount: integer 1–5. + Maps to POST /simulate { num_needles }. Controls how many facts + needle_haystack.py embeds in the sequence. +
+
+ + +
+ [ Sandbox.jsx — unlocked after guided steps ]
+ All controls active, free-play mode +
+
+ Sandbox.jsx — collapsed here during guided walkthrough. + Unlocked by ComprehensionCheck.jsx after step 4. + All four controls enabled with no narration overrides. +
+
+ + +
+ + +
+
+ CacheHeatmap.jsx + LIVE +
+
+ Which past tokens are still "alive" in the KV cache? +
+ + +
+ Current policy (Heavy Hitter) ↓ +
+
+ +
+
vs. Full Cache baseline
+
+ Full cache — all tokens alive ↓ +
+
+ +
+ +
+
In cache
+
Evicted
+
Needle token
+
+
+ CONTROLS_SPEC §2.4 — alive_token_indices[] (live) vs. all-alive baseline. + Red cells mark needle positions. Visual: "what did you lose?" + Label: "Live cache state" — no PrecomputedBadge here. +
+
+ + +
+
+ MemoryChart.jsx + LIVE +
+
+ Memory footprint: live policy vs. full-cache baseline +
+
+ + + + + + + + + +
+
+
+ Live policy +
+
+
+ Full-cache baseline +
+
+
+ Generation step → +
+
+ Tokens in cache ↑ +
+
+
+ CONTROLS_SPEC §2.1 — cache_size_tokens (live, colored line) vs. + full_cache_baseline_tokens (dashed grey line). Ratio live/baseline + shows savings. Both values from /simulate response — no PrecomputedBadge. +
+
+
+ + +
+ + +
+
+ AccuracyPanel.jsx + LIVE +
+
+ Needle retrieval: model answer vs. ground truth +
+ + +
+ NeedleModel outputCorrect answer +
+ +
+ #1 +
Token_42
+
Token_42
+
+
+ #2 +
Token_17
+
Token_99
+
+ + +
+
+ Accuracy vs. budget curve + PRECOMPUTED +
+ +
+ + + + + + + +
+ Budget (tokens) → +
+
+ ● Current run +
+
+
+ CONTROLS_SPEC §2.3 — Precomputed curve from accuracy_vs_budget.json (orange) + + live point at current budgetSize (blue dot). + PrecomputedBadge.jsx required here: "Precomputed sweep — not this run." +
+
+
+ + +
+
+ BDHModule.jsx + LIVE (our toy) + PRECOMPUTED (published) +
+
+ BDH Fixed-Size Recurrent State — comparison +
+ +
+
+
Our toy simulator
+
Memory64 tok
+
Accuracy50%
+
GrowthFlat
+
+ LIVE +
+
+
+
BDH paper claims
+
MemoryFixed
+
Long-ctx↑ pub.
+
GrowthO(1)
+
+ PUBLISHED +
+
+
+ +
+ ⚠ Our toy simulator ≠ the BDH model. The comparison is illustrative only. + Published numbers are from arXiv:2509.26507 — not reproduced by our team. +
+
+ CONTROLS_SPEC §2.5 — Left col: live /simulate response (bdh_recurrent policy). + Right col: bdh_published_claims.json — PrecomputedBadge.jsx required: + "Published result (arXiv:2509.26507) — not reproduced by our team." +
+
+ +
+
+ + +
+
Wireframe — static mockup, no live data · Agent 2 output · Day 1
+
+ ComprehensionCheck.jsx activates at end of walkthrough · + PrecomputedBadge.jsx: 2 placements (see CONTROLS_SPEC §3) +
+ +
+ +
+ + + + diff --git a/frontend/wireframe_notes.md b/frontend/wireframe_notes.md new file mode 100644 index 0000000..0421ffa --- /dev/null +++ b/frontend/wireframe_notes.md @@ -0,0 +1,133 @@ +# wireframe_notes.md — "Memory Under Pressure" +**Agent 2 — Wireframe Agent · Day 1 · DataForge 2026** + +--- + +## Overview + +`wireframe.html` is a static, annotated low-fidelity wireframe of the "Memory Under Pressure" +interactive view. It requires no build step and no backend connection — open directly in a browser. +Every region carries an orange annotation bar referencing the corresponding entry in `CONTROLS_SPEC.md`. + +--- + +## Screen Layout (3-column + 2 header rows) + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ TOP BAR — title, one-sentence claim pill, track label │ +├──────────────────────────────────────────────────────────────────────┤ +│ GUIDED WALKTHROUGH BANNER — step dots | narration text | nav buttons │ +├─────────────────┬────────────────────────────┬───────────────────────┤ +│ LEFT COLUMN │ CENTER COLUMN │ RIGHT COLUMN │ +│ ControlPanel │ CacheHeatmap (top ~40%) │ AccuracyPanel (top) │ +│ .jsx │ │ │ +│ │ MemoryChart (bottom ~60%) │ BDHModule (bottom) │ +├──────────────────┴────────────────────────────┴───────────────────────┤ +│ FOOTER — static label + file links │ +└───────────────────────────────────────────────────────────────────────┘ +``` + +Breakpoint note: the 3-column grid collapses to a single column on screens < 900px wide. +The mobile layout stacks: Walkthrough → Controls → Heatmap → Chart → Accuracy → BDH. + +--- + +## Region-by-Region Notes + +### 1. Top Bar +- Title: "Memory Under Pressure" +- One-sentence claim shown as a pill — this is the framing statement the whole + artifact teaches. Learner sees it before any interaction. +- No CONTROLS_SPEC entry (static display only). + +### 2. GuidedWalkthrough.jsx Banner +- 5 step-dots: step 1 = done (green), step 2 = active (purple), steps 3–5 = inactive. +- Narration text is the scripted explanation for the active step. +- Back / Next navigation. "Skip to sandbox" link shows but greys until step 4. +- CONTROLS_SPEC reference: indirectly maps to all controls (each step unlocks or focuses + one control in sequence). +- ComprehensionCheck.jsx activates **between step 4 and step 5** (inline, below the + narration text — not shown in this wireframe). It gates the sandbox unlock. + +### 3. ControlPanel.jsx (left column) +| Sub-region | CONTROLS_SPEC ref | +|---|---| +| Cache Policy (segmented 4-tab) | §1.1 — `cachePolicy` | +| Budget Size slider | §1.2 — `budgetSize` (disabled/greyed when policy="full") | +| Sequence Length slider | §1.3 — `sequenceLength` | +| Needle Count stepper | §1.4 — `needleCount` | +| Sandbox.jsx (collapsed) | §1.1–1.4 all unlocked, no walkthrough override | + +- The wireframe shows Budget Size greyed out because the active policy is "Full Cache." +- Sandbox.jsx appears as a collapsed toggle at the bottom of the control panel; + it expands to the same control set but without walkthrough constraints. + +### 4. CacheHeatmap.jsx (center, top) +- **Two rows of cells:** current policy (top) vs. Full Cache baseline (bottom). +- Red cells = needle token positions (same in both rows — they're in the sequence regardless). +- Green = alive in cache. Dark grey = evicted. +- **LIVE badge** — `alive_token_indices[]` comes from `/simulate` response (CONTROLS_SPEC §2.4). +- No PrecomputedBadge here. + +### 5. MemoryChart.jsx (center, bottom) +- Two-line chart: live policy (blue/purple line) vs. full-cache baseline (dashed grey). +- X-axis = generation step. Y-axis = tokens in cache. +- **LIVE badge** — both lines from `/simulate` response (CONTROLS_SPEC §2.1). +- No PrecomputedBadge here. +- The divergence between the two lines is the primary visual payoff of the linear-growth claim. + +### 6. AccuracyPanel.jsx (right, top) +| Sub-region | CONTROLS_SPEC ref | +|---|---| +| Needle table (Model output vs. Correct answer) | §2.2 — `model_answers[]` vs. `ground_truth_answers[]` | +| Accuracy-vs-budget curve (orange) | §2.3 — `accuracy_vs_budget.json` (precomputed) | +| Live point on curve (blue dot) | §2.3 — `accuracy_score` from live `/simulate` | + +- **PRECOMPUTED badge** on the curve (not the dot) — text: + "Precomputed sweep — not this run" + (CONTROLS_SPEC §3, PrecomputedBadge.jsx placement #1) + +### 7. BDHModule.jsx (right, bottom) +| Sub-region | CONTROLS_SPEC ref | +|---|---| +| Left column: "Our toy simulator" stats | §2.5 — live `/simulate` response, bdh_recurrent policy | +| Right column: "BDH paper claims" | §2.5 — `bdh_published_claims.json` (precomputed, published) | + +- **LIVE badge** on left column. +- **PUBLISHED / PRECOMPUTED badge** on right column — text: + "Published result (arXiv:2509.26507) — not reproduced by our team" + (CONTROLS_SPEC §3, PrecomputedBadge.jsx placement #2) +- Disclaimer text under the comparison: "Our toy simulator ≠ the BDH model." + +--- + +## PrecomputedBadge Placements (summary) + +| # | Location in wireframe | Badge text | +|---|---|---| +| 1 | AccuracyPanel — accuracy-vs-budget curve legend | "Precomputed sweep — not this run" | +| 2 | BDHModule — right/published column header | "Published result (arXiv:2509.26507) — not reproduced by our team" | + +Both placements use `PrecomputedBadge.jsx` (CONTROLS_SPEC §3). + +--- + +## What Is NOT in the Wireframe (Day 2+ scope) +- Real chart rendering (Chart.js / D3) — placeholder SVG lines only +- Live API calls — no fetch() anywhere +- Real heatmap data — cells are randomly seeded for visual layout only +- ComprehensionCheck.jsx questions — gated for Day 2 content +- Mobile responsive breakpoints — noted in layout but not wired + +--- + +## Files This Wireframe Was Validated Against +- `CONTROLS_SPEC.md` (Agent 1 output) — every annotation checked +- `ARCHITECTURE.md §3` — data flow confirmed (no invented data sources) +- `PRD_KV_Cache_Explainer.md §4.3` — guided path + sandbox flow matches §4.3 exactly + +--- + +*Produced by Agent 2 — Wireframe Agent.* +*Reviewed by Agent 4 — Reviewer Agent. Companion: `CONTROLS_SPEC.md`, `wireframe.html`.*