diff --git a/.DS_Store b/.DS_Store
index a8a6902..6397818 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.env b/.env
index 4d77ff8..458803c 100644
--- a/.env
+++ b/.env
@@ -1,6 +1,18 @@
# Project Codex — environment variables
# Copy this file to .env and fill in your values.
-NEO4J_URI=bolt://127.0.0.1:7687
+DRUGBANK_API_KEY=
+DRUGBANK_API_BASE=https://api.drugbank.com/discovery/v1
+DRUGBANK_RELEASE=API
+
+ICD_CLIENT_ID=
+ICD_CLIENT_SECRET=
+
+NEO4J_URI=bolt://localhost:7687
NEO4J_USER=neo4j
-NEO4J_PASSWORD=codexpassword
+NEO4J_PASSWORD=changeme
+NEO4J_DB=neo4j
+
+RATE_LIMIT_RPM=100
+LOG_LEVEL=INFO
+FORCE=1
\ No newline at end of file
diff --git a/.env.example b/.env.example
index 306639b..458803c 100644
--- a/.env.example
+++ b/.env.example
@@ -1,6 +1,18 @@
# Project Codex — environment variables
# Copy this file to .env and fill in your values.
+DRUGBANK_API_KEY=
+DRUGBANK_API_BASE=https://api.drugbank.com/discovery/v1
+DRUGBANK_RELEASE=API
+
+ICD_CLIENT_ID=
+ICD_CLIENT_SECRET=
+
NEO4J_URI=bolt://localhost:7687
NEO4J_USER=neo4j
-NEO4J_PASSWORD=codex-password
+NEO4J_PASSWORD=changeme
+NEO4J_DB=neo4j
+
+RATE_LIMIT_RPM=100
+LOG_LEVEL=INFO
+FORCE=1
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ffc495e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,30 @@
+# Dependencies
+node_modules/
+.pnp
+.pnp.js
+
+# Build outputs
+dist/
+build/
+*.egg-info/
+
+# Python cache & virtual environments
+__pycache__/
+*.py[cod]
+*$py.class
+.venv/
+venv/
+ENV/
+
+# Environment & local files
+.env
+.env.local
+.env.*.local
+
+# IDE & OS
+.idea/
+.vscode/
+*.swp
+*.swo
+.DS_Store
+Thumbs.db
diff --git a/README.md b/README.md
index 91afe26..7e3e32e 100644
--- a/README.md
+++ b/README.md
@@ -1,84 +1,23 @@
# codex-cli — Medical Translation Terminal
-Test the backend translation engine directly from your terminal.
-No frontend, no Flask, no middleware. Just Neo4j + Python.
-
---
## Setup (once)
-### 1 — Start Neo4j
```bash
-docker compose up -d
-# Wait ~20 seconds for Neo4j to be ready
+docker compose up --build
+# Wait ~30 seconds for Neo4j, frontend, and api to be ready
# Optional: check http://localhost:7474 in a browser (login: neo4j / changeme)
```
-### 2 — Install Python dependencies
-```bash
-pip install -r requirements.txt
-```
-
-### 3 — Run the CLI
-```bash
-python cli.py
-```
-
---
## Usage
```
-codex> demo ← load sample data first (run this once)
-codex> ibuprofen ← type a drug name, then answer prompts
- Target language code: es
- Country code: MX
- → ibuprofeno (Spanish) brand: Advil [MX]
-
-codex> paracetamol
- Target language code: fr
- Country code: FR
- → paracétamol (French) brand: Doliprane [FR]
-
-codex> audit ibuprofen ← show missing translations / brands
-codex> load /path/to/pack.json ← load a new language pack
-codex> quit
-```
-
-### Language codes
-Code - Language
-
-`en` - English
-`es` - Spanish
-`fr` - French
-`ru` - Russian
-`uk` - Ukrainian
-
-### Country codes
-`US GB MX FR ES NG RU UA CA PL IN DE BR AU ZA`
-
----
-
-## Language pack format
-
-```json
-{
- "language": {"code": "pt", "name": "Portuguese"},
- "terms": [
- {
- "canonical": "Ibuprofen",
- "entries": [
- {"translation": "ibuprofeno", "country": "BR", "brand": "Advil"},
- {"translation": "ibuprofeno", "country": "PT", "brand": null}
- ]
- }
- ]
-}
-```
-
-Save as `portuguese_pack.json`, then:
-```bash
-codex> load portuguese_pack.json
+Use http://localhost:9000 for the frontend
+Use http://localhost:7474 for the Neo4j backend
+Use http://localhost:8000/docs to view the API
```
---
@@ -95,7 +34,7 @@ And update `docker-compose.yml`:
NEO4J_AUTH: "neo4j/your_new_password"
```
-Then restart: `docker compose down && docker compose up -d`
+Then restart: `docker compose down && docker compose up -d && docker compose up --build`
---
@@ -104,5 +43,5 @@ Then restart: `docker compose down && docker compose up -d`
```bash
docker compose down -v # -v removes the data volume
docker compose up -d
-# Then reload demo data: codex> demo
+docker compose up --build
```
diff --git a/api/__pycache__/__init__.cpython-312.pyc b/api/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index 3be39dc..0000000
Binary files a/api/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/api/__pycache__/config.cpython-312.pyc b/api/__pycache__/config.cpython-312.pyc
deleted file mode 100644
index ee30601..0000000
Binary files a/api/__pycache__/config.cpython-312.pyc and /dev/null differ
diff --git a/api/__pycache__/db.cpython-312.pyc b/api/__pycache__/db.cpython-312.pyc
deleted file mode 100644
index 9b7cc92..0000000
Binary files a/api/__pycache__/db.cpython-312.pyc and /dev/null differ
diff --git a/api/__pycache__/dependencies.cpython-312.pyc b/api/__pycache__/dependencies.cpython-312.pyc
deleted file mode 100644
index c4eb148..0000000
Binary files a/api/__pycache__/dependencies.cpython-312.pyc and /dev/null differ
diff --git a/api/__pycache__/main.cpython-312.pyc b/api/__pycache__/main.cpython-312.pyc
deleted file mode 100644
index e43eda0..0000000
Binary files a/api/__pycache__/main.cpython-312.pyc and /dev/null differ
diff --git a/api/config.py b/api/config.py
deleted file mode 100644
index 09208df..0000000
--- a/api/config.py
+++ /dev/null
@@ -1,17 +0,0 @@
-"""
-Project Codex — API Configuration
-Reads from .env file with fallback defaults.
-"""
-
-import os
-from dotenv import load_dotenv
-
-load_dotenv()
-
-NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
-NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
-NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "codex-password")
-
-API_TITLE = "Project Codex API"
-API_DESCRIPTION = "REST API for cross-country drug name translation and lookup, backed by Neo4j."
-API_VERSION = "0.1.0"
diff --git a/api/models/condition.py b/api/models/condition.py
deleted file mode 100644
index aa203e8..0000000
--- a/api/models/condition.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""
-Project Codex — Pydantic response models for Condition-related endpoints.
-"""
-
-from typing import Optional
-from pydantic import BaseModel
-
-
-class Condition(BaseModel):
- canonical_name: str
- source: str
- source_id: str
- is_poc: Optional[bool] = None
-
-
-class ConditionDetail(Condition):
- pass
-
-
-class ConditionDrug(BaseModel):
- canonical_name: str
- source: str
- source_id: str
- evidence_level: Optional[str] = None
diff --git a/api/models/drug.py b/api/models/drug.py
deleted file mode 100644
index 9e4f022..0000000
--- a/api/models/drug.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""
-Project Codex — Pydantic response models for Drug-related endpoints.
-"""
-
-from typing import Optional
-from pydantic import BaseModel
-
-
-class DrugName(BaseModel):
- name: str
- country: Optional[str] = None
- language: Optional[str] = None
- name_type: Optional[str] = None
- is_primary: Optional[bool] = None
-
-
-class Drug(BaseModel):
- canonical_name: str
- source: str
- source_id: str
- is_poc: Optional[bool] = None
-
-
-class DrugDetail(Drug):
- names: list[DrugName] = []
-
-
-class DrugInteraction(BaseModel):
- canonical_name: str
- source_id: str
- severity: Optional[str] = None
- description: Optional[str] = None
-
-
-class TranslationResult(BaseModel):
- canonical_name: str
- translated_name: str
- language: Optional[str] = None
- name_type: Optional[str] = None
diff --git a/api/routers/__pycache__/__init__.cpython-312.pyc b/api/routers/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index 10e51bc..0000000
Binary files a/api/routers/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/api/routers/__pycache__/conditions.cpython-312.pyc b/api/routers/__pycache__/conditions.cpython-312.pyc
deleted file mode 100644
index d348803..0000000
Binary files a/api/routers/__pycache__/conditions.cpython-312.pyc and /dev/null differ
diff --git a/api/routers/__pycache__/drugs.cpython-312.pyc b/api/routers/__pycache__/drugs.cpython-312.pyc
deleted file mode 100644
index 879241f..0000000
Binary files a/api/routers/__pycache__/drugs.cpython-312.pyc and /dev/null differ
diff --git a/api/routers/__pycache__/sources.cpython-312.pyc b/api/routers/__pycache__/sources.cpython-312.pyc
deleted file mode 100644
index cf02d21..0000000
Binary files a/api/routers/__pycache__/sources.cpython-312.pyc and /dev/null differ
diff --git a/api/routers/__pycache__/translate.cpython-312.pyc b/api/routers/__pycache__/translate.cpython-312.pyc
deleted file mode 100644
index ada9a6d..0000000
Binary files a/api/routers/__pycache__/translate.cpython-312.pyc and /dev/null differ
diff --git a/api/.DS_Store b/backend/.DS_Store
similarity index 100%
rename from api/.DS_Store
rename to backend/.DS_Store
diff --git a/LICENSE b/backend/LICENSE
similarity index 100%
rename from LICENSE
rename to backend/LICENSE
diff --git a/api_documentation.md b/backend/api_documentation.md
similarity index 96%
rename from api_documentation.md
rename to backend/api_documentation.md
index b5e11ed..fa839f5 100644
--- a/api_documentation.md
+++ b/backend/api_documentation.md
@@ -155,6 +155,14 @@ Returns a list of all language codes currently stored in the system.
* **Method:** GET
* **Path:**/languages
+Returns all language codes currently stored in Neo4j.
+
+Response Example:
+```json
+{
+ "languages": ["en", "es", "fr", "pt"]
+}
+```
#### Error Handling
The API uses standard HTTP status codes.
diff --git a/backend/codex_build/Dockerfile b/backend/codex_build/Dockerfile
new file mode 100644
index 0000000..5ad0748
--- /dev/null
+++ b/backend/codex_build/Dockerfile
@@ -0,0 +1,13 @@
+FROM python:3.10-slim
+
+WORKDIR /app
+
+COPY requirements.txt ./
+RUN pip3 install --no-cache-dir -r requirements.txt
+RUN pip3 install requests
+
+COPY . .
+
+EXPOSE 8000
+
+CMD ["python3", "api.py"]
\ No newline at end of file
diff --git a/codex-main-test-build/api.py b/backend/codex_build/api.py
similarity index 65%
rename from codex-main-test-build/api.py
rename to backend/codex_build/api.py
index e570268..0b56ef1 100644
--- a/codex-main-test-build/api.py
+++ b/backend/codex_build/api.py
@@ -25,13 +25,19 @@
import sys
import tempfile
import logging
-from typing import Optional
-
-from fastapi import FastAPI, HTTPException, UploadFile, File, status
-from fastapi.responses import JSONResponse
+from typing import Any, Dict, List, Optional
+
+from fastapi import Depends, FastAPI, HTTPException, UploadFile, File, status, Query
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse, StreamingResponse
+from contextlib import asynccontextmanager
+from pathlib import Path
+import ast
from pydantic import BaseModel
from dotenv import load_dotenv
+from neo4j_sources.source_data import source_data
+
# ── Load .env before importing codex (which reads env vars at module level) ──
load_dotenv(os.path.join(os.path.dirname(__file__), ".env"))
@@ -40,7 +46,6 @@
from codex.services.translation_service import (
translate,
load_language_pack,
- load_demo_data,
)
from codex.neo4j_driver import (
driver,
@@ -49,28 +54,96 @@
find_missing_brands,
get_equivalent_brands,
resolve_to_base_term,
+ get_translation_data,
+ get_countries_for_term,
+ get_languages_for_term,
+ get_countries_for_brand,
+ get_languages_for_brand,
)
except Exception as exc:
logging.critical("Failed to import codex backend: %s", exc)
- sys.exit(1)
+ raise exc
# ── App setup ────────────────────────────────────────────────────────────────
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s %(message)s")
log = logging.getLogger("codex.api")
+TARGET_FOLDER_PATH = Path("./codex/language_packs")
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ print(f"Scanning directory: {TARGET_FOLDER_PATH.resolve()}")
+
+ if TARGET_FOLDER_PATH.exists() and TARGET_FOLDER_PATH.is_dir():
+ # Iterate over every file in the target folder
+ for file_path in TARGET_FOLDER_PATH.iterdir():
+ if file_path.is_file():
+ print(f"Found file: {file_path.name}")
+
+ # Open the local file from disk
+ with open(file_path, "rb") as f:
+ # Construct a FastAPI UploadFile object dynamically
+ upload_file = UploadFile(
+ filename=file_path.name,
+ file=f
+ )
+
+ # Call your load_pack function for each file
+ await load_pack(upload_file)
+ else:
+ print(f"Warning: Directory '{TARGET_FOLDER_PATH}' does not exist.")
+
+ yield
+
app = FastAPI(
title="Codex Medical Translation API",
description="Translate drug names across languages and countries using Neo4j.",
version="1.0.0",
+ lifespan=lifespan
)
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["http://localhost:9000"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+class SourceSelection(BaseModel):
+ selectedSources: List[str]
+
+@app.post(
+ "/api/populate-sources",
+ tags=["data"]
+)
+async def handle_populate(data: SourceSelection):
+ sources = data.selectedSources
+ return StreamingResponse(
+ source_data(sources),
+ media_type="application/x-ndjson",
+ headers={
+ "Cache-Control": "no-cache",
+ "X-Accel-Buffering": "no",
+ "Connection": "keep-alive",
+ }
+ )
# ── Request / response models ────────────────────────────────────────────────
+class SearchResponse(BaseModel):
+ source_id: str
+ source_name: str
+ name: str
+ brand: Optional[str]
+ type: str
+ country: str
+ language: str
+ uploaded_at: str
+
class TranslateRequest(BaseModel):
term: str
- lang: Optional[str] = None
- country: Optional[str] = None
+ lang: str
+ country: str
model_config = {"json_schema_extra": {"example": {
"term": "ibuprofen",
@@ -78,7 +151,6 @@ class TranslateRequest(BaseModel):
"country": "MX",
}}}
-
class TranslationResult(BaseModel):
translation: str
language: str
@@ -88,12 +160,6 @@ class TranslationResult(BaseModel):
class TranslateResponse(BaseModel):
canonical: str
- requested_language: Optional[str]
- used_language: Optional[str]
- fallback_used: bool
- fallback_type: Optional[str] = None
- fallback_chain: Optional[list[str]] = None
- missing_language_pack: Optional[bool] = None
results: list[TranslationResult]
@@ -155,6 +221,37 @@ def health():
api_version=app.version,
)
+@app.post(
+ "/search",
+ response_model=SearchResponse | None,
+ tags=["translation"],
+)
+def search(term: str):
+ try:
+ with driver.session() as session:
+ canonical, brand = resolve_to_base_term(session, term)
+ if not canonical:
+ return None
+ if not brand:
+ countries = get_countries_for_term(session, canonical)
+ languages = get_languages_for_term(session, canonical)
+ else:
+ countries = get_countries_for_brand(session, brand)
+ languages = get_languages_for_brand(session, brand)
+ except Exception as exc:
+ log.exception("search raised an unexpected error")
+ raise HTTPException(status_code=500, detail=str(exc))
+
+ return SearchResponse(
+ source_id="0",
+ source_name="",
+ name=canonical,
+ brand=brand,
+ type="drug",
+ country=countries,
+ language=languages,
+ uploaded_at=""
+ )
@app.post(
"/translate",
@@ -168,19 +265,14 @@ def translate_term(body: TranslateRequest):
- Resolves brand names and fuzzy input to a canonical term first.
- Falls back through configured language chains if no direct match.
- - Falls back to English as the last resort.
- - Returns `missing_language_pack: true` if the language has no data loaded.
JSON SETUP
incoming
- { "term": "ibuprofen", "lang": "es", "country": "MX" }
+ { "term": "ibuprofen", "source_lang": "en", "target_lang": "es", "country": "MX" }
outgoing
{
"canonical": "ibuprofen",
- "requested_language": "es",
- "used_language": "es",
- "fallback_used": false,
"results": [
{ "translation": "ibuprofeno", "language": "Spanish", "brand": "Advil", "country": "MX" }
]
@@ -189,29 +281,51 @@ def translate_term(body: TranslateRequest):
log.info("Translate term=%r lang=%s country=%s", body.term, body.lang, body.country)
try:
- raw = translate(term=body.term, lang=body.lang, country=body.country)
+ raw_data = translate(term=body.term, lang=body.lang, country=body.country)
+ if isinstance(raw_data, str):
+ raw = ast.literal_eval(raw_data)
+ else:
+ raw = raw_data
except Exception as exc:
log.exception("translate() raised an unexpected error")
raise HTTPException(status_code=500, detail=str(exc))
+ raw_results = raw.get("results", []) if isinstance(raw, dict) else []
+ target_lang_str = (body.lang or "").strip().lower()
+ LANGUAGE_MAP = {
+ "fr": "french",
+ "es": "spanish",
+ "en": "english",
+ "ru": "russian",
+ "uk": "ukrainian",
+ }
+ mapped_lang_name = LANGUAGE_MAP.get(target_lang_str, target_lang_str)
+
results = [
TranslationResult(
- translation=r["translation"],
- language=r["language"],
+ translation=r.get("translation", ""),
+ language=r.get("language", ""),
brand=r.get("brand"),
country=r.get("country"),
)
- for r in raw.get("results", [])
+ for r in raw_results
+ if (
+ not body.lang
+ or str(r.get("language", "")).lower() == target_lang_str
+ or str(r.get("lang_code", "")).lower() == target_lang_str
+ or str(r.get("language_code", "")).lower() == target_lang_str
+ or str(r.get("language", "")).lower() == mapped_lang_name
+ )
+ and (
+ not body.country
+ or str(r.get("country", "")).lower() == body.country.lower()
+ )
]
+ resolved_canonical = raw.get("canonical") if isinstance(raw, dict) and raw.get("canonical") else body.term
+
return TranslateResponse(
- canonical=raw.get("canonical", body.term),
- requested_language=raw.get("requested_language"),
- used_language=raw.get("used_language"),
- fallback_used=raw.get("fallback_used", False),
- fallback_type=raw.get("fallback_type"),
- fallback_chain=raw.get("fallback_chain"),
- missing_language_pack=raw.get("missing_language_pack"),
+ canonical=resolved_canonical,
results=results,
)
@@ -252,30 +366,6 @@ def audit_term(term: str):
],
)
-
-@app.post(
- "/demo/load",
- response_model=MessageResponse,
- status_code=status.HTTP_201_CREATED,
- summary="Load built-in sample data",
- tags=["data"],
-)
-def demo_load():
- """
- Loads Ibuprofen, Paracetamol, and Amoxicillin with translations across
- US, GB, FR, ES, MX, NG, IN into Neo4j.
-
- Safe to call multiple times (uses MERGE — no duplicates).
- """
- log.info("Loading demo data")
- try:
- result = load_demo_data()
- return MessageResponse(message=result.get("status", "Demo data loaded"))
- except Exception as exc:
- log.exception("demo_load failed")
- raise HTTPException(status_code=500, detail=str(exc))
-
-
@app.post(
"/packs/load",
response_model=MessageResponse,
@@ -343,7 +433,6 @@ def list_languages():
log.exception("list_languages failed")
raise HTTPException(status_code=500, detail=str(exc))
-
# ── Entry point (for running directly) ──────────────────────────────────────
if __name__ == "__main__":
import uvicorn
diff --git a/codex-main-test-build/cli.py b/backend/codex_build/cli.py
similarity index 100%
rename from codex-main-test-build/cli.py
rename to backend/codex_build/cli.py
diff --git a/api/__init__.py b/backend/codex_build/codex/__init__.py
similarity index 100%
rename from api/__init__.py
rename to backend/codex_build/codex/__init__.py
diff --git a/codex-main-test-build/codex/config/fallbacks.json b/backend/codex_build/codex/config/fallbacks.json
similarity index 100%
rename from codex-main-test-build/codex/config/fallbacks.json
rename to backend/codex_build/codex/config/fallbacks.json
diff --git a/codex-main-test-build/codex/language_packs/english_pack.json b/backend/codex_build/codex/language_packs/english_pack.json
similarity index 100%
rename from codex-main-test-build/codex/language_packs/english_pack.json
rename to backend/codex_build/codex/language_packs/english_pack.json
diff --git a/codex-main-test-build/codex/language_packs/french_pack.json b/backend/codex_build/codex/language_packs/french_pack.json
similarity index 100%
rename from codex-main-test-build/codex/language_packs/french_pack.json
rename to backend/codex_build/codex/language_packs/french_pack.json
diff --git a/codex-main-test-build/codex/language_packs/russian_pack.json b/backend/codex_build/codex/language_packs/russian_pack.json
similarity index 100%
rename from codex-main-test-build/codex/language_packs/russian_pack.json
rename to backend/codex_build/codex/language_packs/russian_pack.json
diff --git a/codex-main-test-build/codex/language_packs/spanish_pack.json b/backend/codex_build/codex/language_packs/spanish_pack.json
similarity index 89%
rename from codex-main-test-build/codex/language_packs/spanish_pack.json
rename to backend/codex_build/codex/language_packs/spanish_pack.json
index 5c33898..deec695 100644
--- a/codex-main-test-build/codex/language_packs/spanish_pack.json
+++ b/backend/codex_build/codex/language_packs/spanish_pack.json
@@ -7,7 +7,8 @@
{
"canonical": "Ibuprofen",
"entries": [
- {"translation": "Ibuprofeno","country": "ES","brand": "Neobrufen"}
+ {"translation": "Ibuprofeno","country": "ES","brand": "Neobrufen"},
+ {"translation": "Ibuprofeno","country": "MX","brand": "Advil"}
]
},
{
@@ -19,7 +20,8 @@
{
"canonical": "Amoxicillin",
"entries": [
- {"translation": "Amoxicilina","country": "ES","brand": "Clamoxyl"}
+ {"translation": "Amoxicilina","country": "ES","brand": "Clamoxyl"},
+ {"translation": "Amoxicilina","country": "ES","brand": "Amoxil"}
]
},
{
diff --git a/codex-main-test-build/codex/language_packs/ukrainian_pack.json b/backend/codex_build/codex/language_packs/ukrainian_pack.json
similarity index 100%
rename from codex-main-test-build/codex/language_packs/ukrainian_pack.json
rename to backend/codex_build/codex/language_packs/ukrainian_pack.json
diff --git a/codex-main-test-build/codex/neo4j_driver.py b/backend/codex_build/codex/neo4j_driver.py
similarity index 58%
rename from codex-main-test-build/codex/neo4j_driver.py
rename to backend/codex_build/codex/neo4j_driver.py
index ffd3168..bbdd5c6 100644
--- a/codex-main-test-build/codex/neo4j_driver.py
+++ b/backend/codex_build/codex/neo4j_driver.py
@@ -40,6 +40,8 @@ def create_translation(session, canonical, brand, country, lang_code, lang_name,
MERGE (b:Brand {name:$brand})
MERGE (b)-[:SOLD_IN]->(c)
MERGE (tr)-[:HAS_BRAND]->(b)
+ MERGE (t)-[:SOLD_AS]->(b)
+ MERGE (b)-[:CONTAINS]->(t)
"""
session.run(query, canonical=canonical, translation=translation, country=country, brand=brand)
@@ -47,27 +49,32 @@ def create_translation(session, canonical, brand, country, lang_code, lang_name,
print(f"Added {canonical} → {translation} ({lang_name}) / Brand: {brand} in {country}")
# Retrieves all translations and related info for a given term
-def get_translation_data(session, canonical, lang=None, country=None):
+def get_translation_data(session, canonical, lang, country):
query = """
MATCH (t:Term)
- WHERE t.canonical = $canonical
+ WHERE t.canonical IS NOT NULL
+ AND (
+ t.canonical = $canonical
OR apoc.text.jaroWinklerDistance(t.canonical, $canonical) < 0.20
+ )
MATCH (tr:Translation)-[:OF_TERM]->(t)
MATCH (tr)-[:IN_LANGUAGE]->(l:Language)
- MATCH (tr)-[:USED_IN]->(c:Country)
- WHERE ($lang IS NULL OR l.code = $lang)
- AND ($country IS NULL OR c.iso2 = $country)
+ OPTIONAL MATCH (tr)-[:USED_IN]->(c:Country)
OPTIONAL MATCH (tr)-[:HAS_BRAND]->(b:Brand)
+ WHERE ($lang IS NULL OR l.code = $lang)
+ AND ($country IS NULL OR c.iso2 = $country)
RETURN DISTINCT
tr.text AS translation,
l.name AS language,
b.name AS brand,
+ l.code AS lang_code,
c.iso2 AS country,
c.name AS country_name
ORDER BY language, country
"""
- # Runs the query and return the results as a list
- return list(session.run(query, canonical=canonical, lang=lang, country=country))
+
+ result = session.run(query, canonical=canonical, lang=lang, country=country)
+ return [r.data() for r in result]
# Finds countries where this term has no translation
def find_missing_translations(session, term):
@@ -146,26 +153,47 @@ def get_equivalent_brands(session, term):
# Resolves any input (canonical, translated, or fuzzy) to a base canonical term
def resolve_to_base_term(session, term):
query = """
- MATCH (t:Term)
- WHERE t.canonical = $term
- RETURN t.canonical AS base
- UNION
- MATCH (t:Term)<-[:OF_TERM]-(tr:Translation)
- WHERE tr.text = $term
- RETURN t.canonical AS base
- UNION
- MATCH (t:Term)
- WHERE apoc.text.jaroWinklerDistance(toLower(t.canonical), toLower($term)) < 0.20
- RETURN t.canonical AS base
- UNION
- MATCH (t:Term)<-[:OF_TERM]-(tr:Translation)
- WHERE apoc.text.jaroWinklerDistance(toLower(tr.text), toLower($term)) < 0.20
- RETURN t.canonical AS base
-
+ CALL {
+ MATCH (t:Term)
+ WHERE t.canonical = $term
+ RETURN t.canonical AS brand, t.canonical AS base
+ UNION
+ MATCH (t:Term)<-[:CONTAINS]-(b:Brand)
+ WHERE b.name = $term
+ RETURN b.name AS brand, t.canonical AS base
+ UNION
+ MATCH (t:Term)<-[:OF_TERM]-(tr:Translation)
+ WHERE tr.text = $term
+ RETURN t.canonical AS brand, t.canonical AS base
+ UNION
+ MATCH (t:Term)
+ WHERE apoc.text.jaroWinklerDistance(toLower(t.canonical), toLower($term)) < 0.20
+ RETURN t.canonical AS brand, t.canonical AS base
+ UNION
+ MATCH (t:Term)<-[:CONTAINS]-(b:Brand)
+ WHERE apoc.text.jaroWinklerDistance(toLower(b.name), toLower($term)) < 0.20
+ RETURN b.name AS brand, t.canonical AS base
+ UNION
+ MATCH (t:Term)<-[:OF_TERM]-(tr:Translation)
+ WHERE apoc.text.jaroWinklerDistance(toLower(tr.text), toLower($term)) < 0.20
+ RETURN t.canonical AS brand, t.canonical AS base
+ }
+ WITH collect({brand: brand, base: base}) AS results
+ WITH results, any(r IN results WHERE r.brand = r.base) AS exactMatch
+ UNWIND results AS row
+ WITH row, exactMatch
+ WHERE exactMatch = false OR row.brand = row.base
+ RETURN row.brand AS brand, row.base AS base
"""
result = session.run(query, term=term).single()
- return result["base"] if result else term
+ if not result:
+ return None
+ elif (result["base"] == result["brand"]):
+ return result["base"], None
+ else:
+ return result["base"], result["brand"]
+
# Checks whether a language pack exists in the database
def language_exists(lang_code: str) -> bool:
@@ -192,4 +220,72 @@ def get_brands_for_term(session, term):
ORDER BY country
"""
- return list(session.run(query, term=term))
\ No newline at end of file
+ return list(session.run(query, term=term))
+
+# Retrieves all countries associated with a term
+def get_countries_for_term(session, term):
+ query = """
+ MATCH (t:Term)
+ WHERE t.canonical = $term
+ OR apoc.text.jaroWinklerDistance(toLower(t.canonical), toLower($term)) < 0.20
+ MATCH (tr:Translation)-[:OF_TERM]->(t)
+ WHERE toLower(tr.text) = toLower(t.canonical)
+ RETURN collect(DISTINCT tr.country) AS country
+ ORDER BY country
+ """
+
+ result = session.run(query, term=term)
+ countries = result.single().value() if result.peek() else []
+ countries = ', '.join(countries)
+ return countries
+
+# Retrieves all languages associated with a term
+def get_languages_for_term(session, term):
+ query = """
+ MATCH (t:Term)
+ WHERE t.canonical = $term
+ OR apoc.text.jaroWinklerDistance(toLower(t.canonical), toLower($term)) < 0.20
+ MATCH (tr:Translation)-[:OF_TERM]->(t)
+ WHERE toLower(tr.text) = toLower(t.canonical)
+ MATCH (tr)-[:IN_LANGUAGE]->(l:Language)
+ RETURN collect(DISTINCT toUpper(l.code)) AS language
+ ORDER BY language
+ """
+
+ result = session.run(query, term=term)
+ languages = result.single().value() if result.peek() else []
+ languages = ', '.join(languages)
+ return languages
+
+# Retrieves all countries associated with a brand
+def get_countries_for_brand(session, term):
+ query = """
+ MATCH (b:Brand)
+ WHERE b.name = $term
+ OR apoc.text.jaroWinklerDistance(toLower(b.name), toLower($term)) < 0.20
+ MATCH (tr:Translation)-[:HAS_BRAND]->(b)
+ RETURN collect(DISTINCT tr.country) AS country
+ ORDER BY country
+ """
+
+ result = session.run(query, term=term)
+ countries = result.single().value() if result.peek() else []
+ countries = ', '.join(countries)
+ return countries
+
+# Retrieves all languages associated with a brand
+def get_languages_for_brand(session, term):
+ query = """
+ MATCH (b:Brand)
+ WHERE b.name = $term
+ OR apoc.text.jaroWinklerDistance(toLower(b.name), toLower($term)) < 0.20
+ MATCH (tr:Translation)-[:HAS_BRAND]->(b)
+ MATCH (tr)-[:IN_LANGUAGE]->(l:Language)
+ RETURN collect(DISTINCT toUpper(l.code)) AS language
+ ORDER BY language
+ """
+
+ result = session.run(query, term=term)
+ languages = result.single().value() if result.peek() else []
+ languages = ', '.join(languages)
+ return languages
\ No newline at end of file
diff --git a/api/models/__init__.py b/backend/codex_build/codex/services/__init__.py
similarity index 100%
rename from api/models/__init__.py
rename to backend/codex_build/codex/services/__init__.py
diff --git a/codex-main-test-build/codex/services/translation_service.py b/backend/codex_build/codex/services/translation_service.py
similarity index 99%
rename from codex-main-test-build/codex/services/translation_service.py
rename to backend/codex_build/codex/services/translation_service.py
index 81b9217..5ddc0d1 100644
--- a/codex-main-test-build/codex/services/translation_service.py
+++ b/backend/codex_build/codex/services/translation_service.py
@@ -54,7 +54,7 @@ def translate(term: str, lang: str = None, country: str = None):
with driver.session() as session:
# Resolve user input (canonical, translated, or fuzzy) to base term
- canonical = resolve_to_base_term(session, term)
+ canonical, _ = resolve_to_base_term(session, term)
if canonical:
term = canonical.lower()
diff --git a/api/routers/__init__.py b/backend/codex_build/codex/utils/__init__.py
similarity index 100%
rename from api/routers/__init__.py
rename to backend/codex_build/codex/utils/__init__.py
diff --git a/codex-main-test-build/docker-compose.yml b/backend/codex_build/docker-compose.yml
similarity index 95%
rename from codex-main-test-build/docker-compose.yml
rename to backend/codex_build/docker-compose.yml
index 57ad27c..033814e 100644
--- a/codex-main-test-build/docker-compose.yml
+++ b/backend/codex_build/docker-compose.yml
@@ -1,6 +1,6 @@
services:
neo4j:
- image: neo4j:5.18
+ image: neo4j:5.18.0
container_name: codex-neo4j
environment:
NEO4J_AUTH: "neo4j/changeme"
diff --git a/api/main.py b/backend/codex_build/main.py
similarity index 65%
rename from api/main.py
rename to backend/codex_build/main.py
index a8872fe..468ed45 100644
--- a/api/main.py
+++ b/backend/codex_build/main.py
@@ -3,10 +3,13 @@
"""
from fastapi import FastAPI, Depends
-from api.config import API_TITLE, API_DESCRIPTION, API_VERSION
-from api.db import CodexDB
-from api.dependencies import get_db
-from api.routers import translate, drugs, conditions, sources
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel
+
+from neo4j_sources.config import API_TITLE, API_DESCRIPTION, API_VERSION
+from neo4j_sources.db import CodexDB
+from neo4j_sources.dependencies import get_db
+from neo4j_sources.routers import translate, drugs, conditions, sources
app = FastAPI(
title=API_TITLE,
diff --git a/scripts/.DS_Store b/backend/codex_build/neo4j_sources/.DS_Store
similarity index 100%
rename from scripts/.DS_Store
rename to backend/codex_build/neo4j_sources/.DS_Store
diff --git a/schema/codex_schema_design.md b/backend/codex_build/neo4j_sources/codex_schema_design.md
similarity index 100%
rename from schema/codex_schema_design.md
rename to backend/codex_build/neo4j_sources/codex_schema_design.md
diff --git a/scripts/config.py b/backend/codex_build/neo4j_sources/config.py
similarity index 66%
rename from scripts/config.py
rename to backend/codex_build/neo4j_sources/config.py
index 01832cb..2598f59 100644
--- a/scripts/config.py
+++ b/backend/codex_build/neo4j_sources/config.py
@@ -9,10 +9,14 @@
NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
-NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "codexpassword")
+NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "changeme")
# Paths
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SAMPLE_DATA_DIR = os.path.join(BASE_DIR, "sample_data")
CYPHER_DIR = os.path.join(BASE_DIR, "cypher")
+
+API_TITLE = "Project Codex API"
+API_DESCRIPTION = "REST API for cross-country drug name translation and lookup, backed by Neo4j."
+API_VERSION = "0.1.0"
\ No newline at end of file
diff --git a/api/db.py b/backend/codex_build/neo4j_sources/db.py
similarity index 93%
rename from api/db.py
rename to backend/codex_build/neo4j_sources/db.py
index 6e0c509..2c97d60 100644
--- a/api/db.py
+++ b/backend/codex_build/neo4j_sources/db.py
@@ -4,7 +4,7 @@
"""
from neo4j import GraphDatabase
-from api.config import NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD
+from config import NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD
class CodexDB:
diff --git a/api/dependencies.py b/backend/codex_build/neo4j_sources/dependencies.py
similarity index 91%
rename from api/dependencies.py
rename to backend/codex_build/neo4j_sources/dependencies.py
index b7de269..bbdd062 100644
--- a/api/dependencies.py
+++ b/backend/codex_build/neo4j_sources/dependencies.py
@@ -3,7 +3,7 @@
"""
from typing import Generator
-from api.db import CodexDB
+from db import CodexDB
def get_db() -> Generator[CodexDB, None, None]:
diff --git a/api/routers/.DS_Store b/backend/codex_build/neo4j_sources/routers/.DS_Store
similarity index 100%
rename from api/routers/.DS_Store
rename to backend/codex_build/neo4j_sources/routers/.DS_Store
diff --git a/codex-main-test-build/codex/__init__.py b/backend/codex_build/neo4j_sources/routers/__init__.py
similarity index 100%
rename from codex-main-test-build/codex/__init__.py
rename to backend/codex_build/neo4j_sources/routers/__init__.py
diff --git a/api/routers/conditions.py b/backend/codex_build/neo4j_sources/routers/conditions.py
similarity index 96%
rename from api/routers/conditions.py
rename to backend/codex_build/neo4j_sources/routers/conditions.py
index 8f440b2..75c6e96 100644
--- a/api/routers/conditions.py
+++ b/backend/codex_build/neo4j_sources/routers/conditions.py
@@ -4,8 +4,8 @@
"""
from fastapi import APIRouter, Depends, HTTPException, Query
-from api.db import CodexDB
-from api.dependencies import get_db
+from neo4j_sources.db import CodexDB
+from neo4j_sources.dependencies import get_db
router = APIRouter(prefix="/conditions", tags=["conditions"])
diff --git a/api/routers/drugs.py b/backend/codex_build/neo4j_sources/routers/drugs.py
similarity index 98%
rename from api/routers/drugs.py
rename to backend/codex_build/neo4j_sources/routers/drugs.py
index 392c5e3..db20891 100644
--- a/api/routers/drugs.py
+++ b/backend/codex_build/neo4j_sources/routers/drugs.py
@@ -4,8 +4,8 @@
"""
from fastapi import APIRouter, Depends, HTTPException, Query
-from api.db import CodexDB
-from api.dependencies import get_db
+from neo4j_sources.db import CodexDB
+from neo4j_sources.dependencies import get_db
router = APIRouter(prefix="/drugs", tags=["drugs"])
diff --git a/api/routers/sources.py b/backend/codex_build/neo4j_sources/routers/sources.py
similarity index 87%
rename from api/routers/sources.py
rename to backend/codex_build/neo4j_sources/routers/sources.py
index a58aec0..0bbbb3b 100644
--- a/api/routers/sources.py
+++ b/backend/codex_build/neo4j_sources/routers/sources.py
@@ -4,8 +4,8 @@
"""
from fastapi import APIRouter, Depends
-from api.db import CodexDB
-from api.dependencies import get_db
+from neo4j_sources.db import CodexDB
+from neo4j_sources.dependencies import get_db
router = APIRouter(prefix="/sources", tags=["sources"])
diff --git a/api/routers/translate.py b/backend/codex_build/neo4j_sources/routers/translate.py
similarity index 96%
rename from api/routers/translate.py
rename to backend/codex_build/neo4j_sources/routers/translate.py
index aee21b5..1f57275 100644
--- a/api/routers/translate.py
+++ b/backend/codex_build/neo4j_sources/routers/translate.py
@@ -4,8 +4,8 @@
"""
from fastapi import APIRouter, Depends, HTTPException, Query
-from api.db import CodexDB
-from api.dependencies import get_db
+from neo4j_sources.db import CodexDB
+from neo4j_sources.dependencies import get_db
router = APIRouter(prefix="/translate", tags=["translate"])
diff --git a/backend/codex_build/neo4j_sources/source_data.py b/backend/codex_build/neo4j_sources/source_data.py
new file mode 100644
index 0000000..8b67560
--- /dev/null
+++ b/backend/codex_build/neo4j_sources/source_data.py
@@ -0,0 +1,608 @@
+#!/usr/bin/env python3
+
+"""
+Neo4j Singular Data Source
+"""
+
+import os
+import sys
+import time
+import uuid
+import re
+import logging
+import json
+import asyncio
+from datetime import datetime, timezone
+from collections import deque
+
+import requests
+from neo4j import GraphDatabase, WRITE_ACCESS # type: ignore
+from dotenv import load_dotenv
+
+load_dotenv(os.path.join(os.path.dirname(__file__), ".env"))
+
+NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
+NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
+NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "changeme")
+NEO4J_DB = os.getenv("NEO4J_DB", "neo4j")
+
+FORCE = os.getenv("FORCE", "0") == "1"
+
+class SharedRateLimiter:
+ def __init__(self, RATE_LIMIT_RPM: int):
+ self.min_interval = 60.0 / max(1, RATE_LIMIT_RPM)
+ self.last_ts = 0.0
+ def wait(self):
+ now = time.monotonic()
+ delta = now - self.last_ts
+ if delta < self.min_interval:
+ time.sleep(self.min_interval - delta)
+ self.last_ts = time.monotonic()
+
+LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
+logging.basicConfig(
+ level=getattr(logging, LOG_LEVEL, logging.INFO),
+ format="%(asctime)s | %(levelname)s | %(message)s",
+)
+log = logging.getLogger("global_root_loader")
+
+def utc_iso() -> str:
+ return datetime.now(timezone.utc).isoformat()
+
+def _respect_retry_after(r):
+ ra = r.headers.get("Retry-After") if hasattr(r, "headers") else None
+ if ra:
+ try:
+ secs = float(ra)
+ log.warning("Retry-After: sleeping %.2fs", secs)
+ time.sleep(secs)
+ return
+ except Exception:
+ pass
+ time.sleep(2.5)
+
+def smoke_test(sess):
+ sess.run("CREATE (:SmokeTest {ts: timestamp()})")
+ c = sess.run("MATCH (s:SmokeTest) RETURN count(s) AS c").single()["c"]
+ sess.run("MATCH (s:SmokeTest) DELETE s")
+ return c
+
+# Separate by \
+def conceptID(text: str, symptom: str = None, source: str = None) -> str:
+ if not text:
+ return "unknown"
+ text = text.lower().strip()
+ text = text.replace(',', '-')
+ text = re.sub(r'[^a-z0-9,-]', '', text)
+ text = re.sub(r'-+', '-', text)
+
+ clean_symptom = ""
+ if symptom:
+ clean_symptom = re.sub(r'[^a-z0-9-]', '', source.lower().strip())
+ text = f"{text}-{clean_symptom}"
+
+ clean_source = ""
+ if source:
+ clean_source = re.sub(r'[^a-z0-9-]', '', source.lower().strip())
+ text = f"{text}-{clean_source}"
+
+ return text
+
+
+# --------------------
+# DrugBank
+# --------------------
+
+async def drugbank(driver):
+ DRUGBANK_API_BASE = os.getenv("DRUGBANK_API_BASE", "https://api.drugbank.com/discovery/v1")
+ DRUGBANK_API_KEY = os.getenv("DRUGBANK_API_KEY")
+ if not DRUGBANK_API_KEY:
+ print("ERROR: Missing DRUGBANK_API_KEY in .env"); return
+
+ DRUGBANK_RELEASE = os.getenv("DRUGBANK_RELEASE", "API")
+ ROOT_ID = os.getenv("DRUGBANK_ROOT_ID", "drugbank:root")
+ ROOT_TITLE = os.getenv("DRUGBANK_ROOT_TITLE", "DrugBank")
+ DATASET = f"DrugBank:{DRUGBANK_RELEASE}"
+
+ limiter = SharedRateLimiter(int(os.getenv("RATE_LIMIT_RPM", "100")))
+
+ def drugbank_headers():
+ return {"Authorization": DRUGBANK_API_KEY, "Accept": "application/json"}
+
+ def get_json(url: str, params=None, max_retries=6):
+ attempt = 0
+ while True:
+ attempt += 1; limiter.wait()
+ try:
+ r = requests.get(url, headers=drugbank_headers(), params=params or {}, timeout=60)
+ if r.status_code == 429:
+ log.warning("HTTP 429 on %s (attempt %d/%d)", url, attempt, max_retries)
+ _respect_retry_after(r)
+ if attempt < max_retries: continue
+ r.raise_for_status(); return r
+ except requests.HTTPError as e:
+ status = getattr(e.response, "status_code", None)
+ body = (e.response.text[:400] + "...") if getattr(e.response, "text", "") else ""
+ log.warning("Request failed [%s] %s | status=%s | body=%.120s", url, e, status, body)
+ if status in (429, 500, 502, 503, 504) and attempt < max_retries:
+ _respect_retry_after(e.response if hasattr(e, "response") else r)
+ continue
+ raise
+
+ API_DRUGS = lambda: f"{DRUGBANK_API_BASE.rstrip('/')}/drugs"
+
+ def iter_drugs(per_page=100):
+ page = 1
+ while True:
+ resp = get_json(API_DRUGS(), params={"per_page": per_page, "page": page})
+ try:
+ data = resp.json()
+ except Exception:
+ data = []
+ items = data if isinstance(data, list) else (data.get("items") if isinstance(data, dict) else [])
+ if not items: break
+ for d in items:
+ dbid = d.get("drugbank_id") or d.get("id")
+ name = d.get("name") or d.get("generic_name") or d.get("brand_name") or ""
+ if dbid: yield d, dbid, name
+ link = resp.headers.get("Link", "")
+ if link and 'rel="next"' in link:
+ page += 1
+ else:
+ page += 1
+
+ with driver.session(database=NEO4J_DB, default_access_mode=WRITE_ACCESS) as sess:
+ sess.run("CREATE CONSTRAINT drug_node_id IF NOT EXISTS FOR (n:DRUG) REQUIRE n.id IS UNIQUE")
+ sess.run("CREATE CONSTRAINT ingest_uid IF NOT EXISTS FOR (i:Ingest) REQUIRE i.uid IS UNIQUE")
+
+ if bool(sess.run("MATCH (i:Ingest {dataset: $ds}) WHERE i.finishedAt IS NOT NULL RETURN i LIMIT 1", ds=DATASET).single()) and not FORCE:
+ log.info("DrugBank Ingest already completed. Skipping.")
+ return
+
+ if FORCE:
+ sess.run("MATCH (i:Ingest {dataset: $ds}) DETACH DELETE i", ds=DATASET)
+ sess.run("MATCH (n:DRUG {ds: $ds}) DETACH DELETE n", ds=DATASET)
+
+ run_uid = str(uuid.uuid4())
+ sess.run("CREATE (i:Ingest { uid: $uid, dataset: $ds, release: $release, startedAt: $startedAt })", uid=run_uid, ds=DATASET, release=DRUGBANK_RELEASE, startedAt=utc_iso())
+ sess.run("MERGE (n:DRUG {id: $id}) ON CREATE SET n.code = $id, n.title = $title, n.ds = $ds", id=ROOT_ID, title=ROOT_TITLE, ds=DATASET)
+ sess.run("MATCH (i:Ingest {uid: $uid}), (r:DRUG {id: $rootId}) MERGE (i)-[:ROOT]->(r)", uid=run_uid, rootId=ROOT_ID)
+
+ pages = 0
+ for d, code, title in iter_drugs():
+ if pages % 100 == 0:
+ yield json.dumps({"progress": f"Processing page {pages} of ??? for DrugBank"}) + "\n"
+ await asyncio.sleep(0)
+ symptoms = d.get("symptoms", [])
+ for symptom_name in symptoms:
+ if not symptom_name:
+ continue
+ concept_id = concept_id(title, symptom_name, source="drugbank")
+
+ sess.run("MERGE (n:DRUG {id: $id}) ON CREATE SET n.conceptID=$conceptID, n.code=$id, n.title=$t, n.ds=$ds ON MATCH SET n.ds=$ds", id=code, conceptID=concept_id, t=title or None, ds=DATASET)
+ sess.run("MATCH (p:DRUG {id: $parent}), (c:DRUG {id: $child}) MERGE (p)-[:HAS_CHILD]->(c)", parent=ROOT_ID, child=code)
+ pages += 1
+
+ yield json.dumps({"progress": "DrugBank completed successfully!"}) + "\n"
+ await asyncio.sleep(0)
+ rec = sess.run("MATCH (n:DRUG {ds: $ds}) WITH count(n) AS n MATCH (:DRUG {ds: $ds})-[rel:HAS_CHILD]->(:DRUG {ds: $ds}) RETURN n, count(rel) AS r", ds=DATASET).single()
+ sess.run("MATCH (i:Ingest {uid: $uid}) SET i.finishedAt = $finishedAt, i.nodeCount = $nodeCount, i.edgeCount = $edgeCount", uid=run_uid, finishedAt=utc_iso(), nodeCount=rec["n"], edgeCount=rec["r"])
+
+# --------------------
+# SNOMED
+# --------------------
+
+async def snomed(driver):
+ SNOWSTORM_BASE = os.getenv("SNOWSTORM_BASE", "https://snowstorm.ihtsdotools.org/snowstorm/snomed-ct")
+ SNOMED_BRANCH = os.getenv("SNOMED_BRANCH", "MAIN")
+ SNOMED_ROOT_ID = os.getenv("SNOMED_ROOT_ID", "404684003")
+ SNOMED_RELEASE = os.getenv("SNOMED_RELEASE", "")
+
+ limiter = SharedRateLimiter(int(os.getenv("RATE_LIMIT_RPM", "200")))
+
+ def api_get(path, params=None):
+ limiter.wait()
+ r = requests.get(f"{SNOWSTORM_BASE.rstrip('/')}/{path.lstrip('/')}", headers={"Accept": "application/json", "Accept-Language": os.getenv("ACCEPT_LANGUAGE", "en"), "User_Agent": "logan.watersmith@grey-box.ca"}, params=params or {}, timeout=60)
+ r.raise_for_status()
+ return r.json()
+
+ release = SNOMED_RELEASE or "latest"
+ try:
+ v_data = api_get("codesystems/SNOMEDCT/versions")
+ items = v_data.get("items") if isinstance(v_data, dict) else v_data
+ if items: release = items[0].get("version") or items[0].get("effectiveDate") or "latest"
+ except Exception: pass
+
+ DATASET = f"SNOMEDCT:{SNOMED_BRANCH}:{release}"
+
+ with driver.session(database=NEO4J_DB, default_access_mode=WRITE_ACCESS) as sess:
+ sess.run("CREATE CONSTRAINT snomed_node_id IF NOT EXISTS FOR (n:SNOMED) REQUIRE n.id IS UNIQUE")
+ if bool(sess.run("MATCH (i:Ingest {dataset: $ds}) WHERE i.finishedAt IS NOT NULL RETURN i LIMIT 1", ds=DATASET).single()) and not FORCE:
+ log.info("SNOMED CT Ingest already completed. Skipping."); return
+
+ if FORCE:
+ sess.run("MATCH (i:Ingest {dataset: $ds}) DETACH DELETE i", ds=DATASET)
+ sess.run("MATCH (n:SNOMED {ds: $ds}) DETACH DELETE n", ds=DATASET)
+
+ run_uid = str(uuid.uuid4())
+ sess.run("CREATE (i:Ingest {uid:$uid, dataset:$ds, release:$rel, startedAt:$s})", uid=run_uid, ds=DATASET, rel=release, s=utc_iso())
+
+ def upsert_snomed_node(node):
+ cid = node.get("conceptId") or node.get("id") or ""
+ if not cid: return None
+ term = node.get("pt", {}).get("term") or node.get("fsn", {}).get("term") or node.get("term", "")
+ sess.run("MERGE (n:SNOMED {id: $id}) ON CREATE SET n.code=$id, n.title=$t, n.ds=$ds ON MATCH SET n.ds=$ds", id=cid, t=term or None, ds=DATASET)
+ return cid
+
+ root_full = None
+ retries = 3
+
+ for attempt in range(retries):
+ try:
+ log.info("Fetching SNOMED root concept %s (Attempt %d/%d)...", SNOMED_ROOT_ID, attempt + 1, retries)
+ root_full = api_get(f"browser/{SNOMED_BRANCH}/concepts/{SNOMED_ROOT_ID}")
+ break # Success! Break out of the retry loop
+ except (requests.exceptions.ConnectionError, requests.exceptions.ChunkedEncodingError, requests.exceptions.RequestException) as net_err:
+ log.warning("SNOMED Server dropped connection on root lookup: %s", net_err)
+ if attempt < retries - 1:
+ log.info("Sleeping 5s before retrying root lookup...")
+ time.sleep(5.0)
+ else:
+ log.error("Failed all retries to connect to SNOMED server. Skipping SNOMED module completely.")
+ return # Safely exits run_snomed() so main() moves to RxNorm
+
+ # If it passed but root_full is somehow empty, exit gracefully
+ if not root_full:
+ return
+
+ # Core initialization with the database
+ upsert_snomed_node(root_full)
+ sess.run("MATCH (i:Ingest {uid: $uid}), (r:SNOMED {id: $rid}) MERGE (i)-[:ROOT]->(r)", uid=run_uid, rid=SNOMED_ROOT_ID)
+
+ # BFS Tree Traversal
+ queue = deque([(SNOMED_ROOT_ID, None)])
+ visited = set()
+
+ pages = 0
+ while queue:
+ if pages % 100 == 0:
+ yield json.dumps({"progress": f"Processing page {pages} of {len(queue)} for SNOMED"}) + "\n"
+ await asyncio.sleep(0)
+ cid, parent = queue.popleft()
+ if cid in visited: continue
+ visited.add(cid)
+
+ try:
+ full = api_get(f"browser/{SNOMED_BRANCH}/concepts/{cid}")
+ upsert_snomed_node(full)
+ if parent:
+ sess.run("MATCH (p:SNOMED {id: $p}), (c:SNOMED {id: $c}) MERGE (p)-[:HAS_CHILD]->(c)", p=parent, c=cid)
+
+ children = api_get(f"browser/{SNOMED_BRANCH}/concepts/{cid}/children")
+ items = children if isinstance(children, list) else children.get("items", [])
+ for ch in items:
+ ch_id = ch.get("conceptId") or ch.get("id")
+ if ch_id and ch_id not in visited: queue.append((ch_id, cid))
+ except Exception as e:
+ log.warning("Failed step processing SNOMED code %s: %s", cid, e)
+ pages += 1
+
+ yield json.dumps({"progress": "SNOMED completed successfully!"}) + "\n"
+ await asyncio.sleep(0)
+ rec = sess.run("MATCH (n:SNOMED {ds: $ds}) WITH count(n) AS n MATCH (:SNOMED {ds: $ds})-[rel:HAS_CHILD]->(:SNOMED {ds: $ds}) RETURN n, count(rel) AS r", ds=DATASET).single()
+ sess.run("MATCH (i:Ingest {uid: $uid}) SET i.finishedAt=$f, i.nodeCount=$n, i.edgeCount=$r", uid=run_uid, f=utc_iso(), n=rec["n"], r=rec["r"])
+
+# --------------------
+# RXNORM
+# --------------------
+
+async def rxnorm(driver):
+ PRESCRIBABLE_ONLY = os.getenv("RXN_PRESCRIBABLE", "1") == "1"
+ RXN_RELEASE_ID = os.getenv("RXN_RELEASE_ID", "current")
+ DATASET = f"RxNorm:{'prescribable' if PRESCRIBABLE_ONLY else 'all'}:{RXN_RELEASE_ID}"
+ FORCE_RX = os.getenv("RXFORCE", "0") == "1" or FORCE
+
+ limiter = SharedRateLimiter(int(os.getenv("RATE_LIMIT_RPM", "120")))
+ ROOT_TTYS = [t.strip() for t in os.getenv("RXN_ROOT_TTYS", "IN,MIN,PIN").split(",") if t.strip()]
+ CHILD_TTYS = [t.strip() for t in os.getenv("RXN_CHILD_TTYS", "SCD,SBD,GPCK,BPCK,SCDF,SBDF,SCDC,SBDC,BN").split(",") if t.strip()]
+
+ def rx_get(path, params=None):
+ limiter.wait()
+ prefix = "/Prescribe" if PRESCRIBABLE_ONLY else ""
+ r = requests.get(f"https://rxnav.nlm.nih.gov/REST{prefix}{path}", params=params or {}, timeout=60)
+ r.raise_for_status()
+ return r.json()
+
+ with driver.session(database=NEO4J_DB) as sess:
+ sess.run("CREATE CONSTRAINT rxn_node_rxcui IF NOT EXISTS FOR (n:RXN) REQUIRE n.rxcui IS UNIQUE")
+ if bool(sess.run("MATCH (i:Ingest {dataset: $ds}) WHERE i.finishedAt IS NOT NULL RETURN i LIMIT 1", ds=DATASET).single()) and not FORCE_RX:
+ log.info("RxNorm Ingest already completed. Skipping."); return
+
+ if FORCE_RX:
+ sess.run("MATCH (i:Ingest {dataset: $ds}) DETACH DELETE i", ds=DATASET)
+ sess.run("MATCH (n:RXN {ds: $ds}) DETACH DELETE n", ds=DATASET)
+
+ run_uid = str(uuid.uuid4())
+ sess.run("CREATE (i:Ingest {uid:$uid, dataset:$ds, release:$rel, startedAt:$s})", uid=run_uid, ds=DATASET, rel=RXN_RELEASE_ID, s=utc_iso())
+ sess.run("MERGE (n:RXN {rxcui: 'ROOT'}) ON CREATE SET n.name='RxNorm', n.tty='ROOT', n.ds=$ds", ds=DATASET)
+ sess.run("MATCH (i:Ingest {uid: $uid}), (r:RXN {rxcui: 'ROOT'}) MERGE (i)-[:ROOT]->(r)", uid=run_uid)
+
+ # Get top-level concepts
+ js = rx_get("/allconcepts.json", params={"tty": " ".join(ROOT_TTYS)})
+ roots = [{"rxcui": m.get("rxcui"), "name": m.get("name"), "tty": m.get("tty")} for m in js.get("minConceptGroup", {}).get("minConcept", []) if m.get("rxcui")]
+
+ for m in roots:
+ concept_id = conceptID(m.get("name"), source="rxnorm")
+ sess.run("MERGE (n:RXN {rxcui: $rxcui}) ON CREATE SET n.conceptID=$conceptID, n.name=$name, n.tty=$tty, n.ds=$ds", rxcui=m["rxcui"], conceptID=concept_id, name=m["name"], tty=m["tty"], ds=DATASET)
+ sess.run("MATCH (p:RXN {rxcui: 'ROOT'}), (c:RXN {rxcui: $c}) MERGE (p)-[:HAS_CHILD]->(c)", c=m["rxcui"])
+
+ for idx, m in enumerate(roots):
+ if idx % 250 == 0:
+ yield json.dumps({"progress": f"Processing page {idx} of {len(roots)} for RXNorm"}) + "\n"
+ await asyncio.sleep(0)
+ try:
+ rel_js = rx_get(f"/rxcui/{m['rxcui']}/allrelated.json")
+ groups = rel_js.get("allRelatedGroup", {}).get("conceptGroup", [])
+ for g in groups:
+ tty = g.get("tty")
+ if tty not in CHILD_TTYS: continue
+ props = g.get("conceptProperties", [])
+ if isinstance(props, dict): props = [props]
+ for p in props:
+ if p.get("rxcui"):
+ concept_id = conceptID(p.get("name"), source="rxnorm")
+ sess.run("MERGE (n:RXN {rxcui: $rxcui}) ON CREATE SET n.conceptID=$conceptID, n.name=$name, n.tty=$tty, n.ds=$ds ON MATCH SET n.ds=$ds", rxcui=p["rxcui"], conceptID=concept_id, name=p["name"], tty=p["tty"], ds=DATASET)
+ sess.run("MATCH (p:RXN {rxcui: $p}), (c:RXN {rxcui: $c}) MERGE (p)-[:HAS_CHILD]->(c)", p=m["rxcui"], c=p["rxcui"])
+ except Exception as e: log.debug("Skipped paths on CUI %s: %s", m["rxcui"], e)
+
+ yield json.dumps({"progress": "RXNorm completed successfully!"}) + "\n"
+ await asyncio.sleep(0)
+ rec = sess.run("MATCH (n:RXN {ds: $ds}) WITH count(n) AS n MATCH (:RXN {ds: $ds})-[rel:HAS_CHILD]->(:RXN {ds: $ds}) RETURN n, count(rel) AS r", ds=DATASET).single()
+ sess.run("MATCH (i:Ingest {uid: $uid}) SET i.finishedAt=$f, i.nodeCount=$n, i.edgeCount=$r", uid=run_uid, f=utc_iso(), n=rec["n"], r=rec["r"])
+
+# --------------------
+# ICD-11
+# --------------------
+
+async def icd11(driver):
+ ICD_CLIENT_ID = os.getenv("ICD_CLIENT_ID")
+ ICD_CLIENT_SECRET = os.getenv("ICD_CLIENT_SECRET")
+ if not ICD_CLIENT_ID or not ICD_CLIENT_SECRET:
+ log.error("Missing ICD credentials. Skipping ICD-11."); return
+
+ ICD_RELEASE_ID = os.getenv("ICD_RELEASE_ID", "2024-01")
+ DATASET = f"ICD11-21:{ICD_RELEASE_ID}"
+ limiter = SharedRateLimiter(int(os.getenv("RATE_LIMIT_RPM", "200")))
+
+ limiter.wait()
+ tok_r = requests.post("https://icdaccessmanagement.who.int/connect/token", data={"grant_type": "client_credentials", "scope": "icdapi_access"}, auth=(ICD_CLIENT_ID, ICD_CLIENT_SECRET), timeout=30)
+ tok_r.raise_for_status()
+ token = tok_r.json()["access_token"]
+
+ def icd_get(url, params=None, max_retries=6):
+ attempt = 0
+ while True:
+ attempt += 1; limiter.wait()
+ try:
+ r = requests.get(url, headers={"Authorization": f"Bearer {token}", "Accept": "application/json", "API-Version": "v2", "Accept-Language": "en"}, params=params or {}, timeout=60)
+ if r.status_code == 429:
+ _respect_retry_after(r);
+ if attempt < max_retries:
+ continue
+ r.raise_for_status(); return r.json()
+ except requests.HTTPError as e:
+ if getattr(e.response, "status_code", None) in (429, 500, 502, 503, 504) and attempt < max_retries:
+ _respect_retry_after(e.response); continue
+ raise
+
+ def get_node_details(item):
+ if isinstance(item, str):
+ res = icd_get(item, params={"properties": "code,title"})
+ t = res.get("title", "")
+ return item, res.get("code") or res.get("theCode") or "", t.get("@value", "") if isinstance(t, dict) else t
+ return item.get("@id"), item.get("code") or item.get("theCode") or "", item.get("title", {}).get("@value", "") if isinstance(item.get("title"), dict) else item.get("title", "")
+
+ # Look up Chapter 21 Root
+ root_res = icd_get(f"https://id.who.int/icd/release/11/{ICD_RELEASE_ID}/mms", params={"flat": "true"})
+ ch21_id = None
+ for child in root_res.get("child", []):
+ nid, code, title = get_node_details(child)
+ if code == "21" or "Symptoms, signs" in str(title): ch21_id = nid; break
+
+ if not ch21_id: log.error("Could not find ICD-11 Chapter 21 node. Skipping."); return
+
+ with driver.session(database=NEO4J_DB) as sess:
+ sess.run("CREATE CONSTRAINT icd_node_id IF NOT EXISTS FOR (n:ICD) REQUIRE n.id IS UNIQUE")
+ if bool(sess.run("MATCH (i:Ingest {dataset: $ds}) WHERE i.finishedAt IS NOT NULL RETURN i LIMIT 1", ds=DATASET).single()) and not FORCE:
+ log.info("ICD-11 Ingest already completed. Skipping."); return
+
+ if FORCE:
+ sess.run("MATCH (i:Ingest {dataset: $ds}) DETACH DELETE i", ds=DATASET)
+ sess.run("MATCH (n:ICD {ds: $ds}) DETACH DELETE n", ds=DATASET)
+
+ run_uid = str(uuid.uuid4())
+ sess.run("CREATE (i:Ingest {uid:$uid, dataset:$ds, release:$rel, startedAt:$s})", uid=run_uid, ds=DATASET, rel=ICD_RELEASE_ID, s=utc_iso())
+
+ full_root = icd_get(ch21_id)
+ # sess.run("MERGE (n:ICD {id: $id}) ON CREATE SET n.code=$c, n.title=$t, n.ds=$ds", id=ch21_id, c=full_root.get("code"), t=full_root.get("title", {}).get("@value"), ds=DATASET)
+ sess.run("MERGE (n:ICD {id: $id}) ON CREATE SET n.code=$c, n.title='ICD 11', n.ds=$ds", id=ch21_id, c=full_root.get("code"), ds=DATASET)
+ sess.run("MATCH (i:Ingest {uid: $uid}), (r:ICD {id: $rid}) MERGE (i)-[:ROOT]->(r)", uid=run_uid, rid=ch21_id)
+
+ queue = deque([(ch21_id, None)])
+ visited = set()
+ pages = 0
+ while queue:
+ if pages % 10 == 0:
+ yield json.dumps({"progress": f"Processing page {pages} of {len(queue)} for ICD11"}) + "\n"
+ await asyncio.sleep(0)
+ nid, parent = queue.popleft()
+ if nid in visited: continue
+ visited.add(nid)
+
+ full = icd_get(nid)
+ t_val = full.get("title", {})
+ title_str = t_val.get("@value") if isinstance(t_val, dict) else t_val
+ sess.run("MERGE (n:ICD {id: $id}) ON CREATE SET n.code=$c, n.title=$t, n.ds=$ds ON MATCH SET n.ds=$ds", id=nid, c=full.get("code") or full.get("theCode"), t=title_str, ds=DATASET)
+ if parent:
+ sess.run("MATCH (p:ICD {id: $p}), (c:ICD {id: $c}) MERGE (p)-[:HAS_CHILD]->(c)", p=parent, c=nid)
+
+ subtree = icd_get(nid, params={"include": "descendant", "depth": "1", "properties": "code,title"})
+ children = subtree.get("child", []) or subtree.get("descendant", [])
+ for child in children:
+ cid, _, _ = get_node_details(child)
+ if cid and cid not in visited: queue.append((cid, nid))
+ pages += 1
+
+ yield json.dumps({"progress": "ICD11 completed successfully!"}) + "\n"
+ await asyncio.sleep(0)
+ rec = sess.run("MATCH (n:ICD {ds: $ds}) WITH count(n) AS n MATCH (:ICD {ds: $ds})-[rel:HAS_CHILD]->(:ICD {ds: $ds}) RETURN n, count(rel) AS r", ds=DATASET).single()
+ sess.run("MATCH (i:Ingest {uid: $uid}) SET i.finishedAt=$f, i.nodeCount=$n, i.edgeCount=$r", uid=run_uid, f=utc_iso(), n=rec["n"], r=rec["r"])
+
+# --------------------
+# Combining into Singular Data Source
+# --------------------
+
+def unify_graph(driver):
+ log.info(">>> Combining graphes into a singular source...")
+
+ with driver.session(database=NEO4J_DB) as sess:
+ sess.run("""
+ MERGE (g:G_ROOT {id: 'GLOBAL_ROOT'})
+ ON CREATE SET g.title = 'Global Root', g.initializedAt = $ts
+ """, ts=utc_iso())
+
+ # Secondary label (:Concept) applied to every medical node
+ log.info("Creating global structural indexes...")
+ sess.run("CREATE INDEX global_concept_code IF NOT EXISTS FOR (n:Concept) ON n.code")
+
+ sess.run("""
+ MATCH (:Ingest)-[:ROOT]->(root)
+ SET root:Source
+ """)
+
+ log.info("Applying global :Concept labels to all internal medical entities...")
+ sess.run("MATCH (n:DRUG) WHERE NOT n:Source SET n:Concept")
+ sess.run("MATCH (n:SNOMED) WHERE NOT n:Source SET n:Concept")
+ sess.run("MATCH (n:RXN) WHERE NOT n:Source SET n:Concept")
+ sess.run("MATCH (n:ICD) WHERE NOT n:Source SET n:Concept")
+
+ log.info("Creating connections between RxNorm and DrugBank...")
+ sess.run("""
+ MATCH (r:RXN:Concept), (d:DRUG:Concept)
+ WHERE r.name = d.title OR r.rxcui = d.code
+ MERGE (r)-[:SAME_AS {derivedBy: 'property_match'}]->(d)
+ """)
+
+ log.info("Creating connections between SNOMED CT and ICD-11...")
+ sess.run("""
+ MATCH (s:SNOMED:Concept), (i:ICD:Concept)
+ WHERE s.code = i.code OR s.title = i.title
+ MERGE (s)-[:MAPS_TO {derivedBy: 'exact_string_match'}]->(i)
+ """)
+
+ sess.run("""
+ MATCH (g:G_ROOT {id: 'GLOBAL_ROOT'}), (src:Source)
+ MERGE (g)-[:HAS_SOURCE]->(src)
+ """)
+
+ log.info("<<< Graph combination complete. All sources are now connected.")
+
+def print_databases(driver):
+ # Create .txt file
+ with open("databases.txt", "w") as file:
+ file.write("\n" + "="*60 + "\nIMPORTED DATABASES\n" + "="*60 + "\n")
+ vocab_labels = ["DRUG", "SNOMED", "RXN", "ICD"]
+
+ with driver.session(database=NEO4J_DB) as sess:
+ for label in vocab_labels:
+ query = f"""
+ MATCH (n:{label})
+ RETURN n.id AS node_id, properties(n) AS all_fields
+ """
+ results = sess.run(query)
+
+ if label == "DRUG":
+ log.info("Importing DrugBank")
+ file.write(f"--- Database Records for DrugBank ---\n")
+ if label == "SNOMED":
+ log.info("Importing SNOMED")
+ file.write(f"--- Database Records for Snomed ---\n")
+ if label == "RXN":
+ log.info("Importing RxNorm")
+ file.write(f"--- Database Records for RxNorm ---\n")
+ if label == "ICD":
+ log.info("Importing ICD 11")
+ file.write(f"--- Database Records for ICD 11 ---\n")
+
+ has_records = False
+ for record in results:
+ has_records = True
+ node_id = record["node_id"]
+ fields = record["all_fields"]
+
+ formatted_fields = ", ".join([f"{k}: '{v}'" for k, v in fields.items()])
+ file.write(f"ID: [{node_id}] -> {{ {formatted_fields} }}\n")
+
+ if not has_records:
+ file.write(f"No records found for label :{label} (Skipped or Empty).\n")
+
+ file.write("="*60 + "\nEND OF DATABASE READOUT\n" + "="*60)
+
+async def source_data(sources):
+ log.info("=" * 60)
+ log.info("Creating Singular Medical Knowledge Graph")
+ log.info("=" * 60)
+
+ t0 = time.time()
+ driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))
+
+ try:
+ with driver.session(database=NEO4J_DB) as check_session:
+ sc = smoke_test(check_session)
+ log.info("Graph connection smoke test passed.")
+
+ # Ingest separate sources into the single database based on user choice
+ if "drugbank" in sources:
+ log.info("Ingesting DrugBank.")
+ yield json.dumps({"progress": "Ingesting DrugBank..."}) + "\n"
+ await asyncio.sleep(0)
+ async for chunk in drugbank(driver):
+ yield chunk
+ if "snomed" in sources:
+ log.info("Ingesting SNOMED.")
+ yield json.dumps({"progress": "Ingesting SNOMED..."}) + "\n"
+ await asyncio.sleep(0)
+ await asyncio.sleep(0)
+ async for chunk in snomed(driver):
+ yield chunk
+ if "rxnorm" in sources:
+ log.info("Ingesting RXNorm.")
+ yield json.dumps({"progress": "Ingesting RXNorm..."}) + "\n"
+ await asyncio.sleep(0)
+ async for chunk in rxnorm(driver):
+ yield chunk
+ if "icd11" in sources:
+ log.info("Ingesting ICD11.")
+ yield json.dumps({"progress": "Ingesting ICD11..."}) + "\n"
+ await asyncio.sleep(0)
+ async for chunk in icd11(driver):
+ yield chunk
+
+ # Link them together to make a singular source
+ unify_graph(driver)
+
+ print_databases(driver)
+
+ except Exception as e:
+ log.critical("Pipeline was terminated prematurely: %s", e)
+ raise e
+ finally:
+ driver.close()
+
+ log.info("=" * 60)
+ log.info("All pipelines executed | Total duration: %.2f seconds", time.time() - t0)
+ log.info("=" * 60)
+
+if __name__ == "__main__":
+ sources = sys.argv[1:]
+ source_data(sources)
\ No newline at end of file
diff --git a/codex-main-test-build/requirements.txt b/backend/codex_build/requirements.txt
similarity index 84%
rename from codex-main-test-build/requirements.txt
rename to backend/codex_build/requirements.txt
index d396315..9f3ea69 100644
--- a/codex-main-test-build/requirements.txt
+++ b/backend/codex_build/requirements.txt
@@ -2,4 +2,4 @@ neo4j==5.18.0
python-dotenv==1.0.1
fastapi==0.111.0
uvicorn[standard]==0.30.1
-httpx==0.27.0
+httpx==0.27.0
\ No newline at end of file
diff --git a/codex-main-test-build/codex/services/__init__.py b/codex-main-test-build/codex/services/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/codex-main-test-build/codex/utils/__init__.py b/codex-main-test-build/codex/utils/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/cypher/00_setup_constraints.cypher b/cypher/00_setup_constraints.cypher
deleted file mode 100644
index ea79fa3..0000000
--- a/cypher/00_setup_constraints.cypher
+++ /dev/null
@@ -1,77 +0,0 @@
-// =============================================================
-// Project Codex — Neo4j Schema Setup
-// Step 0: Constraints and Indexes
-// Run this FIRST before any data import
-// =============================================================
-
-// ---- Uniqueness Constraints ----
-
-CREATE CONSTRAINT drug_codex_id IF NOT EXISTS
-FOR (d:Drug) REQUIRE d.codex_id IS UNIQUE;
-
-CREATE CONSTRAINT drugname_unique IF NOT EXISTS
-FOR (dn:DrugName) REQUIRE (dn.name, dn.country, dn.language) IS UNIQUE;
-
-CREATE CONSTRAINT condition_codex_id IF NOT EXISTS
-FOR (c:Condition) REQUIRE c.codex_id IS UNIQUE;
-
-CREATE CONSTRAINT ingredient_codex_id IF NOT EXISTS
-FOR (i:Ingredient) REQUIRE i.codex_id IS UNIQUE;
-
-CREATE CONSTRAINT datasource_name IF NOT EXISTS
-FOR (ds:DataSource) REQUIRE ds.name IS UNIQUE;
-
-// ---- Indexes for fast lookups ----
-
-CREATE INDEX drug_source_id IF NOT EXISTS
-FOR (d:Drug) ON (d.source_id);
-
-CREATE INDEX drug_source IF NOT EXISTS
-FOR (d:Drug) ON (d.source);
-
-CREATE INDEX drug_canonical_name IF NOT EXISTS
-FOR (d:Drug) ON (d.canonical_name);
-
-CREATE INDEX drug_poc IF NOT EXISTS
-FOR (d:Drug) ON (d.is_poc);
-
-CREATE INDEX drugname_country IF NOT EXISTS
-FOR (dn:DrugName) ON (dn.country);
-
-CREATE INDEX drugname_language IF NOT EXISTS
-FOR (dn:DrugName) ON (dn.language);
-
-CREATE INDEX condition_icd11 IF NOT EXISTS
-FOR (c:Condition) ON (c.icd11_code);
-
-CREATE INDEX condition_snomed IF NOT EXISTS
-FOR (c:Condition) ON (c.snomed_id);
-
-CREATE INDEX ingredient_inchikey IF NOT EXISTS
-FOR (i:Ingredient) ON (i.inchikey);
-
-// ---- Register known data sources ----
-
-MERGE (ds:DataSource {name: 'drugbank'})
-SET ds.url = 'https://go.drugbank.com/',
- ds.license = 'Creative Commons Attribution-NonCommercial 4.0',
- ds.version = '5.1.10',
- ds.last_refreshed = datetime('2024-01-15T00:00:00Z');
-
-MERGE (ds:DataSource {name: 'rxnorm'})
-SET ds.url = 'https://www.nlm.nih.gov/research/umls/rxnorm/',
- ds.license = 'Public Domain (NLM)',
- ds.version = '2024-01-02',
- ds.last_refreshed = datetime('2024-01-10T00:00:00Z');
-
-MERGE (ds:DataSource {name: 'icd11'})
-SET ds.url = 'https://icd.who.int/en',
- ds.license = 'Creative Commons Attribution-NoDerivatives 3.0 IGO',
- ds.version = '2024-01',
- ds.last_refreshed = datetime('2024-01-08T00:00:00Z');
-
-MERGE (ds:DataSource {name: 'snomedct'})
-SET ds.url = 'https://www.snomed.org/',
- ds.license = 'SNOMED CT License',
- ds.version = '2023-09-01',
- ds.last_refreshed = datetime('2024-01-12T00:00:00Z');
diff --git a/cypher/01_load_drugbank.cypher b/cypher/01_load_drugbank.cypher
deleted file mode 100644
index 950e2d1..0000000
--- a/cypher/01_load_drugbank.cypher
+++ /dev/null
@@ -1,162 +0,0 @@
-// =============================================================
-// Project Codex — DrugBank Source Loader
-// Step 1: Load DrugBank data into normalized Codex schema
-// Assumes: sample_data/drugbank_sample.json accessible via LOAD CSV
-// or data passed as parameters from Python ETL
-// =============================================================
-
-// ---- Create Drug nodes from DrugBank ----
-// Note: In production, this runs per-record from the ETL pipeline.
-// Below is the idiomatic Cypher using MERGE for upsert semantics.
-
-// Acetaminophen (Tylenol / Dolo / Panadol)
-MERGE (d:Drug {source: 'drugbank', source_id: 'DB00316'})
-ON CREATE SET
- d.codex_id = 'codex-drug-' + 'DB00316',
- d.canonical_name = 'Acetaminophen',
- d.drug_type = 'small_molecule',
- d.is_approved = true,
- d.source_attribute_name = 'drugbank_id',
- d.created_at = datetime(),
- d.updated_at = datetime(),
- d.is_poc = true
-ON MATCH SET
- d.updated_at = datetime();
-
-// DrugName nodes for Acetaminophen
-MERGE (dn:DrugName {name: 'Acetaminophen', country: 'US', language: 'en'})
-ON CREATE SET dn.name_type = 'generic', dn.is_primary = true,
- dn.source = 'drugbank', dn.source_attribute_name = 'brands.name',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-MERGE (dn:DrugName {name: 'Tylenol', country: 'US', language: 'en'})
-ON CREATE SET dn.name_type = 'brand', dn.is_primary = false,
- dn.source = 'drugbank', dn.source_attribute_name = 'brands.name',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-MERGE (dn:DrugName {name: 'Panadol', country: 'GB', language: 'en'})
-ON CREATE SET dn.name_type = 'brand', dn.is_primary = true,
- dn.source = 'drugbank', dn.source_attribute_name = 'brands.name',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-MERGE (dn:DrugName {name: 'Dolo', country: 'IN', language: 'hi'})
-ON CREATE SET dn.name_type = 'brand', dn.is_primary = true,
- dn.source = 'drugbank', dn.source_attribute_name = 'brands.name',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-MERGE (dn:DrugName {name: 'Dafalgan', country: 'FR', language: 'fr'})
-ON CREATE SET dn.name_type = 'brand', dn.is_primary = true,
- dn.source = 'drugbank', dn.source_attribute_name = 'brands.name',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-MERGE (dn:DrugName {name: 'Ben-u-ron', country: 'DE', language: 'de'})
-ON CREATE SET dn.name_type = 'brand', dn.is_primary = true,
- dn.source = 'drugbank', dn.source_attribute_name = 'brands.name',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-// Connect Drug → DrugNames
-MATCH (d:Drug {source_id: 'DB00316'})
-MATCH (dn:DrugName) WHERE dn.name IN ['Acetaminophen','Tylenol','Panadol','Dolo','Dafalgan','Ben-u-ron']
-MERGE (d)-[:HAS_NAME {source: 'drugbank', created_at: datetime()}]->(dn);
-
-// Connect Drug → DataSource
-MATCH (d:Drug {source_id: 'DB00316'}), (ds:DataSource {name: 'drugbank'})
-MERGE (d)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
-
-// Ingredient node for Acetaminophen
-MERGE (i:Ingredient {source: 'drugbank', source_id: 'DB00316-active'})
-ON CREATE SET
- i.codex_id = 'codex-ing-APAP',
- i.name = 'Acetaminophen',
- i.cas_number = '103-90-2',
- i.inchikey = 'RZVAJINKPMORJF-UHFFFAOYSA-N',
- i.source_attribute_name = 'cas_number',
- i.created_at = datetime(),
- i.updated_at = datetime(),
- i.is_poc = true;
-
-MATCH (d:Drug {source_id: 'DB00316'}), (i:Ingredient {source_id: 'DB00316-active'})
-MERGE (d)-[:CONTAINS_INGREDIENT {role: 'active', source: 'drugbank'}]->(i);
-
-// ---- Aspirin ----
-MERGE (d:Drug {source: 'drugbank', source_id: 'DB00945'})
-ON CREATE SET
- d.codex_id = 'codex-drug-DB00945',
- d.canonical_name = 'Aspirin',
- d.drug_type = 'small_molecule',
- d.is_approved = true,
- d.source_attribute_name = 'drugbank_id',
- d.created_at = datetime(),
- d.updated_at = datetime(),
- d.is_poc = true
-ON MATCH SET d.updated_at = datetime();
-
-MERGE (dn:DrugName {name: 'Aspirin', country: 'US', language: 'en'})
-ON CREATE SET dn.name_type = 'generic', dn.is_primary = true,
- dn.source = 'drugbank', dn.source_attribute_name = 'brands.name',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-MERGE (dn:DrugName {name: 'Disprin', country: 'IN', language: 'hi'})
-ON CREATE SET dn.name_type = 'brand', dn.is_primary = true,
- dn.source = 'drugbank', dn.source_attribute_name = 'brands.name',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-MERGE (dn:DrugName {name: 'Aspro', country: 'AU', language: 'en'})
-ON CREATE SET dn.name_type = 'brand', dn.is_primary = true,
- dn.source = 'drugbank', dn.source_attribute_name = 'brands.name',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-MATCH (d:Drug {source_id: 'DB00945'})
-MATCH (dn:DrugName) WHERE dn.name IN ['Aspirin','Disprin','Aspro']
-MERGE (d)-[:HAS_NAME {source: 'drugbank', created_at: datetime()}]->(dn);
-
-MATCH (d:Drug {source_id: 'DB00945'}), (ds:DataSource {name: 'drugbank'})
-MERGE (d)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
-
-// ---- Metformin ----
-MERGE (d:Drug {source: 'drugbank', source_id: 'DB00331'})
-ON CREATE SET
- d.codex_id = 'codex-drug-DB00331',
- d.canonical_name = 'Metformin',
- d.drug_type = 'small_molecule',
- d.is_approved = true,
- d.source_attribute_name = 'drugbank_id',
- d.created_at = datetime(),
- d.updated_at = datetime(),
- d.is_poc = true
-ON MATCH SET d.updated_at = datetime();
-
-MERGE (dn:DrugName {name: 'Glucophage', country: 'US', language: 'en'})
-ON CREATE SET dn.name_type = 'brand', dn.is_primary = true,
- dn.source = 'drugbank', dn.source_attribute_name = 'brands.name',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-MERGE (dn:DrugName {name: 'Glycomet', country: 'IN', language: 'hi'})
-ON CREATE SET dn.name_type = 'brand', dn.is_primary = true,
- dn.source = 'drugbank', dn.source_attribute_name = 'brands.name',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-MERGE (dn:DrugName {name: 'Metforal', country: 'IT', language: 'it'})
-ON CREATE SET dn.name_type = 'brand', dn.is_primary = true,
- dn.source = 'drugbank', dn.source_attribute_name = 'brands.name',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-MERGE (dn:DrugName {name: 'Siofor', country: 'DE', language: 'de'})
-ON CREATE SET dn.name_type = 'brand', dn.is_primary = true,
- dn.source = 'drugbank', dn.source_attribute_name = 'brands.name',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-MATCH (d:Drug {source_id: 'DB00331'})
-MATCH (dn:DrugName) WHERE dn.name IN ['Glucophage','Glycomet','Metforal','Siofor']
-MERGE (d)-[:HAS_NAME {source: 'drugbank', created_at: datetime()}]->(dn);
-
-MATCH (d:Drug {source_id: 'DB00331'}), (ds:DataSource {name: 'drugbank'})
-MERGE (d)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
-
-// ---- Drug Interactions ----
-MATCH (d1:Drug {source_id: 'DB00316'}), (d2:Drug {source_id: 'DB00682'})
-MERGE (d1)-[:INTERACTS_WITH {
- severity: 'moderate',
- description: 'Warfarin anticoagulant effect may be increased',
- source: 'drugbank'
-}]->(d2);
diff --git a/cypher/02_load_rxnorm.cypher b/cypher/02_load_rxnorm.cypher
deleted file mode 100644
index 5ffd7ad..0000000
--- a/cypher/02_load_rxnorm.cypher
+++ /dev/null
@@ -1,112 +0,0 @@
-// =============================================================
-// Project Codex — RxNorm Source Loader
-// Step 2: Map RxNorm concepts into the normalized Codex schema
-// RxNorm is US-centric; maps to existing Drug nodes via InChIKey / name match
-// =============================================================
-
-// RxNorm uses MERGE ON MATCH to enrich existing Drug nodes with RxCUI,
-// or creates new Drug nodes if the drug isn't in DrugBank yet.
-
-// ---- Acetaminophen (RxCUI 161) ----
-// Already in graph from DrugBank; enrich with RxNorm IDs
-MERGE (d:Drug {source: 'rxnorm', source_id: '161'})
-ON CREATE SET
- d.codex_id = 'codex-drug-RX161',
- d.canonical_name = 'Acetaminophen',
- d.drug_type = 'small_molecule',
- d.is_approved = true,
- d.source_attribute_name = 'rxcui',
- d.created_at = datetime(),
- d.updated_at = datetime(),
- d.is_poc = true
-ON MATCH SET d.updated_at = datetime();
-
-// Link RxNorm entry to DataSource
-MATCH (d:Drug {source: 'rxnorm', source_id: '161'}), (ds:DataSource {name: 'rxnorm'})
-MERGE (d)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
-
-// Establish EQUIVALENT_TO between DrugBank and RxNorm representations
-MATCH (db:Drug {source: 'drugbank', source_id: 'DB00316'})
-MATCH (rx:Drug {source: 'rxnorm', source_id: '161'})
-MERGE (db)-[:EQUIVALENT_TO {
- confidence: 1.0,
- source: 'codex-normalization',
- match_basis: 'name+inchikey',
- created_at: datetime()
-}]->(rx);
-
-// Add RxNorm-specific name variants (clinical dose forms)
-MERGE (dn:DrugName {name: 'Acetaminophen 325 MG Oral Tablet', country: 'US', language: 'en'})
-ON CREATE SET dn.name_type = 'clinical_dose_form', dn.is_primary = false,
- dn.source = 'rxnorm', dn.source_attribute_name = 'SCD.name',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = false;
-
-MATCH (d:Drug {source: 'rxnorm', source_id: '161'})
-MATCH (dn:DrugName {name: 'Acetaminophen 325 MG Oral Tablet', country: 'US', language: 'en'})
-MERGE (d)-[:HAS_NAME {source: 'rxnorm', created_at: datetime()}]->(dn);
-
-// ---- Aspirin (RxCUI 1191) ----
-MERGE (d:Drug {source: 'rxnorm', source_id: '1191'})
-ON CREATE SET
- d.codex_id = 'codex-drug-RX1191',
- d.canonical_name = 'Aspirin',
- d.drug_type = 'small_molecule',
- d.is_approved = true,
- d.source_attribute_name = 'rxcui',
- d.created_at = datetime(),
- d.updated_at = datetime(),
- d.is_poc = true
-ON MATCH SET d.updated_at = datetime();
-
-MATCH (d:Drug {source: 'rxnorm', source_id: '1191'}), (ds:DataSource {name: 'rxnorm'})
-MERGE (d)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
-
-MATCH (db:Drug {source: 'drugbank', source_id: 'DB00945'})
-MATCH (rx:Drug {source: 'rxnorm', source_id: '1191'})
-MERGE (db)-[:EQUIVALENT_TO {
- confidence: 1.0,
- source: 'codex-normalization',
- match_basis: 'name+inchikey',
- created_at: datetime()
-}]->(rx);
-
-// ---- Metformin (RxCUI 6809) ----
-MERGE (d:Drug {source: 'rxnorm', source_id: '6809'})
-ON CREATE SET
- d.codex_id = 'codex-drug-RX6809',
- d.canonical_name = 'Metformin',
- d.drug_type = 'small_molecule',
- d.is_approved = true,
- d.source_attribute_name = 'rxcui',
- d.created_at = datetime(),
- d.updated_at = datetime(),
- d.is_poc = true
-ON MATCH SET d.updated_at = datetime();
-
-MATCH (d:Drug {source: 'rxnorm', source_id: '6809'}), (ds:DataSource {name: 'rxnorm'})
-MERGE (d)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
-
-MATCH (db:Drug {source: 'drugbank', source_id: 'DB00331'})
-MATCH (rx:Drug {source: 'rxnorm', source_id: '6809'})
-MERGE (db)-[:EQUIVALENT_TO {
- confidence: 1.0,
- source: 'codex-normalization',
- match_basis: 'name+inchikey',
- created_at: datetime()
-}]->(rx);
-
-// ---- Warfarin (RxCUI 11289) ----
-MERGE (d:Drug {source: 'rxnorm', source_id: '11289'})
-ON CREATE SET
- d.codex_id = 'codex-drug-RX11289',
- d.canonical_name = 'Warfarin',
- d.drug_type = 'small_molecule',
- d.is_approved = true,
- d.source_attribute_name = 'rxcui',
- d.created_at = datetime(),
- d.updated_at = datetime(),
- d.is_poc = false
-ON MATCH SET d.updated_at = datetime();
-
-MATCH (d:Drug {source: 'rxnorm', source_id: '11289'}), (ds:DataSource {name: 'rxnorm'})
-MERGE (d)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
diff --git a/cypher/03_load_icd11.cypher b/cypher/03_load_icd11.cypher
deleted file mode 100644
index 2d3d3fb..0000000
--- a/cypher/03_load_icd11.cypher
+++ /dev/null
@@ -1,108 +0,0 @@
-// =============================================================
-// Project Codex — ICD-11 Source Loader
-// Step 3: Load ICD-11 conditions into the normalized Codex schema
-// =============================================================
-
-// ---- Type 2 Diabetes Mellitus (JA00) ----
-MERGE (c:Condition {source: 'icd11', source_id: 'JA00'})
-ON CREATE SET
- c.codex_id = 'codex-cond-ICD-JA00',
- c.canonical_name = 'Type 2 diabetes mellitus',
- c.icd11_code = 'JA00',
- c.source_attribute_name = 'code',
- c.created_at = datetime(),
- c.updated_at = datetime(),
- c.is_poc = true
-ON MATCH SET c.updated_at = datetime();
-
-MATCH (c:Condition {source_id: 'JA00'}), (ds:DataSource {name: 'icd11'})
-MERGE (c)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
-
-// ---- Hypertensive diseases (BA00) ----
-MERGE (c:Condition {source: 'icd11', source_id: 'BA00'})
-ON CREATE SET
- c.codex_id = 'codex-cond-ICD-BA00',
- c.canonical_name = 'Hypertensive diseases',
- c.icd11_code = 'BA00',
- c.source_attribute_name = 'code',
- c.created_at = datetime(),
- c.updated_at = datetime(),
- c.is_poc = true
-ON MATCH SET c.updated_at = datetime();
-
-MATCH (c:Condition {source_id: 'BA00'}), (ds:DataSource {name: 'icd11'})
-MERGE (c)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
-
-// ---- Atrial fibrillation (CA01) ----
-MERGE (c:Condition {source: 'icd11', source_id: 'CA01'})
-ON CREATE SET
- c.codex_id = 'codex-cond-ICD-CA01',
- c.canonical_name = 'Atrial fibrillation',
- c.icd11_code = 'CA01',
- c.source_attribute_name = 'code',
- c.created_at = datetime(),
- c.updated_at = datetime(),
- c.is_poc = false
-ON MATCH SET c.updated_at = datetime();
-
-MATCH (c:Condition {source_id: 'CA01'}), (ds:DataSource {name: 'icd11'})
-MERGE (c)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
-
-// ---- Rheumatoid arthritis (FA24) ----
-MERGE (c:Condition {source: 'icd11', source_id: 'FA24'})
-ON CREATE SET
- c.codex_id = 'codex-cond-ICD-FA24',
- c.canonical_name = 'Rheumatoid arthritis',
- c.icd11_code = 'FA24',
- c.source_attribute_name = 'code',
- c.created_at = datetime(),
- c.updated_at = datetime(),
- c.is_poc = true
-ON MATCH SET c.updated_at = datetime();
-
-MATCH (c:Condition {source_id: 'FA24'}), (ds:DataSource {name: 'icd11'})
-MERGE (c)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
-
-// ---- ICD-11 Chapter hierarchy (PARENT_OF) ----
-// Hypertensive diseases is a parent of Atrial fibrillation (both circulatory)
-MATCH (parent:Condition {source_id: 'BA00'})
-MATCH (child:Condition {source_id: 'CA01'})
-MERGE (parent)-[:PARENT_OF {source: 'icd11'}]->(child);
-
-// ---- Drug → Condition TREATS relationships ----
-// Metformin treats Type 2 diabetes
-MATCH (d:Drug {source: 'drugbank', source_id: 'DB00331'})
-MATCH (c:Condition {source_id: 'JA00'})
-MERGE (d)-[:TREATS {
- evidence_level: 'A',
- source: 'drugbank+icd11',
- created_at: datetime()
-}]->(c);
-
-// Warfarin treats Atrial fibrillation
-MATCH (d:Drug {source: 'drugbank', source_id: 'DB00682'})
-MATCH (c:Condition {source_id: 'CA01'})
-MERGE (d)-[:TREATS {
- evidence_level: 'A',
- source: 'drugbank+icd11',
- created_at: datetime()
-}]->(c);
-
-// Methotrexate treats Rheumatoid arthritis
-MATCH (d:Drug {source: 'drugbank', source_id: 'DB00563'})
-MATCH (c:Condition {source_id: 'FA24'})
-MERGE (d)-[:TREATS {
- evidence_level: 'A',
- source: 'drugbank+icd11',
- created_at: datetime()
-}]->(c);
-
-// Aspirin contraindicated for certain bleeding conditions (example)
-MATCH (d:Drug {source: 'drugbank', source_id: 'DB00945'})
-MATCH (c:Condition {source_id: 'CA01'})
-MERGE (d)-[:TREATS {
- evidence_level: 'B',
- source: 'drugbank+icd11',
- note: 'antiplatelet therapy for AF stroke prevention',
- created_at: datetime()
-}]->(c);
diff --git a/cypher/04_load_snomedct.cypher b/cypher/04_load_snomedct.cypher
deleted file mode 100644
index 9ea5532..0000000
--- a/cypher/04_load_snomedct.cypher
+++ /dev/null
@@ -1,160 +0,0 @@
-// =============================================================
-// Project Codex — SNOMED CT Source Loader
-// Step 4: Load SNOMED CT concepts and link to existing nodes
-// =============================================================
-
-// ---- Paracetamol (SNOMED 387517004) — same as Acetaminophen ----
-// SNOMED uses "Paracetamol" (INN/WHO name) vs US "Acetaminophen"
-// This demonstrates the core Codex translation use case
-
-MERGE (d:Drug {source: 'snomedct', source_id: '387517004'})
-ON CREATE SET
- d.codex_id = 'codex-drug-SCT387517004',
- d.canonical_name = 'Paracetamol',
- d.drug_type = 'small_molecule',
- d.is_approved = true,
- d.source_attribute_name = 'concept_id',
- d.created_at = datetime(),
- d.updated_at = datetime(),
- d.is_poc = true
-ON MATCH SET d.updated_at = datetime();
-
-MATCH (d:Drug {source: 'snomedct', source_id: '387517004'}), (ds:DataSource {name: 'snomedct'})
-MERGE (d)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
-
-// SNOMED synonyms for Paracetamol
-MERGE (dn:DrugName {name: 'Paracetamol', country: 'GB', language: 'en'})
-ON CREATE SET dn.name_type = 'generic', dn.is_primary = true,
- dn.source = 'snomedct', dn.source_attribute_name = 'descriptions.FSN',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-MERGE (dn:DrugName {name: 'Paracetamol', country: 'IN', language: 'en'})
-ON CREATE SET dn.name_type = 'generic', dn.is_primary = false,
- dn.source = 'snomedct', dn.source_attribute_name = 'descriptions.Synonym',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-MERGE (dn:DrugName {name: 'Paracetamol', country: 'AU', language: 'en'})
-ON CREATE SET dn.name_type = 'generic', dn.is_primary = true,
- dn.source = 'snomedct', dn.source_attribute_name = 'descriptions.Synonym',
- dn.created_at = datetime(), dn.updated_at = datetime(), dn.is_poc = true;
-
-MATCH (d:Drug {source: 'snomedct', source_id: '387517004'})
-MATCH (dn:DrugName) WHERE dn.name = 'Paracetamol'
-MERGE (d)-[:HAS_NAME {source: 'snomedct', created_at: datetime()}]->(dn);
-
-// Critical: EQUIVALENT_TO between DrugBank Acetaminophen and SNOMED Paracetamol
-MATCH (db:Drug {source: 'drugbank', source_id: 'DB00316'})
-MATCH (sct:Drug {source: 'snomedct', source_id: '387517004'})
-MERGE (db)-[:EQUIVALENT_TO {
- confidence: 1.0,
- source: 'codex-normalization',
- match_basis: 'inchikey',
- note: 'Same compound, different regional names',
- created_at: datetime()
-}]->(sct);
-
-// ---- Aspirin (SNOMED 387458008) ----
-MERGE (d:Drug {source: 'snomedct', source_id: '387458008'})
-ON CREATE SET
- d.codex_id = 'codex-drug-SCT387458008',
- d.canonical_name = 'Aspirin',
- d.drug_type = 'small_molecule',
- d.is_approved = true,
- d.source_attribute_name = 'concept_id',
- d.created_at = datetime(),
- d.updated_at = datetime(),
- d.is_poc = true
-ON MATCH SET d.updated_at = datetime();
-
-MATCH (d:Drug {source: 'snomedct', source_id: '387458008'}), (ds:DataSource {name: 'snomedct'})
-MERGE (d)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
-
-MATCH (db:Drug {source: 'drugbank', source_id: 'DB00945'})
-MATCH (sct:Drug {source: 'snomedct', source_id: '387458008'})
-MERGE (db)-[:EQUIVALENT_TO {
- confidence: 1.0,
- source: 'codex-normalization',
- match_basis: 'name+inchikey',
- created_at: datetime()
-}]->(sct);
-
-// ---- Metformin (SNOMED 387467008) ----
-MERGE (d:Drug {source: 'snomedct', source_id: '387467008'})
-ON CREATE SET
- d.codex_id = 'codex-drug-SCT387467008',
- d.canonical_name = 'Metformin',
- d.drug_type = 'small_molecule',
- d.is_approved = true,
- d.source_attribute_name = 'concept_id',
- d.created_at = datetime(),
- d.updated_at = datetime(),
- d.is_poc = true
-ON MATCH SET d.updated_at = datetime();
-
-MATCH (d:Drug {source: 'snomedct', source_id: '387467008'}), (ds:DataSource {name: 'snomedct'})
-MERGE (d)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
-
-MATCH (db:Drug {source: 'drugbank', source_id: 'DB00331'})
-MATCH (sct:Drug {source: 'snomedct', source_id: '387467008'})
-MERGE (db)-[:EQUIVALENT_TO {
- confidence: 1.0,
- source: 'codex-normalization',
- match_basis: 'name+inchikey',
- created_at: datetime()
-}]->(sct);
-
-// ---- SNOMED Condition: Type 2 Diabetes (44508008) ----
-MERGE (c:Condition {source: 'snomedct', source_id: '44508008'})
-ON CREATE SET
- c.codex_id = 'codex-cond-SCT44508008',
- c.canonical_name = 'Type 2 diabetes mellitus',
- c.snomed_id = '44508008',
- c.source_attribute_name = 'concept_id',
- c.created_at = datetime(),
- c.updated_at = datetime(),
- c.is_poc = true
-ON MATCH SET c.updated_at = datetime();
-
-MATCH (c:Condition {source: 'snomedct', source_id: '44508008'}), (ds:DataSource {name: 'snomedct'})
-MERGE (c)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
-
-// Link SNOMED condition to ICD-11 condition (same disorder)
-MATCH (icd:Condition {source: 'icd11', source_id: 'JA00'})
-MATCH (sct:Condition {source: 'snomedct', source_id: '44508008'})
-MERGE (icd)-[:EQUIVALENT_TO {
- confidence: 1.0,
- source: 'codex-normalization',
- match_basis: 'clinical-mapping',
- created_at: datetime()
-}]->(sct);
-
-// Enrich ICD-11 condition with SNOMED ID
-MATCH (c:Condition {source: 'icd11', source_id: 'JA00'})
-SET c.snomed_id = '44508008';
-
-// ---- SNOMED Condition: Rheumatoid Arthritis (69896004) ----
-MERGE (c:Condition {source: 'snomedct', source_id: '69896004'})
-ON CREATE SET
- c.codex_id = 'codex-cond-SCT69896004',
- c.canonical_name = 'Rheumatoid arthritis',
- c.snomed_id = '69896004',
- c.source_attribute_name = 'concept_id',
- c.created_at = datetime(),
- c.updated_at = datetime(),
- c.is_poc = true
-ON MATCH SET c.updated_at = datetime();
-
-MATCH (c:Condition {source: 'snomedct', source_id: '69896004'}), (ds:DataSource {name: 'snomedct'})
-MERGE (c)-[:SOURCED_FROM {ingested_at: datetime()}]->(ds);
-
-MATCH (icd:Condition {source: 'icd11', source_id: 'FA24'})
-MATCH (sct:Condition {source: 'snomedct', source_id: '69896004'})
-MERGE (icd)-[:EQUIVALENT_TO {
- confidence: 1.0,
- source: 'codex-normalization',
- match_basis: 'clinical-mapping',
- created_at: datetime()
-}]->(sct);
-
-MATCH (c:Condition {source: 'icd11', source_id: 'FA24'})
-SET c.snomed_id = '69896004';
diff --git a/cypher/05_demo_queries.cypher b/cypher/05_demo_queries.cypher
deleted file mode 100644
index c15c632..0000000
--- a/cypher/05_demo_queries.cypher
+++ /dev/null
@@ -1,116 +0,0 @@
-// =============================================================
-// Project Codex — Demo Queries
-// Showcase the value of the normalized schema
-// =============================================================
-
-// ---- Query 1: Core Codex Use Case — Translate a medicine name ----
-// "What is Tylenol called in India, France, and Germany?"
-
-MATCH (d:Drug)-[:HAS_NAME]->(us_name:DrugName {name: 'Tylenol', country: 'US'})
-MATCH (d)-[:HAS_NAME]->(intl:DrugName)
-WHERE intl.country <> 'US'
-RETURN d.canonical_name AS drug,
- intl.name AS international_name,
- intl.country AS country,
- intl.language AS language,
- intl.name_type AS type
-ORDER BY intl.country;
-
-
-// ---- Query 2: Find all names for the same compound across all sources ----
-// "Show me everything we know about Acetaminophen/Paracetamol"
-
-MATCH (d:Drug)
-WHERE d.canonical_name IN ['Acetaminophen', 'Paracetamol']
- OR d.source_id IN ['DB00316', '387517004', '161']
-OPTIONAL MATCH (d)-[:HAS_NAME]->(dn:DrugName)
-OPTIONAL MATCH (d)-[:EQUIVALENT_TO]->(eq:Drug)
-RETURN d.source AS source,
- d.source_id AS source_id,
- d.canonical_name AS canonical_name,
- collect(DISTINCT dn.name + ' (' + dn.country + ')') AS names,
- collect(DISTINCT eq.canonical_name + ' [' + eq.source + ']') AS equivalents;
-
-
-// ---- Query 3: POC subset — all drugs with their translation names ----
-
-MATCH (d:Drug {is_poc: true})-[:HAS_NAME]->(n:DrugName {is_poc: true})
-RETURN d.canonical_name AS drug,
- d.source AS source,
- collect(n.name + ' (' + n.country + ', ' + n.language + ')') AS names
-ORDER BY d.canonical_name;
-
-
-// ---- Query 4: Drug-Condition treatment graph ----
-
-MATCH (d:Drug)-[t:TREATS]->(c:Condition)
-RETURN d.canonical_name AS drug,
- d.source AS drug_source,
- c.canonical_name AS condition,
- c.icd11_code AS icd11_code,
- t.evidence_level AS evidence
-ORDER BY d.canonical_name;
-
-
-// ---- Query 5: Drug interaction safety check ----
-
-MATCH (d1:Drug)-[i:INTERACTS_WITH]->(d2:Drug)
-RETURN d1.canonical_name AS drug_1,
- d2.canonical_name AS drug_2,
- i.severity AS severity,
- i.description AS description;
-
-
-// ---- Query 6: Provenance — where did this data come from? ----
-
-MATCH (d:Drug)-[:SOURCED_FROM]->(ds:DataSource)
-RETURN d.canonical_name AS drug,
- d.source AS source_system,
- d.source_id AS original_id,
- d.source_attribute_name AS original_field,
- ds.version AS source_version,
- ds.last_refreshed AS last_refreshed,
- d.created_at AS ingested_at
-ORDER BY d.canonical_name, d.source;
-
-
-// ---- Query 7: Cross-source equivalence map ----
-// Show the full graph of equivalent drugs across all sources
-
-MATCH (d1:Drug)-[:EQUIVALENT_TO]->(d2:Drug)
-RETURN d1.canonical_name AS name_a,
- d1.source AS source_a,
- d1.source_id AS id_a,
- d2.canonical_name AS name_b,
- d2.source AS source_b,
- d2.source_id AS id_b;
-
-
-// ---- Query 8: Data freshness audit ----
-
-MATCH (ds:DataSource)
-RETURN ds.name AS source,
- ds.version AS version,
- ds.last_refreshed AS last_refreshed,
- ds.license AS license
-ORDER BY ds.name;
-
-
-// ---- Query 9: Find all brand names for a generic drug across countries ----
-// Given a generic INN name, show all brand names worldwide
-
-MATCH (d:Drug {canonical_name: 'Metformin', source: 'drugbank'})
-MATCH (d)-[:HAS_NAME]->(n:DrugName {name_type: 'brand'})
-RETURN n.name AS brand_name,
- n.country AS country,
- n.language AS language
-ORDER BY n.country;
-
-
-// ---- Query 10: Count records by source and POC flag ----
-
-MATCH (d:Drug)
-RETURN d.source AS source,
- d.is_poc AS is_poc,
- count(d) AS drug_count
-ORDER BY d.source, d.is_poc;
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..33d791a
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,55 @@
+services:
+ # The Frontend Service
+ frontend:
+ build:
+ context: ./frontend
+ container_name: codex-frontend
+ ports:
+ - "9000:9000"
+ depends_on:
+ - api
+ volumes:
+ - ./frontend:/app
+ - /app/node_modules
+
+ # The API Service
+ api:
+ build:
+ context: ./backend/codex_build
+ container_name: codex-api
+ ports:
+ - "8000:8000"
+ env_file:
+ - .env
+ environment:
+ - NEO4J_URI=bolt://neo4j:7687
+ - NEO4J_USER=neo4j
+ - NEO4J_PASSWORD=changeme
+ depends_on:
+ neo4j:
+ condition: service_healthy
+ volumes:
+ - ./backend/codex_build:/app
+
+ # The Backend Service
+ neo4j:
+ image: neo4j:5.18.0
+ container_name: codex-neo4j
+ environment:
+ NEO4J_AUTH: "neo4j/changeme"
+ NEO4J_PLUGINS: '["apoc"]'
+ NEO4J_dbms_security_procedures_unrestricted: "apoc.*"
+ ports:
+ - "7474:7474" # Neo4j Browser → http://localhost:7474
+ - "7687:7687" # Bolt (used by the API)
+ volumes:
+ - neo4j_data:/data
+ healthcheck:
+ test: ["CMD-SHELL", "cypher-shell -u neo4j -p changeme 'RETURN 1' > /dev/null 2>&1"]
+ interval: 10s
+ timeout: 5s
+ retries: 15
+ start_period: 30s
+
+volumes:
+ neo4j_data:
\ No newline at end of file
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
new file mode 100644
index 0000000..30ad434
--- /dev/null
+++ b/frontend/Dockerfile
@@ -0,0 +1,12 @@
+FROM node:26
+
+WORKDIR /app
+
+COPY package*.json ./
+RUN npm install
+
+COPY . .
+
+EXPOSE 9000
+
+CMD ["npm", "run", "dev"]
\ No newline at end of file
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..7dbf7eb
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,73 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
+
+```js
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+
+ // Remove tseslint.configs.recommended and replace with this
+ tseslint.configs.recommendedTypeChecked,
+ // Alternatively, use this for stricter rules
+ tseslint.configs.strictTypeChecked,
+ // Optionally, add this for stylistic rules
+ tseslint.configs.stylisticTypeChecked,
+
+ // Other configs...
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
+
+You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
+
+```js
+// eslint.config.js
+import reactX from 'eslint-plugin-react-x'
+import reactDom from 'eslint-plugin-react-dom'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+ // Enable lint rules for React
+ reactX.configs['recommended-typescript'],
+ // Enable lint rules for React DOM
+ reactDom.configs.recommended,
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 0000000..5e6b472
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..d1e6f14
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ project-codex-frontend
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..9afe848
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,3325 @@
+{
+ "name": "project-codex-frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "project-codex-frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "@tailwindcss/vite": "^4.3.3",
+ "i18next": "^25.10.9",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "react-i18next": "^16.6.6",
+ "tailwindcss": "^4.3.3"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.4",
+ "@types/node": "^24.12.0",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^9.39.4",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.4.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.57.0",
+ "vite": "^8.0.1"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
+ "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
+ "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
+ "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
+ "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.122.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
+ "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.11.tgz",
+ "integrity": "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.11.tgz",
+ "integrity": "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.11.tgz",
+ "integrity": "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.11.tgz",
+ "integrity": "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.11.tgz",
+ "integrity": "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.11.tgz",
+ "integrity": "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.11.tgz",
+ "integrity": "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.11.tgz",
+ "integrity": "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.11.tgz",
+ "integrity": "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.11.tgz",
+ "integrity": "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.11.tgz",
+ "integrity": "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.11.tgz",
+ "integrity": "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.11.tgz",
+ "integrity": "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.11.tgz",
+ "integrity": "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.11.tgz",
+ "integrity": "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.7",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
+ "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
+ "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.24.1",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.3"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz",
+ "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-x64": "4.3.3",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.3",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.3",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.3",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.3"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz",
+ "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz",
+ "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz",
+ "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz",
+ "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz",
+ "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz",
+ "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz",
+ "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz",
+ "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz",
+ "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz",
+ "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.11.1",
+ "@emnapi/runtime": "^1.11.1",
+ "@emnapi/wasi-threads": "^1.2.2",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
+ "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz",
+ "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz",
+ "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.3.3",
+ "@tailwindcss/oxide": "4.3.3",
+ "tailwindcss": "4.3.3"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.12.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
+ "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz",
+ "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.57.2",
+ "@typescript-eslint/type-utils": "8.57.2",
+ "@typescript-eslint/utils": "8.57.2",
+ "@typescript-eslint/visitor-keys": "8.57.2",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.57.2",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz",
+ "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.57.2",
+ "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/typescript-estree": "8.57.2",
+ "@typescript-eslint/visitor-keys": "8.57.2",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz",
+ "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.57.2",
+ "@typescript-eslint/types": "^8.57.2",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz",
+ "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/visitor-keys": "8.57.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz",
+ "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz",
+ "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/typescript-estree": "8.57.2",
+ "@typescript-eslint/utils": "8.57.2",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz",
+ "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz",
+ "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.57.2",
+ "@typescript-eslint/tsconfig-utils": "8.57.2",
+ "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/visitor-keys": "8.57.2",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz",
+ "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.57.2",
+ "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/typescript-estree": "8.57.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz",
+ "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.57.2",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
+ "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-rc.7"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.10",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz",
+ "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001781",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz",
+ "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.322",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.322.tgz",
+ "integrity": "sha512-vFU34OcrvMcH66T+dYC3G4nURmgfDVewMIu6Q2urXpumAPSMmzvcn04KVVV8Opikq8Vs5nUbO/8laNhNRqSzYw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.25.1",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.25.1.tgz",
+ "integrity": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz",
+ "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.4.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz",
+ "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+ "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "license": "MIT",
+ "dependencies": {
+ "void-elements": "3.1.0"
+ }
+ },
+ "node_modules/i18next": {
+ "version": "25.10.9",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.9.tgz",
+ "integrity": "sha512-hQY9/bFoQKGlSKMlaCuLR8w1h5JjieqrsnZvEmj1Ja6Ec7fbyc4cTrCsY9mb9Sd8YQ/swsrKz1S9M8AcvVI70w==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://www.locize.com/i18next"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.locize.com"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.29.2"
+ },
+ "peerDependencies": {
+ "typescript": "^5 || ^6"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.36",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
+ "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
+ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
+ "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.4"
+ }
+ },
+ "node_modules/react-i18next": {
+ "version": "16.6.6",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.6.6.tgz",
+ "integrity": "sha512-ZgL2HUoW34UKUkOV7uSQFE1CDnRPD+tCR3ywSuWH7u2iapnz86U8Bi3Vrs620qNDzCf1F47NxglCEkchCTDOHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.29.2",
+ "html-parse-stringify": "^3.0.1",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "peerDependencies": {
+ "i18next": ">= 25.10.9",
+ "react": ">= 16.8.0",
+ "typescript": "^5 || ^6"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.11.tgz",
+ "integrity": "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.122.0",
+ "@rolldown/pluginutils": "1.0.0-rc.11"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0-rc.11",
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.11",
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.11",
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.11",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11"
+ }
+ },
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz",
+ "integrity": "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==",
+ "license": "MIT"
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
+ "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz",
+ "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.57.2",
+ "@typescript-eslint/parser": "8.57.2",
+ "@typescript-eslint/typescript-estree": "8.57.2",
+ "@typescript-eslint/utils": "8.57.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.2.tgz",
+ "integrity": "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==",
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.8",
+ "rolldown": "1.0.0-rc.11",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.0",
+ "esbuild": "^0.27.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+ "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..b2ee83d
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "project-codex-frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --configLoader runner",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tailwindcss/vite": "^4.3.3",
+ "i18next": "^25.10.9",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "react-i18next": "^16.6.6",
+ "tailwindcss": "^4.3.3"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.4",
+ "@types/node": "^24.12.0",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^9.39.4",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.4.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.57.0",
+ "vite": "^8.0.1"
+ }
+}
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
new file mode 100644
index 0000000..6893eb1
--- /dev/null
+++ b/frontend/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg
new file mode 100644
index 0000000..e952219
--- /dev/null
+++ b/frontend/public/icons.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 0000000..5556f13
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1 @@
+/* Global application styles if any */
\ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000..4e63a8c
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,242 @@
+import { useTranslation } from 'react-i18next'
+import { useEffect, useState } from 'react'
+import { Header } from './components/layout/Header'
+import { SearchBar } from './components/search/SearchBar'
+import { ResultsTable } from './components/results/ResultsTable'
+import { TranslationPanel } from './components/translation/TranslationPanel'
+import type { SearchResultRow, TranslateResultRow, LanguageOption } from './types/codex'
+import { LANGUAGE_COUNTRY_MAP, FALLBACK_LANGUAGES } from './types/codex'
+import { getLanguages, searchDrug, translateDrug } from './services/api'
+
+function App() {
+ const { t, i18n } = useTranslation()
+ const [searchQuery, setSearchQuery] = useState('')
+ const [searchResults, setSearchResults] = useState([])
+ const [selectedResult, setSelectedResult] = useState(null)
+ const [availableLanguages, setAvailableLanguages] = useState(FALLBACK_LANGUAGES)
+ const [searchLanguage, setSearchLanguage] = useState('all')
+ const [targetLanguage, setTargetLanguage] = useState('es')
+ const [targetCountry, setTargetCountry] = useState('MX')
+ const [translatedName, setTranslatedName] = useState('')
+ const [translatedBrand, setTranslatedBrand] = useState('')
+ const [translateError, setTranslateError] = useState('')
+ const [searchError, setSearchError] = useState('')
+ const [isLoading, setIsLoading] = useState(false)
+ const [isTranslating, setIsTranslating] = useState(false)
+ const [hasBrand, setHasBrand] = useState(false)
+
+ const getLanguageLabel = (code: string) => {
+ const raw = code.trim()
+ const normalized = raw.toLowerCase()
+
+ let label = raw
+ if (normalized.length <= 3) {
+ try {
+ const displayNames = new Intl.DisplayNames([i18n.language], { type: 'language' })
+ label = displayNames.of(normalized) ?? normalized.toUpperCase()
+ } catch {
+ label = normalized.toUpperCase()
+ }
+ }
+ return label.charAt(0).toUpperCase() + label.slice(1)
+ }
+
+ const availableCountries = LANGUAGE_COUNTRY_MAP[targetLanguage] ?? []
+
+ const getFirstCountryForLanguage = (langCode: string): string => {
+ const available = LANGUAGE_COUNTRY_MAP[langCode] ?? []
+ return available.length > 0 ? available[0].code : ''
+ }
+
+ const handleLanguageChange = (newLang: string) => {
+ const newCountry = getFirstCountryForLanguage(newLang)
+ setTargetLanguage(newLang)
+ setTargetCountry(newCountry)
+ setTranslatedName('')
+ setTranslatedBrand('')
+ setTranslateError('')
+ }
+
+ const loadLanguages = async (isActive: boolean) => {
+ try {
+ const nextLanguages = await getLanguages()
+ if (!isActive) return
+ setAvailableLanguages(nextLanguages)
+ setTargetLanguage((current) => (nextLanguages.includes(current) ? current : nextLanguages[0]))
+ } catch {
+ if (!isActive) return
+ setAvailableLanguages(FALLBACK_LANGUAGES)
+ setTargetLanguage((current) => (FALLBACK_LANGUAGES.includes(current) ? current : FALLBACK_LANGUAGES[0]))
+ }
+ }
+
+ useEffect(() => {
+ let isActive = true
+ loadLanguages(isActive)
+ return () => {
+ isActive = false
+ }
+ }, [])
+
+ const extractTranslatedName = (rows: TranslateResultRow[]) => {
+ const names = rows
+ .map((row) => row.translation)
+ .filter((name): name is string => Boolean(name && name.trim()))
+
+ if (names.length === 0) return '-'
+ return Array.from(new Set(names)).join(', ')
+ }
+
+ const extractTranslatedBrand = (rows: TranslateResultRow[]) => {
+ const brands = rows
+ .map((row) => row.brand)
+ .filter((brand): brand is string => Boolean(brand && brand.trim()))
+
+ if (brands.length === 0) return '-'
+ return Array.from(new Set(brands)).join(', ')
+ }
+
+ const handleSearch = async () => {
+ if (!searchQuery.trim()) {
+ setSearchError('Please enter a search term')
+ return
+ }
+
+ setIsLoading(true)
+ setSearchError('')
+ setTranslateError('')
+ setSearchResults([])
+ setSelectedResult(null)
+ setTranslatedName('')
+ setTranslatedBrand('')
+
+ try {
+ const data = await searchDrug(searchQuery)
+
+ if (!data || !data.name) {
+ setSearchResults([])
+ setSearchError('No results found')
+ } else {
+ setHasBrand(Boolean(data.brand))
+ setSearchResults([data])
+ setSearchError('')
+ }
+ } catch (err) {
+ setSearchError(err instanceof Error ? err.message : 'An error occurred during search')
+ setSearchResults([])
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleTranslateSelected = async () => {
+ if (!selectedResult) {
+ setTranslateError('Select a search result first')
+ return
+ }
+
+ const validCountries = (LANGUAGE_COUNTRY_MAP[targetLanguage] ?? []).map((c) => c.code)
+ const finalCountry = validCountries.includes(targetCountry)
+ ? targetCountry
+ : getFirstCountryForLanguage(targetLanguage)
+
+ setIsTranslating(true)
+ setTranslateError('')
+ setTranslatedName('')
+ setTranslatedBrand('')
+
+ try {
+ const payload = await translateDrug(selectedResult.name, targetLanguage, finalCountry)
+ const results = payload.results ?? []
+ const name = results.length > 0 ? extractTranslatedName(results) : '-'
+ const brand = results.length > 0 ? extractTranslatedBrand(results) : '-'
+ setTranslatedName(name)
+ setTranslatedBrand(brand)
+ if (results.length === 0 || name === '-') {
+ setTranslateError('No translation found for the selected language')
+ }
+ } catch (err) {
+ setTranslateError(err instanceof Error ? err.message : 'Translation failed')
+ } finally {
+ setIsTranslating(false)
+ }
+ }
+
+ const languages: LanguageOption[] = availableLanguages.map((code) => ({
+ code,
+ label: getLanguageLabel(code),
+ }))
+
+ return (
+
+ {/* Top Navigation */}
+
loadLanguages(true)} />
+
+ {/* Main Content Area */}
+
+ {/* Hero Section */}
+
+
+
+ {t('home.pageTitle') || 'Grey Box Pharma-Cross'}
+
+
+ {t('home.pageDescription') || 'Multilingual medical normalization and cross-border drug intelligence powered by RxNorm and Neo4j knowledge graph.'}
+
+
+
+ {t('common.help') || 'Documentation'}
+
+
+
+ {/* Search & Results Panel */}
+
+
+
+ {
+ setSelectedResult(result)
+ setTranslatedName('')
+ setTranslatedBrand('')
+ setTranslateError('')
+ }}
+ hasBrand={hasBrand}
+ searchError={searchError}
+ />
+
+ {selectedResult && (
+
+ )}
+
+
+
+ )
+}
+
+export default App
diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png
new file mode 100644
index 0000000..cc51a3d
Binary files /dev/null and b/frontend/src/assets/hero.png differ
diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/frontend/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg
new file mode 100644
index 0000000..5101b67
--- /dev/null
+++ b/frontend/src/assets/vite.svg
@@ -0,0 +1 @@
+Vite
diff --git a/frontend/src/components/import/ImportLanguageModal.tsx b/frontend/src/components/import/ImportLanguageModal.tsx
new file mode 100644
index 0000000..34739b0
--- /dev/null
+++ b/frontend/src/components/import/ImportLanguageModal.tsx
@@ -0,0 +1,232 @@
+import React, { useState, useRef } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Modal, Button, Alert } from '../ui'
+
+const API_BASE_URL = 'http://localhost:8000'
+
+interface ImportLanguageModalProps {
+ onImportSuccess?: () => void
+ fullWidth?: boolean
+}
+
+export const ImportLanguageModal: React.FC = ({
+ onImportSuccess,
+ fullWidth = false,
+}) => {
+ const { t } = useTranslation()
+ const [isOpen, setIsOpen] = useState(false)
+ const [selectedFile, setSelectedFile] = useState(null)
+ const [isImporting, setIsImporting] = useState(false)
+ const [importMessage, setImportMessage] = useState('')
+ const [importError, setImportError] = useState('')
+ const [isDragging, setIsDragging] = useState(false)
+
+ const fileInputRef = useRef(null)
+
+ const validateAndSetFile = async (file: File) => {
+ setImportError('')
+ setImportMessage('')
+
+ if (!file.name.toLowerCase().endsWith('.json')) {
+ setImportError('El archivo debe tener extensión .json.')
+ setSelectedFile(null)
+ return
+ }
+
+ try {
+ const text = await file.text()
+ const json = JSON.parse(text)
+
+ if (!json || typeof json !== 'object' || Array.isArray(json)) {
+ throw new Error('El JSON debe ser un objeto válido.')
+ }
+
+ if (!json.language || !json.language.code || !Array.isArray(json.terms)) {
+ throw new Error("El archivo no tiene el formato de Language Pack (debe incluir 'language.code' y la lista 'terms').")
+ }
+
+ setSelectedFile(file)
+ setImportError('')
+ } catch (err) {
+ setSelectedFile(null)
+ if (fileInputRef.current) fileInputRef.current.value = ''
+ setImportError(err instanceof SyntaxError ? 'El archivo está corrupto o no es un JSON válido.' : (err as Error).message)
+ }
+ }
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ if (e.target.files && e.target.files.length > 0) {
+ validateAndSetFile(e.target.files[0])
+ }
+ }
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragging(true)
+ }
+
+ const handleDragLeave = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragging(false)
+ }
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragging(false)
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
+ validateAndSetFile(e.dataTransfer.files[0])
+ }
+ }
+
+ const handleUpload = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setImportMessage('')
+ setImportError('')
+
+ if (!selectedFile) {
+ setImportError('Por favor selecciona un archivo .json válido primero.')
+ return
+ }
+
+ setIsImporting(true)
+ const formData = new FormData()
+ formData.append('file', selectedFile)
+
+ try {
+ const response = await fetch(`${API_BASE_URL}/packs/load`, {
+ method: 'POST',
+ body: formData,
+ })
+
+ if (!response.ok) {
+ const errData = await response.json().catch(() => null)
+ throw new Error(errData?.detail || 'Failed to import language pack')
+ }
+
+ const data = await response.json()
+ setImportMessage(data.message || 'Language pack imported successfully!')
+ onImportSuccess?.()
+ setTimeout(() => {
+ resetAndClose()
+ }, 1500)
+ } catch (error) {
+ setImportError((error as Error).message)
+ } finally {
+ setIsImporting(false)
+ }
+ }
+
+ const resetAndClose = () => {
+ setIsOpen(false)
+ setSelectedFile(null)
+ setImportError('')
+ setImportMessage('')
+ if (fileInputRef.current) {
+ fileInputRef.current.value = ''
+ }
+ }
+
+ return (
+ <>
+ {/* Trigger Button */}
+ setIsOpen(true)}
+ icon={
+
+
+
+ }
+ >
+ {t('home.importTitle') || 'Import Language'}
+
+
+ {/* Modal Dialog */}
+
+
+
+ >
+ )
+}
diff --git a/frontend/src/components/language/LanguageSelector.tsx b/frontend/src/components/language/LanguageSelector.tsx
new file mode 100644
index 0000000..eb33066
--- /dev/null
+++ b/frontend/src/components/language/LanguageSelector.tsx
@@ -0,0 +1,78 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { Button, Dropdown } from '../ui'
+import type { LanguageOption } from '../../types/codex'
+
+interface LanguageSelectorProps {
+ languages: LanguageOption[]
+ fullWidth?: boolean
+}
+
+export const LanguageSelector: React.FC = ({
+ languages,
+ fullWidth = false,
+}) => {
+ const { i18n } = useTranslation()
+
+ const capitalize = (str: string) => (str ? str.charAt(0).toUpperCase() + str.slice(1) : str)
+ const currentLangOption = languages.find((l) => l.code === i18n.language) || languages[0]
+ const currentLangLabel = capitalize(currentLangOption?.label || i18n.language)
+
+ const handleSelectLanguage = (code: string) => {
+ i18n.changeLanguage(code)
+ }
+
+ return (
+ (
+
+
+
+ }
+ >
+ {currentLangLabel}
+
+
+
+
+ )}
+ >
+
+ {languages.map((lang) => (
+ handleSelectLanguage(lang.code)}
+ className={`w-full flex items-center justify-between px-3 py-2 text-xs font-semibold rounded-lg transition-colors cursor-pointer text-left ${
+ i18n.language === lang.code
+ ? 'bg-emerald-50 text-emerald-800'
+ : 'text-slate-700 hover:bg-slate-50'
+ }`}
+ >
+ {capitalize(lang.label)}
+ {lang.code}
+
+ ))}
+
+
+ )
+}
diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx
new file mode 100644
index 0000000..8c37a68
--- /dev/null
+++ b/frontend/src/components/layout/Header.tsx
@@ -0,0 +1,242 @@
+import React, { useState, useEffect } from 'react'
+import { useTranslation } from 'react-i18next'
+import { PopulateDropdown } from '../populate/PopulateDropdown'
+import { ImportLanguageModal } from '../import/ImportLanguageModal'
+import { LanguageSelector } from '../language/LanguageSelector'
+import type { LanguageOption } from '../../types/codex'
+
+interface HeaderProps {
+ languages: LanguageOption[]
+ onImportSuccess?: () => void
+}
+
+export const Header: React.FC = ({ languages, onImportSuccess }) => {
+ const { t } = useTranslation()
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
+
+ // Prevent background scrolling when mobile menu is open
+ useEffect(() => {
+ if (isMobileMenuOpen) {
+ document.body.style.overflow = 'hidden'
+ } else {
+ document.body.style.overflow = ''
+ }
+ return () => {
+ document.body.style.overflow = ''
+ }
+ }, [isMobileMenuOpen])
+
+ // Close on Escape key
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape' && isMobileMenuOpen) {
+ setIsMobileMenuOpen(false)
+ }
+ }
+ window.addEventListener('keydown', handleKeyDown)
+ return () => window.removeEventListener('keydown', handleKeyDown)
+ }, [isMobileMenuOpen])
+
+ return (
+ <>
+
+
+ {/* Fullscreen Mobile Drawer Menu below the static header */}
+ {isMobileMenuOpen && (
+
+ {/* Navigation Sections */}
+
+
+ {/* Actions Section (Bottom-aligned Column) */}
+
+
+ Database & Settings
+
+
+
+
+ {
+ onImportSuccess?.()
+ setIsMobileMenuOpen(false)
+ }}
+ />
+
+
+
+
+
+
+ {/* Social & Region Footer */}
+
+
Connect with us
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ )
+}
+
+
+
+
diff --git a/frontend/src/components/populate/PopulateDropdown.tsx b/frontend/src/components/populate/PopulateDropdown.tsx
new file mode 100644
index 0000000..40c3671
--- /dev/null
+++ b/frontend/src/components/populate/PopulateDropdown.tsx
@@ -0,0 +1,188 @@
+import React, { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Button, Dropdown } from '../ui'
+
+const API_BASE_URL = 'http://localhost:8000'
+
+interface SourceOption {
+ id: string
+ label: string
+ description: string
+}
+
+const SOURCES: SourceOption[] = [
+ { id: 'drugbank', label: 'DrugBank', description: 'Pharmaceutical & commercial brand database' },
+ { id: 'snomed', label: 'SNOMED CT', description: 'Global clinical terminology & diagnoses' },
+ { id: 'rxnorm', label: 'RxNorm', description: 'Standardized clinical drugs (US NLM)' },
+ { id: 'icd11', label: 'ICD-11', description: 'WHO International Classification of Diseases' },
+]
+
+interface PopulateDropdownProps {
+ fullWidth?: boolean
+}
+
+export const PopulateDropdown: React.FC = ({ fullWidth = false }) => {
+ const { t } = useTranslation()
+ const [selectedSources, setSelectedSources] = useState>({
+ drugbank: false,
+ snomed: false,
+ rxnorm: false,
+ icd11: false,
+ })
+ const [isLoading, setIsLoading] = useState(false)
+ const [progress, setProgress] = useState('')
+
+ const toggleSource = (id: string) => {
+ setSelectedSources((prev) => ({ ...prev, [id]: !prev[id] }))
+ }
+
+ const selectedCount = Object.values(selectedSources).filter(Boolean).length
+
+ const handlePopulateClick = async () => {
+ const sourcesToPopulate = Object.entries(selectedSources)
+ .filter(([, isSelected]) => isSelected)
+ .map(([id]) => id)
+
+ if (sourcesToPopulate.length === 0) return
+
+ setIsLoading(true)
+ setProgress('Starting population...')
+
+ try {
+ const response = await fetch(`${API_BASE_URL}/api/populate-sources`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ selectedSources: sourcesToPopulate }),
+ })
+
+ if (!response.body) return
+
+ const reader = response.body.getReader()
+ const decoder = new TextDecoder()
+ let buffer = ''
+
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+
+ buffer += decoder.decode(value, { stream: true })
+ const lines = buffer.split('\n')
+ buffer = lines.pop() || ''
+
+ for (const line of lines) {
+ const trimmed = line.trim()
+ if (!trimmed) continue
+
+ try {
+ const cleanLine = trimmed.startsWith('data:') ? trimmed.replace(/^data:\s*/, '') : trimmed
+ const parsed = JSON.parse(cleanLine)
+ if (parsed.progress) {
+ setProgress(parsed.progress)
+ }
+ } catch (err) {
+ console.error('Error parsing chunk:', err)
+ }
+ }
+ }
+ setProgress('Successfully populated Neo4j!')
+ } catch (error) {
+ console.error('Error during streaming:', error)
+ setProgress('Error processing request.')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+ (
+
+
+
+ }
+ >
+ {t('sides.populateButton') || 'Populate'}
+ {selectedCount > 0 && (
+
+ {selectedCount}
+
+ )}
+
+
+
+
+ )}
+ >
+
+
+ 🧬 {t('sides.populateLabel') || 'Populate local source(s):'}
+
+ Neo4j
+
+
+ {/* Sources Checkboxes */}
+
+ {SOURCES.map((source) => (
+
+ toggleSource(source.id)}
+ className="mt-0.5 w-4 h-4 rounded text-emerald-600 focus:ring-emerald-500 border-slate-300 cursor-pointer"
+ />
+
+
{source.label}
+
{source.description}
+
+
+ ))}
+
+
+ {/* Progress / Status Message */}
+ {progress && (
+
+ {progress}
+
+ )}
+
+ {/* Action Button */}
+
+ {t('sides.populateButton') || 'Populate'} ({selectedCount})
+
+
+ )
+}
diff --git a/frontend/src/components/results/ResultsTable.tsx b/frontend/src/components/results/ResultsTable.tsx
new file mode 100644
index 0000000..a4ab9b1
--- /dev/null
+++ b/frontend/src/components/results/ResultsTable.tsx
@@ -0,0 +1,153 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { Alert } from '../ui'
+import type { SearchResultRow } from '../../types/codex'
+
+interface ResultsTableProps {
+ results: SearchResultRow[]
+ selectedResult: SearchResultRow | null
+ onSelectResult: (result: SearchResultRow) => void
+ hasBrand: boolean
+ searchError?: string
+}
+
+export const ResultsTable: React.FC = ({
+ results,
+ selectedResult,
+ onSelectResult,
+ hasBrand,
+ searchError,
+}) => {
+ const { t } = useTranslation()
+
+ const isRowSelected = (row: SearchResultRow) => {
+ if (!selectedResult) return false
+ return (
+ selectedResult.name === row.name &&
+ selectedResult.brand === row.brand &&
+ selectedResult.language === row.language &&
+ selectedResult.country === row.country &&
+ selectedResult.source_id === row.source_id
+ )
+ }
+
+ return (
+
+
+
+ {t('home.resultsTitle') || 'Search Results'}
+
+ {results.length > 0 && (
+
+ {results.length} {results.length === 1 ? 'match' : 'matches'}
+
+ )}
+
+
+ {searchError &&
}
+
+ {results.length > 0 ? (
+ <>
+ {/* Desktop & Tablet Table View */}
+
+
+
+
+
+
+ {hasBrand ? 'Drug Name for Brand' : 'Drug Name'}
+
+ {hasBrand && Brand }
+ Type
+
+ {hasBrand ? 'Language for Brand' : 'Language'}
+
+
+ {hasBrand ? 'Countries for Brand' : 'Countries'}
+
+
+
+
+ {results.map((row, index) => {
+ const selected = isRowSelected(row)
+ return (
+ onSelectResult(row)}
+ className={`cursor-pointer transition-colors ${
+ selected
+ ? 'bg-emerald-50/80 font-medium text-emerald-950 hover:bg-emerald-100/60'
+ : 'hover:bg-slate-50/80 text-slate-700'
+ }`}
+ >
+
+ {selected && (
+
+ )}
+ {row.name}
+
+ {hasBrand && {row.brand || '-'} }
+
+
+ {hasBrand ? 'brand name drug' : row.type || 'ingredient'}
+
+
+ {row.language}
+ {row.country ?? '-'}
+
+ )
+ })}
+
+
+
+
+
+ {/* Mobile Card View */}
+
+ {results.map((row, index) => {
+ const selected = isRowSelected(row)
+ return (
+
onSelectResult(row)}
+ className={`p-4 rounded-xl border transition-all cursor-pointer ${
+ selected
+ ? 'border-emerald-500 bg-emerald-50/50 shadow-xs ring-1 ring-emerald-500'
+ : 'border-slate-200 bg-white hover:border-slate-300'
+ }`}
+ >
+
+
{row.name}
+
+ {row.language}
+
+
+
+ {row.brand && (
+
+ Brand: {row.brand}
+
+ )}
+
+
+
+ Type: {hasBrand ? 'brand name drug' : row.type || 'ingredient'}
+
+ •
+ Country: {row.country ?? 'N/A'}
+
+
+ )
+ })}
+
+ >
+ ) : (
+
+
+ {t('home.sampleMedicine') || 'Search for a drug or click search to view matching results.'}
+
+
+ )}
+
+ )
+}
diff --git a/frontend/src/components/search/SearchBar.tsx b/frontend/src/components/search/SearchBar.tsx
new file mode 100644
index 0000000..54a4c2e
--- /dev/null
+++ b/frontend/src/components/search/SearchBar.tsx
@@ -0,0 +1,91 @@
+import React, { type KeyboardEvent } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Button, Select, Input } from '../ui'
+import type { LanguageOption } from '../../types/codex'
+
+interface SearchBarProps {
+ searchLanguage: string
+ onSearchLanguageChange: (lang: string) => void
+ searchQuery: string
+ onSearchQueryChange: (query: string) => void
+ onSearch: () => void
+ isLoading: boolean
+ languages: LanguageOption[]
+}
+
+export const SearchBar: React.FC = ({
+ searchLanguage,
+ onSearchLanguageChange,
+ searchQuery,
+ onSearchQueryChange,
+ onSearch,
+ isLoading,
+ languages,
+}) => {
+ const { t } = useTranslation()
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ onSearch()
+ }
+ }
+
+ const languageOptions = [
+ { value: 'all', label: 'All languages' },
+ ...languages.map((lang) => ({
+ value: lang.code,
+ label: `${lang.label} (${lang.code.toUpperCase()})`,
+ })),
+ ]
+
+ return (
+
+
+ {/* Language Filter */}
+
+ onSearchLanguageChange(e.target.value)}
+ />
+
+
+ {/* Drug Input & Search Button */}
+
+
+
onSearchQueryChange(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder={t('home.sourcePlaceholder') || 'Search RxNorm drug, brand or synonym...'}
+ icon={
+
+
+
+ }
+ />
+
+
+ {t('common.search') || 'Search'}
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/translation/TranslationPanel.tsx b/frontend/src/components/translation/TranslationPanel.tsx
new file mode 100644
index 0000000..28590d6
--- /dev/null
+++ b/frontend/src/components/translation/TranslationPanel.tsx
@@ -0,0 +1,146 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { Button, Select, Alert } from '../ui'
+import type { SearchResultRow, LanguageOption, CountryOption } from '../../types/codex'
+
+interface TranslationPanelProps {
+ selectedResult: SearchResultRow
+ targetLanguage: string
+ targetCountry: string
+ onTargetLanguageChange: (lang: string) => void
+ onTargetCountryChange: (country: string) => void
+ onTranslate: () => void
+ isTranslating: boolean
+ languages: LanguageOption[]
+ availableCountries: CountryOption[]
+ translatedName: string
+ translatedBrand: string
+ translateError?: string
+}
+
+export const TranslationPanel: React.FC = ({
+ selectedResult,
+ targetLanguage,
+ targetCountry,
+ onTargetLanguageChange,
+ onTargetCountryChange,
+ onTranslate,
+ isTranslating,
+ languages,
+ availableCountries,
+ translatedName,
+ translatedBrand,
+ translateError,
+}) => {
+ const { t } = useTranslation()
+
+ const languageOptions = languages.map((lang) => ({
+ value: lang.code,
+ label: `${lang.label} (${lang.code.toUpperCase()})`,
+ }))
+
+ const countryOptions =
+ availableCountries.length > 0
+ ? availableCountries.map((c) => ({
+ value: c.code,
+ label: `${c.label} (${c.code.toUpperCase()})`,
+ }))
+ : [{ value: '', label: t('home.noCountries') || 'No countries available' }]
+
+ return (
+
+
+
🌐
+
+
+ {t('home.localizeTitle') || 'Translate & Cross-Reference Drug'}
+
+
+ {t('home.selectedDrug') || 'Selected drug:'} {selectedResult.name} ({selectedResult.language.toUpperCase()})
+
+
+
+
+ {/* Target Language & Country selectors */}
+
+ {/* Target Language */}
+
+ onTargetLanguageChange(e.target.value)}
+ />
+
+
+ {/* Target Country */}
+
+ onTargetCountryChange(e.target.value)}
+ />
+
+
+ {/* Translate Action */}
+
+
+ {t('common.translate') || 'Translate Selected'}
+
+
+
+
+ {/* Error / Info State */}
+ {translateError &&
}
+
+ {/* Translation Result Card */}
+ {(translatedName || isTranslating) && (
+
+
+ {t('home.crossReferencedMatch') || 'Cross-Referenced Match'}
+
+
+
+
+
+ {t('home.original') || 'Original'} ({selectedResult.language.toUpperCase()})
+
+
+ {selectedResult.name}
+
+
+
+
+
+ {t('home.translation') || 'Translation'} ({targetLanguage.toUpperCase()})
+
+
+ {translatedName || (isTranslating ? '...' : '-')}
+
+
+
+
+
+ {t('home.brandInCountry') || 'Brand in'} {targetCountry.toUpperCase()}
+
+
+ {translatedBrand || (isTranslating ? '...' : '-')}
+
+
+
+
+ )}
+
+ )
+}
diff --git a/frontend/src/components/ui/Alert.tsx b/frontend/src/components/ui/Alert.tsx
new file mode 100644
index 0000000..cd1622c
--- /dev/null
+++ b/frontend/src/components/ui/Alert.tsx
@@ -0,0 +1,39 @@
+import React from 'react'
+
+export interface AlertProps {
+ type?: 'error' | 'success' | 'info' | 'warning'
+ message?: string
+ children?: React.ReactNode
+ className?: string
+}
+
+export const Alert: React.FC = ({
+ type = 'error',
+ message,
+ children,
+ className = '',
+}) => {
+ const typeStyles = {
+ error: 'bg-rose-50 border-rose-200 text-rose-700',
+ success: 'bg-emerald-50 border-emerald-200 text-emerald-800',
+ info: 'bg-sky-50 border-sky-200 text-sky-800',
+ warning: 'bg-amber-50 border-amber-200 text-amber-800',
+ }
+
+ const icons = {
+ error: '⚠️',
+ success: '✅',
+ info: 'ℹ️',
+ warning: '⚠️',
+ }
+
+ return (
+
+
{icons[type]}
+
{message || children}
+
+ )
+}
diff --git a/frontend/src/components/ui/Button.tsx b/frontend/src/components/ui/Button.tsx
new file mode 100644
index 0000000..7bd98d8
--- /dev/null
+++ b/frontend/src/components/ui/Button.tsx
@@ -0,0 +1,63 @@
+import React from 'react'
+
+export interface ButtonProps extends React.ButtonHTMLAttributes {
+ variant?: 'primary' | 'nav' | 'secondary' | 'outline' | 'ghost'
+ size?: 'sm' | 'md' | 'lg'
+ isLoading?: boolean
+ icon?: React.ReactNode
+ fullWidth?: boolean
+}
+
+export const Button: React.FC = ({
+ children,
+ variant = 'primary',
+ size = 'md',
+ isLoading = false,
+ icon,
+ fullWidth = false,
+ className = '',
+ disabled,
+ ...props
+}) => {
+ const baseStyles =
+ 'inline-flex items-center justify-center font-semibold transition-all duration-150 focus:outline-none focus:ring-2 focus:ring-emerald-300 disabled:cursor-not-allowed cursor-pointer'
+
+ const variantStyles = {
+ primary:
+ 'bg-[#4e7f77] hover:bg-[#3d6861] active:scale-[0.98] text-white shadow-sm disabled:bg-slate-300 disabled:text-slate-500',
+ nav:
+ 'bg-[#3d6861] hover:bg-[#325650] active:scale-[0.98] text-white shadow-sm border border-[#5d8d85] disabled:opacity-60',
+ secondary:
+ 'bg-slate-100 hover:bg-slate-200 text-slate-800 disabled:bg-slate-50 disabled:text-slate-400',
+ outline:
+ 'border border-slate-300 hover:bg-slate-50 text-slate-700 disabled:opacity-50',
+ ghost:
+ 'hover:bg-slate-100 text-slate-600 hover:text-slate-900 disabled:opacity-50',
+ }
+
+ const sizeStyles = {
+ sm: 'text-xs px-2.5 py-1.5 rounded-md gap-1.5',
+ md: 'text-sm px-3.5 py-1.5 rounded-lg gap-2',
+ lg: 'text-base px-5 py-2.5 rounded-xl gap-2.5',
+ }
+
+ return (
+
+ {isLoading ? (
+ <>
+
+ {children}
+ >
+ ) : (
+ <>
+ {icon && {icon} }
+ {children}
+ >
+ )}
+
+ )
+}
diff --git a/frontend/src/components/ui/Dropdown.tsx b/frontend/src/components/ui/Dropdown.tsx
new file mode 100644
index 0000000..5dd3941
--- /dev/null
+++ b/frontend/src/components/ui/Dropdown.tsx
@@ -0,0 +1,153 @@
+import React, { useState, useRef, useEffect, useLayoutEffect } from 'react'
+
+export interface DropdownProps {
+ trigger: (isOpen: boolean) => React.ReactNode
+ children: React.ReactNode
+ align?: 'left' | 'right'
+ widthClass?: string
+ className?: string
+}
+
+export const Dropdown: React.FC = ({
+ trigger,
+ children,
+ align = 'right',
+ widthClass = 'w-72 sm:w-80',
+ className = '',
+}) => {
+ const [isOpen, setIsOpen] = useState(false)
+ const [openUpward, setOpenUpward] = useState(false)
+ const [horizontalAlign, setHorizontalAlign] = useState<'left' | 'right' | 'center'>(align)
+ const [maxHeightStyle, setMaxHeightStyle] = useState('calc(100vh - 120px)')
+
+ const dropdownRef = useRef(null)
+ const panelRef = useRef(null)
+
+ // Smart viewport auto-positioning
+ const calculatePosition = () => {
+ if (!dropdownRef.current) return
+
+ const rect = dropdownRef.current.getBoundingClientRect()
+ const viewportHeight = window.innerHeight
+ const viewportWidth = window.innerWidth
+
+ // 1. Vertical placement: Check if space below is less than 280px and space above is larger
+ const spaceBelow = viewportHeight - rect.bottom
+ const spaceAbove = rect.top
+
+ const shouldOpenUp = spaceBelow < 300 && spaceAbove > spaceBelow
+ setOpenUpward(shouldOpenUp)
+
+ // Dynamic max-height based on available space
+ const availableHeight = shouldOpenUp ? spaceAbove - 24 : spaceBelow - 24
+ setMaxHeightStyle(`${Math.max(160, availableHeight)}px`)
+
+ // 2. Horizontal placement: Adjust if popping outside screen
+ if (viewportWidth < 640) {
+ setHorizontalAlign(rect.left < 40 ? 'left' : rect.right > viewportWidth - 40 ? 'right' : 'center')
+ } else {
+ if (align === 'right') {
+ if (rect.right < 300) {
+ setHorizontalAlign('left')
+ } else {
+ setHorizontalAlign('right')
+ }
+ } else {
+ if (viewportWidth - rect.left < 300) {
+ setHorizontalAlign('right')
+ } else {
+ setHorizontalAlign('left')
+ }
+ }
+ }
+ }
+
+ useLayoutEffect(() => {
+ if (isOpen) {
+ calculatePosition()
+ }
+ }, [isOpen])
+
+ // Recalculate on window resize or scroll
+ useEffect(() => {
+ if (!isOpen) return
+
+ const handleResizeOrScroll = () => {
+ calculatePosition()
+ }
+
+ window.addEventListener('resize', handleResizeOrScroll)
+ window.addEventListener('scroll', handleResizeOrScroll, true)
+
+ return () => {
+ window.removeEventListener('resize', handleResizeOrScroll)
+ window.removeEventListener('scroll', handleResizeOrScroll, true)
+ }
+ }, [isOpen])
+
+ // Close when clicking outside
+ useEffect(() => {
+ const handleOutsideClick = (event: MouseEvent) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+ setIsOpen(false)
+ }
+ }
+ if (isOpen) {
+ document.addEventListener('mousedown', handleOutsideClick)
+ }
+ return () => {
+ document.removeEventListener('mousedown', handleOutsideClick)
+ }
+ }, [isOpen])
+
+ // Close on Escape key
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape' && isOpen) {
+ setIsOpen(false)
+ }
+ }
+ document.addEventListener('keydown', handleKeyDown)
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown)
+ }
+ }, [isOpen])
+
+ // Compute Tailwind classes for placement
+ const verticalClass = openUpward ? 'bottom-full mb-2' : 'top-full mt-2'
+
+ const horizontalClass =
+ horizontalAlign === 'center'
+ ? 'left-1/2 -translate-x-1/2'
+ : horizontalAlign === 'left'
+ ? 'left-0'
+ : 'right-0'
+
+ const originClass = openUpward
+ ? horizontalAlign === 'right'
+ ? 'origin-bottom-right'
+ : horizontalAlign === 'left'
+ ? 'origin-bottom-left'
+ : 'origin-bottom'
+ : horizontalAlign === 'right'
+ ? 'origin-top-right'
+ : horizontalAlign === 'left'
+ ? 'origin-top-left'
+ : 'origin-top'
+
+ return (
+
+
setIsOpen(!isOpen)}>{trigger(isOpen)}
+
+ {isOpen && (
+
+ {children}
+
+ )}
+
+ )
+}
diff --git a/frontend/src/components/ui/Input.tsx b/frontend/src/components/ui/Input.tsx
new file mode 100644
index 0000000..d130616
--- /dev/null
+++ b/frontend/src/components/ui/Input.tsx
@@ -0,0 +1,50 @@
+import React from 'react'
+
+export interface InputProps extends React.InputHTMLAttributes {
+ label?: string
+ icon?: React.ReactNode
+ error?: string
+}
+
+export const Input: React.FC = ({
+ label,
+ icon,
+ error,
+ className = '',
+ id,
+ disabled,
+ ...props
+}) => {
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+ {icon && (
+
+ {icon}
+
+ )}
+
+
+ {error &&
{error} }
+
+ )
+}
diff --git a/frontend/src/components/ui/Modal.tsx b/frontend/src/components/ui/Modal.tsx
new file mode 100644
index 0000000..00542e6
--- /dev/null
+++ b/frontend/src/components/ui/Modal.tsx
@@ -0,0 +1,79 @@
+import React, { useEffect } from 'react'
+
+export interface ModalProps {
+ isOpen: boolean
+ onClose: () => void
+ title: string
+ subtitle?: string
+ icon?: React.ReactNode
+ children: React.ReactNode
+ maxWidthClass?: string
+}
+
+export const Modal: React.FC = ({
+ isOpen,
+ onClose,
+ title,
+ subtitle,
+ icon,
+ children,
+ maxWidthClass = 'max-w-lg',
+}) => {
+ // Close on Escape key
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape' && isOpen) {
+ onClose()
+ }
+ }
+ if (isOpen) {
+ document.addEventListener('keydown', handleKeyDown)
+ }
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown)
+ }
+ }, [isOpen, onClose])
+
+ if (!isOpen) return null
+
+ return (
+
+
e.stopPropagation()}
+ >
+ {/* Header */}
+
+
+ {icon && (
+
+ {icon}
+
+ )}
+
+
{title}
+ {subtitle &&
{subtitle}
}
+
+
+
+
+
+
+
+
+
+ {/* Content */}
+
{children}
+
+
+ )
+}
diff --git a/frontend/src/components/ui/Select.tsx b/frontend/src/components/ui/Select.tsx
new file mode 100644
index 0000000..d4e0a12
--- /dev/null
+++ b/frontend/src/components/ui/Select.tsx
@@ -0,0 +1,62 @@
+import React from 'react'
+
+export interface SelectOption {
+ value: string
+ label: string
+}
+
+export interface SelectProps extends React.SelectHTMLAttributes {
+ label?: string
+ options?: SelectOption[]
+ error?: string
+}
+
+export const Select: React.FC = ({
+ label,
+ options,
+ children,
+ className = '',
+ id,
+ disabled,
+ error,
+ ...props
+}) => {
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+
+ {options
+ ? options.map((opt) => (
+
+ {opt.label}
+
+ ))
+ : children}
+
+
+
+ {error &&
{error} }
+
+ )
+}
diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts
new file mode 100644
index 0000000..6816ada
--- /dev/null
+++ b/frontend/src/components/ui/index.ts
@@ -0,0 +1,6 @@
+export * from './Button'
+export * from './Dropdown'
+export * from './Modal'
+export * from './Select'
+export * from './Input'
+export * from './Alert'
diff --git a/frontend/src/i18n/config.ts b/frontend/src/i18n/config.ts
new file mode 100644
index 0000000..32c53cf
--- /dev/null
+++ b/frontend/src/i18n/config.ts
@@ -0,0 +1,26 @@
+import i18n from 'i18next'
+import { initReactI18next } from 'react-i18next'
+import en from './locales/en.json'
+import es from './locales/es.json'
+import fr from './locales/fr.json'
+import de from './locales/de.json'
+
+const resources = {
+ en: { translation: en },
+ es: { translation: es },
+ fr: { translation: fr },
+ de: { translation: de },
+}
+
+i18n
+ .use(initReactI18next)
+ .init({
+ resources,
+ lng: 'en',
+ fallbackLng: 'en',
+ interpolation: {
+ escapeValue: false,
+ },
+ })
+
+export default i18n
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json
new file mode 100644
index 0000000..740da7b
--- /dev/null
+++ b/frontend/src/i18n/locales/de.json
@@ -0,0 +1,37 @@
+{
+ "nav": {
+ "home": "Startseite",
+ "about": "Über uns"
+ },
+ "common": {
+ "help": "Hilfe",
+ "search": "Suchen",
+ "translate": "Übersetzen"
+ },
+ "sides": {
+ "populateLabel": "Lokale Datenquellen füllen:",
+ "populateButton": "Füllen",
+ "populateLoader": "Füllt Daten..."
+ },
+ "home": {
+ "pageTitle": "Projekt Medizinischer Kodex",
+ "pageDescription": "Ein Werkzeug zur Koordination medizinischer Ausrüstung zwischen Ländern.",
+ "searchTitle": "Nach Medikamentennamen suchen...",
+ "sourceLanguage": "Ausgangssprache",
+ "sourcePlaceholder": "Wort zum Suchen",
+ "resultsTitle": "Ergebnisse",
+ "sampleMedicine": "Medikament 1",
+ "localizeTitle": "Medikamentennamen übersetzen/lokalisieren...",
+ "selectedDrug": "Ausgewähltes Medikament:",
+ "targetLanguage": "Zielsprache",
+ "targetPlaceholder": "Zielsprache",
+ "targetCountry": "Zielland",
+ "noCountries": "Keine Länder verfügbar",
+ "translationPlaceholder": "Die Übersetzung wird hier angezeigt",
+ "importTitle": "Sprachdatei importieren",
+ "crossReferencedMatch": "Referenzierte Übereinstimmung",
+ "original": "Original",
+ "translation": "Übersetzung",
+ "brandInCountry": "Marke in"
+ }
+}
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
new file mode 100644
index 0000000..98ba018
--- /dev/null
+++ b/frontend/src/i18n/locales/en.json
@@ -0,0 +1,37 @@
+{
+ "nav": {
+ "home": "Home",
+ "about": "About"
+ },
+ "common": {
+ "help": "Help",
+ "search": "Search",
+ "translate": "Translate"
+ },
+ "sides": {
+ "populateLabel": "Populate local source(s):",
+ "populateButton": "Populate",
+ "populateLoader": "Populating..."
+ },
+ "home": {
+ "pageTitle": "Project Medical Codex",
+ "pageDescription": "A tool developed to help coordinate medical equipment between countries.",
+ "searchTitle": "Search for drug name...",
+ "sourceLanguage": "Source Language",
+ "sourcePlaceholder": "Word to search",
+ "resultsTitle": "Results",
+ "sampleMedicine": "Medicine 1",
+ "localizeTitle": "Translate/Localize drug name...",
+ "selectedDrug": "Selected drug:",
+ "targetLanguage": "Target Language",
+ "targetPlaceholder": "Target Language",
+ "targetCountry": "Target Country",
+ "noCountries": "No countries available",
+ "translationPlaceholder": "Translation will appear here",
+ "importTitle": "Import Language File",
+ "crossReferencedMatch": "Cross-Referenced Match",
+ "original": "Original",
+ "translation": "Translation",
+ "brandInCountry": "Brand in"
+ }
+}
diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json
new file mode 100644
index 0000000..66f3c04
--- /dev/null
+++ b/frontend/src/i18n/locales/es.json
@@ -0,0 +1,37 @@
+{
+ "nav": {
+ "home": "Inicio",
+ "about": "Acerca de"
+ },
+ "common": {
+ "help": "Ayuda",
+ "search": "Buscar",
+ "translate": "Traducir"
+ },
+ "sides": {
+ "populateLabel": "Rellenar fuente(s) local(es):",
+ "populateButton": "Rellenar",
+ "populateLoader": "Rellenando..."
+ },
+ "home": {
+ "pageTitle": "Proyecto Códice Médico",
+ "pageDescription": "Una herramienta desarrollada para ayudar a coordinar equipos médicos entre países.",
+ "searchTitle": "Buscar nombre de medicamento...",
+ "sourceLanguage": "Idioma de origen",
+ "sourcePlaceholder": "Palabra a buscar",
+ "resultsTitle": "Resultados",
+ "sampleMedicine": "Medicina 1",
+ "localizeTitle": "Traducir/Localizar nombre de medicamento...",
+ "selectedDrug": "Medicamento seleccionado:",
+ "targetLanguage": "Idioma de destino",
+ "targetPlaceholder": "Idioma de destino",
+ "targetCountry": "País de destino",
+ "noCountries": "No hay países disponibles",
+ "translationPlaceholder": "La traducción aparecerá aquí",
+ "importTitle": "Importar archivo de idioma",
+ "crossReferencedMatch": "Coincidencia Cruzada",
+ "original": "Original",
+ "translation": "Traducción",
+ "brandInCountry": "Marca en"
+ }
+}
diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json
new file mode 100644
index 0000000..c4290bd
--- /dev/null
+++ b/frontend/src/i18n/locales/fr.json
@@ -0,0 +1,37 @@
+{
+ "nav": {
+ "home": "Accueil",
+ "about": "À propos"
+ },
+ "common": {
+ "help": "Aide",
+ "search": "Rechercher",
+ "translate": "Traduire"
+ },
+ "sides": {
+ "populateLabel": "Remplir les sources locales:",
+ "populateButton": "Remplir",
+ "populateLoader": "Remplissage en cours..."
+ },
+ "home": {
+ "pageTitle": "Projet Codex Médical",
+ "pageDescription": "Un outil développé pour aider à coordonner les équipements médicaux entre les pays.",
+ "searchTitle": "Rechercher un nom de médicament...",
+ "sourceLanguage": "Langue source",
+ "sourcePlaceholder": "Mot à rechercher",
+ "resultsTitle": "Résultats",
+ "sampleMedicine": "Médicament 1",
+ "localizeTitle": "Traduire/Localiser le nom du médicament...",
+ "selectedDrug": "Médicament sélectionné:",
+ "targetLanguage": "Langue cible",
+ "targetPlaceholder": "Langue cible",
+ "targetCountry": "Pays cible",
+ "noCountries": "Aucun pays disponible",
+ "translationPlaceholder": "La traduction s'affichera ici",
+ "importTitle": "Importer un fichier de langue",
+ "crossReferencedMatch": "Correspondance Croisée",
+ "original": "Original",
+ "translation": "Traduction",
+ "brandInCountry": "Marque en"
+ }
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..2420e5f
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,41 @@
+@import url('https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;500;600;700;800;900&display=swap');
+@import "tailwindcss";
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ scrollbar-gutter: stable;
+}
+
+html,
+body,
+#root {
+ margin: 0;
+ min-height: 100%;
+}
+
+body {
+ background: #d9dde2;
+ font-family: 'Source Sans 3', 'Trebuchet MS', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+@keyframes slideDownFade {
+ from {
+ opacity: 0;
+ transform: translateY(-16px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.animate-drawer-in {
+ animation: slideDownFade 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
+}
+
+
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 0000000..14bc510
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,15 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import { I18nextProvider } from 'react-i18next'
+import './index.css'
+import './i18n/config'
+import i18n from './i18n/config'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+ ,
+)
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
new file mode 100644
index 0000000..c335716
--- /dev/null
+++ b/frontend/src/services/api.ts
@@ -0,0 +1,82 @@
+import type {
+ LanguagesResponse,
+ SearchResponse,
+ TranslateResponse,
+} from '../types/codex'
+import { FALLBACK_LANGUAGES } from '../types/codex'
+
+export const API_BASE_URL = 'http://localhost:8000'
+
+/**
+ * Fetches available language codes that have data in Neo4j.
+ */
+export async function getLanguages(): Promise {
+ try {
+ const response = await fetch(`${API_BASE_URL}/languages`)
+ if (!response.ok) {
+ throw new Error('Failed to load languages')
+ }
+ const data = (await response.json()) as LanguagesResponse
+ return Array.isArray(data.languages) && data.languages.length > 0
+ ? data.languages
+ : FALLBACK_LANGUAGES
+ } catch (error) {
+ console.warn('Using fallback languages due to error:', error)
+ return FALLBACK_LANGUAGES
+ }
+}
+
+/**
+ * Searches for a drug / medical term in Neo4j.
+ */
+export async function searchDrug(query: string): Promise {
+ const response = await fetch(
+ `${API_BASE_URL}/search?term=${encodeURIComponent(query.toLowerCase())}`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: query,
+ limit: 20,
+ }),
+ }
+ )
+
+ if (!response.ok) {
+ const errorBody = await response.json().catch(() => null)
+ throw new Error(errorBody?.detail ?? 'Failed to search')
+ }
+
+ const data = (await response.json()) as SearchResponse | null
+ return data
+}
+
+/**
+ * Translates a drug term to a destination language and country.
+ */
+export async function translateDrug(
+ term: string,
+ lang: string,
+ country: string
+): Promise {
+ const response = await fetch(`${API_BASE_URL}/translate`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ term,
+ lang,
+ country,
+ }),
+ })
+
+ if (!response.ok) {
+ const errorBody = await response.json().catch(() => null)
+ throw new Error(errorBody?.detail ?? 'Translation request failed')
+ }
+
+ return (await response.json()) as TranslateResponse
+}
diff --git a/frontend/src/types/codex.ts b/frontend/src/types/codex.ts
new file mode 100644
index 0000000..eafa220
--- /dev/null
+++ b/frontend/src/types/codex.ts
@@ -0,0 +1,83 @@
+export interface CountryOption {
+ code: string
+ label: string
+}
+
+export interface LanguageOption {
+ code: string
+ label: string
+}
+
+export interface LanguagesResponse {
+ languages: string[]
+}
+
+export interface SearchResultRow {
+ source_id: string | null
+ source_name: string | null
+ name: string
+ brand: string | null
+ type: string
+ country: string
+ language: string
+ uploaded_at: string | null
+}
+
+export interface SearchResponse {
+ source_id: string
+ source_name: string
+ name: string
+ brand: string
+ type: string
+ country: string
+ language: string
+ uploaded_at: string
+}
+
+export interface TranslateResultRow {
+ source_id: string | null
+ source_name: string | null
+ translation: string
+ brand: string | null
+ type: string
+ country: string | null
+ language: string
+ uploaded_at: string | null
+}
+
+export interface TranslateResponse {
+ found: boolean
+ results: TranslateResultRow[]
+}
+
+export interface TranslateRequest {
+ term: string
+ lang: string
+ country: string
+}
+
+export const LANGUAGE_COUNTRY_MAP: Record = {
+ es: [
+ { code: 'MX', label: 'Mexico' },
+ { code: 'ES', label: 'Spain' },
+ ],
+ en: [
+ { code: 'US', label: 'United States' },
+ { code: 'GB', label: 'United Kingdom' },
+ { code: 'CA', label: 'Canada' },
+ ],
+ fr: [
+ { code: 'FR', label: 'France' },
+ { code: 'CA', label: 'Canada' },
+ { code: 'BE', label: 'Belgium' },
+ ],
+ ru: [
+ { code: 'RU', label: 'Russia' },
+ ],
+ uk: [
+ { code: 'UA', label: 'Ukraine' },
+ { code: 'PL', label: 'Poland' },
+ ],
+}
+
+export const FALLBACK_LANGUAGES = ['en', 'es', 'fr']
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
new file mode 100644
index 0000000..af516fc
--- /dev/null
+++ b/frontend/tsconfig.app.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2023",
+ "useDefineForClassFields": true,
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 0000000..8a67f62
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..e02b756
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,18 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [
+ react(),
+ tailwindcss(),
+ ],
+ server: {
+ host: true,
+ port: 9000,
+ watch: {
+ usePolling: true,
+ },
+ }
+})
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 55fee1d..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-neo4j>=5.0
-fastapi>=0.110.0
-uvicorn[standard]>=0.29.0
-python-dotenv>=1.0.0
diff --git a/sample_data/drugbank_sample.json b/sample_data/drugbank_sample.json
deleted file mode 100644
index cf32681..0000000
--- a/sample_data/drugbank_sample.json
+++ /dev/null
@@ -1,104 +0,0 @@
-[
- {
- "drugbank_id": "DB00316",
- "name": "Acetaminophen",
- "type": "small molecule",
- "groups": ["approved"],
- "description": "Analgesic and antipyretic drug used to treat mild to moderate pain and fever.",
- "cas_number": "103-90-2",
- "inchikey": "RZVAJINKPMORJF-UHFFFAOYSA-N",
- "brands": [
- { "name": "Tylenol", "country": "US" },
- { "name": "Panadol", "country": "GB" },
- { "name": "Dolo", "country": "IN" },
- { "name": "Dafalgan", "country": "FR" },
- { "name": "Ben-u-ron", "country": "DE" }
- ],
- "indication": "For the treatment of mild to moderate pain and fever.",
- "mechanism_of_action": "Inhibits prostaglandin synthesis in the CNS.",
- "affected_organisms": ["Humans"],
- "interactions": [
- { "drugbank_id": "DB00682", "description": "Warfarin anticoagulant effect increased", "severity": "moderate" }
- ]
- },
- {
- "drugbank_id": "DB00945",
- "name": "Aspirin",
- "type": "small molecule",
- "groups": ["approved"],
- "description": "Salicylate anti-inflammatory and analgesic drug.",
- "cas_number": "50-78-2",
- "inchikey": "BSYNRYMUTXBXSQ-UHFFFAOYSA-N",
- "brands": [
- { "name": "Aspirin", "country": "US" },
- { "name": "Ecotrin", "country": "US" },
- { "name": "Disprin", "country": "IN" },
- { "name": "Aspro", "country": "AU" },
- { "name": "ASS", "country": "DE" }
- ],
- "indication": "For mild to moderate pain, fever, and as antiplatelet therapy.",
- "mechanism_of_action": "Irreversibly inhibits COX-1 and COX-2 enzymes.",
- "affected_organisms": ["Humans"],
- "interactions": [
- { "drugbank_id": "DB00682", "description": "Increased bleeding risk with warfarin", "severity": "major" }
- ]
- },
- {
- "drugbank_id": "DB00331",
- "name": "Metformin",
- "type": "small molecule",
- "groups": ["approved"],
- "description": "Biguanide antidiabetic drug used to control blood glucose in type 2 diabetes.",
- "cas_number": "657-24-9",
- "inchikey": "XZWYZXLIPXDOLR-UHFFFAOYSA-N",
- "brands": [
- { "name": "Glucophage", "country": "US" },
- { "name": "Glycomet", "country": "IN" },
- { "name": "Metforal", "country": "IT" },
- { "name": "Siofor", "country": "DE" },
- { "name": "Diabex", "country": "AU" }
- ],
- "indication": "First-line treatment for type 2 diabetes mellitus.",
- "mechanism_of_action": "Activates AMP-activated protein kinase (AMPK), reducing hepatic gluconeogenesis.",
- "affected_organisms": ["Humans"],
- "interactions": []
- },
- {
- "drugbank_id": "DB00682",
- "name": "Warfarin",
- "type": "small molecule",
- "groups": ["approved"],
- "description": "Anticoagulant used to prevent blood clot formation.",
- "cas_number": "81-81-2",
- "inchikey": "PJVWKTKQMONHTI-UHFFFAOYSA-N",
- "brands": [
- { "name": "Coumadin", "country": "US" },
- { "name": "Warf", "country": "IN" },
- { "name": "Marevan", "country": "GB" },
- { "name": "Warfin", "country": "AU" }
- ],
- "indication": "Prevention and treatment of thromboembolic events.",
- "mechanism_of_action": "Vitamin K antagonist; inhibits VKORC1.",
- "affected_organisms": ["Humans"],
- "interactions": []
- },
- {
- "drugbank_id": "DB00563",
- "name": "Methotrexate",
- "type": "small molecule",
- "groups": ["approved"],
- "description": "Antimetabolite used in cancer and autoimmune disease treatment.",
- "cas_number": "59-05-2",
- "inchikey": "FBOZXECLQNJBKD-ZDUSSCGKSA-N",
- "brands": [
- { "name": "Trexall", "country": "US" },
- { "name": "Folitrax", "country": "IN" },
- { "name": "Methofar", "country": "FR" },
- { "name": "Lantarel", "country": "DE" }
- ],
- "indication": "Treatment of certain cancers and autoimmune conditions including rheumatoid arthritis.",
- "mechanism_of_action": "Inhibits dihydrofolate reductase (DHFR), blocking nucleotide synthesis.",
- "affected_organisms": ["Humans"],
- "interactions": []
- }
-]
diff --git a/sample_data/icd11_sample.json b/sample_data/icd11_sample.json
deleted file mode 100644
index 602acbd..0000000
--- a/sample_data/icd11_sample.json
+++ /dev/null
@@ -1,67 +0,0 @@
-[
- {
- "code": "JA00",
- "title": "Type 2 diabetes mellitus",
- "stem_id": "1780132783",
- "chapter": "05",
- "chapter_title": "Endocrine, nutritional or metabolic diseases",
- "parent_code": "5A10-5A14.Z",
- "linearization_uri": "http://id.who.int/icd/entity/1780132783",
- "definition": "A metabolic disorder characterized by high blood sugar, insulin resistance, and relative lack of insulin.",
- "synonyms": ["Adult-onset diabetes", "Non-insulin-dependent diabetes mellitus", "NIDDM"],
- "inclusions": ["Diabetes mellitus due to insulin secretory defect"],
- "exclusions": ["Type 1 diabetes mellitus (5A10)"]
- },
- {
- "code": "BA00",
- "title": "Hypertensive diseases",
- "stem_id": "1388325637",
- "chapter": "11",
- "chapter_title": "Diseases of the circulatory system",
- "parent_code": "BA00-BA0Z",
- "linearization_uri": "http://id.who.int/icd/entity/1388325637",
- "definition": "Conditions characterized by persistently elevated blood pressure in the arteries.",
- "synonyms": ["High blood pressure", "HTN"],
- "inclusions": ["Essential hypertension", "Secondary hypertension"],
- "exclusions": ["Hypertension complicating pregnancy (JA24)"]
- },
- {
- "code": "CA01",
- "title": "Atrial fibrillation",
- "stem_id": "230690328",
- "chapter": "11",
- "chapter_title": "Diseases of the circulatory system",
- "parent_code": "CA01",
- "linearization_uri": "http://id.who.int/icd/entity/230690328",
- "definition": "Supraventricular arrhythmia characterized by uncoordinated atrial activation with consequent deterioration of atrial mechanical function.",
- "synonyms": ["AF", "A-fib"],
- "inclusions": ["Paroxysmal atrial fibrillation", "Persistent atrial fibrillation"],
- "exclusions": []
- },
- {
- "code": "FA24",
- "title": "Rheumatoid arthritis",
- "stem_id": "1068173526",
- "chapter": "16",
- "chapter_title": "Diseases of the musculoskeletal system or connective tissue",
- "parent_code": "FA20-FA2Z",
- "linearization_uri": "http://id.who.int/icd/entity/1068173526",
- "definition": "Chronic inflammatory disorder affecting many joints, including those in the hands and feet.",
- "synonyms": ["RA", "Rheumatoid disease"],
- "inclusions": ["Seronegative rheumatoid arthritis", "Seropositive rheumatoid arthritis"],
- "exclusions": ["Juvenile idiopathic arthritis (KA80)"]
- },
- {
- "code": "2C91.Z",
- "title": "Malignant neoplasm of breast, unspecified",
- "stem_id": "1162746685",
- "chapter": "02",
- "chapter_title": "Neoplasms",
- "parent_code": "2C91",
- "linearization_uri": "http://id.who.int/icd/entity/1162746685",
- "definition": "Malignant tumour arising from epithelial cells of the breast.",
- "synonyms": ["Breast cancer", "Carcinoma of breast"],
- "inclusions": ["Ductal carcinoma in situ (DCIS)", "Lobular carcinoma"],
- "exclusions": ["Benign neoplasm of breast (GA12)"]
- }
-]
diff --git a/sample_data/rxnorm_sample.json b/sample_data/rxnorm_sample.json
deleted file mode 100644
index 123a6dc..0000000
--- a/sample_data/rxnorm_sample.json
+++ /dev/null
@@ -1,80 +0,0 @@
-[
- {
- "rxcui": "161",
- "name": "Acetaminophen",
- "tty": "IN",
- "suppress": "N",
- "umlscui": "C0000970",
- "related_concepts": [
- { "rxcui": "209459", "name": "Acetaminophen 325 MG Oral Tablet", "tty": "SCD" },
- { "rxcui": "209387", "name": "Acetaminophen 500 MG Oral Tablet", "tty": "SCD" },
- { "rxcui": "198440", "name": "Tylenol 325 MG Oral Tablet", "tty": "SBD" }
- ],
- "attributes": [
- { "atv": "Drug", "atn": "SUBSTANCE_TYPE" },
- { "atv": "N", "atn": "OTC" }
- ]
- },
- {
- "rxcui": "1191",
- "name": "Aspirin",
- "tty": "IN",
- "suppress": "N",
- "umlscui": "C0004057",
- "related_concepts": [
- { "rxcui": "212033", "name": "Aspirin 325 MG Oral Tablet", "tty": "SCD" },
- { "rxcui": "437649", "name": "Ecotrin 325 MG Delayed Release Oral Tablet", "tty": "SBD" },
- { "rxcui": "308460", "name": "Aspirin 81 MG Oral Tablet", "tty": "SCD" }
- ],
- "attributes": [
- { "atv": "Drug", "atn": "SUBSTANCE_TYPE" },
- { "atv": "Y", "atn": "OTC" }
- ]
- },
- {
- "rxcui": "6809",
- "name": "Metformin",
- "tty": "IN",
- "suppress": "N",
- "umlscui": "C0025598",
- "related_concepts": [
- { "rxcui": "861007", "name": "Metformin hydrochloride 500 MG Oral Tablet", "tty": "SCD" },
- { "rxcui": "861025", "name": "Glucophage 500 MG Oral Tablet", "tty": "SBD" },
- { "rxcui": "861021", "name": "Metformin hydrochloride 1000 MG Oral Tablet", "tty": "SCD" }
- ],
- "attributes": [
- { "atv": "Drug", "atn": "SUBSTANCE_TYPE" },
- { "atv": "N", "atn": "OTC" }
- ]
- },
- {
- "rxcui": "11289",
- "name": "Warfarin",
- "tty": "IN",
- "suppress": "N",
- "umlscui": "C0043031",
- "related_concepts": [
- { "rxcui": "855295", "name": "Warfarin Sodium 5 MG Oral Tablet", "tty": "SCD" },
- { "rxcui": "855302", "name": "Coumadin 5 MG Oral Tablet", "tty": "SBD" }
- ],
- "attributes": [
- { "atv": "Drug", "atn": "SUBSTANCE_TYPE" },
- { "atv": "N", "atn": "OTC" }
- ]
- },
- {
- "rxcui": "7052",
- "name": "Methotrexate",
- "tty": "IN",
- "suppress": "N",
- "umlscui": "C0025677",
- "related_concepts": [
- { "rxcui": "105586", "name": "Methotrexate 2.5 MG Oral Tablet", "tty": "SCD" },
- { "rxcui": "573497", "name": "Trexall 5 MG Oral Tablet", "tty": "SBD" }
- ],
- "attributes": [
- { "atv": "Drug", "atn": "SUBSTANCE_TYPE" },
- { "atv": "N", "atn": "OTC" }
- ]
- }
-]
diff --git a/sample_data/snomedct_sample.json b/sample_data/snomedct_sample.json
deleted file mode 100644
index 1a19eb3..0000000
--- a/sample_data/snomedct_sample.json
+++ /dev/null
@@ -1,104 +0,0 @@
-[
- {
- "concept_id": "372687004",
- "fsn": "Amoxicillin (substance)",
- "preferred_term": "Amoxicillin",
- "semantic_tag": "substance",
- "active": true,
- "module": "900000000000207008",
- "descriptions": [
- { "type": "FSN", "term": "Amoxicillin (substance)", "language": "en" },
- { "type": "Synonym", "term": "Amoxil", "language": "en" },
- { "type": "Synonym", "term": "p-Hydroxyampicillin", "language": "en" }
- ],
- "relationships": [
- { "type": "Is a", "destination_id": "372687004", "destination_term": "Penicillin" },
- { "type": "Has dose form", "destination_id": "421026006", "destination_term": "Oral tablet" }
- ]
- },
- {
- "concept_id": "387517004",
- "fsn": "Paracetamol (substance)",
- "preferred_term": "Paracetamol",
- "semantic_tag": "substance",
- "active": true,
- "module": "900000000000207008",
- "descriptions": [
- { "type": "FSN", "term": "Paracetamol (substance)", "language": "en" },
- { "type": "Synonym", "term": "Acetaminophen", "language": "en" },
- { "type": "Synonym", "term": "4'-hydroxyacetanilide", "language": "en" }
- ],
- "relationships": [
- { "type": "Is a", "destination_id": "372665008", "destination_term": "Analgesic" },
- { "type": "Is a", "destination_id": "372741007", "destination_term": "Antipyretic" }
- ]
- },
- {
- "concept_id": "387467008",
- "fsn": "Metformin (substance)",
- "preferred_term": "Metformin",
- "semantic_tag": "substance",
- "active": true,
- "module": "900000000000207008",
- "descriptions": [
- { "type": "FSN", "term": "Metformin (substance)", "language": "en" },
- { "type": "Synonym", "term": "Dimethylbiguanide", "language": "en" }
- ],
- "relationships": [
- { "type": "Is a", "destination_id": "84524008", "destination_term": "Biguanide" },
- { "type": "Has therapeutic role", "destination_id": "67901000", "destination_term": "Antidiabetic" }
- ]
- },
- {
- "concept_id": "44508008",
- "fsn": "Type 2 diabetes mellitus (disorder)",
- "preferred_term": "Type 2 diabetes mellitus",
- "semantic_tag": "disorder",
- "active": true,
- "module": "900000000000207008",
- "descriptions": [
- { "type": "FSN", "term": "Type 2 diabetes mellitus (disorder)", "language": "en" },
- { "type": "Synonym", "term": "NIDDM", "language": "en" },
- { "type": "Synonym", "term": "Non-insulin-dependent diabetes mellitus", "language": "en" },
- { "type": "Synonym", "term": "T2DM", "language": "en" }
- ],
- "relationships": [
- { "type": "Is a", "destination_id": "73211009", "destination_term": "Diabetes mellitus" },
- { "type": "Finding site", "destination_id": "113331007", "destination_term": "Endocrine system" }
- ]
- },
- {
- "concept_id": "69896004",
- "fsn": "Rheumatoid arthritis (disorder)",
- "preferred_term": "Rheumatoid arthritis",
- "semantic_tag": "disorder",
- "active": true,
- "module": "900000000000207008",
- "descriptions": [
- { "type": "FSN", "term": "Rheumatoid arthritis (disorder)", "language": "en" },
- { "type": "Synonym", "term": "RA", "language": "en" },
- { "type": "Synonym", "term": "Atrophic arthritis", "language": "en" }
- ],
- "relationships": [
- { "type": "Is a", "destination_id": "3723001", "destination_term": "Arthritis" },
- { "type": "Associated morphology", "destination_id": "23583003", "destination_term": "Inflammation" }
- ]
- },
- {
- "concept_id": "387458008",
- "fsn": "Aspirin (substance)",
- "preferred_term": "Aspirin",
- "semantic_tag": "substance",
- "active": true,
- "module": "900000000000207008",
- "descriptions": [
- { "type": "FSN", "term": "Aspirin (substance)", "language": "en" },
- { "type": "Synonym", "term": "Acetylsalicylic acid", "language": "en" },
- { "type": "Synonym", "term": "ASA", "language": "en" }
- ],
- "relationships": [
- { "type": "Is a", "destination_id": "372665008", "destination_term": "Analgesic" },
- { "type": "Is a", "destination_id": "372578009", "destination_term": "Antiplatelet drug" }
- ]
- }
-]
diff --git a/scripts/__pycache__/config.cpython-313.pyc b/scripts/__pycache__/config.cpython-313.pyc
deleted file mode 100644
index 039d540..0000000
Binary files a/scripts/__pycache__/config.cpython-313.pyc and /dev/null differ
diff --git a/scripts/__pycache__/db.cpython-313.pyc b/scripts/__pycache__/db.cpython-313.pyc
deleted file mode 100644
index af5ec7f..0000000
Binary files a/scripts/__pycache__/db.cpython-313.pyc and /dev/null differ
diff --git a/scripts/db.py b/scripts/db.py
deleted file mode 100644
index 5d2ef74..0000000
--- a/scripts/db.py
+++ /dev/null
@@ -1,42 +0,0 @@
-"""
-Project Codex — Neo4j driver wrapper
-"""
-
-from neo4j import GraphDatabase
-from config import NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD
-
-
-class CodexDB:
- def __init__(self):
- self._driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))
-
- def close(self):
- self._driver.close()
-
- def run(self, cypher: str, parameters: dict = None):
- with self._driver.session() as session:
- result = session.run(cypher, parameters or {})
- return result.data()
-
- def run_file(self, filepath: str):
- """Execute a .cypher file, skipping comment-only lines."""
- with open(filepath, "r") as f:
- content = f.read()
-
- # Split on semicolons to get individual statements
- statements = [s.strip() for s in content.split(";") if s.strip()]
- executed = 0
- for stmt in statements:
- # Skip pure comment blocks
- lines = [l for l in stmt.splitlines() if l.strip() and not l.strip().startswith("//")]
- if not lines:
- continue
- self.run(stmt)
- executed += 1
- return executed
-
- def __enter__(self):
- return self
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- self.close()
diff --git a/scripts/dry_run.py b/scripts/dry_run.py
deleted file mode 100644
index 0616ee1..0000000
--- a/scripts/dry_run.py
+++ /dev/null
@@ -1,122 +0,0 @@
-"""
-Project Codex — Dry Run (no Neo4j required)
-Validates sample data and shows what would be loaded.
-Run this without a Neo4j connection to verify data quality.
-
-Usage:
- python scripts/dry_run.py
-"""
-
-import json
-import os
-import sys
-
-sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
-from config import SAMPLE_DATA_DIR
-
-
-def load_json(filename):
- path = os.path.join(SAMPLE_DATA_DIR, filename)
- with open(path) as f:
- return json.load(f)
-
-
-def section(title):
- print("\n" + "=" * 60)
- print(f" {title}")
- print("=" * 60)
-
-
-def dry_run():
- print("\nProject Codex — ETL Dry Run")
- print("Validating sample data files...\n")
-
- # ---- DrugBank ----
- section("DrugBank Sample Data")
- drugbank = load_json("drugbank_sample.json")
- print(f" Records: {len(drugbank)}")
- total_brands = 0
- for d in drugbank:
- brands = d.get("brands", [])
- total_brands += len(brands)
- countries = [b["country"] for b in brands]
- print(f" [{d['drugbank_id']}] {d['name']:<20} → {len(brands)} brand names: {countries}")
- print(f" Total brand-name nodes to create: {total_brands}")
-
- # ---- RxNorm ----
- section("RxNorm Sample Data")
- rxnorm = load_json("rxnorm_sample.json")
- print(f" Records: {len(rxnorm)}")
- for r in rxnorm:
- dose_forms = [c["name"] for c in r.get("related_concepts", []) if c["tty"] in ("SCD","SBD")]
- print(f" [RxCUI {r['rxcui']:>6}] {r['name']:<20} → {len(dose_forms)} dose-form names")
-
- # ---- ICD-11 ----
- section("ICD-11 Sample Data")
- icd11 = load_json("icd11_sample.json")
- print(f" Records: {len(icd11)}")
- for c in icd11:
- syns = c.get("synonyms", [])
- print(f" [{c['code']:<8}] {c['title']:<40} | Chapter {c['chapter']}: {c['chapter_title'][:30]}")
- if syns:
- print(f" Synonyms: {', '.join(syns)}")
-
- # ---- SNOMED CT ----
- section("SNOMED CT Sample Data")
- snomed = load_json("snomedct_sample.json")
- print(f" Records: {len(snomed)}")
- for c in snomed:
- descs = [d["term"] for d in c.get("descriptions", []) if d["type"] == "Synonym"]
- print(f" [{c['concept_id']}] ({c['semantic_tag']:<10}) {c['preferred_term']}")
- if descs:
- print(f" Synonyms: {', '.join(descs)}")
-
- # ---- Codex Name Translation Preview ----
- section("Codex Name Translation Preview")
- print(" Drug: Acetaminophen (DB00316)")
- print(" All regional names found across all sources:\n")
-
- name_map = {}
- # From DrugBank
- apap = next(d for d in drugbank if d["drugbank_id"] == "DB00316")
- for b in apap["brands"]:
- name_map[b["country"]] = b["name"]
- name_map["US (generic)"] = apap["name"]
-
- # From SNOMED (Paracetamol)
- paracetamol = next(c for c in snomed if c["concept_id"] == "387517004")
- for desc in paracetamol["descriptions"]:
- if desc["term"] == "Paracetamol":
- for country in ["GB", "IN", "AU"]:
- if country not in name_map:
- name_map[country] = "Paracetamol"
-
- print(f" {'Country/Region':<20} {'Name'}")
- print(f" {'-'*20} {'-'*20}")
- for region, name in sorted(name_map.items()):
- print(f" {region:<20} {name}")
-
- # ---- Schema Summary ----
- section("Estimated Graph Size (after full load)")
- drug_nodes = len(set(d["drugbank_id"] for d in drugbank))
- drug_nodes += len(rxnorm) # RxNorm Drug nodes
- drug_nodes += len([c for c in snomed if c["semantic_tag"] == "substance"])
- cond_nodes = len(icd11) + len([c for c in snomed if c["semantic_tag"] == "disorder"])
- name_nodes = total_brands + len(rxnorm) * 2 # approx
- print(f" ~{drug_nodes:>4} Drug nodes")
- print(f" ~{name_nodes:>4} DrugName nodes")
- print(f" ~{cond_nodes:>4} Condition nodes")
- print(f" ~ 4 DataSource nodes")
- print(f" ~ 5 Ingredient nodes")
- print()
- print(" POC-flagged drugs (for dev/demo/debug):")
- poc_names = [d["name"] for d in drugbank[:3]]
- for n in poc_names:
- print(f" ✓ {n}")
-
- print("\nDry run complete. No database connection required.")
- print("Run scripts/run_etl.py to load into Neo4j.\n")
-
-
-if __name__ == "__main__":
- dry_run()
diff --git a/scripts/loaders/.DS_Store b/scripts/loaders/.DS_Store
deleted file mode 100644
index 64adcd6..0000000
Binary files a/scripts/loaders/.DS_Store and /dev/null differ
diff --git a/scripts/loaders/__init__.py b/scripts/loaders/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/scripts/loaders/__pycache__/__init__.cpython-313.pyc b/scripts/loaders/__pycache__/__init__.cpython-313.pyc
deleted file mode 100644
index 5903f7c..0000000
Binary files a/scripts/loaders/__pycache__/__init__.cpython-313.pyc and /dev/null differ
diff --git a/scripts/loaders/__pycache__/drugbank_loader.cpython-313.pyc b/scripts/loaders/__pycache__/drugbank_loader.cpython-313.pyc
deleted file mode 100644
index 6b3fb00..0000000
Binary files a/scripts/loaders/__pycache__/drugbank_loader.cpython-313.pyc and /dev/null differ
diff --git a/scripts/loaders/__pycache__/icd11_loader.cpython-313.pyc b/scripts/loaders/__pycache__/icd11_loader.cpython-313.pyc
deleted file mode 100644
index 5fe93a7..0000000
Binary files a/scripts/loaders/__pycache__/icd11_loader.cpython-313.pyc and /dev/null differ
diff --git a/scripts/loaders/__pycache__/rxnorm_loader.cpython-313.pyc b/scripts/loaders/__pycache__/rxnorm_loader.cpython-313.pyc
deleted file mode 100644
index 6cd3459..0000000
Binary files a/scripts/loaders/__pycache__/rxnorm_loader.cpython-313.pyc and /dev/null differ
diff --git a/scripts/loaders/__pycache__/snomedct_loader.cpython-313.pyc b/scripts/loaders/__pycache__/snomedct_loader.cpython-313.pyc
deleted file mode 100644
index 176003b..0000000
Binary files a/scripts/loaders/__pycache__/snomedct_loader.cpython-313.pyc and /dev/null differ
diff --git a/scripts/loaders/drugbank_loader.py b/scripts/loaders/drugbank_loader.py
deleted file mode 100644
index dfc2aa1..0000000
--- a/scripts/loaders/drugbank_loader.py
+++ /dev/null
@@ -1,159 +0,0 @@
-"""
-Project Codex — DrugBank Source Loader
-Reads sample_data/drugbank_sample.json and writes into normalized Codex schema.
-"""
-
-import json
-import uuid
-from datetime import datetime, timezone
-
-
-def now_iso():
- return datetime.now(timezone.utc).isoformat()
-
-
-def load_drugbank(db, data_path: str, poc_ids: set = None):
- """
- Load DrugBank JSON sample data into Neo4j.
-
- Args:
- db: CodexDB instance
- data_path: Path to drugbank_sample.json
- poc_ids: Set of DrugBank IDs to flag as POC (default: first 3)
- """
- with open(data_path) as f:
- drugs = json.load(f)
-
- if poc_ids is None:
- poc_ids = {d["drugbank_id"] for d in drugs[:3]}
-
- print(f"[DrugBank] Loading {len(drugs)} drugs...")
-
- for drug in drugs:
- db_id = drug["drugbank_id"]
- is_poc = db_id in poc_ids
- ts = now_iso()
-
- # ---- Upsert Drug node ----
- db.run("""
- MERGE (d:Drug {source: 'drugbank', source_id: $source_id})
- ON CREATE SET
- d.codex_id = $codex_id,
- d.canonical_name = $name,
- d.drug_type = $drug_type,
- d.is_approved = $is_approved,
- d.source_attribute_name = 'drugbank_id',
- d.created_at = datetime($ts),
- d.updated_at = datetime($ts),
- d.is_poc = $is_poc
- ON MATCH SET
- d.updated_at = datetime($ts)
- """, {
- "source_id": db_id,
- "codex_id": "codex-drug-" + db_id,
- "name": drug["name"],
- "drug_type": drug.get("type", "small molecule").replace(" ", "_"),
- "is_approved": "approved" in drug.get("groups", []),
- "ts": ts,
- "is_poc": is_poc,
- })
-
- # ---- Upsert Ingredient node ----
- if drug.get("inchikey"):
- db.run("""
- MERGE (i:Ingredient {inchikey: $inchikey})
- ON CREATE SET
- i.codex_id = $codex_id,
- i.name = $name,
- i.cas_number = $cas_number,
- i.source = 'drugbank',
- i.source_id = $source_id,
- i.source_attribute_name = 'cas_number',
- i.created_at = datetime($ts),
- i.updated_at = datetime($ts),
- i.is_poc = $is_poc
- ON MATCH SET i.updated_at = datetime($ts)
- """, {
- "inchikey": drug["inchikey"],
- "codex_id": "codex-ing-" + drug["inchikey"][:8],
- "name": drug["name"],
- "cas_number": drug.get("cas_number", ""),
- "source_id": db_id + "-active",
- "ts": ts,
- "is_poc": is_poc,
- })
-
- db.run("""
- MATCH (d:Drug {source: 'drugbank', source_id: $db_id})
- MATCH (i:Ingredient {inchikey: $inchikey})
- MERGE (d)-[:CONTAINS_INGREDIENT {role: 'active', source: 'drugbank'}]->(i)
- """, {"db_id": db_id, "inchikey": drug["inchikey"]})
-
- # ---- Upsert DrugName nodes ----
- for brand in drug.get("brands", []):
- db.run("""
- MERGE (dn:DrugName {name: $name, country: $country, language: $language})
- ON CREATE SET
- dn.name_type = 'brand',
- dn.is_primary = true,
- dn.source = 'drugbank',
- dn.source_attribute_name = 'brands.name',
- dn.created_at = datetime($ts),
- dn.updated_at = datetime($ts),
- dn.is_poc = $is_poc
- ON MATCH SET dn.updated_at = datetime($ts)
- """, {
- "name": brand["name"],
- "country": brand["country"],
- "language": _country_to_lang(brand["country"]),
- "ts": ts,
- "is_poc": is_poc,
- })
-
- db.run("""
- MATCH (d:Drug {source: 'drugbank', source_id: $db_id})
- MATCH (dn:DrugName {name: $name, country: $country})
- MERGE (d)-[:HAS_NAME {source: 'drugbank', created_at: datetime($ts)}]->(dn)
- """, {"db_id": db_id, "name": brand["name"],
- "country": brand["country"], "ts": ts})
-
- # ---- Generic name entry (US, EN) ----
- db.run("""
- MERGE (dn:DrugName {name: $name, country: 'US', language: 'en'})
- ON CREATE SET
- dn.name_type = 'generic',
- dn.is_primary = true,
- dn.source = 'drugbank',
- dn.source_attribute_name = 'name',
- dn.created_at = datetime($ts),
- dn.updated_at = datetime($ts),
- dn.is_poc = $is_poc
- ON MATCH SET dn.updated_at = datetime($ts)
- """, {"name": drug["name"], "ts": ts, "is_poc": is_poc})
-
- db.run("""
- MATCH (d:Drug {source: 'drugbank', source_id: $db_id})
- MATCH (dn:DrugName {name: $name, country: 'US', language: 'en'})
- MERGE (d)-[:HAS_NAME {source: 'drugbank', created_at: datetime($ts)}]->(dn)
- """, {"db_id": db_id, "name": drug["name"], "ts": ts})
-
- # ---- SOURCED_FROM ----
- db.run("""
- MATCH (d:Drug {source: 'drugbank', source_id: $db_id})
- MATCH (ds:DataSource {name: 'drugbank'})
- MERGE (d)-[:SOURCED_FROM {ingested_at: datetime($ts)}]->(ds)
- """, {"db_id": db_id, "ts": ts})
-
- print(f" [DrugBank] Loaded: {drug['name']} ({db_id})" +
- (" [POC]" if is_poc else ""))
-
- print(f"[DrugBank] Done. {len(drugs)} drugs processed.\n")
-
-
-def _country_to_lang(country: str) -> str:
- mapping = {
- "US": "en", "GB": "en", "AU": "en", "IN": "hi",
- "FR": "fr", "DE": "de", "IT": "it", "ES": "es",
- "JP": "ja", "CN": "zh",
- }
- return mapping.get(country, "en")
diff --git a/scripts/loaders/icd11_loader.py b/scripts/loaders/icd11_loader.py
deleted file mode 100644
index 86b5227..0000000
--- a/scripts/loaders/icd11_loader.py
+++ /dev/null
@@ -1,84 +0,0 @@
-"""
-Project Codex — ICD-11 Source Loader
-Reads sample_data/icd11_sample.json and loads conditions into Codex schema.
-"""
-
-import json
-from datetime import datetime, timezone
-
-# Map ICD-11 codes to DrugBank drug IDs for TREATS relationships
-ICD11_TREATS_DRUGBANK = {
- "JA00": ["DB00331"], # T2DM ← Metformin
- "BA00": [], # Hypertension (drugs loaded separately)
- "CA01": ["DB00682", "DB00945"], # AF ← Warfarin, Aspirin
- "FA24": ["DB00563"], # RA ← Methotrexate
-}
-
-
-def now_iso():
- return datetime.now(timezone.utc).isoformat()
-
-
-def load_icd11(db, data_path: str, poc_codes: set = None):
- with open(data_path) as f:
- conditions = json.load(f)
-
- if poc_codes is None:
- poc_codes = {"JA00", "BA00", "FA24"}
-
- print(f"[ICD-11] Loading {len(conditions)} conditions...")
-
- for cond in conditions:
- code = cond["code"]
- is_poc = code in poc_codes
- ts = now_iso()
-
- # ---- Upsert Condition node ----
- db.run("""
- MERGE (c:Condition {source: 'icd11', source_id: $code})
- ON CREATE SET
- c.codex_id = 'codex-cond-ICD-' + $code,
- c.canonical_name = $name,
- c.icd11_code = $code,
- c.source_attribute_name = 'code',
- c.created_at = datetime($ts),
- c.updated_at = datetime($ts),
- c.is_poc = $is_poc
- ON MATCH SET
- c.updated_at = datetime($ts)
- """, {"code": code, "name": cond["title"], "ts": ts, "is_poc": is_poc})
-
- # ---- SOURCED_FROM ----
- db.run("""
- MATCH (c:Condition {source: 'icd11', source_id: $code})
- MATCH (ds:DataSource {name: 'icd11'})
- MERGE (c)-[:SOURCED_FROM {ingested_at: datetime($ts)}]->(ds)
- """, {"code": code, "ts": ts})
-
- # ---- TREATS relationships ----
- for drug_id in ICD11_TREATS_DRUGBANK.get(code, []):
- db.run("""
- MATCH (d:Drug {source: 'drugbank', source_id: $drug_id})
- MATCH (c:Condition {source: 'icd11', source_id: $code})
- MERGE (d)-[:TREATS {
- evidence_level: 'A',
- source: 'icd11+drugbank',
- created_at: datetime($ts)
- }]->(c)
- """, {"drug_id": drug_id, "code": code, "ts": ts})
-
- print(f" [ICD-11] Loaded: {cond['title']} ({code})" +
- (" [POC]" if is_poc else ""))
-
- # ---- ICD-11 hierarchy (PARENT_OF) ----
- hierarchy = [
- ("BA00", "CA01"), # Circulatory diseases → Atrial fibrillation
- ]
- for parent_code, child_code in hierarchy:
- db.run("""
- MATCH (parent:Condition {source: 'icd11', source_id: $parent})
- MATCH (child:Condition {source: 'icd11', source_id: $child})
- MERGE (parent)-[:PARENT_OF {source: 'icd11'}]->(child)
- """, {"parent": parent_code, "child": child_code})
-
- print(f"[ICD-11] Done. {len(conditions)} conditions processed.\n")
diff --git a/scripts/loaders/rxnorm_loader.py b/scripts/loaders/rxnorm_loader.py
deleted file mode 100644
index 623e428..0000000
--- a/scripts/loaders/rxnorm_loader.py
+++ /dev/null
@@ -1,105 +0,0 @@
-"""
-Project Codex — RxNorm Source Loader
-Reads sample_data/rxnorm_sample.json and maps into normalized Codex schema.
-RxNorm is US-centric; creates EQUIVALENT_TO links to DrugBank nodes.
-"""
-
-import json
-from datetime import datetime, timezone
-
-# Map RxCUI → DrugBank ID for equivalence linking
-RXCUI_TO_DRUGBANK = {
- "161": "DB00316", # Acetaminophen
- "1191": "DB00945", # Aspirin
- "6809": "DB00331", # Metformin
- "11289": "DB00682", # Warfarin
- "7052": "DB00563", # Methotrexate
-}
-
-
-def now_iso():
- return datetime.now(timezone.utc).isoformat()
-
-
-def load_rxnorm(db, data_path: str, poc_ids: set = None):
- with open(data_path) as f:
- concepts = json.load(f)
-
- if poc_ids is None:
- poc_ids = {"161", "1191", "6809"} # Acetaminophen, Aspirin, Metformin
-
- print(f"[RxNorm] Loading {len(concepts)} concepts...")
-
- for concept in concepts:
- rxcui = concept["rxcui"]
- is_poc = rxcui in poc_ids
- ts = now_iso()
-
- # ---- Upsert Drug node ----
- db.run("""
- MERGE (d:Drug {source: 'rxnorm', source_id: $rxcui})
- ON CREATE SET
- d.codex_id = 'codex-drug-RX' + $rxcui,
- d.canonical_name = $name,
- d.drug_type = 'small_molecule',
- d.is_approved = true,
- d.source_attribute_name = 'rxcui',
- d.created_at = datetime($ts),
- d.updated_at = datetime($ts),
- d.is_poc = $is_poc
- ON MATCH SET
- d.updated_at = datetime($ts)
- """, {"rxcui": rxcui, "name": concept["name"], "ts": ts, "is_poc": is_poc})
-
- # ---- SOURCED_FROM ----
- db.run("""
- MATCH (d:Drug {source: 'rxnorm', source_id: $rxcui})
- MATCH (ds:DataSource {name: 'rxnorm'})
- MERGE (d)-[:SOURCED_FROM {ingested_at: datetime($ts)}]->(ds)
- """, {"rxcui": rxcui, "ts": ts})
-
- # ---- EQUIVALENT_TO DrugBank ----
- if rxcui in RXCUI_TO_DRUGBANK:
- db_id = RXCUI_TO_DRUGBANK[rxcui]
- db.run("""
- MATCH (db_drug:Drug {source: 'drugbank', source_id: $db_id})
- MATCH (rx_drug:Drug {source: 'rxnorm', source_id: $rxcui})
- MERGE (db_drug)-[:EQUIVALENT_TO {
- confidence: 1.0,
- source: 'codex-normalization',
- match_basis: 'name+inchikey',
- created_at: datetime($ts)
- }]->(rx_drug)
- """, {"db_id": db_id, "rxcui": rxcui, "ts": ts})
-
- # ---- Clinical dose form names (SCD/SBD) ----
- for rel in concept.get("related_concepts", []):
- if rel["tty"] in ("SCD", "SBD"):
- db.run("""
- MERGE (dn:DrugName {name: $name, country: 'US', language: 'en'})
- ON CREATE SET
- dn.name_type = $name_type,
- dn.is_primary = false,
- dn.source = 'rxnorm',
- dn.source_attribute_name = $tty,
- dn.created_at = datetime($ts),
- dn.updated_at = datetime($ts),
- dn.is_poc = false
- ON MATCH SET dn.updated_at = datetime($ts)
- """, {
- "name": rel["name"],
- "name_type": "clinical_dose_form" if rel["tty"] == "SCD" else "brand_dose_form",
- "tty": rel["tty"],
- "ts": ts,
- })
-
- db.run("""
- MATCH (d:Drug {source: 'rxnorm', source_id: $rxcui})
- MATCH (dn:DrugName {name: $name, country: 'US', language: 'en'})
- MERGE (d)-[:HAS_NAME {source: 'rxnorm', created_at: datetime($ts)}]->(dn)
- """, {"rxcui": rxcui, "name": rel["name"], "ts": ts})
-
- print(f" [RxNorm] Loaded: {concept['name']} (RxCUI {rxcui})" +
- (" [POC]" if is_poc else ""))
-
- print(f"[RxNorm] Done. {len(concepts)} concepts processed.\n")
diff --git a/scripts/loaders/snomedct_loader.py b/scripts/loaders/snomedct_loader.py
deleted file mode 100644
index fab8e5a..0000000
--- a/scripts/loaders/snomedct_loader.py
+++ /dev/null
@@ -1,170 +0,0 @@
-"""
-Project Codex — SNOMED CT Source Loader
-Reads sample_data/snomedct_sample.json and loads into Codex schema.
-Establishes EQUIVALENT_TO links between SNOMED and DrugBank/ICD-11 nodes.
-"""
-
-import json
-from datetime import datetime, timezone
-
-# SNOMED concept_id → DrugBank ID (for EQUIVALENT_TO)
-SNOMED_TO_DRUGBANK = {
- "387517004": "DB00316", # Paracetamol ↔ Acetaminophen
- "387458008": "DB00945", # Aspirin
- "387467008": "DB00331", # Metformin
- "372687004": None, # Amoxicillin — not yet in DrugBank sample
-}
-
-# SNOMED concept_id → ICD-11 code (for conditions)
-SNOMED_DISORDER_TO_ICD11 = {
- "44508008": "JA00", # T2DM
- "69896004": "FA24", # Rheumatoid arthritis
-}
-
-# Names to add as DrugName nodes (SNOMED synonyms)
-SNOMED_NAMES = {
- "387517004": [
- {"name": "Paracetamol", "country": "GB", "language": "en", "name_type": "generic"},
- {"name": "Paracetamol", "country": "IN", "language": "en", "name_type": "generic"},
- {"name": "Paracetamol", "country": "AU", "language": "en", "name_type": "generic"},
- {"name": "Paracetamol", "country": "ZA", "language": "en", "name_type": "generic"},
- ],
- "387458008": [
- {"name": "Acetylsalicylic acid", "country": "US", "language": "en", "name_type": "generic"},
- ],
- "372687004": [
- {"name": "Amoxicillin", "country": "US", "language": "en", "name_type": "generic"},
- {"name": "Amoxil", "country": "US", "language": "en", "name_type": "brand"},
- {"name": "Amoxicillin", "country": "IN", "language": "hi", "name_type": "generic"},
- {"name": "Mox", "country": "IN", "language": "hi", "name_type": "brand"},
- ],
-}
-
-
-def now_iso():
- return datetime.now(timezone.utc).isoformat()
-
-
-def load_snomedct(db, data_path: str, poc_ids: set = None):
- with open(data_path) as f:
- concepts = json.load(f)
-
- if poc_ids is None:
- poc_ids = {"387517004", "387458008", "387467008", "44508008", "69896004"}
-
- print(f"[SNOMED CT] Loading {len(concepts)} concepts...")
-
- for concept in concepts:
- cid = concept["concept_id"]
- is_poc = cid in poc_ids
- ts = now_iso()
- tag = concept["semantic_tag"]
-
- if tag == "substance":
- _load_snomed_drug(db, concept, cid, is_poc, ts)
- elif tag == "disorder":
- _load_snomed_condition(db, concept, cid, is_poc, ts)
-
- print(f" [SNOMED CT] Loaded: {concept['preferred_term']} ({cid}, {tag})" +
- (" [POC]" if is_poc else ""))
-
- print(f"[SNOMED CT] Done. {len(concepts)} concepts processed.\n")
-
-
-def _load_snomed_drug(db, concept, cid, is_poc, ts):
- db.run("""
- MERGE (d:Drug {source: 'snomedct', source_id: $cid})
- ON CREATE SET
- d.codex_id = 'codex-drug-SCT' + $cid,
- d.canonical_name = $name,
- d.drug_type = 'small_molecule',
- d.is_approved = true,
- d.source_attribute_name = 'concept_id',
- d.created_at = datetime($ts),
- d.updated_at = datetime($ts),
- d.is_poc = $is_poc
- ON MATCH SET d.updated_at = datetime($ts)
- """, {"cid": cid, "name": concept["preferred_term"], "ts": ts, "is_poc": is_poc})
-
- db.run("""
- MATCH (d:Drug {source: 'snomedct', source_id: $cid})
- MATCH (ds:DataSource {name: 'snomedct'})
- MERGE (d)-[:SOURCED_FROM {ingested_at: datetime($ts)}]->(ds)
- """, {"cid": cid, "ts": ts})
-
- # Add SNOMED-specific name variants
- for nm in SNOMED_NAMES.get(cid, []):
- db.run("""
- MERGE (dn:DrugName {name: $name, country: $country, language: $language})
- ON CREATE SET
- dn.name_type = $name_type,
- dn.is_primary = true,
- dn.source = 'snomedct',
- dn.source_attribute_name = 'descriptions.Synonym',
- dn.created_at = datetime($ts),
- dn.updated_at = datetime($ts),
- dn.is_poc = $is_poc
- ON MATCH SET dn.updated_at = datetime($ts)
- """, {**nm, "ts": ts, "is_poc": is_poc})
-
- db.run("""
- MATCH (d:Drug {source: 'snomedct', source_id: $cid})
- MATCH (dn:DrugName {name: $name, country: $country, language: $language})
- MERGE (d)-[:HAS_NAME {source: 'snomedct', created_at: datetime($ts)}]->(dn)
- """, {"cid": cid, "name": nm["name"], "country": nm["country"],
- "language": nm["language"], "ts": ts})
-
- # EQUIVALENT_TO DrugBank
- db_id = SNOMED_TO_DRUGBANK.get(cid)
- if db_id:
- db.run("""
- MATCH (db_drug:Drug {source: 'drugbank', source_id: $db_id})
- MATCH (sct_drug:Drug {source: 'snomedct', source_id: $cid})
- MERGE (db_drug)-[:EQUIVALENT_TO {
- confidence: 1.0,
- source: 'codex-normalization',
- match_basis: 'inchikey',
- created_at: datetime($ts)
- }]->(sct_drug)
- """, {"db_id": db_id, "cid": cid, "ts": ts})
-
-
-def _load_snomed_condition(db, concept, cid, is_poc, ts):
- db.run("""
- MERGE (c:Condition {source: 'snomedct', source_id: $cid})
- ON CREATE SET
- c.codex_id = 'codex-cond-SCT' + $cid,
- c.canonical_name = $name,
- c.snomed_id = $cid,
- c.source_attribute_name = 'concept_id',
- c.created_at = datetime($ts),
- c.updated_at = datetime($ts),
- c.is_poc = $is_poc
- ON MATCH SET c.updated_at = datetime($ts)
- """, {"cid": cid, "name": concept["preferred_term"], "ts": ts, "is_poc": is_poc})
-
- db.run("""
- MATCH (c:Condition {source: 'snomedct', source_id: $cid})
- MATCH (ds:DataSource {name: 'snomedct'})
- MERGE (c)-[:SOURCED_FROM {ingested_at: datetime($ts)}]->(ds)
- """, {"cid": cid, "ts": ts})
-
- # EQUIVALENT_TO ICD-11 condition
- icd_code = SNOMED_DISORDER_TO_ICD11.get(cid)
- if icd_code:
- db.run("""
- MATCH (icd:Condition {source: 'icd11', source_id: $icd_code})
- MATCH (sct:Condition {source: 'snomedct', source_id: $cid})
- MERGE (icd)-[:EQUIVALENT_TO {
- confidence: 1.0,
- source: 'codex-normalization',
- match_basis: 'clinical-mapping',
- created_at: datetime($ts)
- }]->(sct)
- """, {"icd_code": icd_code, "cid": cid, "ts": ts})
-
- # Enrich ICD-11 condition with SNOMED ID
- db.run("""
- MATCH (c:Condition {source: 'icd11', source_id: $icd_code})
- SET c.snomed_id = $cid
- """, {"icd_code": icd_code, "cid": cid})
diff --git a/scripts/run_etl.py b/scripts/run_etl.py
deleted file mode 100644
index f05718e..0000000
--- a/scripts/run_etl.py
+++ /dev/null
@@ -1,90 +0,0 @@
-"""
-Project Codex — Full ETL Pipeline Runner
-Runs all four source loaders in the correct order:
- 0. Setup constraints + indexes
- 1. DrugBank
- 2. RxNorm
- 3. ICD-11
- 4. SNOMED CT
-
-Usage:
- python scripts/run_etl.py
-
-Requirements:
- pip install neo4j
- Neo4j running at bolt://localhost:7687 (see scripts/config.py)
-"""
-
-import os
-import sys
-
-# Allow running from repo root
-sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
-
-from db import CodexDB
-from config import SAMPLE_DATA_DIR, CYPHER_DIR
-from loaders.drugbank_loader import load_drugbank
-from loaders.rxnorm_loader import load_rxnorm
-from loaders.icd11_loader import load_icd11
-from loaders.snomedct_loader import load_snomedct
-
-
-SETUP_FILE = os.path.join(CYPHER_DIR, "00_setup_constraints.cypher")
-
-
-def run():
- print("=" * 60)
- print(" Project Codex — Neo4j ETL Pipeline")
- print("=" * 60)
-
- with CodexDB() as db:
-
- # Step 0: Schema setup
- print("\n[Step 0] Setting up constraints and indexes...")
- n = db.run_file(SETUP_FILE)
- print(f" Executed {n} statements from setup file.\n")
-
- # Step 1: DrugBank
- print("[Step 1] Loading DrugBank...")
- load_drugbank(db, os.path.join(SAMPLE_DATA_DIR, "drugbank_sample.json"))
-
- # Step 2: RxNorm
- print("[Step 2] Loading RxNorm...")
- load_rxnorm(db, os.path.join(SAMPLE_DATA_DIR, "rxnorm_sample.json"))
-
- # Step 3: ICD-11
- print("[Step 3] Loading ICD-11...")
- load_icd11(db, os.path.join(SAMPLE_DATA_DIR, "icd11_sample.json"))
-
- # Step 4: SNOMED CT
- print("[Step 4] Loading SNOMED CT...")
- load_snomedct(db, os.path.join(SAMPLE_DATA_DIR, "snomedct_sample.json"))
-
- # Summary
- print("=" * 60)
- print(" ETL Complete — Summary")
- print("=" * 60)
- summary = db.run("""
- MATCH (d:Drug) WITH count(d) AS drugs
- MATCH (dn:DrugName) WITH drugs, count(dn) AS names
- MATCH (c:Condition) WITH drugs, names, count(c) AS conds
- MATCH (i:Ingredient) WITH drugs, names, conds, count(i) AS ings
- RETURN drugs, names, conds, ings
- """)
- if summary:
- r = summary[0]
- print(f" Drug nodes: {r.get('drugs', '?')}")
- print(f" DrugName nodes: {r.get('names', '?')}")
- print(f" Condition nodes: {r.get('conds', '?')}")
- print(f" Ingredient nodes: {r.get('ings', '?')}")
-
- poc_count = db.run("MATCH (d:Drug {is_poc: true}) RETURN count(d) AS n")
- if poc_count:
- print(f" POC drugs: {poc_count[0]['n']}")
-
- print("\n Run cypher/05_demo_queries.cypher in Neo4j Browser to explore!")
- print("=" * 60)
-
-
-if __name__ == "__main__":
- run()