diff --git a/.claude/agents/docs-sync-reviewer.md b/.claude/agents/docs-sync-reviewer.md index 7e428de0dd296e1f02a951edffd6c350e531f043..e867c30338e8f01b4eea25b92ff409da7e4b9e3b 100644 --- a/.claude/agents/docs-sync-reviewer.md +++ b/.claude/agents/docs-sync-reviewer.md @@ -1,6 +1,6 @@ --- name: docs-sync-reviewer -description: Keeps documentation truthful after code changes — README, CHANGELOG, DECISIONS, spec status, and design-doc references. Use after completing a feature or making any architecture-relevant choice. +description: Keeps documentation truthful after code changes - README, CHANGELOG, DECISIONS, spec status, and design-doc references. Use after completing a feature or making any architecture-relevant choice. tools: Read, Grep, Glob model: haiku --- @@ -9,13 +9,13 @@ You are the documentation-sync reviewer for the CCR Platform. Docs that lie are no docs; a future lab member must be able to trust every README sentence. After a change, check: -1. **README.md** — do quickstart commands still work as written? Are new features/flags mentioned? -2. **CHANGELOG.md** — is there an entry for user-visible changes (Keep-a-Changelog style)? -3. **DECISIONS.md** — was any architecture/infrastructure/vendor/model choice made without an +1. **README.md** - do quickstart commands still work as written? Are new features/flags mentioned? +2. **CHANGELOG.md** - is there an entry for user-visible changes (Keep-a-Changelog style)? +3. **DECISIONS.md** - was any architecture/infrastructure/vendor/model choice made without an entry (date, decision, why, rejected alternative)? CLAUDE.md hard rule. -4. **docs/specs/** — does the implemented behavior match the spec? Mark spec status +4. **docs/specs/** - does the implemented behavior match the spec? Mark spec status (draft → implemented) and note deviations in the spec file itself. -5. **CLAUDE.md** — are commands, layout, and phase state still accurate? +5. **CLAUDE.md** - are commands, layout, and phase state still accurate? Return a short list of exact edits needed (file + suggested text), or "docs are in sync." -Do not rewrite documents wholesale — smallest truthful diff. +Do not rewrite documents wholesale - smallest truthful diff. diff --git a/.claude/agents/research-validity-reviewer.md b/.claude/agents/research-validity-reviewer.md index efd030057e77db23ad508f895c75b3e38b3f8b76..9701fb34d23e04cfd5a29fbc8744277ca973e522 100644 --- a/.claude/agents/research-validity-reviewer.md +++ b/.claude/agents/research-validity-reviewer.md @@ -1,6 +1,6 @@ --- name: research-validity-reviewer -description: Reviews changes for research-validity risks — hidden preprocessing changes, missing model/construct metadata, unsupported language assumptions, reverse-scoring mishandling, overconfident user-facing claims, reproducibility gaps. Use proactively after changes to the CCR engine, scoring, warnings, exports, or the model/construct registries. +description: Reviews changes for research-validity risks - hidden preprocessing changes, missing model/construct metadata, unsupported language assumptions, reverse-scoring mishandling, overconfident user-facing claims, reproducibility gaps. Use proactively after changes to the CCR engine, scoring, warnings, exports, or the model/construct registries. tools: Read, Grep, Glob, Bash model: inherit --- @@ -11,28 +11,28 @@ honesty: transparent warnings, reproducible runs, and never overstating what CCR Review the changes you are pointed at for: -1. **Preprocessing drift** — any change to text normalization, deduplication, truncation, or +1. **Preprocessing drift** - any change to text normalization, deduplication, truncation, or row filtering that isn't reflected in run metadata AND documented. Silent preprocessing changes invalidate cross-run comparisons. -2. **Metadata completeness** — every run must record: model id + pinned revision + usage_config, +2. **Metadata completeness** - every run must record: model id + pinned revision + usage_config, construct snapshot + version + item_hash, language selection + detection summary, preprocessing config, scoring.adjustment_strategy, output_schema_version, warnings. -3. **Registry bypasses** — model-specific behavior (prefixes, normalization, max length, +3. **Registry bypasses** - model-specific behavior (prefixes, normalization, max length, language support) hardcoded anywhere outside packages/model_registry. E5-family models without "query: " prefix handling is a correctness bug. -4. **Reverse scoring** — any code that adjusts/negates reversed-item similarities beyond the +4. **Reverse scoring** - any code that adjusts/negates reversed-item similarities beyond the recorded adjustment_strategy, or aggregates that silently include/exclude reversed items. V1 policy: raw + flags; exclude_reversed only when explicit; all-reversed must block. -5. **Language assumptions** — English-only logic applied to multilingual paths; warnings +5. **Language assumptions** - English-only logic applied to multilingual paths; warnings removed or softened; supported-language checks bypassed. -6. **Overconfident claims** — UI/exports/docs text implying CCR measures stance, diagnoses, +6. **Overconfident claims** - UI/exports/docs text implying CCR measures stance, diagnoses, or "accuracy"; missing the relatedness-vs-stance caveat where results are presented. -7. **Reproducibility** — generated scripts that would not run offline (platform credentials, +7. **Reproducibility** - generated scripts that would not run offline (platform credentials, unpinned versions, missing prefix logic); golden/expected outputs edited without approval. Return exactly three sections: -1. **Blocking issues** — must fix before merge (cite file:line). -2. **Non-blocking concerns** — should fix soon. -3. **Suggested fixes** — concrete, smallest-diff suggestions. +1. **Blocking issues** - must fix before merge (cite file:line). +2. **Non-blocking concerns** - should fix soon. +3. **Suggested fixes** - concrete, smallest-diff suggestions. -Be specific and cite evidence. If you find nothing, say so plainly — do not invent findings. +Be specific and cite evidence. If you find nothing, say so plainly - do not invent findings. diff --git a/.claude/agents/test-parity-reviewer.md b/.claude/agents/test-parity-reviewer.md index b3100c6ea61fc6f57ac84320462bdc1701b4a63c..68682ca80de6a2d1210722dcb0d41244e91011ae 100644 --- a/.claude/agents/test-parity-reviewer.md +++ b/.claude/agents/test-parity-reviewer.md @@ -10,7 +10,7 @@ You are the test & parity reviewer for the CCR Platform. Ground rules you enforce: - The published `ccr_wrapper` implementation is the correctness spec. Changes to similarity, scoring, or preprocessing must keep (or add) parity coverage within float tolerance (atol), - with pinned model revisions on both paths — never compare against a moving reference. + with pinned model revisions on both paths - never compare against a moving reference. - Golden evals exist to catch silent drift: output shape, column names/order, metadata fields, and warning behavior. `evals/expected_outputs/` is human-approved only. - New behavior without tests in the same change is incomplete work, not a follow-up. @@ -26,5 +26,5 @@ When reviewing, check: 4. Run the relevant test commands (cd backend && python -m pytest tests/ -q) and report results. Return: (1) coverage gaps with proposed test names, (2) any weakened assertions, -(3) parity/golden risks, (4) test run results. Write new tests when asked — smallest +(3) parity/golden risks, (4) test run results. Write new tests when asked - smallest useful tests, placed beside existing suites, using existing fixtures. diff --git a/.claude/commands/implement-spec.md b/.claude/commands/implement-spec.md index 8716295a29bb5de5bf15aab032a0a604fa5d3387..530c5172441b94078b7a43c355631b147b7e6c63 100644 --- a/.claude/commands/implement-spec.md +++ b/.claude/commands/implement-spec.md @@ -11,5 +11,5 @@ Follow this loop strictly (CLAUDE.md workflow): 2. Restate the contract in 3 bullets: inputs, outputs/columns, warnings/edge cases. Flag anything the spec leaves undefined BEFORE coding. 3. Propose an implementation plan: files to change, tests to add, smallest viable diff. Wait for approval if the plan touches architecture rules. 4. Implement in small increments. Add/update tests in the same change. -5. Run: cd backend && python -m pytest tests/ -q — plus registry validators if registries changed. +5. Run: cd backend && python -m pytest tests/ -q - plus registry validators if registries changed. 6. Summarize: changed files, test results, deviations from spec (update the spec's status/notes section), CHANGELOG entry, and whether a DECISIONS.md entry is needed. diff --git a/.claude/commands/review-scoring-contract.md b/.claude/commands/review-scoring-contract.md index 5842678ec49b10cd93ff95e693777ac2334bc043..19d730adda8816431ef6d50f41ef22535fc7255e 100644 --- a/.claude/commands/review-scoring-contract.md +++ b/.claude/commands/review-scoring-contract.md @@ -1,5 +1,5 @@ --- -description: Audit scoring/output contract — columns, adjustment_strategy, all-reversed block, metadata completeness +description: Audit scoring/output contract - columns, adjustment_strategy, all-reversed block, metadata completeness --- Audit the current scoring and output contract end to end: diff --git a/.claude/hooks/protect-golden.sh b/.claude/hooks/protect-golden.sh index 302ad23dc9b9b5ebe16babc1b1ff57c113b3f179..22afea4a1a5785df43ac82fb32b238a5b4b47956 100755 --- a/.claude/hooks/protect-golden.sh +++ b/.claude/hooks/protect-golden.sh @@ -8,7 +8,7 @@ file_path="$(printf '%s' "$input" | python3 -c 'import sys,json; d=json.load(sys case "$file_path" in *"evals/expected_outputs/"*) - echo "BLOCKED: $file_path is a golden expected output. Golden files change only via explicit human approval — ask Deva to update it manually or approve a regeneration script." >&2 + echo "BLOCKED: $file_path is a golden expected output. Golden files change only via explicit human approval - ask Deva to update it manually or approve a regeneration script." >&2 exit 2 ;; *"backend/data/"*) diff --git a/.claude/hooks/py-syntax-check.sh b/.claude/hooks/py-syntax-check.sh index 566a272d6e946821077a94246de4f7f4d1effd7d..768d8b70ffbe962206f3e5df67c6cdf0d12ee12e 100755 --- a/.claude/hooks/py-syntax-check.sh +++ b/.claude/hooks/py-syntax-check.sh @@ -12,4 +12,14 @@ if [[ "$file_path" == *.py && -f "$file_path" ]]; then exit 2 fi fi + +# Project style rule: no em dashes in any source/text file (CLAUDE.md hard rules). +case "$file_path" in + *.py|*.js|*.jsx|*.css|*.md|*.yaml|*.yml|*.sh|*.html) + if [[ -f "$file_path" ]] && grep -q $'\xe2\x80\x94' "$file_path"; then + echo "EM DASH found in $file_path. Project rule: no em dashes - use a hyphen or restructure the sentence." >&2 + exit 2 + fi + ;; +esac exit 0 diff --git a/.claude/rules/frontend.md b/.claude/rules/frontend.md index beca426ac50a258feb28947c1859d93b500e8f4c..d6eecf0e04c2cdf13181cdef79942bd3df0f5559 100644 --- a/.claude/rules/frontend.md +++ b/.claude/rules/frontend.md @@ -7,10 +7,10 @@ paths: # Frontend rules -- Current stack: React 18 + Vite, JavaScript (TypeScript arrives in Phase 2 cleanup — do not introduce it piecemeal now). -- All server communication through frontend/src/api.js — no fetch calls inside components. +- Current stack: React 18 + Vite, JavaScript (TypeScript arrives in Phase 2 cleanup - do not introduce it piecemeal now). +- All server communication through frontend/src/api.js - no fetch calls inside components. - The UI never computes analysis values; it renders what the API returns. If a number needs computing, it belongs in the backend. - Warnings from the API are always rendered (amber panel); never drop or reword warning text in the UI. - Accessibility floor (design §19): label every input, keyboard-navigable controls, no color-only signals, charts need an accessible alternative. - Keep the dependency count minimal (currently react + react-dom only); adding a UI library requires a DECISIONS.md entry. -- After frontend changes that should ship: npm run build (outputs to backend/static/) — the API serves the built SPA. +- After frontend changes that should ship: npm run build (outputs to backend/static/) - the API serves the built SPA. diff --git a/.claude/rules/python-backend.md b/.claude/rules/python-backend.md index 79ec3688af5ebd9ad733bc44700c302188e8c8bf..ca9817185218f6b35fdd489b36eaaab3ed67b839 100644 --- a/.claude/rules/python-backend.md +++ b/.claude/rules/python-backend.md @@ -8,7 +8,7 @@ paths: - Python 3.10+; type hints on public functions; docstrings explain WHY, not what. - FastAPI: request/response shapes via Pydantic schemas; HTTPException with clear user-facing detail strings; validation errors are 400s, missing resources 404s, state conflicts 409s. -- All embedding/normalization/similarity math stays in the engine module — API handlers orchestrate, never compute. +- All embedding/normalization/similarity math stays in the engine module - API handlers orchestrate, never compute. - Every user-facing failure path returns an actionable message (what happened + what to do), never a stack trace. - Tests: pytest; use the deterministic fake embedder (model "fake-deterministic" / CCR_FAKE_EMBEDDINGS=1); no torch imports in the default suite; poll async jobs with a timeout helper, never sleep-and-hope. - Warnings are structured objects with UPPER_SNAKE codes (design §12), never bare strings. diff --git a/.claude/rules/registries.md b/.claude/rules/registries.md index a318c76c4d8206cd15a9582a5460d9eea56dc148..46a928f2cc5c0292cd8e857888512860b2912b6b 100644 --- a/.claude/rules/registries.md +++ b/.claude/rules/registries.md @@ -7,9 +7,9 @@ paths: # Registry & construct library rules - models.yaml is the single source of model truth: id, provider_model_id, pinned revision, supported_languages OR supported_language_set, embedding_dimension, max_seq_length, usage_config (requires_prefix, prefix strings, normalize_embeddings), tiers, operational_config, user-facing warnings. -- The `pooling` field is DESCRIPTIVE only — models load via their own sentence-transformers config; never reimplement pooling. +- The `pooling` field is DESCRIPTIVE only - models load via their own sentence-transformers config; never reimplement pooling. - E5-family entries must set requires_prefix: true with "query: " on BOTH items and texts. -- supported_language_set values (e.g., xlm_roberta_100) must resolve to real ISO codes in language_sets.py — a bare "multilingual" label is banned. +- supported_language_set values (e.g., xlm_roberta_100) must resolve to real ISO codes in language_sets.py - a bare "multilingual" label is banned. - Constructs are versioned append-only YAML: editing items/wording/flags creates a NEW version with a new item_hash; never mutate an existing version in place. - Every construct carries: name, version, language, items (text + reverse_scored), citation, verification_status (draft|needs_verification|verified|archived). Item wordings must be flagged until verified verbatim against the original publication. - After any change here, run both validators before considering the change done. diff --git a/.env.example b/.env.example index ac6592ca30596bb3062155004f52de0fa544392d..cb470d32f27958cb8857b95f681d372297aa7402 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,7 @@ # Row ceiling for uploads (default 100000; hosted demo uses 20000) # CCR_MAX_ROWS=20000 -# Force the deterministic fake embedder (tests/CI only — never production) +# Force the deterministic fake embedder (tests/CI only - never production) # CCR_FAKE_EMBEDDINGS=1 # ---- Phase 2 (not read yet; reserved names) ---- diff --git a/.gitignore b/.gitignore index 68cbe40846ba13a5c641eeabe90af84fba28493a..40579bd818abccf07eb8fecae90c028dc57733bd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ __pycache__/ .venv/ .pytest_cache/ -# runtime data (DB, uploaded corpora, results) — never commit research data +# runtime data (DB, uploaded corpora, results) - never commit research data backend/data/ # node diff --git a/CHANGELOG.md b/CHANGELOG.md index ec75e0ee2cc469b1bb56768ee2e1e13f19e99f11..684b710addf43b51a08076f53d267985eed62435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ Format: [Keep a Changelog](https://keepachangelog.com/). User-visible changes on ## [Unreleased] +### Added +- Anonymous vs signed-in upload tiers: anonymous uploads capped at 2 MB / 500 rows with a + clear sign-in hint; signing in lifts limits. Sign-in is a labeled placeholder (name only) + until managed auth arrives with lab accounts; tampered sessions are treated as anonymous. +- Project lifecycle: archive/unarchive (reversible, collapses into an Archived sidebar + group) and permanent delete with type-the-name confirmation. Delete cascades to datasets, + runs, uploaded files, and result files, and is logged without retaining any text. + +### Changed +- Project-wide style rule: no em dashes in any project text (enforced by an edit hook). + +### Fixed +- Job worker survives lifespan restarts (dev reload previously killed job submission + permanently). + ### Fixed - Project sidebar redesigned for growing lists: always-visible search, recency groups (Today / This week / Earlier), per-project run count + relative last-activity time, @@ -19,7 +34,7 @@ Format: [Keep a Changelog](https://keepachangelog.com/). User-visible changes on results dashboard with descriptive stats + per-item loadings + top/bottom texts, data-quality warnings, CSV export mirroring ccr_wrapper shape, reproducibility metadata). - Model registry (packages/model_registry) and versioned construct library - (packages/construct_library) with validators — not yet wired into the app (specs 0003/0004). + (packages/construct_library) with validators - not yet wired into the app (specs 0003/0004). - Claude Code project config: CLAUDE.md, path-scoped rules, 3 review agents, 4 commands, golden-file protection + Python syntax hooks. @@ -30,8 +45,8 @@ Format: [Keep a Changelog](https://keepachangelog.com/). User-visible changes on warnings for language mismatch, uncertain detection, and model/language coverage (multilingual model checked against its real ISO language set). (spec 0001/0003) - Very-short-text warning (< 4 words) with affected-row samples. (spec 0001) -- Downloadable Python reproduction script per run — offline-runnable, embeds construct - items, model revision, and E5 prefix logic — plus pinned requirements file. (spec 0002) +- Downloadable Python reproduction script per run - offline-runnable, embeds construct + items, model revision, and E5 prefix logic - plus pinned requirements file. (spec 0002) - Model registry drives the model dropdown, validation, prefixes, and metadata: MiniLM (default), E5-Large-v2, Multilingual-E5-base. (spec 0003) - Construct library loaded from versioned YAML files with item hashes, citations, and @@ -40,9 +55,9 @@ Format: [Keep a Changelog](https://keepachangelog.com/). User-visible changes on block, construct snapshot, model revision, environment pins. ### Changed -- All warnings are structured objects (code/severity/message/count/rows) — UI renders codes. +- All warnings are structured objects (code/severity/message/count/rows) - UI renders codes. - /api/models response shape changed (registry ids like "all-minilm-l6-v2" replace provider ids). -- Delimiter detection restricted to real candidates (, ; tab |) — fixes single-column +- Delimiter detection restricted to real candidates (, ; tab |) - fixes single-column sentence CSVs being split on spaces. ### Removed diff --git a/CLAUDE.md b/CLAUDE.md index ee951d4b18c7eeccbb915716d5c4daa5bcf723b2..46d0d98590e16c64a3026f548a66ee683e8ec533 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,20 +10,20 @@ Current state: Phase 0 (visible features on the demo codebase). Roadmap: @ROADMA ## Commands -- Backend tests: `cd backend && python -m pytest tests/ -q` (fast — fake embedder, no torch needed) +- Backend tests: `cd backend && python -m pytest tests/ -q` (fast - fake embedder, no torch needed) - Run app locally: `./run.sh` → http://127.0.0.1:8000 - Frontend build: `cd frontend && npm install && npm run build` (outputs to backend/static/) - Frontend dev: `cd frontend && npm run dev` (proxies /api to :8000) - Validate registries: `python packages/model_registry/validate_models.py && python packages/construct_library/validate_constructs.py` - Real-model smoke test: `python scripts/verify_install.py` (downloads MiniLM once) -## Architecture rules (from design §15 — non-negotiable) +## Architecture rules (from design §15 - non-negotiable) - Analysis/NLP logic lives ONLY in the engine (today `backend/app/ccr.py`; Phase 1 extracts it to `packages/ccr_engine/`). Never in UI components or API route handlers. - ALL model-specific behavior (prefixes, normalization, dims, max length, language support) goes through `packages/model_registry/models.yaml`. No hardcoded model behavior anywhere. -- Predefined constructs live ONLY in `packages/construct_library/constructs/*.yaml` — versioned, append-only; edits create new versions. (Migration from `backend/app/seed_constructs.py` is a Phase 0 task.) +- Predefined constructs live ONLY in `packages/construct_library/constructs/*.yaml` - versioned, append-only; edits create new versions. (Migration from `backend/app/seed_constructs.py` is a Phase 0 task.) - Every analysis run must produce metadata + a reproduction script (design §14). Exports mirror `ccr_wrapper` output shape. -- MiniLM (all-MiniLM-L6-v2) is the default model — it is the CCR reference model. E5-family models REQUIRE the "query: " prefix on both items and texts. +- MiniLM (all-MiniLM-L6-v2) is the default model - it is the CCR reference model. E5-family models REQUIRE the "query: " prefix on both items and texts. - The published `ccr_wrapper` implementation is the correctness spec (parity tests, design §16). ## Hard rules @@ -31,10 +31,11 @@ Current state: Phase 0 (visible features on the demo codebase). Roadmap: @ROADMA - Never silently change output column names (bump output_schema_version in metadata instead). - Never remove or weaken warnings to make tests pass. - Never log uploaded text content. -- Never edit files under `evals/expected_outputs/` — golden outputs change only via explicit human approval (enforced by a PreToolUse hook). +- Never edit files under `evals/expected_outputs/` - golden outputs change only via explicit human approval (enforced by a PreToolUse hook). - No new infrastructure (Redis, GPU, new vendors, new services) without a DECISIONS.md entry. - Reverse scoring: v1 exports raw similarities + flags only; adjustment_strategy is a recorded parameter (design §11). Do not invent adjustments. - Do not restructure directories big-bang; follow the strangler steps (design §7). +- No em dashes anywhere in project text: UI strings, docs, comments, commit messages, YAML. Use a hyphen or restructure the sentence (Deva's rule, 2026-07-09; enforced by a PostToolUse hook). ## Workflow @@ -45,10 +46,10 @@ Current state: Phase 0 (visible features on the demo codebase). Roadmap: @ROADMA ## PI working norms -- Mohammad (2026-07-09): "you can always suggest better options that I might not know about — +- Mohammad (2026-07-09): "you can always suggest better options that I might not know about - take initiative (newer NLP models, lighter options, less expensive options)." Consequence: when a decision involves a model, service, or cost, ALWAYS check for newer/lighter/cheaper - candidates and present the better option with evidence — recommending beats asking. + candidates and present the better option with evidence - recommending beats asking. New models enter via the registry + benchmark-before-adopt (golden/parity harness), never by swapping defaults silently. Cost options favor free tiers and annual-friendly services. diff --git a/DECISIONS.md b/DECISIONS.md index 8ea478cee3ec62738fbd74bf0bbc84e55df8f52f..74b0e5508cd56536ac0335b38d43b1fb2d7ad8fd 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1,32 +1,48 @@ # Decisions Log > One entry per architecture/infrastructure/vendor/model decision. -> Format: date — decision — why — rejected alternative — revisit trigger. +> Format: date - decision - why - rejected alternative - revisit trigger. > CLAUDE.md hard rule: no new infrastructure without an entry here. -## 2026-07-09 — Repo seeded from demo; demo stays live +## 2026-07-09 - Repo seeded from demo; demo stays live Mainline moves to this repo (future home: Culture-and-Morality-Lab org). The public demo repo -and HF Space remain deployed and untouched — the PI links people to it. Rejected: evolving the +and HF Space remain deployed and untouched - the PI links people to it. Rejected: evolving the demo repo in place (would couple experiments to a live artifact). Revisit: after Phase 1, repo transfers to the lab org (design §7 step 7). -## 2026-07-09 — Postgres jobs table over Redis/RQ (design §6) +## 2026-07-09 - Postgres jobs table over Redis/RQ (design §6) Durable queue via SELECT ... FOR UPDATE SKIP LOCKED + leases + sweeper; job state already in the DB. Rejected: Redis/RQ (a third stateful service before pressure exists). Trigger to revisit: multiple workers, scheduling needs, sustained queue depth. -## 2026-07-09 — Managed auth (Supabase Auth recommended), staged Google-first (design §8, §4.1) +## 2026-07-09 - Managed auth (Supabase Auth recommended), staged Google-first (design §8, §4.1) Rejected: custom password auth (hashing, reset flows, verification email, lockouts = hidden security work). Staged: Google sign-in first (zero email infra), email/password second within the same provider. Trigger: PI sign-off starts Phase 2. -## 2026-07-09 — MiniLM default; e5-large-v2 strong option; multilingual-E5 (design §13) +## 2026-07-09 - MiniLM default; e5-large-v2 strong option; multilingual-E5 (design §13) MiniLM is the CCR reference model → comparability with the published method; E5 models require "query: " prefixes (encoded in registry usage_config). Rejected: e5-large-v2 as default (slow on CPU, breaks anonymous inline tier; cross-family scores not comparable). Trigger: lab validation study could change defaults. -## 2026-07-09 — Reverse scoring: raw + flags only in v1 (design §11) +## 2026-07-09 - Reverse scoring: raw + flags only in v1 (design §11) Aggregates exclude reversed items only when explicitly required; all-reversed blocks aggregate. Rejected: sign-negation by default (silently invented psychometrics). Trigger: lab methods decision after discussion with PI. + +## 2026-07-09 - Placeholder demo sign-in for tier testing (not security) +Anonymous caps (2 MB / 500 rows, env-tunable) enforce the design §5.1 tiers now; a +name-only signed-cookie session lifts them so the tiered UX is testable before Supabase +auth lands. get_current_user() is the single Phase 2 integration point. Rejected: waiting +for real auth (tiers untestable) and client-only gating (trivially bypassed). + +## 2026-07-09 - Open archive/delete until accounts exist +Any visitor can archive/delete any project on the shared instance: there are no owners yet, +and mistake-cleanup matters more than protection on a demo. Delete requires typing the +project name, cascades to all files/rows, and is logged without text (design §9). Phase 2 +adds ownership checks to the same endpoints. + +## 2026-07-09 - No em dashes in project text (Deva) +Style rule across UI, docs, comments, commits. Enforced by a PostToolUse hook; swept 50 +existing files. diff --git a/DEPLOY.md b/DEPLOY.md index 138bfa6682cf56f0017c232d3231cd4364fabaf4..0cf51e3c672136f3dfe711a556eb599cf43d4a15 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,6 +1,6 @@ # Deploying the demo -## Option A — Hugging Face Spaces (free, recommended for the demo) +## Option A - Hugging Face Spaces (free, recommended for the demo) Free Docker Spaces: 2 vCPU / 16 GB RAM, no credit card. Bonus: the original CCR online tool lives on HF Spaces, so the prototype sits where the CCR @@ -34,19 +34,19 @@ community already works. First build takes ~5–10 min (model bakes into the image). Watch the build logs in the Space's "Logs" tab. -4. Optional hardening for the public instance — in Space Settings → +4. Optional hardening for the public instance - in Space Settings → Variables, set `CCR_MAX_ROWS=20000` (tighter ceiling than the 100k default while strangers can reach it). Notes: -- Storage is **ephemeral** — uploads/results vanish on restart or rebuild. +- Storage is **ephemeral** - uploads/results vanish on restart or rebuild. Fine for a demo; the email and the in-app welcome text both say so. - Free Spaces sleep after ~48h without traffic. Visit the URL the evening before and the morning of the interview so it's warm. - The direct app URL (no HF frame) is - `https://-ccr-platform.hf.space` — send that one. + `https://-ccr-platform.hf.space` - send that one. -## Option B — Google Cloud Run (few dollars, more "prod-like" URL) +## Option B - Google Cloud Run (few dollars, more "prod-like" URL) ```bash gcloud run deploy ccr-platform \ @@ -62,7 +62,7 @@ gcloud run deploy ccr-platform \ - `--min-instances 1`: no cold starts while he plays with it (~a few dollars for the week; delete the service after the process ends). -- `--max-instances 1`: SQLite + in-process queue assume one instance — +- `--max-instances 1`: SQLite + in-process queue assume one instance - documented demo trade-off, not an oversight. ## Pre-send checklist (either host) @@ -72,9 +72,9 @@ gcloud run deploy ccr-platform \ Satisfaction with Life → Run → results render → Export CSV downloads. 3. Second run (Individualism) finishes in seconds (model + item cache warm). 4. Upload your own messy CSV (Excel export with a BOM, or semicolon- - delimited) — parses, and any fallback is flagged in the UI. + delimited) - parses, and any fallback is flagged in the UI. 5. Upload rejects a bogus file (.txt/.exe) with a clean error. -6. Refresh the page — SPA loads, project still listed. +6. Refresh the page - SPA loads, project still listed. 7. Morning of the interview: open the URL once (warm the instance), re-run step 2 quickly. diff --git a/Dockerfile b/Dockerfile index 35a27e0a78ca901492eee475c271d5196331b303..705c89fe83821ea6eb1898d4a7769d72ec5ef2a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# CCR Platform — Cloud Run / container deployment. +# CCR Platform - Cloud Run / container deployment. # The embedding model is baked into the image so the first request # doesn't trigger a ~90 MB download (critical for demo cold starts). @@ -15,7 +15,7 @@ WORKDIR /srv COPY backend/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -# Pre-download ALL offered models into the image layer — a user picking a +# Pre-download ALL offered models into the image layer - a user picking a # non-default model must not trigger a multi-hundred-MB download mid-job # (looks like a hang). Make the cache usable by any runtime UID. RUN python -c "from sentence_transformers import SentenceTransformer; \ @@ -28,7 +28,7 @@ RUN python -c "from sentence_transformers import SentenceTransformer; \ COPY backend/app ./app COPY backend/static ./static -# Demo note: SQLite + uploads live on the container's ephemeral disk — +# Demo note: SQLite + uploads live on the container's ephemeral disk - # data resets on restart/redeploy. Acceptable for a demo; use # Postgres + S3/GCS object storage before any real use. EXPOSE 7860 diff --git a/README.md b/README.md index 1bc23583faa383d661d9d60515e237ff116008d8..f3f22338a89882a2a09cc9a446b13c3fb8f13cf5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# CCR Platform — Culture and Morality Lab +# CCR Platform - Culture and Morality Lab Web platform for **Contextualized Construct Representation (CCR)** psychological text analysis ([method](https://github.com/Ali-Omrani/CCR) · [EMNLP 2024](https://aclanthology.org/2024.emnlp-main.151/)). @@ -20,7 +20,7 @@ reproducibility record. ``` ```bash -# fast test suite (deterministic fake embedder — no torch needed) +# fast test suite (deterministic fake embedder - no torch needed) cd backend && pip install -r requirements-dev.txt && python -m pytest tests/ -q # validate the registries @@ -37,7 +37,7 @@ python scripts/verify_install.py backend/ FastAPI app + tests (current working application) frontend/ React/Vite SPA source (prebuilt copy served from backend/static/) packages/ - model_registry/ models.yaml — single source of model truth + validator + model_registry/ models.yaml - single source of model truth + validator construct_library/ versioned construct YAMLs + validator (append-only) ccr_engine/ Phase 1 target: pure analysis package (see spec 0005) docs/ @@ -49,7 +49,7 @@ evals/ golden datasets + expected outputs (human-approved only) ## Working on this repo -Read `CLAUDE.md` first — it carries the architecture rules and hard rules (enforced partly by +Read `CLAUDE.md` first - it carries the architecture rules and hard rules (enforced partly by hooks). Features start from `docs/specs/`; decisions land in `DECISIONS.md`; user-visible changes land in `CHANGELOG.md`. The published `ccr_wrapper` implementation is the correctness spec for the engine. diff --git a/ROADMAP.md b/ROADMAP.md index 900abed3c732e2045f802a9bf4e5bfca24c8987b..bfca3a73f7c64e22d7b380d68de1563307c9cd14 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,23 +1,23 @@ -# Roadmap (from design doc §4 / §20 — capacity: one person, ≤20 hrs/week) +# Roadmap (from design doc §4 / §20 - capacity: one person, ≤20 hrs/week) -## Phase 0 — Visible progress (now) -- [x] Descriptive stats (mean/SD/min/max) — shipped in demo codebase +## Phase 0 - Visible progress (now) +- [x] Descriptive stats (mean/SD/min/max) - shipped in demo codebase - [x] Spec 0001: short-text + language warnings (structured warning objects) - [x] Spec 0002: offline-runnable Python script export + pinned requirements - [x] Spec 0003: registry-driven models (E5 prefixes!) + language selection - [x] Spec 0004: construct library from versioned YAML (assistant's questionnaires land here) -## Phase 1 — Engine hardening (weeks 3–5) +## Phase 1 - Engine hardening (weeks 3–5) - [ ] Spec 0005: extract packages/ccr_engine + golden evals + ccr_wrapper parity tests - [ ] Pin model revisions in models.yaml (remove PIN_ME) - [ ] Registry validation in CI -## Phase 2 — Persistence + auth (weeks 6–9, AFTER §22 sign-off) +## Phase 2 - Persistence + auth (weeks 6–9, AFTER §22 sign-off) - [ ] Supabase Auth (Google first, then email/password) · projects · Postgres metadata + jobs - [ ] Object storage behind the existing storage interface · TTL cleanup (anonymous 48h) - [ ] Anonymous caps per design §5.1 starter table -## Phase 3 — Production-lite launch +## Phase 3 - Production-lite launch - [ ] Repo → Culture-and-Morality-Lab org · staging + production hosting (Option B) - [ ] Backups, monitoring, privacy wording (lab-provided), verified-constructs workflow diff --git a/WORKLOG.md b/WORKLOG.md index 87095aa8f87c7d3edeed90d7f8a4d7d72f899f09..f65c73f7110da50dad8eae54173ad2f616067ed1 100644 --- a/WORKLOG.md +++ b/WORKLOG.md @@ -1,6 +1,6 @@ # Worklog pointer Hours and task-level logging live in the private lab worklog -(`../CAM-Lab/worklog.md` on Deva's machine — not committed here). +(`../CAM-Lab/worklog.md` on Deva's machine - not committed here). This file exists so repo docs can reference a stable path; keep private details out of the repo. diff --git a/backend/app/auth_demo.py b/backend/app/auth_demo.py new file mode 100644 index 0000000000000000000000000000000000000000..0209e9b4c27b60a7debd74c50d2f1febbe94349e --- /dev/null +++ b/backend/app/auth_demo.py @@ -0,0 +1,64 @@ +"""Placeholder sign-in for tier testing (design doc §4.1 staged rollout). + +This is deliberately NOT security. It exists so the anonymous-vs-signed-in +upload tiers have real UX and enforcement before the managed auth provider +(Supabase: Google first, then email/password) lands in Phase 2. The swap +plan: get_current_user() is the only integration point - Phase 2 replaces +its body with provider JWT verification and nothing else changes. + +Session: HMAC-signed cookie. Secret comes from CCR_SESSION_SECRET or is +random per process (restart signs everyone out - acceptable for a demo). +""" + +from __future__ import annotations + +import base64 +import hashlib +import hmac +import os +import secrets + +from fastapi import Request + +COOKIE_NAME = "ccr_demo_session" +_SECRET = (os.environ.get("CCR_SESSION_SECRET") or secrets.token_hex(32)).encode() + +# Tier limits; env-overridable so tests and the hosted demo can tune them. +ANON_MAX_BYTES_DEFAULT = 2 * 1024 * 1024 +ANON_MAX_ROWS_DEFAULT = 500 + + +def anon_max_bytes() -> int: + return int(os.environ.get("CCR_ANON_MAX_BYTES", ANON_MAX_BYTES_DEFAULT)) + + +def anon_max_rows() -> int: + return int(os.environ.get("CCR_ANON_MAX_ROWS", ANON_MAX_ROWS_DEFAULT)) + + +def _sign(payload: bytes) -> str: + return hmac.new(_SECRET, payload, hashlib.sha256).hexdigest() + + +def create_token(name: str) -> str: + payload = base64.urlsafe_b64encode(name.encode()).decode() + return f"{payload}.{_sign(payload.encode())}" + + +def verify_token(token: str | None) -> str | None: + """Return the signed-in name, or None for anonymous/invalid tokens.""" + if not token or "." not in token: + return None + payload, signature = token.rsplit(".", 1) + if not hmac.compare_digest(signature, _sign(payload.encode())): + return None + try: + return base64.urlsafe_b64decode(payload.encode()).decode() + except Exception: + return None + + +def get_current_user(request: Request) -> dict | None: + """Phase 2 integration point: replace body with provider JWT verification.""" + name = verify_token(request.cookies.get(COOKIE_NAME)) + return {"name": name, "tier": "member"} if name else None diff --git a/backend/app/ccr.py b/backend/app/ccr.py index 7309a447523faea0ada4d2fa278ce911643330dd..eb12017802a9d8658fa0cc8f9463828d7b9b1c9b 100644 --- a/backend/app/ccr.py +++ b/backend/app/ccr.py @@ -1,4 +1,4 @@ -"""CCR engine — Contextualized Construct Representations. +"""CCR engine - Contextualized Construct Representations. Method (Atari, Omrani et al.): embed validated questionnaire items and the texts to be analyzed with a contextual sentence-embedding model, then take @@ -7,7 +7,7 @@ similarities are the text's "loadings" on the construct; their mean is the overall CCR score. The embedding model is injected behind a small interface so that: - * production uses sentence-transformers (local, pinned, reproducible — + * production uses sentence-transformers (local, pinned, reproducible - corpora never leave the machine), and * tests/CI use a deterministic hash-based embedder with no ML dependency. """ @@ -126,7 +126,7 @@ class CCRResult: # Item-set embeddings are tiny and constantly reused (same construct run -# against many corpora) — cache them per (model, exact item wording). +# against many corpora) - cache them per (model, exact item wording). _item_embedding_cache: dict[tuple[str, str], np.ndarray] = {} @@ -150,7 +150,7 @@ def run_ccr( ) -> CCRResult: """Compute CCR loadings: cosine(text, item) for every text × item pair. - Prefixes come from the model registry's usage_config — E5-family models require + Prefixes come from the model registry's usage_config - E5-family models require "query: " on BOTH sides for symmetric similarity. Prefixed strings feed the encoder only; raw wording is what gets hashed and exported. """ diff --git a/backend/app/construct_lib.py b/backend/app/construct_lib.py index f569413b9d69ba62882d7117849609f8ba40bb78..7db15f75d5aec990a12e12afe041b5a6fcf791f7 100644 --- a/backend/app/construct_lib.py +++ b/backend/app/construct_lib.py @@ -1,11 +1,11 @@ -"""Construct library loader — seeds the DB from packages/construct_library/constructs/. +"""Construct library loader - seeds the DB from packages/construct_library/constructs/. Source of truth is the versioned YAML files (spec 0004, design doc §10.1). Rules: - * append-only: (construct_id, version) is immutable — same version with changed + * append-only: (construct_id, version) is immutable - same version with changed items is a hard error, never a silent update; * item_hash uses the REFERENCE implementation from validate_constructs.py (loaded by file path) so validator, seeder, and metadata always agree; - * verification_status flows to the UI — unverified wording is visibly flagged. + * verification_status flows to the UI - unverified wording is visibly flagged. New questionnaires from the lab land as new YAML files; `python packages/construct_library/ validate_constructs.py` first, then restart the app (or call sync) to pick them up. diff --git a/backend/app/db.py b/backend/app/db.py index 5fb00810ea294e2f1ef4ec9654dfc143d8adf68a..69a5ac946472244cdeee08e1244cf6032045d02f 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -1,4 +1,4 @@ -"""Database setup — SQLite via SQLAlchemy. +"""Database setup - SQLite via SQLAlchemy. SQLite is a deliberate choice for this deployment size (single-node, few concurrent writers). The models use no SQLite-specific features, so moving diff --git a/backend/app/ingest.py b/backend/app/ingest.py index 4612f8a544a79661c1e0ebf5a9a8b749b6c083d9..bbd234e179216254e1fb587f8013d48ffdceeae3 100644 --- a/backend/app/ingest.py +++ b/backend/app/ingest.py @@ -1,4 +1,4 @@ -"""Corpus ingestion — tolerant of real-world research files. +"""Corpus ingestion - tolerant of real-world research files. Researchers upload CSVs exported from Qualtrics, Excel, R, SPSS, and scrapers: BOMs, latin-1 encodings, semicolon/tab delimiters, ragged rows. @@ -34,7 +34,7 @@ def load_corpus(path: str | Path) -> tuple[pd.DataFrame, dict]: """Parse CSV/XLSX into a DataFrame. Returns (df, parse_info) where parse_info records the format, - encoding, and delimiter actually used — stored with the corpus and + encoding, and delimiter actually used - stored with the corpus and echoed into every run's reproducibility metadata. """ p = Path(path) @@ -85,7 +85,7 @@ _DELIMITER_CANDIDATES = (",", ";", "\t", "|") def _detect_delimiter(p: Path, encoding: str) -> str: """Pick the candidate delimiter most consistent across the first lines. - Single-column files (no candidate present) default to ',' — a comma parse + Single-column files (no candidate present) default to ',' - a comma parse of a delimiter-free file yields one column, which is exactly right. """ try: @@ -111,7 +111,7 @@ def _validate(df: pd.DataFrame) -> pd.DataFrame: raise IngestError("The file parsed but contains no data rows.") if len(df) > max_rows(): raise IngestError( - f"File has {len(df):,} rows — above this instance's " + f"File has {len(df):,} rows - above this instance's " f"{max_rows():,}-row limit. Split the corpus or run locally." ) df.columns = [str(c) for c in df.columns] diff --git a/backend/app/jobs.py b/backend/app/jobs.py index 8c243eaa382c644e6d4b65ab482bf34e610adf52..db2d30ad778afba9f2dc92db68260dceb14eca73 100644 --- a/backend/app/jobs.py +++ b/backend/app/jobs.py @@ -1,6 +1,6 @@ """Background job runner. -Jobs run on a dedicated single-worker executor — a deliberate right-sizing: +Jobs run on a dedicated single-worker executor - a deliberate right-sizing: embedding is CPU-bound, so running jobs sequentially protects the instance's memory and keeps per-job throughput predictable, while job state lives in the DB (queued → running → completed/failed) so the API and UI never depend @@ -42,15 +42,32 @@ TOP_N = 10 SNIPPET_LEN = 220 # Single worker: sequential jobs, bounded memory. See module docstring. -_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="ccr-job") +# Created lazily and re-creatable: a lifespan shutdown (dev reload, test +# client closing) must not permanently kill job submission for the process. +import threading + +_executor: ThreadPoolExecutor | None = None +_executor_lock = threading.Lock() + + +def _get_executor() -> ThreadPoolExecutor: + global _executor + with _executor_lock: + if _executor is None: + _executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="ccr-job") + return _executor def submit_job(job_id: str) -> None: - _executor.submit(_run_job_logged, job_id) + _get_executor().submit(_run_job_logged, job_id) def shutdown_executor() -> None: - _executor.shutdown(wait=False, cancel_futures=True) + global _executor + with _executor_lock: + if _executor is not None: + _executor.shutdown(wait=False, cancel_futures=True) + _executor = None def recover_orphaned_jobs() -> int: @@ -129,7 +146,7 @@ def run_job(job_id: str) -> None: progress_cb=progress, item_prefix=item_prefix, text_prefix=text_prefix, ) - # Structured data-quality warnings (spec 0001) — objects, never bare strings. + # Structured data-quality warnings (spec 0001) - objects, never bare strings. W = warnings_engine.warning warnings: list[dict] = [] if dropped: @@ -141,7 +158,7 @@ def run_job(job_id: str) -> None: if n_dupes: warnings.append(W( "DUPLICATE_TEXTS", "warning", - f"{n_dupes} duplicate text(s) detected — each is scored independently; " + f"{n_dupes} duplicate text(s) detected - each is scored independently; " "deduplicate upstream if unintended.", count=n_dupes, )) short = warnings_engine.short_text_warning(texts) diff --git a/backend/app/main.py b/backend/app/main.py index 87652d501cacd3958fa19e45664be54d932a53e3..d3db3b26490378544d64cf633cb78c662d9d9f65 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,4 +1,4 @@ -"""CCR Platform — FastAPI application. +"""CCR Platform - FastAPI application. Single deployable: serves the JSON API under /api and the prebuilt React dashboard as static files at /. Local-first by design: corpora, embeddings, @@ -13,12 +13,13 @@ import json from contextlib import asynccontextmanager from pathlib import Path -from fastapi import Depends, FastAPI, HTTPException, UploadFile +from fastapi import Depends, FastAPI, HTTPException, Response, UploadFile from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session +from . import auth_demo from . import jobs as jobs_module from . import registry from .ccr import FAKE_MODEL_NAME @@ -31,10 +32,12 @@ from .schemas import ( ConstructCreate, ConstructOut, CorpusOut, + DemoLogin, JobCreate, JobOut, ProjectCreate, ProjectOut, + ProjectPatch, ) MAX_UPLOAD_BYTES = 25 * 1024 * 1024 # sane lab-scale ceiling; raise deliberately @@ -133,7 +136,7 @@ def health(): @app.get("/api/models") def list_models(): - """Model options from the registry (spec 0003) — never hardcoded.""" + """Model options from the registry (spec 0003) - never hardcoded.""" return [ { "id": m.id, @@ -153,10 +156,47 @@ def list_languages(): return SELECTABLE_LANGUAGES +# ---------------------------------------------------- demo auth (placeholder) +@app.get("/api/auth/me") +def auth_me(user: dict | None = Depends(auth_demo.get_current_user)): + return { + "signed_in": user is not None, + "name": user["name"] if user else None, + "demo": True, + "limits": ( + {"max_bytes": MAX_UPLOAD_BYTES, "max_rows": None} + if user + else {"max_bytes": auth_demo.anon_max_bytes(), "max_rows": auth_demo.anon_max_rows()} + ), + } + + +@app.post("/api/auth/demo/login") +def demo_login(body: DemoLogin, response: Response): + """Placeholder session for tier testing. Replaced by managed auth in Phase 2.""" + name = body.name.strip() + if not name: + raise HTTPException(400, "Name cannot be empty.") + response.set_cookie( + auth_demo.COOKIE_NAME, + auth_demo.create_token(name), + httponly=True, + samesite="lax", + max_age=7 * 24 * 3600, + ) + return {"signed_in": True, "name": name, "demo": True} + + +@app.post("/api/auth/logout") +def logout(response: Response): + response.delete_cookie(auth_demo.COOKIE_NAME) + return {"signed_in": False} + + # --------------------------------------------------------------- projects @app.get("/api/projects", response_model=list[ProjectOut]) def list_projects(db: Session = Depends(get_db)): - """Projects ordered by last activity (latest run, else creation) — the + """Projects ordered by last activity (latest run, else creation) - the project a researcher wants is almost always the one they last worked on.""" from sqlalchemy import func @@ -180,6 +220,7 @@ def list_projects(db: Session = Depends(get_db)): created_at=p.created_at, last_activity_at=last or p.created_at, n_runs=count, + archived=bool(p.archived), ) ) out.sort(key=lambda x: x.last_activity_at, reverse=True) @@ -198,9 +239,65 @@ def create_project(body: ProjectCreate, db: Session = Depends(get_db)): created_at=project.created_at, last_activity_at=project.created_at, n_runs=0, + archived=False, + ) + + +@app.patch("/api/projects/{project_id}", response_model=ProjectOut) +def patch_project(project_id: str, body: ProjectPatch, db: Session = Depends(get_db)): + """Archive/unarchive - reversible, no data loss. Archived projects collapse + into the sidebar's Archived section and keep all datasets and runs.""" + project = _get_or_404(db, Project, project_id) + if body.archived is not None: + project.archived = bool(body.archived) + db.commit() + return ProjectOut( + id=project.id, + name=project.name, + description=project.description, + created_at=project.created_at, + last_activity_at=project.created_at, + n_runs=0, + archived=bool(project.archived), ) +@app.delete("/api/projects/{project_id}", status_code=204) +def delete_project(project_id: str, db: Session = Depends(get_db)): + """Permanent delete: removes the project, its datasets, runs, uploaded + files, and result files. Logged without retaining any uploaded text + (design doc §9). Ownership checks arrive with real accounts in Phase 2; + until then the shared instance is deliberately open.""" + import logging + + project = _get_or_404(db, Project, project_id) + corpora = db.query(Corpus).filter_by(project_id=project_id).all() + jobs = db.query(Job).filter_by(project_id=project_id).all() + + n_files = 0 + for corpus in corpora: + if corpus.path and Path(corpus.path).exists(): + Path(corpus.path).unlink(missing_ok=True) + n_files += 1 + for job in jobs: + if job.result_path and Path(job.result_path).exists(): + Path(job.result_path).unlink(missing_ok=True) + n_files += 1 + + for job in jobs: + db.delete(job) + for corpus in corpora: + db.delete(corpus) + db.delete(project) + db.commit() + + logging.getLogger("ccr.projects").info( + "project deleted: id=%s name=%r corpora=%d runs=%d files_removed=%d", + project_id, project.name, len(corpora), len(jobs), n_files, + ) + return Response(status_code=204) + + # ----------------------------------------------------------------- corpora @app.get("/api/projects/{project_id}/corpora", response_model=list[CorpusOut]) def list_corpora(project_id: str, db: Session = Depends(get_db)): @@ -227,7 +324,12 @@ def list_corpora(project_id: str, db: Session = Depends(get_db)): @app.post("/api/projects/{project_id}/corpora", response_model=CorpusOut, status_code=201) -async def upload_corpus(project_id: str, file: UploadFile, db: Session = Depends(get_db)): +async def upload_corpus( + project_id: str, + file: UploadFile, + db: Session = Depends(get_db), + user: dict | None = Depends(auth_demo.get_current_user), +): _get_or_404(db, Project, project_id) suffix = Path(file.filename or "upload.csv").suffix.lower() @@ -238,6 +340,15 @@ async def upload_corpus(project_id: str, file: UploadFile, db: Session = Depends if len(payload) > MAX_UPLOAD_BYTES: raise HTTPException(413, "File exceeds the 25 MB upload limit.") + # Tier gate (design §5.1): anonymous users get strict caps; signing in + # lifts them. Demo sign-in for now; managed auth replaces it in Phase 2. + if user is None and len(payload) > auth_demo.anon_max_bytes(): + mb = auth_demo.anon_max_bytes() // (1024 * 1024) + raise HTTPException( + 413, + f"Anonymous uploads are limited to {mb} MB. Sign in (top right) to upload larger files.", + ) + corpus = Corpus( project_id=project_id, filename=file.filename, path="", n_rows=0, columns_json="[]" ) @@ -251,6 +362,14 @@ async def upload_corpus(project_id: str, file: UploadFile, db: Session = Depends dest.unlink(missing_ok=True) raise HTTPException(400, str(exc)) from exc + if user is None and len(df) > auth_demo.anon_max_rows(): + dest.unlink(missing_ok=True) + raise HTTPException( + 400, + f"Anonymous uploads are limited to {auth_demo.anon_max_rows():,} rows " + f"(this file has {len(df):,}). Sign in (top right) to upload larger corpora.", + ) + corpus.n_rows = int(len(df)) corpus.columns_json = json.dumps(list(df.columns)) corpus.parse_info_json = json.dumps(parse_info) diff --git a/backend/app/models.py b/backend/app/models.py index f4c108595e373adfca775edf7073fb4cb0b282b3..8483b5367e0cd4b50a2cbae575226c806f273b29 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -2,7 +2,7 @@ IDs are UUID strings (portable across SQLite/Postgres). JSON-ish payloads (column lists, construct items, job metadata/summaries) are stored as JSON -text — they are read-mostly blobs, not queried relationally. +text - they are read-mostly blobs, not queried relationally. """ import uuid @@ -28,6 +28,7 @@ class Project(Base): id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid) name: Mapped[str] = mapped_column(String(200)) description: Mapped[str] = mapped_column(Text, default="") + archived: Mapped[bool] = mapped_column(Boolean, default=False) created_at: Mapped[str] = mapped_column(String(32), default=_now) diff --git a/backend/app/registry.py b/backend/app/registry.py index c3c227c01d3903d402babc249914ca707cd195bb..ed7d089c5ffbb45ac1c75c5ac8070f7659c80353 100644 --- a/backend/app/registry.py +++ b/backend/app/registry.py @@ -1,4 +1,4 @@ -"""Model registry loader — the app-side reader of packages/model_registry/models.yaml. +"""Model registry loader - the app-side reader of packages/model_registry/models.yaml. Single source of model truth (design doc §13): the UI dropdown, backend validation, prefix handling, language-support warnings, run metadata, and generated reproduction @@ -57,7 +57,7 @@ class ModelConfig: return None if self.revision in ("PIN_ME", "", None) else self.revision def supports_language(self, iso_code: str) -> bool: - if not self.supported_languages: # unknown coverage — never block/warn on it + if not self.supported_languages: # unknown coverage - never block/warn on it return True return iso_code.lower() in self.supported_languages diff --git a/backend/app/reproducibility.py b/backend/app/reproducibility.py index 1bbec6ff8e8c6af4cc836c838108be52c899c7f1..c428e264ab0362911319e53039328beecbc35575 100644 --- a/backend/app/reproducibility.py +++ b/backend/app/reproducibility.py @@ -1,7 +1,7 @@ """Reproduction-script generation (spec 0002, design doc §14). -The generated script is built ONLY from the run's stored metadata — never from live -state — so it reproduces what actually ran. It must be runnable outside the platform: +The generated script is built ONLY from the run's stored metadata - never from live +state - so it reproduces what actually ran. It must be runnable outside the platform: input CSV + Python + internet for the (pinned) model download. No platform credentials. """ @@ -108,7 +108,7 @@ def main(csv_path: str) -> None: work.to_csv("reproduced_results.csv", index=False) print(f"Wrote reproduced_results.csv ({{len(work)}} rows, {{sims.shape[1]}} items).") - print("Compare against the platform export — values should match to ~1e-5.") + print("Compare against the platform export - values should match to ~1e-5.") if __name__ == "__main__": diff --git a/backend/app/schemas.py b/backend/app/schemas.py index c4bb95e7c9788809b248da8191a6898c023aca07..d15bd055fbc217eb996addf8dcd1fddf1d9917fb 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -15,6 +15,15 @@ class ProjectOut(BaseModel): created_at: str last_activity_at: str = "" # latest run creation, else project creation n_runs: int = 0 + archived: bool = False + + +class ProjectPatch(BaseModel): + archived: bool | None = None + + +class DemoLogin(BaseModel): + name: str = Field(min_length=1, max_length=80) class CorpusOut(BaseModel): diff --git a/backend/app/warnings_engine.py b/backend/app/warnings_engine.py index f953d6e88038fa6ebfd784df1231a27ce38c1f8b..20538b9d7569cb99a30c262e95d7cbbc1a3d4eb2 100644 --- a/backend/app/warnings_engine.py +++ b/backend/app/warnings_engine.py @@ -1,12 +1,12 @@ """Structured data-quality warnings (spec 0001, design doc §12). -Every warning is an object — {code, severity, message, count?, affected_rows_sample?} — +Every warning is an object - {code, severity, message, count?, affected_rows_sample?} - never a bare string. Codes are UPPER_SNAKE and stable: downstream notebooks and the UI key off them. Severity: "info" (status, not a problem) | "warning" (proceed with care). Language detection is corpus-level only; short texts are exactly where detection is unreliable, so uncertainty is reported instead of guessed away. -Deviation from spec 0001 (recorded there): langdetect instead of lingua — pure-Python, +Deviation from spec 0001 (recorded there): langdetect instead of lingua - pure-Python, ~1 MB vs ~100 MB wheels; seeded for determinism. Upgrade path preserved by recording detector + version in metadata. """ @@ -75,7 +75,7 @@ def detect_corpus_language(texts: list[str], selected: str) -> tuple[LanguageRes except Exception: detector_version = "unknown" - DetectorFactory.seed = 0 # determinism — same corpus, same result, every run + DetectorFactory.seed = 0 # determinism - same corpus, same result, every run detectable = [t for t in texts if len(t.split()) >= DETECT_MIN_TOKENS][:DETECT_SAMPLE_MAX] warnings: list[dict] = [] diff --git a/backend/static/assets/index-CHG17tHs.js b/backend/static/assets/index-CHG17tHs.js deleted file mode 100644 index 21dbf30f65ab3f65ba598be88969c6fd60d1c5e8..0000000000000000000000000000000000000000 --- a/backend/static/assets/index-CHG17tHs.js +++ /dev/null @@ -1,42 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();function cc(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Zu={exports:{}},cl={},Ju={exports:{}},M={};/** - * @license React - * react.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var or=Symbol.for("react.element"),fc=Symbol.for("react.portal"),dc=Symbol.for("react.fragment"),pc=Symbol.for("react.strict_mode"),mc=Symbol.for("react.profiler"),hc=Symbol.for("react.provider"),vc=Symbol.for("react.context"),yc=Symbol.for("react.forward_ref"),gc=Symbol.for("react.suspense"),wc=Symbol.for("react.memo"),xc=Symbol.for("react.lazy"),Ao=Symbol.iterator;function Sc(e){return e===null||typeof e!="object"?null:(e=Ao&&e[Ao]||e["@@iterator"],typeof e=="function"?e:null)}var qu={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},bu=Object.assign,es={};function vn(e,t,n){this.props=e,this.context=t,this.refs=es,this.updater=n||qu}vn.prototype.isReactComponent={};vn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};vn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function ts(){}ts.prototype=vn.prototype;function Qi(e,t,n){this.props=e,this.context=t,this.refs=es,this.updater=n||qu}var Ki=Qi.prototype=new ts;Ki.constructor=Qi;bu(Ki,vn.prototype);Ki.isPureReactComponent=!0;var Vo=Array.isArray,ns=Object.prototype.hasOwnProperty,Yi={current:null},rs={key:!0,ref:!0,__self:!0,__source:!0};function ls(e,t,n){var r,l={},i=null,o=null;if(t!=null)for(r in t.ref!==void 0&&(o=t.ref),t.key!==void 0&&(i=""+t.key),t)ns.call(t,r)&&!rs.hasOwnProperty(r)&&(l[r]=t[r]);var u=arguments.length-2;if(u===1)l.children=n;else if(1>>1,X=C[B];if(0>>1;Bl(Vt,L))wl(ee,Vt)?(C[B]=ee,C[w]=L,B=w):(C[B]=Vt,C[Ae]=L,B=Ae);else if(wl(ee,L))C[B]=ee,C[w]=L,B=w;else break e}}return z}function l(C,z){var L=C.sortIndex-z.sortIndex;return L!==0?L:C.id-z.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var o=Date,u=o.now();e.unstable_now=function(){return o.now()-u}}var s=[],d=[],m=1,h=null,v=3,x=!1,S=!1,k=!1,D=typeof setTimeout=="function"?setTimeout:null,f=typeof clearTimeout=="function"?clearTimeout:null,a=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function p(C){for(var z=n(d);z!==null;){if(z.callback===null)r(d);else if(z.startTime<=C)r(d),z.sortIndex=z.expirationTime,t(s,z);else break;z=n(d)}}function y(C){if(k=!1,p(C),!S)if(n(s)!==null)S=!0,$e(E);else{var z=n(d);z!==null&&At(y,z.startTime-C)}}function E(C,z){S=!1,k&&(k=!1,f(P),P=-1),x=!0;var L=v;try{for(p(z),h=n(s);h!==null&&(!(h.expirationTime>z)||C&&!pe());){var B=h.callback;if(typeof B=="function"){h.callback=null,v=h.priorityLevel;var X=B(h.expirationTime<=z);z=e.unstable_now(),typeof X=="function"?h.callback=X:h===n(s)&&r(s),p(z)}else r(s);h=n(s)}if(h!==null)var Et=!0;else{var Ae=n(d);Ae!==null&&At(y,Ae.startTime-z),Et=!1}return Et}finally{h=null,v=L,x=!1}}var _=!1,j=null,P=-1,A=5,T=-1;function pe(){return!(e.unstable_now()-TC||125B?(C.sortIndex=L,t(d,C),n(s)===null&&C===n(d)&&(k?(f(P),P=-1):k=!0,At(y,L-B))):(C.sortIndex=X,t(s,C),S||x||(S=!0,$e(E))),C},e.unstable_shouldYield=pe,e.unstable_wrapCallback=function(C){var z=v;return function(){var L=v;v=z;try{return C.apply(this,arguments)}finally{v=L}}}})(as);ss.exports=as;var Mc=ss.exports;/** - * @license React - * react-dom.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Oc=R,Ce=Mc;function g(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),bl=Object.prototype.hasOwnProperty,Dc=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Ho={},Wo={};function Fc(e){return bl.call(Wo,e)?!0:bl.call(Ho,e)?!1:Dc.test(e)?Wo[e]=!0:(Ho[e]=!0,!1)}function Ic(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Uc(e,t,n,r){if(t===null||typeof t>"u"||Ic(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function de(e,t,n,r,l,i,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=o}var le={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){le[e]=new de(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];le[t]=new de(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){le[e]=new de(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){le[e]=new de(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){le[e]=new de(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){le[e]=new de(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){le[e]=new de(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){le[e]=new de(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){le[e]=new de(e,5,!1,e.toLowerCase(),null,!1,!1)});var Gi=/[\-:]([a-z])/g;function Zi(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Gi,Zi);le[t]=new de(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Gi,Zi);le[t]=new de(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Gi,Zi);le[t]=new de(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){le[e]=new de(e,1,!1,e.toLowerCase(),null,!1,!1)});le.xlinkHref=new de("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){le[e]=new de(e,1,!1,e.toLowerCase(),null,!0,!0)});function Ji(e,t,n,r){var l=le.hasOwnProperty(t)?le[t]:null;(l!==null?l.type!==0:r||!(2u||l[o]!==i[u]){var s=` -`+l[o].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=o&&0<=u);break}}}finally{Pl=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Tn(e):""}function $c(e){switch(e.tag){case 5:return Tn(e.type);case 16:return Tn("Lazy");case 13:return Tn("Suspense");case 19:return Tn("SuspenseList");case 0:case 2:case 15:return e=zl(e.type,!1),e;case 11:return e=zl(e.type.render,!1),e;case 1:return e=zl(e.type,!0),e;default:return""}}function ri(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Qt:return"Fragment";case Wt:return"Portal";case ei:return"Profiler";case qi:return"StrictMode";case ti:return"Suspense";case ni:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case ds:return(e.displayName||"Context")+".Consumer";case fs:return(e._context.displayName||"Context")+".Provider";case bi:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case eo:return t=e.displayName||null,t!==null?t:ri(e.type)||"Memo";case it:t=e._payload,e=e._init;try{return ri(e(t))}catch{}}return null}function Ac(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return ri(t);case 8:return t===qi?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function wt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function ms(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Vc(e){var t=ms(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(o){r=""+o,i.call(this,o)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(o){r=""+o},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function pr(e){e._valueTracker||(e._valueTracker=Vc(e))}function hs(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=ms(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Ar(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function li(e,t){var n=t.checked;return Q({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Ko(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=wt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function vs(e,t){t=t.checked,t!=null&&Ji(e,"checked",t,!1)}function ii(e,t){vs(e,t);var n=wt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?oi(e,t.type,n):t.hasOwnProperty("defaultValue")&&oi(e,t.type,wt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Yo(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function oi(e,t,n){(t!=="number"||Ar(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Ln=Array.isArray;function nn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=mr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Wn(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var On={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Bc=["Webkit","ms","Moz","O"];Object.keys(On).forEach(function(e){Bc.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),On[t]=On[e]})});function xs(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||On.hasOwnProperty(e)&&On[e]?(""+t).trim():t+"px"}function Ss(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=xs(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var Hc=Q({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function ai(e,t){if(t){if(Hc[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(g(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(g(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(g(61))}if(t.style!=null&&typeof t.style!="object")throw Error(g(62))}}function ci(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var fi=null;function to(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var di=null,rn=null,ln=null;function Zo(e){if(e=ar(e)){if(typeof di!="function")throw Error(g(280));var t=e.stateNode;t&&(t=hl(t),di(e.stateNode,e.type,t))}}function ks(e){rn?ln?ln.push(e):ln=[e]:rn=e}function Cs(){if(rn){var e=rn,t=ln;if(ln=rn=null,Zo(e),t)for(e=0;e>>=0,e===0?32:31-(ef(e)/tf|0)|0}var hr=64,vr=4194304;function Rn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Wr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,o=n&268435455;if(o!==0){var u=o&~l;u!==0?r=Rn(u):(i&=o,i!==0&&(r=Rn(i)))}else o=n&~l,o!==0?r=Rn(o):i!==0&&(r=Rn(i));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,i=t&-t,l>=i||l===16&&(i&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function ur(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Fe(t),e[t]=n}function of(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Fn),iu=" ",ou=!1;function Hs(e,t){switch(e){case"keyup":return Of.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Ws(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Kt=!1;function Ff(e,t){switch(e){case"compositionend":return Ws(t);case"keypress":return t.which!==32?null:(ou=!0,iu);case"textInput":return e=t.data,e===iu&&ou?null:e;default:return null}}function If(e,t){if(Kt)return e==="compositionend"||!ao&&Hs(e,t)?(e=Vs(),Lr=oo=at=null,Kt=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=cu(n)}}function Xs(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Xs(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Gs(){for(var e=window,t=Ar();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Ar(e.document)}return t}function co(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Kf(e){var t=Gs(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Xs(n.ownerDocument.documentElement,n)){if(r!==null&&co(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,i=Math.min(r.start,l);r=r.end===void 0?i:Math.min(r.end,l),!e.extend&&i>r&&(l=r,r=i,i=l),l=fu(n,i);var o=fu(n,r);l&&o&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==o.node||e.focusOffset!==o.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(o.node,o.offset)):(t.setEnd(o.node,o.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Yt=null,gi=null,Un=null,wi=!1;function du(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;wi||Yt==null||Yt!==Ar(r)||(r=Yt,"selectionStart"in r&&co(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Un&&Zn(Un,r)||(Un=r,r=Yr(gi,"onSelect"),0Zt||(e.current=Ni[Zt],Ni[Zt]=null,Zt--)}function I(e,t){Zt++,Ni[Zt]=e.current,e.current=t}var xt={},se=kt(xt),ve=kt(!1),Mt=xt;function cn(e,t){var n=e.type.contextTypes;if(!n)return xt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function ye(e){return e=e.childContextTypes,e!=null}function Gr(){$(ve),$(se)}function wu(e,t,n){if(se.current!==xt)throw Error(g(168));I(se,t),I(ve,n)}function la(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(g(108,Ac(e)||"Unknown",l));return Q({},n,r)}function Zr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||xt,Mt=se.current,I(se,e),I(ve,ve.current),!0}function xu(e,t,n){var r=e.stateNode;if(!r)throw Error(g(169));n?(e=la(e,t,Mt),r.__reactInternalMemoizedMergedChildContext=e,$(ve),$(se),I(se,e)):$(ve),I(ve,n)}var Ye=null,vl=!1,Hl=!1;function ia(e){Ye===null?Ye=[e]:Ye.push(e)}function ld(e){vl=!0,ia(e)}function Ct(){if(!Hl&&Ye!==null){Hl=!0;var e=0,t=F;try{var n=Ye;for(F=1;e>=o,l-=o,Xe=1<<32-Fe(t)+l|n<P?(A=j,j=null):A=j.sibling;var T=v(f,j,p[P],y);if(T===null){j===null&&(j=A);break}e&&j&&T.alternate===null&&t(f,j),a=i(T,a,P),_===null?E=T:_.sibling=T,_=T,j=A}if(P===p.length)return n(f,j),V&&_t(f,P),E;if(j===null){for(;PP?(A=j,j=null):A=j.sibling;var pe=v(f,j,T.value,y);if(pe===null){j===null&&(j=A);break}e&&j&&pe.alternate===null&&t(f,j),a=i(pe,a,P),_===null?E=pe:_.sibling=pe,_=pe,j=A}if(T.done)return n(f,j),V&&_t(f,P),E;if(j===null){for(;!T.done;P++,T=p.next())T=h(f,T.value,y),T!==null&&(a=i(T,a,P),_===null?E=T:_.sibling=T,_=T);return V&&_t(f,P),E}for(j=r(f,j);!T.done;P++,T=p.next())T=x(j,f,P,T.value,y),T!==null&&(e&&T.alternate!==null&&j.delete(T.key===null?P:T.key),a=i(T,a,P),_===null?E=T:_.sibling=T,_=T);return e&&j.forEach(function(nt){return t(f,nt)}),V&&_t(f,P),E}function D(f,a,p,y){if(typeof p=="object"&&p!==null&&p.type===Qt&&p.key===null&&(p=p.props.children),typeof p=="object"&&p!==null){switch(p.$$typeof){case dr:e:{for(var E=p.key,_=a;_!==null;){if(_.key===E){if(E=p.type,E===Qt){if(_.tag===7){n(f,_.sibling),a=l(_,p.props.children),a.return=f,f=a;break e}}else if(_.elementType===E||typeof E=="object"&&E!==null&&E.$$typeof===it&&Cu(E)===_.type){n(f,_.sibling),a=l(_,p.props),a.ref=jn(f,_,p),a.return=f,f=a;break e}n(f,_);break}else t(f,_);_=_.sibling}p.type===Qt?(a=Rt(p.props.children,f.mode,y,p.key),a.return=f,f=a):(y=$r(p.type,p.key,p.props,null,f.mode,y),y.ref=jn(f,a,p),y.return=f,f=y)}return o(f);case Wt:e:{for(_=p.key;a!==null;){if(a.key===_)if(a.tag===4&&a.stateNode.containerInfo===p.containerInfo&&a.stateNode.implementation===p.implementation){n(f,a.sibling),a=l(a,p.children||[]),a.return=f,f=a;break e}else{n(f,a);break}else t(f,a);a=a.sibling}a=Jl(p,f.mode,y),a.return=f,f=a}return o(f);case it:return _=p._init,D(f,a,_(p._payload),y)}if(Ln(p))return S(f,a,p,y);if(kn(p))return k(f,a,p,y);Cr(f,p)}return typeof p=="string"&&p!==""||typeof p=="number"?(p=""+p,a!==null&&a.tag===6?(n(f,a.sibling),a=l(a,p),a.return=f,f=a):(n(f,a),a=Zl(p,f.mode,y),a.return=f,f=a),o(f)):n(f,a)}return D}var dn=aa(!0),ca=aa(!1),br=kt(null),el=null,bt=null,ho=null;function vo(){ho=bt=el=null}function yo(e){var t=br.current;$(br),e._currentValue=t}function Pi(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function un(e,t){el=e,ho=bt=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(he=!0),e.firstContext=null)}function Te(e){var t=e._currentValue;if(ho!==e)if(e={context:e,memoizedValue:t,next:null},bt===null){if(el===null)throw Error(g(308));bt=e,el.dependencies={lanes:0,firstContext:e}}else bt=bt.next=e;return t}var zt=null;function go(e){zt===null?zt=[e]:zt.push(e)}function fa(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,go(t)):(n.next=l.next,l.next=n),t.interleaved=n,be(e,r)}function be(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var ot=!1;function wo(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function da(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Ze(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function ht(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,O&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,be(e,n)}return l=r.interleaved,l===null?(t.next=t,go(r)):(t.next=l.next,l.next=t),r.interleaved=t,be(e,n)}function Mr(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ro(e,n)}}function Eu(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var o={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=o:i=i.next=o,n=n.next}while(n!==null);i===null?l=i=t:i=i.next=t}else l=i=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function tl(e,t,n,r){var l=e.updateQueue;ot=!1;var i=l.firstBaseUpdate,o=l.lastBaseUpdate,u=l.shared.pending;if(u!==null){l.shared.pending=null;var s=u,d=s.next;s.next=null,o===null?i=d:o.next=d,o=s;var m=e.alternate;m!==null&&(m=m.updateQueue,u=m.lastBaseUpdate,u!==o&&(u===null?m.firstBaseUpdate=d:u.next=d,m.lastBaseUpdate=s))}if(i!==null){var h=l.baseState;o=0,m=d=s=null,u=i;do{var v=u.lane,x=u.eventTime;if((r&v)===v){m!==null&&(m=m.next={eventTime:x,lane:0,tag:u.tag,payload:u.payload,callback:u.callback,next:null});e:{var S=e,k=u;switch(v=t,x=n,k.tag){case 1:if(S=k.payload,typeof S=="function"){h=S.call(x,h,v);break e}h=S;break e;case 3:S.flags=S.flags&-65537|128;case 0:if(S=k.payload,v=typeof S=="function"?S.call(x,h,v):S,v==null)break e;h=Q({},h,v);break e;case 2:ot=!0}}u.callback!==null&&u.lane!==0&&(e.flags|=64,v=l.effects,v===null?l.effects=[u]:v.push(u))}else x={eventTime:x,lane:v,tag:u.tag,payload:u.payload,callback:u.callback,next:null},m===null?(d=m=x,s=h):m=m.next=x,o|=v;if(u=u.next,u===null){if(u=l.shared.pending,u===null)break;v=u,u=v.next,v.next=null,l.lastBaseUpdate=v,l.shared.pending=null}}while(!0);if(m===null&&(s=h),l.baseState=s,l.firstBaseUpdate=d,l.lastBaseUpdate=m,t=l.shared.interleaved,t!==null){l=t;do o|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);Ft|=o,e.lanes=o,e.memoizedState=h}}function Nu(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Ql.transition;Ql.transition={};try{e(!1),t()}finally{F=n,Ql.transition=r}}function za(){return Le().memoizedState}function sd(e,t,n){var r=yt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Ta(e))La(t,n);else if(n=fa(e,t,n,r),n!==null){var l=ce();Ie(n,e,r,l),Ra(n,t,r)}}function ad(e,t,n){var r=yt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ta(e))La(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var o=t.lastRenderedState,u=i(o,n);if(l.hasEagerState=!0,l.eagerState=u,Ue(u,o)){var s=t.interleaved;s===null?(l.next=l,go(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=fa(e,t,l,r),n!==null&&(l=ce(),Ie(n,e,r,l),Ra(n,t,r))}}function Ta(e){var t=e.alternate;return e===W||t!==null&&t===W}function La(e,t){$n=rl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Ra(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ro(e,n)}}var ll={readContext:Te,useCallback:ie,useContext:ie,useEffect:ie,useImperativeHandle:ie,useInsertionEffect:ie,useLayoutEffect:ie,useMemo:ie,useReducer:ie,useRef:ie,useState:ie,useDebugValue:ie,useDeferredValue:ie,useTransition:ie,useMutableSource:ie,useSyncExternalStore:ie,useId:ie,unstable_isNewReconciler:!1},cd={readContext:Te,useCallback:function(e,t){return Be().memoizedState=[e,t===void 0?null:t],e},useContext:Te,useEffect:ju,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Dr(4194308,4,Ea.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Dr(4194308,4,e,t)},useInsertionEffect:function(e,t){return Dr(4,2,e,t)},useMemo:function(e,t){var n=Be();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Be();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=sd.bind(null,W,e),[r.memoizedState,e]},useRef:function(e){var t=Be();return e={current:e},t.memoizedState=e},useState:_u,useDebugValue:jo,useDeferredValue:function(e){return Be().memoizedState=e},useTransition:function(){var e=_u(!1),t=e[0];return e=ud.bind(null,e[1]),Be().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=W,l=Be();if(V){if(n===void 0)throw Error(g(407));n=n()}else{if(n=t(),b===null)throw Error(g(349));Dt&30||va(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,ju(ga.bind(null,r,i,e),[e]),r.flags|=2048,lr(9,ya.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=Be(),t=b.identifierPrefix;if(V){var n=Ge,r=Xe;n=(r&~(1<<32-Fe(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=nr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=o.createElement(n,{is:r.is}):(e=o.createElement(n),n==="select"&&(o=e,r.multiple?o.multiple=!0:r.size&&(o.size=r.size))):e=o.createElementNS(e,n),e[He]=t,e[bn]=r,Ba(e,t,!1,!1),t.stateNode=e;e:{switch(o=ci(n,r),n){case"dialog":U("cancel",e),U("close",e),l=r;break;case"iframe":case"object":case"embed":U("load",e),l=r;break;case"video":case"audio":for(l=0;lhn&&(t.flags|=128,r=!0,Pn(i,!1),t.lanes=4194304)}else{if(!r)if(e=nl(o),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Pn(i,!0),i.tail===null&&i.tailMode==="hidden"&&!o.alternate&&!V)return oe(t),null}else 2*Y()-i.renderingStartTime>hn&&n!==1073741824&&(t.flags|=128,r=!0,Pn(i,!1),t.lanes=4194304);i.isBackwards?(o.sibling=t.child,t.child=o):(n=i.last,n!==null?n.sibling=o:t.child=o,i.last=o)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=Y(),t.sibling=null,n=H.current,I(H,r?n&1|2:n&1),t):(oe(t),null);case 22:case 23:return Mo(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?xe&1073741824&&(oe(t),t.subtreeFlags&6&&(t.flags|=8192)):oe(t),null;case 24:return null;case 25:return null}throw Error(g(156,t.tag))}function gd(e,t){switch(po(t),t.tag){case 1:return ye(t.type)&&Gr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return pn(),$(ve),$(se),ko(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return So(t),null;case 13:if($(H),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(g(340));fn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return $(H),null;case 4:return pn(),null;case 10:return yo(t.type._context),null;case 22:case 23:return Mo(),null;case 24:return null;default:return null}}var Nr=!1,ue=!1,wd=typeof WeakSet=="function"?WeakSet:Set,N=null;function en(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){K(e,t,r)}else n.current=null}function Ii(e,t,n){try{n()}catch(r){K(e,t,r)}}var Uu=!1;function xd(e,t){if(xi=Qr,e=Gs(),co(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var o=0,u=-1,s=-1,d=0,m=0,h=e,v=null;t:for(;;){for(var x;h!==n||l!==0&&h.nodeType!==3||(u=o+l),h!==i||r!==0&&h.nodeType!==3||(s=o+r),h.nodeType===3&&(o+=h.nodeValue.length),(x=h.firstChild)!==null;)v=h,h=x;for(;;){if(h===e)break t;if(v===n&&++d===l&&(u=o),v===i&&++m===r&&(s=o),(x=h.nextSibling)!==null)break;h=v,v=h.parentNode}h=x}n=u===-1||s===-1?null:{start:u,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(Si={focusedElem:e,selectionRange:n},Qr=!1,N=t;N!==null;)if(t=N,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,N=e;else for(;N!==null;){t=N;try{var S=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(S!==null){var k=S.memoizedProps,D=S.memoizedState,f=t.stateNode,a=f.getSnapshotBeforeUpdate(t.elementType===t.type?k:Me(t.type,k),D);f.__reactInternalSnapshotBeforeUpdate=a}break;case 3:var p=t.stateNode.containerInfo;p.nodeType===1?p.textContent="":p.nodeType===9&&p.documentElement&&p.removeChild(p.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(g(163))}}catch(y){K(t,t.return,y)}if(e=t.sibling,e!==null){e.return=t.return,N=e;break}N=t.return}return S=Uu,Uu=!1,S}function An(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var i=l.destroy;l.destroy=void 0,i!==void 0&&Ii(t,n,i)}l=l.next}while(l!==r)}}function wl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Ui(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Qa(e){var t=e.alternate;t!==null&&(e.alternate=null,Qa(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[He],delete t[bn],delete t[Ei],delete t[nd],delete t[rd])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Ka(e){return e.tag===5||e.tag===3||e.tag===4}function $u(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Ka(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function $i(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Xr));else if(r!==4&&(e=e.child,e!==null))for($i(e,t,n),e=e.sibling;e!==null;)$i(e,t,n),e=e.sibling}function Ai(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Ai(e,t,n),e=e.sibling;e!==null;)Ai(e,t,n),e=e.sibling}var te=null,Oe=!1;function lt(e,t,n){for(n=n.child;n!==null;)Ya(e,t,n),n=n.sibling}function Ya(e,t,n){if(We&&typeof We.onCommitFiberUnmount=="function")try{We.onCommitFiberUnmount(fl,n)}catch{}switch(n.tag){case 5:ue||en(n,t);case 6:var r=te,l=Oe;te=null,lt(e,t,n),te=r,Oe=l,te!==null&&(Oe?(e=te,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):te.removeChild(n.stateNode));break;case 18:te!==null&&(Oe?(e=te,n=n.stateNode,e.nodeType===8?Bl(e.parentNode,n):e.nodeType===1&&Bl(e,n),Xn(e)):Bl(te,n.stateNode));break;case 4:r=te,l=Oe,te=n.stateNode.containerInfo,Oe=!0,lt(e,t,n),te=r,Oe=l;break;case 0:case 11:case 14:case 15:if(!ue&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,o=i.destroy;i=i.tag,o!==void 0&&(i&2||i&4)&&Ii(n,t,o),l=l.next}while(l!==r)}lt(e,t,n);break;case 1:if(!ue&&(en(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){K(n,t,u)}lt(e,t,n);break;case 21:lt(e,t,n);break;case 22:n.mode&1?(ue=(r=ue)||n.memoizedState!==null,lt(e,t,n),ue=r):lt(e,t,n);break;default:lt(e,t,n)}}function Au(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new wd),t.forEach(function(r){var l=zd.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Re(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=o),r&=~i}if(r=l,r=Y()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*kd(r/1960))-r,10e?16:e,ct===null)var r=!1;else{if(e=ct,ct=null,ul=0,O&6)throw Error(g(331));var l=O;for(O|=4,N=e.current;N!==null;){var i=N,o=i.child;if(N.flags&16){var u=i.deletions;if(u!==null){for(var s=0;sY()-Lo?Lt(e,0):To|=n),ge(e,t)}function tc(e,t){t===0&&(e.mode&1?(t=vr,vr<<=1,!(vr&130023424)&&(vr=4194304)):t=1);var n=ce();e=be(e,t),e!==null&&(ur(e,t,n),ge(e,n))}function Pd(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),tc(e,n)}function zd(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(g(314))}r!==null&&r.delete(t),tc(e,n)}var nc;nc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ve.current)he=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return he=!1,vd(e,t,n);he=!!(e.flags&131072)}else he=!1,V&&t.flags&1048576&&oa(t,qr,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Fr(e,t),e=t.pendingProps;var l=cn(t,se.current);un(t,n),l=Eo(null,t,r,e,l,n);var i=No();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,ye(r)?(i=!0,Zr(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,wo(t),l.updater=gl,t.stateNode=l,l._reactInternals=t,Ti(t,r,e,n),t=Mi(null,t,r,!0,i,n)):(t.tag=0,V&&i&&fo(t),ae(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Fr(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Ld(r),e=Me(r,e),l){case 0:t=Ri(null,t,r,e,n);break e;case 1:t=Du(null,t,r,e,n);break e;case 11:t=Mu(null,t,r,e,n);break e;case 14:t=Ou(null,t,r,Me(r.type,e),n);break e}throw Error(g(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Me(r,l),Ri(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Me(r,l),Du(e,t,r,l,n);case 3:e:{if($a(t),e===null)throw Error(g(387));r=t.pendingProps,i=t.memoizedState,l=i.element,da(e,t),tl(t,r,null,n);var o=t.memoizedState;if(r=o.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:o.cache,pendingSuspenseBoundaries:o.pendingSuspenseBoundaries,transitions:o.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=mn(Error(g(423)),t),t=Fu(e,t,r,n,l);break e}else if(r!==l){l=mn(Error(g(424)),t),t=Fu(e,t,r,n,l);break e}else for(Se=mt(t.stateNode.containerInfo.firstChild),ke=t,V=!0,De=null,n=ca(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(fn(),r===l){t=et(e,t,n);break e}ae(e,t,r,n)}t=t.child}return t;case 5:return pa(t),e===null&&ji(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,o=l.children,ki(r,l)?o=null:i!==null&&ki(r,i)&&(t.flags|=32),Ua(e,t),ae(e,t,o,n),t.child;case 6:return e===null&&ji(t),null;case 13:return Aa(e,t,n);case 4:return xo(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=dn(t,null,r,n):ae(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Me(r,l),Mu(e,t,r,l,n);case 7:return ae(e,t,t.pendingProps,n),t.child;case 8:return ae(e,t,t.pendingProps.children,n),t.child;case 12:return ae(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,o=l.value,I(br,r._currentValue),r._currentValue=o,i!==null)if(Ue(i.value,o)){if(i.children===l.children&&!ve.current){t=et(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var u=i.dependencies;if(u!==null){o=i.child;for(var s=u.firstContext;s!==null;){if(s.context===r){if(i.tag===1){s=Ze(-1,n&-n),s.tag=2;var d=i.updateQueue;if(d!==null){d=d.shared;var m=d.pending;m===null?s.next=s:(s.next=m.next,m.next=s),d.pending=s}}i.lanes|=n,s=i.alternate,s!==null&&(s.lanes|=n),Pi(i.return,n,t),u.lanes|=n;break}s=s.next}}else if(i.tag===10)o=i.type===t.type?null:i.child;else if(i.tag===18){if(o=i.return,o===null)throw Error(g(341));o.lanes|=n,u=o.alternate,u!==null&&(u.lanes|=n),Pi(o,n,t),o=i.sibling}else o=i.child;if(o!==null)o.return=i;else for(o=i;o!==null;){if(o===t){o=null;break}if(i=o.sibling,i!==null){i.return=o.return,o=i;break}o=o.return}i=o}ae(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,un(t,n),l=Te(l),r=r(l),t.flags|=1,ae(e,t,r,n),t.child;case 14:return r=t.type,l=Me(r,t.pendingProps),l=Me(r.type,l),Ou(e,t,r,l,n);case 15:return Fa(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Me(r,l),Fr(e,t),t.tag=1,ye(r)?(e=!0,Zr(t)):e=!1,un(t,n),Ma(t,r,l),Ti(t,r,l,n),Mi(null,t,r,!0,e,n);case 19:return Va(e,t,n);case 22:return Ia(e,t,n)}throw Error(g(156,t.tag))};function rc(e,t){return Ts(e,t)}function Td(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Pe(e,t,n,r){return new Td(e,t,n,r)}function Do(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Ld(e){if(typeof e=="function")return Do(e)?1:0;if(e!=null){if(e=e.$$typeof,e===bi)return 11;if(e===eo)return 14}return 2}function gt(e,t){var n=e.alternate;return n===null?(n=Pe(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function $r(e,t,n,r,l,i){var o=2;if(r=e,typeof e=="function")Do(e)&&(o=1);else if(typeof e=="string")o=5;else e:switch(e){case Qt:return Rt(n.children,l,i,t);case qi:o=8,l|=8;break;case ei:return e=Pe(12,n,t,l|2),e.elementType=ei,e.lanes=i,e;case ti:return e=Pe(13,n,t,l),e.elementType=ti,e.lanes=i,e;case ni:return e=Pe(19,n,t,l),e.elementType=ni,e.lanes=i,e;case ps:return Sl(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case fs:o=10;break e;case ds:o=9;break e;case bi:o=11;break e;case eo:o=14;break e;case it:o=16,r=null;break e}throw Error(g(130,e==null?e:typeof e,""))}return t=Pe(o,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function Rt(e,t,n,r){return e=Pe(7,e,r,t),e.lanes=n,e}function Sl(e,t,n,r){return e=Pe(22,e,r,t),e.elementType=ps,e.lanes=n,e.stateNode={isHidden:!1},e}function Zl(e,t,n){return e=Pe(6,e,null,t),e.lanes=n,e}function Jl(e,t,n){return t=Pe(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Rd(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Ll(0),this.expirationTimes=Ll(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Ll(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Fo(e,t,n,r,l,i,o,u,s){return e=new Rd(e,t,n,u,s),t===1?(t=1,i===!0&&(t|=8)):t=0,i=Pe(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},wo(i),e}function Md(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(uc)}catch(e){console.error(e)}}uc(),us.exports=Ee;var Ud=us.exports,sc,Xu=Ud;sc=Xu.createRoot,Xu.hydrateRoot;async function we(e,t={}){const n=await fetch(e,t);if(!n.ok){let r=n.statusText;try{r=(await n.json()).detail||r}catch{}throw new Error(r)}return n.json()}const ql=(e,t)=>({method:e,headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),ne={health:()=>we("/api/health"),models:()=>we("/api/models"),languages:()=>we("/api/languages"),listProjects:()=>we("/api/projects"),createProject:e=>we("/api/projects",ql("POST",e)),listCorpora:e=>we(`/api/projects/${e}/corpora`),uploadCorpus:(e,t)=>{const n=new FormData;return n.append("file",t),we(`/api/projects/${e}/corpora`,{method:"POST",body:n})},listConstructs:()=>we("/api/constructs"),createConstruct:e=>we("/api/constructs",ql("POST",e)),createJob:e=>we("/api/jobs",ql("POST",e)),listJobs:e=>we(`/api/jobs?project_id=${e}`),getJob:e=>we(`/api/jobs/${e}`),jobResults:e=>we(`/api/jobs/${e}/results`),exportUrl:e=>`/api/jobs/${e}/export`,metadataUrl:e=>`/api/jobs/${e}/metadata`,scriptUrl:e=>`/api/jobs/${e}/script`,scriptRequirementsUrl:e=>`/api/jobs/${e}/script-requirements`};function $d({jobId:e,onBack:t}){var d;const[n,r]=R.useState(null),[l,i]=R.useState("");if(R.useEffect(()=>{ne.jobResults(e).then(r).catch(m=>i(m.message))},[e]),l)return c.jsxs("div",{className:"card",children:[c.jsx("div",{className:"error-banner",children:l}),c.jsx("button",{className:"ghost",onClick:t,children:"← Back"})]});if(!n)return c.jsx("div",{className:"card",children:"Loading results…"});const{summary:o,metadata:u}=n,s=Math.max(...o.item_means.map(m=>Math.abs(m.mean)),1e-9);return c.jsxs(c.Fragment,{children:[c.jsxs("div",{className:"results-toolbar",children:[c.jsx("button",{className:"ghost",onClick:t,children:"← Back to workspace"}),c.jsxs("div",{className:"row result-actions",children:[c.jsx("a",{href:ne.exportUrl(e),children:c.jsx("button",{className:"primary",children:"Export results CSV"})}),c.jsx("a",{href:ne.scriptUrl(e),children:c.jsx("button",{className:"ghost",children:"Python script"})}),c.jsx("a",{href:ne.scriptRequirementsUrl(e),children:c.jsx("button",{className:"ghost",children:"requirements.txt"})}),c.jsx("a",{href:ne.metadataUrl(e),children:c.jsx("button",{className:"ghost",children:"Run metadata (JSON)"})})]})]}),c.jsxs("div",{className:"card",children:[c.jsxs("h3",{children:[u.construct," × ",u.corpus_file]}),c.jsx("p",{className:"hint",children:"CCR score = mean cosine similarity between each text and the construct's scale items. Higher = the text expresses the construct more strongly."}),c.jsxs("div",{className:"stat-grid",children:[c.jsx(Ht,{k:"Texts scored",v:o.n_docs.toLocaleString()}),c.jsx(Ht,{k:"Mean score",v:o.score_mean.toFixed(3)}),c.jsx(Ht,{k:"SD",v:o.score_sd.toFixed(3)}),c.jsx(Ht,{k:"Min",v:o.score_min.toFixed(3)}),c.jsx(Ht,{k:"Max",v:o.score_max.toFixed(3)}),o.n_dropped_empty>0&&c.jsx(Ht,{k:"Empty rows dropped",v:o.n_dropped_empty})]}),((d=o.warnings)==null?void 0:d.length)>0&&c.jsxs("div",{className:"warnings mt",children:[c.jsx("strong",{className:"small",children:"Data-quality notes"}),c.jsx("ul",{className:"small",style:{margin:"4px 0 0",paddingLeft:20},children:o.warnings.map((m,h)=>c.jsx("li",{children:typeof m=="string"?m:c.jsxs(c.Fragment,{children:[c.jsx("code",{style:{fontSize:11},children:m.code})," — ",m.message]})},h))})]})]}),c.jsxs("div",{className:"card",children:[c.jsx("h3",{children:"Score distribution"}),c.jsx(Ad,{histogram:o.histogram})]}),c.jsxs("div",{className:"card",children:[c.jsx("h3",{children:"Per-item mean loadings"}),c.jsx("p",{className:"hint",children:"Mean similarity of the corpus to each scale item — a face-validity check on which items drive the construct signal."}),o.item_means.map((m,h)=>c.jsxs("div",{className:"item-bar-row",children:[c.jsx("span",{className:"item-bar-label",title:m.item,children:m.item.length>80?m.item.slice(0,80)+"…":m.item}),c.jsx("div",{className:"item-bar-track",children:c.jsx("div",{className:"item-bar-fill",style:{width:`${Math.max(2,Math.abs(m.mean)/s*100)}%`}})}),c.jsx("span",{className:"item-bar-val",children:m.mean.toFixed(3)})]},h))]}),c.jsxs("div",{className:"row",children:[c.jsxs("div",{className:"grow card",children:[c.jsx("h3",{children:"Highest-scoring texts"}),c.jsx(Gu,{docs:o.top_docs})]}),c.jsxs("div",{className:"grow card",children:[c.jsx("h3",{children:"Lowest-scoring texts"}),c.jsx(Gu,{docs:o.bottom_docs})]})]}),c.jsxs("div",{className:"meta-footer",children:[c.jsx("strong",{children:"Reproducibility record"})," — model: ",c.jsx("code",{children:u.model})," (dim"," ",u.embedding_dim,") · items hash: ",c.jsx("code",{children:u.items_sha256_16})," · text column: ",c.jsx("code",{children:u.text_column})," · run:"," ",u.started_at," → ",u.finished_at," (",u.duration_seconds,"s) · numpy ",u.numpy,u.sentence_transformers&&` · sentence-transformers ${u.sentence_transformers}`,c.jsxs("div",{className:"mt small",children:["Construct reference: ",u.construct_reference||"—"]})]})]})}function Ht({k:e,v:t}){return c.jsxs("div",{className:"stat",children:[c.jsx("div",{className:"v",children:t}),c.jsx("div",{className:"k",children:e})]})}function Gu({docs:e}){return c.jsx("div",{className:"table-wrap",children:c.jsxs("table",{className:"docs",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{style:{width:60},children:"Score"}),c.jsx("th",{children:"Text"})]})}),c.jsx("tbody",{children:e.map(t=>c.jsxs("tr",{children:[c.jsx("td",{className:"score",children:t.score.toFixed(3)}),c.jsx("td",{children:t.text})]},t.row))})]})})}function Ad({histogram:e}){const{counts:t,edges:n}=e,r=640,l=180,i={top:10,right:10,bottom:26,left:34},o=r-i.left-i.right,u=l-i.top-i.bottom,s=Math.max(...t,1),d=o/t.length;return c.jsxs("svg",{viewBox:`0 0 ${r} ${l}`,style:{width:"100%",maxWidth:720},children:[[.25,.5,.75,1].map(m=>{const h=i.top+u-m*u;return c.jsxs("g",{children:[c.jsx("line",{x1:i.left,x2:r-i.right,y1:h,y2:h,stroke:"#eceef1"}),c.jsx("text",{x:i.left-6,y:h+4,fontSize:"10",fill:"#98a2b3",textAnchor:"end",children:Math.round(m*s)})]},m)}),t.map((m,h)=>{const v=m/s*u;return c.jsx("rect",{x:i.left+h*d+1.5,y:i.top+u-v,width:Math.max(1,d-3),height:v,rx:"2",fill:"#7a1f3d",opacity:"0.85",children:c.jsxs("title",{children:[n[h].toFixed(3)," – ",n[h+1].toFixed(3),": ",m]})},h)}),[0,Math.floor(t.length/2),t.length].map(m=>c.jsx("text",{x:i.left+m*d,y:l-8,fontSize:"10",fill:"#98a2b3",textAnchor:"middle",children:n[m].toFixed(2)},m)),c.jsx("line",{x1:i.left,x2:r-i.right,y1:i.top+u,y2:i.top+u,stroke:"#d0d5dd"})]})}function Vd({project:e}){var Et,Ae,Vt;const[t,n]=R.useState([]),[r,l]=R.useState([]),[i,o]=R.useState([]),[u,s]=R.useState([]),[d,m]=R.useState(""),[h,v]=R.useState(""),[x,S]=R.useState(""),[k,D]=R.useState(""),[f,a]=R.useState(["en"]),[p,y]=R.useState("en"),[E,_]=R.useState(!1),[j,P]=R.useState(!1),[A,T]=R.useState(""),[pe,nt]=R.useState(!1),[rt,wn]=R.useState(null),xn=R.useRef(null),$e=R.useCallback(()=>ne.listJobs(e.id).then(s).catch(()=>{}),[e.id]);R.useEffect(()=>{ne.listCorpora(e.id).then(n).catch(w=>T(w.message)),ne.listConstructs().then(l).catch(w=>T(w.message)),ne.models().then(w=>{o(w);const ee=w.find(Nt=>Nt.default)||w[0];ee&&D(ee.id)}).catch(w=>T(w.message)),ne.languages().then(a).catch(()=>{}),$e()},[e.id,$e]);const At=u.some(w=>w.status==="queued"||w.status==="running");R.useEffect(()=>{if(!At)return;const w=setInterval($e,1200);return()=>clearInterval(w)},[At,$e]);const C=t.find(w=>w.id===d)||null,z=r.find(w=>w.id===x)||null;async function L(w){var Nt;const ee=(Nt=w.target.files)==null?void 0:Nt[0];if(ee){_(!0),T("");try{const Sn=await ne.uploadCorpus(e.id,ee),ac=await ne.listCorpora(e.id);n(ac),m(Sn.id),v(Sn.suggested_text_column||Sn.columns[0])}catch(Sn){T(Sn.message)}finally{_(!1),xn.current&&(xn.current.value="")}}}async function B(){P(!0),T("");try{await ne.createJob({project_id:e.id,corpus_id:d,construct_id:x,text_column:h,model_name:k,language:p}),await $e()}catch(w){T(w.message)}finally{P(!1)}}if(rt)return c.jsx($d,{jobId:rt,onBack:()=>{wn(null),$e()}});const X=d&&h&&x&&k&&!j;return c.jsxs(c.Fragment,{children:[A&&c.jsx("div",{className:"error-banner",onClick:()=>T(""),children:A}),c.jsxs("div",{className:"card",children:[c.jsxs("h3",{children:[c.jsx("span",{className:"step-badge",children:"1"}),"Corpus"]}),c.jsx("p",{className:"hint",children:"Upload a CSV or XLSX file, then choose the column containing the text to analyze."}),c.jsxs("div",{className:"row",children:[c.jsxs("div",{className:"grow",children:[c.jsxs("label",{className:"field",children:["Upload file",c.jsx("input",{ref:xn,type:"file",accept:".csv,.xlsx,.xls",onChange:L,disabled:E})]}),E&&c.jsx("span",{className:"small muted",children:"Uploading…"})]}),c.jsx("div",{className:"grow",children:c.jsxs("label",{className:"field",children:["Corpus",c.jsxs("select",{value:d,onChange:w=>m(w.target.value),children:[c.jsx("option",{value:"",children:"— select —"}),t.map(w=>c.jsxs("option",{value:w.id,children:[w.filename," (",w.n_rows.toLocaleString()," rows)"]},w.id))]})]})}),c.jsx("div",{className:"grow",children:c.jsxs("label",{className:"field",children:["Text column",c.jsxs("select",{value:h,onChange:w=>v(w.target.value),disabled:!C,children:[c.jsx("option",{value:"",children:"— select —"}),C==null?void 0:C.columns.map(w=>c.jsxs("option",{value:w,children:[w,w===C.suggested_text_column?" (suggested)":""]},w))]})]})})]}),((Et=C==null?void 0:C.parse_info)==null?void 0:Et.note)&&c.jsxs("p",{className:"small muted",children:["⚠ ",C.parse_info.note]})]}),c.jsxs("div",{className:"card",children:[c.jsxs("h3",{children:[c.jsx("span",{className:"step-badge",children:"2"}),"Construct"]}),c.jsx("p",{className:"hint",children:"Pick a validated scale from the library, or define custom items. CCR scores each text by its similarity to these items."}),c.jsxs("div",{className:"construct-row",children:[c.jsx("div",{className:"grow",children:c.jsxs("select",{value:x,onChange:w=>S(w.target.value),children:[c.jsx("option",{value:"",children:"— select construct —"}),r.map(w=>c.jsxs("option",{value:w.id,children:[w.name," (",w.items.length," items",w.is_seed?", library":", custom",")"]},w.id))]})}),c.jsx("button",{className:"ghost",onClick:()=>nt(w=>!w),children:pe?"Close":"+ Custom construct"})]}),z&&c.jsxs(c.Fragment,{children:[c.jsx("ul",{className:"construct-items",children:z.items.map((w,ee)=>{var Nt;return c.jsxs("li",{children:[w,(Nt=z.reverse_scored)!=null&&Nt[ee]?" (reverse-scored)":""]},ee)})}),z.reference&&c.jsxs("p",{className:"small muted mt",children:["Reference: ",z.reference]}),z.verification_status!=="verified"&&c.jsxs("p",{className:"small muted",children:["⚠ Item wording not yet verified verbatim against the original publication (status: ",z.verification_status.replace("_"," "),")."]})]}),pe&&c.jsx(Bd,{onCreated:async w=>{const ee=await ne.listConstructs();l(ee),S(w.id),nt(!1)},onError:T})]}),c.jsxs("div",{className:"card",children:[c.jsxs("h3",{children:[c.jsx("span",{className:"step-badge",children:"3"}),"Language, model & run"]}),c.jsx("p",{className:"hint",children:"Embeddings run locally via sentence-transformers; model and language are recorded in the run metadata. If the corpus doesn't match the selected language or the model doesn't support it, you'll get a warning — never a silent result."}),c.jsxs("div",{className:"run-settings",children:[c.jsxs("label",{className:"field language-control",children:["Text language",c.jsx("select",{value:p,onChange:w=>y(w.target.value),children:f.map(w=>c.jsx("option",{value:w,children:w},w))})]}),c.jsxs("label",{className:"field model-control",children:["Embedding model",c.jsx("select",{value:k,onChange:w=>D(w.target.value),children:i.map(w=>c.jsx("option",{value:w.id,children:w.label},w.id))})]}),c.jsx("button",{className:"primary run-button",disabled:!X,onClick:B,children:j?"Starting…":"Run CCR analysis"})]}),(Vt=(Ae=i.find(w=>w.id===k))==null?void 0:Ae.warnings)==null?void 0:Vt.map((w,ee)=>c.jsxs("p",{className:"small muted",children:["⚠ ",w]},ee))]}),u.length>0&&c.jsxs("div",{className:"card",children:[c.jsx("h3",{children:"Runs"}),c.jsx("div",{className:"table-wrap",children:c.jsxs("table",{className:"docs",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{children:"Started"}),c.jsx("th",{children:"Corpus"}),c.jsx("th",{children:"Construct"}),c.jsx("th",{style:{width:"24%"},children:"Status"}),c.jsx("th",{})]})}),c.jsx("tbody",{children:u.map(w=>c.jsxs("tr",{children:[c.jsx("td",{className:"muted",children:(w.started_at||w.created_at).replace("T"," ").slice(0,16)}),c.jsx("td",{children:w.corpus_filename}),c.jsx("td",{children:w.construct_name}),c.jsxs("td",{children:[w.status==="running"?c.jsx("div",{className:"progress-track",title:`${Math.round(w.progress*100)}%`,children:c.jsx("div",{className:"progress-fill",style:{width:`${Math.max(3,w.progress*100)}%`}})}):c.jsx("span",{className:`pill ${w.status}`,children:w.status}),w.status==="failed"&&c.jsx("div",{className:"small muted",title:w.error,children:w.error.split(` -`).pop()})]}),c.jsx("td",{children:w.status==="completed"&&c.jsx("button",{className:"linkish",onClick:()=>wn(w.id),children:"View results"})})]},w.id))})]})})]})]})}function Bd({onCreated:e,onError:t}){const[n,r]=R.useState(""),[l,i]=R.useState(""),[o,u]=R.useState(""),[s,d]=R.useState(!1);async function m(h){h.preventDefault();const v=o.split(` -`).map(x=>x.trim()).filter(Boolean);if(!n.trim()||v.length===0){t("A custom construct needs a name and at least one item (one per line).");return}d(!0);try{const x=await ne.createConstruct({name:n.trim(),reference:l,items:v});e(x)}catch(x){t(x.message)}finally{d(!1)}}return c.jsxs("form",{onSubmit:m,className:"mt",children:[c.jsxs("div",{className:"row",children:[c.jsx("div",{className:"grow",children:c.jsxs("label",{className:"field",children:["Name",c.jsx("input",{type:"text",value:n,onChange:h=>r(h.target.value)})]})}),c.jsx("div",{className:"grow",children:c.jsxs("label",{className:"field",children:["Reference (publication, optional)",c.jsx("input",{type:"text",value:l,onChange:h=>i(h.target.value)})]})})]}),c.jsxs("label",{className:"field",children:["Scale items — one per line, verbatim from the validated instrument",c.jsx("textarea",{rows:5,value:o,onChange:h=>u(h.target.value)})]}),c.jsx("button",{className:"primary",type:"submit",disabled:s,children:s?"Saving…":"Save construct"})]})}function Hd(e){if(!e)return"";const t=new Date(e.endsWith("Z")||e.includes("+")?e:e+"Z"),n=Math.max(0,Math.floor((Date.now()-t.getTime())/6e4));if(n<1)return"just now";if(n<60)return`${n}m ago`;const r=Math.floor(n/60);if(r<24)return`${r}h ago`;const l=Math.floor(r/24);return l<7?`${l}d ago`:t.toISOString().slice(0,10)}function Wd(e){const t=Date.now(),n=864e5,r={Today:[],"This week":[],Earlier:[]};for(const l of e){const i=l.last_activity_at||l.created_at,o=new Date(i.endsWith("Z")||i.includes("+")?i:i+"Z").getTime(),u=t-o;ul.length>0)}function Qd(){const[e,t]=R.useState([]),[n,r]=R.useState(null),[l,i]=R.useState(!1),[o,u]=R.useState(""),[s,d]=R.useState(""),[m,h]=R.useState(""),v=()=>ne.listProjects().then(t).catch(f=>h(f.message));R.useEffect(()=>{v()},[]),R.useEffect(()=>{if(e.length===0){r(null);return}(!n||!e.some(f=>f.id===n))&&r(e[0].id)},[e,n]);async function x(f){if(f.preventDefault(),!!o.trim())try{const a=await ne.createProject({name:o.trim()});u(""),i(!1),await v(),r(a.id)}catch(a){h(a.message)}}const S=e.find(f=>f.id===n)||null,k=s.trim().toLowerCase(),D=e.filter(f=>f.name.toLowerCase().includes(k));return c.jsxs("div",{className:"app",children:[c.jsxs("header",{className:"header",children:[c.jsx("h1",{children:"CCR Platform"}),c.jsx("span",{className:"sub",children:"Contextualized Construct Representations · theory-driven psychological text analysis"})]}),c.jsxs("div",{className:"layout",children:[c.jsxs("aside",{className:"sidebar",children:[c.jsxs("h2",{children:["Projects",e.length>0&&c.jsx("span",{className:"count",children:e.length})]}),c.jsx("input",{type:"text",className:"sidebar-filter",placeholder:"Search projects...",value:s,onChange:f=>d(f.target.value)}),c.jsxs("div",{className:"project-list",children:[Wd(D).map(([f,a])=>c.jsxs("div",{children:[c.jsx("div",{className:"group-label",children:f}),a.map(p=>c.jsxs("button",{className:"project-item"+(p.id===n?" active":""),onClick:()=>r(p.id),title:p.name,children:[c.jsx("span",{className:"project-name",children:p.name}),c.jsxs("span",{className:"date",children:[p.n_runs>0?`${p.n_runs} run${p.n_runs===1?"":"s"} · `:"",Hd(p.last_activity_at||p.created_at)]})]},p.id))]},f)),s&&D.length===0&&c.jsxs("p",{className:"small muted",children:['No projects match "',s,'".']})]}),c.jsx("div",{className:"project-create",children:l?c.jsxs("form",{onSubmit:x,children:[c.jsx("input",{type:"text",autoFocus:!0,placeholder:"Project name",value:o,onChange:f=>u(f.target.value)}),c.jsxs("div",{className:"row mt",children:[c.jsx("button",{className:"primary",type:"submit",children:"Create"}),c.jsx("button",{className:"ghost",type:"button",onClick:()=>i(!1),children:"Cancel"})]})]}):c.jsx("button",{className:"ghost",onClick:()=>i(!0),children:"+ New project"})})]}),c.jsxs("main",{className:"main",children:[m&&c.jsx("div",{className:"error-banner",onClick:()=>h(""),children:m}),S?c.jsx(Vd,{project:S},S.id):c.jsxs("div",{className:"card",children:[c.jsx("h3",{children:"Welcome"}),c.jsx("p",{className:"hint",children:"Create or select a project, upload a corpus (CSV/XLSX), choose a validated construct, and run a CCR analysis. Results include per-item loadings, score distributions, and a reproducibility record for every run."}),c.jsx("p",{className:"small muted",children:"Self-contained by design: embeddings run on this server itself — no third-party AI APIs. Demo instance: storage is ephemeral and may reset; please don't upload sensitive or identifiable data."})]})]})]})]})}sc(document.getElementById("root")).render(c.jsx(_c.StrictMode,{children:c.jsx(Qd,{})})); diff --git a/backend/static/assets/index-BDrO2FRr.css b/backend/static/assets/index-Cu39pU_-.css similarity index 76% rename from backend/static/assets/index-BDrO2FRr.css rename to backend/static/assets/index-Cu39pU_-.css index d586ce1b697bbfe3d77282b80f5f47a1b6c42aaa..d0ef77a94752d7f386d3f2975d35aeb8a0ef57ab 100644 --- a/backend/static/assets/index-BDrO2FRr.css +++ b/backend/static/assets/index-Cu39pU_-.css @@ -1 +1 @@ -:root{--maroon: #7a1f3d;--maroon-dark: #5e1730;--ink: #1d2129;--muted: #667085;--line: #e5e7eb;--bg: #f7f7f8;--card: #ffffff;--ok: #157f3d;--err: #b42318;--accent-soft: #f6ebef;--control-height: 40px}*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;color:var(--ink);background:var(--bg);font-size:14.5px;line-height:1.5}.app{display:flex;flex-direction:column;min-height:100vh}.header{background:var(--maroon);color:#fff;padding:14px 28px;display:flex;align-items:baseline;flex-wrap:wrap;gap:14px}.header h1{flex:0 0 auto;font-size:17px;margin:0;font-weight:650;letter-spacing:.2px;white-space:nowrap}.header .sub{flex:1 1 280px;min-width:0;font-size:12.5px;opacity:.85}.layout{display:flex;flex:1;min-width:0;min-height:0}.sidebar{width:250px;background:var(--card);border-right:1px solid var(--line);padding:18px 14px;flex-shrink:0;display:flex;flex-direction:column;min-height:0}.sidebar h2{font-size:11.5px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);margin:0 0 10px 4px;display:flex;align-items:center;gap:8px}.sidebar h2 .count{background:var(--bg);border:1px solid var(--line);border-radius:999px;padding:0 8px;font-size:10.5px;letter-spacing:0;color:var(--muted)}.sidebar-filter{width:100%;padding:7px 10px;margin-bottom:10px;border:1px solid var(--line);border-radius:8px;font:inherit;font-size:13px;background:#fff;color:var(--ink)}.sidebar-filter:focus{outline:none;border-color:var(--maroon)}.project-list{flex:1;min-height:0;overflow-y:auto;margin:0 -4px;padding:0 4px 4px}.group-label{font-size:10.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);margin:10px 4px 5px}div:first-child>.group-label{margin-top:2px}.project-item{display:block;width:100%;text-align:left;padding:9px 12px;margin-bottom:5px;border:1px solid transparent;border-radius:8px;background:none;cursor:pointer;font:inherit;color:var(--ink)}.project-item:hover{background:var(--bg)}.project-item.active{background:var(--accent-soft);border-color:var(--maroon);font-weight:600}.project-item .project-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.project-item .date{display:block;font-size:11.5px;color:var(--muted);font-weight:400}.project-create{margin-top:12px;padding-top:12px;border-top:1px solid var(--line)}.project-create>button{width:100%}.main{flex:1;padding:22px 28px;overflow-y:auto;min-width:0}.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:18px 20px;margin-bottom:16px}.card h3{margin:0 0 4px;font-size:15px}.card .hint{color:var(--muted);font-size:12.5px;margin:0 0 12px}.step-badge{display:inline-flex;align-items:center;justify-content:center;width:21px;height:21px;border-radius:50%;background:var(--maroon);color:#fff;font-size:12px;font-weight:700;margin-right:8px;vertical-align:-3px}button{white-space:nowrap}button.primary{background:var(--maroon);color:#fff;border:none;min-height:var(--control-height);display:inline-flex;align-items:center;justify-content:center;padding:0 18px;border-radius:8px;font:inherit;font-weight:600;cursor:pointer;line-height:1.2}button.primary:hover{background:var(--maroon-dark)}button.primary:disabled{background:#c9ccd1;cursor:not-allowed}button.ghost{background:none;border:1px solid var(--line);color:var(--ink);min-height:var(--control-height);display:inline-flex;align-items:center;justify-content:center;padding:0 14px;border-radius:8px;font:inherit;cursor:pointer;line-height:1.2}button.ghost:hover{border-color:var(--maroon);color:var(--maroon)}button.linkish{background:none;border:none;color:var(--maroon);font:inherit;cursor:pointer;padding:0;text-decoration:underline}input[type=text],textarea,select{width:100%;padding:8px 10px;border:1px solid var(--line);border-radius:8px;font:inherit;background:#fff;color:var(--ink)}input[type=text],select{height:var(--control-height)}button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid rgba(122,31,61,.38);outline-offset:2px}input[type=file]{display:block;max-width:100%;margin-top:6px;font-size:13px;color:var(--muted)}input[type=file]::file-selector-button{background:#fff;border:1px solid var(--line);color:var(--ink);padding:7px 14px;border-radius:8px;font:inherit;font-size:13px;cursor:pointer;margin-right:10px}input[type=file]::file-selector-button:hover{border-color:var(--maroon);color:var(--maroon)}.row>button{align-self:flex-end;margin-bottom:1px}textarea{resize:vertical}label.field{display:block;margin-bottom:10px;font-size:13px;font-weight:600}label.field>*{margin-top:4px;font-weight:400}.row{display:flex;gap:14px;flex-wrap:wrap}.row>*{min-width:0}.row>.grow{flex:1;min-width:min(220px,100%)}.language-control{min-width:170px}.model-control{min-width:260px}.run-settings{display:grid;grid-template-columns:minmax(160px,230px) minmax(320px,720px) max-content;justify-content:start;gap:14px;align-items:end}.run-settings .field{margin-bottom:0}.run-button{min-width:180px;height:var(--control-height)}.construct-row{display:grid;grid-template-columns:minmax(320px,1120px) max-content;justify-content:start;align-items:end;gap:14px}.results-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:14px;flex-wrap:wrap}.result-actions{justify-content:flex-end}.result-actions a{display:inline-flex;text-decoration:none}.pill{display:inline-block;padding:2px 10px;border-radius:999px;font-size:11.5px;font-weight:600}.pill.completed{background:#e6f4ea;color:var(--ok)}.pill.running{background:#fff3e0;color:#b45309}.pill.queued{background:#eef2f7;color:var(--muted)}.pill.failed{background:#fdecea;color:var(--err)}.progress-track{background:var(--line);border-radius:999px;height:7px;overflow:hidden}.progress-fill{background:var(--maroon);height:100%;transition:width .4s ease}.warnings{background:#fff8e6;border:1px solid #f2dfa8;color:#7a5b00;border-radius:8px;padding:10px 14px}.error-banner{background:#fdecea;color:var(--err);border:1px solid #f5c6c0;padding:10px 14px;border-radius:8px;margin-bottom:14px;font-size:13px}.table-wrap{width:100%;overflow-x:auto}table.docs{width:100%;border-collapse:collapse;font-size:13px}table.docs th{text-align:left;color:var(--muted);font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;padding:6px 8px;border-bottom:1px solid var(--line)}table.docs td{padding:7px 8px;border-bottom:1px solid var(--bg);vertical-align:top;overflow-wrap:anywhere}table.docs td.score{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap}.stat-grid{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:4px}.stat{flex:1;min-width:110px;background:var(--bg);border-radius:8px;padding:10px 14px}.stat .v{font-size:20px;font-weight:700;font-variant-numeric:tabular-nums}.stat .k{font-size:11.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}.item-bar-row{display:flex;align-items:center;gap:10px;margin-bottom:7px}.item-bar-label{flex:1;font-size:12.5px;min-width:0;overflow-wrap:anywhere}.item-bar-track{flex:1.2;background:var(--bg);border-radius:999px;height:10px}.item-bar-fill{background:var(--maroon);opacity:.85;height:100%;border-radius:999px}.item-bar-val{width:52px;text-align:right;font-variant-numeric:tabular-nums;font-size:12.5px;font-weight:600}.construct-items{margin:8px 0 0;padding-left:20px;color:var(--muted);font-size:12.5px}.construct-items li{margin-bottom:2px}.meta-footer{font-size:12px;color:var(--muted);background:var(--bg);border-radius:8px;padding:10px 14px;margin-top:14px;font-variant-numeric:tabular-nums}.meta-footer code{font-size:11.5px}.muted{color:var(--muted)}.small{font-size:12.5px}.mt{margin-top:12px}@media (max-width: 820px){body{font-size:14px}.header{padding:12px 16px;align-items:flex-start;gap:2px 12px}.header h1{font-size:16px}.header .sub{flex:1 1 210px;font-size:12px;line-height:1.35}.layout{display:block}.sidebar{width:100%;border-right:0;border-bottom:1px solid var(--line);padding:14px}.project-list{max-height:220px;margin-right:0;flex:none}.project-create{border-top:0}.main{width:100%;padding:16px 14px 28px;overflow:visible}.card{padding:16px;margin-bottom:14px}.row,.run-settings,.construct-row,.results-toolbar,.result-actions{gap:10px}.row,.results-toolbar,.result-actions{flex-direction:column;align-items:stretch}.run-settings,.construct-row{grid-template-columns:1fr}.row>.grow,.construct-row>.grow,.language-control,.model-control{width:100%;min-width:0}.main .row>button,.main .row>a,.main .row>a>button,.results-toolbar>button{align-self:stretch;margin-bottom:0;width:100%}.run-button{width:100%;min-width:0}.stat-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.stat{min-width:0}.item-bar-row{display:grid;grid-template-columns:minmax(0,1fr) 56px;gap:6px 10px}.item-bar-track{grid-column:1 / -1;width:100%}.item-bar-val{width:auto}table.docs{min-width:520px}}@media (max-width: 460px){.header .sub{flex-basis:100%}.stat-grid{grid-template-columns:1fr}} +:root{--maroon: #7a1f3d;--maroon-dark: #5e1730;--ink: #1d2129;--muted: #667085;--line: #e5e7eb;--bg: #f7f7f8;--card: #ffffff;--ok: #157f3d;--err: #b42318;--accent-soft: #f6ebef;--control-height: 40px}*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;color:var(--ink);background:var(--bg);font-size:14.5px;line-height:1.5}.app{display:flex;flex-direction:column;min-height:100vh}.header{background:var(--maroon);color:#fff;padding:14px 28px;display:flex;align-items:baseline;flex-wrap:wrap;gap:14px}.header h1{flex:0 0 auto;font-size:17px;margin:0;font-weight:650;letter-spacing:.2px;white-space:nowrap}.header .sub{flex:1 1 280px;min-width:0;font-size:12.5px;opacity:.85}.layout{display:flex;flex:1;min-width:0;min-height:0}.sidebar{width:250px;background:var(--card);border-right:1px solid var(--line);padding:18px 14px;flex-shrink:0;display:flex;flex-direction:column;min-height:0}.sidebar h2{font-size:11.5px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);margin:0 0 10px 4px;display:flex;align-items:center;gap:8px}.sidebar h2 .count{background:var(--bg);border:1px solid var(--line);border-radius:999px;padding:0 8px;font-size:10.5px;letter-spacing:0;color:var(--muted)}.sidebar-filter{width:100%;padding:7px 10px;margin-bottom:10px;border:1px solid var(--line);border-radius:8px;font:inherit;font-size:13px;background:#fff;color:var(--ink)}.sidebar-filter:focus{outline:none;border-color:var(--maroon)}.project-list{flex:1;min-height:0;overflow-y:auto;margin:0 -4px;padding:0 4px 4px}.group-label{font-size:10.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);margin:10px 4px 5px}div:first-child>.group-label{margin-top:2px}.project-item{display:block;width:100%;text-align:left;padding:9px 12px;margin-bottom:5px;border:1px solid transparent;border-radius:8px;background:none;cursor:pointer;font:inherit;color:var(--ink)}.project-item:hover{background:var(--bg)}.project-item.active{background:var(--accent-soft);border-color:var(--maroon);font-weight:600}.project-item .project-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.project-item .date{display:block;font-size:11.5px;color:var(--muted);font-weight:400}.project-create{margin-top:12px;padding-top:12px;border-top:1px solid var(--line)}.project-create>button{width:100%}.main{flex:1;padding:22px 28px;overflow-y:auto;min-width:0}.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:18px 20px;margin-bottom:16px}.card h3{margin:0 0 4px;font-size:15px}.card .hint{color:var(--muted);font-size:12.5px;margin:0 0 12px}.step-badge{display:inline-flex;align-items:center;justify-content:center;width:21px;height:21px;border-radius:50%;background:var(--maroon);color:#fff;font-size:12px;font-weight:700;margin-right:8px;vertical-align:-3px}button{white-space:nowrap}button.primary{background:var(--maroon);color:#fff;border:none;min-height:var(--control-height);display:inline-flex;align-items:center;justify-content:center;padding:0 18px;border-radius:8px;font:inherit;font-weight:600;cursor:pointer;line-height:1.2}button.primary:hover{background:var(--maroon-dark)}button.primary:disabled{background:#c9ccd1;cursor:not-allowed}button.ghost{background:none;border:1px solid var(--line);color:var(--ink);min-height:var(--control-height);display:inline-flex;align-items:center;justify-content:center;padding:0 14px;border-radius:8px;font:inherit;cursor:pointer;line-height:1.2}button.ghost:hover{border-color:var(--maroon);color:var(--maroon)}button.linkish{background:none;border:none;color:var(--maroon);font:inherit;cursor:pointer;padding:0;text-decoration:underline}input[type=text],textarea,select{width:100%;padding:8px 10px;border:1px solid var(--line);border-radius:8px;font:inherit;background:#fff;color:var(--ink)}input[type=text],select{height:var(--control-height)}button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid rgba(122,31,61,.38);outline-offset:2px}input[type=file]{display:block;max-width:100%;margin-top:6px;font-size:13px;color:var(--muted)}input[type=file]::file-selector-button{background:#fff;border:1px solid var(--line);color:var(--ink);padding:7px 14px;border-radius:8px;font:inherit;font-size:13px;cursor:pointer;margin-right:10px}input[type=file]::file-selector-button:hover{border-color:var(--maroon);color:var(--maroon)}.row>button{align-self:flex-end;margin-bottom:1px}textarea{resize:vertical}label.field{display:block;margin-bottom:10px;font-size:13px;font-weight:600}label.field>*{margin-top:4px;font-weight:400}.row{display:flex;gap:14px;flex-wrap:wrap}.row>*{min-width:0}.row>.grow{flex:1;min-width:min(220px,100%)}.language-control{min-width:170px}.model-control{min-width:260px}.run-settings{display:grid;grid-template-columns:minmax(160px,230px) minmax(320px,720px) max-content;justify-content:start;gap:14px;align-items:end}.run-settings .field{margin-bottom:0}.run-button{min-width:180px;height:var(--control-height)}.construct-row{display:grid;grid-template-columns:minmax(320px,1120px) max-content;justify-content:start;align-items:end;gap:14px}.results-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:14px;flex-wrap:wrap}.result-actions{justify-content:flex-end}.result-actions a{display:inline-flex;text-decoration:none}.pill{display:inline-block;padding:2px 10px;border-radius:999px;font-size:11.5px;font-weight:600}.pill.completed{background:#e6f4ea;color:var(--ok)}.pill.running{background:#fff3e0;color:#b45309}.pill.queued{background:#eef2f7;color:var(--muted)}.pill.failed{background:#fdecea;color:var(--err)}.progress-track{background:var(--line);border-radius:999px;height:7px;overflow:hidden}.progress-fill{background:var(--maroon);height:100%;transition:width .4s ease}.warnings{background:#fff8e6;border:1px solid #f2dfa8;color:#7a5b00;border-radius:8px;padding:10px 14px}.error-banner{background:#fdecea;color:var(--err);border:1px solid #f5c6c0;padding:10px 14px;border-radius:8px;margin-bottom:14px;font-size:13px}.table-wrap{width:100%;overflow-x:auto}table.docs{width:100%;border-collapse:collapse;font-size:13px}table.docs th{text-align:left;color:var(--muted);font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;padding:6px 8px;border-bottom:1px solid var(--line)}table.docs td{padding:7px 8px;border-bottom:1px solid var(--bg);vertical-align:top;overflow-wrap:anywhere}table.docs td.score{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap}.stat-grid{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:4px}.stat{flex:1;min-width:110px;background:var(--bg);border-radius:8px;padding:10px 14px}.stat .v{font-size:20px;font-weight:700;font-variant-numeric:tabular-nums}.stat .k{font-size:11.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}.item-bar-row{display:flex;align-items:center;gap:10px;margin-bottom:7px}.item-bar-label{flex:1;font-size:12.5px;min-width:0;overflow-wrap:anywhere}.item-bar-track{flex:1.2;background:var(--bg);border-radius:999px;height:10px}.item-bar-fill{background:var(--maroon);opacity:.85;height:100%;border-radius:999px}.item-bar-val{width:52px;text-align:right;font-variant-numeric:tabular-nums;font-size:12.5px;font-weight:600}.construct-items{margin:8px 0 0;padding-left:20px;color:var(--muted);font-size:12.5px}.construct-items li{margin-bottom:2px}.meta-footer{font-size:12px;color:var(--muted);background:var(--bg);border-radius:8px;padding:10px 14px;margin-top:14px;font-variant-numeric:tabular-nums}.meta-footer code{font-size:11.5px}.muted{color:var(--muted)}.small{font-size:12.5px}.mt{margin-top:12px}.header-auth{margin-left:auto;display:flex;align-items:center;gap:10px;color:#fff}.header-btn{background:#ffffff1f;color:#fff;border:1px solid rgba(255,255,255,.45);padding:5px 14px;border-radius:7px;font:inherit;font-size:13px;cursor:pointer}.header-btn:hover{background:#ffffff38}.project-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px;flex-wrap:wrap}.project-title{font-size:17px;font-weight:650;margin-right:10px}button.danger{color:var(--err);border-color:#f0c4be}button.danger:hover{color:var(--err);border-color:var(--err)}button.danger-solid{background:var(--err)}button.danger-solid:hover{background:#93261b}button.danger-solid:disabled{background:#c9ccd1}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#14161a73;display:flex;align-items:center;justify-content:center;z-index:40;padding:16px}.modal{background:var(--card);border-radius:10px;padding:22px 24px;width:100%;max-width:430px;box-shadow:0 12px 40px #0000002e}.modal h3{margin:0 0 6px}@media (max-width: 820px){body{font-size:14px}.header{padding:12px 16px;align-items:flex-start;gap:2px 12px}.header h1{font-size:16px}.header .sub{flex:1 1 210px;font-size:12px;line-height:1.35}.layout{display:block}.sidebar{width:100%;border-right:0;border-bottom:1px solid var(--line);padding:14px}.project-list{max-height:220px;margin-right:0;flex:none}.project-create{border-top:0}.main{width:100%;padding:16px 14px 28px;overflow:visible}.card{padding:16px;margin-bottom:14px}.row,.run-settings,.construct-row,.results-toolbar,.result-actions{gap:10px}.row,.results-toolbar,.result-actions{flex-direction:column;align-items:stretch}.run-settings,.construct-row{grid-template-columns:1fr}.row>.grow,.construct-row>.grow,.language-control,.model-control{width:100%;min-width:0}.main .row>button,.main .row>a,.main .row>a>button,.results-toolbar>button{align-self:stretch;margin-bottom:0;width:100%}.run-button{width:100%;min-width:0}.stat-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.stat{min-width:0}.item-bar-row{display:grid;grid-template-columns:minmax(0,1fr) 56px;gap:6px 10px}.item-bar-track{grid-column:1 / -1;width:100%}.item-bar-val{width:auto}table.docs{min-width:520px}}@media (max-width: 460px){.header .sub{flex-basis:100%}.stat-grid{grid-template-columns:1fr}} diff --git a/backend/static/assets/index-DOOkGzjl.js b/backend/static/assets/index-DOOkGzjl.js new file mode 100644 index 0000000000000000000000000000000000000000..cba156d4b8b579e9baef0cd78a3ac40d16a8bab3 --- /dev/null +++ b/backend/static/assets/index-DOOkGzjl.js @@ -0,0 +1,42 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();function xc(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var rs={exports:{}},ml={},ls={exports:{}},O={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var ar=Symbol.for("react.element"),Sc=Symbol.for("react.portal"),kc=Symbol.for("react.fragment"),Cc=Symbol.for("react.strict_mode"),Nc=Symbol.for("react.profiler"),Ec=Symbol.for("react.provider"),_c=Symbol.for("react.context"),jc=Symbol.for("react.forward_ref"),Pc=Symbol.for("react.suspense"),zc=Symbol.for("react.memo"),Tc=Symbol.for("react.lazy"),Yo=Symbol.iterator;function Lc(e){return e===null||typeof e!="object"?null:(e=Yo&&e[Yo]||e["@@iterator"],typeof e=="function"?e:null)}var is={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},os=Object.assign,us={};function wn(e,t,n){this.props=e,this.context=t,this.refs=us,this.updater=n||is}wn.prototype.isReactComponent={};wn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};wn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function ss(){}ss.prototype=wn.prototype;function Xi(e,t,n){this.props=e,this.context=t,this.refs=us,this.updater=n||is}var Gi=Xi.prototype=new ss;Gi.constructor=Xi;os(Gi,wn.prototype);Gi.isPureReactComponent=!0;var Xo=Array.isArray,as=Object.prototype.hasOwnProperty,Zi={current:null},cs={key:!0,ref:!0,__self:!0,__source:!0};function fs(e,t,n){var r,l={},i=null,o=null;if(t!=null)for(r in t.ref!==void 0&&(o=t.ref),t.key!==void 0&&(i=""+t.key),t)as.call(t,r)&&!cs.hasOwnProperty(r)&&(l[r]=t[r]);var u=arguments.length-2;if(u===1)l.children=n;else if(1>>1,X=N[H];if(0>>1;Hl(Be,R))Hel(ve,Be)?(N[H]=ve,N[He]=R,H=He):(N[H]=Be,N[Ge]=R,H=Ge);else if(Hel(ve,R))N[H]=ve,N[He]=R,H=He;else break e}}return z}function l(N,z){var R=N.sortIndex-z.sortIndex;return R!==0?R:N.id-z.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var o=Date,u=o.now();e.unstable_now=function(){return o.now()-u}}var s=[],f=[],h=1,v=null,m=3,x=!1,S=!1,k=!1,D=typeof setTimeout=="function"?setTimeout:null,d=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function p(N){for(var z=n(f);z!==null;){if(z.callback===null)r(f);else if(z.startTime<=N)r(f),z.sortIndex=z.expirationTime,t(s,z);else break;z=n(f)}}function g(N){if(k=!1,p(N),!S)if(n(s)!==null)S=!0,Bt(C);else{var z=n(f);z!==null&&Ht(g,z.startTime-N)}}function C(N,z){S=!1,k&&(k=!1,d(P),P=-1),x=!0;var R=m;try{for(p(z),v=n(s);v!==null&&(!(v.expirationTime>z)||N&&!M());){var H=v.callback;if(typeof H=="function"){v.callback=null,m=v.priorityLevel;var X=H(v.expirationTime<=z);z=e.unstable_now(),typeof X=="function"?v.callback=X:v===n(s)&&r(s),p(z)}else r(s);v=n(s)}if(v!==null)var Wt=!0;else{var Ge=n(f);Ge!==null&&Ht(g,Ge.startTime-z),Wt=!1}return Wt}finally{v=null,m=R,x=!1}}var j=!1,_=null,P=-1,$=5,L=-1;function M(){return!(e.unstable_now()-L<$)}function ue(){if(_!==null){var N=e.unstable_now();L=N;var z=!0;try{z=_(!0,N)}finally{z?U():(j=!1,_=null)}}else j=!1}var U;if(typeof c=="function")U=function(){c(ue)};else if(typeof MessageChannel<"u"){var kn=new MessageChannel,mr=kn.port2;kn.port1.onmessage=ue,U=function(){mr.postMessage(null)}}else U=function(){D(ue,0)};function Bt(N){_=N,j||(j=!0,U())}function Ht(N,z){P=D(function(){N(e.unstable_now())},z)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(N){N.callback=null},e.unstable_continueExecution=function(){S||x||(S=!0,Bt(C))},e.unstable_forceFrameRate=function(N){0>N||125H?(N.sortIndex=R,t(f,N),n(s)===null&&N===n(f)&&(k?(d(P),P=-1):k=!0,Ht(g,R-H))):(N.sortIndex=X,t(s,N),S||x||(S=!0,Bt(C))),N},e.unstable_shouldYield=M,e.unstable_wrapCallback=function(N){var z=m;return function(){var R=m;m=z;try{return N.apply(this,arguments)}finally{m=R}}}})(vs);hs.exports=vs;var Hc=hs.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Wc=T,Ee=Hc;function y(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ni=Object.prototype.hasOwnProperty,Qc=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Zo={},Jo={};function Kc(e){return ni.call(Jo,e)?!0:ni.call(Zo,e)?!1:Qc.test(e)?Jo[e]=!0:(Zo[e]=!0,!1)}function Yc(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Xc(e,t,n,r){if(t===null||typeof t>"u"||Yc(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function he(e,t,n,r,l,i,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=o}var oe={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){oe[e]=new he(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];oe[t]=new he(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){oe[e]=new he(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){oe[e]=new he(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){oe[e]=new he(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){oe[e]=new he(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){oe[e]=new he(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){oe[e]=new he(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){oe[e]=new he(e,5,!1,e.toLowerCase(),null,!1,!1)});var qi=/[\-:]([a-z])/g;function bi(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(qi,bi);oe[t]=new he(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(qi,bi);oe[t]=new he(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(qi,bi);oe[t]=new he(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){oe[e]=new he(e,1,!1,e.toLowerCase(),null,!1,!1)});oe.xlinkHref=new he("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){oe[e]=new he(e,1,!1,e.toLowerCase(),null,!0,!0)});function eo(e,t,n,r){var l=oe.hasOwnProperty(t)?oe[t]:null;(l!==null?l.type!==0:r||!(2u||l[o]!==i[u]){var s=` +`+l[o].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=o&&0<=u);break}}}finally{Rl=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Mn(e):""}function Gc(e){switch(e.tag){case 5:return Mn(e.type);case 16:return Mn("Lazy");case 13:return Mn("Suspense");case 19:return Mn("SuspenseList");case 0:case 2:case 15:return e=Ml(e.type,!1),e;case 11:return e=Ml(e.type.render,!1),e;case 1:return e=Ml(e.type,!0),e;default:return""}}function oi(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Xt:return"Fragment";case Yt:return"Portal";case ri:return"Profiler";case to:return"StrictMode";case li:return"Suspense";case ii:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case ws:return(e.displayName||"Context")+".Consumer";case ys:return(e._context.displayName||"Context")+".Provider";case no:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ro:return t=e.displayName||null,t!==null?t:oi(e.type)||"Memo";case ut:t=e._payload,e=e._init;try{return oi(e(t))}catch{}}return null}function Zc(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return oi(t);case 8:return t===to?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function St(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Ss(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Jc(e){var t=Ss(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(o){r=""+o,i.call(this,o)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(o){r=""+o},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function gr(e){e._valueTracker||(e._valueTracker=Jc(e))}function ks(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Ss(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Wr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ui(e,t){var n=t.checked;return Y({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function bo(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=St(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Cs(e,t){t=t.checked,t!=null&&eo(e,"checked",t,!1)}function si(e,t){Cs(e,t);var n=St(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ai(e,t.type,n):t.hasOwnProperty("defaultValue")&&ai(e,t.type,St(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function eu(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ai(e,t,n){(t!=="number"||Wr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var On=Array.isArray;function on(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=yr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Yn(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var In={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},qc=["Webkit","ms","Moz","O"];Object.keys(In).forEach(function(e){qc.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),In[t]=In[e]})});function js(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||In.hasOwnProperty(e)&&In[e]?(""+t).trim():t+"px"}function Ps(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=js(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var bc=Y({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function di(e,t){if(t){if(bc[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(y(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(y(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(y(61))}if(t.style!=null&&typeof t.style!="object")throw Error(y(62))}}function pi(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var mi=null;function lo(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var hi=null,un=null,sn=null;function ru(e){if(e=dr(e)){if(typeof hi!="function")throw Error(y(280));var t=e.stateNode;t&&(t=wl(t),hi(e.stateNode,e.type,t))}}function zs(e){un?sn?sn.push(e):sn=[e]:un=e}function Ts(){if(un){var e=un,t=sn;if(sn=un=null,ru(e),t)for(e=0;e>>=0,e===0?32:31-(ff(e)/df|0)|0}var wr=64,xr=4194304;function Dn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Xr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,o=n&268435455;if(o!==0){var u=o&~l;u!==0?r=Dn(u):(i&=o,i!==0&&(r=Dn(i)))}else o=n&~l,o!==0?r=Dn(o):i!==0&&(r=Dn(i));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,i=t&-t,l>=i||l===16&&(i&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function cr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-$e(t),e[t]=n}function vf(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=$n),du=" ",pu=!1;function Zs(e,t){switch(e){case"keyup":return Wf.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Js(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Gt=!1;function Kf(e,t){switch(e){case"compositionend":return Js(t);case"keypress":return t.which!==32?null:(pu=!0,du);case"textInput":return e=t.data,e===du&&pu?null:e;default:return null}}function Yf(e,t){if(Gt)return e==="compositionend"||!po&&Zs(e,t)?(e=Xs(),Dr=ao=ft=null,Gt=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=gu(n)}}function ta(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?ta(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function na(){for(var e=window,t=Wr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Wr(e.document)}return t}function mo(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function nd(e){var t=na(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&ta(n.ownerDocument.documentElement,n)){if(r!==null&&mo(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,i=Math.min(r.start,l);r=r.end===void 0?i:Math.min(r.end,l),!e.extend&&i>r&&(l=r,r=i,i=l),l=yu(n,i);var o=yu(n,r);l&&o&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==o.node||e.focusOffset!==o.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(o.node,o.offset)):(t.setEnd(o.node,o.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Zt=null,Si=null,Vn=null,ki=!1;function wu(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;ki||Zt==null||Zt!==Wr(r)||(r=Zt,"selectionStart"in r&&mo(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Vn&&bn(Vn,r)||(Vn=r,r=Jr(Si,"onSelect"),0bt||(e.current=Pi[bt],Pi[bt]=null,bt--)}function A(e,t){bt++,Pi[bt]=e.current,e.current=t}var kt={},fe=Nt(kt),we=Nt(!1),Dt=kt;function pn(e,t){var n=e.type.contextTypes;if(!n)return kt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function xe(e){return e=e.childContextTypes,e!=null}function br(){B(we),B(fe)}function _u(e,t,n){if(fe.current!==kt)throw Error(y(168));A(fe,t),A(we,n)}function fa(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(y(108,Zc(e)||"Unknown",l));return Y({},n,r)}function el(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||kt,Dt=fe.current,A(fe,e),A(we,we.current),!0}function ju(e,t,n){var r=e.stateNode;if(!r)throw Error(y(169));n?(e=fa(e,t,Dt),r.__reactInternalMemoizedMergedChildContext=e,B(we),B(fe),A(fe,e)):B(we),A(we,n)}var Je=null,xl=!1,Yl=!1;function da(e){Je===null?Je=[e]:Je.push(e)}function md(e){xl=!0,da(e)}function Et(){if(!Yl&&Je!==null){Yl=!0;var e=0,t=I;try{var n=Je;for(I=1;e>=o,l-=o,qe=1<<32-$e(t)+l|n<P?($=_,_=null):$=_.sibling;var L=m(d,_,p[P],g);if(L===null){_===null&&(_=$);break}e&&_&&L.alternate===null&&t(d,_),c=i(L,c,P),j===null?C=L:j.sibling=L,j=L,_=$}if(P===p.length)return n(d,_),W&&Pt(d,P),C;if(_===null){for(;PP?($=_,_=null):$=_.sibling;var M=m(d,_,L.value,g);if(M===null){_===null&&(_=$);break}e&&_&&M.alternate===null&&t(d,_),c=i(M,c,P),j===null?C=M:j.sibling=M,j=M,_=$}if(L.done)return n(d,_),W&&Pt(d,P),C;if(_===null){for(;!L.done;P++,L=p.next())L=v(d,L.value,g),L!==null&&(c=i(L,c,P),j===null?C=L:j.sibling=L,j=L);return W&&Pt(d,P),C}for(_=r(d,_);!L.done;P++,L=p.next())L=x(_,d,P,L.value,g),L!==null&&(e&&L.alternate!==null&&_.delete(L.key===null?P:L.key),c=i(L,c,P),j===null?C=L:j.sibling=L,j=L);return e&&_.forEach(function(ue){return t(d,ue)}),W&&Pt(d,P),C}function D(d,c,p,g){if(typeof p=="object"&&p!==null&&p.type===Xt&&p.key===null&&(p=p.props.children),typeof p=="object"&&p!==null){switch(p.$$typeof){case vr:e:{for(var C=p.key,j=c;j!==null;){if(j.key===C){if(C=p.type,C===Xt){if(j.tag===7){n(d,j.sibling),c=l(j,p.props.children),c.return=d,d=c;break e}}else if(j.elementType===C||typeof C=="object"&&C!==null&&C.$$typeof===ut&&Tu(C)===j.type){n(d,j.sibling),c=l(j,p.props),c.ref=zn(d,j,p),c.return=d,d=c;break e}n(d,j);break}else t(d,j);j=j.sibling}p.type===Xt?(c=Ot(p.props.children,d.mode,g,p.key),c.return=d,d=c):(g=Hr(p.type,p.key,p.props,null,d.mode,g),g.ref=zn(d,c,p),g.return=d,d=g)}return o(d);case Yt:e:{for(j=p.key;c!==null;){if(c.key===j)if(c.tag===4&&c.stateNode.containerInfo===p.containerInfo&&c.stateNode.implementation===p.implementation){n(d,c.sibling),c=l(c,p.children||[]),c.return=d,d=c;break e}else{n(d,c);break}else t(d,c);c=c.sibling}c=ti(p,d.mode,g),c.return=d,d=c}return o(d);case ut:return j=p._init,D(d,c,j(p._payload),g)}if(On(p))return S(d,c,p,g);if(Nn(p))return k(d,c,p,g);jr(d,p)}return typeof p=="string"&&p!==""||typeof p=="number"?(p=""+p,c!==null&&c.tag===6?(n(d,c.sibling),c=l(c,p),c.return=d,d=c):(n(d,c),c=ei(p,d.mode,g),c.return=d,d=c),o(d)):n(d,c)}return D}var hn=va(!0),ga=va(!1),rl=Nt(null),ll=null,nn=null,yo=null;function wo(){yo=nn=ll=null}function xo(e){var t=rl.current;B(rl),e._currentValue=t}function Li(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function cn(e,t){ll=e,yo=nn=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(ye=!0),e.firstContext=null)}function Me(e){var t=e._currentValue;if(yo!==e)if(e={context:e,memoizedValue:t,next:null},nn===null){if(ll===null)throw Error(y(308));nn=e,ll.dependencies={lanes:0,firstContext:e}}else nn=nn.next=e;return t}var Lt=null;function So(e){Lt===null?Lt=[e]:Lt.push(e)}function ya(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,So(t)):(n.next=l.next,l.next=n),t.interleaved=n,rt(e,r)}function rt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var st=!1;function ko(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function wa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function et(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function gt(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,F&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,rt(e,n)}return l=r.interleaved,l===null?(t.next=t,So(r)):(t.next=l.next,l.next=t),r.interleaved=t,rt(e,n)}function Ir(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,oo(e,n)}}function Lu(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var o={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=o:i=i.next=o,n=n.next}while(n!==null);i===null?l=i=t:i=i.next=t}else l=i=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function il(e,t,n,r){var l=e.updateQueue;st=!1;var i=l.firstBaseUpdate,o=l.lastBaseUpdate,u=l.shared.pending;if(u!==null){l.shared.pending=null;var s=u,f=s.next;s.next=null,o===null?i=f:o.next=f,o=s;var h=e.alternate;h!==null&&(h=h.updateQueue,u=h.lastBaseUpdate,u!==o&&(u===null?h.firstBaseUpdate=f:u.next=f,h.lastBaseUpdate=s))}if(i!==null){var v=l.baseState;o=0,h=f=s=null,u=i;do{var m=u.lane,x=u.eventTime;if((r&m)===m){h!==null&&(h=h.next={eventTime:x,lane:0,tag:u.tag,payload:u.payload,callback:u.callback,next:null});e:{var S=e,k=u;switch(m=t,x=n,k.tag){case 1:if(S=k.payload,typeof S=="function"){v=S.call(x,v,m);break e}v=S;break e;case 3:S.flags=S.flags&-65537|128;case 0:if(S=k.payload,m=typeof S=="function"?S.call(x,v,m):S,m==null)break e;v=Y({},v,m);break e;case 2:st=!0}}u.callback!==null&&u.lane!==0&&(e.flags|=64,m=l.effects,m===null?l.effects=[u]:m.push(u))}else x={eventTime:x,lane:m,tag:u.tag,payload:u.payload,callback:u.callback,next:null},h===null?(f=h=x,s=v):h=h.next=x,o|=m;if(u=u.next,u===null){if(u=l.shared.pending,u===null)break;m=u,u=m.next,m.next=null,l.lastBaseUpdate=m,l.shared.pending=null}}while(!0);if(h===null&&(s=v),l.baseState=s,l.firstBaseUpdate=f,l.lastBaseUpdate=h,t=l.shared.interleaved,t!==null){l=t;do o|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);Ut|=o,e.lanes=o,e.memoizedState=v}}function Ru(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Gl.transition;Gl.transition={};try{e(!1),t()}finally{I=n,Gl.transition=r}}function Fa(){return Oe().memoizedState}function yd(e,t,n){var r=wt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Ia(e))Ua(t,n);else if(n=ya(e,t,n,r),n!==null){var l=pe();Ae(n,e,r,l),$a(n,t,r)}}function wd(e,t,n){var r=wt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ia(e))Ua(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var o=t.lastRenderedState,u=i(o,n);if(l.hasEagerState=!0,l.eagerState=u,Ve(u,o)){var s=t.interleaved;s===null?(l.next=l,So(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=ya(e,t,l,r),n!==null&&(l=pe(),Ae(n,e,r,l),$a(n,t,r))}}function Ia(e){var t=e.alternate;return e===K||t!==null&&t===K}function Ua(e,t){Bn=ul=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function $a(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,oo(e,n)}}var sl={readContext:Me,useCallback:se,useContext:se,useEffect:se,useImperativeHandle:se,useInsertionEffect:se,useLayoutEffect:se,useMemo:se,useReducer:se,useRef:se,useState:se,useDebugValue:se,useDeferredValue:se,useTransition:se,useMutableSource:se,useSyncExternalStore:se,useId:se,unstable_isNewReconciler:!1},xd={readContext:Me,useCallback:function(e,t){return Qe().memoizedState=[e,t===void 0?null:t],e},useContext:Me,useEffect:Ou,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,$r(4194308,4,La.bind(null,t,e),n)},useLayoutEffect:function(e,t){return $r(4194308,4,e,t)},useInsertionEffect:function(e,t){return $r(4,2,e,t)},useMemo:function(e,t){var n=Qe();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Qe();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=yd.bind(null,K,e),[r.memoizedState,e]},useRef:function(e){var t=Qe();return e={current:e},t.memoizedState=e},useState:Mu,useDebugValue:To,useDeferredValue:function(e){return Qe().memoizedState=e},useTransition:function(){var e=Mu(!1),t=e[0];return e=gd.bind(null,e[1]),Qe().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=K,l=Qe();if(W){if(n===void 0)throw Error(y(407));n=n()}else{if(n=t(),ne===null)throw Error(y(349));It&30||Ca(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,Ou(Ea.bind(null,r,i,e),[e]),r.flags|=2048,ur(9,Na.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=Qe(),t=ne.identifierPrefix;if(W){var n=be,r=qe;n=(r&~(1<<32-$e(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=ir++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=o.createElement(n,{is:r.is}):(e=o.createElement(n),n==="select"&&(o=e,r.multiple?o.multiple=!0:r.size&&(o.size=r.size))):e=o.createElementNS(e,n),e[Ke]=t,e[nr]=r,Ga(e,t,!1,!1),t.stateNode=e;e:{switch(o=pi(n,r),n){case"dialog":V("cancel",e),V("close",e),l=r;break;case"iframe":case"object":case"embed":V("load",e),l=r;break;case"video":case"audio":for(l=0;lyn&&(t.flags|=128,r=!0,Tn(i,!1),t.lanes=4194304)}else{if(!r)if(e=ol(o),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Tn(i,!0),i.tail===null&&i.tailMode==="hidden"&&!o.alternate&&!W)return ae(t),null}else 2*J()-i.renderingStartTime>yn&&n!==1073741824&&(t.flags|=128,r=!0,Tn(i,!1),t.lanes=4194304);i.isBackwards?(o.sibling=t.child,t.child=o):(n=i.last,n!==null?n.sibling=o:t.child=o,i.last=o)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=J(),t.sibling=null,n=Q.current,A(Q,r?n&1|2:n&1),t):(ae(t),null);case 22:case 23:return Fo(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ke&1073741824&&(ae(t),t.subtreeFlags&6&&(t.flags|=8192)):ae(t),null;case 24:return null;case 25:return null}throw Error(y(156,t.tag))}function Pd(e,t){switch(vo(t),t.tag){case 1:return xe(t.type)&&br(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return vn(),B(we),B(fe),Eo(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return No(t),null;case 13:if(B(Q),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(y(340));mn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return B(Q),null;case 4:return vn(),null;case 10:return xo(t.type._context),null;case 22:case 23:return Fo(),null;case 24:return null;default:return null}}var zr=!1,ce=!1,zd=typeof WeakSet=="function"?WeakSet:Set,E=null;function rn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Z(e,t,r)}else n.current=null}function Ai(e,t,n){try{n()}catch(r){Z(e,t,r)}}var Qu=!1;function Td(e,t){if(Ci=Gr,e=na(),mo(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var o=0,u=-1,s=-1,f=0,h=0,v=e,m=null;t:for(;;){for(var x;v!==n||l!==0&&v.nodeType!==3||(u=o+l),v!==i||r!==0&&v.nodeType!==3||(s=o+r),v.nodeType===3&&(o+=v.nodeValue.length),(x=v.firstChild)!==null;)m=v,v=x;for(;;){if(v===e)break t;if(m===n&&++f===l&&(u=o),m===i&&++h===r&&(s=o),(x=v.nextSibling)!==null)break;v=m,m=v.parentNode}v=x}n=u===-1||s===-1?null:{start:u,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(Ni={focusedElem:e,selectionRange:n},Gr=!1,E=t;E!==null;)if(t=E,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,E=e;else for(;E!==null;){t=E;try{var S=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(S!==null){var k=S.memoizedProps,D=S.memoizedState,d=t.stateNode,c=d.getSnapshotBeforeUpdate(t.elementType===t.type?k:Fe(t.type,k),D);d.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var p=t.stateNode.containerInfo;p.nodeType===1?p.textContent="":p.nodeType===9&&p.documentElement&&p.removeChild(p.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(y(163))}}catch(g){Z(t,t.return,g)}if(e=t.sibling,e!==null){e.return=t.return,E=e;break}E=t.return}return S=Qu,Qu=!1,S}function Hn(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var i=l.destroy;l.destroy=void 0,i!==void 0&&Ai(t,n,i)}l=l.next}while(l!==r)}}function Cl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Vi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function qa(e){var t=e.alternate;t!==null&&(e.alternate=null,qa(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ke],delete t[nr],delete t[ji],delete t[dd],delete t[pd])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function ba(e){return e.tag===5||e.tag===3||e.tag===4}function Ku(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||ba(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Bi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=qr));else if(r!==4&&(e=e.child,e!==null))for(Bi(e,t,n),e=e.sibling;e!==null;)Bi(e,t,n),e=e.sibling}function Hi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Hi(e,t,n),e=e.sibling;e!==null;)Hi(e,t,n),e=e.sibling}var le=null,Ie=!1;function ot(e,t,n){for(n=n.child;n!==null;)ec(e,t,n),n=n.sibling}function ec(e,t,n){if(Ye&&typeof Ye.onCommitFiberUnmount=="function")try{Ye.onCommitFiberUnmount(hl,n)}catch{}switch(n.tag){case 5:ce||rn(n,t);case 6:var r=le,l=Ie;le=null,ot(e,t,n),le=r,Ie=l,le!==null&&(Ie?(e=le,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):le.removeChild(n.stateNode));break;case 18:le!==null&&(Ie?(e=le,n=n.stateNode,e.nodeType===8?Kl(e.parentNode,n):e.nodeType===1&&Kl(e,n),Jn(e)):Kl(le,n.stateNode));break;case 4:r=le,l=Ie,le=n.stateNode.containerInfo,Ie=!0,ot(e,t,n),le=r,Ie=l;break;case 0:case 11:case 14:case 15:if(!ce&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,o=i.destroy;i=i.tag,o!==void 0&&(i&2||i&4)&&Ai(n,t,o),l=l.next}while(l!==r)}ot(e,t,n);break;case 1:if(!ce&&(rn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){Z(n,t,u)}ot(e,t,n);break;case 21:ot(e,t,n);break;case 22:n.mode&1?(ce=(r=ce)||n.memoizedState!==null,ot(e,t,n),ce=r):ot(e,t,n);break;default:ot(e,t,n)}}function Yu(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new zd),t.forEach(function(r){var l=$d.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function De(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=o),r&=~i}if(r=l,r=J()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Rd(r/1960))-r,10e?16:e,dt===null)var r=!1;else{if(e=dt,dt=null,fl=0,F&6)throw Error(y(331));var l=F;for(F|=4,E=e.current;E!==null;){var i=E,o=i.child;if(E.flags&16){var u=i.deletions;if(u!==null){for(var s=0;sJ()-Oo?Mt(e,0):Mo|=n),Se(e,t)}function sc(e,t){t===0&&(e.mode&1?(t=xr,xr<<=1,!(xr&130023424)&&(xr=4194304)):t=1);var n=pe();e=rt(e,t),e!==null&&(cr(e,t,n),Se(e,n))}function Ud(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),sc(e,n)}function $d(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(y(314))}r!==null&&r.delete(t),sc(e,n)}var ac;ac=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||we.current)ye=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return ye=!1,_d(e,t,n);ye=!!(e.flags&131072)}else ye=!1,W&&t.flags&1048576&&pa(t,nl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Ar(e,t),e=t.pendingProps;var l=pn(t,fe.current);cn(t,n),l=jo(null,t,r,e,l,n);var i=Po();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,xe(r)?(i=!0,el(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,ko(t),l.updater=kl,t.stateNode=l,l._reactInternals=t,Mi(t,r,e,n),t=Fi(null,t,r,!0,i,n)):(t.tag=0,W&&i&&ho(t),de(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Ar(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Vd(r),e=Fe(r,e),l){case 0:t=Di(null,t,r,e,n);break e;case 1:t=Bu(null,t,r,e,n);break e;case 11:t=Au(null,t,r,e,n);break e;case 14:t=Vu(null,t,r,Fe(r.type,e),n);break e}throw Error(y(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Fe(r,l),Di(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Fe(r,l),Bu(e,t,r,l,n);case 3:e:{if(Ka(t),e===null)throw Error(y(387));r=t.pendingProps,i=t.memoizedState,l=i.element,wa(e,t),il(t,r,null,n);var o=t.memoizedState;if(r=o.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:o.cache,pendingSuspenseBoundaries:o.pendingSuspenseBoundaries,transitions:o.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=gn(Error(y(423)),t),t=Hu(e,t,r,n,l);break e}else if(r!==l){l=gn(Error(y(424)),t),t=Hu(e,t,r,n,l);break e}else for(Ce=vt(t.stateNode.containerInfo.firstChild),Ne=t,W=!0,Ue=null,n=ga(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(mn(),r===l){t=lt(e,t,n);break e}de(e,t,r,n)}t=t.child}return t;case 5:return xa(t),e===null&&Ti(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,o=l.children,Ei(r,l)?o=null:i!==null&&Ei(r,i)&&(t.flags|=32),Qa(e,t),de(e,t,o,n),t.child;case 6:return e===null&&Ti(t),null;case 13:return Ya(e,t,n);case 4:return Co(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=hn(t,null,r,n):de(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Fe(r,l),Au(e,t,r,l,n);case 7:return de(e,t,t.pendingProps,n),t.child;case 8:return de(e,t,t.pendingProps.children,n),t.child;case 12:return de(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,o=l.value,A(rl,r._currentValue),r._currentValue=o,i!==null)if(Ve(i.value,o)){if(i.children===l.children&&!we.current){t=lt(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var u=i.dependencies;if(u!==null){o=i.child;for(var s=u.firstContext;s!==null;){if(s.context===r){if(i.tag===1){s=et(-1,n&-n),s.tag=2;var f=i.updateQueue;if(f!==null){f=f.shared;var h=f.pending;h===null?s.next=s:(s.next=h.next,h.next=s),f.pending=s}}i.lanes|=n,s=i.alternate,s!==null&&(s.lanes|=n),Li(i.return,n,t),u.lanes|=n;break}s=s.next}}else if(i.tag===10)o=i.type===t.type?null:i.child;else if(i.tag===18){if(o=i.return,o===null)throw Error(y(341));o.lanes|=n,u=o.alternate,u!==null&&(u.lanes|=n),Li(o,n,t),o=i.sibling}else o=i.child;if(o!==null)o.return=i;else for(o=i;o!==null;){if(o===t){o=null;break}if(i=o.sibling,i!==null){i.return=o.return,o=i;break}o=o.return}i=o}de(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,cn(t,n),l=Me(l),r=r(l),t.flags|=1,de(e,t,r,n),t.child;case 14:return r=t.type,l=Fe(r,t.pendingProps),l=Fe(r.type,l),Vu(e,t,r,l,n);case 15:return Ha(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Fe(r,l),Ar(e,t),t.tag=1,xe(r)?(e=!0,el(t)):e=!1,cn(t,n),Aa(t,r,l),Mi(t,r,l,n),Fi(null,t,r,!0,e,n);case 19:return Xa(e,t,n);case 22:return Wa(e,t,n)}throw Error(y(156,t.tag))};function cc(e,t){return Is(e,t)}function Ad(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Le(e,t,n,r){return new Ad(e,t,n,r)}function Uo(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Vd(e){if(typeof e=="function")return Uo(e)?1:0;if(e!=null){if(e=e.$$typeof,e===no)return 11;if(e===ro)return 14}return 2}function xt(e,t){var n=e.alternate;return n===null?(n=Le(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Hr(e,t,n,r,l,i){var o=2;if(r=e,typeof e=="function")Uo(e)&&(o=1);else if(typeof e=="string")o=5;else e:switch(e){case Xt:return Ot(n.children,l,i,t);case to:o=8,l|=8;break;case ri:return e=Le(12,n,t,l|2),e.elementType=ri,e.lanes=i,e;case li:return e=Le(13,n,t,l),e.elementType=li,e.lanes=i,e;case ii:return e=Le(19,n,t,l),e.elementType=ii,e.lanes=i,e;case xs:return El(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case ys:o=10;break e;case ws:o=9;break e;case no:o=11;break e;case ro:o=14;break e;case ut:o=16,r=null;break e}throw Error(y(130,e==null?e:typeof e,""))}return t=Le(o,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function Ot(e,t,n,r){return e=Le(7,e,r,t),e.lanes=n,e}function El(e,t,n,r){return e=Le(22,e,r,t),e.elementType=xs,e.lanes=n,e.stateNode={isHidden:!1},e}function ei(e,t,n){return e=Le(6,e,null,t),e.lanes=n,e}function ti(e,t,n){return t=Le(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Bd(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Dl(0),this.expirationTimes=Dl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Dl(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function $o(e,t,n,r,l,i,o,u,s){return e=new Bd(e,t,n,u,s),t===1?(t=1,i===!0&&(t|=8)):t=0,i=Le(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},ko(i),e}function Hd(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(mc)}catch(e){console.error(e)}}mc(),ms.exports=_e;var Xd=ms.exports,hc,ts=Xd;hc=ts.createRoot,ts.hydrateRoot;async function re(e,t={}){const n=await fetch(e,t);if(!n.ok){let r=n.statusText;try{r=(await n.json()).detail||r}catch{}throw new Error(r)}return n.json()}const Rn=(e,t)=>({method:e,headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),G={health:()=>re("/api/health"),models:()=>re("/api/models"),languages:()=>re("/api/languages"),listProjects:()=>re("/api/projects"),createProject:e=>re("/api/projects",Rn("POST",e)),patchProject:(e,t)=>re(`/api/projects/${e}`,Rn("PATCH",t)),deleteProject:e=>fetch(`/api/projects/${e}`,{method:"DELETE"}),authMe:()=>re("/api/auth/me"),demoLogin:e=>re("/api/auth/demo/login",Rn("POST",{name:e})),logout:()=>re("/api/auth/logout",{method:"POST"}),listCorpora:e=>re(`/api/projects/${e}/corpora`),uploadCorpus:(e,t)=>{const n=new FormData;return n.append("file",t),re(`/api/projects/${e}/corpora`,{method:"POST",body:n})},listConstructs:()=>re("/api/constructs"),createConstruct:e=>re("/api/constructs",Rn("POST",e)),createJob:e=>re("/api/jobs",Rn("POST",e)),listJobs:e=>re(`/api/jobs?project_id=${e}`),getJob:e=>re(`/api/jobs/${e}`),jobResults:e=>re(`/api/jobs/${e}/results`),exportUrl:e=>`/api/jobs/${e}/export`,metadataUrl:e=>`/api/jobs/${e}/metadata`,scriptUrl:e=>`/api/jobs/${e}/script`,scriptRequirementsUrl:e=>`/api/jobs/${e}/script-requirements`};function Gd({jobId:e,onBack:t}){var f;const[n,r]=T.useState(null),[l,i]=T.useState("");if(T.useEffect(()=>{G.jobResults(e).then(r).catch(h=>i(h.message))},[e]),l)return a.jsxs("div",{className:"card",children:[a.jsx("div",{className:"error-banner",children:l}),a.jsx("button",{className:"ghost",onClick:t,children:"← Back"})]});if(!n)return a.jsx("div",{className:"card",children:"Loading results…"});const{summary:o,metadata:u}=n,s=Math.max(...o.item_means.map(h=>Math.abs(h.mean)),1e-9);return a.jsxs(a.Fragment,{children:[a.jsxs("div",{className:"results-toolbar",children:[a.jsx("button",{className:"ghost",onClick:t,children:"← Back to workspace"}),a.jsxs("div",{className:"row result-actions",children:[a.jsx("a",{href:G.exportUrl(e),children:a.jsx("button",{className:"primary",children:"Export results CSV"})}),a.jsx("a",{href:G.scriptUrl(e),children:a.jsx("button",{className:"ghost",children:"Python script"})}),a.jsx("a",{href:G.scriptRequirementsUrl(e),children:a.jsx("button",{className:"ghost",children:"requirements.txt"})}),a.jsx("a",{href:G.metadataUrl(e),children:a.jsx("button",{className:"ghost",children:"Run metadata (JSON)"})})]})]}),a.jsxs("div",{className:"card",children:[a.jsxs("h3",{children:[u.construct," × ",u.corpus_file]}),a.jsx("p",{className:"hint",children:"CCR score = mean cosine similarity between each text and the construct's scale items. Higher = the text expresses the construct more strongly."}),a.jsxs("div",{className:"stat-grid",children:[a.jsx(Kt,{k:"Texts scored",v:o.n_docs.toLocaleString()}),a.jsx(Kt,{k:"Mean score",v:o.score_mean.toFixed(3)}),a.jsx(Kt,{k:"SD",v:o.score_sd.toFixed(3)}),a.jsx(Kt,{k:"Min",v:o.score_min.toFixed(3)}),a.jsx(Kt,{k:"Max",v:o.score_max.toFixed(3)}),o.n_dropped_empty>0&&a.jsx(Kt,{k:"Empty rows dropped",v:o.n_dropped_empty})]}),((f=o.warnings)==null?void 0:f.length)>0&&a.jsxs("div",{className:"warnings mt",children:[a.jsx("strong",{className:"small",children:"Data-quality notes"}),a.jsx("ul",{className:"small",style:{margin:"4px 0 0",paddingLeft:20},children:o.warnings.map((h,v)=>a.jsx("li",{children:typeof h=="string"?h:a.jsxs(a.Fragment,{children:[a.jsx("code",{style:{fontSize:11},children:h.code})," - ",h.message]})},v))})]})]}),a.jsxs("div",{className:"card",children:[a.jsx("h3",{children:"Score distribution"}),a.jsx(Zd,{histogram:o.histogram})]}),a.jsxs("div",{className:"card",children:[a.jsx("h3",{children:"Per-item mean loadings"}),a.jsx("p",{className:"hint",children:"Mean similarity of the corpus to each scale item - a face-validity check on which items drive the construct signal."}),o.item_means.map((h,v)=>a.jsxs("div",{className:"item-bar-row",children:[a.jsx("span",{className:"item-bar-label",title:h.item,children:h.item.length>80?h.item.slice(0,80)+"…":h.item}),a.jsx("div",{className:"item-bar-track",children:a.jsx("div",{className:"item-bar-fill",style:{width:`${Math.max(2,Math.abs(h.mean)/s*100)}%`}})}),a.jsx("span",{className:"item-bar-val",children:h.mean.toFixed(3)})]},v))]}),a.jsxs("div",{className:"row",children:[a.jsxs("div",{className:"grow card",children:[a.jsx("h3",{children:"Highest-scoring texts"}),a.jsx(ns,{docs:o.top_docs})]}),a.jsxs("div",{className:"grow card",children:[a.jsx("h3",{children:"Lowest-scoring texts"}),a.jsx(ns,{docs:o.bottom_docs})]})]}),a.jsxs("div",{className:"meta-footer",children:[a.jsx("strong",{children:"Reproducibility record"})," - model: ",a.jsx("code",{children:u.model})," (dim"," ",u.embedding_dim,") · items hash: ",a.jsx("code",{children:u.items_sha256_16})," · text column: ",a.jsx("code",{children:u.text_column})," · run:"," ",u.started_at," → ",u.finished_at," (",u.duration_seconds,"s) · numpy ",u.numpy,u.sentence_transformers&&` · sentence-transformers ${u.sentence_transformers}`,a.jsxs("div",{className:"mt small",children:["Construct reference: ",u.construct_reference||"-"]})]})]})}function Kt({k:e,v:t}){return a.jsxs("div",{className:"stat",children:[a.jsx("div",{className:"v",children:t}),a.jsx("div",{className:"k",children:e})]})}function ns({docs:e}){return a.jsx("div",{className:"table-wrap",children:a.jsxs("table",{className:"docs",children:[a.jsx("thead",{children:a.jsxs("tr",{children:[a.jsx("th",{style:{width:60},children:"Score"}),a.jsx("th",{children:"Text"})]})}),a.jsx("tbody",{children:e.map(t=>a.jsxs("tr",{children:[a.jsx("td",{className:"score",children:t.score.toFixed(3)}),a.jsx("td",{children:t.text})]},t.row))})]})})}function Zd({histogram:e}){const{counts:t,edges:n}=e,r=640,l=180,i={top:10,right:10,bottom:26,left:34},o=r-i.left-i.right,u=l-i.top-i.bottom,s=Math.max(...t,1),f=o/t.length;return a.jsxs("svg",{viewBox:`0 0 ${r} ${l}`,style:{width:"100%",maxWidth:720},children:[[.25,.5,.75,1].map(h=>{const v=i.top+u-h*u;return a.jsxs("g",{children:[a.jsx("line",{x1:i.left,x2:r-i.right,y1:v,y2:v,stroke:"#eceef1"}),a.jsx("text",{x:i.left-6,y:v+4,fontSize:"10",fill:"#98a2b3",textAnchor:"end",children:Math.round(h*s)})]},h)}),t.map((h,v)=>{const m=h/s*u;return a.jsx("rect",{x:i.left+v*f+1.5,y:i.top+u-m,width:Math.max(1,f-3),height:m,rx:"2",fill:"#7a1f3d",opacity:"0.85",children:a.jsxs("title",{children:[n[v].toFixed(3)," – ",n[v+1].toFixed(3),": ",h]})},v)}),[0,Math.floor(t.length/2),t.length].map(h=>a.jsx("text",{x:i.left+h*f,y:l-8,fontSize:"10",fill:"#98a2b3",textAnchor:"middle",children:n[h].toFixed(2)},h)),a.jsx("line",{x1:i.left,x2:r-i.right,y1:i.top+u,y2:i.top+u,stroke:"#d0d5dd"})]})}function Jd({project:e,auth:t,onProjectChanged:n,onProjectDeleted:r}){var Ho,Wo,Qo,Ko;const[l,i]=T.useState([]),[o,u]=T.useState([]),[s,f]=T.useState([]),[h,v]=T.useState([]),[m,x]=T.useState(""),[S,k]=T.useState(""),[D,d]=T.useState(""),[c,p]=T.useState(""),[g,C]=T.useState(["en"]),[j,_]=T.useState("en"),[P,$]=T.useState(!1),[L,M]=T.useState(!1),[ue,U]=T.useState(""),[kn,mr]=T.useState(!1),[Bt,Ht]=T.useState(null),[N,z]=T.useState(!1),[R,H]=T.useState(""),X=T.useRef(null);async function Wt(){try{await G.patchProject(e.id,{archived:!e.archived}),n==null||n()}catch(w){U(w.message)}}async function Ge(){try{await G.deleteProject(e.id),z(!1),r==null||r()}catch(w){U(w.message)}}const Be=T.useCallback(()=>G.listJobs(e.id).then(v).catch(()=>{}),[e.id]);T.useEffect(()=>{G.listCorpora(e.id).then(i).catch(w=>U(w.message)),G.listConstructs().then(u).catch(w=>U(w.message)),G.models().then(w=>{f(w);const Pe=w.find(jt=>jt.default)||w[0];Pe&&p(Pe.id)}).catch(w=>U(w.message)),G.languages().then(C).catch(()=>{}),Be()},[e.id,Be]);const He=h.some(w=>w.status==="queued"||w.status==="running");T.useEffect(()=>{if(!He)return;const w=setInterval(Be,1200);return()=>clearInterval(w)},[He,Be]);const ve=l.find(w=>w.id===m)||null,_t=o.find(w=>w.id===D)||null;async function vc(w){var jt;const Pe=(jt=w.target.files)==null?void 0:jt[0];if(Pe){$(!0),U("");try{const Cn=await G.uploadCorpus(e.id,Pe),wc=await G.listCorpora(e.id);i(wc),x(Cn.id),k(Cn.suggested_text_column||Cn.columns[0])}catch(Cn){U(Cn.message)}finally{$(!1),X.current&&(X.current.value="")}}}async function gc(){M(!0),U("");try{await G.createJob({project_id:e.id,corpus_id:m,construct_id:D,text_column:S,model_name:c,language:j}),await Be()}catch(w){U(w.message)}finally{M(!1)}}if(Bt)return a.jsx(Gd,{jobId:Bt,onBack:()=>{Ht(null),Be()}});const yc=m&&S&&D&&c&&!L;return a.jsxs(a.Fragment,{children:[ue&&a.jsx("div",{className:"error-banner",onClick:()=>U(""),children:ue}),a.jsxs("div",{className:"project-header",children:[a.jsxs("div",{children:[a.jsx("span",{className:"project-title",children:e.name}),e.archived&&a.jsx("span",{className:"pill queued",children:"archived"})]}),a.jsxs("div",{className:"row",children:[a.jsx("button",{className:"ghost",onClick:Wt,children:e.archived?"Unarchive":"Archive"}),a.jsx("button",{className:"ghost danger",onClick:()=>z(!0),children:"Delete"})]})]}),N&&a.jsx("div",{className:"modal-backdrop",onClick:()=>z(!1),children:a.jsxs("div",{className:"modal",onClick:w=>w.stopPropagation(),children:[a.jsxs("h3",{children:['Delete "',e.name,'"?']}),a.jsxs("p",{className:"hint",children:["This permanently deletes ",l.length," dataset",l.length===1?"":"s",","," ",h.length," run",h.length===1?"":"s",", and all uploaded and result files. This cannot be undone. If you might need it later, use Archive instead."]}),a.jsxs("label",{className:"field",children:["Type the project name to confirm",a.jsx("input",{type:"text",autoFocus:!0,value:R,onChange:w=>H(w.target.value),placeholder:e.name})]}),a.jsxs("div",{className:"row",children:[a.jsx("button",{className:"primary danger-solid",disabled:R!==e.name,onClick:Ge,children:"Delete permanently"}),a.jsx("button",{className:"ghost",onClick:()=>{z(!1),H("")},children:"Cancel"})]})]})}),a.jsxs("div",{className:"card",children:[a.jsxs("h3",{children:[a.jsx("span",{className:"step-badge",children:"1"}),"Corpus"]}),a.jsxs("p",{className:"hint",children:["Upload a CSV or XLSX file, then choose the column containing the text to analyze.",t&&!t.signed_in&&((Ho=t.limits)==null?void 0:Ho.max_rows)&&a.jsxs(a.Fragment,{children:[" ","Anonymous limit: ",Math.round(t.limits.max_bytes/1048576)," MB /"," ",t.limits.max_rows.toLocaleString()," rows per file; sign in (top right) for larger uploads."]})]}),a.jsxs("div",{className:"row",children:[a.jsxs("div",{className:"grow",children:[a.jsxs("label",{className:"field",children:["Upload file",a.jsx("input",{ref:X,type:"file",accept:".csv,.xlsx,.xls",onChange:vc,disabled:P})]}),P&&a.jsx("span",{className:"small muted",children:"Uploading…"})]}),a.jsx("div",{className:"grow",children:a.jsxs("label",{className:"field",children:["Corpus",a.jsxs("select",{value:m,onChange:w=>x(w.target.value),children:[a.jsx("option",{value:"",children:"- select -"}),l.map(w=>a.jsxs("option",{value:w.id,children:[w.filename," (",w.n_rows.toLocaleString()," rows)"]},w.id))]})]})}),a.jsx("div",{className:"grow",children:a.jsxs("label",{className:"field",children:["Text column",a.jsxs("select",{value:S,onChange:w=>k(w.target.value),disabled:!ve,children:[a.jsx("option",{value:"",children:"- select -"}),ve==null?void 0:ve.columns.map(w=>a.jsxs("option",{value:w,children:[w,w===ve.suggested_text_column?" (suggested)":""]},w))]})]})})]}),((Wo=ve==null?void 0:ve.parse_info)==null?void 0:Wo.note)&&a.jsxs("p",{className:"small muted",children:["⚠ ",ve.parse_info.note]})]}),a.jsxs("div",{className:"card",children:[a.jsxs("h3",{children:[a.jsx("span",{className:"step-badge",children:"2"}),"Construct"]}),a.jsx("p",{className:"hint",children:"Pick a validated scale from the library, or define custom items. CCR scores each text by its similarity to these items."}),a.jsxs("div",{className:"construct-row",children:[a.jsx("div",{className:"grow",children:a.jsxs("select",{value:D,onChange:w=>d(w.target.value),children:[a.jsx("option",{value:"",children:"- select construct -"}),o.map(w=>a.jsxs("option",{value:w.id,children:[w.name," (",w.items.length," items",w.is_seed?", library":", custom",")"]},w.id))]})}),a.jsx("button",{className:"ghost",onClick:()=>mr(w=>!w),children:kn?"Close":"+ Custom construct"})]}),_t&&a.jsxs(a.Fragment,{children:[a.jsx("ul",{className:"construct-items",children:_t.items.map((w,Pe)=>{var jt;return a.jsxs("li",{children:[w,(jt=_t.reverse_scored)!=null&&jt[Pe]?" (reverse-scored)":""]},Pe)})}),_t.reference&&a.jsxs("p",{className:"small muted mt",children:["Reference: ",_t.reference]}),_t.verification_status!=="verified"&&a.jsxs("p",{className:"small muted",children:["⚠ Item wording not yet verified verbatim against the original publication (status: ",_t.verification_status.replace("_"," "),")."]})]}),kn&&a.jsx(qd,{onCreated:async w=>{const Pe=await G.listConstructs();u(Pe),d(w.id),mr(!1)},onError:U})]}),a.jsxs("div",{className:"card",children:[a.jsxs("h3",{children:[a.jsx("span",{className:"step-badge",children:"3"}),"Language, model & run"]}),a.jsx("p",{className:"hint",children:"Embeddings run locally via sentence-transformers; model and language are recorded in the run metadata. If the corpus doesn't match the selected language or the model doesn't support it, you'll get a warning - never a silent result."}),a.jsxs("div",{className:"run-settings",children:[a.jsxs("label",{className:"field language-control",children:["Text language",a.jsx("select",{value:j,onChange:w=>_(w.target.value),children:g.map(w=>a.jsx("option",{value:w,children:w},w))})]}),a.jsxs("label",{className:"field model-control",children:["Embedding model",a.jsx("select",{value:c,onChange:w=>p(w.target.value),children:s.map(w=>a.jsx("option",{value:w.id,children:w.label},w.id))})]}),a.jsx("button",{className:"primary run-button",disabled:!yc,onClick:gc,children:L?"Starting…":"Run CCR analysis"})]}),(Ko=(Qo=s.find(w=>w.id===c))==null?void 0:Qo.warnings)==null?void 0:Ko.map((w,Pe)=>a.jsxs("p",{className:"small muted",children:["⚠ ",w]},Pe))]}),h.length>0&&a.jsxs("div",{className:"card",children:[a.jsx("h3",{children:"Runs"}),a.jsx("div",{className:"table-wrap",children:a.jsxs("table",{className:"docs",children:[a.jsx("thead",{children:a.jsxs("tr",{children:[a.jsx("th",{children:"Started"}),a.jsx("th",{children:"Corpus"}),a.jsx("th",{children:"Construct"}),a.jsx("th",{style:{width:"24%"},children:"Status"}),a.jsx("th",{})]})}),a.jsx("tbody",{children:h.map(w=>a.jsxs("tr",{children:[a.jsx("td",{className:"muted",children:(w.started_at||w.created_at).replace("T"," ").slice(0,16)}),a.jsx("td",{children:w.corpus_filename}),a.jsx("td",{children:w.construct_name}),a.jsxs("td",{children:[w.status==="running"?a.jsx("div",{className:"progress-track",title:`${Math.round(w.progress*100)}%`,children:a.jsx("div",{className:"progress-fill",style:{width:`${Math.max(3,w.progress*100)}%`}})}):a.jsx("span",{className:`pill ${w.status}`,children:w.status}),w.status==="failed"&&a.jsx("div",{className:"small muted",title:w.error,children:w.error.split(` +`).pop()})]}),a.jsx("td",{children:w.status==="completed"&&a.jsx("button",{className:"linkish",onClick:()=>Ht(w.id),children:"View results"})})]},w.id))})]})})]})]})}function qd({onCreated:e,onError:t}){const[n,r]=T.useState(""),[l,i]=T.useState(""),[o,u]=T.useState(""),[s,f]=T.useState(!1);async function h(v){v.preventDefault();const m=o.split(` +`).map(x=>x.trim()).filter(Boolean);if(!n.trim()||m.length===0){t("A custom construct needs a name and at least one item (one per line).");return}f(!0);try{const x=await G.createConstruct({name:n.trim(),reference:l,items:m});e(x)}catch(x){t(x.message)}finally{f(!1)}}return a.jsxs("form",{onSubmit:h,className:"mt",children:[a.jsxs("div",{className:"row",children:[a.jsx("div",{className:"grow",children:a.jsxs("label",{className:"field",children:["Name",a.jsx("input",{type:"text",value:n,onChange:v=>r(v.target.value)})]})}),a.jsx("div",{className:"grow",children:a.jsxs("label",{className:"field",children:["Reference (publication, optional)",a.jsx("input",{type:"text",value:l,onChange:v=>i(v.target.value)})]})})]}),a.jsxs("label",{className:"field",children:["Scale items - one per line, verbatim from the validated instrument",a.jsx("textarea",{rows:5,value:o,onChange:v=>u(v.target.value)})]}),a.jsx("button",{className:"primary",type:"submit",disabled:s,children:s?"Saving…":"Save construct"})]})}function bd(e){if(!e)return"";const t=new Date(e.endsWith("Z")||e.includes("+")?e:e+"Z"),n=Math.max(0,Math.floor((Date.now()-t.getTime())/6e4));if(n<1)return"just now";if(n<60)return`${n}m ago`;const r=Math.floor(n/60);if(r<24)return`${r}h ago`;const l=Math.floor(r/24);return l<7?`${l}d ago`:t.toISOString().slice(0,10)}function ep(e){const t=Date.now(),n=864e5,r={Today:[],"This week":[],Earlier:[],Archived:[]};for(const l of e){if(l.archived){r.Archived.push(l);continue}const i=l.last_activity_at||l.created_at,o=new Date(i.endsWith("Z")||i.includes("+")?i:i+"Z").getTime(),u=t-o;ul.length>0)}function tp(){var L;const[e,t]=T.useState([]),[n,r]=T.useState(null),[l,i]=T.useState(!1),[o,u]=T.useState(""),[s,f]=T.useState(""),[h,v]=T.useState(""),[m,x]=T.useState(null),[S,k]=T.useState(!1),[D,d]=T.useState(""),c=()=>G.listProjects().then(t).catch(M=>v(M.message)),p=()=>G.authMe().then(x).catch(()=>{});T.useEffect(()=>{c(),p()},[]);async function g(M){if(M.preventDefault(),!!D.trim())try{await G.demoLogin(D.trim()),k(!1),d(""),await p()}catch(ue){v(ue.message)}}async function C(){try{await G.logout(),await p()}catch(M){v(M.message)}}T.useEffect(()=>{if(e.length===0){r(null);return}(!n||!e.some(M=>M.id===n))&&r(e[0].id)},[e,n]);async function j(M){if(M.preventDefault(),!!o.trim())try{const ue=await G.createProject({name:o.trim()});u(""),i(!1),await c(),r(ue.id)}catch(ue){v(ue.message)}}const _=e.find(M=>M.id===n)||null,P=s.trim().toLowerCase(),$=e.filter(M=>M.name.toLowerCase().includes(P));return a.jsxs("div",{className:"app",children:[a.jsxs("header",{className:"header",children:[a.jsx("h1",{children:"CCR Platform"}),a.jsx("span",{className:"sub",children:"Contextualized Construct Representations · theory-driven psychological text analysis"}),a.jsx("span",{className:"header-auth",children:m!=null&&m.signed_in?a.jsxs(a.Fragment,{children:[a.jsxs("span",{className:"small",children:["Hi, ",m.name]}),a.jsx("button",{className:"header-btn",onClick:C,children:"Sign out"})]}):a.jsx("button",{className:"header-btn",onClick:()=>k(!0),children:"Sign in"})})]}),S&&a.jsx("div",{className:"modal-backdrop",onClick:()=>k(!1),children:a.jsxs("div",{className:"modal",onClick:M=>M.stopPropagation(),children:[a.jsx("h3",{children:"Sign in"}),a.jsxs("p",{className:"hint",children:["Signing in lifts the anonymous upload limits",(L=m==null?void 0:m.limits)!=null&&L.max_rows?` (currently ${Math.round(m.limits.max_bytes/1048576)} MB / ${m.limits.max_rows.toLocaleString()} rows per file)`:"","."]}),a.jsx("button",{className:"ghost",disabled:!0,title:"Arrives with lab accounts",children:"Sign in with Google (coming soon)"}),a.jsxs("form",{onSubmit:g,className:"mt",children:[a.jsxs("label",{className:"field",children:["Your name (placeholder sign-in for now)",a.jsx("input",{type:"text",autoFocus:!0,value:D,onChange:M=>d(M.target.value),placeholder:"e.g. Mohammad"})]}),a.jsxs("div",{className:"row",children:[a.jsx("button",{className:"primary",type:"submit",disabled:!D.trim(),children:"Continue"}),a.jsx("button",{className:"ghost",type:"button",onClick:()=>k(!1),children:"Cancel"})]})]}),a.jsx("p",{className:"small muted mt",children:"This is a temporary placeholder so larger uploads can be tested. Real sign-in (Google + university account) arrives with lab accounts."})]})}),a.jsxs("div",{className:"layout",children:[a.jsxs("aside",{className:"sidebar",children:[a.jsxs("h2",{children:["Projects",e.length>0&&a.jsx("span",{className:"count",children:e.length})]}),a.jsx("input",{type:"text",className:"sidebar-filter",placeholder:"Search projects...",value:s,onChange:M=>f(M.target.value)}),a.jsxs("div",{className:"project-list",children:[ep($).map(([M,ue])=>a.jsxs("div",{children:[a.jsx("div",{className:"group-label",children:M}),ue.map(U=>a.jsxs("button",{className:"project-item"+(U.id===n?" active":""),onClick:()=>r(U.id),title:U.name,children:[a.jsx("span",{className:"project-name",children:U.name}),a.jsxs("span",{className:"date",children:[U.n_runs>0?`${U.n_runs} run${U.n_runs===1?"":"s"} · `:"",bd(U.last_activity_at||U.created_at)]})]},U.id))]},M)),s&&$.length===0&&a.jsxs("p",{className:"small muted",children:['No projects match "',s,'".']})]}),a.jsx("div",{className:"project-create",children:l?a.jsxs("form",{onSubmit:j,children:[a.jsx("input",{type:"text",autoFocus:!0,placeholder:"Project name",value:o,onChange:M=>u(M.target.value)}),a.jsxs("div",{className:"row mt",children:[a.jsx("button",{className:"primary",type:"submit",children:"Create"}),a.jsx("button",{className:"ghost",type:"button",onClick:()=>i(!1),children:"Cancel"})]})]}):a.jsx("button",{className:"ghost",onClick:()=>i(!0),children:"+ New project"})})]}),a.jsxs("main",{className:"main",children:[h&&a.jsx("div",{className:"error-banner",onClick:()=>v(""),children:h}),_?a.jsx(Jd,{project:_,auth:m,onProjectChanged:c,onProjectDeleted:()=>{r(null),c()}},_.id):a.jsxs("div",{className:"card",children:[a.jsx("h3",{children:"Welcome"}),a.jsx("p",{className:"hint",children:"Create or select a project, upload a corpus (CSV/XLSX), choose a validated construct, and run a CCR analysis. Results include per-item loadings, score distributions, and a reproducibility record for every run."}),a.jsx("p",{className:"small muted",children:"Self-contained by design: embeddings run on this server itself - no third-party AI APIs. Demo instance: storage is ephemeral and may reset; please don't upload sensitive or identifiable data."})]})]})]})]})}hc(document.getElementById("root")).render(a.jsx(Fc.StrictMode,{children:a.jsx(tp,{})})); diff --git a/backend/static/index.html b/backend/static/index.html index cd03bc0bb7fc3dbf2b33101ff2dba84a9b8cfdd2..4188c4f2064bd5f678e23be02232c68ffc034c9f 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -4,8 +4,8 @@ CCR Platform — Contextualized Construct Representations - - + +
diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 12f277ee02ab497fd82aac71e792a249ffbc028b..24db6a29bee44f61cc211f362d24d1d2b26d2864 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -1,8 +1,8 @@ """End-to-end API tests: project -> upload -> job -> results -> export. Run against the fake embedding backend (model_name = 'fake-deterministic'), -exercising the full pipeline — tolerant upload parsing, the real job queue -(worker thread + polling), results summary, warnings, and export shape — +exercising the full pipeline - tolerant upload parsing, the real job queue +(worker thread + polling), results summary, warnings, and export shape - without ML dependencies. """ @@ -81,7 +81,7 @@ def test_health(client): def test_seed_constructs_present(client): names = {c["name"] for c in client.get("/api/constructs").json()} assert "Satisfaction with Life" in names - assert "Moral Foundations — Care" in names + assert "Moral Foundations - Care" in names def test_corpus_upload_parses_columns(flow): diff --git a/backend/tests/test_auth_tiers_and_lifecycle.py b/backend/tests/test_auth_tiers_and_lifecycle.py new file mode 100644 index 0000000000000000000000000000000000000000..f5994a9dbc29b1e65f9524b5bca72a720b9e53f5 --- /dev/null +++ b/backend/tests/test_auth_tiers_and_lifecycle.py @@ -0,0 +1,134 @@ +"""Demo sign-in tiers (anonymous caps lift on sign-in) and project lifecycle +(archive is reversible; delete cascades to files and rows).""" + +import io +import time +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from app.main import app + + +def upload(client, project_id, name, payload: bytes): + return client.post( + f"/api/projects/{project_id}/corpora", + files={"file": (name, io.BytesIO(payload), "text/csv")}, + ) + + +def wait_for_job(client, job_id, timeout=10.0): + deadline = time.time() + timeout + while time.time() < deadline: + job = client.get(f"/api/jobs/{job_id}").json() + if job["status"] in ("completed", "failed"): + return job + time.sleep(0.05) + raise TimeoutError(job_id) + + +@pytest.fixture() +def client(): + with TestClient(app) as c: + yield c + + +def csv_rows(n: int) -> bytes: + return ("text\n" + "\n".join(f"sample sentence number {i} here" for i in range(n))).encode() + + +# ------------------------------------------------------------------- tiers +def test_anonymous_row_cap_and_signin_lifts_it(client, monkeypatch): + monkeypatch.setenv("CCR_ANON_MAX_ROWS", "5") + project = client.post("/api/projects", json={"name": "Tiers"}).json() + + me = client.get("/api/auth/me").json() + assert me["signed_in"] is False and me["limits"]["max_rows"] == 5 + + resp = upload(client, project["id"], "big.csv", csv_rows(10)) + assert resp.status_code == 400 + assert "Sign in" in resp.json()["detail"] + + assert client.post("/api/auth/demo/login", json={"name": "Deva"}).status_code == 200 + me = client.get("/api/auth/me").json() + assert me["signed_in"] is True and me["name"] == "Deva" + + resp = upload(client, project["id"], "big.csv", csv_rows(10)) + assert resp.status_code == 201, resp.json() + + client.post("/api/auth/logout") + assert client.get("/api/auth/me").json()["signed_in"] is False + + +def test_anonymous_size_cap(client, monkeypatch): + monkeypatch.setenv("CCR_ANON_MAX_BYTES", "200") + project = client.post("/api/projects", json={"name": "SizeCap"}).json() + resp = upload(client, project["id"], "big.csv", csv_rows(50)) + assert resp.status_code == 413 + assert "Sign in" in resp.json()["detail"] + + +def test_tampered_session_cookie_is_anonymous(client): + client.cookies.set("ccr_demo_session", "aGFja2Vy.badsignature") + assert client.get("/api/auth/me").json()["signed_in"] is False + + +# --------------------------------------------------------------- lifecycle +def test_archive_toggle_is_reversible(client): + project = client.post("/api/projects", json={"name": "Archivable"}).json() + assert project["archived"] is False + + patched = client.patch(f"/api/projects/{project['id']}", json={"archived": True}).json() + assert patched["archived"] is True + listed = next(p for p in client.get("/api/projects").json() if p["id"] == project["id"]) + assert listed["archived"] is True + + patched = client.patch(f"/api/projects/{project['id']}", json={"archived": False}).json() + assert patched["archived"] is False + + +def test_delete_cascades_rows_and_files(client): + project = client.post("/api/projects", json={"name": "Doomed"}).json() + corpus = upload(client, project["id"], "corpus.csv", csv_rows(6)).json() + + constructs = client.get("/api/constructs").json() + swls = next(c for c in constructs if c["name"] == "Satisfaction with Life") + job = client.post( + "/api/jobs", + json={ + "project_id": project["id"], + "corpus_id": corpus["id"], + "construct_id": swls["id"], + "text_column": "text", + "model_name": "fake-deterministic", + }, + ).json() + job = wait_for_job(client, job["id"]) + assert job["status"] == "completed" + + # capture file paths before deletion + results = client.get(f"/api/jobs/{job['id']}/results") + assert results.status_code == 200 + + resp = client.delete(f"/api/projects/{project['id']}") + assert resp.status_code == 204 + + assert client.get(f"/api/jobs/{job['id']}").status_code == 404 + assert all(p["id"] != project["id"] for p in client.get("/api/projects").json()) + # corpora listing for the deleted project 404s + assert client.get(f"/api/projects/{project['id']}/corpora").status_code == 404 + + +def test_delete_removes_files_on_disk(client, tmp_path): + import os + + data_dir = Path(os.environ["CCR_DATA_DIR"]) + project = client.post("/api/projects", json={"name": "FileCheck"}).json() + before = set((data_dir / "corpora").glob("*")) + upload(client, project["id"], "corpus.csv", csv_rows(6)) + created = set((data_dir / "corpora").glob("*")) - before + assert len(created) == 1 + + client.delete(f"/api/projects/{project['id']}") + assert not created.pop().exists() diff --git a/backend/tests/test_ccr.py b/backend/tests/test_ccr.py index 6cf1dd8d85305546052970a05f08d1c88c31f306..225c8692e64617bf7438df6f002dd348571125cf 100644 --- a/backend/tests/test_ccr.py +++ b/backend/tests/test_ccr.py @@ -1,4 +1,4 @@ -"""Unit tests for the CCR engine (deterministic fake backend — no torch).""" +"""Unit tests for the CCR engine (deterministic fake backend - no torch).""" import numpy as np import pytest diff --git a/docs/specs/0001-data-quality-warnings.md b/docs/specs/0001-data-quality-warnings.md index 080c34528f29db45f1fee9afecfc290b81d5bf7e..381dd2bfba09e97fa6586fb92b2bd4a6f102cc2f 100644 --- a/docs/specs/0001-data-quality-warnings.md +++ b/docs/specs/0001-data-quality-warnings.md @@ -1,13 +1,13 @@ -# Spec 0001 — Data-quality warnings: very short texts + likely wrong language +# Spec 0001 - Data-quality warnings: very short texts + likely wrong language **Status:** implemented (2026-07-09) **Phase:** 0 **Design doc ref:** §12 (language), §11 pipeline step 3 (Text QA) -**Requested by:** Mohammad (Slack brief: "proper warnings — wrong language, super short texts, super long texts") +**Requested by:** Mohammad (Slack brief: "proper warnings - wrong language, super short texts, super long texts") ## Problem Researchers upload corpora with rows that embed unreliably (very short texts) or that don't match the language they selected. Today the platform warns about empties, duplicates, and -truncation-length texts, but not about short texts or language mismatch — so silent validity +truncation-length texts, but not about short texts or language mismatch - so silent validity problems reach published results. ## Contract @@ -25,7 +25,7 @@ problems reach published results. **Language detection design:** - Corpus-level only: sample up to 200 rows with ≥ 5 tokens; if < 20 such rows → LANGUAGE_UNCERTAIN. -- Library: `lingua-language-detector` (pure-Python wheels, no torch) — add to requirements; record library+version in metadata. +- Library: `lingua-language-detector` (pure-Python wheels, no torch) - add to requirements; record library+version in metadata. - Never block on language; user may proceed (design §12 user-override rule). **Warning object schema (all warnings migrate to this shape):** @@ -51,12 +51,12 @@ splitting (future spec). - golden: sample_corpus.csv against SWLS produces byte-stable warning set ## Implementation notes -- Warning construction centralizes in a new `warnings.py` module (engine-side, pure) — first +- Warning construction centralizes in a new `warnings.py` module (engine-side, pure) - first extraction step toward packages/ccr_engine. - Detection runs once at job start on the parsed dataframe; result flows into metadata + warnings. -- UI: no new components — existing warnings panel renders the message strings. +- UI: no new components - existing warnings panel renders the message strings. ## Deviations (filled after implementation) -- Detector: `langdetect` (seeded, deterministic) instead of lingua — ~1 MB pure-Python vs ~100 MB wheels; detector + version recorded in metadata so an upgrade stays traceable. +- Detector: `langdetect` (seeded, deterministic) instead of lingua - ~1 MB pure-Python vs ~100 MB wheels; detector + version recorded in metadata so an upgrade stays traceable. - LANGUAGE_MISMATCH threshold implemented as majority-share >= 0.70 across sampled rows. - MODEL_NOTE (info) added: registry user_warnings surface per run (not in original spec). diff --git a/docs/specs/0002-python-script-export.md b/docs/specs/0002-python-script-export.md index 2f3d8f450cf1df325d3c1a80664af5bf8ea81acd..107afc8c3fd543e40703d340b7e12a36f92a6578 100644 --- a/docs/specs/0002-python-script-export.md +++ b/docs/specs/0002-python-script-export.md @@ -1,4 +1,4 @@ -# Spec 0002 — Reproduction script export (offline-runnable) +# Spec 0002 - Reproduction script export (offline-runnable) **Status:** implemented (2026-07-09) **Phase:** 0 **Design doc ref:** §14 diff --git a/docs/specs/0003-model-registry-and-language-selection.md b/docs/specs/0003-model-registry-and-language-selection.md index 6f5aea3fe27ede723c9b66edf4f9de101c791d0f..8d6be4a5a8e38d3a6f3d5ed8f55243eaa0623035 100644 --- a/docs/specs/0003-model-registry-and-language-selection.md +++ b/docs/specs/0003-model-registry-and-language-selection.md @@ -1,4 +1,4 @@ -# Spec 0003 — Registry-driven models + language selection (brief) +# Spec 0003 - Registry-driven models + language selection (brief) **Status:** implemented (2026-07-09) **Phase:** 0 **Design doc ref:** §12, §13 @@ -12,5 +12,5 @@ into spec-0001 warnings. Add e5-large-v2 and multilingual-e5-base to the UI via Validator (`validate_models.py`) runs in tests. Pin revisions before Phase 1 exit. **Key tests:** loader validation errors, prefix application for E5 (assert embedded strings -receive "query: " — testable via fake backend capturing inputs), /api/models shape, metadata +receive "query: " - testable via fake backend capturing inputs), /api/models shape, metadata records registry snapshot, MiniLM stays default. diff --git a/docs/specs/0004-construct-library-yaml.md b/docs/specs/0004-construct-library-yaml.md index f4e931004453e76ecc1f47bd1455d105ef6b3144..71f61b9572f3f0ac777e2567755372f66cfef2a8 100644 --- a/docs/specs/0004-construct-library-yaml.md +++ b/docs/specs/0004-construct-library-yaml.md @@ -1,4 +1,4 @@ -# Spec 0004 — Construct library from YAML (brief) +# Spec 0004 - Construct library from YAML (brief) **Status:** implemented (2026-07-09) **Phase:** 0/1 **Design doc ref:** §10.1, §11 @@ -7,7 +7,7 @@ Replace `backend/app/seed_constructs.py` seeding with `packages/construct_library/constructs/*.yaml` as the source of truth: loader validates via the same rules as `validate_constructs.py`, computes item_hash with the reference algorithm, seeds/updates DB rows keyed -(construct_id, version) — append-only, never mutate an existing version. Run metadata embeds +(construct_id, version) - append-only, never mutate an existing version. Run metadata embeds the full construct snapshot. UI shows verification_status (verified prominently; others with a "wording not yet verified" chip). New questionnaires from the lab = new YAML files + validator run; item wordings marked needs_verification until checked verbatim against the publication. diff --git a/docs/specs/0005-engine-extraction-and-parity.md b/docs/specs/0005-engine-extraction-and-parity.md index a0603ff27edf4032ba5dbb4c91b4d8cc0a50fd21..090655be98a29a51bf9edf299fb269f7b3cda925 100644 --- a/docs/specs/0005-engine-extraction-and-parity.md +++ b/docs/specs/0005-engine-extraction-and-parity.md @@ -1,6 +1,6 @@ -# Spec 0005 — ccr_engine extraction + reference parity tests (brief) +# Spec 0005 - ccr_engine extraction + reference parity tests (brief) -**Status:** draft (Phase 1 opener — refine after Phase 0 ships) +**Status:** draft (Phase 1 opener - refine after Phase 0 ships) **Phase:** 1 **Design doc ref:** §7 (strangler steps 1–3), §10, §16 Extract the pure analysis package per the strangler plan: move similarity/scoring/warnings/ @@ -8,7 +8,7 @@ stats/reproducibility logic from backend/app into `packages/ccr_engine/` with th `run_ccr_analysis(...)` interface (design §10); backend becomes an orchestration layer. Freeze behavior first with golden datasets (evals/golden_datasets → evals/expected_outputs, human- approved). Then add reference parity: same corpus + items through the published `ccr_wrapper` -(pyccr) and through ccr_engine — similarities match within atol (1e-5 target), pinned model +(pyccr) and through ccr_engine - similarities match within atol (1e-5 target), pinned model revisions on BOTH paths, run in the same container as production. Document any expected differences. Editable-install workspace wiring so backend imports the package (no sys.path hacks). diff --git a/docs/specs/TEMPLATE.md b/docs/specs/TEMPLATE.md index 6c289dea5e16a7286384788493dd0549a46c505f..c20ced92c0d5dcf42dca918b5f626bdeba09a112 100644 --- a/docs/specs/TEMPLATE.md +++ b/docs/specs/TEMPLATE.md @@ -1,4 +1,4 @@ -# Spec NNNN — +# Spec NNNN - **Status:** draft | approved | implemented (+ deviations noted below) **Phase:** 0 | 1 | 2 | 3 **Design doc ref:** § diff --git a/evals/README.md b/evals/README.md index 3752600f8930367c128eb3b14286ef579a8814e1..c1ae6f532cf21069371d47961ca5b3a2ba916b0d 100644 --- a/evals/README.md +++ b/evals/README.md @@ -1,10 +1,10 @@ # Golden evals -Purpose: catch silent research-behavior drift — output shape, column names/order, metadata -fields, warning codes — per design doc §16. +Purpose: catch silent research-behavior drift - output shape, column names/order, metadata +fields, warning codes - per design doc §16. -- `golden_datasets/` — frozen input corpora (never edit rows in place; add new files). -- `expected_outputs/` — human-approved expected results. **Protected by a PreToolUse hook:** +- `golden_datasets/` - frozen input corpora (never edit rows in place; add new files). +- `expected_outputs/` - human-approved expected results. **Protected by a PreToolUse hook:** Claude Code cannot edit files here. To update after an approved behavior change, regenerate manually and review the diff yourself before committing. - Runner arrives with spec 0005 (engine extraction freezes behavior first). Until then, the diff --git a/frontend/index.html b/frontend/index.html index f20239879df9b2fb69576502ec73babfbdc12e2d..d654ed2b37053c6d02a2b692b981276826d5f77b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@ - CCR Platform — Contextualized Construct Representations + CCR Platform - Contextualized Construct Representations
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a392275682799fafc699903cf33e4cf3d1085733..e97853a70d94a20d43f52c20419c43503c48610f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -16,12 +16,17 @@ function relativeTime(iso) { } function groupProjects(projects) { - // Buckets by last activity: Today / This week / Earlier. Projects arrive + // Buckets by last activity: Today / This week / Earlier, with archived + // projects collapsed into their own group at the bottom. Projects arrive // sorted by last activity (backend), so group order falls out naturally. const now = Date.now(); const DAY = 86400000; - const groups = { Today: [], "This week": [], Earlier: [] }; + const groups = { Today: [], "This week": [], Earlier: [], Archived: [] }; for (const p of projects) { + if (p.archived) { + groups.Archived.push(p); + continue; + } const iso = p.last_activity_at || p.created_at; const t = new Date(iso.endsWith("Z") || iso.includes("+") ? iso : iso + "Z").getTime(); const age = now - t; @@ -39,14 +44,41 @@ export default function App() { const [newName, setNewName] = useState(""); const [filter, setFilter] = useState(""); const [error, setError] = useState(""); + const [auth, setAuth] = useState(null); + const [showLogin, setShowLogin] = useState(false); + const [loginName, setLoginName] = useState(""); const loadProjects = () => api.listProjects().then(setProjects).catch((e) => setError(e.message)); + const loadAuth = () => api.authMe().then(setAuth).catch(() => {}); useEffect(() => { loadProjects(); + loadAuth(); }, []); + async function handleLogin(e) { + e.preventDefault(); + if (!loginName.trim()) return; + try { + await api.demoLogin(loginName.trim()); + setShowLogin(false); + setLoginName(""); + await loadAuth(); + } catch (err) { + setError(err.message); + } + } + + async function handleLogout() { + try { + await api.logout(); + await loadAuth(); + } catch (err) { + setError(err.message); + } + } + useEffect(() => { if (projects.length === 0) { setSelectedId(null); @@ -84,8 +116,64 @@ export default function App() { Contextualized Construct Representations · theory-driven psychological text analysis + + {auth?.signed_in ? ( + <> + Hi, {auth.name} + + + ) : ( + + )} + + {showLogin && ( +
setShowLogin(false)}> +
e.stopPropagation()}> +

Sign in

+

+ Signing in lifts the anonymous upload limits + {auth?.limits?.max_rows + ? ` (currently ${Math.round(auth.limits.max_bytes / 1048576)} MB / ${auth.limits.max_rows.toLocaleString()} rows per file)` + : ""} + . +

+ +
+ +
+ + +
+
+

+ This is a temporary placeholder so larger uploads can be tested. Real + sign-in (Google + university account) arrives with lab accounts. +

+
+
+ )} +
)} {selected ? ( - + { + setSelectedId(null); + loadProjects(); + }} + /> ) : (

Welcome

@@ -168,7 +265,7 @@ export default function App() { score distributions, and a reproducibility record for every run.

- Self-contained by design: embeddings run on this server itself — no + Self-contained by design: embeddings run on this server itself - no third-party AI APIs. Demo instance: storage is ephemeral and may reset; please don't upload sensitive or identifiable data.

diff --git a/frontend/src/ResultsView.jsx b/frontend/src/ResultsView.jsx index a6db837d8bea7673da42cb283d3ee52c280a5527..321940d7437311207a1e0f18079c2c8c08a69630 100644 --- a/frontend/src/ResultsView.jsx +++ b/frontend/src/ResultsView.jsx @@ -73,7 +73,7 @@ export default function ResultsView({ jobId, onBack }) {
  • {typeof w === "string" ? w : ( <> - {w.code} — {w.message} + {w.code} - {w.message} )}
  • @@ -91,7 +91,7 @@ export default function ResultsView({ jobId, onBack }) {

    Per-item mean loadings

    - Mean similarity of the corpus to each scale item — a face-validity check on which + Mean similarity of the corpus to each scale item - a face-validity check on which items drive the construct signal.

    {summary.item_means.map((m, i) => ( @@ -122,7 +122,7 @@ export default function ResultsView({ jobId, onBack }) {
    - Reproducibility record — model: {metadata.model} (dim{" "} + Reproducibility record - model: {metadata.model} (dim{" "} {metadata.embedding_dim}) · items hash: {metadata.items_sha256_16} · text column: {metadata.text_column} · run:{" "} {metadata.started_at} → {metadata.finished_at} ({metadata.duration_seconds}s) · @@ -130,7 +130,7 @@ export default function ResultsView({ jobId, onBack }) { {metadata.sentence_transformers && ` · sentence-transformers ${metadata.sentence_transformers}`}
    - Construct reference: {metadata.construct_reference || "—"} + Construct reference: {metadata.construct_reference || "-"}
    diff --git a/frontend/src/Workspace.jsx b/frontend/src/Workspace.jsx index 8bb9bff9998360b1ecaad0239eea21b493b5c08a..c516b6c0e70b0ab3fc7dcc05d22ec863194797ab 100644 --- a/frontend/src/Workspace.jsx +++ b/frontend/src/Workspace.jsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { api } from "./api.js"; import ResultsView from "./ResultsView.jsx"; -export default function Workspace({ project }) { +export default function Workspace({ project, auth, onProjectChanged, onProjectDeleted }) { const [corpora, setCorpora] = useState([]); const [constructs, setConstructs] = useState([]); const [models, setModels] = useState([]); @@ -19,8 +19,29 @@ export default function Workspace({ project }) { const [error, setError] = useState(""); const [showNewConstruct, setShowNewConstruct] = useState(false); const [viewJobId, setViewJobId] = useState(null); + const [confirmDelete, setConfirmDelete] = useState(false); + const [deleteText, setDeleteText] = useState(""); const fileRef = useRef(null); + async function toggleArchive() { + try { + await api.patchProject(project.id, { archived: !project.archived }); + onProjectChanged?.(); + } catch (err) { + setError(err.message); + } + } + + async function handleDelete() { + try { + await api.deleteProject(project.id); + setConfirmDelete(false); + onProjectDeleted?.(); + } catch (err) { + setError(err.message); + } + } + const refreshJobs = useCallback( () => api.listJobs(project.id).then(setJobs).catch(() => {}), [project.id] @@ -113,13 +134,78 @@ export default function Workspace({ project }) {
    )} - {/* Step 1 — corpus */} + {/* Project header + actions */} +
    +
    + {project.name} + {project.archived && archived} +
    +
    + + +
    +
    + + {confirmDelete && ( +
    setConfirmDelete(false)}> +
    e.stopPropagation()}> +

    Delete "{project.name}"?

    +

    + This permanently deletes {corpora.length} dataset{corpora.length === 1 ? "" : "s"},{" "} + {jobs.length} run{jobs.length === 1 ? "" : "s"}, and all uploaded and result files. + This cannot be undone. If you might need it later, use Archive instead. +

    + +
    + + +
    +
    +
    + )} + + {/* Step 1 - corpus */}

    1Corpus

    Upload a CSV or XLSX file, then choose the column containing the text to analyze. + {auth && !auth.signed_in && auth.limits?.max_rows && ( + <> + {" "} + Anonymous limit: {Math.round(auth.limits.max_bytes / 1048576)} MB /{" "} + {auth.limits.max_rows.toLocaleString()} rows per file; sign in (top right) for + larger uploads. + + )}

    @@ -139,7 +225,7 @@ export default function Workspace({ project }) {
    - {/* Step 3 — language, model + run */} + {/* Step 3 - language, model + run */}

    3Language, model & run @@ -240,7 +326,7 @@ export default function Workspace({ project }) {

    Embeddings run locally via sentence-transformers; model and language are recorded in the run metadata. If the corpus doesn't match the selected language or the - model doesn't support it, you'll get a warning — never a silent result. + model doesn't support it, you'll get a warning - never a silent result.