From adfdb72f7b0972c2affd7574f1aceddcfbb9a015 Mon Sep 17 00:00:00 2001 From: Sofia Vargas <140123401+sofievargas@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:55:19 -0700 Subject: [PATCH 01/23] Update api_documentation.md --- api_documentation.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api_documentation.md b/api_documentation.md index b5e11ed..fa839f5 100644 --- a/api_documentation.md +++ b/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. From 6bcec0c170838577d23fb2e7c006fc30fb55342f Mon Sep 17 00:00:00 2001 From: Logan Watersmith Date: Wed, 22 Jul 2026 13:37:38 -0700 Subject: [PATCH 02/23] Added source_data so graph is created in a singular file --- scripts/loaders/drugbank_loader.py | 159 ---------- scripts/loaders/icd11_loader.py | 84 ------ scripts/loaders/rxnorm_loader.py | 105 ------- scripts/loaders/snomedct_loader.py | 170 ----------- scripts/loaders/source_data.py | 454 +++++++++++++++++++++++++++++ 5 files changed, 454 insertions(+), 518 deletions(-) delete mode 100644 scripts/loaders/drugbank_loader.py delete mode 100644 scripts/loaders/icd11_loader.py delete mode 100644 scripts/loaders/rxnorm_loader.py delete mode 100644 scripts/loaders/snomedct_loader.py create mode 100644 scripts/loaders/source_data.py 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/loaders/source_data.py b/scripts/loaders/source_data.py new file mode 100644 index 0000000..771813a --- /dev/null +++ b/scripts/loaders/source_data.py @@ -0,0 +1,454 @@ +#!/usr/bin/env python3 + +""" +Neo4j Singular Data Source +""" + +import os +import sys +import time +import uuid +import json +import logging +from datetime import datetime, timezone +from collections import deque + +import requests +from neo4j import GraphDatabase, WRITE_ACCESS +from dotenv import load_dotenv, find_dotenv + +env_path = find_dotenv(usecwd=True) +if not env_path: + print("ERROR: .env not found at repo root") + sys.exit(1) +load_dotenv(env_path) + +NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687") +NEO4J_USER = os.getenv("NEO4J_USER", "neo4j") +NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "password") +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 + + +# -------------------- +# DrugBank +# -------------------- + +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"); sys.exit(1) + + 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 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) + + for code, title in iter_drugs(): + sess.run("MERGE (n:DRUG {id: $id}) ON CREATE SET n.code=$id, n.title=$t, n.ds=$ds ON MATCH SET n.ds=$ds", id=code, 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) + processed += 1 + + 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 +# -------------------- + +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")}, 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 + + # BFS Tree Traversal + root_full = api_get(f"browser/{SNOMED_BRANCH}/concepts/{SNOMED_ROOT_ID}") + 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) + + queue = deque([(SNOMED_ROOT_ID, None)]) + visited = set() + while queue: + 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) + + 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 +# -------------------- + +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: + sess.run("MERGE (n:RXN {rxcui: $rxcui}) ON CREATE SET n.name=$name, n.tty=$tty, n.ds=$ds", rxcui=m["rxcui"], 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: log.info("RxNorm build processing roots: %d / %d", idx, len(roots)) + 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"): + sess.run("MERGE (n:RXN {rxcui: $rxcui}) ON CREATE SET n.name=$name, n.tty=$tty, n.ds=$ds ON MATCH SET n.ds=$ds", rxcui=p["rxcui"], 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) + + 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 +# -------------------- + +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("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() + while queue: + 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)) + + 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: + # 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) REQUIRE n.code") + + log.info("Applying global :Concept labels...") + sess.run("MATCH (n:DRUG) SET n:Concept") + sess.run("MATCH (n:SNOMED) SET n:Concept") + sess.run("MATCH (n:RXN) SET n:Concept") + sess.run("MATCH (n:ICD) SET n:Concept") + + log.info("Creating connections between RxNorm and DrugBank...") + sess.run(""" + MATCH (r:RXN), (d:DRUG) + 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), (i:ICD) + WHERE s.code = i.code OR s.title = i.title + MERGE (s)-[:MAPS_TO {derivedBy: 'exact_string_match'}]->(i) + """) + + log.info("<<< Graph combination complete. All sources are now connected.") + +def main(): + 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 + drugbank(driver) + snomed(driver) + rxnorm(driver) + icd11(driver) + + # Link them together to make a singular source + unify_graph(driver) + + except Exception as e: + log.critical("Pipeline was terminated prematurely: %s", e) + sys.exit(2) + finally: + driver.close() + + log.info("=" * 60) + log.info("All pipelines executed | Total duration: %.2f seconds", time.time() - t0) + log.info("=" * 60) + +if __name__ == "__main__": + main() \ No newline at end of file From afa8ff22d932027a9827332eb8adc53af7c9d92f Mon Sep 17 00:00:00 2001 From: Logan Watersmith Date: Tue, 28 Jul 2026 20:26:57 -0700 Subject: [PATCH 03/23] Source loader has been tested to ensure it works In the process of integrating it with the UI --- .env | 16 +- {scripts => backend/neo4j_sources}/.DS_Store | Bin .../__pycache__/config.cpython-313.pyc | Bin .../__pycache__/config.cpython-314.pyc | Bin 0 -> 1044 bytes .../__pycache__/db.cpython-313.pyc | Bin .../__pycache__/db.cpython-314.pyc | Bin 0 -> 3368 bytes {scripts => backend/neo4j_sources}/config.py | 2 +- .../neo4j_sources}/source_data.py | 86 ++++++++-- .../codex => backend/translation}/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 183 bytes .../__pycache__/neo4j_driver.cpython-314.pyc | Bin 0 -> 7274 bytes .../translation}/config/fallbacks.json | 0 .../language_packs/english_pack.json | 0 .../language_packs/french_pack.json | 0 .../language_packs/russian_pack.json | 0 .../language_packs/spanish_pack.json | 0 .../language_packs/ukrainian_pack.json | 0 .../translation}/neo4j_driver.py | 0 .../translation}/services/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 192 bytes .../translation_service.cpython-314.pyc | Bin 0 -> 7836 bytes .../services/translation_service.py | 2 +- .../translation}/utils/__init__.py | 0 codex-build/__pycache__/api.cpython-314.pyc | Bin 0 -> 23950 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 173 bytes .../__pycache__/neo4j_driver.cpython-314.pyc | Bin 0 -> 18165 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 182 bytes .../translation_service.cpython-314.pyc | Bin 0 -> 1489 bytes .../__pycache__/api.cpython-314.pyc | Bin 0 -> 15698 bytes cypher/00_setup_constraints.cypher | 77 --------- cypher/01_load_drugbank.cypher | 162 ------------------ cypher/02_load_rxnorm.cypher | 112 ------------ cypher/03_load_icd11.cypher | 108 ------------ cypher/04_load_snomedct.cypher | 160 ----------------- cypher/05_demo_queries.cypher | 116 ------------- docker-compose.yml | 51 ++++++ sample_data/drugbank_sample.json | 104 ----------- sample_data/icd11_sample.json | 67 -------- sample_data/rxnorm_sample.json | 80 --------- sample_data/snomedct_sample.json | 104 ----------- scripts/db.py | 42 ----- scripts/dry_run.py | 122 ------------- scripts/loaders/.DS_Store | Bin 6148 -> 0 bytes scripts/loaders/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 166 -> 0 bytes .../drugbank_loader.cpython-313.pyc | Bin 7107 -> 0 bytes .../__pycache__/icd11_loader.cpython-313.pyc | Bin 3636 -> 0 bytes .../__pycache__/rxnorm_loader.cpython-313.pyc | Bin 4748 -> 0 bytes .../snomedct_loader.cpython-313.pyc | Bin 7114 -> 0 bytes scripts/run_etl.py | 90 ---------- 50 files changed, 133 insertions(+), 1368 deletions(-) rename {scripts => backend/neo4j_sources}/.DS_Store (100%) rename {scripts => backend/neo4j_sources}/__pycache__/config.cpython-313.pyc (100%) create mode 100644 backend/neo4j_sources/__pycache__/config.cpython-314.pyc rename {scripts => backend/neo4j_sources}/__pycache__/db.cpython-313.pyc (100%) create mode 100644 backend/neo4j_sources/__pycache__/db.cpython-314.pyc rename {scripts => backend/neo4j_sources}/config.py (87%) rename {scripts/loaders => backend/neo4j_sources}/source_data.py (85%) rename {codex-main-test-build/codex => backend/translation}/__init__.py (100%) create mode 100644 backend/translation/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/translation/__pycache__/neo4j_driver.cpython-314.pyc rename {codex-main-test-build/codex => backend/translation}/config/fallbacks.json (100%) rename {codex-main-test-build/codex => backend/translation}/language_packs/english_pack.json (100%) rename {codex-main-test-build/codex => backend/translation}/language_packs/french_pack.json (100%) rename {codex-main-test-build/codex => backend/translation}/language_packs/russian_pack.json (100%) rename {codex-main-test-build/codex => backend/translation}/language_packs/spanish_pack.json (100%) rename {codex-main-test-build/codex => backend/translation}/language_packs/ukrainian_pack.json (100%) rename {codex-main-test-build/codex => backend/translation}/neo4j_driver.py (100%) rename {codex-main-test-build/codex => backend/translation}/services/__init__.py (100%) create mode 100644 backend/translation/services/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/translation/services/__pycache__/translation_service.cpython-314.pyc rename {codex-main-test-build/codex => backend/translation}/services/translation_service.py (97%) rename {codex-main-test-build/codex => backend/translation}/utils/__init__.py (100%) create mode 100644 codex-build/__pycache__/api.cpython-314.pyc create mode 100644 codex-build/codex/__pycache__/__init__.cpython-314.pyc create mode 100644 codex-build/codex/__pycache__/neo4j_driver.cpython-314.pyc create mode 100644 codex-build/codex/services/__pycache__/__init__.cpython-314.pyc create mode 100644 codex-build/codex/services/__pycache__/translation_service.cpython-314.pyc create mode 100644 codex-main-test-build/__pycache__/api.cpython-314.pyc delete mode 100644 cypher/00_setup_constraints.cypher delete mode 100644 cypher/01_load_drugbank.cypher delete mode 100644 cypher/02_load_rxnorm.cypher delete mode 100644 cypher/03_load_icd11.cypher delete mode 100644 cypher/04_load_snomedct.cypher delete mode 100644 cypher/05_demo_queries.cypher create mode 100644 docker-compose.yml delete mode 100644 sample_data/drugbank_sample.json delete mode 100644 sample_data/icd11_sample.json delete mode 100644 sample_data/rxnorm_sample.json delete mode 100644 sample_data/snomedct_sample.json delete mode 100644 scripts/db.py delete mode 100644 scripts/dry_run.py delete mode 100644 scripts/loaders/.DS_Store delete mode 100644 scripts/loaders/__init__.py delete mode 100644 scripts/loaders/__pycache__/__init__.cpython-313.pyc delete mode 100644 scripts/loaders/__pycache__/drugbank_loader.cpython-313.pyc delete mode 100644 scripts/loaders/__pycache__/icd11_loader.cpython-313.pyc delete mode 100644 scripts/loaders/__pycache__/rxnorm_loader.cpython-313.pyc delete mode 100644 scripts/loaders/__pycache__/snomedct_loader.cpython-313.pyc delete mode 100644 scripts/run_etl.py diff --git a/.env b/.env index 4d77ff8..68e01a6 100644 --- a/.env +++ b/.env @@ -1,6 +1,16 @@ # 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 -NEO4J_URI=bolt://127.0.0.1:7687 +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/scripts/.DS_Store b/backend/neo4j_sources/.DS_Store similarity index 100% rename from scripts/.DS_Store rename to backend/neo4j_sources/.DS_Store diff --git a/scripts/__pycache__/config.cpython-313.pyc b/backend/neo4j_sources/__pycache__/config.cpython-313.pyc similarity index 100% rename from scripts/__pycache__/config.cpython-313.pyc rename to backend/neo4j_sources/__pycache__/config.cpython-313.pyc diff --git a/backend/neo4j_sources/__pycache__/config.cpython-314.pyc b/backend/neo4j_sources/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94402ee883c506f996eaf296982f1aa4514f81cc GIT binary patch literal 1044 zcmbtSO-~a+7@lp*cG;zcf~4g#Jw z#tSEcF#%2_p2`Wm`7=oNLMAcs;}3}a1J3qSO*k0mFz@p|^SsYH^JXTZ5fQNUF>4in zasYhylhz71LG1%OaRCGbya15Pa)e_skPR#ZN$@6E4UU3DQ_3VkY*#b2rlL!qq-x9-H$eVvXilpAQFPW*4>V8^S4R;2`+lE40rr00@;!wvoK z`)UvDZoe%0PjzCut`~B*z__#-XU<{{qoS#kk$Gg8+aurHcO#1?N)%ac+l;r6?Zb6z zV-stBCN7S1R4O6OPz^*(`z7sO%q%4ytIII^YH$se$h0h%kgk;q$RX3py(xu?O+`Kf zX_xKW(`bXrrSkceC77lW-S1z~9A^t*gG!E8u9QsG(1=EPeWy||@pvB%tE{Q6sx*Kc z$~XFB|I<)@!}KqMgDk-zf8%{RRMAL*h7AkbTG^ywZOy4Of~u}t%wMI#Tq>VY(^*JE zMP%8uH=lZ%d%~F1vs9fH=bz^mGSJ5-pvFuAot2jz6Fcmg8=AeP5r)eaDaenoxig2h z<>pM)`b=d<$5w?nvR)tKoyrasX3EIeESa--h}rqy!g5AR1*Lveg-$b z0_jcUOKjj^=7gr}_g^^}+aEia+@Cz$u3q>06}R_K P?wZ#he=Xvx?3%v-P>JHC literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/db.cpython-313.pyc b/backend/neo4j_sources/__pycache__/db.cpython-313.pyc similarity index 100% rename from scripts/__pycache__/db.cpython-313.pyc rename to backend/neo4j_sources/__pycache__/db.cpython-313.pyc diff --git a/backend/neo4j_sources/__pycache__/db.cpython-314.pyc b/backend/neo4j_sources/__pycache__/db.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64ea89b9e9b53b0a4d693543a8acaf880354e76b GIT binary patch literal 3368 zcmb6cTTC3+_1@W?+1X{6N7(g(17Cj|!<$ldq^!rPN8K{!}W|sL*MpR84>ShgnsmRX=*pof(^; zh$?r@x#ynynDaizJ@ttgf$_nZd1E(B$Y;0+2CVt0y$I6^QHevQiAu-lsNe`4hznz( z(XbO96&-O@a-0S8Myi~Ibu7oqH;jU#TrAHTx0JuH+*c-y za^DSQmYHuFOj%;OWf?4%p>ARX@P#4W(Pwnq$b{VJ#PBP9FKbuTF*oL;SB6!$-otZ) zSFT)pMI9=5U-e+fU)0_JLam^5+1ssK<_>+<9*Rl{jglvNR6 zW05)%r%9X^K^ll7!)aNK0*tCLCzdBpof-lW<8FkDHS|(}`X9zggg;Crf zVTL!{i00AP;Jj@V=WOJ++r5yzY8%YX7R&Q`X-RhgUo@SC>a~pjn^ad1kF#CA`E99@~6IT6891T)BH>Vk30upg>1|5JMr7`E&1q%eDqIp8*2jc1m`p`GU^risC5GvperO# zriF9gBV-n+ZR9dtpensWCm~z_(yRcc3jNcwPhr_wFqj*&bfzyFsPT;8hHQtq;aRib zz(SfUvqjx;6Pi{i>b8w!O|$X(@f9wC8`m_wR4O}QG0=Z95M@8L5sq^ke7-{VNJ#8_ zg5alJJ2-NE^VjURwwUGK0LCOIlrDc|2})1 zx=_8e-Q2p`f3N?Y7xqL_e*(_JzsHHL|JdluKkFH66(6=;rz0HkmS1ay48LdPzmN zbc&FhLx;K4Q^(=XI1Y+A+yy6u@zaDv$uVMyR(P6NQeJE%mmm#Tk$fZ<4V3Y04xAUb zl6|f&0$0_q$d!E9`5b!7fdRGI*GC|um#D@EK}H$m0AJ;DL@hZV4bFH$Ier7qkQ|1? zfsu~{uXd2UG)+(MJ7Y=X-}m0kOaCt{zxA0HlobfoieQCJfE<%ek`dqP8@^=}DvqJ( zO0SnZ6?EUziv3;Fvdq%FQYbGj8YSoJIDU?eNe8;6WKkFFyRoj&f zkHJ|et|b<2<+G!)=j*$gN=$o>F#R>(gRO?OW|xVV*2HWSLj z!R;gI)wz3f?<`bD_Cln-b4Mnp&it}{t^Iv_Gt>V#HMWuIe>nKj(yx|2TE1`I)mP{5 z&Huy%Vl}p&8ryD8R^`7Wk5z~MrkwbB!+Q;z%9+Q>nGNO4gEJpq`{3G#*Y98c&CTB} zJzDx?d23{HV`OrD>W!@tef9F)%W$YVyq=u-w7KQajV;^p#yg|8NAJG+qlxW!@&Wzv zw|_YLBtcRgJN2ZorP{FfJTM1L|94j=M_#7?hACB`b_Qg6w`quqk7#-*8Gh8B1o&}0 zJ=7|FeDXNJPZ}hEzom6UO>4@O=AuC0A_OT(r)", uid=run_uid, rid=SNOMED_ROOT_ID) + 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() + while queue: cid, parent = queue.popleft() if cid in visited: continue @@ -358,7 +382,8 @@ def get_node_details(item): 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=$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)]) @@ -392,29 +417,44 @@ 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) REQUIRE n.code") + 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...") - sess.run("MATCH (n:DRUG) SET n:Concept") - sess.run("MATCH (n:SNOMED) SET n:Concept") - sess.run("MATCH (n:RXN) SET n:Concept") - sess.run("MATCH (n:ICD) SET n:Concept") + 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), (d:DRUG) + 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), (i:ICD) + 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.") @@ -431,11 +471,19 @@ def main(): sc = smoke_test(check_session) log.info("Graph connection smoke test passed.") - # Ingest separate sources into the single database - drugbank(driver) - snomed(driver) - rxnorm(driver) - icd11(driver) + # Ingest separate sources into the single database based on user choice + if "drugbank" in sys.argv[1:]: + log.info("Ingesting DrugBank.") + drugbank(driver) + if "snomed" in sys.argv[1:]: + log.info("Ingesting SNOMED.") + snomed(driver) + if "rxnorm" in sys.argv[1:]: + log.info("Ingesting RXNorm.") + rxnorm(driver) + if "icd11" in sys.argv[1:]: + log.info("Ingesting ICD11.") + icd11(driver) # Link them together to make a singular source unify_graph(driver) diff --git a/codex-main-test-build/codex/__init__.py b/backend/translation/__init__.py similarity index 100% rename from codex-main-test-build/codex/__init__.py rename to backend/translation/__init__.py diff --git a/backend/translation/__pycache__/__init__.cpython-314.pyc b/backend/translation/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c4a54b2dc31b91997abba9dabe3cc03a2c22f6da GIT binary patch literal 183 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 literal 0 HcmV?d00001 diff --git a/codex-main-test-build/codex/config/fallbacks.json b/backend/translation/config/fallbacks.json similarity index 100% rename from codex-main-test-build/codex/config/fallbacks.json rename to backend/translation/config/fallbacks.json diff --git a/codex-main-test-build/codex/language_packs/english_pack.json b/backend/translation/language_packs/english_pack.json similarity index 100% rename from codex-main-test-build/codex/language_packs/english_pack.json rename to backend/translation/language_packs/english_pack.json diff --git a/codex-main-test-build/codex/language_packs/french_pack.json b/backend/translation/language_packs/french_pack.json similarity index 100% rename from codex-main-test-build/codex/language_packs/french_pack.json rename to backend/translation/language_packs/french_pack.json diff --git a/codex-main-test-build/codex/language_packs/russian_pack.json b/backend/translation/language_packs/russian_pack.json similarity index 100% rename from codex-main-test-build/codex/language_packs/russian_pack.json rename to backend/translation/language_packs/russian_pack.json diff --git a/codex-main-test-build/codex/language_packs/spanish_pack.json b/backend/translation/language_packs/spanish_pack.json similarity index 100% rename from codex-main-test-build/codex/language_packs/spanish_pack.json rename to backend/translation/language_packs/spanish_pack.json diff --git a/codex-main-test-build/codex/language_packs/ukrainian_pack.json b/backend/translation/language_packs/ukrainian_pack.json similarity index 100% rename from codex-main-test-build/codex/language_packs/ukrainian_pack.json rename to backend/translation/language_packs/ukrainian_pack.json diff --git a/codex-main-test-build/codex/neo4j_driver.py b/backend/translation/neo4j_driver.py similarity index 100% rename from codex-main-test-build/codex/neo4j_driver.py rename to backend/translation/neo4j_driver.py diff --git a/codex-main-test-build/codex/services/__init__.py b/backend/translation/services/__init__.py similarity index 100% rename from codex-main-test-build/codex/services/__init__.py rename to backend/translation/services/__init__.py diff --git a/backend/translation/services/__pycache__/__init__.cpython-314.pyc b/backend/translation/services/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a988c488a1326521617f107a6bccbfb10d87c98f GIT binary patch literal 192 zcmdPq_I|p@<2{{|u766{{awoLW?@ zpOc@Sm{*=y0%YfAmSpI=7o}D@ literal 0 HcmV?d00001 diff --git a/backend/translation/services/__pycache__/translation_service.cpython-314.pyc b/backend/translation/services/__pycache__/translation_service.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9928913e0044a23953d8459f1ca86194fcd717b7 GIT binary patch literal 7836 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 literal 0 HcmV?d00001 diff --git a/codex-main-test-build/codex/services/translation_service.py b/backend/translation/services/translation_service.py similarity index 97% rename from codex-main-test-build/codex/services/translation_service.py rename to backend/translation/services/translation_service.py index 81b9217..d880512 100644 --- a/codex-main-test-build/codex/services/translation_service.py +++ b/backend/translation/services/translation_service.py @@ -1,5 +1,5 @@ # Import dependencies -from codex.neo4j_driver import create_translation, get_translation_data, find_missing_translations, find_missing_brands, get_equivalent_brands, language_exists, resolve_to_base_term, driver +from backend.translation.neo4j_driver import create_translation, get_translation_data, find_missing_translations, find_missing_brands, get_equivalent_brands, language_exists, resolve_to_base_term, driver import json import os diff --git a/codex-main-test-build/codex/utils/__init__.py b/backend/translation/utils/__init__.py similarity index 100% rename from codex-main-test-build/codex/utils/__init__.py rename to backend/translation/utils/__init__.py diff --git a/codex-build/__pycache__/api.cpython-314.pyc b/codex-build/__pycache__/api.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..46dfe0108a5c7876803780a296972b252e9e9b3b GIT binary patch literal 23950 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 y-QGDX9AQO#x^?_<%1N7}_`DC{Qm+w*#1=j literal 0 HcmV?d00001 diff --git a/codex-build/codex/__pycache__/__init__.cpython-314.pyc b/codex-build/codex/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2c427dc8375c14e82e8fc1aaa334753d8e0e853e GIT binary patch literal 173 zcmdPq2KczG$)vkyYXf(+3Vi4mKGb1Bo5i^hl0H2jAasU7T literal 0 HcmV?d00001 diff --git a/codex-build/codex/__pycache__/neo4j_driver.cpython-314.pyc b/codex-build/codex/__pycache__/neo4j_driver.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..42b3d09324edce5c1febf5825a36a3d5ecd7d3ef GIT binary patch literal 18165 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 literal 0 HcmV?d00001 diff --git a/codex-build/codex/services/__pycache__/translation_service.cpython-314.pyc b/codex-build/codex/services/__pycache__/translation_service.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..076d53a3982193b00eba50494bd115f702897976 GIT binary patch literal 1489 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

d&=(^2NbSALyb}XrR8o(v7q%PglP0C0= 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 Hj`;rtNhyWj literal 0 HcmV?d00001 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..46de4ca --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +services: + + frontend: + build: + context: ./frontend + containter_name: codex-frontend + ports: + - "9000:9000" + depends_on: + - api + volumes: + - ./frontend:/app + - /app/node_modules + + api: + build: + context: ./backend/translation + container_name: codex-api + ports: + - "8000:8000" + environment: + - NEO4J_URI=bolt://neo4j:7687 + - NEO4J_USER=neo4j + - NEO4J_PASSWORD=changeme + depends_on: + neo4j: + condition: service_healthy + volumes: + - ./backend/translation:/app + + neo4j: + image: neo4j:5.18 + 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/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/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 64adcd68189aac5ea8532a9c21fb8ac4c55a77a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKOHKnZ47H()O5JqHGFRvghH5xLH{A_%is+)%%!UQKY`F;HCOr@r;Jw&CvSVH=Y2s#AxM8TG`39G$Voj;uLeA zA=^fJxkK)6`8WHU#j5QNy`{Z=`rPu+)%9lIZD2v79`7HH=U-WGzhT|)-e-{!HY6Ab z27-ZLAQ<=&1Guv()0tuP!9Xw&47@VH^C6)TE5~eDjt+FC1OW0GT?9I73Hl_*$}t;a z1)}B(G*|T!L(LuY$?Yn~Y-sMHUVNzU{8GGV*&XpyF&C{2qYnmxfguB@Hk|YRzrZgu zTI3I1ViXJn1OJQxoz$y(iI4KT^~2}!u1#o{XcXFIDiG*{M*s#qN6yNkttaZRs~oc- Uqj0{s1LGl32#GEj_yq=@0e`JB3jhEB 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 5903f7c42df0ab5b021cdf28976694ccba0b8fea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 166 zcmey&%ge<81dsR3$pq6N0tOhNjL$4U##DxM22Do4l?+8pK;CDN)Gb&2(BjmhV*TWd z#7cda)Z*-t`~rRF#De0I{Jd0!;*!#|G=-GZ+@J%z~0){ha*76p)tq_{_Y_ mlK6PNg34PQHo5sJr8%i~MXW$mLADly7$2D#85xV1fh+*x9x6is 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 6b3fb008e5b9f4bf66888815a1cbaf8b8715a0e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7107 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 f&Npu$(yypr8U4m3#zh@mN^BtL@`h&)(j z6Qh;#R;OAuZMW&B+DN5u**6~ZkjFmkqtUkAXcV<-w=aD&CCW;D>bWx>+YlmcwIg}% zxp&SzKi~QB*$;;W1kabxpA?cIgubC0_wm$;{R|L~kcbEp8Ihf52m>oO&t78*dyOL; zMk1eJ5=iuj-UKW95{&4N^Mn@zClT?8VNtl@6@xc?7kDvr65Zx+GsM5f#v&yX1Pj-R zep{7IoYEEbF8=5J-{PyO^vKv47WER5Rs4Z2DJl^%s-zg$kT#1OYF?2{DN&G>v9Y{! z62EQe8kRH#ZveZ2WnEJWWq{Wp? z*$Na4eN`u$lKBPNr~KB{MAlG=k&xG=dx^Aa+%ffHA|(|K6JWrGSz2Aiin^)e#e^Y~ zLeVr56kK}6f!^80i}x%a=*oiv(g}i(!_PPkulwj_u>IlXUtE6T|3m2ap|65xzYZpL zgNd(#V^w~Pv{NGjG?e+GWUgC*qAurU$O1#A4!95X*G5qR)Bwdg4hUr#7ZVm~T&{s> zrh%g?0-=7Cx!7a^HNG*Q2SQI?hA&KRWk9Y$kwKKGu;<{D`Kn0N0-kjRZUq`_^z z9lieuA<~$OxQhbuA*l}6}|NK%Fjh#oNux2Xnz07 zx)_MFO}<>jm6nORRCCRG6Ah2S-fpli@D@vimEUFkVld7%WiOmWD2Om>!6pRW+V%n- z;j9~MsMk?DHsm|6&t)Vk$7`YZIkV&g7Mt$HHMsv&}KgIQ-RVH>~ahfI^p{fHbYXw=_$ZOK3T9-712k7o7ey0sk4J}%MpPn4WUw2V5O;Wg3GF3-S z-N?bCZpcK14z1)R^MD4oZ4V8YM(n72r6Ohd|L+xyJanNJU6cfmobv;aK|mIg%c=L@ z5|8g+0>$xVt&*-UCCouADtE#NJv1=Voip2P=Y^YP7*=^@2vrgDt6T_o>-pVCjD z0uMOW;+!&>CZ)BOI#FPIOVaKfDiwMRX@AJp7D3;u>TEeq^(xfwWmIT4=JR+wlx7D)fY%Ktc^t)4waiKv?So!gqjekfFKGOrgllom-X zkw3CSQuf1=oIp8RXtb^;Gc$|J`K8%6ggHo28BMfc9`rhQEssn;m5((rO2bGL$K|e; zevgYpmsP`1mADWKSiD|THH)WnsKwLC$nq2k%$^pvp=$PAWO1aVS)I^@VQh60DWA?{ z=dL6d=PhncHDh6mRc?_J05|EPeSlfDgytkm|6Mf23F0ZrLu-i8LCXrP7gQn3pDY#AjaE2;6bI^onJfwqO6{xLw8)9-E4yblx1qu_Uw?Md3_@wV~@?~ewXCMCN z!^c1U^hQPamP0~M&5QieU0=7=F0Nt`RQ-kPFDCA z(avA3{`wu8*p2pAyf35OpY~K(5D*AH^gi&`P^c|b>qcF@PrLrO@a*_*c=1(FC~%#r zoo;aOz~Kju-SF900X{JJDuTL4zU~^`?Hb+b*dc%Inyk#d2t^*wKA5c`e@9!b4V^yw ztbaE;^*ZVgzqiMs?n|#upg^SB_Ji#-)wBKHPXEsG&g`=@)yU<)^`3cfcbn{S%&~K? z5fd2RTVhbOw+1K#dTJaS_1?d_??nS6yZ*jP@P%J+>~^f`8Q2ZK`x>me1ZoCGz%-9g zS$dM%7Cv99_6}Ev(^X;SFZ}GcTMXd#%f9~_w46TaN@MO%SV$*)gpO^HK)!rlfdM#A z24HWG@b>ghVsuOAYKt>W0x#9H14ZbZZ4c2-ApMs&b)~eSPLqr90Ro7O58zef7>3zn zIfmaC5EJ@48U!dKU6pwN>_zy*V+dV8td;q_fTxG8qR#)cG0gcr|1>klJeF&Su21FH I_G)YUA7CLpvH$=8 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 6cd3459d89748979ffc23f4612dcb0a34cf75b2b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4748 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 It%+&sKQuy6EC2ui 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 176003bc7e827cf5970948a81da444bfd254d98a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7114 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 Date: Tue, 28 Jul 2026 20:30:06 -0700 Subject: [PATCH 04/23] Reorganization/clean up --- .env | 16 ---------------- .env.example | 14 +++++++++++++- README.md | 1 + codex-build/__pycache__/api.cpython-314.pyc | Bin 23950 -> 15698 bytes {codex-main-test-build => codex-build}/api.py | 0 {codex-main-test-build => codex-build}/cli.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 173 -> 0 bytes .../__pycache__/neo4j_driver.cpython-314.pyc | Bin 18165 -> 0 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 182 -> 0 bytes .../translation_service.cpython-314.pyc | Bin 1489 -> 0 bytes .../codex_schema_design.md | 0 .../docker-compose.yml | 0 .../requirements.txt | 0 .../__pycache__/api.cpython-314.pyc | Bin 15698 -> 0 bytes 14 files changed, 14 insertions(+), 17 deletions(-) delete mode 100644 .env rename {codex-main-test-build => codex-build}/api.py (100%) rename {codex-main-test-build => codex-build}/cli.py (100%) delete mode 100644 codex-build/codex/__pycache__/__init__.cpython-314.pyc delete mode 100644 codex-build/codex/__pycache__/neo4j_driver.cpython-314.pyc delete mode 100644 codex-build/codex/services/__pycache__/__init__.cpython-314.pyc delete mode 100644 codex-build/codex/services/__pycache__/translation_service.cpython-314.pyc rename {schema => codex-build}/codex_schema_design.md (100%) rename {codex-main-test-build => codex-build}/docker-compose.yml (100%) rename {codex-main-test-build => codex-build}/requirements.txt (100%) delete mode 100644 codex-main-test-build/__pycache__/api.cpython-314.pyc diff --git a/.env b/.env deleted file mode 100644 index 68e01a6..0000000 --- a/.env +++ /dev/null @@ -1,16 +0,0 @@ -# Project Codex — environment variables -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=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/README.md b/README.md index 91afe26..ab5f798 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +# UPDATE THIS # codex-cli — Medical Translation Terminal Test the backend translation engine directly from your terminal. diff --git a/codex-build/__pycache__/api.cpython-314.pyc b/codex-build/__pycache__/api.cpython-314.pyc index 46dfe0108a5c7876803780a296972b252e9e9b3b..18b31129a249ef890a5d7f54d26c75d1102fa185 100644 GIT binary patch literal 15698 zcmb_@Yj7J^mR>iy@qX}4kd#O^DN!UOKJ>KSlKB*AQ8eA6Woa!Mh6sp+4FYg?Ln7gI zietNCdTJ|{lHHZO>y+e5wk%gAwQ?nsIH^iyB-_a7krCKSAJBG>z%e8VAcMeqyR%(@v+<0XZH!h4~gzKNtj`bf&H@HRHGPqUS%Esjxsvm678U`D+#z9Gw2Ai~| z!Dg*_aGSObV|9#1~gwXP_2H%r}u)U8FSdsu2c zQX7g=_p;PRq)J7p`&eodQk#oX_p{V(NZnqPdSJYLbo%l6%Pr#vUE&c&LEd4wQ3*Uq}Q(Tz4GpLwfCU*-Ziz4Y|{6> z@%`h0(JLE_YQKB{BYNT>P8B`!!J?TQ+oZKao3?g%ZENFx?1LlbZrR7nM=|1KG~xtr z&G`5xGd@1PPd>4Z&L@jTcVhgc{N$nY92XtrxUB{Ucv5?EyqlfQH;wrehZsw@+_U~< zX}#mSM&B+jXT;^+@m{km&cc(UKYqLnTI`ei$9v@g`INaA8o`OSv5&-!Z+CFfJv2J4 z1t)HJ!xp^bEk*r^qaz!YdVx2o*vQqRb*-hlrJo;OF=BB(&K(u%xi}vh%=|45u%|Z_ zjm=17N+hW!B3e9^l)6WUJjXYBJOdF`L$0L8lq>Oxn50cbG)awXs&px)U5&+(NQp_k z!$VSAN|7STS!r-=Y*d;^#A8VP?W68;QGS=zwbSDzh zrHqOfRh1H{$@qknh+m1RQamY5(15!-C24Fbu1c5ViI|j1CT5R#B&kE{i(RHlk`j9* z6H_&*3o$jFN~$qQor6LAjU+X+IE{u9YKYa7QyC4J#&9Evgeqw%HW?*8Ii*Q# ztQh^}Omc!~#Oe&Bl<5fO9#fQ*qGDW0HLhxyX=2vn=}$(}sd!RTJsswQN~ikAB&lmE z7D;GR(wb)&(_^!pn2JrjEdA}=pVB%=yCg;|R2_Z#?3mPLQY%(e*<8Dr6p^AzW>V5( z%Cyy4BomEmU9VEwYi6ZqGLb}Fo0V9e)HWSgL2A@1EQJcnUBYXW(Z}dJ8k7 z*$lpi>Ge`3p3pk5G-_lzjpc|&w1_q0G?rOiSMAv}YlilN5s?y+ zN302$S!!{&AatP)?FcLwZN{m{6{COJDF*36yzY%8lPQhuZgm9juBX$qijhRft~>La zTKCi9_s>kkSf1`N204KJrHd?dsAwXiqP~v?DCaj8-rYSBHFk!!6Lm&Y3P)4e30JaW zC&G}RyG`20^m3MO&MHh3)&oWz5I9W43psR0RKbo=^b)IJYzC87^;&H3XgI%QVY6Ax zM%63UWEsn+S53yWFiu1KN+f~38O|qH;#{N>S7Ko;6~2VghiQXkn+GD8FAil&ichCg zie{WPbN?TaTGZ_JmbOIfN-U9#Ovgf4l$N$aY#Qeks~A#4Hr>uj>5j|nm}K4T+;&FN z@oc@pbI!z~@rg)cJ@3grS=i8QJ5%$j6q!&`#0>Hq1qs+a6RAv6Q{u=bW-)23cxTqW zr*n7bZjjxs#Wk#-7yDyEi5m;+1X)$kYoeHYT1w+Nq|R~UoW?_Ow7oE(3EQDOj*GHw z2XbUVv&$mh?D;nbdvmZir|gn#vRk&x9@!y#yM!*ji`Sg8Pjkt_HZJ7XORbs727l58 zRa114Xg~6Mzqu=)N8-3H(o}V)F~wQE>?Jjo469%d(~)p&1`Dq{V>9f)>F)TYOj=1@ zjwN+lOx0~?&VR^-+`20qCfbF=x+fe4x633b?hA)s!THbUl!U{V9{u4bxMiN_M~RIMwQe{u?a2Ac0(829v#z>c(MZxX&uB=qFvZ7o#|O+3)*AB z03zy>2;Smy9{(Fdxw?kAzWI(tu_0IAG+ka*xHZlPOx2U0+lUU$i4Zx9tYu-6BL}JV54O| zNEsAdz(=KGSmu)QH%4+bEsJ7Hu0mQArCddDQ4Hp`wa%Tnc6?E6&F$=*8(FAqT@*WW z?YkGm+M6d9#oamo_GNLqu^FhvjW@#)

d&=(^2NbSALyb}XrR8o(v7q%PglP0C0= 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 Hj`;rtNhyWj literal 23950 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 y-QGDX9AQO#x^?_<%1N7}_`DC{Qm+w*#1=j diff --git a/codex-main-test-build/api.py b/codex-build/api.py similarity index 100% rename from codex-main-test-build/api.py rename to codex-build/api.py diff --git a/codex-main-test-build/cli.py b/codex-build/cli.py similarity index 100% rename from codex-main-test-build/cli.py rename to codex-build/cli.py diff --git a/codex-build/codex/__pycache__/__init__.cpython-314.pyc b/codex-build/codex/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 2c427dc8375c14e82e8fc1aaa334753d8e0e853e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 173 zcmdPq2KczG$)vkyYXf(+3Vi4mKGb1Bo5i^hl0H2jAasU7T diff --git a/codex-build/codex/__pycache__/neo4j_driver.cpython-314.pyc b/codex-build/codex/__pycache__/neo4j_driver.cpython-314.pyc deleted file mode 100644 index 42b3d09324edce5c1febf5825a36a3d5ecd7d3ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18165 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 diff --git a/codex-build/codex/services/__pycache__/translation_service.cpython-314.pyc b/codex-build/codex/services/__pycache__/translation_service.cpython-314.pyc deleted file mode 100644 index 076d53a3982193b00eba50494bd115f702897976..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1489 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

d&=(^2NbSALyb}XrR8o(v7q%PglP0C0= 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 Hj`;rtNhyWj From 5f7ed1cfa04ea5aa57cfddc2ada59d5a8cc78a6e Mon Sep 17 00:00:00 2001 From: Logan Watersmith Date: Wed, 29 Jul 2026 00:27:53 -0700 Subject: [PATCH 05/23] Got UI working, struggling to get backend integrated unfortunately --- .DS_Store | Bin 6148 -> 6148 bytes .env | 18 + .gitignore | 1 + api/config.py | 17 - api/models/condition.py | 24 - api/models/drug.py | 39 - {api => backend}/.DS_Store | Bin LICENSE => backend/LICENSE | 0 .../api_documentation.md | 0 backend/codex_build/Dockerfile | 12 + .../__pycache__/__init__.cpython-312.pyc | Bin .../__pycache__/api.cpython-310.pyc | Bin 0 -> 10635 bytes .../__pycache__/api.cpython-314.pyc | Bin 0 -> 16764 bytes .../__pycache__/config.cpython-312.pyc | Bin .../__pycache__/db.cpython-312.pyc | Bin .../__pycache__/dependencies.cpython-312.pyc | Bin .../__pycache__/main.cpython-312.pyc | Bin .../__pycache__/main.cpython-314.pyc | Bin 0 -> 3074 bytes {codex-build => backend/codex_build}/api.py | 24 +- {codex-build => backend/codex_build}/cli.py | 0 .../codex_build/codex}/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 115 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 181 bytes .../__pycache__/neo4j_driver.cpython-310.pyc | Bin 0 -> 5745 bytes .../__pycache__/neo4j_driver.cpython-314.pyc | Bin 7274 -> 7272 bytes .../codex}/config/fallbacks.json | 0 .../codex}/language_packs/english_pack.json | 0 .../codex}/language_packs/french_pack.json | 0 .../codex}/language_packs/russian_pack.json | 0 .../codex}/language_packs/spanish_pack.json | 0 .../codex}/language_packs/ukrainian_pack.json | 0 .../codex}/neo4j_driver.py | 0 .../codex_build/codex/services}/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 124 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 190 bytes .../translation_service.cpython-310.pyc | Bin 0 -> 4384 bytes .../translation_service.cpython-314.pyc | Bin 7836 -> 7834 bytes .../codex}/services/translation_service.py | 2 +- .../codex_build/codex/utils}/__init__.py | 0 .../codex_build}/docker-compose.yml | 2 +- {api => backend/codex_build}/main.py | 11 +- .../{ => codex_build}/neo4j_sources/.DS_Store | Bin .../__pycache__/config.cpython-313.pyc | Bin .../__pycache__/config.cpython-314.pyc | Bin 0 -> 1238 bytes .../__pycache__/db.cpython-313.pyc | Bin .../__pycache__/db.cpython-314.pyc | Bin 0 -> 2640 bytes .../__pycache__/source_data.cpython-310.pyc | Bin 0 -> 19893 bytes .../__pycache__/source_data.cpython-314.pyc | Bin 0 -> 38044 bytes .../neo4j_sources}/codex_schema_design.md | 0 .../{ => codex_build}/neo4j_sources/config.py | 4 + .../codex_build/neo4j_sources}/db.py | 2 +- .../neo4j_sources}/dependencies.py | 2 +- .../neo4j_sources}/routers/.DS_Store | Bin .../neo4j_sources/routers}/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin .../__pycache__/conditions.cpython-312.pyc | Bin .../routers/__pycache__/drugs.cpython-312.pyc | Bin .../__pycache__/sources.cpython-312.pyc | Bin .../__pycache__/translate.cpython-312.pyc | Bin .../neo4j_sources}/routers/conditions.py | 4 +- .../neo4j_sources}/routers/drugs.py | 4 +- .../neo4j_sources}/routers/sources.py | 4 +- .../neo4j_sources}/routers/translate.py | 4 +- .../neo4j_sources/source_data.py | 13 +- .../codex_build}/requirements.txt | 0 .../__pycache__/config.cpython-314.pyc | Bin 1044 -> 0 bytes .../__pycache__/db.cpython-314.pyc | Bin 3368 -> 0 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 183 -> 0 bytes backend/translation/services/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 192 -> 0 bytes backend/translation/utils/__init__.py | 0 codex-build/__pycache__/api.cpython-314.pyc | Bin 15698 -> 0 bytes docker-compose.yml | 12 +- frontend/Dockerfile | 12 + frontend/README.md | 73 + frontend/eslint.config.js | 23 + frontend/index.html | 13 + frontend/package-lock.json | 3061 +++++++++++++++++ frontend/package.json | 32 + frontend/public/favicon.svg | 1 + frontend/public/icons.svg | 24 + frontend/src/App.css | 510 +++ frontend/src/App.tsx | 596 ++++ frontend/src/assets/hero.png | Bin 0 -> 44919 bytes frontend/src/assets/react.svg | 1 + frontend/src/assets/vite.svg | 1 + frontend/src/i18n/config.ts | 26 + frontend/src/i18n/locales/de.json | 24 + frontend/src/i18n/locales/en.json | 24 + frontend/src/i18n/locales/es.json | 24 + frontend/src/i18n/locales/fr.json | 24 + frontend/src/index.css | 17 + frontend/src/main.tsx | 15 + frontend/tsconfig.app.json | 28 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 26 + frontend/vite.config.ts | 11 + requirements.txt | 4 - 98 files changed, 4664 insertions(+), 112 deletions(-) create mode 100644 .env create mode 100644 .gitignore delete mode 100644 api/config.py delete mode 100644 api/models/condition.py delete mode 100644 api/models/drug.py rename {api => backend}/.DS_Store (100%) rename LICENSE => backend/LICENSE (100%) rename api_documentation.md => backend/api_documentation.md (100%) create mode 100644 backend/codex_build/Dockerfile rename {api => backend/codex_build}/__pycache__/__init__.cpython-312.pyc (100%) create mode 100644 backend/codex_build/__pycache__/api.cpython-310.pyc create mode 100644 backend/codex_build/__pycache__/api.cpython-314.pyc rename {api => backend/codex_build}/__pycache__/config.cpython-312.pyc (100%) rename {api => backend/codex_build}/__pycache__/db.cpython-312.pyc (100%) rename {api => backend/codex_build}/__pycache__/dependencies.cpython-312.pyc (100%) rename {api => backend/codex_build}/__pycache__/main.cpython-312.pyc (100%) create mode 100644 backend/codex_build/__pycache__/main.cpython-314.pyc rename {codex-build => backend/codex_build}/api.py (94%) rename {codex-build => backend/codex_build}/cli.py (100%) rename {api => backend/codex_build/codex}/__init__.py (100%) create mode 100644 backend/codex_build/codex/__pycache__/__init__.cpython-310.pyc create mode 100644 backend/codex_build/codex/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/codex_build/codex/__pycache__/neo4j_driver.cpython-310.pyc rename backend/{translation => codex_build/codex}/__pycache__/neo4j_driver.cpython-314.pyc (95%) rename backend/{translation => codex_build/codex}/config/fallbacks.json (100%) rename backend/{translation => codex_build/codex}/language_packs/english_pack.json (100%) rename backend/{translation => codex_build/codex}/language_packs/french_pack.json (100%) rename backend/{translation => codex_build/codex}/language_packs/russian_pack.json (100%) rename backend/{translation => codex_build/codex}/language_packs/spanish_pack.json (100%) rename backend/{translation => codex_build/codex}/language_packs/ukrainian_pack.json (100%) rename backend/{translation => codex_build/codex}/neo4j_driver.py (100%) rename {api/models => backend/codex_build/codex/services}/__init__.py (100%) create mode 100644 backend/codex_build/codex/services/__pycache__/__init__.cpython-310.pyc create mode 100644 backend/codex_build/codex/services/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/codex_build/codex/services/__pycache__/translation_service.cpython-310.pyc rename backend/{translation => codex_build/codex}/services/__pycache__/translation_service.cpython-314.pyc (92%) rename backend/{translation => codex_build/codex}/services/translation_service.py (97%) rename {api/routers => backend/codex_build/codex/utils}/__init__.py (100%) rename {codex-build => backend/codex_build}/docker-compose.yml (95%) rename {api => backend/codex_build}/main.py (65%) rename backend/{ => codex_build}/neo4j_sources/.DS_Store (100%) rename backend/{ => codex_build}/neo4j_sources/__pycache__/config.cpython-313.pyc (100%) create mode 100644 backend/codex_build/neo4j_sources/__pycache__/config.cpython-314.pyc rename backend/{ => codex_build}/neo4j_sources/__pycache__/db.cpython-313.pyc (100%) create mode 100644 backend/codex_build/neo4j_sources/__pycache__/db.cpython-314.pyc create mode 100644 backend/codex_build/neo4j_sources/__pycache__/source_data.cpython-310.pyc create mode 100644 backend/codex_build/neo4j_sources/__pycache__/source_data.cpython-314.pyc rename {codex-build => backend/codex_build/neo4j_sources}/codex_schema_design.md (100%) rename backend/{ => codex_build}/neo4j_sources/config.py (75%) rename {api => backend/codex_build/neo4j_sources}/db.py (93%) rename {api => backend/codex_build/neo4j_sources}/dependencies.py (91%) rename {api => backend/codex_build/neo4j_sources}/routers/.DS_Store (100%) rename backend/{translation => codex_build/neo4j_sources/routers}/__init__.py (100%) rename {api => backend/codex_build/neo4j_sources}/routers/__pycache__/__init__.cpython-312.pyc (100%) rename {api => backend/codex_build/neo4j_sources}/routers/__pycache__/conditions.cpython-312.pyc (100%) rename {api => backend/codex_build/neo4j_sources}/routers/__pycache__/drugs.cpython-312.pyc (100%) rename {api => backend/codex_build/neo4j_sources}/routers/__pycache__/sources.cpython-312.pyc (100%) rename {api => backend/codex_build/neo4j_sources}/routers/__pycache__/translate.cpython-312.pyc (100%) rename {api => backend/codex_build/neo4j_sources}/routers/conditions.py (96%) rename {api => backend/codex_build/neo4j_sources}/routers/drugs.py (98%) rename {api => backend/codex_build/neo4j_sources}/routers/sources.py (87%) rename {api => backend/codex_build/neo4j_sources}/routers/translate.py (96%) rename backend/{ => codex_build}/neo4j_sources/source_data.py (99%) rename {codex-build => backend/codex_build}/requirements.txt (100%) delete mode 100644 backend/neo4j_sources/__pycache__/config.cpython-314.pyc delete mode 100644 backend/neo4j_sources/__pycache__/db.cpython-314.pyc delete mode 100644 backend/translation/__pycache__/__init__.cpython-314.pyc delete mode 100644 backend/translation/services/__init__.py delete mode 100644 backend/translation/services/__pycache__/__init__.cpython-314.pyc delete mode 100644 backend/translation/utils/__init__.py delete mode 100644 codex-build/__pycache__/api.cpython-314.pyc create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/icons.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/assets/hero.png create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/assets/vite.svg create mode 100644 frontend/src/i18n/config.ts create mode 100644 frontend/src/i18n/locales/de.json create mode 100644 frontend/src/i18n/locales/en.json create mode 100644 frontend/src/i18n/locales/es.json create mode 100644 frontend/src/i18n/locales/fr.json create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts delete mode 100644 requirements.txt diff --git a/.DS_Store b/.DS_Store index a8a6902472d2aef46cff403ae19648582bb76279..63978181cc407a4ef18dfaf30783a645909972b8 100644 GIT binary patch literal 6148 zcmeHK%}N6?5Kd~fDOKn}p~rw14+Yz;h~i~k>kGJ|2bJ1&YrDAJly++mrLbpxAs@k$ zN1w;Z&x*2&UMeaxF!_?nOu~Fwk{v<_b;f?3kTM~JflAC3q4_}Mywr^3jAaRssx@NL zArbMt$!L)AM#FDpK%N~V*JMZpoZOr9^Fral+=Dj=gHc?oJ=5GmesQV53arE~+@6@Y zaXTK5oc8ccj*f*0y|mx__5GlS$#bQ?g<(g?s2^@(R0k z_e%8New!JezGya0?rh^}=k$5`asQ*W`ojN~-=O#Pp11BW3Psd~ebRi=JP?rqWB?hM z2LtA=VWoL2j%y+V$iSawK;91=RHAJ#(WtHtXjBORmfKVL@ zs6(kKF{lm)J2!r|!9=4DC)5-l)a*=6g+kTtus&DrgxVUhMFx<8_Y5rPVOhTaho9H~ z?}KoJ3?KvliUFE$x=jaCGH+`pIr-Ld&`VG$(l62Y8Uh{l6oapPiYuT>z|Lg{&^DN8 R1PciM2q+q`K?Z)5ffq^$ZRY?0 literal 6148 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 literal 0 HcmV?d00001 diff --git a/backend/codex_build/__pycache__/api.cpython-314.pyc b/backend/codex_build/__pycache__/api.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..161927d2a018fb54752711e4629b39c5f76dacc1 GIT binary patch literal 16764 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 literal 0 HcmV?d00001 diff --git a/api/__pycache__/config.cpython-312.pyc b/backend/codex_build/__pycache__/config.cpython-312.pyc similarity index 100% rename from api/__pycache__/config.cpython-312.pyc rename to backend/codex_build/__pycache__/config.cpython-312.pyc diff --git a/api/__pycache__/db.cpython-312.pyc b/backend/codex_build/__pycache__/db.cpython-312.pyc similarity index 100% rename from api/__pycache__/db.cpython-312.pyc rename to backend/codex_build/__pycache__/db.cpython-312.pyc diff --git a/api/__pycache__/dependencies.cpython-312.pyc b/backend/codex_build/__pycache__/dependencies.cpython-312.pyc similarity index 100% rename from api/__pycache__/dependencies.cpython-312.pyc rename to backend/codex_build/__pycache__/dependencies.cpython-312.pyc diff --git a/api/__pycache__/main.cpython-312.pyc b/backend/codex_build/__pycache__/main.cpython-312.pyc similarity index 100% rename from api/__pycache__/main.cpython-312.pyc rename to backend/codex_build/__pycache__/main.cpython-312.pyc diff --git a/backend/codex_build/__pycache__/main.cpython-314.pyc b/backend/codex_build/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1dec78f1ffb45bdc7bf5c1e3a406bd94ad731b3a GIT binary patch literal 3074 zcmb_eTW=f36`t8$UL=YaiJ~aU7gCh$go!OWsa@BuEF`|fOVk%-<*F?jda>GFiEA%+ z+1X)a=}C42s1JQmfEIDlmptSp5B)C`EWpYDLem?9yvdP}q;EYlOX`vsEf92oJ$vTt znK_ql&Y68ZmFz_@c0O?){3U|WKlr3O;vHu99SNaTq#=s#BMn!vDp5(5sa)W5sj8?E z8WFr)jjAyk6TDK5t39+w@R4e-nxF}_kM^laniR5VHKnF$TJW)IM(wBlf{#}R)GW=a zgLF{M(VRL&hlISRnpcPEZ~@h%F;q*``f5q7_o5v7NJnZTGZ(s!AG~W3Eis9;J}n89 zKdFllB_9QpLt{uw=aH7F#m;y4IR^V&3N;Q>t^Yg@=h^_Q$mWroIp0yBCt%H$!`dNV z%g#)HU)tRV;&}P0!5_3L_k&gq{m)fze?J0kP|HCkhqQb*`!yL5+0W3Aur>@i8sRx| z@qU)Zw9#=?KGx!b-WlRQuuWRL=3Dlo;@?;QP<+P-=-V^ZqS0)+j%iTG_lmYh$#Svj zJ06|vElVucQDU*{cGLE(pp04S+Ffn-wqseYy<`v@Bt2J+z`hL|yD(91IRP!ptOxei z?^W;Jy1~+%y?$f%npT~;SG{|OCAeVjhBgb6Pce%Lr>ehT&VLg->Uf>$k zW)X|D76aC6`kv)*KLZvE{1!27?oT3Qb<3az*o{z!x;9H#c3={x$<X9uD_)F#94H}M1MFniETkz9K#K|8V#G)eQSSO zw+##SHxB~`^()5i8F(2`%QZwX)U$NIK;!xc2f#sM~*8{$^w93@V&jSb= zGP!7YOMnsy8V;>jZW4R>s{g3cId8gvwlZ&+58?P!On&2SdMU8E)rGTGtmG5>VIiY|n?!{f8_qv^*0;Ifxhha!lQzlsNM(Y6qd_ zenw2&b#=Xr$r$&Iqx2Rj0VT(}L^vIT892R$K0xGD_sdGIVlFX50~?yOKG4(1ta*(lkkvE7CZ+AUhb2 ze3YlT#ml;7LRACP0BS7SD<@_{$wJF@d1ne!Wfkw({`m)E5bQ}XLHdCJU*UWPh&ZLE ziy~B(t{WaSE4{mz@P^1S3SI^BCHVh&YT$XQw2>;U-u$PMUXN`m!=LvJt~(!(w9ovk zow(IjZv8vpaX&d(#4E81RJ=>;EE>L7Ug2#tjGzdnLf?wetvp`9?>lt4Xx44>ArzHD z4d`SLW}{({<^K=Tk~L5GKgEAT`Ar^f76?aK#?vo2`Tdf3y&Pf=-R@Pf0^seoADDMX z_nvvBBe39}KMwya8s^6k-T<+taFELp@&+g|$$!WqHX%M?QOl-=d8(bb(pIjB5bu;rET-$0&r3VD0M{Q>AK?W?c(5$r5luGsFSkk4CjsHevv_ma zGT;jXKl}P z?11!fh1^jE;zJh#r;>c*6enp;_^D?jM^!_A#KkBogvoH%vy@?3`ay`IK|spkG8Bcu zQjTZ3Ez8zJODx)X%WyS|>=S;Cgk6kR5sOcSEq0pRg(*KNK^Ewaj4^&8Nm%(Nj_|2% z^yW4?xsCE)Aaw(&+h}4No&77i`3&9s7kcAw(d1Tdb}IwO<@aJrOj?z9`Vdy0qk#=H zu%!%aWnXI#yuLLwx|JPTKPAXc0^#f)iehOmgK=Tq*@Ii4uoFd6WNrBIaJzTx87}Qf zyr=ETNE+V}>`rEX+x1J&v*R1t@l7g`k0-MszED-$|L?8o3AjbiSi&=m~3PUi1CZpd_Zh8r#l literal 0 HcmV?d00001 diff --git a/backend/codex_build/codex/__pycache__/neo4j_driver.cpython-310.pyc b/backend/codex_build/codex/__pycache__/neo4j_driver.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..489fe1025cbbe2889a92ba919fb9c86293f2fd8c GIT binary patch literal 5745 zcmd^D&2QVt73Yu?_2G}ivh!glbqC#SYc3o+8|=w&7jw z8B$5?(7>V&dJBrS$Fx9ltI=!!8XntI&%L$Sw(ku;M9OxYq(v{K;n8sB%?IcG-tWCP z&e&Kgya+-;kCjYB{LoMNNZR!6G|FOSAG;VM}KDmvXDfuCXF3y&$a;yUs?~DAY#S7#n{f zwMN+mb`gHZ*aR!X?>L)eQ}BC%U9v8+%htq}%C4}hFVt3!1C&y-}~WN>Q~@skg?%`j;z>#)I|x z8Vpm-zV|RF?OLqcF}<#1cM7;d^c3ND1pZgy-`#?Ok;8&7?MQyXC%)pVe$LnYJdJvW8l$4AHQZiFt=7{7Gmh;IgiD|wDW!}$0S@2~ji@x$o`hDq096x+!$uNIH zB&kHjtzV5E!{|}^VHklvzBgnAkoI^p`ISvTdJA%cHi8# zV7S9Tum|sLtj<&0+>INsnjPu!r6xCRwWi&emLxH6ygM(p**(i;t%ds>J{qo1w429Lz+H$x?-$hSM&|o8&!qsQ>&eIg6VyQ7~5RB1FB{8c?gP4 zd^tr5H{4jS4V|r6F~X`)>%Emm`%WFUi3->Z6@Wsz$~7=0j{L;)Py{)#(t)v42Xx+#MNhNegl-~3PRpikGPdmWm(B@pqSXd9Hfl8r-_-+%GYdZ( zt#kk*#Q$83`SgI!0un{aAi^>0UpA}j#!8K5_MQc4i2qQ&UTJRB4Z2osG^@sHGdb$m z99J4(ehD|u(?p=RfaXJ?A2*D&JD>DzCVc|(sZr6cVN_(sgF&5SP}J4N1;m`-#42@q z=c~a|@o>?tE5alK6?YPmS4mI%_A%j^ug4&<)tm5 z&A|2Eh8`Dzuae7?{0dY$0OjP#n8DAW8O%W5mJSu+L1mwS5y)%O*Rx<8U>Ho9mW~Kh zAIa|Z@Qq2^rL&n3W^-Caiac_Jung zpy^aQL(WfBht&0+W`^T47IqSYY5)2Tj$pv{AzsApBBUleJqe`&I8VpY0Bfu3wOVDh zITxW^M;NDI?utc@A#TENYmKJ6IA|au^wBg>4?Z@W^c-{x96E!NQ}1EdjRAYCgy=)Q z3^br#*RcV=h|@snS$3cSRa-VYjzBw&t07c_{B9TM-`)nV;5;1$?(|mIX6;BKjbOJ5 zW8JGzs6-trlUbq?P1cA^)Nj>)$=|4X{ypeC8_`YZJp<9#57iLC+Y+F8CWGc50Gbi) zsz5VfnrSSbL36<{fBpZ9=94ypc5yCG|5wo;(zF1-;OP+d9Kq7k!j=J84<-TMqEQ6X zA-wrDkkw@YUp@QLY2*^%&-h{-4k#PzAt@XIyePROldF>Dzzks2U!!3{T@55UL2f`IYqli1P z7jckwwy((>biWFpfCv=aZA0=g22~BS@V+r<;v1tgO$C_fGcYR`-@IW!)Fo=MCPUA= z42nwfLU1=3BnbJF*-z|_KXA#^tmg#FPR)60@pRCvLyjG_iZol#`1KFm(7T?&JMK2j z6aS-~iX##*4ODTl5;9nzp?~dJkh*1Ii;mju_q<{FPC>KNdjiRm)5dw)2$$a?dNK7= zNqO=j;h#WNk{z0j6ugg zRYm?*Q*Zo(FFG+n;nsc(!N% zAWjzu##nur(vyyp;c==fPTkT0tmr<6(D}!pmSLq&B8i90-G*>i7{Q-FE~fBtzKF^s z9F?ofhM`XdIdKiaF^>YQiR(;``VVBs4K$b#?l%La&%1%zcP$Tt$yf|R(6 z5Sf?=UinAZhMqteTcAMV2Zrr9JpnSYd}`AIvm)IqEblt3-?KhPi*zXzqhyp6h^Ca3 W5-E`~{?fvg!nML|VWNC>wD@lzF|;}W literal 0 HcmV?d00001 diff --git a/backend/translation/__pycache__/neo4j_driver.cpython-314.pyc b/backend/codex_build/codex/__pycache__/neo4j_driver.cpython-314.pyc similarity index 95% rename from backend/translation/__pycache__/neo4j_driver.cpython-314.pyc rename to backend/codex_build/codex/__pycache__/neo4j_driver.cpython-314.pyc index 9a64efc40c31ba869b169e84bc4bf6bc2d972c51..a1eac94a750bba2777685a3f22dfa16134affcc8 100644 GIT binary patch delta 45 zcmaE5@xp>jn~#@^0SIhLH*&3DWel6VnN^oBDKR-aH7`X!IX@+}B7XA+)+#XoCa(@O delta 47 zcmaE1@ydcrn~#@^0SGpFZsc0Q$`~G(QW}a?IYH^9~=69@BVgO9f B4;26a diff --git a/backend/translation/config/fallbacks.json b/backend/codex_build/codex/config/fallbacks.json similarity index 100% rename from backend/translation/config/fallbacks.json rename to backend/codex_build/codex/config/fallbacks.json diff --git a/backend/translation/language_packs/english_pack.json b/backend/codex_build/codex/language_packs/english_pack.json similarity index 100% rename from backend/translation/language_packs/english_pack.json rename to backend/codex_build/codex/language_packs/english_pack.json diff --git a/backend/translation/language_packs/french_pack.json b/backend/codex_build/codex/language_packs/french_pack.json similarity index 100% rename from backend/translation/language_packs/french_pack.json rename to backend/codex_build/codex/language_packs/french_pack.json diff --git a/backend/translation/language_packs/russian_pack.json b/backend/codex_build/codex/language_packs/russian_pack.json similarity index 100% rename from backend/translation/language_packs/russian_pack.json rename to backend/codex_build/codex/language_packs/russian_pack.json diff --git a/backend/translation/language_packs/spanish_pack.json b/backend/codex_build/codex/language_packs/spanish_pack.json similarity index 100% rename from backend/translation/language_packs/spanish_pack.json rename to backend/codex_build/codex/language_packs/spanish_pack.json diff --git a/backend/translation/language_packs/ukrainian_pack.json b/backend/codex_build/codex/language_packs/ukrainian_pack.json similarity index 100% rename from backend/translation/language_packs/ukrainian_pack.json rename to backend/codex_build/codex/language_packs/ukrainian_pack.json diff --git a/backend/translation/neo4j_driver.py b/backend/codex_build/codex/neo4j_driver.py similarity index 100% rename from backend/translation/neo4j_driver.py rename to backend/codex_build/codex/neo4j_driver.py 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/backend/codex_build/codex/services/__pycache__/__init__.cpython-310.pyc b/backend/codex_build/codex/services/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6be17769e14a4de2a00dd342b73ccdd851062a0a GIT binary patch literal 124 zcmd1j<>g`k0-MszED-$|L?8o3AjbiSi&=m~3PUi1CZpdORa%!=De0*kJW=VX!UP0w84x8Nkl+v73yCPPgT_D#KgBYKf85tRin1L(+ DeiSjL literal 0 HcmV?d00001 diff --git a/backend/codex_build/codex/services/__pycache__/translation_service.cpython-310.pyc b/backend/codex_build/codex/services/__pycache__/translation_service.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c28414000a4c6b4bc01e2326595106052ea196ae GIT binary patch literal 4384 zcmbtX&2JmW72lctj_u-2JYjD1XJy_|JmQkKjqZgn}uIgi4FJq(xoYQeCyBxmrti z^_JlpEz>nymTM85N5g_!khSgFzJ0g2SCZv~EGPY`eZo|xF@0a#C+@WWia*VaUHa*S zTlNckNBlBKXWj!GP0^HHl_-&QSxJ`f5u)J#eR!D3tOtra%gU_a&oLXGqCeL#Scy$M z(A{~IHYl59Qx7!vD4S-l!0ZJ^*OkhVr|>pP#S-N@_r27wrQC~>&`X0j5;ILd9T(J? zmwIBM5k#!k3X&vIBoNzQ=3BwI`U9 zK9eN3jXH{`p?wP18$a($y?r`ef+tSex8pC2&2TwZyz>K1NpblN;__>rqG zvF#vq>AUNKLf2f4nqiP^ib61)Vl49xPNi$V5VXNvV~zV!eX~=9OUq|{@pdy&U*#ZoK2AJl^|3;;%U zKnR-+h1N=l-JQ@ajfwryq#*JHy;&7Tl6q+;=}ukuTd{+V=6HWnm^hI9wcY z3cU*9vrs)z_#9Nv&tLM|?MwBT`S&j+KHm=NesXEdk!!hdvArWE!`Ndr2AfE4MwsLr z6gHWmGi3Nz54Prr{WoiV`l&U~kHTc3N#E4ag@svMD#3et3)cpe)%07yMY+ zQ}(4BJ_Lu`Q^6hgdTOd=>c`41?P;0D=Q0i5^r4aw{&uS4NJcWctM&9u=Woj%VdOVT zLptySE7PwlyM$?v^!F4xW~}Ro&`gazGb2r9pZ1K*xUPI~rf24;rbM+ei#H^yi7uWQ z64_)bAX^gIdSp`I*Td*rOMM(xkT`{`z#k$G(1#8LQfT&dEJ~!p0O>Fa%vU-j{w%dJ zvP()z&z2ZUW;fyCzh|Y4{7zvLkJX4EcRw-?BY-ph;eazL!06M}wK@{u;D`2tvFivt zUtNF#(3KC$J<{K80re)bq_R{O6g<(-=oZ9>C%Pz(8IrIDq%Cwwt2{#gT`QGMHhv-7sXKkBcjY9+W1N8v)WMIiB58$I|v{(lnWC=PbW;1 zJFdn3CU}__{?{|)$5HnaSje|Hg{?P+U3@8i8j26GaR!Qt$=}5GSuD`icm)bEF;pSl zY5QUr(rWchFNpYgoQTZhPBz1M!wYMEpF{s|#kLU{73-d<#?Ihe;iVI&0iMhjojaSp zGdMZtY1SKvv?qonGi9)YBaWXR$)RT*#@Q8LOa83oRr1&&u4%~S@w z0&E6+&ve-`pk-p~v4JbL%8>>$Eo?(wkhPZMVs!Xdj9wGvSreE`6?-?#cv6qrqRqWbu^giKC;3tJ>e zgf_8Nuzv7>bA-OMR4ey~QO_dhVdV9v{D(08fZZa8;v?<$&(KIA{`EfrK2blN0P7`t zV88pMN5}72er3&%-*85m2C)&Wx2NW91KMDVCS$VDFh3L;8i=c7`j$v46DAlI2RrD8Z^?5GoYH`E zu`TUZ6eMj)lztPTt`5{Q5H+K?u4^PG81ZczJ#DmbQoS6nGx9&O>S+Hmox4IZOj&l=u@YPH^vOQXIEaJ^!3ED&ShX4+M6Ko)SAJGq zyLqE_d+E-P`E^`P8XWT_ei;jyldBN!ByCF{kgATI#>m&KtF4%ILjM|Oi3z4E=is$1 aVyFhbeEmU0f!B0 delta 47 zcmbPbJI9twn~#@^0SGpFZsclaW6YS`$EGWgoS%|fp_`kSnWtNlT3n*L`6Sy#aR4lt B4u1du diff --git a/backend/translation/services/translation_service.py b/backend/codex_build/codex/services/translation_service.py similarity index 97% rename from backend/translation/services/translation_service.py rename to backend/codex_build/codex/services/translation_service.py index d880512..81b9217 100644 --- a/backend/translation/services/translation_service.py +++ b/backend/codex_build/codex/services/translation_service.py @@ -1,5 +1,5 @@ # Import dependencies -from backend.translation.neo4j_driver import create_translation, get_translation_data, find_missing_translations, find_missing_brands, get_equivalent_brands, language_exists, resolve_to_base_term, driver +from codex.neo4j_driver import create_translation, get_translation_data, find_missing_translations, find_missing_brands, get_equivalent_brands, language_exists, resolve_to_base_term, driver import json import os 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-build/docker-compose.yml b/backend/codex_build/docker-compose.yml similarity index 95% rename from codex-build/docker-compose.yml rename to backend/codex_build/docker-compose.yml index 57ad27c..033814e 100644 --- a/codex-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/backend/neo4j_sources/.DS_Store b/backend/codex_build/neo4j_sources/.DS_Store similarity index 100% rename from backend/neo4j_sources/.DS_Store rename to backend/codex_build/neo4j_sources/.DS_Store diff --git a/backend/neo4j_sources/__pycache__/config.cpython-313.pyc b/backend/codex_build/neo4j_sources/__pycache__/config.cpython-313.pyc similarity index 100% rename from backend/neo4j_sources/__pycache__/config.cpython-313.pyc rename to backend/codex_build/neo4j_sources/__pycache__/config.cpython-313.pyc diff --git a/backend/codex_build/neo4j_sources/__pycache__/config.cpython-314.pyc b/backend/codex_build/neo4j_sources/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62ce6d8f20457bb3c20e8a41b2e41a44e95e8537 GIT binary patch literal 1238 zcmbtSO-~a+7@loQKXz%MMG_G|))-?5NU^CdQDal-Dm4Plw02~@I?ZfYhXmh+f~=GBGdE$t4>t_>HFO)kgdHZpX(j9hYA zxiqa%v8%|dkoGw|J&kmzTsoh>w*WIVXq0uQWK}IHxq7u`Tbij8o%)SNt!&}7=G=(d@lrY%+2AQ}x`Pv=!FlZDi`j4BQt$fxIWw;7Wzq}#MO^B^~?!gd^DBWZd5@5k@B7IxX4mvrZePFP&6kaGMcwi?&b zYP@@MTKj?H?YSK@Zt&;UiuHUC->@mASZ-ln!w@26YinbxS_t6;ql6zjH@hu|!k+6g$hx$8^3s@>IgdSl2%R!lff$ literal 0 HcmV?d00001 diff --git a/backend/neo4j_sources/__pycache__/db.cpython-313.pyc b/backend/codex_build/neo4j_sources/__pycache__/db.cpython-313.pyc similarity index 100% rename from backend/neo4j_sources/__pycache__/db.cpython-313.pyc rename to backend/codex_build/neo4j_sources/__pycache__/db.cpython-313.pyc diff --git a/backend/codex_build/neo4j_sources/__pycache__/db.cpython-314.pyc b/backend/codex_build/neo4j_sources/__pycache__/db.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27311d65dca089938c0c25c20e9a5f1502ac3f86 GIT binary patch literal 2640 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 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/backend/translation/__init__.py b/backend/codex_build/neo4j_sources/routers/__init__.py similarity index 100% rename from backend/translation/__init__.py rename to backend/codex_build/neo4j_sources/routers/__init__.py diff --git a/api/routers/__pycache__/__init__.cpython-312.pyc b/backend/codex_build/neo4j_sources/routers/__pycache__/__init__.cpython-312.pyc similarity index 100% rename from api/routers/__pycache__/__init__.cpython-312.pyc rename to backend/codex_build/neo4j_sources/routers/__pycache__/__init__.cpython-312.pyc diff --git a/api/routers/__pycache__/conditions.cpython-312.pyc b/backend/codex_build/neo4j_sources/routers/__pycache__/conditions.cpython-312.pyc similarity index 100% rename from api/routers/__pycache__/conditions.cpython-312.pyc rename to backend/codex_build/neo4j_sources/routers/__pycache__/conditions.cpython-312.pyc diff --git a/api/routers/__pycache__/drugs.cpython-312.pyc b/backend/codex_build/neo4j_sources/routers/__pycache__/drugs.cpython-312.pyc similarity index 100% rename from api/routers/__pycache__/drugs.cpython-312.pyc rename to backend/codex_build/neo4j_sources/routers/__pycache__/drugs.cpython-312.pyc diff --git a/api/routers/__pycache__/sources.cpython-312.pyc b/backend/codex_build/neo4j_sources/routers/__pycache__/sources.cpython-312.pyc similarity index 100% rename from api/routers/__pycache__/sources.cpython-312.pyc rename to backend/codex_build/neo4j_sources/routers/__pycache__/sources.cpython-312.pyc diff --git a/api/routers/__pycache__/translate.cpython-312.pyc b/backend/codex_build/neo4j_sources/routers/__pycache__/translate.cpython-312.pyc similarity index 100% rename from api/routers/__pycache__/translate.cpython-312.pyc rename to backend/codex_build/neo4j_sources/routers/__pycache__/translate.cpython-312.pyc 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/neo4j_sources/source_data.py b/backend/codex_build/neo4j_sources/source_data.py similarity index 99% rename from backend/neo4j_sources/source_data.py rename to backend/codex_build/neo4j_sources/source_data.py index 600759c..27d6bfa 100644 --- a/backend/neo4j_sources/source_data.py +++ b/backend/codex_build/neo4j_sources/source_data.py @@ -458,7 +458,7 @@ def unify_graph(driver): log.info("<<< Graph combination complete. All sources are now connected.") -def main(): +def source_data(sources): log.info("=" * 60) log.info("Creating Singular Medical Knowledge Graph") log.info("=" * 60) @@ -472,16 +472,16 @@ def main(): log.info("Graph connection smoke test passed.") # Ingest separate sources into the single database based on user choice - if "drugbank" in sys.argv[1:]: + if "drugbank" in sources: log.info("Ingesting DrugBank.") drugbank(driver) - if "snomed" in sys.argv[1:]: + if "snomed" in sources: log.info("Ingesting SNOMED.") snomed(driver) - if "rxnorm" in sys.argv[1:]: + if "rxnorm" in sources: log.info("Ingesting RXNorm.") rxnorm(driver) - if "icd11" in sys.argv[1:]: + if "icd11" in sources: log.info("Ingesting ICD11.") icd11(driver) @@ -499,4 +499,5 @@ def main(): log.info("=" * 60) if __name__ == "__main__": - main() \ No newline at end of file + sources = sys.argv[1:] + source_data(sources) \ No newline at end of file diff --git a/codex-build/requirements.txt b/backend/codex_build/requirements.txt similarity index 100% rename from codex-build/requirements.txt rename to backend/codex_build/requirements.txt diff --git a/backend/neo4j_sources/__pycache__/config.cpython-314.pyc b/backend/neo4j_sources/__pycache__/config.cpython-314.pyc deleted file mode 100644 index 94402ee883c506f996eaf296982f1aa4514f81cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1044 zcmbtSO-~a+7@lp*cG;zcf~4g#Jw z#tSEcF#%2_p2`Wm`7=oNLMAcs;}3}a1J3qSO*k0mFz@p|^SsYH^JXTZ5fQNUF>4in zasYhylhz71LG1%OaRCGbya15Pa)e_skPR#ZN$@6E4UU3DQ_3VkY*#b2rlL!qq-x9-H$eVvXilpAQFPW*4>V8^S4R;2`+lE40rr00@;!wvoK z`)UvDZoe%0PjzCut`~B*z__#-XU<{{qoS#kk$Gg8+aurHcO#1?N)%ac+l;r6?Zb6z zV-stBCN7S1R4O6OPz^*(`z7sO%q%4ytIII^YH$se$h0h%kgk;q$RX3py(xu?O+`Kf zX_xKW(`bXrrSkceC77lW-S1z~9A^t*gG!E8u9QsG(1=EPeWy||@pvB%tE{Q6sx*Kc z$~XFB|I<)@!}KqMgDk-zf8%{RRMAL*h7AkbTG^ywZOy4Of~u}t%wMI#Tq>VY(^*JE zMP%8uH=lZ%d%~F1vs9fH=bz^mGSJ5-pvFuAot2jz6Fcmg8=AeP5r)eaDaenoxig2h z<>pM)`b=d<$5w?nvR)tKoyrasX3EIeESa--h}rqy!g5AR1*Lveg-$b z0_jcUOKjj^=7gr}_g^^}+aEia+@Cz$u3q>06}R_K P?wZ#he=Xvx?3%v-P>JHC diff --git a/backend/neo4j_sources/__pycache__/db.cpython-314.pyc b/backend/neo4j_sources/__pycache__/db.cpython-314.pyc deleted file mode 100644 index 64ea89b9e9b53b0a4d693543a8acaf880354e76b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3368 zcmb6cTTC3+_1@W?+1X{6N7(g(17Cj|!<$ldq^!rPN8K{!}W|sL*MpR84>ShgnsmRX=*pof(^; zh$?r@x#ynynDaizJ@ttgf$_nZd1E(B$Y;0+2CVt0y$I6^QHevQiAu-lsNe`4hznz( z(XbO96&-O@a-0S8Myi~Ibu7oqH;jU#TrAHTx0JuH+*c-y za^DSQmYHuFOj%;OWf?4%p>ARX@P#4W(Pwnq$b{VJ#PBP9FKbuTF*oL;SB6!$-otZ) zSFT)pMI9=5U-e+fU)0_JLam^5+1ssK<_>+<9*Rl{jglvNR6 zW05)%r%9X^K^ll7!)aNK0*tCLCzdBpof-lW<8FkDHS|(}`X9zggg;Crf zVTL!{i00AP;Jj@V=WOJ++r5yzY8%YX7R&Q`X-RhgUo@SC>a~pjn^ad1kF#CA`E99@~6IT6891T)BH>Vk30upg>1|5JMr7`E&1q%eDqIp8*2jc1m`p`GU^risC5GvperO# zriF9gBV-n+ZR9dtpensWCm~z_(yRcc3jNcwPhr_wFqj*&bfzyFsPT;8hHQtq;aRib zz(SfUvqjx;6Pi{i>b8w!O|$X(@f9wC8`m_wR4O}QG0=Z95M@8L5sq^ke7-{VNJ#8_ zg5alJJ2-NE^VjURwwUGK0LCOIlrDc|2})1 zx=_8e-Q2p`f3N?Y7xqL_e*(_JzsHHL|JdluKkFH66(6=;rz0HkmS1ay48LdPzmN zbc&FhLx;K4Q^(=XI1Y+A+yy6u@zaDv$uVMyR(P6NQeJE%mmm#Tk$fZ<4V3Y04xAUb zl6|f&0$0_q$d!E9`5b!7fdRGI*GC|um#D@EK}H$m0AJ;DL@hZV4bFH$Ier7qkQ|1? zfsu~{uXd2UG)+(MJ7Y=X-}m0kOaCt{zxA0HlobfoieQCJfE<%ek`dqP8@^=}DvqJ( zO0SnZ6?EUziv3;Fvdq%FQYbGj8YSoJIDU?eNe8;6WKkFFyRoj&f zkHJ|et|b<2<+G!)=j*$gN=$o>F#R>(gRO?OW|xVV*2HWSLj z!R;gI)wz3f?<`bD_Cln-b4Mnp&it}{t^Iv_Gt>V#HMWuIe>nKj(yx|2TE1`I)mP{5 z&Huy%Vl}p&8ryD8R^`7Wk5z~MrkwbB!+Q;z%9+Q>nGNO4gEJpq`{3G#*Y98c&CTB} zJzDx?d23{HV`OrD>W!@tef9F)%W$YVyq=u-w7KQajV;^p#yg|8NAJG+qlxW!@&Wzv zw|_YLBtcRgJN2ZorP{FfJTM1L|94j=M_#7?hACB`b_Qg6w`quqk7#-*8Gh8B1o&}0 zJ=7|FeDXNJPZ}hEzom6UO>4@O=AuC0A_OT_I|p@<2{{|u766`~(noLW?@ zpOc@Sm{*=y0%YfAmSpI=7o}D@_I|p@<2{{|u766{{awoLW?@ zpOc@Sm{*=y0%YfAmSpI=7o}D@ diff --git a/backend/translation/utils/__init__.py b/backend/translation/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/codex-build/__pycache__/api.cpython-314.pyc b/codex-build/__pycache__/api.cpython-314.pyc deleted file mode 100644 index 18b31129a249ef890a5d7f54d26c75d1102fa185..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15698 zcmb_@Yj7J^mR>iy@qX}4kd#O^DN!UOKJ>KSlKB*AQ8eA6Woa!Mh6sp+4FYg?Ln7gI zietNCdTJ|{lHHZO>y+e5wk%gAwQ?nsIH^iyB-_a7krCKSAJBG>z%e8VAcMeqyR%(@v+<0XZH!h4~gzKNtj`bf&H@HRHGPqUS%Esjxsvm678U`D+#z9Gw2Ai~| z!Dg*_aGSObV|9#1~gwXP_2H%r}u)U8FSdsu2c zQX7g=_p;PRq)J7p`&eodQk#oX_p{V(NZnqPdSJYLbo%l6%Pr#vUE&c&LEd4wQ3*Uq}Q(Tz4GpLwfCU*-Ziz4Y|{6> z@%`h0(JLE_YQKB{BYNT>P8B`!!J?TQ+oZKao3?g%ZENFx?1LlbZrR7nM=|1KG~xtr z&G`5xGd@1PPd>4Z&L@jTcVhgc{N$nY92XtrxUB{Ucv5?EyqlfQH;wrehZsw@+_U~< zX}#mSM&B+jXT;^+@m{km&cc(UKYqLnTI`ei$9v@g`INaA8o`OSv5&-!Z+CFfJv2J4 z1t)HJ!xp^bEk*r^qaz!YdVx2o*vQqRb*-hlrJo;OF=BB(&K(u%xi}vh%=|45u%|Z_ zjm=17N+hW!B3e9^l)6WUJjXYBJOdF`L$0L8lq>Oxn50cbG)awXs&px)U5&+(NQp_k z!$VSAN|7STS!r-=Y*d;^#A8VP?W68;QGS=zwbSDzh zrHqOfRh1H{$@qknh+m1RQamY5(15!-C24Fbu1c5ViI|j1CT5R#B&kE{i(RHlk`j9* z6H_&*3o$jFN~$qQor6LAjU+X+IE{u9YKYa7QyC4J#&9Evgeqw%HW?*8Ii*Q# ztQh^}Omc!~#Oe&Bl<5fO9#fQ*qGDW0HLhxyX=2vn=}$(}sd!RTJsswQN~ikAB&lmE z7D;GR(wb)&(_^!pn2JrjEdA}=pVB%=yCg;|R2_Z#?3mPLQY%(e*<8Dr6p^AzW>V5( z%Cyy4BomEmU9VEwYi6ZqGLb}Fo0V9e)HWSgL2A@1EQJcnUBYXW(Z}dJ8k7 z*$lpi>Ge`3p3pk5G-_lzjpc|&w1_q0G?rOiSMAv}YlilN5s?y+ zN302$S!!{&AatP)?FcLwZN{m{6{COJDF*36yzY%8lPQhuZgm9juBX$qijhRft~>La zTKCi9_s>kkSf1`N204KJrHd?dsAwXiqP~v?DCaj8-rYSBHFk!!6Lm&Y3P)4e30JaW zC&G}RyG`20^m3MO&MHh3)&oWz5I9W43psR0RKbo=^b)IJYzC87^;&H3XgI%QVY6Ax zM%63UWEsn+S53yWFiu1KN+f~38O|qH;#{N>S7Ko;6~2VghiQXkn+GD8FAil&ichCg zie{WPbN?TaTGZ_JmbOIfN-U9#Ovgf4l$N$aY#Qeks~A#4Hr>uj>5j|nm}K4T+;&FN z@oc@pbI!z~@rg)cJ@3grS=i8QJ5%$j6q!&`#0>Hq1qs+a6RAv6Q{u=bW-)23cxTqW zr*n7bZjjxs#Wk#-7yDyEi5m;+1X)$kYoeHYT1w+Nq|R~UoW?_Ow7oE(3EQDOj*GHw z2XbUVv&$mh?D;nbdvmZir|gn#vRk&x9@!y#yM!*ji`Sg8Pjkt_HZJ7XORbs727l58 zRa114Xg~6Mzqu=)N8-3H(o}V)F~wQE>?Jjo469%d(~)p&1`Dq{V>9f)>F)TYOj=1@ zjwN+lOx0~?&VR^-+`20qCfbF=x+fe4x633b?hA)s!THbUl!U{V9{u4bxMiN_M~RIMwQe{u?a2Ac0(829v#z>c(MZxX&uB=qFvZ7o#|O+3)*AB z03zy>2;Smy9{(Fdxw?kAzWI(tu_0IAG+ka*xHZlPOx2U0+lUU$i4Zx9tYu-6BL}JV54O| zNEsAdz(=KGSmu)QH%4+bEsJ7Hu0mQArCddDQ4Hp`wa%Tnc6?E6&F$=*8(FAqT@*WW z?YkGm+M6d9#oamo_GNLqu^FhvjW@#)

d&=(^2NbSALyb}XrR8o(v7q%PglP0C0= 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 Hj`;rtNhyWj diff --git a/docker-compose.yml b/docker-compose.yml index 46de4ca..f14eeca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,9 @@ services: - + # The Frontend Service frontend: build: context: ./frontend - containter_name: codex-frontend + container_name: codex-frontend ports: - "9000:9000" depends_on: @@ -12,9 +12,10 @@ services: - ./frontend:/app - /app/node_modules + # The API Service api: build: - context: ./backend/translation + context: ./backend/codex_build container_name: codex-api ports: - "8000:8000" @@ -26,10 +27,11 @@ services: neo4j: condition: service_healthy volumes: - - ./backend/translation:/app + - ./backend/codex_build:/app + # The Backend Service neo4j: - image: neo4j:5.18 + image: neo4j:5.18.0 container_name: codex-neo4j environment: NEO4J_AUTH: "neo4j/changeme" 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..d8d3bc7 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3061 @@ +{ + "name": "project-codex-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "project-codex-frontend", + "version": "0.0.0", + "dependencies": { + "i18next": "^25.10.9", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-i18next": "^16.6.6" + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "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==", + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/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==", + "dev": 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==", + "dev": true, + "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/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==", + "dev": true, + "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==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "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/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/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==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "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==", + "dev": 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==", + "dev": true, + "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..9bc8234 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,32 @@ +{ + "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": { + "i18next": "^25.10.9", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-i18next": "^16.6.6" + }, + "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..299c67d --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,510 @@ +.page-shell { + width: 100%; + margin: 0 auto; + background: #edeef0; + min-height: 100vh; +} + +.top-nav { + background: #4e7f77; + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px clamp(16px, 4vw, 56px); +} + +.top-nav__left, +.top-nav__right { + display: flex; + align-items: center; +} + +.top-nav__left { + gap: 26px; +} + +.brand { + text-decoration: none; + color: #fff; + background: #6b7b92; + border: 2px solid #d7e1e6; + width: 78px; + height: 56px; + display: grid; + place-items: center; + font-size: 12px; + letter-spacing: 0.35px; +} + +.main-links { + display: flex; + gap: 32px; +} + +.main-links a { + color: #ffffff; + text-decoration: none; + font-weight: 600; + font-size: 21px; +} + +.top-nav__right { + gap: 12px; +} + +.top-nav__right select { + width: 146px; + height: 34px; + border-radius: 8px; + border: none; + padding: 0 12px; + background: #f3f4f6; + font-size: 14px; + color: #253242; +} + +.icon-btn { + width: 28px; + height: 28px; + border: none; + background: transparent; + color: #f6f8fa; + display: grid; + place-items: center; + cursor: pointer; + padding: 0; +} + +.icon-btn svg { + width: 21px; + height: 21px; + fill: currentColor; +} + +.content { + width: min(1180px, calc(100% - 56px)); + margin: 0 auto; + padding: 30px 0 56px; +} + +.hero-row { + border-bottom: 1px solid #c7ccd2; + padding-bottom: 20px; + margin-bottom: 28px; + display: flex; + justify-content: space-between; + align-items: start; + gap: 16px; +} + +.hero-row h1 { + margin: 0 0 6px; + color: #101827; + font-size: clamp(2rem, 2.3vw, 2.75rem); + font-weight: 700; + letter-spacing: -0.2px; + line-height: 1.15; +} + +.hero-row p { + margin: 0; + color: #2f4057; + font-size: clamp(1rem, 1.1vw, 1.35rem); + line-height: 1.35; +} + +.help-btn { + border: none; + border-radius: 8px; + background: #4e7f77; + color: #fff; + font-weight: 700; + font-size: 17px; + width: 78px; + height: 42px; + cursor: pointer; +} + +.populate-btn { + border: none; + border-radius: 8px; + background: #4e7f77; + color: #fff; + font-weight: 700; + font-size: 17px; + width: 150px; + height: 50px; + cursor: pointer; + position: absolute; + top: 200px; + left: 75px; +} + +/* Customize the label (the container) */ +.container { + display: block; + position: relative; + padding-left: 35px; + margin-bottom: 12px; + cursor: pointer; + font-size: 22px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* Hide the browser's default checkbox */ +.container input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} + +/* Create a custom checkbox */ +.checkmark { + position: absolute; + top: 0; + left: 0; + height: 25px; + width: 25px; + background-color: #eee; +} + +/* On mouse-over, add a grey background color */ +.container:hover input ~ .checkmark { + background-color: #ccc; +} + +/* When the checkbox is checked, add a blue background */ +.container input:checked ~ .checkmark { + background-color: #2196F3; +} + +/* Create the checkmark/indicator (hidden when not checked) */ +.checkmark:after { + content: ""; + position: absolute; + display: none; +} + +/* Show the checkmark when checked */ +.container input:checked ~ .checkmark:after { + display: block; +} + +/* Style the checkmark/indicator */ +.container .checkmark:after { + left: 9px; + top: 5px; + width: 5px; + height: 10px; + border: solid white; + border-width: 0 3px 3px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} + +.populateDrugbank-checkbox { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + font-family: sans-serif; + user-select: none; + position: absolute; + top: 100px; + left: 100px; + font-size: 1.2rem; +} + +.populateSnomed-checkbox { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + font-family: sans-serif; + user-select: none; + position: absolute; + top: 125px; + left: 100px; + font-size: 1.2rem; +} + +.populateRxNorm-checkbox { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + font-family: sans-serif; + user-select: none; + position: absolute; + top: 150px; + left: 100px; + font-size: 1.2rem; +} + +.populateICD11-checkbox { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + font-family: sans-serif; + user-select: none; + position: absolute; + top: 175px; + left: 100px; + font-size: 1.2rem; +} + +.tool-panel { + border-bottom: 1px solid #c7ccd2; + padding: 4px 0 30px; +} + +.tool-panel + .tool-panel { + margin-top: 14px; + border-bottom: none; + padding-bottom: 0; +} + +.tool-panel h2 { + margin: 0 0 22px; + color: #0f1826; + text-align: center; + font-size: clamp(1.8rem, 2vw, 2.25rem); + font-weight: 700; +} + +.search-surface { + border: 1px solid #d7dbe0; + border-radius: 12px; + background: #f8fafc; + padding: 14px; +} + +.field-stack label { + display: block; + margin-bottom: 7px; + color: #384a60; + font-size: 1.12rem; + font-weight: 600; +} + +.field-stack select, +.search-row input, +.translate-grid input, +.results-select { + width: 100%; + height: 44px; + border: 1px solid #d7dbe0; + border-radius: 9px; + background: #f4f5f7; + padding: 0 12px; + color: #202e3d; + font-size: 1.02rem; + box-sizing: border-box; +} + +.search-grid { + display: grid; + grid-template-columns: minmax(200px, 250px) 1fr; + gap: 14px; + align-items: end; +} + +.search-row { + display: grid; + grid-template-columns: 1fr 170px; + gap: 10px; +} + +.search-row button, +.translate-btn { + height: 44px; + border: none; + border-radius: 9px; + background: #8e97a6; + color: #fff; + font-size: 1rem; + font-weight: 700; + cursor: pointer; +} + +.results-block { + margin-top: 18px; +} + +.message { + margin-bottom: 10px; + padding: 8px 10px; + border-radius: 8px; + font-size: 0.95rem; +} + +.message--error { + color: #9c2c2c; + background: #fdecec; + border: 1px solid #f6c8c8; +} + +.message--info { + color: #2f4d69; + background: #eaf2fb; + border: 1px solid #cfe1f4; +} + +.message--muted { + color: #667085; +} + +.results-block h3 { + margin: 0 0 10px; + text-align: center; + color: #111827; + font-size: clamp(1.45rem, 1.8vw, 2rem); +} + +.results-table-wrap { + width: 100%; + border: 1px solid #dce2e8; + border-radius: 10px; + background: #fff; + overflow-x: auto; +} + +.results-table { + width: 100%; + border-collapse: collapse; + min-width: 680px; +} + +.results-table th, +.results-table td { + padding: 10px 12px; + text-align: left; + border-bottom: 1px solid #e7ecf1; + vertical-align: top; +} + +.results-table th { + background: #eff4f8; + color: #223246; + font-size: 0.95rem; +} + +.results-table td { + color: #2e435a; +} + +.results-table tbody tr:nth-child(even) { + background: #fbfcfe; +} + +.results-table tbody tr.row-selected { + background: #e5f1ef; +} + +.results-table tbody tr:last-child td { + border-bottom: none; +} + +.results-select { + display: flex; + justify-content: space-between; + align-items: center; + border: 1px solid #e2e5e9; + color: #8f9bb0; +} + +.translate-grid { + display: grid; + grid-template-columns: minmax(200px, 250px) 170px 1fr; + gap: 10px; + align-items: end; +} + +.translate-grid input::placeholder, +.search-row input::placeholder { + color: #9ca6b8; +} + +button:focus-visible, +select:focus-visible, +input:focus-visible, +a:focus-visible { + outline: 3px solid #2a5e59; + outline-offset: 2px; +} + +@media (max-width: 1100px) { + .top-nav { + padding: 12px 16px; + } + + .main-links a { + font-size: 17px; + } + + .top-nav__left { + gap: 16px; + } + + .help-btn { + width: 72px; + height: 40px; + font-size: 15px; + } +} + +@media (max-width: 840px) { + .top-nav { + flex-direction: column; + align-items: stretch; + gap: 12px; + } + + .top-nav__right { + justify-content: flex-end; + } + + .hero-row { + flex-direction: column; + gap: 12px; + } + + .search-grid, + .translate-grid, + .search-row { + grid-template-columns: 1fr; + } + + .content { + width: calc(100% - 20px); + padding-top: 18px; + } + + .main-links { + gap: 18px; + } + + .top-nav__right { + gap: 8px; + } + + .hero-row h1 { + font-size: 1.9rem; + } + + .tool-panel h2 { + font-size: 1.6rem; + text-align: left; + } + + .results-block h3 { + text-align: left; + } +} \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..1bc88b3 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,596 @@ +import './App.css' +import { useTranslation } from 'react-i18next' +import { useEffect, useState, type KeyboardEvent } from 'react' + +interface LanguagesResponse { + languages: string[] +} + +interface SearchResultRow { + source_id: string | null + source_name: string | null + name: string + type: string + country: string | null + language: string + uploaded_at: string | null +} + +interface SearchResponse { + query: string + count: number + results: SearchResultRow[] +} + +interface TranslateResultRow { + source_id: string | null + source_name: string | null + name: string + type: string + country: string | null + language: string + uploaded_at: string | null +} + +interface TranslateResponse { + found: boolean + results: TranslateResultRow[] +} + +const API_BASE_URL = 'http://localhost:8000' +const FALLBACK_LANGUAGES = ['en', 'es', 'fr', 'de'] + +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 [translatedName, setTranslatedName] = useState('') + const [translateError, setTranslateError] = useState('') + const [searchError, setSearchError] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [isTranslating, setIsTranslating] = useState(false) + const [exportLanguage, setExportLanguage] = useState(availableLanguages[0]) + const [exportError, setExportError] = useState('') + const [isExporting, setIsExporting] = useState(false) + const [exportMessage, setExportMessage] = useState('') + + const getLanguageLabel = (code: string) => { + const raw = code.trim() + const normalized = raw.toLowerCase() + + // Backend may return either ISO codes (en) or full names (English). + if (normalized.length > 3) { + return raw + } + + try { + const displayNames = new Intl.DisplayNames([i18n.language], { type: 'language' }) + return displayNames.of(normalized) ?? normalized.toUpperCase() + } catch { + return normalized.toUpperCase() + } + } + + useEffect(() => { + let isActive = true + + const loadLanguages = async () => { + 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 + const nextLanguages = Array.isArray(data.languages) && data.languages.length > 0 ? data.languages : FALLBACK_LANGUAGES + if (!isActive) { + return + } + + setAvailableLanguages(nextLanguages) + setTargetLanguage((current) => (nextLanguages.includes(current) ? current : nextLanguages[0])) + setExportLanguage((current) => (nextLanguages.includes(current) ? current : nextLanguages[0])) + } catch { + if (!isActive) { + return + } + + setAvailableLanguages(FALLBACK_LANGUAGES) + setTargetLanguage((current) => (FALLBACK_LANGUAGES.includes(current) ? current : FALLBACK_LANGUAGES[0])) + } + } + + void loadLanguages() + + return () => { + isActive = false + } + }, []) + + const extractTranslatedName = (rows: TranslateResultRow[]) => { + const names = rows + .map((row) => row.name) + .filter((name): name is string => Boolean(name && name.trim())) + + if (names.length === 0) { + return '-' + } + + return Array.from(new Set(names)).join(', ') + } + + const handleSearch = async () => { + if (!searchQuery.trim()) { + setSearchError('Please enter a search term') + return + } + + setIsLoading(true) + setSearchError('') + setTranslateError('') + setSearchResults([]) + setSelectedResult(null) + setTranslatedName('') + + try { + const response = await fetch(`${API_BASE_URL}/search`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: searchQuery, + 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 + const rows = Array.isArray(data.results) ? data.results : [] + const filteredRows = + searchLanguage === 'all' + ? rows + : rows.filter((row) => row.language.toLowerCase() === searchLanguage.toLowerCase()) + + setSearchResults(filteredRows) + + if (filteredRows.length === 0) { + setSearchError('No results found') + } + } catch (err) { + setSearchError(err instanceof Error ? err.message : 'An error occurred during search') + setSearchResults([]) + } finally { + setIsLoading(false) + } + } + + const handleKeyPress = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + void handleSearch() + } + } + + const handleTranslateSelected = async () => { + if (!selectedResult) { + setTranslateError('Select a search result first') + return + } + + setIsTranslating(true) + setTranslateError('') + setTranslatedName('') + + try { + const response = await fetch(`${API_BASE_URL}/translate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + term: selectedResult.name, + source_lang: selectedResult.language, + target_lang: targetLanguage, + }), + }) + + if (!response.ok) { + const errorBody = await response.json().catch(() => null) + throw new Error(errorBody?.detail ?? 'Translation request failed') + } + + const payload = (await response.json()) as TranslateResponse + const name = payload.found ? extractTranslatedName(payload.results ?? []) : '-' + setTranslatedName(name) + if (!payload.found || name === '-') { + setTranslateError('No translation found for the selected language') + } + } catch (err) { + setTranslateError(err instanceof Error ? err.message : 'Translation failed') + } finally { + setIsTranslating(false) + } + } + + const rowsToCsv = (rows: any[]) => { + if (!rows || rows.length === 0) return '' + const headerOrder = ['concept_id', 'source_id', 'source_name', 'name', 'type', 'country', 'language', 'uploaded_at'] + const keys = Array.from(new Set([...headerOrder, ...Object.keys(rows[0] || {})])) + const escapeVal = (v: any) => { + if (v === null || v === undefined) return '' + const s = String(v) + if (s.includes('"') || s.includes(',') || s.includes('\n')) { + return `"${s.replace(/"/g, '""')}"` + } + return s + } + const lines = [keys.join(',')] + rows.forEach((r) => { + lines.push(keys.map((k) => escapeVal((r as any)[k])).join(',')) + }) + return lines.join('\n') + } + + const downloadBlob = (filename: string, text: string) => { + const blob = new Blob([text], { type: 'text/csv;charset=utf-8;' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + a.remove() + URL.revokeObjectURL(url) + } + + const fetchAndDownload = async (urlSuffix: string, filename: string) => { + setIsExporting(true) + setExportError('') + setExportMessage('') + try { + const res = await fetch(`${API_BASE_URL}${urlSuffix}`) + if (!res.ok) { + const err = await res.json().catch(() => null) + throw new Error(err?.detail ?? 'Export failed') + } + const data = await res.json() + const rows = Array.isArray(data.rows) ? data.rows : data.results ?? [] + const csv = rowsToCsv(rows) + if (!csv) throw new Error('No rows to export') + downloadBlob(filename, csv) + setExportMessage(`Downloaded ${rows.length} rows`) + } catch (err) { + setExportError(err instanceof Error ? err.message : 'Export failed') + } finally { + setIsExporting(false) + } + } + const downloadByLanguage = () => { + if (!exportLanguage.trim()) { + setExportError('Choose a language') + return + } + fetchAndDownload(`/csv/language/${encodeURIComponent(exportLanguage.trim())}`, `codex_language_${exportLanguage.trim()}.csv`) + } + + const languages = availableLanguages.map((code) => ({ code, label: getLanguageLabel(code) })) + + const [isDrugbankSelected, setIsDrugbankSelected] = useState(false); + const [isSnomedSelected, setIsSnomedSelected] = useState(false); + const [isRxNormSelected, setIsRxNormSelected] = useState(false); + const [isICD11Selected, setIsICD11Selected] = useState(false); + + const handlePopulateClick = async () => { + let sources: string[] = [] + if (isDrugbankSelected) { + sources.push("drugbank") + } + if (isSnomedSelected) { + sources.push("snomed") + } + if (isRxNormSelected) { + sources.push("rxnorm") + } + if (isICD11Selected) { + sources.push("icd11") + } + try{ + const response = await fetch('http://localhost:8000/api/populate-sources', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ selectedSources: sources }), + }); + + const data = await response.json(); + if (data.status === "success") { + alert(data.message); + } + } catch (error) { + console.error("Error calling Python function:", error); + alert("Something went wrong on the backend."); + } + }; + + return ( +
+ ) +} + +export default App \ No newline at end of file diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..cc51a3d20ad4bc961b596a6adfd686685cd84bb0 GIT binary patch literal 44919 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 JPDHLkV1myZcL)Fg literal 0 HcmV?d00001 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/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..3702330 --- /dev/null +++ b/frontend/src/i18n/locales/de.json @@ -0,0 +1,24 @@ +{ + "nav": { + "home": "Startseite", + "about": "Über uns" + }, + "common": { + "help": "Hilfe", + "search": "Suchen", + "translate": "Übersetzen" + }, + "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...", + "targetLanguage": "Zielsprache", + "targetPlaceholder": "Zielsprache", + "translationPlaceholder": "Die Übersetzung wird hier angezeigt" + } +} diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json new file mode 100644 index 0000000..40f0023 --- /dev/null +++ b/frontend/src/i18n/locales/en.json @@ -0,0 +1,24 @@ +{ + "nav": { + "home": "Home", + "about": "About" + }, + "common": { + "help": "Help", + "search": "Search", + "translate": "Translate" + }, + "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...", + "targetLanguage": "Target Language", + "targetPlaceholder": "Target Language", + "translationPlaceholder": "Translation will appear here" + } +} diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json new file mode 100644 index 0000000..c380342 --- /dev/null +++ b/frontend/src/i18n/locales/es.json @@ -0,0 +1,24 @@ +{ + "nav": { + "home": "Inicio", + "about": "Acerca de" + }, + "common": { + "help": "Ayuda", + "search": "Buscar", + "translate": "Traducir" + }, + "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...", + "targetLanguage": "Idioma de destino", + "targetPlaceholder": "Idioma de destino", + "translationPlaceholder": "La traducción aparecerá aquí" + } +} diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json new file mode 100644 index 0000000..6cd2fc9 --- /dev/null +++ b/frontend/src/i18n/locales/fr.json @@ -0,0 +1,24 @@ +{ + "nav": { + "home": "Accueil", + "about": "À propos" + }, + "common": { + "help": "Aide", + "search": "Rechercher", + "translate": "Traduire" + }, + "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...", + "targetLanguage": "Langue cible", + "targetPlaceholder": "Langue cible", + "translationPlaceholder": "La traduction s'affichera ici" + } +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..af968bf --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,17 @@ +@import url('https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;600;700&display=swap'); + +* { + box-sizing: border-box; +} + +html, +body, +#root { + margin: 0; + min-height: 100%; +} + +body { + background: #d9dde2; + font-family: 'Source Sans 3', 'Trebuchet MS', sans-serif; +} 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/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..aab5dff --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + host: true, + port: 9000, + } +}) 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 From fa0ecfa7b29a874b8cdc3f10d561c90d88ae0951 Mon Sep 17 00:00:00 2001 From: Logan Watersmith Date: Wed, 29 Jul 2026 12:22:19 -0700 Subject: [PATCH 06/23] Checkbox to load local sources and merge into database Generate concept IDs for DrugBank and RxNorm --- backend/codex_build/Dockerfile | 1 + .../__pycache__/api.cpython-310.pyc | Bin 10635 -> 11323 bytes .../__pycache__/source_data.cpython-310.pyc | Bin 19893 -> 20200 bytes .../codex_build/neo4j_sources/source_data.py | 51 ++++++--- backend/codex_build/requirements.txt | 2 +- docker-compose.yml | 2 + frontend/src/App.css | 100 +++++++++++++----- frontend/src/App.tsx | 18 +++- frontend/src/i18n/locales/de.json | 5 + frontend/src/i18n/locales/en.json | 5 + frontend/src/i18n/locales/es.json | 5 + frontend/src/i18n/locales/fr.json | 5 + 12 files changed, 148 insertions(+), 46 deletions(-) diff --git a/backend/codex_build/Dockerfile b/backend/codex_build/Dockerfile index 25b6ddf..5ad0748 100644 --- a/backend/codex_build/Dockerfile +++ b/backend/codex_build/Dockerfile @@ -4,6 +4,7 @@ WORKDIR /app COPY requirements.txt ./ RUN pip3 install --no-cache-dir -r requirements.txt +RUN pip3 install requests COPY . . diff --git a/backend/codex_build/__pycache__/api.cpython-310.pyc b/backend/codex_build/__pycache__/api.cpython-310.pyc index b47ca5eb06c135702da4b39a54b928be2b91fa8a..629609b671c7c5971a2b248b2e6f1c4324898da2 100644 GIT binary patch delta 3850 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 J7fh62{|~ME%Par@ delta 5674 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 z z=1_P4%0ckJV}r*S<~40E*X(~zdX7Mi~yS-Ee$GBq# z4tqGd+rm zVY*j6v^VcD{n%y=l8A*^UZs5n3qU6QuiAi2it~$tN;*}0fbcs^C^KjR?-JI z`jp`uPeGPLq4r|g+&hK$IdZPBc0;g|kixIJfXw`2ID3K2yg+7t4;_Y)_~|}ivk_#o zwwHg8%|=f|y%Qfk5qo6f(8&W6M@ILL9y`Q*{EO{_{?EZyM;#L%K0H2g;1DZ?5)j_6 z+7I-juU#XMG$V^$lPx*Y%`RXyR&1~UNS12|6N^M>yEFH+is~N%z;)tBC~qlH%C^OzARNgZ(kK zy_W^q4^Ss^CfLPvhRr&_WsHyV+xt=qxt7TA_QPrEB5*TXbq4S9ul{TJHxH+FJpxm_B*^|Up(*&Y_DuY}=l zfKxjT*99Ne{TzGPEJre(NaY~&0k6%%zWE6dc4?m57gsYCLy0k4!qfSg!V%UwX zWBs=g77(rjfbkeONz^GWe=(SH)M4Z%O{VMYJJ{$o0E@)7B{Q~ygPg|Ne?kx;Z3n;X z={ZCoqEozjG)vxJUm5Kn@g%J>)m*qet!#^&hd_O+GpnCM!HO77Su)v`~5kvOmLwA6!BwMP3_WxA+&v z0_1)Eow3D7w}v6ILOa?JpLU~!c@!%~*c#*z7FQ6^KD&kRKEiJhcJWKcx(3lgv?y^# zPn?Zy++Cxxktp!r{qr$0IhN2xYWZXSt7Btb{|UV3Q}6mbX?!NMf6{q1g=0rSxHNAjv>NRAf!TdsE>bQ({fV)Yv5};(UOULG}<&C+5m> lvFvp#Ajw4iol7Z}HJj$q+}bHEp$%(KXq{Rc9w*B0{uh^@P3iyu diff --git a/backend/codex_build/neo4j_sources/source_data.py b/backend/codex_build/neo4j_sources/source_data.py index 27d6bfa..b4428ce 100644 --- a/backend/codex_build/neo4j_sources/source_data.py +++ b/backend/codex_build/neo4j_sources/source_data.py @@ -8,20 +8,16 @@ import sys import time import uuid -import json +import re import logging from datetime import datetime, timezone from collections import deque import requests from neo4j import GraphDatabase, WRITE_ACCESS -from dotenv import load_dotenv, find_dotenv +from dotenv import load_dotenv -env_path = find_dotenv(usecwd=True) -if not env_path: - print("ERROR: .env not found at repo root") - sys.exit(1) -load_dotenv(env_path) +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") @@ -69,6 +65,24 @@ def smoke_test(sess): sess.run("MATCH (s:SmokeTest) DELETE s") return c +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) + + if symptom: + clean_symptom = re.sub(r'[^a-z0-9-]', '', source.lower().strip()) + text = f"{text}-{clean_symptom}" + + if source: + clean_source = re.sub(r'[^a-z0-9-]', '', source.lower().strip()) + text = f"{text}-{clean_source}" + + return text + # -------------------- # DrugBank @@ -125,7 +139,7 @@ def iter_drugs(per_page=100): 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 dbid, name + if dbid: yield d, dbid, name link = resp.headers.get("Link", "") if link and 'rel="next"' in link: page += 1 @@ -149,10 +163,15 @@ def iter_drugs(per_page=100): 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) - for code, title in iter_drugs(): - sess.run("MERGE (n:DRUG {id: $id}) ON CREATE SET n.code=$id, n.title=$t, n.ds=$ds ON MATCH SET n.ds=$ds", id=code, 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) - processed += 1 + for d, code, title in iter_drugs(): + 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) 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"]) @@ -218,8 +237,6 @@ def upsert_snomed_node(node): time.sleep(5.0) else: log.error("Failed all retries to connect to SNOMED server. Skipping SNOMED module completely.") - sess.run("MERGE (n:SNOMED {id: $id}) ON CREATE SET n.code=$id, n.title='SNOMED', n.ds=$ds ON MATCH SET n.ds=$ds", id=SNOMED_ROOT_ID, ds=DATASET) - sess.run("MATCH (i:Ingest {uid: $uid}), (r:SNOMED {id: $rid}) MERGE (i)-[:ROOT]->(r)", uid=run_uid, rid=SNOMED_ROOT_ID) return # Safely exits run_snomed() so main() moves to RxNorm # If it passed but root_full is somehow empty, exit gracefully @@ -296,7 +313,8 @@ def rx_get(path, params=None): 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: - sess.run("MERGE (n:RXN {rxcui: $rxcui}) ON CREATE SET n.name=$name, n.tty=$tty, n.ds=$ds", rxcui=m["rxcui"], name=m["name"], tty=m["tty"], ds=DATASET) + 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): @@ -311,7 +329,8 @@ def rx_get(path, params=None): if isinstance(props, dict): props = [props] for p in props: if p.get("rxcui"): - sess.run("MERGE (n:RXN {rxcui: $rxcui}) ON CREATE SET n.name=$name, n.tty=$tty, n.ds=$ds ON MATCH SET n.ds=$ds", rxcui=p["rxcui"], name=p["name"], tty=p["tty"], ds=DATASET) + 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) diff --git a/backend/codex_build/requirements.txt b/backend/codex_build/requirements.txt index d396315..9f3ea69 100644 --- a/backend/codex_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/docker-compose.yml b/docker-compose.yml index f14eeca..33d791a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,8 @@ services: container_name: codex-api ports: - "8000:8000" + env_file: + - .env environment: - NEO4J_URI=bolt://neo4j:7687 - NEO4J_USER=neo4j diff --git a/frontend/src/App.css b/frontend/src/App.css index 299c67d..af8d63f 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -125,21 +125,6 @@ cursor: pointer; } -.populate-btn { - border: none; - border-radius: 8px; - background: #4e7f77; - color: #fff; - font-weight: 700; - font-size: 17px; - width: 150px; - height: 50px; - cursor: pointer; - position: absolute; - top: 200px; - left: 75px; -} - /* Customize the label (the container) */ .container { display: block; @@ -208,16 +193,26 @@ transform: rotate(45deg); } +.populate-label { + display: flex; + align-items: center; + font-size: 1.5rem; + font-weight: 700; + color: #0f1826; + position: absolute; + top: 265px; + left: 32px; +} + .populateDrugbank-checkbox { display: flex; align-items: center; gap: 12px; cursor: pointer; - font-family: sans-serif; user-select: none; position: absolute; - top: 100px; - left: 100px; + top: 320px; + left: 90px; font-size: 1.2rem; } @@ -226,11 +221,10 @@ align-items: center; gap: 12px; cursor: pointer; - font-family: sans-serif; user-select: none; position: absolute; - top: 125px; - left: 100px; + top: 345px; + left: 90px; font-size: 1.2rem; } @@ -239,11 +233,10 @@ align-items: center; gap: 12px; cursor: pointer; - font-family: sans-serif; user-select: none; position: absolute; - top: 150px; - left: 100px; + top: 370px; + left: 90px; font-size: 1.2rem; } @@ -252,14 +245,67 @@ align-items: center; gap: 12px; cursor: pointer; - font-family: sans-serif; user-select: none; position: absolute; - top: 175px; - left: 100px; + top: 395px; + left: 90px; font-size: 1.2rem; } + +.populate-btn { + border: none; + border-radius: 8px; + background: #4e7f77; + color: #fff; + font-weight: 700; + font-size: 17px; + width: 150px; + height: 50px; + cursor: pointer; + position: absolute; + top: 425px; + left: 75px; +} + +.populate-btn:disabled { + cursor: not-allowed; + opacity: 0.6; +} + +.populate-overlay { + position: absolute; + top: 425px; + left: 75px; + width: 150px; + height: 50px; + background-color: rgba(255, 255, 255, 0.85); + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + z-index: 10; + border-radius: 8px; /* Match your button's corners if needed */ + font-size: 17px; + color: #1e293b; + font-weight: 700; +} + +.spinner { + width: 16px; + height: 16px; + border: 2px solid #cbd5e1; + border-top-color: #2563eb; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + .tool-panel { border-bottom: 1px solid #c7ccd2; padding: 4px 0 30px; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1bc88b3..177062a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -287,6 +287,7 @@ function App() { const [isSnomedSelected, setIsSnomedSelected] = useState(false); const [isRxNormSelected, setIsRxNormSelected] = useState(false); const [isICD11Selected, setIsICD11Selected] = useState(false); + const [isPopulationLoading, setIsPopulationLoading] = useState(false); const handlePopulateClick = async () => { let sources: string[] = [] @@ -302,6 +303,7 @@ function App() { if (isICD11Selected) { sources.push("icd11") } + setIsPopulationLoading(true); try{ const response = await fetch('http://localhost:8000/api/populate-sources', { method: 'POST', @@ -316,6 +318,8 @@ function App() { } catch (error) { console.error("Error calling Python function:", error); alert("Something went wrong on the backend."); + } finally { + setIsPopulationLoading(false); } }; @@ -374,6 +378,8 @@ function App() {
+

{t('sides.populateLabel')}

+
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 3702330..bf08427 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -8,6 +8,11 @@ "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.", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 40f0023..f0bd529 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -8,6 +8,11 @@ "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.", diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index c380342..2c262ec 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -8,6 +8,11 @@ "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.", diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 6cd2fc9..879a85b 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -8,6 +8,11 @@ "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.", From 3d9400f9a24b0ab78d68f9599f0525590daf107c Mon Sep 17 00:00:00 2001 From: Logan Watersmith Date: Wed, 29 Jul 2026 12:30:18 -0700 Subject: [PATCH 07/23] Updated the README --- README.md | 72 ++---------------- .../__pycache__/source_data.cpython-310.pyc | Bin 20200 -> 20358 bytes 2 files changed, 7 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index ab5f798..5bee07d 100644 --- a/README.md +++ b/README.md @@ -8,78 +8,20 @@ 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 ~20 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 ``` --- @@ -96,7 +38,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` --- @@ -105,5 +47,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/backend/codex_build/neo4j_sources/__pycache__/source_data.cpython-310.pyc b/backend/codex_build/neo4j_sources/__pycache__/source_data.cpython-310.pyc index 16aac384390ddce2718d0b08903dfb6a3a8bd4e0..176248813f1bb5e08411fbf7911a1cd91249b1c3 100644 GIT binary patch delta 3586 zcmZ{ndvH`&9mn_VBo2CIs>-6Y?*dgHSe%RBTk|L4M&HF#?$&Q;a1-bU_G zm-1q~OYqK9m+q4JQeJw%;jiFjy!?R0U&$+YB|g2}%a`GE8L#5i_^jeJ+=tI^R`IEXZQ9PA+@a6UXTOur4u6;XiX37k zRwr5rs}O#7Fsufm%AJmY-&NDP82xr;XK8q)<7KyI&!gCg$w7v=`^!Ls=9W6IEwREy z*KXDbD^iMC6Zli|+4C@#^3tSlhiyWha80D>@^luj7_iJ(CsOs)^_HEUi8MV;dv)ZJ z?$%TEG+xQQQ??1WE)BW(GTlYq_#e1MxAUqg2eX*|YTc^4c+HfJSw=36ylDD-n4@sY zI-NRWnMgN1wYoF0V82CA=XEK0^r+9X;0p9rzVGiITy7jJIqv3Qp!FQ#r_tQ{nG z5gSaFyI)~!EnG`)V_TstV+Gp=eHqO;+i{SybLd0MD~pH7(E~4JG_Y=XJHx|((J@+QHL!Do%*3%VLDP zb$QGpM&Rm#2G#@4tQPFY`mBv*gVa(5sM|-xgp;XiDt-DRevBle2R%xP@FxHPIZ{dFjzw9$p8dXMd6bX8>2V-0ndnxsCgg@<0`${wr zQQ<$ic{O8{wu#VA=q8v$?xkYjY;vr=~OS zuzYv#2zjJHJg!E$H&+~?Twf;~B^)En5GE0RX-v7dVZ@ioeTLjFlDD#JG#vIjg^J6l zYN8Krlov{;DbEv-Sy9xWkohcu7R+lGK=#}+l6+_PnxsWz5tY9Lfr{hoQu4!!H*M@& z@MhJ{DhlMMtcKNBC(K1Km)-;^)ktXxhqOd67KyJef`WYIt zTl`hBeh$B>UYPP4njVbJ;+OE(>NBY3=W0ruNW8erW~oQWb_fqX=4p6NtadR?Nj>n- zn(~~F2vicEBYl=Yy|0En28z#rSA`dRb!-A|`f8+;RQlVnwDuG#>2hrm8-g3PN?C;5 zzead%;y26!;MV+J|DH=&Yov z4~9#Pjz&}wNT`M_tVZX_zLAn0TX? zHtc~>j6x&;e_9|IR5eXW#D=lMTi{IdKPEdU!8+C;j-!DB@5G1bLX4-beW<-=n<}(W zEGkZrn-pb+L$Q^tZG_JtyeYy%_FQx{la`b3p2h@)W3hv<9pRPCBeZZn0lLvVKyR zhs|(?qQUSeS4lr{T(`eK!J7S(A1CZDP=O`$BCsjnA5xo{gi?ZsK+A%MQ4G(jxz#Wy z#+;)6yC2+Cm?H9|xPLDQibmo`gqI1g5X?EJnHE1rc=Kn4|28?U5#AvH!tX%+DMH*N zTQ;LGE{We69Bimp@FUNft4#L1;5Mu zu2E>nPHnV5Ayiddq)ev~22lQf*XjAW&0r9k(YM~ z9peky@kG-Sq3D38r+Z7+hV?#i6;5>Iuv_qhj(r==Ci1By{Uw;4PWnC&EfOg~x*EGrx8IO;wwHGnKIc;$2?W3rD*=(ljN#4i~#} z*wgSv*UFvdmM=%I;lg92Gd6(7`0QzK%8zDEn4!WPgBjmHb|93G`X`F1E_*->jEN5+ d-rb*FAjvuM4mlvNksp<7@I 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 WWuCH2@hMHpUd63cWRi4Z`|Q7;4^EH( From 1b0964aba3fba02eee26b8dd9fe0f1b538dc9744 Mon Sep 17 00:00:00 2001 From: Logan Watersmith Date: Wed, 29 Jul 2026 12:31:02 -0700 Subject: [PATCH 08/23] Removed "update me" line from top --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 5bee07d..5148cf7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -# UPDATE THIS # codex-cli — Medical Translation Terminal Test the backend translation engine directly from your terminal. From 3a6be03e3823fd75106ea381949cb04c7c5896de Mon Sep 17 00:00:00 2001 From: Logan Watersmith Date: Wed, 29 Jul 2026 12:50:43 -0700 Subject: [PATCH 09/23] Added printouts for each database source (maxed at 10 entries per) --- .../__pycache__/source_data.cpython-310.pyc | Bin 20358 -> 21837 bytes .../codex_build/neo4j_sources/source_data.py | 40 ++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/backend/codex_build/neo4j_sources/__pycache__/source_data.cpython-310.pyc b/backend/codex_build/neo4j_sources/__pycache__/source_data.cpython-310.pyc index 176248813f1bb5e08411fbf7911a1cd91249b1c3..7835474708c4404c4c8ddd5e7a94ffd967590f64 100644 GIT binary patch delta 1554 zcmah}%WoS+7@wJ4+v`W1NAo7YRFozTjT;vN!ipl?tQ(TlcCj6mrU{ml*#yV$StljS zt_-9gI7Fpc2@WVC;lP0d63Sn|5hOr8A+3bOg*)d$8@}C|0;y1UG~dkrzQ_FLn{VcW z&+!jm;b66`&4=JKu5E1mvUWQ-gundexp9mq%q2WG&LZr>O|%$R&a>x>gx^uf#yER{ zk@v{}T8t=RC9;DtVkG1A(2GYu;<^W0N3VEihGF<{yfD!8Lv(wIY3S+@MyP=fFvEx7 zxDIY#U<2!DgERyt?BZKBgXmaB%Zec|Q5QZ#%(X8VqD~G(T|C5wOBa|MsEjyKJa!a7 zBtRk#;%y;5K%zF%Wd0Tu0BG7VYO@g*G~BuimO5j2SUb!;tYZKf5{#WNdRfXyX<)N8QrbkaJ2mn@qv8-@Cj7!x{4SQq3sA5 zRip?~-@5@0cDn}MaT#3DV_ourj;fzeoo zR;odkJ zMt&u)Dm1OERykAYT9s3oZ?0d=SBexY_#d|Mng2J=RH|i#(Xl4j{`nZ&^jpa)FFRrH zJ;oH$Mk5j1^itOHw03X#TAPKvYUCVUg~I*&Ya#)rZs-~4Dh08QbhI2;A_lhW7m5Pu z1SH-UZ<9J)j}Xy>C>_3whj|~EhXno|3>Ge0c*{W^h<*V1_aN@Waw>rdMcOkpUZ^y3 ztd&1C{KjOdx|%PkQ{l-{L2ay7%e5)ZH7iHybA!knC! z@+(S-MzwA_k|`EyHApF(M7&&UY=^?yo>O82zIalmsY?$lkEY`>Ikk{&6`+SlY^t|x zv1^NfEnc$4&$cLn2>JOQ42yv+V7roYmR0%709h{Q$8yy-6~1kGxT0>B8tS6|&Aggx z5w_e9Cj7qb;D)k;lUVG4qABN_jfTSPN(AUc_N?|3+4|MaWE5ifup|VDgb6h1*yoZ6 zAo!myf=&cP!u@0ri=+#acgSx;$HAw#1Tp?y0Os|m*X_J#+&rG06yDXA&8fMsM;jeF zu^+^%sPd$U8lBspVq~~5(mLUecd?-EIwWx4PaSld!THnvomse z6|j#0<9@uCyw_*b=C%tzZRb$T^Ytwr#OCi}ula1M9bYjgQghz3(r6F|uz3G3ohFFc delta 291 zcmX@Rim`1zBVRr*FBbz4Eb!0Dx{)xEPlj>cMr{K|M&rpbj9wb4dMSq4Ohso?nX>d# zj8d3dnIsva3{rJd4VoDl8B&;n!C2E|^EJjocE(?uJviOffz$+pOi4aAps66Eoq@PG zYw`wv8|K=o$jR^h*C~ZFbaGcv;N%BE8Eg+2frdIw_6yc$;$fcL7%Zvot(r`Jewv22*yH0<@{@sLoj|doKMa!(1pgHRDKAn43fy8zEJ`mc Z`p+==N{A|>-{fB*UhEotj694i{{TtyQIh}w diff --git a/backend/codex_build/neo4j_sources/source_data.py b/backend/codex_build/neo4j_sources/source_data.py index b4428ce..4e60995 100644 --- a/backend/codex_build/neo4j_sources/source_data.py +++ b/backend/codex_build/neo4j_sources/source_data.py @@ -477,6 +477,44 @@ def unify_graph(driver): log.info("<<< Graph combination complete. All sources are now connected.") +def print_databases(driver): + log.info("\n" + "="*60 + "\IMPORTED DATABASES\n" + "="*60) + + # We query for distinct source labels in your system + 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 + LIMIT 10 + """ + results = sess.run(query) + + if label == "DRUG": + log.info(f"\n--- Database Records for DrugBank ---") + if label == "SNOMED": + log.info(f"\n--- Database Records for Snomed ---") + if label == "RXN": + log.info(f"\n--- Database Records for RxNorm ---") + if label == "ICD": + log.info(f"\n--- Database Records for ICD 11 ---") + + 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()]) + log.info(f"ID: [{node_id}] -> {{ {formatted_fields} }}") + + if not has_records: + log.info(f"No records found for label :{label} (Skipped or Empty).") + + log.info("\n" + "="*60 + "\nEND OF DATABASE READOUT\n" + "="*60) + def source_data(sources): log.info("=" * 60) log.info("Creating Singular Medical Knowledge Graph") @@ -507,6 +545,8 @@ def source_data(sources): # 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) sys.exit(2) From e348547197985b062d5e0751d56dcbb264837d01 Mon Sep 17 00:00:00 2001 From: Logan Watersmith Date: Wed, 29 Jul 2026 13:45:27 -0700 Subject: [PATCH 10/23] Trying to fix the search functionality, still needs work --- README.md | 3 -- .../__pycache__/api.cpython-310.pyc | Bin 11323 -> 12129 bytes backend/codex_build/api.py | 36 ++++++++++++++++-- frontend/src/App.tsx | 19 +++++---- 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 5148cf7..620bc58 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ # 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) diff --git a/backend/codex_build/__pycache__/api.cpython-310.pyc b/backend/codex_build/__pycache__/api.cpython-310.pyc index 629609b671c7c5971a2b248b2e6f1c4324898da2..7a5ff08be08398c6b9c5257312f51e14905a999e 100644 GIT binary patch delta 4345 zcmaJ^U2Igx6~1%#?vHoZ>$S1L|KP;fzBV?-_=gZfFc<>C7%&jv64+etjA6~*-R<1F zICdaQVwxst+$NePsS>HN+o}-{sZ#l|k=mz5eXA1nWgaS}FI81ls+5-|QktGK*FOo` z;MIJ0&N(w@&YU^t%=q`0KEIa8S5`(f_?w)bo&C@5Hxtd|+M7LNB`0b{otPCP8YMKC zjXRZArIWA{PL);VB(0=VZB;upR*kd8+M>pUvb9c~Rj2yl>{e%+wN3RS*?OnJYEb=( zY@^d;HK~3y+w8PhEkt8YEHK-;yj|fA*0w@uY^9_x5~3|M!{YJmBhI7NqfWck?(DR7 zs>zkv4yV)Vbh@lA$FxkR+v;|DtR82VwToyMwOos#vEKQ2iDvbImTDGS-c1v%Z$*dY zjOBh>wF25NYL_({u==j_<%0fqj?8H+G`oj|O0xqrNvoG|LTGs}tyv+~AWP6Kw3gP< ztxExGhz-pSvpTx%I-&Kn0T5f`Qjlm*YP5+q-_vL_Ygi0eBW!4SA8mnsP1Egk$30__ zSdYP&Hu?yRc|?ub5A>t79q4vNmmUYkPTB#C4#gM+x|4PR-KFRQK%2B1=x#+H1iFXr z0(zICp8&d-_5t0e=tDs7ru{(oD|!s*J#+x*0Yx7MdM_OWdJyT7bp-Y|PKVeLI!s4k zXZw~6`WW4RPXp_Zf@~jsob9KhAUUuUplN!Dj;fs1=u=91T+Kbn4zp6t>U86WMlNwhWVGNrL;`2Q}oJB{SCrDg!l?%C#^|#ngy26&}Zn?6^K0R zY!@3`o}$;-Srsyqv*&1vKFiL+N!v>Xgka#DYUa%9RN!m9(~yC&+`Kf#Gigs66B*Z& zk<$e)lh4^%Gc3d7tiW>A71N=Cune9pGCsfhv(P{F?%j}i9O0|jZo5!`mgy_Z^DazC zXkQorQ7uk*m-wLK<)Ju`GVCzdwXZW%=O)m+1_1g4O6KN^Jk4CC(}#XB9qk~)qN=Ke z9238bZxw&5ij)WoP-7I58@|D{Z)oisUi(JiTX9Y^qcU-h**tw?in)b+&Sf&-dR$ic zYo#(&#@4zy+hNl1(VH(Y8BXVmIgiiFNY>6>FM`!FRxD)mV>X;s%JwFCH*R6W4QnPN zZv@~OXl@c9szu1=5BxDAUX9Lm@O>bYhR3)w`Bmk*$FG2OSEYd~@&hnHbjJotW6nauUF_8o!=44RAnNBiRNGjyHcFYz@;c^aDk zCzj($@vqqNE6U=S$L*Y(wJ}2YK^Vag1IXYsu&FZeA&xl-LkrMctlR630hgS_BNiXU zy=t{aMcU5gbD6ZA<%h*k<;SDtHhdmb@Cyh}Ba}M_jKew6n`jb=s#Y7#1FQ#cdPj$a0Nh<~nt z_!VehY-$4UTv6Wnq-r9jw5f17En1V)gWns0S3${7pt+cK>yE~A=2t;kv(7ukr^)`V z@11YMw6~%0JqR|=nc$t!Hw{^J%1`3;lv0t*2SNCdonbh=i@KD;G$q*1q;4_pLXbT~DTOJ&z`2-goUZPxIZ8;gcwtL_jZ%p*&SwD3DBmjn zxMlA+&X55pSG*0`hK#5MQ~8_RK@miNTg-Ygz@AHgE2R3B>;|*5-i_agky^8N4mb

m8kP>_gcp<8L;oE^lxDS@} zmk5OsF9h&C;60LTjPB2#w>uRx|cY%GqZ z;k!poI<09ZG=2*QY$h>!oIeNfF*LUWKo3Ppk~Du~+)e1%i`>=!ryG9FTF20rg4VW^ ziPcwY{~jPGM7F*s%I8tl0)SYppRy*9Qr2%|C0J9At8*+@GH{)F_jRfF^-IDZuuw)v zvv6ak^G@O5mm<~hMCl@`haGAB48mE2a{$Nqck7iIDL3kYK^(Q2ao?FnCO4D!-L_Hd z`4-f#9R4EqDvl#6H!orj3n1(ci<1m+d(QW=vhlmFmWlPU&Qf?KeI{-;?j^5@_ZxSu z{1q{B?vS1guKs2E*%D z<8O%PTb?Ry9E>WLW`IAyfQkr8Aldx2SvhsRpib4}kf!VP--Nz_qCSR8tFyU=y%fT; z0A{7~FvrRZ!3_7=x1zZonGFby2;cHGlFHRsY#~>adI!0u#mlW`TbYQKQM*Vpo^o*& z5r1hNCU1%G_THo_8_(n148rW{`1UiT5sTPDa-2E&9n|*h9oam-J;3*a;+^;INXiHjj z?d&+UX)Joy4Dp}C2cKd&kL92&drpDBhr{u@DQ`cTo+{YsnyI|s2Exd^2MqPt6!?TxR&eC~KT^UTnGt4b( zV*-mtYW-R1K97#Z=+Yxmak;CWd?UH-o4HCy-cAaX;<4bRxF2e&N`BP)WSwuCx) z3;~0XSBhD)When%S-m!+53Wk}q;`1~@*{Ul`M7Sb)vVszFplO+fZJQu$gL`m&q-W=^h9vr@LaMyS9X0kwzg8ujOH!G=gvl z0ZXfA-9Xo$?T*W%S zj{`nL=)t$W3>R-@;GAJ4Ystj` delta 3572 zcmaJ@U2Gf25xyml6vaQ0`mt<_vi@jElqg!FS+-@xKWE1@!(Bby}UAd-QsvL2HonYJG>%s5Q#DSKn#u(smJrH8N*z z_j;4W2y1>qsPBp60U^r02-x)NdyE#X++wt9twx*HCN~H4cB4b)b%*6c>?OMDAyF~)OxP>ckV~>KZCoI9C*1{zWA~bi zC=&{8qLGIRjj)CXPHm74tPfH2>jrJ1tq;`)MB5K*+GsnhX_spb0Np{OKu0Bg5a_+M z6X;G!mk$A>i*^H}TQUv<9iu%!_elB((7iMcbX?L$f$pOTpc9fl2J}AK4|Knzj{`kG zlRzgWJq+%gpo46f4$=MK$bmJL9;AmJDsb2lHc-wD(ZlQzJwlH@6TVK5(_!h$sN8yj z9i=0i(wp=Y9h1_NARVIP>^Q5*oua4d9@syz=Ax7Ij8r}*hw&;N zf)7M#L>l@kPzp;!)tPj@kk4k)IzJ%F!Jl8P|XSqt#o92&GA zJ|@lnxaN2k&C0`D{%5gG<MB1`L~ z#gE1PhL~yqN8E3WlF^MHHU7m(hQ&Kgz2c)C5%G(rS#1=g{3OEWHOEw5iR+NXVL2CA z>8ZnVB>FAFUpDMLrb8df6pZ5VA4M*5qI?cDk0YE$m_RrKaGGzgj+m#?s+SgV)E4ve z1^BbnY<|ALxAzuagZk%DTn%Ai#To1b+>Km$%_h#U=)lK3E_3qqGS`tSPr6kkGLFIL z{e-1Tj2k}{#pXfsrueKm?zJtW(v3ef|C+e5ig{6d)G{nv?^D9DxSl%r68u@jcqvkR z8Q1~gY8~tQHtzfm!gmqM2;0vg7Y7OO?~6OFXUm&6B?~g<+`2lO0W}9opmW={StWJw zNo1G3TX8sce(nAmdWT(_XnzjjHH6EkDIi16?;@jCfozc}$QzNaM`%FUfq=p1D+pUr z$8v_YFV$o2@^Nv$E!HN(zTH#Hn_i;Xl8pAh6!E*Z17uY=+WSH`aoe{LQV6#;PPAVj zq*^@c*jdKfy%(a)D5Ug4nzqiTvpS>Pz7dek#HwF7e;D^;J#(2170wHRyN&Wn=d0c{ zvjYQ&ZCi9j>&V;UX!J0t+E|MI+d+yV-!(Y#eUMq6l2J?*)1}3GAv_yQGq+znV65Kq z%)@c9ms=i|r{;3Dv}nf;)12QGf9*;HEEN~<5OS`G_U_JeTj$0cjJf$cXb#@H`4Y=R zR{*>G2e=%gQ`wcx1NDgyx(|}a8^7;EOk5-Cr5c@~!`J3B(T}<|NkWYl(JHIc1N}~X9KUll+ zGXUl?tz-}!G6=j1*UShKZ}NQrQ}`@dG5I6W72nfhRn4c(5_}7B7iy3xipx2^v3Rw( z8*ko^Z+K;h~LIr+(A_Gq4-C9bV4>F zd0m)qR&55pY3?TM4o-RY4&!#r>Tt_TB_zvA-mfl}`;uiD!z`)^rYTJsSxR-boaRhM z!%~X{`0h%gqCtca1nhCL1Ibpvu>$Q|k(rP07CZg%<-p%WSOu^`lXG(yQpx^-)T!C= z(Yf(4{M@yEF06ND}?lGr`=DNbcOkoUcnDRLG^Ri0z;9k-;jVW< row.language.toLowerCase() === searchLanguage.toLowerCase()) - setSearchResults(filteredRows) + setSearchResults([data]) - if (filteredRows.length === 0) { + if (searchResults.length === 0) { setSearchError('No results found') } } catch (err) { From 8540e59b994c6c33c30f15b60a9da0df544e4e7c Mon Sep 17 00:00:00 2001 From: Logan Watersmith Date: Wed, 12 Aug 2026 13:01:52 -0700 Subject: [PATCH 11/23] + Database printout is in a .txt file now. Prints out all of the data. + Translation works (language packs and demo pack load), UI allows country selection - Seems some brand names don't appear when searched, not a high priority atm imo but should be looked at --- README.md | 2 +- .../__pycache__/api.cpython-310.pyc | Bin 12129 -> 12828 bytes backend/codex_build/api.py | 99 +++++++++++----- .../__pycache__/neo4j_driver.cpython-310.pyc | Bin 5745 -> 6000 bytes backend/codex_build/codex/neo4j_driver.py | 21 ++-- .../__pycache__/source_data.cpython-310.pyc | Bin 21837 -> 22045 bytes .../codex_build/neo4j_sources/source_data.py | 80 +++++++------ frontend/src/App.css | 2 +- frontend/src/App.tsx | 109 +++++++++++++++--- 9 files changed, 222 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 620bc58..7e3e32e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ```bash docker compose up --build -# Wait ~20 seconds for Neo4j, frontend, and api to be ready +# Wait ~30 seconds for Neo4j, frontend, and api to be ready # Optional: check http://localhost:7474 in a browser (login: neo4j / changeme) ``` diff --git a/backend/codex_build/__pycache__/api.cpython-310.pyc b/backend/codex_build/__pycache__/api.cpython-310.pyc index 7a5ff08be08398c6b9c5257312f51e14905a999e..8cb86547d3f7a55f7d0205d5392c3c48344107f2 100644 GIT binary patch delta 5574 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 diff --git a/backend/codex_build/api.py b/backend/codex_build/api.py index f86b38d..03a2030 100644 --- a/backend/codex_build/api.py +++ b/backend/codex_build/api.py @@ -30,6 +30,9 @@ from fastapi import Depends, FastAPI, HTTPException, UploadFile, File, status, Query from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse +from contextlib import asynccontextmanager +from pathlib import Path +import ast from pydantic import BaseModel from dotenv import load_dotenv @@ -52,6 +55,7 @@ find_missing_brands, get_equivalent_brands, resolve_to_base_term, + get_translation_data ) except Exception as exc: logging.critical("Failed to import codex backend: %s", exc) @@ -61,10 +65,38 @@ 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): + demo_load() + 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( @@ -100,8 +132,8 @@ class SearchResponse(BaseModel): 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", @@ -118,12 +150,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] @@ -187,19 +213,21 @@ def health(): @app.post( "/search", - response_model=SearchResponse, + response_model=SearchResponse | None, tags=["search"], ) def search(term: str): try: with driver.session() as session: - canonical = resolve_to_base_term(session, term) or term + canonical = resolve_to_base_term(session, term) + if not canonical: + return None 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_id="0", source_name="", name=canonical, type="drug", @@ -220,19 +248,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" } ] @@ -241,29 +264,49 @@ 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", + "ua": "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() + ) ] 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=body.term, results=results, ) diff --git a/backend/codex_build/codex/__pycache__/neo4j_driver.cpython-310.pyc b/backend/codex_build/codex/__pycache__/neo4j_driver.cpython-310.pyc index 489fe1025cbbe2889a92ba919fb9c86293f2fd8c..de16526b74fc6dfdf129450ec9759b8834bc6d90 100644 GIT binary patch delta 1225 zcmZ8g&2JM&6yMpetUoq(ytc6+F@xG7FAx)?&>lh&5m1qk8Wk5Ip;p%7-9$#4-7xD2 znr?)uBDIHdu%<#imP;k~gs=ocZPh;jaYuuCslEyLzx9v1TE@-Zm--Kv$!!1C(TYelFJ4((ZC|^(BGO~Ab@8eg zmdTGwCIz>diIC<4_4E1UoJ-CK8H6lC;xi4B&~9&g?3;naiu?m@<#HKx9Ti}2=^)@f z4?Fr0z$LqCEZve~H72nfE3W)N-PK&hRqso&c0u}{v?N<=I*<9O{5)N0(dLyeY4h6J z8cow=EjKUF`dFs?rB;<`nm1FQ@S0v$D7E=X{VV=3Rp3ACHJ(q+@E6&`{E6P+|LP0; zZLz{{OkKD9MkjPVy4<3^6Ku5cz-LM;{M}Ub9#$<>)HCd7hXCv?83Zh^1v@GNY)G;6 zwZ2PY@~iYn>dUv~7W4uPHW8}p^c-6ja$`XdmS{&%0=r0`@w-v46K-yuy^L%~U6gj} z{0pPF+BoBf9mkK(g0Sah6P8C}ofY82lHnXc3A1DtfY6y2ZTr2+P<+j(WpM&3DIAz4 zY;M>yG?}FNPI~EHa+8j4I#C$dN)kgTz847H+1l~~cc6^|9tOWrn5oTf6m^3Qp|J4I zWH4l^u6F~sJ@#n#m=i?4)9Z#o)C6->0Z@r*7D$co#mo$;4?oM~S zQPAvI&5$Na8aSICT@SYdmmL9%Ehm{Wd{gWr1nl}mf35fO7!h~*ALbEa5C1WrlLkf{ z$S`SWaKbfE_7yrw$^Xn9%>4jYBAh=o(_{_)CU;(TrQxx>B`+bJP;WYsH%?9H*dyPw z)%~=y;QAi-w}oo?7vvrf%E}XV3Y{|i_rkMt$-Hs)Yykjfl>CW_c0U!^MGoKYwzJ{2 zy}M9b^q4=g4w2LRFKdn*8@{%FA_p;9b{t_IVWM^XyHch49F%)?PP32U1dq>v4}V(v z^wJ_;!9ShNA=D9Uga!iUEfi=eG$Rasb{dsPF%cz+SK>?M`m{lfS>upVGiC~vZ0diF C1OjLP delta 1081 zcmZuwOKTHR6waNwnaMlNB+a9bG<{e)YGV;W@dc$8tk4u%>LLhX+L?xwbf&$NXqEUt z1zqT(Tz4wC(O{JAv5-$%pz zv((2`InS5n!~A0?#sl&Yzav}xM?m3I$w|KG%go@e9a`1QGQ-YI0i23mV_|SqFN?y5 zxsxT(!V=bl5ZeQVpY>-0I0hxjI7_vd-zyNAZwF6Yck6KwM`DI%Tc#?y%#K(!Oh?i- zHw{y7cG_f+63HETqhi|?v+Rh>y42kYlbN-Osh2txr5e*ryQ+S|GbDZ8zz5@zH4OV5nZAg!rbTsV!~4qee~qq8wbLbr#mA`wwj#RcT96TC-TGX0VM{NZrvS z!VZC_5{5sGDijL*b@J5NNo2zRilq=n5JnNk5pYOHgxd|`Syq+hF^Du>Gr7|#{CH|2 R63F>dzJ6cU7gN$9{~yn0<7fZ? diff --git a/backend/codex_build/codex/neo4j_driver.py b/backend/codex_build/codex/neo4j_driver.py index ffd3168..8d064db 100644 --- a/backend/codex_build/codex/neo4j_driver.py +++ b/backend/codex_build/codex/neo4j_driver.py @@ -47,27 +47,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): @@ -165,7 +170,7 @@ def resolve_to_base_term(session, term): """ result = session.run(query, term=term).single() - return result["base"] if result else term + return result["base"] if result else None # Checks whether a language pack exists in the database def language_exists(lang_code: str) -> bool: diff --git a/backend/codex_build/neo4j_sources/__pycache__/source_data.cpython-310.pyc b/backend/codex_build/neo4j_sources/__pycache__/source_data.cpython-310.pyc index 7835474708c4404c4c8ddd5e7a94ffd967590f64..b395fd7028c5676057f030aa4d519152e002f10a 100644 GIT binary patch delta 1125 zcmZ8hO>7%Q6rQ&`w#VzWk~b(NHH+4UG~b(f-#2gG z?9T4DAH&Kg5UK`)8o_pV&77Tj>2~NH2Jgw6>;&T;OEp2nWZ2&=@3A%~KutwVU|fr`8o{tRBT##us#~Er5N(DYOQJGzbT-EGo+%Sztm7 zxVj2JW(cQiU@0bfjwlZx_t=6<>bqochYTT^bGf!%F1*9FBRB1G9lKnoGvZyA&-7wR2Q9|C zam;v6gjjy`z34UG*YfcQ?_VOO)?%w9Nfs9XD-F1H4VZpB!#*>BxPJvt0OWkZ^wqDr z?4WzXp%wROobP*za6-`*RUj3XAoc6>sCVpX?CuJ~$#B1plRQD??faDjPB1#0Z3cOv zQFy*kFV^FY3ypP{Z@w+tv&UsAodZpNkv6=8nt@Gzda75eJlcFFx;_0PeP%%XjW4g+ zk(#SjMWa-iF_PltnejrU+&m=jo{k*)Z)(J1F-`pAh{OMj^VwWxGTA)nNad~5TE8al z?U77f$X3OiD;Yt0a}S-GNE(U6c80+A#NA4UGH)tWk?&_tP}Q(Bxse&_>oyMO%cWYa z$c?HnCg*C6i_y5&yebpvQ8||O$ZYn>dI(@Npt|-6Dj=`1;=Z-z`E8gh(mfpyq||>P7E@-UWdY6cj=92Sl0n>>}hm%=7GgcIG?Z8Fq84@O}l< zjMu9WY*x?E(5I^})yEVbv1hbVGQl{`e$oob6pYi%@2P`z*5!Fn-injQf|w+bARj}; zkc1K^^G(kS5w7v)-VFiLY{vHrrg%d^wE(yIV38$5FaI8>li>{yX&w>QdHB!|3D$UV z)wTp7RveiDjW0yn01oo6)oXxzF4QbTJ#UP*N#N%*wfAHg;NBAp026#A7IMJ_{;v6# z44l12A(qO}rJ!Cabi*?OaG zraU&`y!AWA133|SEg3C5Pjn(-Op8E9Xq1fInZN~h^k8?E1RYDHn}qizCubG5)Lt*e zVzG;CqrHQrM5~a q>KQ&e9YE1_sm~a=VW{lsxhthIEu|M}A8n$&G(s!sQ5Am=Y3m=^F5QU$ diff --git a/backend/codex_build/neo4j_sources/source_data.py b/backend/codex_build/neo4j_sources/source_data.py index 4e60995..e76feaa 100644 --- a/backend/codex_build/neo4j_sources/source_data.py +++ b/backend/codex_build/neo4j_sources/source_data.py @@ -65,6 +65,7 @@ def smoke_test(sess): 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" @@ -73,13 +74,15 @@ def conceptID(text: str, symptom: str = None, source: str = None) -> str: 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}" - + 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}" + text = f"{text}-{clean_source}" return text @@ -478,42 +481,45 @@ def unify_graph(driver): log.info("<<< Graph combination complete. All sources are now connected.") def print_databases(driver): - log.info("\n" + "="*60 + "\IMPORTED DATABASES\n" + "="*60) - - # We query for distinct source labels in your system - 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 - LIMIT 10 - """ - results = sess.run(query) - - if label == "DRUG": - log.info(f"\n--- Database Records for DrugBank ---") - if label == "SNOMED": - log.info(f"\n--- Database Records for Snomed ---") - if label == "RXN": - log.info(f"\n--- Database Records for RxNorm ---") - if label == "ICD": - log.info(f"\n--- Database Records for ICD 11 ---") - - 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()]) - log.info(f"ID: [{node_id}] -> {{ {formatted_fields} }}") + # 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") - if not has_records: - log.info(f"No records found for label :{label} (Skipped or Empty).") + 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") - log.info("\n" + "="*60 + "\nEND OF DATABASE READOUT\n" + "="*60) + file.write("="*60 + "\nEND OF DATABASE READOUT\n" + "="*60) def source_data(sources): log.info("=" * 60) diff --git a/frontend/src/App.css b/frontend/src/App.css index af8d63f..18ef47a 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -344,7 +344,7 @@ .search-row input, .translate-grid input, .results-select { - width: 100%; + width: auto; height: 44px; border: 1px solid #d7dbe0; border-radius: 9px; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ae7e795..71e5a66 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,37 @@ import './App.css' import { useTranslation } from 'react-i18next' import { useEffect, useState, type KeyboardEvent } from 'react' +interface CountryOption { + code: string; + label: string; +} + +// Map language codes to available countries +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' }, + { code: 'NG', label: 'Nigeria' }, + ], + fr: [ + { code: 'FR', label: 'France' }, + { code: 'CA', label: 'Canada' }, + { code: 'BE', label: 'Belgium' }, + ], + ru: [ + { code: 'RU', label: 'Russia' }, + ], + ua: [ + { code: 'UA', label: 'Ukraine' }, + { code: 'PL', label: 'Poland' }, + ], +}; + interface LanguagesResponse { languages: string[] } @@ -29,7 +60,7 @@ interface SearchResponse { interface TranslateResultRow { source_id: string | null source_name: string | null - name: string + translation: string type: string country: string | null language: string @@ -52,6 +83,7 @@ function App() { 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 [translateError, setTranslateError] = useState('') const [searchError, setSearchError] = useState('') @@ -79,6 +111,23 @@ function App() { } } + 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 = (event: React.ChangeEvent) => { + const newLang = event.target.value; + const newCountry = getFirstCountryForLanguage(newLang); + + // Update React state + setTargetLanguage(newLang); + setTargetCountry(newCountry); + setTranslatedName('') + }; + useEffect(() => { let isActive = true @@ -117,7 +166,7 @@ function App() { const extractTranslatedName = (rows: TranslateResultRow[]) => { const names = rows - .map((row) => row.name) + .map((row) => row.translation) .filter((name): name is string => Boolean(name && name.trim())) if (names.length === 0) { @@ -141,7 +190,7 @@ function App() { setTranslatedName('') try { - const response = await fetch(`${API_BASE_URL}/search`, { + const response = await fetch(`${API_BASE_URL}/search?term=${encodeURIComponent(searchQuery.toLowerCase())}`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -157,12 +206,14 @@ function App() { throw new Error(errorBody?.detail ?? 'Failed to search') } - const data = (await response.json()) as SearchResponse - - setSearchResults([data]) + const data = (await response.json()) as SearchResponse | null - if (searchResults.length === 0) { + if (!data || !data.name) { + setSearchResults([]) setSearchError('No results found') + } else { + setSearchResults([data]) + setSearchError('') } } catch (err) { setSearchError(err instanceof Error ? err.message : 'An error occurred during search') @@ -178,12 +229,20 @@ function App() { } } - const handleTranslateSelected = async () => { + const handleTranslateSelected = async ( + lang = targetLanguage, + country = targetCountry + ) => { if (!selectedResult) { setTranslateError('Select a search result first') return } + const validCountries = (LANGUAGE_COUNTRY_MAP[lang] ?? []).map((c) => c.code); + const finalCountry = validCountries.includes(country) + ? country + : getFirstCountryForLanguage(lang); + setIsTranslating(true) setTranslateError('') setTranslatedName('') @@ -196,8 +255,8 @@ function App() { }, body: JSON.stringify({ term: selectedResult.name, - source_lang: selectedResult.language, - target_lang: targetLanguage, + lang: lang, + country: finalCountry }), }) @@ -207,9 +266,10 @@ function App() { } const payload = (await response.json()) as TranslateResponse - const name = payload.found ? extractTranslatedName(payload.results ?? []) : '-' + const results = payload.results ?? [] + const name = results.length > 0 ? extractTranslatedName(results) : '-' setTranslatedName(name) - if (!payload.found || name === '-') { + if (results.length === 0 || name === '-') { setTranslateError('No translation found for the selected language') } } catch (err) { @@ -520,8 +580,6 @@ function App() { - ) : searchQuery && !isLoading ? ( -

No results found
) : ( From 239289e46493dd25153b4ca0a3e176941fc360d6 Mon Sep 17 00:00:00 2001 From: Logan Watersmith Date: Wed, 19 Aug 2026 09:23:04 -0700 Subject: [PATCH 12/23] + Fixed ukrainian so it can be translated into now + When brand names are searched, the canonical drug name is now the result --- .../__pycache__/api.cpython-310.pyc | Bin 12828 -> 12828 bytes backend/codex_build/api.py | 2 +- .../__pycache__/neo4j_driver.cpython-310.pyc | Bin 6000 -> 6340 bytes backend/codex_build/codex/neo4j_driver.py | 10 ++++++++++ frontend/src/App.tsx | 2 +- 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/backend/codex_build/__pycache__/api.cpython-310.pyc b/backend/codex_build/__pycache__/api.cpython-310.pyc index 8cb86547d3f7a55f7d0205d5392c3c48344107f2..c74fad59cffc87b4c442e8cfd2aa043cab5d5883 100644 GIT binary patch delta 27 hcmbP}GAD&QpO=@50SM$TwQl5=mSxP|tSIKq{R5{X!f){lH2}Cik;kXXM_T&RWLI=sWoyM>C_z<|NMjOpF1W zm3XExF=lVx$*0W3*g5&GKqQkm^JELbVjYA58c9}8MTvPS5R1Y+T!UN{lJxQtb5j*; z6;w)6i*hID3#CjxFZe(RQ`2NgAtN@3>Pf=3llui7CaVgmPyQt|mvo5gHEf`w5? ldh!zC|BSmfFA@2{$ar}2T(M)kN*ppA(j0Oef<08K4s r0}?EZLc)`Oi2P^Vu=$7R4@SnFlRt_d<5l30;gIH#;}8^*;Nb)SVIC)- diff --git a/backend/codex_build/codex/neo4j_driver.py b/backend/codex_build/codex/neo4j_driver.py index 8d064db..80a3f1a 100644 --- a/backend/codex_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) @@ -155,6 +157,10 @@ def resolve_to_base_term(session, term): WHERE t.canonical = $term RETURN t.canonical AS base UNION + MATCH (t:Term)<-[:CONTAINS]-(b:Brand) + WHERE b.name = $term + RETURN t.canonical AS base + UNION MATCH (t:Term)<-[:OF_TERM]-(tr:Translation) WHERE tr.text = $term RETURN t.canonical AS base @@ -163,6 +169,10 @@ def resolve_to_base_term(session, term): WHERE apoc.text.jaroWinklerDistance(toLower(t.canonical), toLower($term)) < 0.20 RETURN t.canonical AS base UNION + MATCH (t:Term)<-[:CONTAINS]-(b:Brand) + WHERE apoc.text.jaroWinklerDistance(toLower(b.name), 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 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 71e5a66..8b79c1a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -27,7 +27,7 @@ const LANGUAGE_COUNTRY_MAP: Record = { ru: [ { code: 'RU', label: 'Russia' }, ], - ua: [ + uk: [ { code: 'UA', label: 'Ukraine' }, { code: 'PL', label: 'Poland' }, ], From 6875e1dd04c6ac0cebc8d1cb21e355364e2e7fe7 Mon Sep 17 00:00:00 2001 From: loganwatersmith-gb Date: Tue, 1 Sep 2026 16:05:43 -0700 Subject: [PATCH 13/23] Removed demo_load function for local import --- backend/codex_build/api.py | 32 +++++--------------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/backend/codex_build/api.py b/backend/codex_build/api.py index 7ea8d7d..c3aeaad 100644 --- a/backend/codex_build/api.py +++ b/backend/codex_build/api.py @@ -68,7 +68,6 @@ TARGET_FOLDER_PATH = Path("./codex/language_packs") @asynccontextmanager async def lifespan(app: FastAPI): - demo_load() print(f"Scanning directory: {TARGET_FOLDER_PATH.resolve()}") if TARGET_FOLDER_PATH.exists() and TARGET_FOLDER_PATH.is_dir(): @@ -110,7 +109,10 @@ async def lifespan(app: FastAPI): class SourceSelection(BaseModel): selectedSources: List[str] -@app.post("/api/populate-sources") +@app.post( + "/api/populate-sources", + tags=["data"] +) async def handle_populate(data: SourceSelection): sources = data.selectedSources @@ -214,7 +216,7 @@ def health(): @app.post( "/search", response_model=SearchResponse | None, - tags=["search"], + tags=["translation"], ) def search(term: str): try: @@ -347,30 +349,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, From dea76919b9b1c0e9e77796ce5b2f4cdf0ebb675c Mon Sep 17 00:00:00 2001 From: loganwatersmith-gb Date: Tue, 1 Sep 2026 16:06:24 -0700 Subject: [PATCH 14/23] Added local language import functionality --- frontend/src/App.tsx | 137 ++++++++++++++++--------------------------- 1 file changed, 52 insertions(+), 85 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8b79c1a..14bcfee 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -89,10 +89,10 @@ function App() { const [searchError, setSearchError] = useState('') const [isLoading, setIsLoading] = useState(false) const [isTranslating, setIsTranslating] = useState(false) - const [exportLanguage, setExportLanguage] = useState(availableLanguages[0]) - const [exportError, setExportError] = useState('') - const [isExporting, setIsExporting] = useState(false) - const [exportMessage, setExportMessage] = useState('') + const [importError, setImportError] = useState('') + const [isImporting, setIsImporting] = useState(false) + const [importLanguage, setImportLanguage] = useState(null) + const [importMessage, setImportMessage] = useState('') const getLanguageLabel = (code: string) => { const raw = code.trim() @@ -128,10 +128,13 @@ function App() { setTranslatedName('') }; - useEffect(() => { - let isActive = true + const handleImportLanguageChange = (event: React.ChangeEvent) => { + if (event.target.files && event.target.files.length > 0) { + setImportLanguage(event.target.files[0]) + } + }; - const loadLanguages = async () => { + const loadLanguages = async (isActive: boolean) => { try { const response = await fetch(`${API_BASE_URL}/languages`) if (!response.ok) { @@ -146,7 +149,6 @@ function App() { setAvailableLanguages(nextLanguages) setTargetLanguage((current) => (nextLanguages.includes(current) ? current : nextLanguages[0])) - setExportLanguage((current) => (nextLanguages.includes(current) ? current : nextLanguages[0])) } catch { if (!isActive) { return @@ -157,7 +159,10 @@ function App() { } } - void loadLanguages() + useEffect(() => { + let isActive = true + + loadLanguages(isActive) return () => { isActive = false @@ -279,66 +284,38 @@ function App() { } } - const rowsToCsv = (rows: any[]) => { - if (!rows || rows.length === 0) return '' - const headerOrder = ['concept_id', 'source_id', 'source_name', 'name', 'type', 'country', 'language', 'uploaded_at'] - const keys = Array.from(new Set([...headerOrder, ...Object.keys(rows[0] || {})])) - const escapeVal = (v: any) => { - if (v === null || v === undefined) return '' - const s = String(v) - if (s.includes('"') || s.includes(',') || s.includes('\n')) { - return `"${s.replace(/"/g, '""')}"` - } - return s + const handleImportLanguageLocal = async () => { + setImportMessage('') + setImportError('') + if (!importLanguage) { + setImportError('Please select a language file!') + return } - const lines = [keys.join(',')] - rows.forEach((r) => { - lines.push(keys.map((k) => escapeVal((r as any)[k])).join(',')) - }) - return lines.join('\n') - } - - const downloadBlob = (filename: string, text: string) => { - const blob = new Blob([text], { type: 'text/csv;charset=utf-8;' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = filename - document.body.appendChild(a) - a.click() - a.remove() - URL.revokeObjectURL(url) - } - - const fetchAndDownload = async (urlSuffix: string, filename: string) => { - setIsExporting(true) - setExportError('') - setExportMessage('') + setIsImporting(true) + const formData = new FormData() + formData.append("file", importLanguage) + try { - const res = await fetch(`${API_BASE_URL}${urlSuffix}`) - if (!res.ok) { - const err = await res.json().catch(() => null) - throw new Error(err?.detail ?? 'Export failed') + const response = await fetch(`${API_BASE_URL}/packs/load`, { + method: 'POST', + body: formData, + }) + + if (!response.ok) { + throw new Error('Failed to import language') } - const data = await res.json() - const rows = Array.isArray(data.rows) ? data.rows : data.results ?? [] - const csv = rowsToCsv(rows) - if (!csv) throw new Error('No rows to export') - downloadBlob(filename, csv) - setExportMessage(`Downloaded ${rows.length} rows`) - } catch (err) { - setExportError(err instanceof Error ? err.message : 'Export failed') + + const data = await response.json(); + + loadLanguages(true) + setImportError('') + setImportMessage(`Success! ${data.message}`); + } catch (error) { + setImportError(`Error: ${(error as Error).message}`); } finally { - setIsExporting(false) + setIsImporting(false) } } - const downloadByLanguage = () => { - if (!exportLanguage.trim()) { - setExportError('Choose a language') - return - } - fetchAndDownload(`/csv/language/${encodeURIComponent(exportLanguage.trim())}`, `codex_language_${exportLanguage.trim()}.csv`) - } const languages = availableLanguages.map((code) => ({ code, label: getLanguageLabel(code) })) @@ -652,30 +629,20 @@ function App() { -
-

Export CSV

+
-
-
- - -
-
- +

{t('home.importTitle')}

+
+ + +
+
+
+ {isImporting &&
Importing language...
} + {importError &&
{importError}
} + {importMessage &&
{importMessage}
}
-
- {isExporting &&
Preparing CSV...
} - {exportError &&
{exportError}
} - {exportMessage &&
{exportMessage}
} -
-
@@ -683,4 +650,4 @@ function App() { ) } -export default App \ No newline at end of file +export default App From e84d33b14663863b57689614804007fbe864530a Mon Sep 17 00:00:00 2001 From: loganwatersmith-gb Date: Tue, 1 Sep 2026 16:10:35 -0700 Subject: [PATCH 15/23] Added UI languages for language import --- frontend/src/i18n/locales/de.json | 3 ++- frontend/src/i18n/locales/en.json | 3 ++- frontend/src/i18n/locales/es.json | 3 ++- frontend/src/i18n/locales/fr.json | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index bf08427..9fd6c8b 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -24,6 +24,7 @@ "localizeTitle": "Medikamentennamen übersetzen/lokalisieren...", "targetLanguage": "Zielsprache", "targetPlaceholder": "Zielsprache", - "translationPlaceholder": "Die Übersetzung wird hier angezeigt" + "translationPlaceholder": "Die Übersetzung wird hier angezeigt", + "importTitle": "Sprachdatei importieren" } } diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index f0bd529..3856e30 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -24,6 +24,7 @@ "localizeTitle": "Translate/Localize drug name...", "targetLanguage": "Target Language", "targetPlaceholder": "Target Language", - "translationPlaceholder": "Translation will appear here" + "translationPlaceholder": "Translation will appear here", + "importTitle": "Import Language File" } } diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index 2c262ec..8efc4ee 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -24,6 +24,7 @@ "localizeTitle": "Traducir/Localizar nombre de medicamento...", "targetLanguage": "Idioma de destino", "targetPlaceholder": "Idioma de destino", - "translationPlaceholder": "La traducción aparecerá aquí" + "translationPlaceholder": "La traducción aparecerá aquí", + "importTitle": "Importar archivo de idioma" } } diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 879a85b..cdf6b44 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -24,6 +24,7 @@ "localizeTitle": "Traduire/Localiser le nom du médicament...", "targetLanguage": "Langue cible", "targetPlaceholder": "Langue cible", - "translationPlaceholder": "La traduction s'affichera ici" + "translationPlaceholder": "La traduction s'affichera ici", + "importTitle": "Importer un fichier de langue" } } From 39cf2efd02846448f4aa4febc8f77cbaaa107914 Mon Sep 17 00:00:00 2001 From: loganwatersmith-gb Date: Tue, 1 Sep 2026 19:28:58 -0700 Subject: [PATCH 16/23] + Can now search by brand, and brand info is returned (generic name, languages, and countries) + Added languages and country info for generic drug name searches + Now displays brand name for selected country for translation --- .../__pycache__/api.cpython-310.pyc | Bin 12828 -> 12466 bytes backend/codex_build/api.py | 21 ++- .../__pycache__/__init__.cpython-310.pyc | Bin 115 -> 115 bytes .../__pycache__/neo4j_driver.cpython-310.pyc | Bin 6340 -> 8774 bytes .../codex/language_packs/spanish_pack.json | 6 +- backend/codex_build/codex/neo4j_driver.py | 133 ++++++++++++++---- .../__pycache__/__init__.cpython-310.pyc | Bin 124 -> 124 bytes .../translation_service.cpython-310.pyc | Bin 4384 -> 4391 bytes .../codex/services/translation_service.py | 2 +- .../__pycache__/source_data.cpython-310.pyc | Bin 22045 -> 22045 bytes frontend/src/App.tsx | 55 +++++++- 11 files changed, 176 insertions(+), 41 deletions(-) diff --git a/backend/codex_build/__pycache__/api.cpython-310.pyc b/backend/codex_build/__pycache__/api.cpython-310.pyc index c74fad59cffc87b4c442e8cfd2aa043cab5d5883..3081ab718c0a337a92c061e59af086954bf154e5 100644 GIT binary patch delta 4265 zcmaJDTTEQX_0H_wWmy)y5Ci6o9mjA1^D;KXZ)`Bw2K>M;)=oCp%m9mb?{4N^jEx~{ z2P=t_t{i8i_9ICHt(wGDR4I0wR#lWr{cu$&jgp^!T($jcRkf*FDUniDdGwsQgy0l+ zSNF`EbLN~muQ@ZXusdc!DnCD@!EfRE_at_d*bH^0_5YP0p!-|c=-+)xWh_#m=E%z$rJ2^irvYqpBJ)I^JI*$NjAcw*Zp9|Nu}p3A7JN}O?-?! zu1q||PqWy83-m=I?m?ya5@(`k_z3qcMA;L}T%;g=mhW0P$IkP!swU1%KE_5F<7Zh7 z<5?Z*%Qvh({GXUSyFvP^Im;xJEdmdsHn7df+(=~+7Gb3*dT+^W)j)1Vpu zBk(<0Vg*5i-y!lxxmQS@Yzpy1kAOLD?pQp*V`x7%o4Ah!c0&AN%vlEC1l-QF)^vU33hX;3pQ9 zj`bl<%e*yP2hRdooI`LLz>wh=y@(t@(1!ry?1qym#uHGW)>wReRU@LX>;;)!Q$eEW zw!F8de?)DW=RjTB5TSU=5@%532?Qq*s4c|<9b>TOyYSiAz`u439AavR7b9KdnEY*| zz43tx#z5{>R1jEF5SAYo*2)8gyAo)d+TRU($L?~>GLw8rS?2}>H|>X z7M<^SUu#FU^}Kj6C zFwAALv0Pp({wvuaVruGFZ17AP}0EjBmB=!D>?jEWf;lpR8410x`xC@aoD zA0hVpD6!Yk7RaJ@vwe;@&_QRld3uh{L7$wZc3CdG8JMQSf8K}h2c`{cvp##1T~s2dMo@#G7QuQ1*viDy0Nh;2 z2v4iVk&8rCUfAu79533aC?m#^hkLrY%QvXNb}XJj@XbN>QtFxi2VmLT0nk8*6cC+I zQuKwchu~y{Xo%3Om7nQX3qm0hA;GKkOR7^?vGO1Ix3~r3GjdDWhP%<=b zK8JhW05BvbaGI5Ws_Y6r0aKT9OV(8VJ)pW^Rz|9;hgE>El#~O8uFA@bn2I9ujeysn z(S6qk7r*OIq^7y>qKi%PHg#!pt4qXfpLj`LtF9|iPAh*yI}>r+8BHbAyPM>D)x#MT zE;P+3^1`ggEqHI-e-Vgc+$V;7`^g~qDDL<-thb7zFSxO2?=;Hm$e&qH{iP4&~tI1Vi5+HB)?Sy z;&mX8%I7xpY`z0CaTmc)5&R6n{RN{IGXnIV!vYfN_l&oFh3T2Tjs8{`(Vr{m6qs*fccTt0 z4J1;dX2N!9%64^p4~iy5p0W~g>%t1Z@EpLIs7G!NJV_jGLHBl&Y4Ix{3A}$+JNcp= z7#A?=%`l-?Wq#vk(tdYGz(nm0knZNO z9qM)qf~KWMt9t6fT-B2b7saEeiKw>*LnrW2WjrpgG*{%^gQ@GYFWchRacdv?;hU6LOt`MsWleVQ*J93hZjCX{eK|j+4MD_Tq{4BBl^vqY=*{xP{L delta 4528 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 diff --git a/backend/codex_build/api.py b/backend/codex_build/api.py index c3aeaad..26ccaf9 100644 --- a/backend/codex_build/api.py +++ b/backend/codex_build/api.py @@ -46,7 +46,6 @@ from codex.services.translation_service import ( translate, load_language_pack, - load_demo_data, ) from codex.neo4j_driver import ( driver, @@ -55,7 +54,11 @@ find_missing_brands, get_equivalent_brands, resolve_to_base_term, - get_translation_data + 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) @@ -127,6 +130,7 @@ class SearchResponse(BaseModel): source_id: str source_name: str name: str + brand: Optional[str] type: str country: str language: str @@ -221,9 +225,15 @@ def health(): def search(term: str): try: with driver.session() as session: - canonical = resolve_to_base_term(session, term) + 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)) @@ -232,9 +242,10 @@ def search(term: str): source_id="0", source_name="", name=canonical, + brand=brand, type="drug", - country="US", - language="en", + country=countries, + language=languages, uploaded_at="" ) diff --git a/backend/codex_build/codex/__pycache__/__init__.cpython-310.pyc b/backend/codex_build/codex/__pycache__/__init__.cpython-310.pyc index bbf1dd7015348d175deb31f3570a092ed64d10e6..1a0c818733473fad79b488a0dfef408a0748103f 100644 GIT binary patch delta 17 XcmXRe=FaEk%|xWVUo~b3)c!!)Y3?(rPvZq$RZhAsObR~&q5()7tJmu zKepUTRZndtG>4ulVX2oMqpH{TQl-kJ%_#@eQ|>)+L6IW$&Frp$rtVLqjN~_O-rwx| z-Z%bv@=-o&pF7th!TRu%uQz`i--`~TM|%^u(WSl5Z@w`^hCh>TYh;9+hrOSS5)6Bi zTp$-=A0VMcDKqx)>23_&V0wJ$hSNu+MX4Slp+PQ{LFtZ6N4ztU*5uPZe$Iq-Lk&w( z4~8{6Ge3_X2$ik$O|u{317T`dB+GtH3`TiGMK3PsUcf;-H^og$bA?g?ujVXc%f!=o z%r)tjh~UmrprgUtC3)YO9W8#GpPd>ht>u|5A=1(v1PZHjeH$g860);6^pyz?e2MRAh`#@za zZ9$EKq9FC6f)o@(XHiUM&$Somm+z1Ik5Kaocpb$HiPE<~smdUN1=N(dcNTt!Qy>Ux z$aQH`s>^jmIZ(Du^A5ej)$qqgZO1%NHth`y z+$z}UC>IGXhIvT0S7yU>sK$*#^m9*g-E^1R-sLr$F7v1O419L2-OU0eaBe$xVKQs z7fSg35AZ3gAQDE0a%amxKGNcWEf2RuTtl`|TP zstF&m3HCTS$UarHTt^f-Q{=cwI?dbt%d`hxp0;?HVW4=>NVnfe{=YP0Oq_Zg2zP3R zJ?qN8=CRV5P96J`yJR|IbfV>c_;C6j&nSS%AR6IYiuIfcNOwey#3Sn2*q^U5>wy3O delta 312 zcmX@+a>S4?pO=@50SL}sZ_Uz|naC%@xNM?!6r=RSHg8tuUIMeyPc(Y(kTScqTWBich{L!oe2m=jrdqHQ7K!Z*m_y z+vEju!st>DgiIzsW)@;eEz0GZTq`OK(#8tbHo2Z(V)7K>P;?zI!)6JqO@1J!f-Vg* zaEh?*4N?E;b;Sg;7XafN`>$_+Q3dn_VP+FiIZ= niWaj#r1*-=fDBE>A`TEGGPzK(c=8WLB}RqG(n<=<3Ot+uEmc!< diff --git a/backend/codex_build/codex/language_packs/spanish_pack.json b/backend/codex_build/codex/language_packs/spanish_pack.json index 5c33898..deec695 100644 --- a/backend/codex_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/backend/codex_build/codex/neo4j_driver.py b/backend/codex_build/codex/neo4j_driver.py index 80a3f1a..bbdd5c6 100644 --- a/backend/codex_build/codex/neo4j_driver.py +++ b/backend/codex_build/codex/neo4j_driver.py @@ -153,34 +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)<-[:CONTAINS]-(b:Brand) - WHERE b.name = $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)<-[:CONTAINS]-(b:Brand) - WHERE apoc.text.jaroWinklerDistance(toLower(b.name), 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 None + 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: @@ -207,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/backend/codex_build/codex/services/__pycache__/__init__.cpython-310.pyc b/backend/codex_build/codex/services/__pycache__/__init__.cpython-310.pyc index 6be17769e14a4de2a00dd342b73ccdd851062a0a..543fab79a48761f52117b7fc2daf9ef6ec33835a 100644 GIT binary patch delta 17 Xcmb=a;m+sfJF(XHMW22@gZIFjk?KTg*23^$;>QS9I9F^Sxd^=%%-bS z*79`EV5XEdrL@hXMsNE}u-~)geup{q3BPE>!QnZhKhAxNK@GyB8bTG1u+WMaw85Vn zL$JpWjP0pD(S(#vd&CbNKqL`^2wb9ZfnBAkoE)Rrvt0CZ*CNFZHMXdYJROuGyIu#Ci9=Q z?xE@k|6%P#{w|f{Z|zRV@Q-!^V*Hyu+8Yl1^x_xNF#ciW0h_SIGtT_5bR`X;m!|F0 ze7RD}`-QV2t@r|A#ZlUL-PwTg{Uf1k0DZg|e$jzV+)@*#5(58sky7CnUR# zg_=kC7NgftU2x)0DxHH}t z>4E`15jiIeu1C_7Ggvl@kj`XjDW{{{=09S=9k!0~IkEtAd^kD_v%DOgx^M|k0Y!Ql b)lMV2aYn89l#XLMjhI9HansWJ8lC(Nb^*mE delta 1045 zcma)4&rcIU6rS1NyJdkyp+FTXSh0;F1_=q;#2?6k7(HNOLV~rh+hw&aWp*K=(-2Y< zAs&pG=*h(PL^u$mB*qy30#BazUohT0c`(kKB^W)qoB8(5oAMB*qgxFyRKSd?%NRIr( zpep;MrZ&7#Yw8qxs3p}ks!gH!OkF?7*G0ZT4N?;Mx}!QgMsVnY!=>_g31M^x zNytG8e;YDJvuNc?%;T7X$D}6N1|G-cJeFg6T#d!$Bm|Yo?rHt`tclqXee7i{nsmlO%hnf7mz% z=5YYwJOS3w;Wi1@Ne=hG$Z*K&PO@iK3R`T)s^PKyp2Tg8Q>@bV z@=}^7Jd)Gr(m!!Fs}U3O1&P9~NUagS8WI(}8V5cnaVroJ9c80j#;iRQ*EtyfrhOCV z_rKal@H|{wq%1BwZlqL;6?aYzT~Y~zzDRdDvF`@)4GZyUsG!9+0k&us1M(%uGUM>b8I36{9Bezf(07`oX4*&oF delta 22 ccmbQchH>s1M(%uGUM>b8IIyT@Bezf(084WQH2?qr diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 14bcfee..e218927 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -41,8 +41,9 @@ interface SearchResultRow { source_id: string | null source_name: string | null name: string + brand: string | null type: string - country: string | null + country: string language: string uploaded_at: string | null } @@ -51,6 +52,7 @@ interface SearchResponse { source_id: string source_name: string name: string + brand: string type: string country: string language: string @@ -61,6 +63,7 @@ interface TranslateResultRow { source_id: string | null source_name: string | null translation: string + brand: string | null type: string country: string | null language: string @@ -85,6 +88,7 @@ function App() { 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) @@ -93,6 +97,7 @@ function App() { const [isImporting, setIsImporting] = useState(false) const [importLanguage, setImportLanguage] = useState(null) const [importMessage, setImportMessage] = useState('') + const [hasBrand, setHasBrand] = useState(false) const getLanguageLabel = (code: string) => { const raw = code.trim() @@ -126,6 +131,7 @@ function App() { setTargetLanguage(newLang); setTargetCountry(newCountry); setTranslatedName('') + setTranslatedBrand('') }; const handleImportLanguageChange = (event: React.ChangeEvent) => { @@ -181,6 +187,18 @@ function App() { 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') @@ -193,6 +211,7 @@ function App() { setSearchResults([]) setSelectedResult(null) setTranslatedName('') + setTranslatedBrand('') try { const response = await fetch(`${API_BASE_URL}/search?term=${encodeURIComponent(searchQuery.toLowerCase())}`, { @@ -217,6 +236,11 @@ function App() { setSearchResults([]) setSearchError('No results found') } else { + if (data.brand) { + setHasBrand(true) + } else { + setHasBrand(false) + } setSearchResults([data]) setSearchError('') } @@ -251,6 +275,7 @@ function App() { setIsTranslating(true) setTranslateError('') setTranslatedName('') + setTranslatedBrand('') try { const response = await fetch(`${API_BASE_URL}/translate`, { @@ -273,7 +298,9 @@ function App() { const payload = (await response.json()) as TranslateResponse 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') } @@ -522,19 +549,28 @@ function App() { - + {hasBrand && } + {!hasBrand && } + + {hasBrand && } + - - + + {hasBrand && } + {!hasBrand && } + + {hasBrand && } + {!hasBrand && } {searchResults.map((row, index) => ( - + {hasBrand && } + {hasBrand && } + {!hasBrand && } @@ -611,14 +650,16 @@ function App() {
Drug NameDrug Name for BrandDrug NameBrandTypeLanguageCountryLanguage for BrandLanguageCountries for BrandCountries
{row.name}{row.type}{row.brand}brand name drug{row.type}{row.language} {row.country ?? '-'}
- + + +
Original Drug Name (Language)Original Drug Name (Languages) Drug Name in {targetLanguage.toUpperCase()}Drug Brand Name in {targetCountry.toUpperCase()}
{selectedResult.name} ({selectedResult.language}) {translatedName || '-'}{translatedBrand || '-'}
From 2866a5422150961d02fcbc6a074487b6265a4d74 Mon Sep 17 00:00:00 2001 From: loganwatersmith-gb Date: Wed, 2 Sep 2026 13:27:18 -0700 Subject: [PATCH 17/23] + Adds progress/status for database population, including number processed out of number remaining (if available), as well as which database is currently being worked on - Does not include info on what the most recent successful fetch was - Still need to implement OCR, I tried to implement it from the backend repo and could not get it working --- backend/codex_build/api.py | 19 +++--- .../codex_build/neo4j_sources/source_data.py | 62 +++++++++++++++---- frontend/src/App.css | 16 +++++ frontend/src/App.tsx | 48 +++++++++++--- 4 files changed, 118 insertions(+), 27 deletions(-) diff --git a/backend/codex_build/api.py b/backend/codex_build/api.py index 26ccaf9..cfb30b3 100644 --- a/backend/codex_build/api.py +++ b/backend/codex_build/api.py @@ -29,7 +29,7 @@ from fastapi import Depends, FastAPI, HTTPException, UploadFile, File, status, Query from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse +from fastapi.responses import JSONResponse, StreamingResponse from contextlib import asynccontextmanager from pathlib import Path import ast @@ -62,7 +62,7 @@ ) 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") @@ -118,11 +118,15 @@ class SourceSelection(BaseModel): ) async def handle_populate(data: SourceSelection): sources = data.selectedSources - - source_data(sources) - - return {"status": "success", "message": "Successfully populated Neo4j!"} - + 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 ──────────────────────────────────────────────── @@ -427,7 +431,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/backend/codex_build/neo4j_sources/source_data.py b/backend/codex_build/neo4j_sources/source_data.py index e76feaa..7f5d133 100644 --- a/backend/codex_build/neo4j_sources/source_data.py +++ b/backend/codex_build/neo4j_sources/source_data.py @@ -10,6 +10,8 @@ import uuid import re import logging +import json +import asyncio from datetime import datetime, timezone from collections import deque @@ -91,7 +93,7 @@ def conceptID(text: str, symptom: str = None, source: str = None) -> str: # DrugBank # -------------------- -def drugbank(driver): +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: @@ -166,7 +168,11 @@ def iter_drugs(per_page=100): 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: @@ -175,7 +181,10 @@ def iter_drugs(per_page=100): 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"]) @@ -183,7 +192,7 @@ def iter_drugs(per_page=100): # SNOMED # -------------------- -def snomed(driver): +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") @@ -254,7 +263,11 @@ def upsert_snomed_node(node): 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) @@ -272,7 +285,10 @@ def upsert_snomed_node(node): 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"]) @@ -280,7 +296,7 @@ def upsert_snomed_node(node): # RXNORM # -------------------- -def rxnorm(driver): +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}" @@ -321,7 +337,9 @@ def rx_get(path, params=None): 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: log.info("RxNorm build processing roots: %d / %d", idx, len(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", []) @@ -337,6 +355,8 @@ def rx_get(path, params=None): 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"]) @@ -344,7 +364,7 @@ def rx_get(path, params=None): # ICD-11 # -------------------- -def icd11(driver): +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: @@ -410,7 +430,11 @@ def get_node_details(item): 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) @@ -427,7 +451,10 @@ def get_node_details(item): 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"]) @@ -521,7 +548,7 @@ def print_databases(driver): file.write("="*60 + "\nEND OF DATABASE READOUT\n" + "="*60) -def source_data(sources): +async def source_data(sources): log.info("=" * 60) log.info("Creating Singular Medical Knowledge Graph") log.info("=" * 60) @@ -537,16 +564,29 @@ def source_data(sources): # Ingest separate sources into the single database based on user choice if "drugbank" in sources: log.info("Ingesting DrugBank.") - drugbank(driver) + 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.") - snomed(driver) + 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.") - rxnorm(driver) + 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.") - icd11(driver) + 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) @@ -555,7 +595,7 @@ def source_data(sources): except Exception as e: log.critical("Pipeline was terminated prematurely: %s", e) - sys.exit(2) + raise e finally: driver.close() diff --git a/frontend/src/App.css b/frontend/src/App.css index 18ef47a..9a1ed0a 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -204,6 +204,22 @@ left: 32px; } +.populate-loading-label { + display: flex; + align-items: center; + justify-content: center; + text-align: center; + font-size: 1.2rem; + font-weight: 500; + color: #0f1826; + position: absolute; + top: 450px; + left: 10px; + width: 300px; + height: 5vh; + box-sizing: border-box; +} + .populateDrugbank-checkbox { display: flex; align-items: center; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e218927..3df0f4d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,7 +17,6 @@ const LANGUAGE_COUNTRY_MAP: Record = { { code: 'US', label: 'United States' }, { code: 'GB', label: 'United Kingdom' }, { code: 'CA', label: 'Canada' }, - { code: 'NG', label: 'Nigeria' }, ], fr: [ { code: 'FR', label: 'France' }, @@ -76,7 +75,7 @@ interface TranslateResponse { } const API_BASE_URL = 'http://localhost:8000' -const FALLBACK_LANGUAGES = ['en', 'es', 'fr', 'de'] +const FALLBACK_LANGUAGES = ['en', 'es', 'fr'] function App() { const { t, i18n } = useTranslation() @@ -98,6 +97,7 @@ function App() { const [importLanguage, setImportLanguage] = useState(null) const [importMessage, setImportMessage] = useState('') const [hasBrand, setHasBrand] = useState(false) + const [progress, setProgress] = useState(''); const getLanguageLabel = (code: string) => { const raw = code.trim() @@ -367,20 +367,50 @@ function App() { sources.push("icd11") } setIsPopulationLoading(true); - try{ + setProgress('Starting population...'); + try { const response = await fetch('http://localhost:8000/api/populate-sources', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ selectedSources: sources }), }); - const data = await response.json(); - if (data.status === "success") { - alert(data.message); + 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'); + + // Keep the incomplete last line in the buffer + 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 JSON chunk:', err); + } + } } + setProgress('Successfully populated Neo4j!'); } catch (error) { - console.error("Error calling Python function:", error); - alert("Something went wrong on the backend."); + console.error('Error during streaming:', error); + setProgress('Error processing request.'); } finally { setIsPopulationLoading(false); } @@ -493,6 +523,8 @@ function App() { {t('sides.populateButton')}
+ +

{progress}

From 327f864e764b4538d1c1f48623c3c0b9b76e5ec7 Mon Sep 17 00:00:00 2001 From: Sebas Taboada Date: Tue, 15 Sep 2026 21:03:38 -0300 Subject: [PATCH 18/23] chore: update .gitignore, untrack pycache, and build configs --- .gitignore | 31 +- .../__pycache__/__init__.cpython-312.pyc | Bin 154 -> 0 bytes .../__pycache__/api.cpython-310.pyc | Bin 12466 -> 0 bytes .../__pycache__/api.cpython-314.pyc | Bin 16764 -> 0 bytes .../__pycache__/config.cpython-312.pyc | Bin 790 -> 0 bytes .../__pycache__/db.cpython-312.pyc | Bin 2138 -> 0 bytes .../__pycache__/dependencies.cpython-312.pyc | Bin 704 -> 0 bytes .../__pycache__/main.cpython-312.pyc | Bin 1405 -> 0 bytes .../__pycache__/main.cpython-314.pyc | Bin 3074 -> 0 bytes .../__pycache__/__init__.cpython-310.pyc | Bin 115 -> 0 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 181 -> 0 bytes .../__pycache__/neo4j_driver.cpython-310.pyc | Bin 8774 -> 0 bytes .../__pycache__/neo4j_driver.cpython-314.pyc | Bin 7272 -> 0 bytes .../__pycache__/__init__.cpython-310.pyc | Bin 124 -> 0 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 190 -> 0 bytes .../translation_service.cpython-310.pyc | Bin 4391 -> 0 bytes .../translation_service.cpython-314.pyc | Bin 7834 -> 0 bytes .../__pycache__/config.cpython-313.pyc | Bin 1044 -> 0 bytes .../__pycache__/config.cpython-314.pyc | Bin 1238 -> 0 bytes .../__pycache__/db.cpython-313.pyc | Bin 2936 -> 0 bytes .../__pycache__/db.cpython-314.pyc | Bin 2640 -> 0 bytes .../__pycache__/source_data.cpython-310.pyc | Bin 22045 -> 0 bytes .../__pycache__/source_data.cpython-314.pyc | Bin 38044 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 162 -> 0 bytes .../__pycache__/conditions.cpython-312.pyc | Bin 3506 -> 0 bytes .../routers/__pycache__/drugs.cpython-312.pyc | Bin 6416 -> 0 bytes .../__pycache__/sources.cpython-312.pyc | Bin 1076 -> 0 bytes .../__pycache__/translate.cpython-312.pyc | Bin 2789 -> 0 bytes frontend/package-lock.json | 372 +++++++++++++++--- frontend/package.json | 4 +- frontend/vite.config.ts | 9 +- 31 files changed, 359 insertions(+), 57 deletions(-) delete mode 100644 backend/codex_build/__pycache__/__init__.cpython-312.pyc delete mode 100644 backend/codex_build/__pycache__/api.cpython-310.pyc delete mode 100644 backend/codex_build/__pycache__/api.cpython-314.pyc delete mode 100644 backend/codex_build/__pycache__/config.cpython-312.pyc delete mode 100644 backend/codex_build/__pycache__/db.cpython-312.pyc delete mode 100644 backend/codex_build/__pycache__/dependencies.cpython-312.pyc delete mode 100644 backend/codex_build/__pycache__/main.cpython-312.pyc delete mode 100644 backend/codex_build/__pycache__/main.cpython-314.pyc delete mode 100644 backend/codex_build/codex/__pycache__/__init__.cpython-310.pyc delete mode 100644 backend/codex_build/codex/__pycache__/__init__.cpython-314.pyc delete mode 100644 backend/codex_build/codex/__pycache__/neo4j_driver.cpython-310.pyc delete mode 100644 backend/codex_build/codex/__pycache__/neo4j_driver.cpython-314.pyc delete mode 100644 backend/codex_build/codex/services/__pycache__/__init__.cpython-310.pyc delete mode 100644 backend/codex_build/codex/services/__pycache__/__init__.cpython-314.pyc delete mode 100644 backend/codex_build/codex/services/__pycache__/translation_service.cpython-310.pyc delete mode 100644 backend/codex_build/codex/services/__pycache__/translation_service.cpython-314.pyc delete mode 100644 backend/codex_build/neo4j_sources/__pycache__/config.cpython-313.pyc delete mode 100644 backend/codex_build/neo4j_sources/__pycache__/config.cpython-314.pyc delete mode 100644 backend/codex_build/neo4j_sources/__pycache__/db.cpython-313.pyc delete mode 100644 backend/codex_build/neo4j_sources/__pycache__/db.cpython-314.pyc delete mode 100644 backend/codex_build/neo4j_sources/__pycache__/source_data.cpython-310.pyc delete mode 100644 backend/codex_build/neo4j_sources/__pycache__/source_data.cpython-314.pyc delete mode 100644 backend/codex_build/neo4j_sources/routers/__pycache__/__init__.cpython-312.pyc delete mode 100644 backend/codex_build/neo4j_sources/routers/__pycache__/conditions.cpython-312.pyc delete mode 100644 backend/codex_build/neo4j_sources/routers/__pycache__/drugs.cpython-312.pyc delete mode 100644 backend/codex_build/neo4j_sources/routers/__pycache__/sources.cpython-312.pyc delete mode 100644 backend/codex_build/neo4j_sources/routers/__pycache__/translate.cpython-312.pyc diff --git a/.gitignore b/.gitignore index 3c3629e..ffc495e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,30 @@ -node_modules +# 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/backend/codex_build/__pycache__/__init__.cpython-312.pyc b/backend/codex_build/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 3be39dcd67dd4257bd6e517b87021ccb6ae19c85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 154 zcmX@j%ge<81osck$pq1lK?FMZ%mNgd&QQsq$>_I|p@<2{`wUX^%ThnIIJKx)KRF|@ zQr{)DIJ+djK;JpBptvMIFIAzqq%}FK0Y%qvm`!Vub}c5hfQvN bN@-52T@fqLBt{@E1~EP|Gcqz3F#}luojxUf diff --git a/backend/codex_build/__pycache__/api.cpython-310.pyc b/backend/codex_build/__pycache__/api.cpython-310.pyc deleted file mode 100644 index 3081ab718c0a337a92c061e59af086954bf154e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12466 zcma)CYiu0Xb)MJGKDZ>8)LWM25o6g}TUttrV>^r=qTaG4N+#*o%F24UcSsI3`|zDv zNn9^eIWl6W1?!|J+5&M}vMt&OC<-)v0Y%XQ36Sel}#y=`OrYNfa9bHj`QdWd2 zG*7K*Wlg2H?&)Qnzed^MZ>pT)uUR(nHN12!Q_j?~hKWr$@)5||pmGU#_b;L2(o)rb>nRTq1W2}8ujI3j{*OfOF zp_iYz{7gL+eXpz66~|nf!0&5fRBTzL`sUhmV(YqEe%^UVY`dkFUvPGbd9hRMdf!0m zMQ2Ft77vL%&WqyVRik{&Ikq(EY!iDIRPl(g5YlhoYD!hkC}K=}>3v0f$=Q2LFCRzE z6XH>^ALWm&YGOe=E)H;?CsDH~ovKjPzkn zpGEqJIEwU9PM<^i%i<}dpW^gONPk5W{4&Vp1G`U%`4@;#r+=P6_?EB2J?8)T%BP#aVHV=k^Mh zzvP@1FLNoib;SiS#ii5S^F`-naj9FnC|(iMTsp&TXPgVp=+dm1<&}GNHRX(m*KVov zs#p?lit_s@Q1L4FbJ;mB-s)0#MSPtpe9d`XRIX~@QRPqgq`Xmn!+F!u*UI8;VG|#g zFFVh#y(Q+H%girtEL{;*A)L$DPiNHtPwF%LD_(8=R8y4PiH2})ShLcu`<@-Rjk+~? z>0Ivl&Hi$yZ9hP%ykuFki>`0YyPjh;>fZ81&aw_!C!Kj}WJ%|pmg5K3IAVUYQTH9oU$mPjIdExN z*!2KC*3gmX7rEVZqZObqBG>jj-wGN$8R;%81{Tu_>gQYaDyIo*&o8*_K0Y)Bt; z)qU3wFjH?impfG#&4ybK{M@1B4~@>8nzgL)MaTAnMXT@cJf_F9tu8v%tJY6%{xjW%n%E9u2X>d@CbrqX ztJ$kf?uPgwidddqUufA2jz!h2mu4ZDlg_(;@+FRQzro;3}r|Dq!VLImdaFBT8 z)QzgcmBL&!htpuc(BRPY(Rs@cQ?Ilfxr_maPF$Rxx!?-nIoECJASX`~n~oV0=`FUu zT(5$00_R3hv+Gy@iEQJN9W3ITJ#PEX1>#a9256-!q5%fGMoGWXl2xZdYuz?Vl&OW; zWCxval&dFeSD|Gs4uqzV;4>KxcMF^wn7SYC!m$yRm=P*TH&_in+|pMRF-5p-!3ioj zW9~KE1IJh5?5#L$4ey#$2^y6-5MLp-3%63`4Sh1v;daWcHd^&Sx{hC&Z^)=N=O^@S z$lq~qeI7OBg8l|v&>lN&V|yTC8kSpYHe?XVj^s2?So{5-sbG%wqx%nd&NauY+cl>M zKHGnw7pp-8VPA?q);GnqLKC#&N&u$vkHL+p&0nTrK?Ikb42lvUsqM+ zs{j?M^3L{p20f<&^?QmizmIlM8_H`a5z35GOi#79&Qx)lNM;CEI@O>dmnW=t{xo(I zLK2HUVTGEU!;)nvMXMPa6l-sN-ImmF!aA9>v>tDdJZ^~w&M*$B#T;1zv!B2a}HI+h6RhEIv)Ej5?JjOWhd4BPdyDDuY;Xg5c- zf|l1ZYC+BGnr5h)YJOfY_xwV;seM`)&S~;lRBJzcf^?U4!4YoN_U=}??PIgC0EP%7 zIZl-OmR*$%(t@#sL2}d8({JS5R6snMj|VXlkek5 zN3?8P41(swI3zPjS#0>h#8(d=K0F($PoViARAA$Jr2*~dLIjLN$*Ob&4!di6eo~|6 z1d9!b$+!VDo4_DdOt$Y2_@6C-PqriCC}kDafc7d_-qD5jT!crAaCnAg+Kl6Yr9fyq zOLFfss5OZ zp52JNr*@PjWd$d@qw*<&0VMQ~HJl=2Dop#WY8B$8{qRf_nQwXCvXu-dqFvor)MOrg zh3P~TY4QZpaw`Iqy~JfPad*uu99{&s5Oc?q@KM<(d8FMmQyC46c@c>!etmy5iDNX0 zDMU8eFWNU9`$k^_-xR)PoKDBzO#IEp-yFUJn0P)MnsIDdT}%{VsAGr2Y&3(4E4sRk zwoo1h1-~3jRCB}(sYvh}!E)0H)6rR=a~U6zc)RC0hQmgG$t~S(b7yH^8Z;HVT!cKh^)wcth=hK!qknHq0l|<(-B&gub@k? z73oZaCL>>`jFBb3QLp#_hBdnapdjrq?c89f6=vPJR#P_SoqDJ_eyCk|~quOua*S9`~ra`)C>diiCV zK1u&%m$AE)e}C%tqI47L^@QO;t=Xg2Nq9M;HVf_8sFRFu@9;>3-7~8ztimuo{{j3t z4;sFYpHDnTl!I!@yFb-WpmYulb6*{~Jx#MJ}Y?wipC*%_P zdBE~|DERom}@k=FjXgq2Mo}rTZu8)1ITQFuzw&jYZOqbosL}w z?V)}*L1+Mf*+o4ZlCe=qFv#%;S=4Ga&h*P@-Z#q#;lT7D74~~573zx8-XADy>bmmL z4mK?`p{=v=v7&Xf#&(zx`Ys6StJ(?~4yiuFVc$l>0lv$fh7uUdFRIE4OpA6tNUfQ| z?C9i+Vvl8NMgN8lk0pP}V;Q77iiE$dt_sA0%u-flR^g*6X4^-nouDP_K6^qU>zcvu z^+ljgM@I0-f)fa$sk&&-c~0qcduaO9?5opL))8xR#%f#*wZ>IBhIvl4A7+1m6?V#C;2ak;!4`~a1}Cq5%9=^zDY*v5xhAQCeKFZ%Fm8X2S&nHl~C zk@m5e5sm`0!m{irx3ssk71)m*&EJ*nvm42VoKZTu|5%`-rglqPF#;owrRupLg*a?c zC85H6ucHz0mxH{4V-v=X!p} z_O=Ae^tl^DNIJ2+DRb#0Y5oh{5NiqbLuE06rI zM=a0*3#@2$Fyx8`8AD8xTYf&4F@xxLK5(5VnF1@pcwi%wUX+j)`mXRKGfE=Z>t|D zAE+N_^ST%oh4(WZ<0NhkR?J|8X3#Ok$SRq3qjpxq!LsnTaj|N#*-!W$?#aZ#9ASU( zW1=hJ|E8vgEGQImrop>T5GQd5kcXw!ZZD&YW-B1)mTh(MCi{NjfsuYtihDWD?rid4 z!$m3^t%XH$RRi<4V9s|3wID!Y>RawSoG`Ih$AXiqx)_r~u1jkMvh@=8Yhn0mqXsz_ zC*QTkXkud%)>zjj89Qi=#bzB%j>?=mWd^n+Z(cXkr_301QmqSbj9Dvj$MFXa`UN=D zyTirVW8JCWHF8oO30WVdm*XtUB3gU)L|?-OQIhNgmCV4=0RfcNM5{nFxve||rljq$V*mveO39!p4b4iq0Cxa5bZEWeFlN-k1#6#-n6 z(y3P$p{gRXVJSjMfqvK@u>~-=O?GmygGU2ROQ0trHId2*ZmRo#JX;%dtCVX;0_9Wa<*>YrDx(mNx%a+eZ4){9OJ88n*|>`ED$}H*4?gzcIra zLFkgW>pLrbaO$7YO{DQU&^6F=2Lp{b_M^SfZUmD6t(v191&(?GN(7&D21*f-O2>Hw znY2E{eQ*=Vlv#ec3z-x^rbrv+pbaC)6lp`P2bnqwP(2(|?*qV*4ugY=;;hf0|0tf~ z{M>2^CuX31n?+g-$myJJpWwIqwewd$P1|KS}nP_yP2(pX^42WnUKirwGMWI_e** zzo|wt<+jSF79SV&ZfkNKpHQvch0NriAe+Ycg!u$YF8RpqVr&cn1N{^fxK~rxfgRptiZIKUA@E!$S=@a2-a$($qqr+U{K?!d3;&U<_=&_lQW`H!;KE+jJ}Qt$U=5dygtj}{Qg%g$d@}pwQ=dr}T|2@A9OxJ#yQ;oU zCS)$i-%_qAvK$O94Xq8sJD|$T!QfhfVk)?j{(wfw-EZ_t$)zZoGlDQqdC#|+VBaU?R|e*0~5=JVk1a%lCT&36HU zg+mat84$jt8xTre!sT^q0WVsdv6X{KI})^a;o7)}q;o$oK9^h*j3v#v4sP@4@*P9; zz%doP+r>9^+R-5X@idrp7{7?M1MVJ4_G`L-9X zQ+iXudmcH0SP>Mr%E!4LJpj4x!f0Z{yeo%7KtWre8>iOfE9TKDY?GKpvLWGgBkIG{ zbxF{OT?O0GnUc=#}W?lFyO9 z7pL)7!8-mXr}_jmDSlch*1e?1Soh}0@k*Lk!i1r3tZ{M6O#MY_bPcsPQ>Z_n4X1wh z;Or2-XXApNQ~8^LiYXifAGO=t`s-R7&H$gUVs?Yd{WTg~Qt%!Hd>Z<-Eff_gXS&&{ zHY6Seq=@v*HR(|Q(BGpOkSf7M#GKH`ry2w~YMiXTNBt;d`He;nhy#eFVRe&_jb>J>VEZwvsQF zWC4H+y$InKJBYtT@#6>(XN1DXQe)eNZ$FXWRl~)vhv^C)XjP;42=guYuJIm0(#FW2 zAizEMvM+x`u{0dTcv-O!nz+KoSL#%W*g~c#AT1uI=fRSoTM|@bk|IGw>Rk>{Kz9ss z5J9*ju3M^g-z zwY|{5L!uxYz@s&{dV<|!p>b~N^u;g>tNuK83r0Qj{1em{8JN-Ap&?t)ySe0%A9D-w z6b#;rROu20WeNlZ^AwPY!1s)j@M(0YNIgV124pa^JD+{8Y>qN&3=8xEv2yhAk;;kb zQbKkF@EMF+%pn5#x`{3x#;!G3pj>~ z|L+J4CBH3$_e}J^Q+-q;3`_rXF^GR{x31|0^;u(R5bA`z#|5C?4D9N>s_jfX$mSg* zP4^S@!!5<9g$$e(6#qPL6ms|l_ik(fR0trxI+@{OkBbD-hpoy$(YyZga zdp+GV00B{1r-6C>?&Ev!eRps7``jEJ;a==ju8rgVni9t2)Fb?pe`DjgDNf>4?jk4g z{d~}(TIwin>9?v@_7+ruy=|(Ez3r+UZ)?9J=v19Sm+E5mg?@L?qk4i~)f@DwzMxD3)&)gX4A!glY+l~} zhTvv(b8w5gCD^Do2DhqPgH38vuvu-!TbMFH z{Zt1yX~Rgx9;2V#=qIMvC=8{I#`w}Ew5SPi@nT~$)V-{yTIAOi^t6xVipZ@m$lcF! z8<4xXAol>v-Gbc4g4}~FcPnz63UUvz+-Bsq6yzRex!aJty&(4p%WXw&TS4womfMcp zj)L4{Sot3nu33*uJ4TL4JEdKK-R}KDE^kkOGgtrIIuP4CazNTA?O)OE0LDDH0(PD2 zjCyF~uylA;>mz7=bVci~b;dq6a&)9<=fz(y7lAd*X>t46Fo|V0g z_;I3ojgzvQm-;Z{6Ex#EZ^Em0ofQX1j!6Bh;QV~S?D|GdNGA`Ra%9KQ?Wb!?gCi%6y10kIp+9}P4thK-4UL?{eR{z-3w6Md zo7qp~Mq2D#WGBr|-G3eRQqI-4|9=l!{7Jz7iXait2b+ z6_uE(h?k=3)o3(&niz{EqU~;XXFM)mN-IbxiWpCh#YV+=>`GJ-V+nDTCfw03io@eEMZ6q~N5y0! zK6%J3ifv*~^fEOP<>T##NEc6|=vbPK*+aSew42JQ2p)qq3Zo70fH4#1s`PjZeDWy@^OF z8B3^&yUlpe=tS?ZD0YlT!*O+7T=5KGd2F?#vqgTUn6ggg+xRg6^NV~eiO7s*RGnxb~H55tWgj}Jdl1$5^(GUSEBeYYdRdX3z z5YO8k^msU@vT}mS(@qZd1m^u6KJl70cAA5_TU(e$}xTC9RRBWcx5WaRW)^}14kIM7*8tdp@Tbi>=@SgZ7qW41FlG34JCml zVgPN4o*b2<5dbk3jw?ouiKsfBjF?HFfC%~@EyUkBMv8yZfZQo=gj0Di)s~Bcs&xw( z>k&b+Y(XM;nP@!9&{D=l$N6PNl}EH9h2=&gdL88+H+avkIYS|W zQz)dlLm?o*be!VeQ0Uu03AvJ@Q0Q_@R@68!X(EYYWE5N=WK@JgbPHmmp|A?LULxEX z3dxO_-4j}ocB11&&yhs9c=4E zgiRtHH~{UbNqIARW5FOI3W*%=aM_C5MWI%PJfat6DyVixDw#@yf<@c(i=~Vbd!A#_ zIbxFmU;fD^M5eg2+(pZQx4;k}ZxiT;PVUK|DI?bvFkp?i&~R_v}%D>l)Fr1J=x757CG z2(bL0yKPI)3SCwQUq<}^`)(@YNvXp+vbJZY)$>tiRqDfq5fCpO?O+ePXCOD0wL=xnc1`C5cb)Z zIANOuLAmp-0ljnTcf~H`&KduN#wgf@@m0G(_{hp&PSDr)qF^6@qnv=ZODi$p4O|M+ zn2xLRA@rrY%t|vfJQa&$~XZ
u762c<V}L0X+mMKfjQ36C4JVnp+sZAQn#u|&piR-!IPv@G5w_H+xjTvH?gh&+$@VgL$8%VO5#T|6Q|)pz43wgS$+14B zAW3gc;1H^mDahcb1P}l?zT)cx+0u%6p(49Uoa&oCwJ3=CqN(QEi{6Klf<;fQyJ%Xz zeixw3lk8ZqLvweLs8F!re)Oha-Z76Bl|2mrea8@A00+p^_b=7lZUK+~eo zq(hAwtPQnQOy>7iK`qZ@mJArYv;mQf%S0WT%fJZwU0-{b!bCq~^|Z`8 zLboMHFL1mZ3_bXRO#WXvQ)1iX(+D(Mg483BSyQo4P6qj!C0wR<6#M``;{th$uAj_$ zC@3jk6v}lVQ>Ba}r+moxS1KGd0Rl=GqL5$CiLWFbMB~IsPE3U5Niay^v6b=)!OO~V zwkQ7xTrms)dVgV2iCR; z6#o=2*@w9c#YT-G!cwaq$ziC|H*+Gl-RxrwFxbS!U=FFlZ0K)bpRRrrxeGadm*b%N ztEYM?>jh?Rq0Wb}g4K^?8@A>ZfKcuUDeH0TKuCWVsP>$hT~bs{%L#=kk#vm(lZ3C1 zLk^9~I@>Qoe+B{sl7A^4ZSTwYrQYEer9pA0*m*`wzNT4{uMK8um=;CM>8y0^)MY0A zlbEkLBhfJ#vTUTq%0v%&46~MrBWrQX1Sy;#+)OZknpI9GG&>34khNWT9g7uGqUL}K zC4nM#mNjcQm6A76CxYf=_aT&gO-@mFAeqEkXx8YpQ6@)_T1Xd8?D{0-4C?(;{FL_) zOmR!aTye!zaLHn`9eU&|yK(Wm7Z-gs3%;5=fo#R5TQA>yd9Hrve8sNCii4k39GtH> zJl&TK)GP*e&johh*?PD9U$)+_|Eyv6bnr_X=dWG%a^4NU4Ai}=-uYJ6U-_s!aO>pF zlgkdlOWd{1{-qC<(EnGAB%~*E3hX`~I0l=fTgM2ftdfBLDXao!+-< zx~jMj_I2584+2g^AKE-!6|RTH0>vw=6jph{A6TOmpllEJ}mRgUQW&4`B=EcgF?#Z<$%iIMTojreezmmnc_$hvvAF42V z&c7u`J7-+lV(vw2+S1OYEqxICaNUVvW6!Wvauy;gq+Ki1YxeCnP~)(5&2o=XE_n^B z!=UBla~$X5YPl59#|09b*t5>;eZyy#Hgqr6xudBzB(&*-7-8Rg|> zxgb^S&z%@T-WR+EL=1~e%2GhoOhRGQBB9Xe3xuS^$d`l*Wk!71Bw>=xu-HQ7WKUU+ zTk>c-4Kk6oq^vQnr81lWw;-)8>@vid zi#gROGhk#glW3kwtE9aRi>8#xtOhWrK=Id(9{QLOgv3ZF*5zb+Y@7@n&`zYG+cXCg znYakE%w;i=6eBSiN<9%qkX;=V8o}~O2``=jDI0BB3MT+{%4TPQ@;W0|V)1p> zI)V}2aA%tGM~@whqm8o2cKv80bon_LAr7j~BqLLmmDy?tLDA+GQ4Ytj7;qk9IuX5= z!s$SY*~@;t45sm(1!fRtPJ!wx5`{GbOlT}VC=cW2FajnADQ1e3&~Fl1Tb0Y?GWir0 z3{pTE)INEoYN5rsR;^W)*tjh|Sw#n$5KF)&v2t}XU4L1jVi))i-AE)871EV9R#;J( zN{T5gT5R$^psH3md{v&L7%#IS6k>C?<{1~i2{UG2k>5e_5AjpF0i-Ez`3Proz7zaT zaL%{yW8uHJ{-f(hdwz28!NH$K{%8EZ#TSH^mjuCf?2$h(JN*9n+vo2N|M~fQ=YJ&p z#QVUz;6F8OU8>?rD{cjD24<`8bS;!LJt=Y)*&qA4(&p*TWgq9-kgckoZTq6CIa^UV zllha8Y-!o8#+!|^j(^;qEw8xMd$V`8WxjmNmoA&j{n(3UOGTW`yTqZF<#Ntj16{+d zrkhQ(+wZ6!PJS>sU%E59vG)D_xA)I&-ao(bK(?~_*4WLl+2nj>yBWWmp0C_%rl=&a z;nvlgSLf%R>y_>ayxkAAgeL!H0-jT5tE;_}C>y>{-wfli@M5I(hg`grT7 zCEI(CSU){tLprfUn@^uH|0sJOMgtp#a_U;n9PV#YZc>SvaVoU@@%QyU~N_~ zE|!%2xcw={#SsHc84Ht-Il;_$Gk@aFk@rKO^xdX07Q(1ZP9zXR8CR2|<_0o^%=k^J z=UE)o?u6q=UaPi}{n0JMI_-ij*q!k+C7x&yxWP=$A!2>1CI3S}PbOclJWY~cLK1Y) zBvN{YEw3_^N2tL?3PKbxVXBBx8Tn1hAUT;yw=zLqCOJx`{g5juAaWyLr@$mMOcBWR zzD#)vBNL-jDXK(DO3iJsQR=mP{F1DB^1xAYBxyq9qYB+(Mv(GE$<6SgPLOV(_%VJ; zwN8*Wm;|Y2R$UObED28Fv1j_^moAd7OF3_K)>o1(-T1Tpo%1zaA7$>>zxV3xv`yjS$mhR*^=9^1Lf`X@H)M)zM}QZu)8GPZM7Zj4XOD{;WcLO!byM22J_0%n;e#PtleG3(kY<(iT5g+s*$@ z3mo~~Z!G9h=>v#nYq}3{KiuElWP8x+MD)S&@}2|Uht=ghNBs|XxAt0!f86Bh-tYSH zE&-)K-fyMwkf&#_^%L9no*mXtcGwVyfN#3nWU2?&^Ri)n`|^6e`?60}JGj671=K_&trqDK<;*q$^Xw zWg;oQ07;p|KyN{zWXsPcPZlJ5UeX#i4dPJ4_z<4 zxmI!TM61|8s0(LIYX)KDO!zWTToQ5?Fw+DSS26OCQe)(+(hN>t5l{6>CwiH~7fCYd zR2()x_*1mByE7#;Sx6lsodqW4FdbW_xW`E6q@H`cycdhh6jQ|zc?m&$AQzoH!WsmU zAih?|I3%Vr&A0p(#nwPEa09qTMZ~u<7XU-c<}$~SNo`(<-DP@V@Tr?jv>+}B^`vk+ z_Nb(8zNCKI{-~(-&Xo@{A7t(~&m9?@Z#unDG&JWN`t2i6`7&3?%r!4KTh_U7zgGw; z-fBF`{bx_-0qX-<5QP?4TU1Mye5gnDvCID|e{KazP>j2_0oDQ)5| zn3tTcF0QP3jV=()Vb7Wu-M04F#^omjf*L00XY|FHTK^NAWPcSiy~a;j2W)N*o-U>W zW(adz{lJlym*C}L%y11mg5|7b$W+7R+ZX!HpvD?n{FD_wBls)eFio!LoT=nV74S!A zVf8oAo8-H~yLE5y*I{G5!~P@NIWzt5T@8haI~TGf-7uWiKYxqQ1i zpj`TMa0CDP8C+!AORS&cVmb+ zrmmz2!%%)PujPp}G+>vLkS3GISsZ_@1(QVlT^?pt)X!uT^c>4V4+4X`lCky&c651yrVC4KpF>>6p6;!!~1CJwIVR3}nUDXQbJ z$Ae5V?P4LVY$?~Av^xY&%^8IQG_e3W=WUWtO@P@fg1({?DXOfM4x)Nw7|EmzV<{~_ zvt5;AD!hiNCz)h3MsGDc^CN|WSW?jhnuuo4x$N5039uZm6%b=m6tvjo@6*KLgq9%Z za?Oer$g|WRk$jy#6Oz{1cij@^GS?=5h@v*4LWdEMgWl2}&Rcxr=#8V(NACz9`abY2 ziu*nj_uap`C?5Y@)D?i(6SGyD-gn)0 z{h4R6bn~?1k*{LW*ZjG!`M%>vwnbmx7rwrqZq9m(ZuGy~KQp%AttI8owVT&;6WR%W z*~yhP-Z?j4+K#qh46ZH|bu5ARu6(cZ&#D$Y_5b41hhra%eVF(lasS-n)?=S<=I#?N z{@=GDc(A3c2h9C`zNb-ma9|7K4>#5JG}#^=szva_HAQR){t8Gjt%p75h(W=c=-F(j94s;J86>XWBypyF z&?IrfvkdRXItyI|R>&ZNQDcP+VoeFhg*=ICab^Y#LkE20&oSLQSAJAiFu1wm`n!7< zigwO9cmD1-R<7b0%V!q*r;$k8g0p>HX`~m^d$Xp?#=T$Jxy$y$;?6eb1GkUDfCU*3 zs>@LJpv^|{U7jwB>%mbca$w-^=By8kw|5=3K0ItgoZRv!;CBhvdMCd62@Rc=hGWlx zCxqKJywFvmaoqslHRjHNPqf%K7mbmO15QGR;46`F;6sMo#|#2}qX5zZK7gDhh!V66 zYK{;-JsHIS@cX}pB+#=y4>W11cAqIk=|A)G9*qRTjv z7*Qrvpzk+bN}2AH?sp!@)o7nEKa0UJlogr8eyzkPHGGT|nIr?vnaX{VB@^opdd!4O%Ik(`)Veu=>|E`spVH>T1;-Q`a%!nVEN-_<96iMbLLepy{gS!6!4Z z(Qalp)r9`RzSEiu7NP+_7)CI5b*|8u{&0l8)iJ&#VxJw*4#F{gKaEOaMBc2F*Zb*e zz%Z3YD7Z|)TNGp{c!vT~-D$=2Ra0o!j-8=yskd{uw}*-Dj5lDnjd`AGjx@gNOUm%n zfqhxCW2X2jNxo0Lyh#DO`i&H8qo5su<~*W*MRH93A4uZTa!Mmk4E$So{;{Qu7yi#S zj^F&(oa^VD`{!KI&pH1ux%&Uj^?bqg{DRx{3vSylxLW-GFK#DpFz=o2x^d#&6E_Cl z9hfbBKX5zne%JE@_dOte#x@a%2&*sr?wSval%{z-w9*| zU$(41>)n)f7f)9$xj2jK31_od9((<~{fQ5;C5kOIbqI} zWiR}9a`h2giu2|GW$?JpdbgSl^Z}N=Fi+(gAhvX@aDawvaphuh(`Us^S#Og*GY=cd zj+yy={IMrRh%HfUNi-XlWXlR0wmsF5Yi_;U{m&==mUJq)9uQlS@^jS}*YOlEJQz|3 z24YLKoZy|#ELf|5E|mVv9nD4(l1`0?%q;W+E!&EWJ4)RJtI keDUuU$n)Go*M{foxWA}#oUF9|#n#4?CD#95Vnh7@0*S$iwEzGB diff --git a/backend/codex_build/__pycache__/config.cpython-312.pyc b/backend/codex_build/__pycache__/config.cpython-312.pyc deleted file mode 100644 index ee306017f1b0d775bac8014dcbededf321a4bc8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 790 zcmZ`%PiqrF9G%^5{%uTa0v@U$bMa6#*$UN4t%cI;A)-mxq!#wFOm=3Iw3D4MvujOG z5&QrOLOu8aDn0leym~40QWk`&CvTSCdU7^tBSM|S%zr*9H-H-qlUMM^=sjGoS0wVi zZad^C=rUio2*Lt-ob_Od_%ET?r3CJFgAVi%r7h&{KukOo(m<5-U$OVv(X$!@EC_KXF?QR7Wx4nK+MB7^ihw%fFobfaX5&4 z3@KwfVgDYCqYw;R16Yj>l!l4L(*4rnLN-b{GKuRrk;+7roP^?cVU%(Ub zl#1z$bmKHD&1Ysb7nfzTv0+u}%}T8rWnJQG-t38H8O4fKm?j!#u5RROFYl@?ZA08hT%SU;0lsTSKtNU9=`3EC?${gOhdsuvOlwTXo Vo)qp#X#tJqjtldzv;4X|;}3=$)rtTB diff --git a/backend/codex_build/__pycache__/db.cpython-312.pyc b/backend/codex_build/__pycache__/db.cpython-312.pyc deleted file mode 100644 index 9b7cc92c68b9e75f2612a254bfbb870ee488cd2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2138 zcmbVNU2GIp6ux)5QmhALmjC~Yf?#~sLNHkrj(SLT2gCT zNuxv}$B3gW5l6i%#b1|FO|6h>)M2Hc;$DT>fpNrh*qrhE;!UH-yn_|P;qE2IjahE_ zKFnp08^H`S#!j6us#cTnVf_P_bB_x{*xdC4G2%=Q`%PoI5g2aG_jn+TGWTkR<-5bS zS1-F4hxI>k{{jv3#~|#?xD{B_mS6=r%oHb19(>XATob-}X$~YPD z(H$LRwka2KVJgC4{IE@**$20QB9PYEXq%w(uF?kc8DA14?MX?&eOhMzgU8uzc;24pkE60do%8c!Iz+JFvSztcK=IbM81mj}Fp0S!E z<4k-Mc>c%{%NK!HXNCwG<+94=deOVBCOs5WrZw|3c73=74+CL?zYzy zu((5u1&Qat5~_C9gJb1EkI%`sgUWkB%;%GoBcE;P@&brOvX;(W%Pr>~rgyHUcm9&z z#k;{=3^lvy&oCb9zQwH?3ji(q*-Q}KDEF^G`9Hb8K`zS+a-~yM2-6-5bU~V<0@~9h zN&PP?3rZ!Gcs!jGBy7on=5{*%luVJm3vtd73Qv>F$>+!{Y?I5j}=so`0zdyJ5UUO2sO?`qZ$az*uW8^I|$49Y=SNXLHxuq5P)sI``U%& z3lH-{tNEe(`Mv8z$_(9_UF+Mma{k8oTRU#={JHS<*Zh9pJ6Ddb^$e~Yx^d{!cb}-> z^aMROP;3e$*XOT{W##X(>R4|I6A2o;{v4H%_E9;Gg0ey~NTyEffvG(ldbe!_IHbKK z=rrX%!es=iDbKcHDk$@NCZnB<)4ah9x7@z!Tw!o#LF-Ic+33H(Eng=sctn?ciO zVG6ZNRyEWj)3m_7Iy}SMhs|xhePYWHI7TdqyIEh>ujZn3;|+88Z5^7}G(+7qYo61n zqMkF&j~iAse#5&UNR(M9i-1EXxONbiQDt{T@??Le@3fwc^Bf4Yo(&i}eS1jHH zu|zf`s+Q=6M%9-dYqC1@8So#b+Xq`Xt?QaPc-?6cm^Qko+S>xhzBnWXDi+dkdz+4D zL#k>Wf3opSuRr}mZn^EF1h!54)S z5B|U(h*%FE1TTts(|^FL7rn`XAnHMHktmen!8b{xM<2|4-<$c^_kC~QPE8dM#H%-t z!{ZpCFS?n8wFib(0NaS6CSsV8Htvun!5SNFqhmHrz$Pc$WEQgmV;0x}VfHn%X{A;< z*Ik5*!m@}rxUc9^%=i}l+Jik#XeAhC{ zILn{q5f`3{1vI(Qcj~pw7F=~j)bKTH5+;BWoS}kr9$`?gz}+}r?A0qMk1nH!WE*3s z8^+}QU!5>9N^$AGoF3XTR!|SWG_x11M)J!0$Lwre znI0SnB*z9yZDP9# z&f6pXkoPAJ1pkp$dZVoJ8)a4gH_IAzdmd2^{4?nLilU*`QCTtr%pmYURyPtIQBi)p))7uB%fbRL*EChhFK@&x+y$9C;l97VcN76d5v?kL= ztuQItW-2BmBD<5LmeP=5$K7%^eQZgj#_KT+Vd{83ACnS$VaI~xB6la^S@YG+`WFc& zUFRvi`Z7+oX%yF&oH(ITh&ki5-8K;pBC}a{Vy_-JUN|53$RtnB;F2hHmBSwRUC-a& zxw~_(H>_MbnC+LR`?=|1?cKh$FfbPT%Hk=H0!^#N^i~T+ZmY%gHeVH9mWY86g##;a zbDqz&?RALw3lL3!F0M3jg*Hh}7?sS()&_^JhZB+)ZbP_9lv2)#i2n;Q1WX7hiU65! zB`bNM>!%2}Mv+V#OPQ}x#_A_P-VrHpb9j|o2qKjFxIwOQQ;aS7nZt<;A$%!GQ28eZ z;M_CN7=Xr8aOEkOc?K4rfW<$w;;>L1R;Gs&Q-AAXb!JeV zIRdjoAdh`F_ZJ>690BvS=cD85?4UY(1m<4*KG<(OY#f2BCmN8{`%`zP9$Y@Sc5tPC l{r29}6ZqwCm(3q*KTN*_GHg89`AdjX8AxC8X1SUX`xn+JPKy8l diff --git a/backend/codex_build/__pycache__/main.cpython-314.pyc b/backend/codex_build/__pycache__/main.cpython-314.pyc deleted file mode 100644 index 1dec78f1ffb45bdc7bf5c1e3a406bd94ad731b3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3074 zcmb_eTW=f36`t8$UL=YaiJ~aU7gCh$go!OWsa@BuEF`|fOVk%-<*F?jda>GFiEA%+ z+1X)a=}C42s1JQmfEIDlmptSp5B)C`EWpYDLem?9yvdP}q;EYlOX`vsEf92oJ$vTt znK_ql&Y68ZmFz_@c0O?){3U|WKlr3O;vHu99SNaTq#=s#BMn!vDp5(5sa)W5sj8?E z8WFr)jjAyk6TDK5t39+w@R4e-nxF}_kM^laniR5VHKnF$TJW)IM(wBlf{#}R)GW=a zgLF{M(VRL&hlISRnpcPEZ~@h%F;q*``f5q7_o5v7NJnZTGZ(s!AG~W3Eis9;J}n89 zKdFllB_9QpLt{uw=aH7F#m;y4IR^V&3N;Q>t^Yg@=h^_Q$mWroIp0yBCt%H$!`dNV z%g#)HU)tRV;&}P0!5_3L_k&gq{m)fze?J0kP|HCkhqQb*`!yL5+0W3Aur>@i8sRx| z@qU)Zw9#=?KGx!b-WlRQuuWRL=3Dlo;@?;QP<+P-=-V^ZqS0)+j%iTG_lmYh$#Svj zJ06|vElVucQDU*{cGLE(pp04S+Ffn-wqseYy<`v@Bt2J+z`hL|yD(91IRP!ptOxei z?^W;Jy1~+%y?$f%npT~;SG{|OCAeVjhBgb6Pce%Lr>ehT&VLg->Uf>$k zW)X|D76aC6`kv)*KLZvE{1!27?oT3Qb<3az*o{z!x;9H#c3={x$<X9uD_)F#94H}M1MFniETkz9K#K|8V#G)eQSSO zw+##SHxB~`^()5i8F(2`%QZwX)U$NIK;!xc2f#sM~*8{$^w93@V&jSb= zGP!7YOMnsy8V;>jZW4R>s{g3cId8gvwlZ&+58?P!On&2SdMU8E)rGTGtmG5>VIiY|n?!{f8_qv^*0;Ifxhha!lQzlsNM(Y6qd_ zenw2&b#=Xr$r$&Iqx2Rj0VT(}L^vIT892R$K0xGD_sdGIVlFX50~?yOKG4(1ta*(lkkvE7CZ+AUhb2 ze3YlT#ml;7LRACP0BS7SD<@_{$wJF@d1ne!Wfkw({`m)E5bQ}XLHdCJU*UWPh&ZLE ziy~B(t{WaSE4{mz@P^1S3SI^BCHVh&YT$XQw2>;U-u$PMUXN`m!=LvJt~(!(w9ovk zow(IjZv8vpaX&d(#4E81RJ=>;EE>L7Ug2#tjGzdnLf?wetvp`9?>lt4Xx44>ArzHD z4d`SLW}{({<^K=Tk~L5GKgEAT`Ar^f76?aK#?vo2`Tdf3y&Pf=-R@Pf0^seoADDMX z_nvvBBe39}KMwya8s^6k-T<+taFELp@&+g|$$!WqHX%M?QOl-=d8(bb(pIjB5bu;rET-$0&r3VD0M{Q>AK?W?c(5$r5luGsFSkk4CjsHevv_ma zGT;jXKl}P z?11!fh1^jE;zJh#r;>c*6enp;_^D?jM^!_A#KkBogvoH%vy@?3`ay`IK|spkG8Bcu zQjTZ3Ez8zJODx)X%WyS|>=S;Cgk6kR5sOcSEq0pRg(*KNK^Ewaj4^&8Nm%(Nj_|2% z^yW4?xsCE)Aaw(&+h}4No&77i`3&9s7kcAw(d1Tdb}IwO<@aJrOj?z9`Vdy0qk#=H zu%!%aWnXI#yuLLwx|JPTKPAXc0^#f)iehOmgK=Tq*@Ii4uoFd6WNrBIaJzTx87}Qf zyr=ETNE+V}>`rEX+x1J&v*R1t@l7g`kg5y!svq1D?5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HRequp^esX?F kYK4A$d}dx|NqoFsLFFwDo80`A(wtN~kosaKAi=@_0PX)3761SM diff --git a/backend/codex_build/codex/__pycache__/__init__.cpython-314.pyc b/backend/codex_build/codex/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 574806e1c6e16fac439e68dff1452780c40ab71d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 181 zcmdPq_Zh8r#l diff --git a/backend/codex_build/codex/__pycache__/neo4j_driver.cpython-310.pyc b/backend/codex_build/codex/__pycache__/neo4j_driver.cpython-310.pyc deleted file mode 100644 index e2b0663ad204ea568dd30fff57cefd5f33c3c22c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8774 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 diff --git a/backend/codex_build/codex/__pycache__/neo4j_driver.cpython-314.pyc b/backend/codex_build/codex/__pycache__/neo4j_driver.cpython-314.pyc deleted file mode 100644 index a1eac94a750bba2777685a3f22dfa16134affcc8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7272 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 diff --git a/backend/codex_build/codex/services/__pycache__/__init__.cpython-310.pyc b/backend/codex_build/codex/services/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 543fab79a48761f52117b7fc2daf9ef6ec33835a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 124 zcmd1j<>g`kg5y!svq1D?5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!H#equp^esX?F tYK4ArYEfBca%!=De0*kJW=VX!UP0w84x8Nkl+v73JCMd=CLqDW008Ak7_tBW diff --git a/backend/codex_build/codex/services/__pycache__/__init__.cpython-314.pyc b/backend/codex_build/codex/services/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 911e3e57cdbb863309f878dd9ee495d4840ac896..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 190 zcmdPqORa%!=De0*kJW=VX!UP0w84x8Nkl+v73yCPPgT_D#KgBYKf85tRin1L(+ DeiSjL diff --git a/backend/codex_build/codex/services/__pycache__/translation_service.cpython-310.pyc b/backend/codex_build/codex/services/__pycache__/translation_service.cpython-310.pyc deleted file mode 100644 index ac55eab586dc227ddb09a5d2dac57d2c719c26e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4391 zcmbtX&2JmW72lct{KC#5zXH<9_W(yzH06Oxl*qcIB-if|qTv61c$mqohl)GJDy-m7GaH_wKixD~ ziH$wf-5HcNDJ!$_hnhRfCfF-5dXCXmrF!Toe2r4IM5V!fFZJsw_o5{9(jbn+`N z(nL&hKZ(OFP$90bc?r}$ZwZ6(V9V#VE}fqjMk9`z!Fspc^uln>Yy32szng%fgg5Xr z20sFg0z8ZGBx~@NDm!FX`E<9Zq%?!0W3<2Ju+b(N$Y2v$f!VA$B7+UYquDo3fsH&U zqih_vl~qRO9%7RRWK(Q9Uw39WZi>z3Ys`(v4(GbOIwCuguRA(U$=BGE9c9PzHC`Le zJ<6OM@A!!9Sg!Ml5t+hH%5Rf%!RWx``bNi2vC{!rRlxK<+h=;Yy!v|0E!tT>=h(a(NVqE7P97>meBxsWhW10I=W1~}qL(4~f{#oR^ zX00PJ1zoxB(%Ook@BA1*6Jf5ly$C>Edk7l4g{7n&umFGsU0QXEw>|DP{M2j3p=;jR z1_Ok~SYH%GaeD(aFc!VA4dP;ZWN_g|90qN$V82rWjbzv7PjaB!UgR;HvCxX|2aO;M z1Avh)AcVz+LTfR^*X__PjfnldK|$mRdb1{sB=yox(jC9yw_*n!&G8uXSykm|%EC;1 zaJV?)6yB`S!Lbhq1@%3>J~z zJO%TUvryP%l1`H0U)|rDCia)s%*0b`hQA7fg(iJdLl+iiBkn{g-)^YTGx*`2;Yp$_ z3Bd(FQg)PG>4qPJ!|kZxj(_N>sg|i9DG%sRXiw|unZ{=`4PEtPB_sTDs$*|PGWtO4 z8JWQ^%QwQvFO{Zr;)hmd+)y46rajW%Q|O4nt|CH^8arl2)|Fk_Gc)ss^13SBL`?q63K(*84gZC6YZr zI*0<}l@5u2lUf;hKuSulC^3|*xDF5BWu=V#USVU8)rcT}KQay?fJ6RqpF=9Z>Jzo) z29n_5hc<)p>kvF&oreO@l@BUC(qC;3^(L~UvQ+03+|ggtO^6XsbWt2JCSeVTTj-L? z`7r>j>ih^){5TdTusDeYc1at3LIs`wHb;|qLi$6|bopT@gnbprA#KF1_O)er|EdCo zp-_uHH%R}ly)a&Oj}El(=dXrw!wZvZ&=}0D>Y~u_qBshmM^rdO96w2YRv)T6)=Bog z4uVJ><-&yU(+Sh$j%#s$9X!no|LY#|*HCu{3;7kNu=V=z6+eoff#O4Kpm$YGUd8q~ zEWU*Wo`O3zR3Y7N`|dET)f*dL5b+B*5IxB)uZQuP7uNkgkN)3^Z6Pu$);(Q|ox#4s zM<-4byqV2AcQ$-yuyf9t?$Q}2NSr86ogf0H3PT^pydQU*mbdLhzR#Q#SS!94A-y;; zcVzADL9`)S&v!5X$JGGS8J^d9XRs;f5hsrv^Ht-(4!To=6l9=g9oLYXCUl?km{$uz zPf{MVg&xNDK!i*xnUkY8a3m*DSzfqx>-xgt+x1%uwUu`lR+d(U3MmUWp|@J<+v-sq zE{z#^S85_i-8ERi{YE-+j%bdq*<6!W=i1PK0WoKZJD#82P*6g>2YFQAj-&}mmnaO* z#QzQ~n50mYs7=dMBNoxrGSLX7l;Gbc)YAU0y_on~J5nMR9qrrex1LS>l^!oo?aiZr zkyg*;yL#2RaLo}0B;t_Nc4rsDaKCG|fubOT z?oQbQ^y`>nJ2jY{T
    1WCyb;w8>t{3&~Bn0`BQO5!Xuc$_58qKw@EOqYZW`dxIM zlPY-Ip08+d)Ix9b0HVAOZEipTj3x;m{Xt9)TybEv$RvWaM0wC5)k;Kh?;ZgCe&6c% z0>M5SdK>iR4288S&AzyK-rqJRxWaOH> ztZ5h;M$GFCgqB%t$t|_LbOSGd{gKTe1YISqOP@VfUN0T}H#8AC%m4rY diff --git a/backend/codex_build/codex/services/__pycache__/translation_service.cpython-314.pyc b/backend/codex_build/codex/services/__pycache__/translation_service.cpython-314.pyc deleted file mode 100644 index db41a7dd62f4022450387f486d85c88ef2aca510..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7834 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 diff --git a/backend/codex_build/neo4j_sources/__pycache__/config.cpython-313.pyc b/backend/codex_build/neo4j_sources/__pycache__/config.cpython-313.pyc deleted file mode 100644 index 039d540811105ce3bc64b936d036a34d2eaae828..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1044 zcmbtSO;6NN6n*V{w6qK`APGn`4I4s0n@ku>0G*7?7y%t7t%R5mo6>fe;!I!j+EH*d zCT>^}j0RYdxEfr@)}KMr4S9))A1ik1`~hE?FEwFde2aU}Jw5l{)0bC|#W+CD6Xw13 zlLFvdP_z!y1oaQ(%UhrT1Qbf4mncM$99p6mLntJGwa_R?Hl?JZWVSV+lShFPRw7Bd z!Jx>cs;I)WRnbdTtkTt1F)dF--PhUJLh?Whb@G2J2RpM`XpK*jTMEwK8FgQ>Z=|9B zJ+5^tT>H;@{;7{|_w{bj;`ZJJ^AH8dN^5D67kI2FLWkgrm9cbBGMgyIR>PnW~|A8jkAQPSHf;eV9?nQe9Os z4P6{<>_;JD3P(50zzhnL0!4xlJWw2VG_Qym!$P)JHZh~EyB7?rss)S8S224xtz^~A zf{eqP(6Vu_l75(fK$!HS^aah$KhEFJ$^kwGbutv-S#iZRkt^y&ZClKk?o$ss;=Ja# z9<)ut^(uveV3=hnpB<52~@I?ZfYhXmh+f~=GBGdE$t4>t_>HFO)kgdHZpX(j9hYA zxiqa%v8%|dkoGw|J&kmzTsoh>w*WIVXq0uQWK}IHxq7u`Tbij8o%)SNt!&}7=G=(d@lrY%+2AQ}x`Pv=!FlZDi`j4BQt$fxIWw;7Wzq}#MO^B^~?!gd^DBWZd5@5k@B7IxX4mvrZePFP&6kaGMcwi?&b zYP@@MTKj?H?YSK@Zt&;UiuHUC->@mASZ-ln!w@26YinbxS_t6;ql6zjH@hu|!k+6g$hx$8^3s@>IgdSl2%R!lff$ diff --git a/backend/codex_build/neo4j_sources/__pycache__/db.cpython-313.pyc b/backend/codex_build/neo4j_sources/__pycache__/db.cpython-313.pyc deleted file mode 100644 index af5ec7f64ba1ec5901ec7ddb6b9f202b9a6cd320..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2936 zcmbUjTWlQF_0D^|-u2qp8yl05abh8cK()GYr zF^>5_{;Jkhqa+H!k>JDqf=HE0zYr1<1U@QN{fLtwo1to}wmUS*rm{wQ$N^%Ay~L1?^3i3g%*eVj|2V7?r)l-9WBb}jwd9=B{;~9qmUpUy zXSEV_-*l+9K&@KMp@}^8n4=3pOa9~wgX3ngFcu^lsBy9ov_<&o zVZ%7{LLp~I4@XdHEVfSra)nG0%7Grd!BN5BdkGZ{0ia~03~5R>@_bgu|l~^A#Lv%h5MiQ-ge^!?%J{8E3B`NZvNJ^1c zpqSDAH6UkBrOZ%HEqEqdWDaFnd)8Xa<{b9AU#(?FtQzyHo})2;zFgKy&RkW?XPHgi zn$NPO>BF_fKs8O*bA8hc+8gR0j(0Gc*dqXz$X~kq*Sinh>^}6nu}@$9^P@M8zr5D{ zN?kmk+=k;H+6pZa+Ugh}S4fc*kfdo8kfLcEZLBr0$2Bgm^E3rJfn-;zpjsQ?k*>5I zP`U@tQnAP(4U)5m04$LWHF+VqoLpCX*VNvx)V;J5w)25(FV_j*Xyb)W z@=KBNS;F{Ea!Mzpn%m7!JN^JEC%~e+d-XIwafpys@R%ogq9J;6QG!?ACd5;UN&$;d z(}?2%q*8aJ8V0HA|CdziZqx$_&nq8fnb3Fg4s#jUmDz4|S!vRP00XIZJXz~`H&E7$MbtMq1 zH3!m#I#wx=SgqpvF!@kFgG9IrZv*aI)MpE>KT9!B^hA_d8sGs}4ivlU`S4=Ev5?kx z@O>7jP9$mxO1VD=$CW?|IWjE0Ve*9_%)mf}q%%>K|5oaF2Q!`h3BVHhSE}RU<5x#+ zr?l1KjXm8f6_ZX)q#)C{Nc>UuUvld z^EdvuaDCy+#r4t2wb98NQ?IR$S}QNEX#@3>H@c?(+S&7Ud(TF){lcl`Qx{+QO@1TU zb(LG5`1Pr~DU#X0*+$xX>goHBfM{Q(w101@WY0MF9ZVUUV;D&1J4TKOpY-HJ@p`5U z;1^;~u0{SrI{@%YSw?syk>ivfG;4I70KaXl=2*o0NmvT6k>(~6w3sG1!lBUpI^xo{ zv-KEUXIOclz@U@di#Q9_>uFi$!KYBNX&EA8) z2kItFbFNyNuOOZ@%{S()O5-GLLXc7BV<37})1)21JZiTBgg*2R*Y?9Or$_MQ83ZQ) z1R2wWMDShPw0y{sX-Gl__n$@r?7vYXQ4vFc1&-n2ugDh9Nw0ESGAHf3D+^NJ)q|S^ zrVj`2;k21ZOFgTd-x8R%4scS}Js`|PERj)OSK+hBELCk#M)L48wDAcAc~^F4nm(bz y$D5=I)Z>vkeoDuHBf6382e2t{9Cw%JIq{xKIPGub*cv%@U+&{FTLeHT!+!uA#!is{ diff --git a/backend/codex_build/neo4j_sources/__pycache__/db.cpython-314.pyc b/backend/codex_build/neo4j_sources/__pycache__/db.cpython-314.pyc deleted file mode 100644 index 27311d65dca089938c0c25c20e9a5f1502ac3f86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2640 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 F`hR$}4730M diff --git a/backend/codex_build/neo4j_sources/__pycache__/source_data.cpython-314.pyc b/backend/codex_build/neo4j_sources/__pycache__/source_data.cpython-314.pyc deleted file mode 100644 index 49a96de395b33c9dfb6a362bea163ef1c6fca063..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38044 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 diff --git a/backend/codex_build/neo4j_sources/routers/__pycache__/drugs.cpython-312.pyc b/backend/codex_build/neo4j_sources/routers/__pycache__/drugs.cpython-312.pyc deleted file mode 100644 index 879241f7906d77cdd71c1e733b49a5629e4140b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6416 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 D_`V)M diff --git a/backend/codex_build/neo4j_sources/routers/__pycache__/translate.cpython-312.pyc b/backend/codex_build/neo4j_sources/routers/__pycache__/translate.cpython-312.pyc deleted file mode 100644 index ada9a6d4b26a099c40ce128b8cb54c796b952d55..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2789 zcmb_dOKcNI7~Yqki4&6-A%QYg5y4ergH%y9rAQ%g2*99j;QJZsTZThZC{xkO4PDrFy>PS1!f4={p zf4^_$r$E3jK>6{9Tj`&@g77;VIEAmkVB?f12;T}=hznT6Qbx>5aVaau<*X7{vaYyG zWb<;yo%O^$ysu=uSzp{I3X(u1?85Gpxa_$n)qvxE;=i=panfwD7yFj|juq^5;Wpg9 z9N03`@zPA^rWu*|?m@h)Bo!Pa?RXb_UGQ}iPtuEfzHr^EEer9$4w3ZYzW*Fat6#a{ zm-%$#-7P*l3jLuy`2>XEKTVBGM6=Mifyq_$Tlpr6SX9-`jA{`?jl4yuf83y?Zjd(# z(o~a-B8?iRIieYP-J%7AX+DK?HA|4QJ8kG;|1);VkYfABPS4D7FxxXhazw|b9hi>A zPETIdNX`OV+x32)&_YPGJv@|&6VP?1h?T$>ivDIyw$E`uLXz#yQIbqwwG~TEnYK_o z5^Kb+w#1aAMiv=5gkBHVGLWhp)dER3j}r50P*xjADfh(Tz+9 zjN33VHJauz7e{91-)mrMkU?Z55uhTohQkw5gIJgk6^CCkhd3I?L1`U^HS3vZ$X3{H zyPd_JsK;&lEu+zw@C8k&vxqfJDl&WL7EIv4y!P!eI?4pr}& znotsmj-jS#sOT9&TmVG{PJv#C;k%OEW?HJ1H-Su;*ltWLHJzdCa)dRR=5^|Ve#pJW z7wv*s+pQIHpd4GF#wVufhAEy9^x{Nh!6ej-XcyH&WP+HNEh85hS97Mtv}aoRWD;SL zHBdC7=F*Yc?P4~y9nKZ(jz&^>PU$NEAArw{Lsb^49o`Oad8*naw0C@c^!m{o2fu&g z{@%($;?cnQ)i$*}vEJ7COUK~j{=vt+krn06m6gT&XYWTJ9(Y)Mn5o3iRJzap<&r!7 zRYCUot6rf!ctgDYW;wb6=t5D6sF_Uht#^rqpwsxjkWiE7@})aPMXB@^qC2eB8u|e= zc*<&eJP#eNN4X?zt4EpX5!5I-+5+V&X3x%QyzOc}WuQX^6zGkrk$~Zo*+m#~wV*Mn&_P!1h00`l;xeK8pl1h~GzX^9 zS7GiTGmAn6EN|bA<&OrYR@>g)MDfEbJuB|Y(WwWe2YMy;L8W_f3%koF>3)EWhB|Fe zB7qGpk+3~U71W9q>R+IyQW-QhUeLq zO^b4s@>j;Io0RPE`6&mIqeFQ%{4vH?+#o^jdIcy?@`5R*8JP z)}L7K8(bQ%jGS5PJG|c%vHP(=6.0.0" @@ -556,14 +552,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -574,7 +568,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -591,7 +584,6 @@ "version": "0.122.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -604,7 +596,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -621,7 +612,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -638,7 +628,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -655,7 +644,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -672,7 +660,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -689,7 +676,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -706,7 +692,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -723,7 +708,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -740,7 +724,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -757,7 +740,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -774,7 +756,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -791,7 +772,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -808,7 +788,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -825,7 +804,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -842,7 +820,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -859,11 +836,267 @@ "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==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -888,7 +1121,7 @@ "version": "24.12.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -1496,7 +1729,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -1509,6 +1741,19 @@ "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", @@ -1741,7 +1986,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -1810,7 +2054,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1857,6 +2100,12 @@ "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", @@ -1991,6 +2240,15 @@ "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", @@ -2086,7 +2344,6 @@ "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -2119,7 +2376,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2140,7 +2396,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2161,7 +2416,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2182,7 +2436,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2203,7 +2456,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2224,7 +2476,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2245,7 +2496,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2266,7 +2516,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2287,7 +2536,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2308,7 +2556,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2329,7 +2576,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2376,6 +2622,15 @@ "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", @@ -2400,7 +2655,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -2516,14 +2770,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2536,7 +2788,6 @@ "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2643,7 +2894,6 @@ "version": "1.0.0-rc.11", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.11.tgz", "integrity": "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==", - "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.122.0", @@ -2677,7 +2927,6 @@ "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==", - "dev": true, "license": "MIT" }, "node_modules/scheduler": { @@ -2723,7 +2972,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -2755,11 +3003,29 @@ "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==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -2789,7 +3055,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD", "optional": true }, @@ -2848,7 +3113,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/update-browserslist-db": { @@ -2905,7 +3170,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.2.tgz", "integrity": "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==", - "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", diff --git a/frontend/package.json b/frontend/package.json index 9bc8234..b2ee83d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,10 +10,12 @@ "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" + "react-i18next": "^16.6.6", + "tailwindcss": "^4.3.3" }, "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index aab5dff..e02b756 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,11 +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()], + plugins: [ + react(), + tailwindcss(), + ], server: { host: true, port: 9000, + watch: { + usePolling: true, + }, } }) From 318c7fbbd6d52a16561a206b0fe9afda1b4ed59f Mon Sep 17 00:00:00 2001 From: Sebas Taboada Date: Tue, 15 Sep 2026 21:03:42 -0300 Subject: [PATCH 19/23] feat(ui): add modular design system and reusable UI primitives --- frontend/src/App.css | 573 +----------------------- frontend/src/components/ui/Alert.tsx | 39 ++ frontend/src/components/ui/Button.tsx | 63 +++ frontend/src/components/ui/Dropdown.tsx | 153 +++++++ frontend/src/components/ui/Input.tsx | 50 +++ frontend/src/components/ui/Modal.tsx | 79 ++++ frontend/src/components/ui/Select.tsx | 62 +++ frontend/src/components/ui/index.ts | 6 + frontend/src/index.css | 26 +- 9 files changed, 478 insertions(+), 573 deletions(-) create mode 100644 frontend/src/components/ui/Alert.tsx create mode 100644 frontend/src/components/ui/Button.tsx create mode 100644 frontend/src/components/ui/Dropdown.tsx create mode 100644 frontend/src/components/ui/Input.tsx create mode 100644 frontend/src/components/ui/Modal.tsx create mode 100644 frontend/src/components/ui/Select.tsx create mode 100644 frontend/src/components/ui/index.ts diff --git a/frontend/src/App.css b/frontend/src/App.css index 9a1ed0a..5556f13 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,572 +1 @@ -.page-shell { - width: 100%; - margin: 0 auto; - background: #edeef0; - min-height: 100vh; -} - -.top-nav { - background: #4e7f77; - display: flex; - justify-content: space-between; - align-items: center; - padding: 14px clamp(16px, 4vw, 56px); -} - -.top-nav__left, -.top-nav__right { - display: flex; - align-items: center; -} - -.top-nav__left { - gap: 26px; -} - -.brand { - text-decoration: none; - color: #fff; - background: #6b7b92; - border: 2px solid #d7e1e6; - width: 78px; - height: 56px; - display: grid; - place-items: center; - font-size: 12px; - letter-spacing: 0.35px; -} - -.main-links { - display: flex; - gap: 32px; -} - -.main-links a { - color: #ffffff; - text-decoration: none; - font-weight: 600; - font-size: 21px; -} - -.top-nav__right { - gap: 12px; -} - -.top-nav__right select { - width: 146px; - height: 34px; - border-radius: 8px; - border: none; - padding: 0 12px; - background: #f3f4f6; - font-size: 14px; - color: #253242; -} - -.icon-btn { - width: 28px; - height: 28px; - border: none; - background: transparent; - color: #f6f8fa; - display: grid; - place-items: center; - cursor: pointer; - padding: 0; -} - -.icon-btn svg { - width: 21px; - height: 21px; - fill: currentColor; -} - -.content { - width: min(1180px, calc(100% - 56px)); - margin: 0 auto; - padding: 30px 0 56px; -} - -.hero-row { - border-bottom: 1px solid #c7ccd2; - padding-bottom: 20px; - margin-bottom: 28px; - display: flex; - justify-content: space-between; - align-items: start; - gap: 16px; -} - -.hero-row h1 { - margin: 0 0 6px; - color: #101827; - font-size: clamp(2rem, 2.3vw, 2.75rem); - font-weight: 700; - letter-spacing: -0.2px; - line-height: 1.15; -} - -.hero-row p { - margin: 0; - color: #2f4057; - font-size: clamp(1rem, 1.1vw, 1.35rem); - line-height: 1.35; -} - -.help-btn { - border: none; - border-radius: 8px; - background: #4e7f77; - color: #fff; - font-weight: 700; - font-size: 17px; - width: 78px; - height: 42px; - cursor: pointer; -} - -/* Customize the label (the container) */ -.container { - display: block; - position: relative; - padding-left: 35px; - margin-bottom: 12px; - cursor: pointer; - font-size: 22px; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -/* Hide the browser's default checkbox */ -.container input { - position: absolute; - opacity: 0; - cursor: pointer; - height: 0; - width: 0; -} - -/* Create a custom checkbox */ -.checkmark { - position: absolute; - top: 0; - left: 0; - height: 25px; - width: 25px; - background-color: #eee; -} - -/* On mouse-over, add a grey background color */ -.container:hover input ~ .checkmark { - background-color: #ccc; -} - -/* When the checkbox is checked, add a blue background */ -.container input:checked ~ .checkmark { - background-color: #2196F3; -} - -/* Create the checkmark/indicator (hidden when not checked) */ -.checkmark:after { - content: ""; - position: absolute; - display: none; -} - -/* Show the checkmark when checked */ -.container input:checked ~ .checkmark:after { - display: block; -} - -/* Style the checkmark/indicator */ -.container .checkmark:after { - left: 9px; - top: 5px; - width: 5px; - height: 10px; - border: solid white; - border-width: 0 3px 3px 0; - -webkit-transform: rotate(45deg); - -ms-transform: rotate(45deg); - transform: rotate(45deg); -} - -.populate-label { - display: flex; - align-items: center; - font-size: 1.5rem; - font-weight: 700; - color: #0f1826; - position: absolute; - top: 265px; - left: 32px; -} - -.populate-loading-label { - display: flex; - align-items: center; - justify-content: center; - text-align: center; - font-size: 1.2rem; - font-weight: 500; - color: #0f1826; - position: absolute; - top: 450px; - left: 10px; - width: 300px; - height: 5vh; - box-sizing: border-box; -} - -.populateDrugbank-checkbox { - display: flex; - align-items: center; - gap: 12px; - cursor: pointer; - user-select: none; - position: absolute; - top: 320px; - left: 90px; - font-size: 1.2rem; -} - -.populateSnomed-checkbox { - display: flex; - align-items: center; - gap: 12px; - cursor: pointer; - user-select: none; - position: absolute; - top: 345px; - left: 90px; - font-size: 1.2rem; -} - -.populateRxNorm-checkbox { - display: flex; - align-items: center; - gap: 12px; - cursor: pointer; - user-select: none; - position: absolute; - top: 370px; - left: 90px; - font-size: 1.2rem; -} - -.populateICD11-checkbox { - display: flex; - align-items: center; - gap: 12px; - cursor: pointer; - user-select: none; - position: absolute; - top: 395px; - left: 90px; - font-size: 1.2rem; -} - - -.populate-btn { - border: none; - border-radius: 8px; - background: #4e7f77; - color: #fff; - font-weight: 700; - font-size: 17px; - width: 150px; - height: 50px; - cursor: pointer; - position: absolute; - top: 425px; - left: 75px; -} - -.populate-btn:disabled { - cursor: not-allowed; - opacity: 0.6; -} - -.populate-overlay { - position: absolute; - top: 425px; - left: 75px; - width: 150px; - height: 50px; - background-color: rgba(255, 255, 255, 0.85); - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - z-index: 10; - border-radius: 8px; /* Match your button's corners if needed */ - font-size: 17px; - color: #1e293b; - font-weight: 700; -} - -.spinner { - width: 16px; - height: 16px; - border: 2px solid #cbd5e1; - border-top-color: #2563eb; - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -.tool-panel { - border-bottom: 1px solid #c7ccd2; - padding: 4px 0 30px; -} - -.tool-panel + .tool-panel { - margin-top: 14px; - border-bottom: none; - padding-bottom: 0; -} - -.tool-panel h2 { - margin: 0 0 22px; - color: #0f1826; - text-align: center; - font-size: clamp(1.8rem, 2vw, 2.25rem); - font-weight: 700; -} - -.search-surface { - border: 1px solid #d7dbe0; - border-radius: 12px; - background: #f8fafc; - padding: 14px; -} - -.field-stack label { - display: block; - margin-bottom: 7px; - color: #384a60; - font-size: 1.12rem; - font-weight: 600; -} - -.field-stack select, -.search-row input, -.translate-grid input, -.results-select { - width: auto; - height: 44px; - border: 1px solid #d7dbe0; - border-radius: 9px; - background: #f4f5f7; - padding: 0 12px; - color: #202e3d; - font-size: 1.02rem; - box-sizing: border-box; -} - -.search-grid { - display: grid; - grid-template-columns: minmax(200px, 250px) 1fr; - gap: 14px; - align-items: end; -} - -.search-row { - display: grid; - grid-template-columns: 1fr 170px; - gap: 10px; -} - -.search-row button, -.translate-btn { - height: 44px; - border: none; - border-radius: 9px; - background: #8e97a6; - color: #fff; - font-size: 1rem; - font-weight: 700; - cursor: pointer; -} - -.results-block { - margin-top: 18px; -} - -.message { - margin-bottom: 10px; - padding: 8px 10px; - border-radius: 8px; - font-size: 0.95rem; -} - -.message--error { - color: #9c2c2c; - background: #fdecec; - border: 1px solid #f6c8c8; -} - -.message--info { - color: #2f4d69; - background: #eaf2fb; - border: 1px solid #cfe1f4; -} - -.message--muted { - color: #667085; -} - -.results-block h3 { - margin: 0 0 10px; - text-align: center; - color: #111827; - font-size: clamp(1.45rem, 1.8vw, 2rem); -} - -.results-table-wrap { - width: 100%; - border: 1px solid #dce2e8; - border-radius: 10px; - background: #fff; - overflow-x: auto; -} - -.results-table { - width: 100%; - border-collapse: collapse; - min-width: 680px; -} - -.results-table th, -.results-table td { - padding: 10px 12px; - text-align: left; - border-bottom: 1px solid #e7ecf1; - vertical-align: top; -} - -.results-table th { - background: #eff4f8; - color: #223246; - font-size: 0.95rem; -} - -.results-table td { - color: #2e435a; -} - -.results-table tbody tr:nth-child(even) { - background: #fbfcfe; -} - -.results-table tbody tr.row-selected { - background: #e5f1ef; -} - -.results-table tbody tr:last-child td { - border-bottom: none; -} - -.results-select { - display: flex; - justify-content: space-between; - align-items: center; - border: 1px solid #e2e5e9; - color: #8f9bb0; -} - -.translate-grid { - display: grid; - grid-template-columns: minmax(200px, 250px) 170px 1fr; - gap: 10px; - align-items: end; -} - -.translate-grid input::placeholder, -.search-row input::placeholder { - color: #9ca6b8; -} - -button:focus-visible, -select:focus-visible, -input:focus-visible, -a:focus-visible { - outline: 3px solid #2a5e59; - outline-offset: 2px; -} - -@media (max-width: 1100px) { - .top-nav { - padding: 12px 16px; - } - - .main-links a { - font-size: 17px; - } - - .top-nav__left { - gap: 16px; - } - - .help-btn { - width: 72px; - height: 40px; - font-size: 15px; - } -} - -@media (max-width: 840px) { - .top-nav { - flex-direction: column; - align-items: stretch; - gap: 12px; - } - - .top-nav__right { - justify-content: flex-end; - } - - .hero-row { - flex-direction: column; - gap: 12px; - } - - .search-grid, - .translate-grid, - .search-row { - grid-template-columns: 1fr; - } - - .content { - width: calc(100% - 20px); - padding-top: 18px; - } - - .main-links { - gap: 18px; - } - - .top-nav__right { - gap: 8px; - } - - .hero-row h1 { - font-size: 1.9rem; - } - - .tool-panel h2 { - font-size: 1.6rem; - text-align: left; - } - - .results-block h3 { - text-align: left; - } -} \ No newline at end of file +/* Global application styles if any */ \ No newline at end of file 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 ( + + ) +} 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 && ( + + )} +
    + {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 && ( + + )} +
    + +
    + + + +
    +
    + {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/index.css b/frontend/src/index.css index af968bf..2420e5f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,9 +1,14 @@ -@import url('https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;600;700&display=swap'); +@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 { @@ -14,4 +19,23 @@ body, 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; +} + + From 7cdf25248edf513cac01668bd7fbac014e7e8df5 Mon Sep 17 00:00:00 2001 From: Sebas Taboada Date: Tue, 15 Sep 2026 21:03:47 -0300 Subject: [PATCH 20/23] feat(frontend): refactor domain components and responsive navigation --- frontend/src/App.tsx | 708 +++--------------- .../components/import/ImportLanguageModal.tsx | 232 ++++++ .../components/language/LanguageSelector.tsx | 78 ++ frontend/src/components/layout/Header.tsx | 242 ++++++ .../components/populate/PopulateDropdown.tsx | 188 +++++ .../src/components/results/ResultsTable.tsx | 153 ++++ frontend/src/components/search/SearchBar.tsx | 91 +++ .../translation/TranslationPanel.tsx | 143 ++++ frontend/src/services/api.ts | 82 ++ frontend/src/types/codex.ts | 83 ++ 10 files changed, 1404 insertions(+), 596 deletions(-) create mode 100644 frontend/src/components/import/ImportLanguageModal.tsx create mode 100644 frontend/src/components/language/LanguageSelector.tsx create mode 100644 frontend/src/components/layout/Header.tsx create mode 100644 frontend/src/components/populate/PopulateDropdown.tsx create mode 100644 frontend/src/components/results/ResultsTable.tsx create mode 100644 frontend/src/components/search/SearchBar.tsx create mode 100644 frontend/src/components/translation/TranslationPanel.tsx create mode 100644 frontend/src/services/api.ts create mode 100644 frontend/src/types/codex.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3df0f4d..4e63a8c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,81 +1,12 @@ -import './App.css' import { useTranslation } from 'react-i18next' -import { useEffect, useState, type KeyboardEvent } from 'react' - -interface CountryOption { - code: string; - label: string; -} - -// Map language codes to available countries -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' }, - ], -}; - -interface LanguagesResponse { - languages: string[] -} - -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 -} - -interface SearchResponse { - source_id: string - source_name: string - name: string - brand: string - type: string - country: string - language: string - uploaded_at: string -} - -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 -} - -interface TranslateResponse { - found: boolean - results: TranslateResultRow[] -} - -const API_BASE_URL = 'http://localhost:8000' -const FALLBACK_LANGUAGES = ['en', 'es', 'fr'] +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() @@ -85,91 +16,63 @@ function App() { const [availableLanguages, setAvailableLanguages] = useState(FALLBACK_LANGUAGES) const [searchLanguage, setSearchLanguage] = useState('all') const [targetLanguage, setTargetLanguage] = useState('es') - const [targetCountry, setTargetCountry] = useState("MX") + 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 [importError, setImportError] = useState('') - const [isImporting, setIsImporting] = useState(false) - const [importLanguage, setImportLanguage] = useState(null) - const [importMessage, setImportMessage] = useState('') const [hasBrand, setHasBrand] = useState(false) - const [progress, setProgress] = useState(''); const getLanguageLabel = (code: string) => { const raw = code.trim() const normalized = raw.toLowerCase() - // Backend may return either ISO codes (en) or full names (English). - if (normalized.length > 3) { - return raw - } - - try { - const displayNames = new Intl.DisplayNames([i18n.language], { type: 'language' }) - return displayNames.of(normalized) ?? normalized.toUpperCase() - } catch { - return normalized.toUpperCase() + 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 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 = (event: React.ChangeEvent) => { - const newLang = event.target.value; - const newCountry = getFirstCountryForLanguage(newLang); + const available = LANGUAGE_COUNTRY_MAP[langCode] ?? [] + return available.length > 0 ? available[0].code : '' + } - // Update React state - setTargetLanguage(newLang); - setTargetCountry(newCountry); + const handleLanguageChange = (newLang: string) => { + const newCountry = getFirstCountryForLanguage(newLang) + setTargetLanguage(newLang) + setTargetCountry(newCountry) setTranslatedName('') setTranslatedBrand('') - }; - - const handleImportLanguageChange = (event: React.ChangeEvent) => { - if (event.target.files && event.target.files.length > 0) { - setImportLanguage(event.target.files[0]) - } - }; + setTranslateError('') + } const loadLanguages = async (isActive: boolean) => { - 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 - const nextLanguages = Array.isArray(data.languages) && data.languages.length > 0 ? data.languages : FALLBACK_LANGUAGES - 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])) - } + 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 } @@ -180,10 +83,7 @@ function App() { .map((row) => row.translation) .filter((name): name is string => Boolean(name && name.trim())) - if (names.length === 0) { - return '-' - } - + if (names.length === 0) return '-' return Array.from(new Set(names)).join(', ') } @@ -192,10 +92,7 @@ function App() { .map((row) => row.brand) .filter((brand): brand is string => Boolean(brand && brand.trim())) - if (brands.length === 0) { - return '-' - } - + if (brands.length === 0) return '-' return Array.from(new Set(brands)).join(', ') } @@ -214,33 +111,13 @@ function App() { setTranslatedBrand('') try { - const response = await fetch(`${API_BASE_URL}/search?term=${encodeURIComponent(searchQuery.toLowerCase())}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query: searchQuery, - 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 + const data = await searchDrug(searchQuery) if (!data || !data.name) { setSearchResults([]) setSearchError('No results found') } else { - if (data.brand) { - setHasBrand(true) - } else { - setHasBrand(false) - } + setHasBrand(Boolean(data.brand)) setSearchResults([data]) setSearchError('') } @@ -252,25 +129,16 @@ function App() { } } - const handleKeyPress = (e: KeyboardEvent) => { - if (e.key === 'Enter') { - void handleSearch() - } - } - - const handleTranslateSelected = async ( - lang = targetLanguage, - country = targetCountry - ) => { + const handleTranslateSelected = async () => { if (!selectedResult) { setTranslateError('Select a search result first') return } - const validCountries = (LANGUAGE_COUNTRY_MAP[lang] ?? []).map((c) => c.code); - const finalCountry = validCountries.includes(country) - ? country - : getFirstCountryForLanguage(lang); + const validCountries = (LANGUAGE_COUNTRY_MAP[targetLanguage] ?? []).map((c) => c.code) + const finalCountry = validCountries.includes(targetCountry) + ? targetCountry + : getFirstCountryForLanguage(targetLanguage) setIsTranslating(true) setTranslateError('') @@ -278,24 +146,7 @@ function App() { setTranslatedBrand('') try { - const response = await fetch(`${API_BASE_URL}/translate`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - term: selectedResult.name, - lang: lang, - country: finalCountry - }), - }) - - if (!response.ok) { - const errorBody = await response.json().catch(() => null) - throw new Error(errorBody?.detail ?? 'Translation request failed') - } - - const payload = (await response.json()) as TranslateResponse + 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) : '-' @@ -311,413 +162,78 @@ function App() { } } - const handleImportLanguageLocal = async () => { - setImportMessage('') - setImportError('') - if (!importLanguage) { - setImportError('Please select a language file!') - return - } - setIsImporting(true) - const formData = new FormData() - formData.append("file", importLanguage) - - try { - const response = await fetch(`${API_BASE_URL}/packs/load`, { - method: 'POST', - body: formData, - }) - - if (!response.ok) { - throw new Error('Failed to import language') - } - - const data = await response.json(); - - loadLanguages(true) - setImportError('') - setImportMessage(`Success! ${data.message}`); - } catch (error) { - setImportError(`Error: ${(error as Error).message}`); - } finally { - setIsImporting(false) - } - } - - const languages = availableLanguages.map((code) => ({ code, label: getLanguageLabel(code) })) - - const [isDrugbankSelected, setIsDrugbankSelected] = useState(false); - const [isSnomedSelected, setIsSnomedSelected] = useState(false); - const [isRxNormSelected, setIsRxNormSelected] = useState(false); - const [isICD11Selected, setIsICD11Selected] = useState(false); - const [isPopulationLoading, setIsPopulationLoading] = useState(false); - - const handlePopulateClick = async () => { - let sources: string[] = [] - if (isDrugbankSelected) { - sources.push("drugbank") - } - if (isSnomedSelected) { - sources.push("snomed") - } - if (isRxNormSelected) { - sources.push("rxnorm") - } - if (isICD11Selected) { - sources.push("icd11") - } - setIsPopulationLoading(true); - setProgress('Starting population...'); - try { - const response = await fetch('http://localhost:8000/api/populate-sources', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ selectedSources: sources }), - }); - - 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'); - - // Keep the incomplete last line in the buffer - 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 JSON chunk:', err); - } - } - } - setProgress('Successfully populated Neo4j!'); - } catch (error) { - console.error('Error during streaming:', error); - setProgress('Error processing request.'); - } finally { - setIsPopulationLoading(false); - } - }; + const languages: LanguageOption[] = availableLanguages.map((code) => ({ + code, + label: getLanguageLabel(code), + })) return ( -
    + ) +} diff --git a/frontend/src/components/translation/TranslationPanel.tsx b/frontend/src/components/translation/TranslationPanel.tsx new file mode 100644 index 0000000..211631c --- /dev/null +++ b/frontend/src/components/translation/TranslationPanel.tsx @@ -0,0 +1,143 @@ +import React from 'react' +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 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: 'No countries available' }] + + return ( +
    +
    + 🌐 +
    +

    + Translate & Cross-Reference Drug +

    +

    + Selected drug: {selectedResult.name} ({selectedResult.language.toUpperCase()}) +

    +
    +
    + + {/* Target Language & Country selectors */} +
    + {/* Target Language */} +
    + onTargetCountryChange(e.target.value)} + /> +
    + + {/* Translate Action */} +
    + +
    +
    + + {/* Error / Info State */} + {translateError && } + + {/* Translation Result Card */} + {(translatedName || isTranslating) && ( +
    +
    + Cross-Referenced Match +
    + +
    +
    +
    + Original ({selectedResult.language.toUpperCase()}) +
    +
    + {selectedResult.name} +
    +
    + +
    +
    + Translation ({targetLanguage.toUpperCase()}) +
    +
    + {translatedName || (isTranslating ? '...' : '-')} +
    +
    + +
    +
    + Brand in {targetCountry.toUpperCase()} +
    +
    + {translatedBrand || (isTranslating ? '...' : '-')} +
    +
    +
    +
    + )} +
    + ) +} 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'] From e2be5100eef08cb127ab47e2d3f067149a562e7a Mon Sep 17 00:00:00 2001 From: Sebas Taboada Date: Tue, 15 Sep 2026 21:28:23 -0300 Subject: [PATCH 21/23] fix(backend): resolve canonical term in translate response and suppress ide warning --- backend/codex_build/api.py | 4 +++- backend/codex_build/neo4j_sources/source_data.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/codex_build/api.py b/backend/codex_build/api.py index cfb30b3..0b56ef1 100644 --- a/backend/codex_build/api.py +++ b/backend/codex_build/api.py @@ -322,8 +322,10 @@ def translate_term(body: TranslateRequest): ) ] + resolved_canonical = raw.get("canonical") if isinstance(raw, dict) and raw.get("canonical") else body.term + return TranslateResponse( - canonical=body.term, + canonical=resolved_canonical, results=results, ) diff --git a/backend/codex_build/neo4j_sources/source_data.py b/backend/codex_build/neo4j_sources/source_data.py index 7f5d133..8b67560 100644 --- a/backend/codex_build/neo4j_sources/source_data.py +++ b/backend/codex_build/neo4j_sources/source_data.py @@ -16,7 +16,7 @@ from collections import deque import requests -from neo4j import GraphDatabase, WRITE_ACCESS +from neo4j import GraphDatabase, WRITE_ACCESS # type: ignore from dotenv import load_dotenv load_dotenv(os.path.join(os.path.dirname(__file__), ".env")) From 7c58469908e0d4b67737fb4b228077f89c597155 Mon Sep 17 00:00:00 2001 From: Sebas Taboada Date: Tue, 15 Sep 2026 21:28:24 -0300 Subject: [PATCH 22/23] style(frontend): update tailwind v4 class suggestions in components --- frontend/src/components/populate/PopulateDropdown.tsx | 2 +- frontend/src/components/search/SearchBar.tsx | 2 +- frontend/src/components/translation/TranslationPanel.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/populate/PopulateDropdown.tsx b/frontend/src/components/populate/PopulateDropdown.tsx index 223998b..40c3671 100644 --- a/frontend/src/components/populate/PopulateDropdown.tsx +++ b/frontend/src/components/populate/PopulateDropdown.tsx @@ -166,7 +166,7 @@ export const PopulateDropdown: React.FC = ({ fullWidth = {/* Progress / Status Message */} {progress && ( -
    +
    {progress}
    )} diff --git a/frontend/src/components/search/SearchBar.tsx b/frontend/src/components/search/SearchBar.tsx index 4738e1f..54a4c2e 100644 --- a/frontend/src/components/search/SearchBar.tsx +++ b/frontend/src/components/search/SearchBar.tsx @@ -80,7 +80,7 @@ export const SearchBar: React.FC = ({ size="md" onClick={onSearch} isLoading={isLoading} - className="sm:w-auto w-full px-6 h-[42px] shrink-0" + className="sm:w-auto w-full px-6 h-10.5 shrink-0" > {t('common.search') || 'Search'} diff --git a/frontend/src/components/translation/TranslationPanel.tsx b/frontend/src/components/translation/TranslationPanel.tsx index 211631c..6f7ddfa 100644 --- a/frontend/src/components/translation/TranslationPanel.tsx +++ b/frontend/src/components/translation/TranslationPanel.tsx @@ -91,7 +91,7 @@ export const TranslationPanel: React.FC = ({ size="md" onClick={onTranslate} isLoading={isTranslating} - className="w-full sm:w-auto px-6 h-[42px]" + className="w-full sm:w-auto px-6 h-10.5" > Translate Selected From adf31d3bffa14ed303147f35ab0993e210a04bdf Mon Sep 17 00:00:00 2001 From: Sebas Taboada Date: Thu, 17 Sep 2026 12:32:18 -0300 Subject: [PATCH 23/23] fix(i18n): connect TranslationPanel to useTranslation and add missing locale keys --- .../translation/TranslationPanel.tsx | 23 +++++++++++-------- frontend/src/i18n/locales/de.json | 9 +++++++- frontend/src/i18n/locales/en.json | 9 +++++++- frontend/src/i18n/locales/es.json | 9 +++++++- frontend/src/i18n/locales/fr.json | 9 +++++++- 5 files changed, 45 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/translation/TranslationPanel.tsx b/frontend/src/components/translation/TranslationPanel.tsx index 6f7ddfa..28590d6 100644 --- a/frontend/src/components/translation/TranslationPanel.tsx +++ b/frontend/src/components/translation/TranslationPanel.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { Button, Select, Alert } from '../ui' import type { SearchResultRow, LanguageOption, CountryOption } from '../../types/codex' @@ -31,6 +32,8 @@ export const TranslationPanel: React.FC = ({ translatedBrand, translateError, }) => { + const { t } = useTranslation() + const languageOptions = languages.map((lang) => ({ value: lang.code, label: `${lang.label} (${lang.code.toUpperCase()})`, @@ -42,7 +45,7 @@ export const TranslationPanel: React.FC = ({ value: c.code, label: `${c.label} (${c.code.toUpperCase()})`, })) - : [{ value: '', label: 'No countries available' }] + : [{ value: '', label: t('home.noCountries') || 'No countries available' }] return (
    @@ -50,10 +53,10 @@ export const TranslationPanel: React.FC = ({ 🌐

    - Translate & Cross-Reference Drug + {t('home.localizeTitle') || 'Translate & Cross-Reference Drug'}

    - Selected drug: {selectedResult.name} ({selectedResult.language.toUpperCase()}) + {t('home.selectedDrug') || 'Selected drug:'} {selectedResult.name} ({selectedResult.language.toUpperCase()})

    @@ -64,7 +67,7 @@ export const TranslationPanel: React.FC = ({
    = ({ isLoading={isTranslating} className="w-full sm:w-auto px-6 h-10.5" > - Translate Selected + {t('common.translate') || 'Translate Selected'}
    @@ -105,13 +108,13 @@ export const TranslationPanel: React.FC = ({ {(translatedName || isTranslating) && (
    - Cross-Referenced Match + {t('home.crossReferencedMatch') || 'Cross-Referenced Match'}
    - Original ({selectedResult.language.toUpperCase()}) + {t('home.original') || 'Original'} ({selectedResult.language.toUpperCase()})
    {selectedResult.name} @@ -120,7 +123,7 @@ export const TranslationPanel: React.FC = ({
    - Translation ({targetLanguage.toUpperCase()}) + {t('home.translation') || 'Translation'} ({targetLanguage.toUpperCase()})
    {translatedName || (isTranslating ? '...' : '-')} @@ -129,7 +132,7 @@ export const TranslationPanel: React.FC = ({
    - Brand in {targetCountry.toUpperCase()} + {t('home.brandInCountry') || 'Brand in'} {targetCountry.toUpperCase()}
    {translatedBrand || (isTranslating ? '...' : '-')} diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 9fd6c8b..740da7b 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -22,9 +22,16 @@ "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" + "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 index 3856e30..98ba018 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -22,9 +22,16 @@ "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" + "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 index 8efc4ee..66f3c04 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -22,9 +22,16 @@ "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" + "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 index cdf6b44..c4290bd 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -22,9 +22,16 @@ "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" + "importTitle": "Importer un fichier de langue", + "crossReferencedMatch": "Correspondance Croisée", + "original": "Original", + "translation": "Traduction", + "brandInCountry": "Marque en" } }
    -
    - - -
    - - - - - - - - - -
    -
    - -
    -
    -

    {t('sides.populateLabel')}

    - - -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - {isPopulationLoading && ( -
    -
    - {t('sides.populateLoader')} -
    - )} - - -
    - -

    {progress}

    -
    - -
    -
    +
    + {/* Top Navigation */} +
    loadLanguages(true)} /> + + {/* Main Content Area */} +
    + {/* Hero Section */} +
    -

    {t('home.pageTitle')}

    -

    {t('home.pageDescription')}

    +

    + {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('home.searchTitle')}

    -
    -
    -
    - - -
    -
    - setSearchQuery(e.target.value)} - onKeyDown={handleKeyPress} - /> - -
    -
    - -
    -

    {t('home.resultsTitle')}

    - {searchError &&
    {searchError}
    } - {translateError &&
    {translateError}
    } - {isTranslating &&
    Translating selected result...
    } - {searchResults.length > 0 ? ( -
    - - - - {hasBrand && } - {!hasBrand && } - - {hasBrand && } - - - - {hasBrand && } - {!hasBrand && } - - {hasBrand && } - {!hasBrand && } - - - - {searchResults.map((row, index) => ( - { - setSelectedResult(row) - setTranslatedName('') - setTranslateError('') - setTranslatedBrand('') - }} - style={{ cursor: 'pointer' }} - > - - {hasBrand && } - {hasBrand && } - {!hasBrand && } - - - - ))} - -
    Drug Name for BrandDrug NameBrandTypeLanguage for BrandLanguageCountries for BrandCountries
    {row.name}{row.brand}brand name drug{row.type}{row.language}{row.country ?? '-'}
    -
    - ) : ( - - )} - - {selectedResult && ( -
    -
    -
    - - -
    -
    - - -
    -
    - -
    -
    - -
    - - - - - - - - - - - - - - - -
    Original Drug Name (Languages)Drug Name in {targetLanguage.toUpperCase()}Drug Brand Name in {targetCountry.toUpperCase()}
    {selectedResult.name} ({selectedResult.language}){translatedName || '-'}{translatedBrand || '-'}
    -
    -
    - )} -
    -
    -
    - -
    -
    -

    {t('home.importTitle')}

    -
    - - -
    -
    -
    - {isImporting &&
    Importing language...
    } - {importError &&
    {importError}
    } - {importMessage &&
    {importMessage}
    } -
    -
    + {/* Search & Results Panel */} +
    + + + { + setSelectedResult(result) + setTranslatedName('') + setTranslatedBrand('') + setTranslateError('') + }} + hasBrand={hasBrand} + searchError={searchError} + /> + + {selectedResult && ( + + )}
    -
    ) 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 */} + + + {/* Modal Dialog */} + +
    + {/* Drag & Drop Area */} +
    fileInputRef.current?.click()} + className={`border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-colors ${ + isDragging + ? 'border-emerald-500 bg-emerald-50/50' + : selectedFile + ? 'border-emerald-300 bg-emerald-50/20' + : 'border-slate-300 hover:border-emerald-400 hover:bg-slate-50/60' + }`} + > + + + {selectedFile ? ( +
    + 📄 + {selectedFile.name} + + {(selectedFile.size / 1024).toFixed(1)} KB + + + Click to choose another file + +
    + ) : ( +
    +
    + + + +
    +
    + Click to upload + or drag and drop +
    +

    Supported format: JSON (.json)

    +
    + )} +
    + + {/* Status Messages */} + {importError && } + {importMessage && } + + {/* Action Buttons */} +
    + + +
    + +
    + + ) +} 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 ( + ( + + )} + > +
    + {languages.map((lang) => ( + + ))} +
    +
    + ) +} 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 ( + <> +
    +
    + {/* Left side: Brand and main links */} + + + {/* Desktop Controls (md: and up) */} +
    + + + + +
    + + + + + + + +
    +
    + + {/* Mobile Hamburger / Close Button (md:hidden) */} +
    + +
    +
    +
    + + {/* 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..223998b --- /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.populateLabel') || 'Populate local source(s):'} +

    + Neo4j +
    + + {/* Sources Checkboxes */} +
    + {SOURCES.map((source) => ( + + ))} +
    + + {/* Progress / Status Message */} + {progress && ( +
    + {progress} +
    + )} + + {/* Action Button */} + +
    + ) +} 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 && } + + + + + + + {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' + }`} + > + + {hasBrand && } + + + + + ) + })} + +
    + {hasBrand ? 'Drug Name for Brand' : 'Drug Name'} + BrandType + {hasBrand ? 'Language for Brand' : 'Language'} + + {hasBrand ? 'Countries for Brand' : 'Countries'} +
    + {selected && ( + + )} + {row.name} + {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..4738e1f --- /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 */} +
    + onSearchQueryChange(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={t('home.sourcePlaceholder') || 'Search RxNorm drug, brand or synonym...'} + icon={ + + + + } + /> +
    + +
    +
    +
+
+ + +
+ + + + + + + + + +
+
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+
+
+

{t('home.pageTitle')}

+

{t('home.pageDescription')}

+
+ +
+ +
+

{t('home.searchTitle')}

+
+
+
+ + +
+
+ setSearchQuery(e.target.value)} + onKeyDown={handleKeyPress} + /> + +
+
+ +
+

{t('home.resultsTitle')}

+ {searchError &&
{searchError}
} + {translateError &&
{translateError}
} + {isTranslating &&
Translating selected result...
} + {searchResults.length > 0 ? ( +
+ + + + + + + + + + + {searchResults.map((row, index) => ( + { + setSelectedResult(row) + setTranslatedName('') + setTranslateError('') + }} + style={{ cursor: 'pointer' }} + > + + + + + + ))} + +
Drug NameTypeLanguageCountry
{row.name}{row.type}{row.language}{row.country ?? '-'}
+
+ ) : searchQuery && !isLoading ? ( +
No results found
+ ) : ( + + )} + + {selectedResult && ( +
+
+
+ + +
+
+ +
+
+ +
+ + + + + + + + + + + + + +
Original Drug Name (Language)Drug Name in {targetLanguage.toUpperCase()}
{selectedResult.name} ({selectedResult.language}){translatedName || '-'}
+
+
+ )} +
+
+
+ +
+

Export CSV

+
+
+
+ + +
+
+ +
+
+
+ {isExporting &&
Preparing CSV...
} + {exportError &&
{exportError}
} + {exportMessage &&
{exportMessage}
} +
+
+
+ +
+