diff --git a/adtech_series_sp26/README.md b/adtech_series_sp26/README.md index e9db175..506b417 100644 --- a/adtech_series_sp26/README.md +++ b/adtech_series_sp26/README.md @@ -65,8 +65,13 @@ adtech_series_sp26/ │ │ ├── src/ # FastAPI backend │ │ └── frontend/ # React + Tailwind dashboard │ └── scripts/deploy.sh +├── segment_builder/ # Session 1 — Audience Segmentation app (FastAPI + React) +│ ├── app.yaml # Databricks App config +│ ├── backend/ # FastAPI: routers, services, models, config +│ ├── frontend/ # React + Vite + Tailwind (Agent + Builder modes) +│ ├── docs/DATA_DICTIONARY.md +│ └── tests/ # Playwright app spec ├── identity_graph/ # (Session 2 — placeholder) -├── segment_builder/ # (Session 1 — placeholder) └── measurement/ # (Session 4 — placeholder) ``` diff --git a/adtech_series_sp26/segment_builder/.env.example b/adtech_series_sp26/segment_builder/.env.example new file mode 100644 index 0000000..fb41e8e --- /dev/null +++ b/adtech_series_sp26/segment_builder/.env.example @@ -0,0 +1,19 @@ +# Copy to .env and fill in values. See CLAUDE.md for details. +# Optional when running in Databricks Apps (auth is automatic). +# Required for local dev unless using a Databricks CLI profile. + +# --- Databricks SQL (preview, build segment) --- +# Workspace host, e.g. https://your-workspace.cloud.databricks.com +DATABRICKS_SERVER_HOSTNAME= +# SQL warehouse HTTP path, e.g. /sql/1.0/warehouses/ +DATABRICKS_HTTP_PATH= +# Personal or service principal token (dapi...). Omit if using profile. +DATABRICKS_TOKEN= + +# --- Local dev: CLI profile (alternative to host/path/token) --- +# Profile name in ~/.databrickscfg. Used when host/path/token are not set. +DATABRICKS_CONFIG_PROFILE=e2-demo-field-eng + +# --- Databricks Model Serving (Agent mode) --- +# Model endpoint name for natural-language segment parsing. +DATABRICKS_MODEL_ENDPOINT=databricks-claude-sonnet-4-5 diff --git a/adtech_series_sp26/segment_builder/.gitignore b/adtech_series_sp26/segment_builder/.gitignore new file mode 100644 index 0000000..8819708 --- /dev/null +++ b/adtech_series_sp26/segment_builder/.gitignore @@ -0,0 +1,58 @@ +# Dependencies +node_modules/ +.venv/ +__pycache__/ +*.pyc +*.pyo +*.pyd + +# Claude Code +.claude/ +.databricks/ + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.env.development +.env.test +.env.production + +# Build outputs +dist/ +build/ + +# Playwright +playwright-report/ +test-results/ +playwright/.cache/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Dev docs (local only) +dev/ + +# Screenshots folder (development artifacts) +screenshots/ + +# Legacy scripts +explore_data.py + +# Bun lock file (Databricks Apps uses npm) +bun.lock \ No newline at end of file diff --git a/adtech_series_sp26/segment_builder/README.md b/adtech_series_sp26/segment_builder/README.md new file mode 100644 index 0000000..2301507 --- /dev/null +++ b/adtech_series_sp26/segment_builder/README.md @@ -0,0 +1,61 @@ +# Audience Segmentation Databricks App + +A Databricks App that enables advertisers to create audience segments without writing SQL. Built with React frontend and FastAPI backend. + +## Features + +**Agent Mode** - Conversational segment building powered by LLM +- Natural language input: "Dog owners in California aged 25-54" +- Automatic conversion to SQL queries +- Real-time preview with audience counts + +**Builder Mode** - Visual no-code query builder +- Drag-and-drop condition builder +- Support for AND/OR logic with nested groups +- Multi-select dropdowns for categorical values + +## Quick Start + +```bash +# Install dependencies +bun install +uv venv && uv pip install -r requirements.txt + +# Build frontend +bun run build + +# Run backend +source .venv/bin/activate && uvicorn backend.main:app --reload +``` + +See [CLAUDE.md](CLAUDE.md) for comprehensive documentation including architecture, API endpoints, deployment, and troubleshooting. + +## Architecture + +``` +frontend/ React + TypeScript + Tailwind CSS + src/features/segment-builder/ + components/ UI components + hooks/ Custom React hooks + api/ API client functions + +backend/ FastAPI + Python + routers/ API endpoints + services/ Business logic (SQL generation, LLM integration) + config/ Feature metadata +``` + +## Live Demo + +https://dq-adtech-1444828305810485.aws.databricksapps.com/ + +## Deployment + +```bash +databricks apps deploy dq-adtech +``` + +## Data + +- **Input**: Unity Catalog table with 3.4M audience profiles +- **Output**: Segments saved to Unity Catalog for activation diff --git a/adtech_series_sp26/segment_builder/app.yaml b/adtech_series_sp26/segment_builder/app.yaml new file mode 100644 index 0000000..670431c --- /dev/null +++ b/adtech_series_sp26/segment_builder/app.yaml @@ -0,0 +1 @@ +command: ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/adtech_series_sp26/segment_builder/.gitkeep b/adtech_series_sp26/segment_builder/backend/__init__.py similarity index 100% rename from adtech_series_sp26/segment_builder/.gitkeep rename to adtech_series_sp26/segment_builder/backend/__init__.py diff --git a/adtech_series_sp26/segment_builder/backend/config/__init__.py b/adtech_series_sp26/segment_builder/backend/config/__init__.py new file mode 100644 index 0000000..a38cc87 --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/config/__init__.py @@ -0,0 +1 @@ +# Config module diff --git a/adtech_series_sp26/segment_builder/backend/config/column_overrides.py b/adtech_series_sp26/segment_builder/backend/config/column_overrides.py new file mode 100644 index 0000000..3f95d9d --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/config/column_overrides.py @@ -0,0 +1,120 @@ +"""In-memory overrides for column names and UI labels. Used by Settings API.""" + +import re +from typing import Any + +# Defaults matching current schema +DEFAULT_PROFILE = { + "features_layout": "by_column", + "identity_household_column": "megacorp_hhid", + "identity_individual_column": "megacorp_indid", +} +DEFAULT_SEGMENT_LIST = { + "identity_household_column": "megacorp_hhid", + "identity_individual_column": "megacorp_indid", + "segment_name_column": "campaign_name", +} +DEFAULT_SEGMENT_INFO_LABELS: dict[str, str] = { + "segment_name": "Segment Name", + "segment_definition": "Segment Definition", + "quarter": "Quarter", + "start_date": "Flight Start Date", + "end_date": "Flight End Date", + "megacorp_indid": "Individual Identifier", + "megacorp_hhid": "Household Identifier", +} + +_overrides: dict[str, Any] = {} + + +def _safe_column(name: str) -> str: + """Allow only alphanumeric and underscore for column names.""" + if not name or not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", name): + return "" + return name + + +def set_column_config( + *, + profile: dict[str, Any] | None = None, + segment_list: dict[str, Any] | None = None, + segment_info_labels: dict[str, str] | None = None, +) -> None: + """Replace or merge column config. Pass full dict per section to replace.""" + if "column_configs" not in _overrides: + _overrides["column_configs"] = { + "profile": dict(DEFAULT_PROFILE), + "segment_list": dict(DEFAULT_SEGMENT_LIST), + "segment_info_labels": dict(DEFAULT_SEGMENT_INFO_LABELS), + } + cfg = _overrides["column_configs"] + if profile is not None: + cfg["profile"] = {**DEFAULT_PROFILE, **profile} + cfg["profile"]["identity_household_column"] = _safe_column( + cfg["profile"].get("identity_household_column", "") + ) or DEFAULT_PROFILE["identity_household_column"] + cfg["profile"]["identity_individual_column"] = _safe_column( + cfg["profile"].get("identity_individual_column", "") + ) or DEFAULT_PROFILE["identity_individual_column"] + if cfg["profile"].get("features_layout") not in ("by_column", "by_row"): + cfg["profile"]["features_layout"] = DEFAULT_PROFILE["features_layout"] + if segment_list is not None: + cfg["segment_list"] = {**DEFAULT_SEGMENT_LIST, **segment_list} + cfg["segment_list"]["identity_household_column"] = _safe_column( + cfg["segment_list"].get("identity_household_column", "") + ) or DEFAULT_SEGMENT_LIST["identity_household_column"] + cfg["segment_list"]["identity_individual_column"] = _safe_column( + cfg["segment_list"].get("identity_individual_column", "") + ) or DEFAULT_SEGMENT_LIST["identity_individual_column"] + cfg["segment_list"]["segment_name_column"] = _safe_column( + cfg["segment_list"].get("segment_name_column", "") + ) or DEFAULT_SEGMENT_LIST["segment_name_column"] + if segment_info_labels is not None: + cfg["segment_info_labels"] = {**DEFAULT_SEGMENT_INFO_LABELS, **segment_info_labels} + + +def get_column_configs() -> dict[str, Any]: + """Return full column config (for API).""" + if "column_configs" not in _overrides: + _overrides["column_configs"] = { + "profile": dict(DEFAULT_PROFILE), + "segment_list": dict(DEFAULT_SEGMENT_LIST), + "segment_info_labels": dict(DEFAULT_SEGMENT_INFO_LABELS), + } + return _overrides["column_configs"] + + +def get_profile_identity_household_column() -> str: + return get_column_configs()["profile"].get( + "identity_household_column", DEFAULT_PROFILE["identity_household_column"] + ) + + +def get_profile_identity_individual_column() -> str: + return get_column_configs()["profile"].get( + "identity_individual_column", DEFAULT_PROFILE["identity_individual_column"] + ) + + +def get_campaigns_identity_household_column() -> str: + return get_column_configs()["segment_list"].get( + "identity_household_column", DEFAULT_SEGMENT_LIST["identity_household_column"] + ) + + +def get_campaigns_identity_individual_column() -> str: + return get_column_configs()["segment_list"].get( + "identity_individual_column", DEFAULT_SEGMENT_LIST["identity_individual_column"] + ) + + +def get_campaigns_segment_name_column() -> str: + return get_column_configs()["segment_list"].get( + "segment_name_column", DEFAULT_SEGMENT_LIST["segment_name_column"] + ) + + +def get_segment_info_column_labels() -> dict[str, str]: + return dict( + get_column_configs().get("segment_info_labels", DEFAULT_SEGMENT_INFO_LABELS) + ) diff --git a/adtech_series_sp26/segment_builder/backend/config/constants.py b/adtech_series_sp26/segment_builder/backend/config/constants.py new file mode 100644 index 0000000..40e5ab9 --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/config/constants.py @@ -0,0 +1,20 @@ +"""Shared constants for API responses.""" + +# Generic message returned for 500 errors (avoid leaking internal details to clients) +GENERIC_ERROR_MESSAGE = "An error occurred. Please try again." + + +def get_databricks_forbidden_message( + catalog: str | None = None, + profiles_schema: str | None = None, + segments_schema: str | None = None, +) -> str: + """Build 403 message using current table settings. Uses placeholder names if not set.""" + c = catalog or "catalog" + p = profiles_schema or "profiles" + s = segments_schema or "segments" + return ( + f"Databricks access denied (403). In Databricks Apps the app runs as a service principal. " + f"Grant that identity: USE_CATALOG on {c}; USE_SCHEMA and SELECT on {c}.{p}; " + f"USE_SCHEMA, SELECT, and MODIFY on {c}.{s}. Get the app's client ID with: databricks apps get " + ) diff --git a/adtech_series_sp26/segment_builder/backend/config/features.py b/adtech_series_sp26/segment_builder/backend/config/features.py new file mode 100644 index 0000000..7ecd46e --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/config/features.py @@ -0,0 +1,187 @@ +"""Feature metadata for the 14 audience profile columns.""" + +from typing import Any + +# Feature type definitions +FEATURE_TYPES = { + "categorical": "categorical", + "numeric": "numeric", + "boolean": "boolean" +} + +# All available features with their metadata +FEATURES: dict[str, dict[str, Any]] = { + "state": { + "display_name": "State", + "column": "state", + "type": "categorical", + "operators": ["IS", "IN", "NOT"], + "description": "US state or territory code (2-letter)", + "nullable": False, + "distinct_values": 57, + }, + "zip5": { + "display_name": "ZIP Code", + "column": "zip5", + "type": "categorical", + "operators": ["IS", "IN", "NOT"], + "description": "5-digit ZIP code", + "nullable": False, + "distinct_values": 40328, + "searchable": True, # Too many values, use search + }, + "age": { + "display_name": "Age", + "column": "age", + "type": "numeric", + "operators": ["IN", "NOT", "IS", "BETWEEN", "GT", "LT", "GTE", "LTE"], + "description": "Age in years (18-115)", + "nullable": True, + "null_rate": 0.05, + "range": {"min": 18, "max": 115}, + "bracket_style": "inclusive", + "brackets": [ + {"label": "Under 18", "max": 17}, + {"label": "18-24", "min": 18, "max": 24}, + {"label": "25-34", "min": 25, "max": 34}, + {"label": "35-44", "min": 35, "max": 44}, + {"label": "45-54", "min": 45, "max": 54}, + {"label": "55-64", "min": 55, "max": 64}, + {"label": "65+", "min": 65}, + ], + }, + "gender": { + "display_name": "Gender", + "column": "gender", + "type": "categorical", + "operators": ["IS", "IN", "NOT"], + "description": "Gender classification", + "nullable": False, + "distinct_values": 5, + "values": ["Male", "Female", "Unknown", "Other", "Prefer not to say"], + }, + "is_dog_owner": { + "display_name": "Dog Owner", + "column": "is_dog_owner", + "type": "boolean", + "operators": ["IS"], + "description": "Owns a dog", + "nullable": True, + "null_rate": 0.23, + }, + "is_cat_owner": { + "display_name": "Cat Owner", + "column": "is_cat_owner", + "type": "boolean", + "operators": ["IS"], + "description": "Owns a cat", + "nullable": True, + "null_rate": 0.57, + }, + "qsr_propensity": { + "display_name": "QSR Propensity", + "column": "qsr_propensity", + "type": "categorical", + "operators": ["IS", "IN", "NOT"], + "description": "Quick-service restaurant propensity score", + "nullable": True, + "null_rate": 0.31, + "distinct_values": 4, + "values": ["Low", "Medium", "High", "Very High"], + }, + "martial_status": { + "display_name": "Marital Status", + "column": "martial_status", + "type": "categorical", + "operators": ["IS", "IN", "NOT"], + "description": "Marital status", + "nullable": False, + "distinct_values": 5, + "values": ["Single", "Married", "Divorced", "Widowed", "Unknown"], + }, + "income_level": { + "display_name": "Income Level", + "column": "income_level", + "type": "numeric", + "operators": ["IN", "NOT", "IS", "BETWEEN", "GT", "LT", "GTE", "LTE"], + "description": "Annual household income", + "nullable": False, + "bracket_style": "exclusive_upper", + "brackets": [ + {"label": "<10K", "max": 10000}, + {"label": "10K-15K", "min": 10000, "max": 15000}, + {"label": "15K-25K", "min": 15000, "max": 25000}, + {"label": "25K-35K", "min": 25000, "max": 35000}, + {"label": "35K-50K", "min": 35000, "max": 50000}, + {"label": "50K-75K", "min": 50000, "max": 75000}, + {"label": "75K-100K", "min": 75000, "max": 100000}, + {"label": "100K-150K", "min": 100000, "max": 150000}, + {"label": "150K-200K", "min": 150000, "max": 200000}, + {"label": "200K+", "min": 200000}, + ], + }, + "education_level": { + "display_name": "Education Level", + "column": "education_level", + "type": "categorical", + "operators": ["IS", "IN", "NOT"], + "description": "Highest education attained", + "nullable": False, + "distinct_values": 4, + "values": ["High School", "Some College", "Bachelor's", "Graduate"], + }, + "is_active_auto_loan": { + "display_name": "Active Auto Loan", + "column": "is_active_auto_loan", + "type": "boolean", + "operators": ["IS"], + "description": "Currently has auto loan", + "nullable": True, + "null_rate": 0.07, + }, + "is_cord_cutter": { + "display_name": "Cord Cutter", + "column": "is_cord_cutter", + "type": "boolean", + "operators": ["IS"], + "description": "No traditional cable subscription", + "nullable": True, + "null_rate": 0.46, + }, + "luxury_propensity": { + "display_name": "Luxury Propensity", + "column": "luxury_propensity", + "type": "categorical", + "operators": ["IS", "IN", "NOT"], + "description": "Luxury goods purchase propensity", + "nullable": True, + "null_rate": 0.31, + "distinct_values": 6, + "values": ["Very Low", "Low", "Medium", "High", "Very High", "Ultra"], + }, + "auto_intenders": { + "display_name": "Auto Intenders", + "column": "auto_intenders", + "type": "boolean", + "operators": ["IS"], + "description": "In-market for vehicle purchase", + "nullable": True, + "null_rate": 0.15, + }, +} + +# Safe columns whitelist for SQL injection prevention +SAFE_COLUMNS = set(FEATURES.keys()) + + +def get_feature(name: str) -> dict[str, Any] | None: + """Get feature metadata by name.""" + return FEATURES.get(name) + + +def get_all_features() -> list[dict[str, Any]]: + """Get all features as a list with names included.""" + return [ + {"name": name, **metadata} + for name, metadata in FEATURES.items() + ] diff --git a/adtech_series_sp26/segment_builder/backend/config/settings.py b/adtech_series_sp26/segment_builder/backend/config/settings.py new file mode 100644 index 0000000..9b62d26 --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/config/settings.py @@ -0,0 +1,88 @@ +"""Environment configuration for the application. + +Uses Pydantic BaseSettings to validate env at startup. All Databricks vars +are optional when using profile auth (local) or Databricks Apps SDK auth. +.env is loaded when present (local); in Databricks Apps, env vars come from the platform. +""" + +from functools import lru_cache +from pathlib import Path + +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +def _env_file_path() -> str | None: + """Use .env only if it exists so the app works in Databricks Apps without a .env file.""" + p = Path(".env") + return ".env" if p.is_file() else None + + +class Settings(BaseSettings): + """Application settings loaded and validated from environment variables.""" + + model_config = SettingsConfigDict( + env_file=_env_file_path(), + env_file_encoding="utf-8", + extra="ignore", + ) + + # Databricks SQL Connection (optional when using profile or Apps auth) + databricks_server_hostname: str = Field( + default="", + description="Databricks workspace hostname (e.g. xxx.cloud.databricks.com)", + ) + databricks_http_path: str = Field( + default="", + description="SQL warehouse HTTP path (e.g. /sql/1.0/warehouses/xxx)", + ) + databricks_token: str = Field( + default="", + description="Databricks personal access token for SQL and model serving", + ) + + # Local dev: use a Databricks CLI profile (e.g. ~/.databrickscfg) + databricks_config_profile: str = Field( + default="e2-demo-field-eng", + description="Databricks CLI profile name when not using env vars", + ) + + # Databricks Model Serving (for Agent Mode) + databricks_model_endpoint: str = Field( + default="databricks-claude-sonnet-4-5", + description="Model serving endpoint name for agent LLM", + ) + + # Unity Catalog Tables (defaults; can be overridden via API) + profiles_table: str = Field( + default="media_advertising.profiles.megacorp_audience_census_profile", + description="Unity Catalog table for audience profiles", + ) + campaigns_table: str = Field( + default="media_advertising.segments.megacorp_campaigns", + description="Unity Catalog table for campaign segment lists", + ) + definitions_table: str = Field( + default="media_advertising.segments.megacorp_segment_definitions", + description="Unity Catalog table for segment definitions", + ) + + @property + def is_databricks_configured(self) -> bool: + """True if explicit hostname + http_path + token are set (env vars).""" + return bool( + self.databricks_server_hostname + and self.databricks_http_path + and self.databricks_token + ) + + @property + def use_profile_auth(self) -> bool: + """True when not using Apps and not using explicit env; use SDK profile for local dev.""" + return bool(self.databricks_config_profile) + + +@lru_cache +def get_settings() -> Settings: + """Get cached settings instance (validated at first access).""" + return Settings() diff --git a/adtech_series_sp26/segment_builder/backend/config/table_overrides.py b/adtech_series_sp26/segment_builder/backend/config/table_overrides.py new file mode 100644 index 0000000..5a1f1b8 --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/config/table_overrides.py @@ -0,0 +1,92 @@ +"""In-memory overrides for Unity Catalog table names. Used by Settings API.""" + +from backend.config.settings import get_settings + +_overrides: dict[str, str] = {} + + +def set_table_overrides( + *, + profiles_table: str | None = None, + campaigns_table: str | None = None, + definitions_table: str | None = None, +) -> None: + """Set table name overrides. None = leave unchanged; empty string = clear (use default).""" + if profiles_table is not None: + if profiles_table == "": + _overrides.pop("profiles_table", None) + else: + _overrides["profiles_table"] = profiles_table + if campaigns_table is not None: + if campaigns_table == "": + _overrides.pop("campaigns_table", None) + else: + _overrides["campaigns_table"] = campaigns_table + if definitions_table is not None: + if definitions_table == "": + _overrides.pop("definitions_table", None) + else: + _overrides["definitions_table"] = definitions_table + + +def get_profiles_table() -> str: + """Profiles (audience census) table. Override or config default.""" + return _overrides.get("profiles_table") or get_settings().profiles_table + + +def get_campaigns_table() -> str: + """Campaigns / segment list table. Override or config default.""" + return _overrides.get("campaigns_table") or get_settings().campaigns_table + + +def get_definitions_table() -> str: + """Segment definitions table. Override or config default.""" + return _overrides.get("definitions_table") or get_settings().definitions_table + + +def get_tables() -> dict[str, str]: + """Return current table names (overrides or defaults).""" + return { + "profiles_table": get_profiles_table(), + "campaigns_table": get_campaigns_table(), + "definitions_table": get_definitions_table(), + } + + +def _parse_catalog_schema(fully_qualified: str) -> tuple[str, str] | None: + """Return (catalog, schema) from 'catalog.schema.table' or None if invalid.""" + parts = (fully_qualified or "").strip().split(".") + if len(parts) >= 3: + return (parts[0], parts[1]) + return None + + +def get_catalog_schemas_for_grants() -> dict[str, str | None]: + """Return catalog and schema names from current table settings (for error messages and grants). + Keys: catalog, profiles_schema, segments_schema. Values are None when not determinable. + """ + tables = get_tables() + profiles = _parse_catalog_schema(tables.get("profiles_table", "")) + campaigns = _parse_catalog_schema(tables.get("campaigns_table", "")) + definitions = _parse_catalog_schema(tables.get("definitions_table", "")) + + catalog: str | None = None + profiles_schema: str | None = None + segments_schema: str | None = None + + if profiles: + catalog, profiles_schema = profiles + if campaigns: + c, segments_schema = campaigns + if catalog is None: + catalog = c + if definitions and segments_schema is None: + c, segments_schema = definitions + if catalog is None: + catalog = c + + return { + "catalog": catalog, + "profiles_schema": profiles_schema, + "segments_schema": segments_schema, + } diff --git a/adtech_series_sp26/segment_builder/backend/main.py b/adtech_series_sp26/segment_builder/backend/main.py new file mode 100644 index 0000000..8422c37 --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/main.py @@ -0,0 +1,167 @@ +"""FastAPI application for Audience Segmentation Databricks App.""" + +from dotenv import load_dotenv +load_dotenv() # Load .env before any other imports read env vars + +import os +import logging + +from fastapi import FastAPI, HTTPException, Request +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse, JSONResponse +from fastapi.middleware.cors import CORSMiddleware + +from backend.config.constants import GENERIC_ERROR_MESSAGE +from backend.config.settings import get_settings +from backend.routers import agent, features, segments, settings +from backend.services.agent_service import get_agent_service +from backend.services.databricks_client import get_databricks_client + +# --- Logging Setup --- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s" +) +logger = logging.getLogger(__name__) + +# --- App Setup --- +app = FastAPI( + title="Audience Segmentation App", + description="Databricks App for building audience segments without SQL", + version="1.0.0", +) + + +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + """Log unhandled exceptions and return generic 500; let HTTPException pass through.""" + if isinstance(exc, HTTPException): + return JSONResponse( + status_code=exc.status_code, + content={"detail": exc.detail}, + ) + logger.exception( + "Unhandled exception: %s", + exc, + extra={"path": getattr(request, "url", None) and request.url.path}, + ) + return JSONResponse( + status_code=500, + content={"detail": GENERIC_ERROR_MESSAGE}, + ) + + +# --- CORS Middleware --- +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# --- Register Routers --- +app.include_router(features.router) +app.include_router(segments.router) +app.include_router(agent.router) +app.include_router(settings.router) + + +# --- Health Check (resilient: never 500s so Apps and load balancers get 200) --- +@app.get("/api/health") +async def health_check(): + """Health check endpoint. Works in Databricks Apps and locally; never returns 500.""" + logger.info("Health check at /api/health") + + databricks_connected = False + agent_mode = "unavailable" + model_endpoint = "" + + try: + app_settings = get_settings() + model_endpoint = app_settings.databricks_model_endpoint or "" + except Exception as e: + logger.warning("Health: settings unavailable: %s", e) + + try: + db = get_databricks_client() + databricks_connected = db.is_configured + except Exception as e: + logger.warning("Health: Databricks client unavailable: %s", e) + + try: + agent_svc = get_agent_service() + agent_mode = agent_svc.agent_mode + except Exception: + pass + + return { + "status": "healthy", + "databricks_connected": databricks_connected, + "agent_mode": agent_mode, + "model_endpoint": model_endpoint, + } + + +# --- Static Files Setup --- +static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static") +assets_dir = os.path.join(static_dir, "assets") + +# Create directories if they don't exist +os.makedirs(static_dir, exist_ok=True) +os.makedirs(assets_dir, exist_ok=True) + +# Mount static files AFTER API routes +app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") + + +# --- Catch-all for React Routes --- +def _media_type(path: str) -> str: + if path.endswith(".png"): + return "image/png" + if path.endswith(".jpg") or path.endswith(".jpeg"): + return "image/jpeg" + if path.endswith(".svg"): + return "image/svg+xml" + if path.endswith(".ico"): + return "image/x-icon" + return "application/octet-stream" + + +@app.get("/{full_path:path}") +async def serve_react(full_path: str): + """Serve static files from static_dir if they exist, else index.html for SPA routing.""" + if not isinstance(full_path, str): + full_path = str(full_path) if full_path is not None else "" + if full_path.startswith("api/"): + raise HTTPException(status_code=404, detail="API endpoint not found") + + try: + index_html = os.path.join(static_dir, "index.html") + except Exception as e: + logger.exception("Bad static_dir path: %s", e) + raise HTTPException(status_code=500, detail=GENERIC_ERROR_MESSAGE) + + if not os.path.exists(index_html): + logger.error("Frontend not built. index.html missing at %s", index_html) + raise HTTPException( + status_code=404, + detail="Frontend not built. Please run 'bun run build' first.", + ) + + # Serve existing files from static root (e.g. logo from public/) + if full_path and full_path != "/": + try: + safe_path = os.path.normpath(full_path).lstrip("/").replace("\\", "/") + if ".." not in safe_path and safe_path: + file_path = os.path.normpath(os.path.join(static_dir, *safe_path.split("/"))) + static_real = os.path.realpath(static_dir) + if os.path.isfile(file_path): + file_real = os.path.realpath(file_path) + if file_real.startswith(static_real): + return FileResponse(file_path, media_type=_media_type(safe_path)) + except Exception as e: + logger.debug("Static file check for %s: %s", full_path, e) + + logger.info("Serving React frontend for path: /%s", full_path or "/") + return FileResponse(index_html, media_type="text/html") diff --git a/adtech_series_sp26/segment_builder/backend/models/__init__.py b/adtech_series_sp26/segment_builder/backend/models/__init__.py new file mode 100644 index 0000000..e2313c5 --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/models/__init__.py @@ -0,0 +1 @@ +# Models module diff --git a/adtech_series_sp26/segment_builder/backend/models/segment.py b/adtech_series_sp26/segment_builder/backend/models/segment.py new file mode 100644 index 0000000..d6e7f0f --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/models/segment.py @@ -0,0 +1,121 @@ +"""Pydantic models for segment definitions and API requests/responses.""" + +from datetime import date +from typing import Literal + +from pydantic import BaseModel, Field + + +class SegmentCondition(BaseModel): + """A single condition in a segment rule.""" + + id: str = Field(..., description="Unique identifier for this condition") + feature: str = Field(..., description="Column name (e.g., 'age', 'state')") + operator: Literal["IS", "IN", "NOT", "BETWEEN", "GT", "LT", "GTE", "LTE"] = Field( + ..., description="Comparison operator" + ) + values: list[str | int | float | bool] = Field( + ..., description="Values to compare against" + ) + + +class SegmentGroup(BaseModel): + """A group of conditions combined with AND/OR logic.""" + + id: str = Field(..., description="Unique identifier for this group") + logic: Literal["AND", "OR"] = Field( + default="AND", description="Logic operator between conditions" + ) + conditions: list[SegmentCondition] = Field( + default_factory=list, description="List of conditions in this group" + ) + + +class SegmentDefinition(BaseModel): + """Complete segment definition with groups of conditions.""" + + name: str = Field(default="", description="Segment name") + description: str = Field(default="", description="Human-readable description") + groups: list[SegmentGroup] = Field( + default_factory=list, description="Groups of conditions" + ) + group_logic: Literal["AND", "OR"] = Field( + default="AND", description="Logic operator between groups" + ) + + +# API Request/Response Models + + +class SegmentPreviewRequest(BaseModel): + """Request to preview a segment.""" + + segment: SegmentDefinition + include_sql: bool = Field(default=True, description="Include generated SQL in response") + + +class SegmentPreviewResponse(BaseModel): + """Response from segment preview.""" + + individual_count: int = Field(..., description="Count of unique individuals") + household_count: int = Field(..., description="Count of unique households") + sql: str | None = Field(None, description="Generated SQL query") + execution_time_ms: float = Field(..., description="Query execution time in milliseconds") + + +class SegmentBuildRequest(BaseModel): + """Request to build and save a segment.""" + + segment: SegmentDefinition + name: str = Field(..., description="Segment name for saving") + quarter: str = Field(..., description="Fiscal quarter (e.g., 'Q1')") + start_date: date = Field(..., description="Campaign start date") + end_date: date = Field(..., description="Campaign end date") + + +class SegmentBuildResponse(BaseModel): + """Response from building a segment.""" + + segment_id: str = Field(..., description="Generated segment ID") + rows_inserted: int = Field(..., description="Number of rows inserted into campaigns") + campaign_name: str = Field(..., description="Campaign name in the database") + + +class SegmentUpdateRequest(BaseModel): + """Request to update segment metadata (definition, quarter, flight dates).""" + + segment_definition: str = Field(..., description="Human-readable segment description") + quarter: str = Field(..., description="Fiscal quarter (e.g., '25Q1')") + start_date: date = Field(..., description="Flight start date") + end_date: date = Field(..., description="Flight end date") + + +class FeatureResponse(BaseModel): + """Response for a single feature.""" + + name: str + display_name: str + column: str + type: str + operators: list[str] + description: str + nullable: bool = False + null_rate: float | None = None + distinct_values: int | None = None + values: list[str] | None = None + searchable: bool = False + range: dict[str, int] | None = None + brackets: list[dict] | None = None + + +class FeaturesListResponse(BaseModel): + """Response for listing all features.""" + + features: list[FeatureResponse] + + +class FeatureValuesResponse(BaseModel): + """Response for feature values.""" + + values: list[str] + total: int diff --git a/adtech_series_sp26/segment_builder/backend/routers/__init__.py b/adtech_series_sp26/segment_builder/backend/routers/__init__.py new file mode 100644 index 0000000..22da696 --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/routers/__init__.py @@ -0,0 +1 @@ +# Routers module diff --git a/adtech_series_sp26/segment_builder/backend/routers/agent.py b/adtech_series_sp26/segment_builder/backend/routers/agent.py new file mode 100644 index 0000000..7d2836e --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/routers/agent.py @@ -0,0 +1,112 @@ +"""API routes for agent-powered segment building.""" + +import logging +from typing import Any + +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel, Field + +from backend.config.constants import GENERIC_ERROR_MESSAGE +from backend.models.segment import SegmentDefinition +from backend.services.agent_service import AgentService, get_agent_service + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/agent", tags=["agent"]) + + +class ChatMessage(BaseModel): + """A single chat message.""" + role: str = Field(..., description="Role: 'user' or 'assistant'") + content: str = Field(..., description="Message content") + + +class AgentParseRequest(BaseModel): + """Request to parse natural language to segment rules.""" + input: str = Field(..., description="User's natural language input") + conversation_history: list[ChatMessage] = Field( + default_factory=list, + description="Previous conversation messages" + ) + current_segment: SegmentDefinition | None = Field( + None, + description="Current segment state to modify" + ) + + +class AgentParseResponse(BaseModel): + """Response from agent parsing.""" + response_text: str = Field(..., description="Agent's response text") + segment: dict[str, Any] | None = Field(None, description="Generated segment definition") + preview: dict[str, int] | None = Field(None, description="Preview counts") + sql: str | None = Field(None, description="Generated SQL query") + + +class AgentSummarizeRequest(BaseModel): + """Request to generate a short segment description from chat context.""" + conversation_history: list[ChatMessage] = Field( + default_factory=list, + description="Chat messages that led to this segment", + ) + segment: SegmentDefinition = Field(..., description="Current segment definition") + + +class AgentSummarizeResponse(BaseModel): + """Response with a 1-2 sentence segment summary and optional suggested name.""" + summary: str = Field(..., description="Short description for the segment") + suggested_name: str = Field("", description="Suggested segment/campaign name (identifier-friendly)") + + +@router.post("/parse", response_model=AgentParseResponse) +async def parse_input( + request: AgentParseRequest, + agent: AgentService = Depends(get_agent_service), +): + """Parse natural language input to segment rules using LLM.""" + try: + # Convert chat messages to dict format + history = [{"role": m.role, "content": m.content} for m in request.conversation_history] + + result = agent.parse_input( + user_input=request.input, + conversation_history=history, + current_segment=request.current_segment, + ) + + return AgentParseResponse( + response_text=result.get("response_text", ""), + segment=result.get("segment"), + preview=result.get("preview"), + sql=result.get("sql"), + ) + + except Exception as e: + logger.exception( + "Agent parse error", + extra={"error": str(e), "endpoint": "parse"}, + ) + raise HTTPException(status_code=500, detail=GENERIC_ERROR_MESSAGE) + + +@router.post("/summarize", response_model=AgentSummarizeResponse) +async def summarize_segment( + request: AgentSummarizeRequest, + agent: AgentService = Depends(get_agent_service), +): + """Generate a 1-2 sentence segment description and suggested name from the conversation using the LLM.""" + try: + history = [{"role": m.role, "content": m.content} for m in request.conversation_history] + summary, suggested_name = agent.summarize_segment(request.segment, history) + return AgentSummarizeResponse( + summary=summary or request.segment.description or "", + suggested_name=suggested_name or "", + ) + except Exception as e: + logger.warning( + "Agent summarize error", + extra={"error": str(e), "endpoint": "summarize"}, + ) + return AgentSummarizeResponse( + summary=request.segment.description or "", + suggested_name="", + ) diff --git a/adtech_series_sp26/segment_builder/backend/routers/features.py b/adtech_series_sp26/segment_builder/backend/routers/features.py new file mode 100644 index 0000000..780eca9 --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/routers/features.py @@ -0,0 +1,88 @@ +"""API routes for feature metadata.""" + +import logging + +from fastapi import APIRouter, Depends, HTTPException, Query + +from backend.config.constants import GENERIC_ERROR_MESSAGE +from backend.config.features import get_all_features, get_feature +from backend.models.segment import FeatureResponse, FeatureValuesResponse, FeaturesListResponse +from backend.services.databricks_client import DatabricksClient, get_databricks_client + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/features", tags=["features"]) + + +@router.get("", response_model=FeaturesListResponse) +async def list_features(): + """List all available features with their metadata.""" + features_data = get_all_features() + + features = [] + for f in features_data: + features.append( + FeatureResponse( + name=f["name"], + display_name=f["display_name"], + column=f["column"], + type=f["type"], + operators=f["operators"], + description=f["description"], + nullable=f.get("nullable", False), + null_rate=f.get("null_rate"), + distinct_values=f.get("distinct_values"), + values=f.get("values"), + searchable=f.get("searchable", False), + range=f.get("range"), + brackets=f.get("brackets"), + ) + ) + + return FeaturesListResponse(features=features) + + +@router.get("/{feature_name}/values", response_model=FeatureValuesResponse) +async def get_feature_values( + feature_name: str, + search: str | None = Query(None, description="Search filter"), + limit: int = Query(100, le=1000, description="Maximum values to return"), + db: DatabricksClient = Depends(get_databricks_client), +): + """Get distinct values for a categorical feature.""" + feature = get_feature(feature_name) + + if not feature: + raise HTTPException(status_code=404, detail=f"Feature not found: {feature_name}") + + if feature["type"] not in ("categorical",): + raise HTTPException( + status_code=400, + detail=f"Feature {feature_name} is not categorical" + ) + + # If feature has static values and no search, return them + if "values" in feature and not search: + values = feature["values"][:limit] + return FeatureValuesResponse(values=values, total=len(feature["values"])) + + # Otherwise, query Databricks + try: + values = db.fetch_distinct_values( + column=feature["column"], + search=search, + limit=limit, + ) + return FeatureValuesResponse(values=values, total=len(values)) + except Exception as e: + # If Databricks not configured, return static values if available + if "values" in feature: + values = feature["values"] + if search: + values = [v for v in values if search.lower() in v.lower()] + return FeatureValuesResponse(values=values[:limit], total=len(values)) + logger.exception( + "Feature values error", + extra={"error": str(e), "endpoint": "get_feature_values", "feature_name": feature_name}, + ) + raise HTTPException(status_code=500, detail=GENERIC_ERROR_MESSAGE) diff --git a/adtech_series_sp26/segment_builder/backend/routers/segments.py b/adtech_series_sp26/segment_builder/backend/routers/segments.py new file mode 100644 index 0000000..78518e8 --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/routers/segments.py @@ -0,0 +1,232 @@ +"""API routes for segment operations.""" + +import logging + +from databricks.sql.exc import RequestError +from fastapi import APIRouter, Depends, HTTPException + +from backend.models.segment import ( + SegmentBuildRequest, + SegmentBuildResponse, + SegmentPreviewRequest, + SegmentPreviewResponse, + SegmentUpdateRequest, +) +from backend.config.constants import GENERIC_ERROR_MESSAGE, get_databricks_forbidden_message +from backend.config.table_overrides import get_catalog_schemas_for_grants +from backend.services.databricks_client import DatabricksClient, get_databricks_client +from backend.services.segment_service import SegmentService, get_segment_service +from backend.services.sql_generator import SqlGenerator, SqlGeneratorError, get_sql_generator + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/segment", tags=["segments"]) + + +def _is_forbidden(err: RequestError) -> bool: + """True if the Databricks error is 403 Forbidden (auth/perms).""" + msg = (getattr(err, "message", None) or str(err)).upper() + return "403" in msg or "FORBIDDEN" in msg + + +def _forbidden_detail() -> str: + """403 detail message using current catalog/schema from settings.""" + ctx = get_catalog_schemas_for_grants() + return get_databricks_forbidden_message( + catalog=ctx.get("catalog"), + profiles_schema=ctx.get("profiles_schema"), + segments_schema=ctx.get("segments_schema"), + ) + + +@router.post("/preview", response_model=SegmentPreviewResponse) +async def preview_segment( + request: SegmentPreviewRequest, + db: DatabricksClient = Depends(get_databricks_client), + service: SegmentService = Depends(get_segment_service), + sql_gen: SqlGenerator = Depends(get_sql_generator), +): + """Preview a segment - returns counts and generated SQL.""" + try: + if not db.is_configured: + # Return mock data if Databricks not configured + sql = sql_gen.generate_count_query(request.segment) + return SegmentPreviewResponse( + individual_count=0, + household_count=0, + sql=sql if request.include_sql else None, + execution_time_ms=0, + ) + + return service.preview_segment(request.segment, request.include_sql) + + except SqlGeneratorError as e: + logger.error( + "SQL generation error", + extra={"error": str(e), "endpoint": "preview"}, + ) + raise HTTPException(status_code=400, detail=str(e)) + except RequestError as e: + if _is_forbidden(e): + logger.warning( + "Databricks 403 on preview_segment", + extra={"error": str(e), "endpoint": "preview"}, + ) + raise HTTPException(status_code=403, detail=_forbidden_detail()) + logger.exception( + "Preview error", + extra={"error": str(e), "endpoint": "preview"}, + ) + raise HTTPException(status_code=500, detail=GENERIC_ERROR_MESSAGE) + except Exception as e: + logger.exception( + "Preview error", + extra={"error": str(e), "endpoint": "preview"}, + ) + raise HTTPException(status_code=500, detail=GENERIC_ERROR_MESSAGE) + + +@router.post("/build", response_model=SegmentBuildResponse) +async def build_segment( + request: SegmentBuildRequest, + db: DatabricksClient = Depends(get_databricks_client), + service: SegmentService = Depends(get_segment_service), +): + """Build and save a segment to Databricks tables.""" + try: + if not db.is_configured: + raise HTTPException( + status_code=503, + detail="Databricks not configured. Cannot build segment." + ) + + return service.build_segment( + segment=request.segment, + name=request.name, + quarter=request.quarter, + start_date=request.start_date, + end_date=request.end_date, + ) + + except SqlGeneratorError as e: + logger.error( + "SQL generation error", + extra={"error": str(e), "endpoint": "build"}, + ) + raise HTTPException(status_code=400, detail=str(e)) + except RequestError as e: + if _is_forbidden(e): + logger.warning( + "Databricks 403 on build_segment", + extra={"error": str(e), "endpoint": "build"}, + ) + raise HTTPException(status_code=403, detail=_forbidden_detail()) + logger.exception( + "Build error", + extra={"error": str(e), "endpoint": "build"}, + ) + raise HTTPException(status_code=500, detail=GENERIC_ERROR_MESSAGE) + except Exception as e: + logger.exception( + "Build error", + extra={"error": str(e), "endpoint": "build"}, + ) + raise HTTPException(status_code=500, detail=GENERIC_ERROR_MESSAGE) + + +@router.get("") +async def list_segments( + db: DatabricksClient = Depends(get_databricks_client), + service: SegmentService = Depends(get_segment_service), +): + """List all existing segments.""" + try: + if not db.is_configured: + return {"segments": []} + + segments = service.list_segments() + return {"segments": segments} + + except RequestError as e: + if _is_forbidden(e): + logger.warning( + "Databricks 403 on list_segments", + extra={"error": str(e), "endpoint": "list_segments"}, + ) + raise HTTPException(status_code=403, detail=_forbidden_detail()) + logger.exception( + "List segments error", + extra={"error": str(e), "endpoint": "list_segments"}, + ) + raise HTTPException(status_code=500, detail=GENERIC_ERROR_MESSAGE) + except Exception as e: + logger.exception( + "List segments error", + extra={"error": str(e), "endpoint": "list_segments"}, + ) + raise HTTPException(status_code=500, detail=GENERIC_ERROR_MESSAGE) + + +@router.get("/all") +async def all_segments_overview( + db: DatabricksClient = Depends(get_databricks_client), + service: SegmentService = Depends(get_segment_service), +): + """All segments: definitions joined with campaigns, grouped by campaign with individual/household counts.""" + try: + if not db.is_configured: + return {"rows": []} + + rows = service.all_segments_overview() + return {"rows": rows} + + except RequestError as e: + if _is_forbidden(e): + logger.warning( + "Databricks 403 on all_segments_overview", + extra={"error": str(e), "endpoint": "all_segments_overview"}, + ) + raise HTTPException(status_code=403, detail=_forbidden_detail()) + logger.exception( + "All segments overview error", + extra={"error": str(e), "endpoint": "all_segments_overview"}, + ) + raise HTTPException(status_code=500, detail=GENERIC_ERROR_MESSAGE) + except Exception as e: + logger.exception( + "All segments overview error", + extra={"error": str(e), "endpoint": "all_segments_overview"}, + ) + raise HTTPException(status_code=500, detail=GENERIC_ERROR_MESSAGE) + + +@router.patch("/{segment_name}") +async def update_segment_metadata( + segment_name: str, + request: SegmentUpdateRequest, + db: DatabricksClient = Depends(get_databricks_client), + service: SegmentService = Depends(get_segment_service), +): + """Update segment metadata (definition, quarter, flight dates) in Delta table.""" + try: + if not db.is_configured: + raise HTTPException( + status_code=503, + detail="Databricks not configured. Cannot update segment.", + ) + + service.update_segment_metadata( + segment_name=segment_name, + segment_definition=request.segment_definition, + quarter=request.quarter, + start_date=request.start_date, + end_date=request.end_date, + ) + return {"status": "updated", "segment_name": segment_name} + + except Exception as e: + logger.exception( + "Update segment error", + extra={"error": str(e), "endpoint": "update_segment", "segment_name": segment_name}, + ) + raise HTTPException(status_code=500, detail=GENERIC_ERROR_MESSAGE) diff --git a/adtech_series_sp26/segment_builder/backend/routers/settings.py b/adtech_series_sp26/segment_builder/backend/routers/settings.py new file mode 100644 index 0000000..70d650e --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/routers/settings.py @@ -0,0 +1,115 @@ +"""API routes for app settings (e.g. table configuration).""" + +import os +from typing import Any + +from fastapi import APIRouter +from pydantic import BaseModel + +from backend.config.column_overrides import get_column_configs, set_column_config +from backend.config.table_overrides import get_catalog_schemas_for_grants, get_tables, set_table_overrides + +router = APIRouter(prefix="/api/settings", tags=["settings"]) + +PRINCIPAL_PLACEHOLDER = "" + + +def _get_app_principal() -> str | None: + """Return this app's identity when running in Databricks Apps (DATABRICKS_CLIENT_ID).""" + client_id = (os.environ.get("DATABRICKS_CLIENT_ID") or "").strip() + return client_id if client_id else None + + +class TablesSettingsBody(BaseModel): + """Table names and optional column configs.""" + + profiles_table: str = "" + campaigns_table: str = "" + definitions_table: str = "" + column_configs: dict[str, Any] | None = None + + +@router.get("/grants-sql") +async def get_grants_sql(): + """Return Unity Catalog grant SQL for the current table settings (copy-paste ready). + When running in Databricks Apps, the app's identity (DATABRICKS_CLIENT_ID) is detected + and used in the SQL so no placeholder replacement is needed. + """ + ctx = get_catalog_schemas_for_grants() + catalog = ctx.get("catalog") + profiles_schema = ctx.get("profiles_schema") + segments_schema = ctx.get("segments_schema") + + principal = _get_app_principal() + + if not catalog: + return { + "sql": "-- Set table names above (catalog.schema.table) and save to generate grant SQL.", + "principal_placeholder": PRINCIPAL_PLACEHOLDER, + "principal_detected": principal, + } + + if principal: + lines = [ + "-- Unity Catalog grants for this app's tables.", + f"-- Identity detected from this app (DATABRICKS_CLIENT_ID): {principal}", + "", + f"GRANT USE_CATALOG ON CATALOG {catalog} TO `{principal}`;", + "", + ] + else: + lines = [ + "-- Unity Catalog grants for this app's tables.", + "-- Replace " + PRINCIPAL_PLACEHOLDER + " with your app's service principal client ID.", + "-- In Databricks Apps this is set automatically (DATABRICKS_CLIENT_ID).", + "-- Locally: databricks apps get | grep service_principal_client_id", + "", + f"GRANT USE_CATALOG ON CATALOG {catalog} TO {PRINCIPAL_PLACEHOLDER};", + "", + ] + + to_sql = f"`{principal}`" if principal else PRINCIPAL_PLACEHOLDER + if profiles_schema: + lines.append("-- Profiles (read-only)") + lines.append( + f"GRANT USE_SCHEMA, SELECT ON SCHEMA {catalog}.{profiles_schema} TO {to_sql};" + ) + lines.append("") + if segments_schema: + lines.append("-- Segments (read/write)") + lines.append( + f"GRANT USE_SCHEMA, SELECT, MODIFY ON SCHEMA {catalog}.{segments_schema} TO {to_sql};" + ) + + return { + "sql": "\n".join(lines), + "principal_placeholder": PRINCIPAL_PLACEHOLDER, + "principal_detected": principal, + } + + +@router.get("/tables") +async def get_settings_tables(): + """Return current table names and column configs.""" + data: dict[str, Any] = dict(get_tables()) + data["column_configs"] = get_column_configs() + return data + + +@router.put("/tables") +async def put_settings_tables(body: TablesSettingsBody): + """Update table name overrides and/or column configs.""" + set_table_overrides( + profiles_table=body.profiles_table, + campaigns_table=body.campaigns_table, + definitions_table=body.definitions_table, + ) + if body.column_configs: + set_column_config( + profile=body.column_configs.get("profile"), + segment_list=body.column_configs.get("segment_list"), + segment_info_labels=body.column_configs.get("segment_info_labels"), + ) + data: dict[str, Any] = dict(get_tables()) + data["column_configs"] = get_column_configs() + return data diff --git a/adtech_series_sp26/segment_builder/backend/services/__init__.py b/adtech_series_sp26/segment_builder/backend/services/__init__.py new file mode 100644 index 0000000..0557eb6 --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/services/__init__.py @@ -0,0 +1 @@ +# Services module diff --git a/adtech_series_sp26/segment_builder/backend/services/agent_service.py b/adtech_series_sp26/segment_builder/backend/services/agent_service.py new file mode 100644 index 0000000..2b0a850 --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/services/agent_service.py @@ -0,0 +1,381 @@ +"""Agent service for LLM-powered segment building.""" + +import json +import logging +import os +import re +from typing import Any + +from openai import OpenAI + +from backend.config.features import FEATURES, get_all_features +from backend.config.settings import get_settings +from backend.models.segment import SegmentCondition, SegmentDefinition, SegmentGroup +from backend.services.segment_service import get_segment_service +from backend.services.sql_generator import get_sql_generator + +logger = logging.getLogger(__name__) + + +def _detect_databricks_apps_env() -> bool: + """Detect if running in Databricks Apps environment (use SDK default auth).""" + return bool( + os.environ.get("DATABRICKS_APP_NAME") + or os.environ.get("DATABRICKS_HOST") + ) + + +def get_feature_schema() -> str: + """Generate a description of available features for the LLM.""" + lines = ["Available features for audience segmentation:"] + + for name, meta in FEATURES.items(): + feature_type = meta["type"] + operators = ", ".join(meta["operators"]) + desc = meta.get("description", "") + + line = f"- {name} ({meta['display_name']}): {feature_type}, operators: [{operators}]" + if desc: + line += f" - {desc}" + if meta.get("values"): + line += f" Values: {meta['values']}" + if meta.get("brackets"): + bracket_labels = [b["label"] for b in meta["brackets"]] + line += f" Brackets: {bracket_labels}" + elif meta.get("range"): + line += f" Range: {meta['range']['min']}-{meta['range']['max']}" + + lines.append(line) + + return "\n".join(lines) + + +SYSTEM_PROMPT = f"""You are an audience segmentation assistant for advertising campaigns. +You help users build audience segments by translating natural language requests into structured segment rules. + +{get_feature_schema()} + +When the user describes an audience, respond with: +1. A brief natural language confirmation of what you understood +2. A JSON segment definition wrapped in ```json blocks + +IMPORTANT: For features with brackets (age, income_level), ALWAYS use the IN operator with bracket labels. +- age brackets: "Under 18", "18-24", "25-34", "35-44", "45-54", "55-64", "65+" + - "over 50" → select brackets that include ages over 50: IN ["45-54", "55-64", "65+"] + - "25-54" → select brackets that overlap: IN ["25-34", "35-44", "45-54"] + - "young adults" → IN ["18-24", "25-34"] +- income_level brackets: "<10K", "10K-15K", "15K-25K", "25K-35K", "35K-50K", "50K-75K", "75K-100K", "100K-150K", "150K-200K", "200K+" + - "high income" → IN ["100K-150K", "150K-200K", "200K+"] + - "under 50K" → IN ["<10K", "10K-15K", "15K-25K", "25K-35K", "35K-50K"] +Do NOT use GT, LT, GTE, LTE, or BETWEEN operators for age or income_level. Always use bracket labels with IN. + +JSON format: + +```json +{{ + "name": "segment_name", + "description": "Human readable description", + "groups": [ + {{ + "id": "group_1", + "logic": "AND", + "conditions": [ + {{ + "id": "cond_1", + "feature": "feature_name", + "operator": "IS|IN|NOT", + "values": ["value1", "value2"] + }} + ] + }} + ], + "group_logic": "AND" +}} +``` + +Rules: +- Use exact feature names from the list above +- Use appropriate operators for the feature type +- For boolean features, use true/false as values +- For IN/NOT operators, provide multiple values as an array +- Generate unique IDs for groups and conditions (e.g., "group_1", "cond_1") +- Always wrap the JSON output in ```json code blocks + +Example: +User: "Cat owners over 50 in Texas" +Response: I'll create a segment for cat owners aged 45 and above in Texas. + +```json +{{ + "name": "cat_owners_over_50_texas", + "description": "Cat owners over 50 in Texas", + "groups": [ + {{ + "id": "group_1", + "logic": "AND", + "conditions": [ + {{"id": "cond_1", "feature": "is_cat_owner", "operator": "IS", "values": [true]}}, + {{"id": "cond_2", "feature": "age", "operator": "IN", "values": ["45-54", "55-64", "65+"]}}, + {{"id": "cond_3", "feature": "state", "operator": "IS", "values": ["TX"]}} + ] + }} + ], + "group_logic": "AND" +}} +``` + +If the user asks to modify an existing segment, update only the relevant parts while preserving the rest. +If the request is unclear, ask clarifying questions. +Always respond with valid JSON that can be parsed.""" + + +class AgentService: + """Service for LLM-powered segment building.""" + + def __init__(self): + self.settings = get_settings() + self._use_sdk_auth = _detect_databricks_apps_env() + self._workspace_client = None + self._static_client: OpenAI | None = None + self.segment_service = get_segment_service() + self.sql_generator = get_sql_generator() + + if self._use_sdk_auth: + from databricks.sdk import WorkspaceClient + self._workspace_client = WorkspaceClient() + logger.info("AgentService: Using Databricks Apps SDK auth") + elif self.settings.databricks_server_hostname and self.settings.databricks_token: + base_url = f"https://{self.settings.databricks_server_hostname}/serving-endpoints" + self._static_client = OpenAI( + api_key=self.settings.databricks_token, + base_url=base_url, + ) + logger.info("AgentService: Using env var auth") + elif self.settings.databricks_config_profile: + from databricks.sdk import WorkspaceClient + self._workspace_client = WorkspaceClient(profile=self.settings.databricks_config_profile) + logger.info("AgentService: Using profile '%s' for local dev", self.settings.databricks_config_profile) + else: + raise RuntimeError( + "AgentService: No Databricks config. Set DATABRICKS_SERVER_HOSTNAME and DATABRICKS_TOKEN, " + "set DATABRICKS_CONFIG_PROFILE for local dev (default: e2-demo-field-eng), " + "or deploy to Databricks Apps." + ) + + def _get_client(self) -> OpenAI: + """Get an OpenAI client (profile/SDK or static env).""" + if self._workspace_client: + host = self._workspace_client.config.host.replace("https://", "").replace("http://", "") + headers = self._workspace_client.config.authenticate() + token = headers.get("Authorization", "").replace("Bearer ", "") + if not token: + raise RuntimeError("Failed to get OAuth token from Databricks SDK") + return OpenAI(api_key=token, base_url=f"https://{host}/serving-endpoints") + return self._static_client + + @property + def agent_mode(self) -> str: + """Return the current agent mode.""" + return "llm" + + def _extract_json_from_response(self, text: str) -> dict | None: + """Extract JSON from LLM response text.""" + # Try to find JSON between ```json and ``` + json_match = re.search(r'```json\s*([\s\S]*?)\s*```', text) + if json_match: + try: + return json.loads(json_match.group(1)) + except json.JSONDecodeError: + pass + + # Try to find raw JSON + try: + # Find first { and last } + start = text.find('{') + end = text.rfind('}') + if start != -1 and end != -1: + return json.loads(text[start:end + 1]) + except json.JSONDecodeError: + pass + + return None + + def _strip_json_from_response(self, text: str) -> str: + """Remove JSON code blocks from the response text shown to the user.""" + # Remove ```json ... ``` blocks + cleaned = re.sub(r'```json\s*[\s\S]*?\s*```', '', text) + # Remove any remaining ``` blocks that look like JSON + cleaned = re.sub(r'```\s*\{[\s\S]*?\}\s*```', '', cleaned) + # Collapse multiple blank lines into one + cleaned = re.sub(r'\n{3,}', '\n\n', cleaned) + return cleaned.strip() + + def _json_to_segment(self, data: dict) -> SegmentDefinition: + """Convert JSON dict to SegmentDefinition.""" + groups = [] + for g in data.get("groups", []): + conditions = [] + for c in g.get("conditions", []): + conditions.append(SegmentCondition( + id=c.get("id", f"cond_{len(conditions)}"), + feature=c["feature"], + operator=c["operator"], + values=c["values"], + )) + groups.append(SegmentGroup( + id=g.get("id", f"group_{len(groups)}"), + logic=g.get("logic", "AND"), + conditions=conditions, + )) + + return SegmentDefinition( + name=data.get("name", ""), + description=data.get("description", ""), + groups=groups, + group_logic=data.get("group_logic", "AND"), + ) + + def parse_input( + self, + user_input: str, + conversation_history: list[dict[str, str]], + current_segment: SegmentDefinition | None = None, + ) -> dict[str, Any]: + """Parse natural language input to segment rules.""" + # Build messages for LLM + messages = [{"role": "system", "content": SYSTEM_PROMPT}] + + # Add conversation history + for msg in conversation_history: + messages.append({ + "role": msg.get("role", "user"), + "content": msg.get("content", ""), + }) + + # Add current segment context if exists + if current_segment and current_segment.groups: + context = f"\n\nCurrent segment state:\n```json\n{json.dumps(current_segment.model_dump(), indent=2)}\n```" + messages.append({ + "role": "system", + "content": f"The user has an existing segment. Modify it based on their request.{context}" + }) + + # Add user input + messages.append({"role": "user", "content": user_input}) + + try: + client = self._get_client() + logger.info(f"AgentService: Calling LLM (model={self.settings.databricks_model_endpoint})") + response = client.chat.completions.create( + model=self.settings.databricks_model_endpoint, + messages=messages, + max_tokens=2000, + temperature=0.1, + ) + + raw_response = response.choices[0].message.content + segment_json = self._extract_json_from_response(raw_response) + response_text = self._strip_json_from_response(raw_response) + + # Convert to segment definition + segment = None + preview = None + sql = None + + if segment_json: + segment = self._json_to_segment(segment_json) + + # Generate SQL + sql = self.sql_generator.generate_count_query(segment) + + # Try to get preview (may fail if Databricks not connected) + try: + preview_result = self.segment_service.preview_segment(segment, include_sql=False) + preview = { + "individual_count": preview_result.individual_count, + "household_count": preview_result.household_count, + } + except Exception as e: + logger.warning(f"Could not get preview: {e}") + preview = {"individual_count": 0, "household_count": 0} + + return { + "response_text": response_text, + "segment": segment.model_dump() if segment else None, + "preview": preview, + "sql": sql, + } + + except Exception as e: + error_msg = str(e) + logger.error(f"AgentService: LLM call failed: {error_msg}") + + if "404" in error_msg or "not found" in error_msg.lower(): + raise RuntimeError( + f"Model endpoint '{self.settings.databricks_model_endpoint}' not found. " + "Check DATABRICKS_MODEL_ENDPOINT configuration." + ) from e + + raise + + def summarize_segment( + self, + segment: SegmentDefinition, + conversation_history: list[dict[str, str]], + ) -> tuple[str, str]: + """Generate a 1-2 sentence summary and a suggested segment name from the conversation context. + Returns (summary, suggested_name).""" + messages = [{"role": "system", "content": ( + "You are an advertising audience analyst. Given a segment definition and the " + "conversation that led to it, respond with valid JSON only (no markdown, no extra text), " + "with exactly two keys: \"summary\" and \"suggested_name\". " + "\"summary\": 1-2 short sentences summarizing the target audience for the description field. " + "Be concise and business-friendly. " + "\"suggested_name\": a short segment name suitable for a campaign identifier: use underscores, " + "no spaces, alphanumeric plus underscores only (e.g. CA_Dog_Owners_25_54). Keep it under 50 characters." + )}] + for msg in conversation_history: + messages.append({"role": msg.get("role", "user"), "content": msg.get("content", "")}) + segment_context = ( + f"Segment definition (JSON):\n{json.dumps(segment.model_dump(), indent=2)}\n\n" + "Output JSON with keys: summary, suggested_name." + ) + messages.append({"role": "user", "content": segment_context}) + + try: + client = self._get_client() + response = client.chat.completions.create( + model=self.settings.databricks_model_endpoint, + messages=messages, + max_tokens=200, + temperature=0.2, + ) + raw = (response.choices[0].message.content or "").strip() + # Strip markdown code fence if present + if raw.startswith("```"): + raw = raw.split("\n", 1)[-1].rsplit("```", 1)[0].strip() + data = json.loads(raw) + summary = (data.get("summary") or "").strip() or "" + name = (data.get("suggested_name") or "").strip() + # Sanitize name: alphanumeric and underscores only + name = "".join(c if c.isalnum() or c == "_" else "_" for c in name)[:50] + return (summary, name or "Suggested_Segment") + except (json.JSONDecodeError, KeyError) as e: + logger.warning("AgentService: summarize_segment JSON parse failed: %s", e) + # Fallback: treat whole response as summary + return (raw if isinstance(raw, str) else "Suggested segment.", "Suggested_Segment") + except Exception as e: + logger.warning("AgentService: summarize_segment failed: %s", e) + raise + + +# Singleton instance +_agent: AgentService | None = None + + +def get_agent_service() -> AgentService: + """Get the singleton agent service.""" + global _agent + if _agent is None: + _agent = AgentService() + return _agent diff --git a/adtech_series_sp26/segment_builder/backend/services/databricks_client.py b/adtech_series_sp26/segment_builder/backend/services/databricks_client.py new file mode 100644 index 0000000..e97095d --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/services/databricks_client.py @@ -0,0 +1,186 @@ +"""Databricks SQL connector wrapper.""" + +import logging +import os +from typing import Any + +from databricks.sql.exc import RequestError + +from backend.config.settings import get_settings +from backend.config.table_overrides import get_profiles_table + +logger = logging.getLogger(__name__) + + +def _is_auth_403(err: RequestError) -> bool: + """True if the error is a 403 FORBIDDEN auth/credential issue (e.g. expired token).""" + msg = (getattr(err, "message", None) or str(err)).upper() + return "403" in msg or "FORBIDDEN" in msg + + +class DatabricksClient: + """Client for executing SQL queries against Databricks.""" + + def __init__(self): + self.settings = get_settings() + self._connection = None + self._use_sdk_auth = self._detect_databricks_apps_env() + + def _detect_databricks_apps_env(self) -> bool: + """Detect if running in Databricks Apps environment (use SDK default auth).""" + # Official Databricks Apps env vars: https://docs.databricks.com/dev-tools/databricks-apps/system-env + return bool( + os.environ.get("DATABRICKS_APP_NAME") + or os.environ.get("DATABRICKS_HOST") + ) + + @property + def is_configured(self) -> bool: + """Check if Databricks is properly configured.""" + if self._use_sdk_auth: + return True + if self.settings.is_databricks_configured: + return True + return bool(self.settings.databricks_config_profile) + + def _get_workspace_client(self): + """Create a WorkspaceClient (Apps default auth or profile for local dev). Prefer env vars over profile.""" + from databricks.sdk import WorkspaceClient + if self._use_sdk_auth: + return WorkspaceClient() + if self.settings.is_databricks_configured: + return None # use env var path in _get_connection + if self.settings.databricks_config_profile: + return WorkspaceClient(profile=self.settings.databricks_config_profile) + return None + + def _get_connection(self): + """Get or create a Databricks SQL connection.""" + if self._connection is None: + from databricks import sql + + w = self._get_workspace_client() + if w is not None: + # Apps or profile-based auth: use SDK to get host, warehouse, token + auth_label = "Databricks Apps" if self._use_sdk_auth else f"profile '{self.settings.databricks_config_profile}'" + logger.info("Using %s for SQL connection", auth_label) + from databricks.sdk.service.sql import State + + host = w.config.host.replace("https://", "").replace("http://", "") + warehouses = list(w.warehouses.list()) + running_warehouses = [wh for wh in warehouses if wh.state == State.RUNNING] + if not running_warehouses: + raise RuntimeError("No running SQL warehouses available") + warehouse = next( + (wh for wh in running_warehouses if "serverless" in wh.name.lower() or "shared" in wh.name.lower()), + running_warehouses[0], + ) + logger.info("Using warehouse: %s (%s)", warehouse.name, warehouse.id) + headers = w.config.authenticate() + auth_header = headers.get("Authorization", "") + if not auth_header.startswith("Bearer "): + raise RuntimeError("Failed to get OAuth token from Databricks SDK") + token = auth_header.replace("Bearer ", "") + self._connection = sql.connect( + server_hostname=host, + http_path=f"/sql/1.0/warehouses/{warehouse.id}", + access_token=token, + ) + elif self.settings.is_databricks_configured: + logger.info("Using environment variable authentication") + self._connection = sql.connect( + server_hostname=self.settings.databricks_server_hostname, + http_path=self.settings.databricks_http_path, + access_token=self.settings.databricks_token, + ) + else: + raise RuntimeError( + "Databricks not configured. Set DATABRICKS_SERVER_HOSTNAME, " + "DATABRICKS_HTTP_PATH, and DATABRICKS_TOKEN, or set DATABRICKS_CONFIG_PROFILE for local dev." + ) + return self._connection + + def execute_query(self, query: str) -> list[dict[str, Any]]: + """Execute a SQL query and return results as list of dicts. + On 403 (e.g. expired token), clears the cached connection and retries once. + """ + last_error: RequestError | None = None + for attempt in range(2): + try: + logger.info("Executing query: %s...", query[:200]) + connection = self._get_connection() + with connection.cursor() as cursor: + cursor.execute(query) + columns = [desc[0] for desc in cursor.description] if cursor.description else [] + rows = cursor.fetchall() + return [dict(zip(columns, row)) for row in rows] + except RequestError as e: + last_error = e + if attempt == 0 and _is_auth_403(e): + logger.warning( + "Databricks 403 (likely expired token), clearing connection and retrying once: %s", + e, + ) + self._clear_connection() + continue + raise + if last_error: + raise last_error + return [] + + def execute_count_query(self, query: str) -> dict[str, int]: + """Execute a count query and return the first row as a dict.""" + results = self.execute_query(query) + if results: + return results[0] + return {} + + def fetch_distinct_values( + self, + column: str, + table: str | None = None, + search: str | None = None, + limit: int = 100, + ) -> list[str]: + """Fetch distinct values for a column.""" + if table is None: + table = get_profiles_table() + + query = f""" + SELECT DISTINCT {column} + FROM {table} + WHERE {column} IS NOT NULL + """ + + if search: + query += f" AND LOWER({column}) LIKE LOWER('%{search}%')" + + query += f" ORDER BY {column} LIMIT {limit}" + + results = self.execute_query(query) + return [str(row[column]) for row in results] + + def _clear_connection(self) -> None: + """Close and clear the cached connection (e.g. after token expiry 403).""" + if self._connection: + try: + self._connection.close() + except Exception as e: + logger.warning("Error closing Databricks connection: %s", e) + self._connection = None + + def close(self): + """Close the connection.""" + self._clear_connection() + + +# Singleton instance +_client: DatabricksClient | None = None + + +def get_databricks_client() -> DatabricksClient: + """Get the singleton Databricks client.""" + global _client + if _client is None: + _client = DatabricksClient() + return _client diff --git a/adtech_series_sp26/segment_builder/backend/services/segment_service.py b/adtech_series_sp26/segment_builder/backend/services/segment_service.py new file mode 100644 index 0000000..a4305d1 --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/services/segment_service.py @@ -0,0 +1,194 @@ +"""Business logic for segment operations.""" + +import logging +import time +import uuid +from datetime import date + +from backend.config.table_overrides import ( + get_campaigns_table, + get_definitions_table, + get_profiles_table, +) +from backend.config.column_overrides import ( + get_campaigns_identity_household_column, + get_campaigns_identity_individual_column, + get_campaigns_segment_name_column, + get_profile_identity_household_column, + get_profile_identity_individual_column, +) +from backend.models.segment import ( + SegmentBuildResponse, + SegmentDefinition, + SegmentPreviewResponse, +) +from backend.services.databricks_client import get_databricks_client +from backend.services.sql_generator import get_sql_generator + +logger = logging.getLogger(__name__) + + +class SegmentService: + """Service for segment operations.""" + + def __init__(self): + self.db = get_databricks_client() + self.sql_gen = get_sql_generator() + + def preview_segment( + self, segment: SegmentDefinition, include_sql: bool = True + ) -> SegmentPreviewResponse: + """Execute segment query and return preview counts.""" + # Generate SQL + sql = self.sql_gen.generate_count_query(segment) + logger.info(f"Preview SQL: {sql}") + + # Execute query + start_time = time.time() + result = self.db.execute_count_query(sql) + execution_time_ms = (time.time() - start_time) * 1000 + + return SegmentPreviewResponse( + individual_count=result.get("individual_count", 0), + household_count=result.get("household_count", 0), + sql=sql if include_sql else None, + execution_time_ms=execution_time_ms, + ) + + def build_segment( + self, + segment: SegmentDefinition, + name: str, + quarter: str, + start_date: date, + end_date: date, + ) -> SegmentBuildResponse: + """Build and save a segment to Databricks tables.""" + segment_id = str(uuid.uuid4())[:8] + campaign_name = f"{name}_{segment_id}" + + # 1. Insert into segment_definitions + definition_sql = f""" +INSERT INTO {get_definitions_table()} +(segment_name, segment_definition, quarter, start_date, end_date) +VALUES ( + '{campaign_name}', + '{segment.description.replace("'", "''")}', + '{quarter}', + '{start_date.isoformat()}', + '{end_date.isoformat()}' +) + """.strip() + + logger.info(f"Inserting segment definition: {definition_sql}") + self.db.execute_query(definition_sql) + + # 2. Insert into campaigns table (profile identity cols in SELECT, campaigns cols in INSERT) + where_clause = self.sql_gen.generate_where_clause(segment) + p_ind = get_profile_identity_individual_column() + p_hh = get_profile_identity_household_column() + c_ind = get_campaigns_identity_individual_column() + c_hh = get_campaigns_identity_household_column() + seg_col = get_campaigns_segment_name_column() + campaigns_sql = f""" +INSERT INTO {get_campaigns_table()} +({c_ind}, {c_hh}, {seg_col}) +SELECT {p_ind}, {p_hh}, '{campaign_name}' +FROM {get_profiles_table()} +WHERE {where_clause} + """.strip() + + logger.info(f"Inserting campaign members: {campaigns_sql[:200]}...") + self.db.execute_query(campaigns_sql) + + # 3. Count inserted rows + seg_col = get_campaigns_segment_name_column() + count_sql = f""" +SELECT COUNT(*) as count +FROM {get_campaigns_table()} +WHERE {seg_col} = '{campaign_name}' + """.strip() + + result = self.db.execute_count_query(count_sql) + rows_inserted = result.get("count", 0) + + return SegmentBuildResponse( + segment_id=segment_id, + rows_inserted=rows_inserted, + campaign_name=campaign_name, + ) + + def list_segments(self) -> list[dict]: + """List all existing segments.""" + sql = f""" +SELECT + segment_name, + segment_definition, + quarter, + start_date, + end_date +FROM {get_definitions_table()} +ORDER BY segment_name + """.strip() + + return self.db.execute_query(sql) + + def all_segments_overview(self) -> list[dict]: + """Definitions joined with campaigns, grouped by campaign with individual/household counts.""" + c_ind = get_campaigns_identity_individual_column() + c_hh = get_campaigns_identity_household_column() + seg_col = get_campaigns_segment_name_column() + sql = f""" +SELECT + d.segment_name, + d.segment_definition, + d.quarter, + d.start_date, + d.end_date, + COUNT(DISTINCT c.{c_ind}) AS individual_count, + COUNT(DISTINCT c.{c_hh}) AS household_count +FROM {get_definitions_table()} d +LEFT JOIN {get_campaigns_table()} c ON d.segment_name = c.{seg_col} +GROUP BY d.segment_name, d.segment_definition, d.quarter, d.start_date, d.end_date +ORDER BY d.segment_name + """.strip() + + return self.db.execute_query(sql) + + def update_segment_metadata( + self, + segment_name: str, + segment_definition: str, + quarter: str, + start_date: date, + end_date: date, + ) -> None: + """Update a segment's metadata in megacorp_segment_definitions.""" + # Escape single quotes for SQL + safe_def = segment_definition.replace("'", "''") + safe_name = segment_name.replace("'", "''") + + sql = f""" +UPDATE {get_definitions_table()} +SET + segment_definition = '{safe_def}', + quarter = '{quarter}', + start_date = '{start_date.isoformat()}', + end_date = '{end_date.isoformat()}' +WHERE segment_name = '{safe_name}' + """.strip() + + logger.info("Updating segment metadata: %s", segment_name) + self.db.execute_query(sql) + + +# Singleton instance +_service: SegmentService | None = None + + +def get_segment_service() -> SegmentService: + """Get the singleton segment service.""" + global _service + if _service is None: + _service = SegmentService() + return _service diff --git a/adtech_series_sp26/segment_builder/backend/services/sql_generator.py b/adtech_series_sp26/segment_builder/backend/services/sql_generator.py new file mode 100644 index 0000000..cd6b3bb --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/services/sql_generator.py @@ -0,0 +1,227 @@ +"""SQL generator service for converting segment rules to SQL.""" + +from backend.config.features import SAFE_COLUMNS, get_feature +from backend.config.settings import get_settings +from backend.config.table_overrides import get_profiles_table +from backend.config.column_overrides import ( + get_profile_identity_household_column, + get_profile_identity_individual_column, +) +from backend.models.segment import SegmentCondition, SegmentDefinition, SegmentGroup + + +class SqlGeneratorError(Exception): + """Error in SQL generation.""" + + pass + + +class SqlGenerator: + """Generate SQL queries from segment definitions.""" + + def __init__(self): + self.settings = get_settings() # for other settings if needed + + def _validate_column(self, column: str) -> str: + """Validate column name against whitelist.""" + if column not in SAFE_COLUMNS: + raise SqlGeneratorError(f"Invalid column: {column}") + return column + + def _sanitize_string_value(self, value: str) -> str: + """Sanitize string values for SQL (escape single quotes).""" + return str(value).replace("'", "''") + + def _format_value(self, value: str | int | float | bool, feature_type: str) -> str: + """Format a value for SQL based on feature type.""" + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (int, float)): + return str(value) + if feature_type == "boolean": + return "true" if str(value).lower() in ("true", "1", "yes") else "false" + # String value + return f"'{self._sanitize_string_value(value)}'" + + def _generate_bracket_case_expression( + self, column: str, brackets: list[dict], bracket_style: str + ) -> str: + """Build a CASE WHEN expression from bracket definitions.""" + whens = [] + for b in brackets: + label = self._sanitize_string_value(b["label"]) + has_min = "min" in b + has_max = "max" in b + + if bracket_style == "inclusive": + if has_min and has_max: + whens.append( + f"WHEN {column} BETWEEN {b['min']} AND {b['max']} THEN '{label}'" + ) + elif has_max: + whens.append(f"WHEN {column} < {b['max'] + 1} THEN '{label}'") + elif has_min: + whens.append(f"WHEN {column} >= {b['min']} THEN '{label}'") + else: # exclusive_upper + if has_min and has_max: + whens.append( + f"WHEN {column} >= {b['min']} AND {column} < {b['max']} THEN '{label}'" + ) + elif has_max: + whens.append(f"WHEN {column} < {b['max']} THEN '{label}'") + elif has_min: + whens.append(f"WHEN {column} >= {b['min']} THEN '{label}'") + + return f"CASE {' '.join(whens)} ELSE NULL END" + + def _is_bracket_condition(self, condition: SegmentCondition, feature: dict) -> bool: + """Check if a condition should use bracket-based SQL.""" + if not feature.get("brackets"): + return False + if condition.operator not in ("IN", "NOT"): + return False + bracket_labels = {b["label"] for b in feature["brackets"]} + return all(str(v) in bracket_labels for v in condition.values) + + def _generate_condition_sql(self, condition: SegmentCondition) -> str: + """Generate SQL for a single condition.""" + column = self._validate_column(condition.feature) + feature = get_feature(condition.feature) + + if not feature: + raise SqlGeneratorError(f"Unknown feature: {condition.feature}") + + feature_type = feature["type"] + values = condition.values + + if not values: + raise SqlGeneratorError(f"No values provided for condition on {column}") + + # Handle bracket-based IN/NOT conditions + if self._is_bracket_condition(condition, feature): + case_expr = self._generate_bracket_case_expression( + column, feature["brackets"], feature.get("bracket_style", "inclusive") + ) + sanitized_labels = [ + f"'{self._sanitize_string_value(str(v))}'" for v in values + ] + labels_sql = ", ".join(sanitized_labels) + if condition.operator == "NOT": + return f"{case_expr} NOT IN ({labels_sql})" + return f"{case_expr} IN ({labels_sql})" + + # Handle different operators + if condition.operator == "IS": + formatted = self._format_value(values[0], feature_type) + return f"{column} = {formatted}" + + elif condition.operator == "IN": + formatted_values = [self._format_value(v, feature_type) for v in values] + return f"{column} IN ({', '.join(formatted_values)})" + + elif condition.operator == "NOT": + formatted_values = [self._format_value(v, feature_type) for v in values] + return f"{column} NOT IN ({', '.join(formatted_values)})" + + elif condition.operator == "BETWEEN": + if len(values) < 2: + raise SqlGeneratorError("BETWEEN requires two values") + min_val = self._format_value(values[0], feature_type) + max_val = self._format_value(values[1], feature_type) + return f"{column} BETWEEN {min_val} AND {max_val}" + + elif condition.operator == "GT": + formatted = self._format_value(values[0], feature_type) + return f"{column} > {formatted}" + + elif condition.operator == "LT": + formatted = self._format_value(values[0], feature_type) + return f"{column} < {formatted}" + + elif condition.operator == "GTE": + formatted = self._format_value(values[0], feature_type) + return f"{column} >= {formatted}" + + elif condition.operator == "LTE": + formatted = self._format_value(values[0], feature_type) + return f"{column} <= {formatted}" + + else: + raise SqlGeneratorError(f"Unknown operator: {condition.operator}") + + def _is_valid_condition(self, condition: SegmentCondition) -> bool: + """Check if a condition is valid (has feature and values).""" + return bool(condition.feature and condition.values) + + def _generate_group_sql(self, group: SegmentGroup) -> str: + """Generate SQL for a group of conditions.""" + if not group.conditions: + return "1=1" # Empty group matches all + + condition_sqls = [] + for condition in group.conditions: + # Skip incomplete conditions + if not self._is_valid_condition(condition): + continue + + sql = self._generate_condition_sql(condition) + + # Handle nullable columns + feature = get_feature(condition.feature) + if feature and feature.get("nullable"): + sql = f"({condition.feature} IS NOT NULL AND {sql})" + + condition_sqls.append(sql) + + # If all conditions were skipped, return match all + if not condition_sqls: + return "1=1" + + joiner = f" {group.logic} " + return f"({joiner.join(condition_sqls)})" + + def generate_where_clause(self, segment: SegmentDefinition) -> str: + """Generate the WHERE clause from a segment definition.""" + if not segment.groups: + return "1=1" # No conditions, match all + + group_sqls = [self._generate_group_sql(group) for group in segment.groups] + joiner = f" {segment.group_logic} " + return joiner.join(group_sqls) + + def generate_count_query(self, segment: SegmentDefinition) -> str: + """Generate a COUNT query for segment preview.""" + where_clause = self.generate_where_clause(segment) + + ind_col = get_profile_identity_individual_column() + hh_col = get_profile_identity_household_column() + return f""" +SELECT + COUNT(DISTINCT {ind_col}) as individual_count, + COUNT(DISTINCT {hh_col}) as household_count +FROM {get_profiles_table()} +WHERE {where_clause} + """.strip() + + def generate_select_query(self, segment: SegmentDefinition) -> str: + """Generate a SELECT query to get segment members.""" + where_clause = self.generate_where_clause(segment) + ind_col = get_profile_identity_individual_column() + hh_col = get_profile_identity_household_column() + return f""" +SELECT {ind_col}, {hh_col} +FROM {get_profiles_table()} +WHERE {where_clause} + """.strip() + + +# Singleton instance +_generator: SqlGenerator | None = None + + +def get_sql_generator() -> SqlGenerator: + """Get the singleton SQL generator.""" + global _generator + if _generator is None: + _generator = SqlGenerator() + return _generator diff --git a/adtech_series_sp26/segment_builder/backend/static/MegaCorp_Logo_-_Transparent.png b/adtech_series_sp26/segment_builder/backend/static/MegaCorp_Logo_-_Transparent.png new file mode 100644 index 0000000..c305727 Binary files /dev/null and b/adtech_series_sp26/segment_builder/backend/static/MegaCorp_Logo_-_Transparent.png differ diff --git a/adtech_series_sp26/segment_builder/backend/static/assets/index-C2L6jfCE.css b/adtech_series_sp26/segment_builder/backend/static/assets/index-C2L6jfCE.css new file mode 100644 index 0000000..0b97824 --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/static/assets/index-C2L6jfCE.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.bottom-2{bottom:.5rem}.right-2{right:.5rem}.top-2{top:.5rem}.z-50{z-index:50}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.mt-0{margin-top:0}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.box-border{box-sizing:border-box}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.size-12{width:3rem;height:3rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-12{max-height:3rem}.min-h-0{min-height:0px}.min-h-12{min-height:3rem}.min-h-\[120px\]{min-height:120px}.min-h-\[38px\]{min-height:38px}.w-12{width:3rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-40{width:10rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-auto{width:auto}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[120px\]{min-width:120px}.min-w-\[140px\]{min-width:140px}.min-w-\[200px\]{min-width:200px}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-\[60\%\]{max-width:60%}.max-w-\[80\%\]{max-width:80%}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.rotate-90{--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.resize-y{resize:vertical}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity, 1))}.self-start{align-self:flex-start}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-blue-100{--tw-border-opacity: 1;border-color:rgb(219 234 254 / var(--tw-border-opacity, 1))}.border-blue-300{--tw-border-opacity: 1;border-color:rgb(147 197 253 / var(--tw-border-opacity, 1))}.border-gray-100{--tw-border-opacity: 1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-200\/50{border-color:#e5e7eb80}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-purple-300{--tw-border-opacity: 1;border-color:rgb(216 180 254 / var(--tw-border-opacity, 1))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.bg-black\/50{background-color:#00000080}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-gray-50\/50{background-color:#f9fafb80}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-purple-100{--tw-bg-opacity: 1;background-color:rgb(243 232 255 / var(--tw-bg-opacity, 1))}.bg-purple-50{--tw-bg-opacity: 1;background-color:rgb(250 245 255 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.from-blue-100{--tw-gradient-from: #dbeafe var(--tw-gradient-from-position);--tw-gradient-to: rgb(219 234 254 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-purple-100{--tw-gradient-to: #f3e8ff var(--tw-gradient-to-position)}.object-contain{-o-object-fit:contain;object-fit:contain}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pl-1{padding-left:.25rem}.pl-5{padding-left:1.25rem}.pr-9{padding-right:2.25rem}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[11px\]{font-size:11px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-\[1\.25rem\]{line-height:1.25rem}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-blue-700{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-purple-600{--tw-text-opacity: 1;color:rgb(147 51 234 / var(--tw-text-opacity, 1))}.text-purple-700{--tw-text-opacity: 1;color:rgb(126 34 206 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.no-underline{text-decoration-line:none}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity, 1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity, 1))}.opacity-25{opacity:.25}.opacity-60{opacity:.6}.opacity-75{opacity:.75}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-\[width\]{transition-property:width;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}*,*:before,*:after{box-sizing:border-box}*{margin:0}body{line-height:1.5;-webkit-font-smoothing:antialiased}img,picture,video,canvas,svg{display:block;max-width:100%}input,button,textarea,select{font:inherit}p,h1,h2,h3,h4,h5,h6{overflow-wrap:break-word}#root{isolation:isolate;height:100vh}:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{margin:0;min-width:320px;min-height:100vh;background-color:#f5f5f5}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#f1f1f1;border-radius:4px}::-webkit-scrollbar-thumb{background:#c1c1c1;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#a8a8a8}.hover\:border-blue-400:hover{--tw-border-opacity: 1;border-color:rgb(96 165 250 / var(--tw-border-opacity, 1))}.hover\:bg-blue-200:hover{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-50:hover{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-600:hover{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-600:hover{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.hover\:bg-purple-200:hover{--tw-bg-opacity: 1;background-color:rgb(233 213 255 / var(--tw-bg-opacity, 1))}.hover\:text-blue-500:hover{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.hover\:text-blue-600:hover{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.hover\:text-blue-700:hover{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1))}.hover\:text-blue-800:hover{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.hover\:text-blue-900:hover{--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity, 1))}.hover\:text-gray-600:hover{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.hover\:text-gray-700:hover{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.hover\:text-gray-900:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.hover\:text-red-500:hover{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.focus\:border-gray-500:focus{--tw-border-opacity: 1;border-color:rgb(107 114 128 / var(--tw-border-opacity, 1))}.focus\:border-transparent:focus{border-color:transparent}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.focus\:ring-gray-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(107 114 128 / var(--tw-ring-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-gray-100:disabled{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-50:disabled{opacity:.5} diff --git a/adtech_series_sp26/segment_builder/backend/static/assets/index-yZs-_JiG.js b/adtech_series_sp26/segment_builder/backend/static/assets/index-yZs-_JiG.js new file mode 100644 index 0000000..0c3d0c6 --- /dev/null +++ b/adtech_series_sp26/segment_builder/backend/static/assets/index-yZs-_JiG.js @@ -0,0 +1,811 @@ +var Qf=e=>{throw TypeError(e)};var Vl=(e,t,n)=>t.has(e)||Qf("Cannot "+n);var b=(e,t,n)=>(Vl(e,t,"read from private field"),n?n.call(e):t.get(e)),z=(e,t,n)=>t.has(e)?Qf("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),L=(e,t,n,r)=>(Vl(e,t,"write to private field"),r?r.call(e,n):t.set(e,n),n),G=(e,t,n)=>(Vl(e,t,"access private method"),n);var Li=(e,t,n,r)=>({set _(s){L(e,t,s,n)},get _(){return b(e,t,r)}});(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))r(s);new MutationObserver(s=>{for(const a of s)if(a.type==="childList")for(const i of a.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(s){const a={};return s.integrity&&(a.integrity=s.integrity),s.referrerPolicy&&(a.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?a.credentials="include":s.crossOrigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function r(s){if(s.ep)return;s.ep=!0;const a=n(s);fetch(s.href,a)}})();function Nx(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Hm={exports:{}},ml={},Wm={exports:{}},K={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var yi=Symbol.for("react.element"),Rx=Symbol.for("react.portal"),jx=Symbol.for("react.fragment"),Px=Symbol.for("react.strict_mode"),Ox=Symbol.for("react.profiler"),Tx=Symbol.for("react.provider"),Ax=Symbol.for("react.context"),Ix=Symbol.for("react.forward_ref"),Lx=Symbol.for("react.suspense"),Fx=Symbol.for("react.memo"),Dx=Symbol.for("react.lazy"),qf=Symbol.iterator;function Mx(e){return e===null||typeof e!="object"?null:(e=qf&&e[qf]||e["@@iterator"],typeof e=="function"?e:null)}var Vm={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Qm=Object.assign,qm={};function ta(e,t,n){this.props=e,this.context=t,this.refs=qm,this.updater=n||Vm}ta.prototype.isReactComponent={};ta.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};ta.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function Gm(){}Gm.prototype=ta.prototype;function yd(e,t,n){this.props=e,this.context=t,this.refs=qm,this.updater=n||Vm}var vd=yd.prototype=new Gm;vd.constructor=yd;Qm(vd,ta.prototype);vd.isPureReactComponent=!0;var Gf=Array.isArray,Zm=Object.prototype.hasOwnProperty,xd={current:null},Km={key:!0,ref:!0,__self:!0,__source:!0};function Ym(e,t,n){var r,s={},a=null,i=null;if(t!=null)for(r in t.ref!==void 0&&(i=t.ref),t.key!==void 0&&(a=""+t.key),t)Zm.call(t,r)&&!Km.hasOwnProperty(r)&&(s[r]=t[r]);var o=arguments.length-2;if(o===1)s.children=n;else if(1>>1,ue=I[se];if(0>>1;ses(rs,q))Srs(Ii,rs)?(I[se]=Ii,I[Sr]=q,se=Sr):(I[se]=rs,I[gt]=q,se=gt);else if(Srs(Ii,q))I[se]=Ii,I[Sr]=q,se=Sr;else break e}}return H}function s(I,H){var q=I.sortIndex-H.sortIndex;return q!==0?q:I.id-H.id}if(typeof performance=="object"&&typeof performance.now=="function"){var a=performance;e.unstable_now=function(){return a.now()}}else{var i=Date,o=i.now();e.unstable_now=function(){return i.now()-o}}var l=[],u=[],c=1,d=null,m=3,v=!1,y=!1,x=!1,S=typeof setTimeout=="function"?setTimeout:null,p=typeof clearTimeout=="function"?clearTimeout:null,h=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function g(I){for(var H=n(u);H!==null;){if(H.callback===null)r(u);else if(H.startTime<=I)r(u),H.sortIndex=H.expirationTime,t(l,H);else break;H=n(u)}}function _(I){if(x=!1,g(I),!y)if(n(l)!==null)y=!0,ke(E);else{var H=n(u);H!==null&&ns(_,H.startTime-I)}}function E(I,H){y=!1,x&&(x=!1,p(k),k=-1),v=!0;var q=m;try{for(g(H),d=n(l);d!==null&&(!(d.expirationTime>H)||I&&!J());){var se=d.callback;if(typeof se=="function"){d.callback=null,m=d.priorityLevel;var ue=se(d.expirationTime<=H);H=e.unstable_now(),typeof ue=="function"?d.callback=ue:d===n(l)&&r(l),g(H)}else r(l);d=n(l)}if(d!==null)var Tn=!0;else{var gt=n(u);gt!==null&&ns(_,gt.startTime-H),Tn=!1}return Tn}finally{d=null,m=q,v=!1}}var C=!1,R=null,k=-1,T=5,A=-1;function J(){return!(e.unstable_now()-AI||125se?(I.sortIndex=q,t(u,I),n(l)===null&&I===n(u)&&(x?(p(k),k=-1):x=!0,ns(_,q-se))):(I.sortIndex=ue,t(l,I),y||v||(y=!0,ke(E))),I},e.unstable_shouldYield=J,e.unstable_wrapCallback=function(I){var H=m;return function(){var q=m;m=H;try{return I.apply(this,arguments)}finally{m=q}}}})(ng);tg.exports=ng;var Zx=tg.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Kx=w,dt=Zx;function j(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Iu=Object.prototype.hasOwnProperty,Yx=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Kf={},Yf={};function Jx(e){return Iu.call(Yf,e)?!0:Iu.call(Kf,e)?!1:Yx.test(e)?Yf[e]=!0:(Kf[e]=!0,!1)}function Xx(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function e1(e,t,n,r){if(t===null||typeof t>"u"||Xx(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function Ye(e,t,n,r,s,a,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=s,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=a,this.removeEmptyString=i}var De={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){De[e]=new Ye(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];De[t]=new Ye(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){De[e]=new Ye(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){De[e]=new Ye(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){De[e]=new Ye(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){De[e]=new Ye(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){De[e]=new Ye(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){De[e]=new Ye(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){De[e]=new Ye(e,5,!1,e.toLowerCase(),null,!1,!1)});var Sd=/[\-:]([a-z])/g;function _d(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Sd,_d);De[t]=new Ye(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Sd,_d);De[t]=new Ye(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Sd,_d);De[t]=new Ye(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){De[e]=new Ye(e,1,!1,e.toLowerCase(),null,!1,!1)});De.xlinkHref=new Ye("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){De[e]=new Ye(e,1,!1,e.toLowerCase(),null,!0,!0)});function kd(e,t,n,r){var s=De.hasOwnProperty(t)?De[t]:null;(s!==null?s.type!==0:r||!(2o||s[i]!==a[o]){var l=` +`+s[i].replace(" at new "," at ");return e.displayName&&l.includes("")&&(l=l.replace("",e.displayName)),l}while(1<=i&&0<=o);break}}}finally{Gl=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Sa(e):""}function t1(e){switch(e.tag){case 5:return Sa(e.type);case 16:return Sa("Lazy");case 13:return Sa("Suspense");case 19:return Sa("SuspenseList");case 0:case 2:case 15:return e=Zl(e.type,!1),e;case 11:return e=Zl(e.type.render,!1),e;case 1:return e=Zl(e.type,!0),e;default:return""}}function Mu(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case cs:return"Fragment";case us:return"Portal";case Lu:return"Profiler";case Ed:return"StrictMode";case Fu:return"Suspense";case Du:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case ag:return(e.displayName||"Context")+".Consumer";case sg:return(e._context.displayName||"Context")+".Provider";case Cd:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case Nd:return t=e.displayName||null,t!==null?t:Mu(e.type)||"Memo";case Ln:t=e._payload,e=e._init;try{return Mu(e(t))}catch{}}return null}function n1(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return Mu(t);case 8:return t===Ed?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function ur(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function og(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function r1(e){var t=og(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var s=n.get,a=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return s.call(this)},set:function(i){r=""+i,a.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Mi(e){e._valueTracker||(e._valueTracker=r1(e))}function lg(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=og(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Oo(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function $u(e,t){var n=t.checked;return ye({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Xf(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=ur(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function ug(e,t){t=t.checked,t!=null&&kd(e,"checked",t,!1)}function zu(e,t){ug(e,t);var n=ur(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?Uu(e,t.type,n):t.hasOwnProperty("defaultValue")&&Uu(e,t.type,ur(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function eh(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function Uu(e,t,n){(t!=="number"||Oo(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var _a=Array.isArray;function Ss(e,t,n,r){if(e=e.options,t){t={};for(var s=0;s"+t.valueOf().toString()+"",t=$i.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Wa(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Oa={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},s1=["Webkit","ms","Moz","O"];Object.keys(Oa).forEach(function(e){s1.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Oa[t]=Oa[e]})});function hg(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Oa.hasOwnProperty(e)&&Oa[e]?(""+t).trim():t+"px"}function pg(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,s=hg(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,s):e[n]=s}}var a1=ye({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Wu(e,t){if(t){if(a1[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(j(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(j(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(j(61))}if(t.style!=null&&typeof t.style!="object")throw Error(j(62))}}function Vu(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Qu=null;function Rd(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var qu=null,_s=null,ks=null;function rh(e){if(e=wi(e)){if(typeof qu!="function")throw Error(j(280));var t=e.stateNode;t&&(t=wl(t),qu(e.stateNode,e.type,t))}}function mg(e){_s?ks?ks.push(e):ks=[e]:_s=e}function gg(){if(_s){var e=_s,t=ks;if(ks=_s=null,rh(e),t)for(e=0;e>>=0,e===0?32:31-(g1(e)/y1|0)|0}var zi=64,Ui=4194304;function ka(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Lo(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,s=e.suspendedLanes,a=e.pingedLanes,i=n&268435455;if(i!==0){var o=i&~s;o!==0?r=ka(o):(a&=i,a!==0&&(r=ka(a)))}else i=n&~s,i!==0?r=ka(i):a!==0&&(r=ka(a));if(r===0)return 0;if(t!==0&&t!==r&&!(t&s)&&(s=r&-r,a=t&-t,s>=a||s===16&&(a&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function vi(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Dt(t),e[t]=n}function b1(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Aa),fh=" ",hh=!1;function Fg(e,t){switch(e){case"keyup":return Z1.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Dg(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var ds=!1;function Y1(e,t){switch(e){case"compositionend":return Dg(t);case"keypress":return t.which!==32?null:(hh=!0,fh);case"textInput":return e=t.data,e===fh&&hh?null:e;default:return null}}function J1(e,t){if(ds)return e==="compositionend"||!Fd&&Fg(e,t)?(e=Ig(),co=Ad=Kn=null,ds=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=yh(n)}}function Ug(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Ug(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Bg(){for(var e=window,t=Oo();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Oo(e.document)}return t}function Dd(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function ow(e){var t=Bg(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Ug(n.ownerDocument.documentElement,n)){if(r!==null&&Dd(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var s=n.textContent.length,a=Math.min(r.start,s);r=r.end===void 0?a:Math.min(r.end,s),!e.extend&&a>r&&(s=r,r=a,a=s),s=vh(n,a);var i=vh(n,r);s&&i&&(e.rangeCount!==1||e.anchorNode!==s.node||e.anchorOffset!==s.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(s.node,s.offset),e.removeAllRanges(),a>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,fs=null,Xu=null,La=null,ec=!1;function xh(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;ec||fs==null||fs!==Oo(r)||(r=fs,"selectionStart"in r&&Dd(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),La&&Ka(La,r)||(La=r,r=Mo(Xu,"onSelect"),0ms||(e.current=ic[ms],ic[ms]=null,ms--)}function le(e,t){ms++,ic[ms]=e.current,e.current=t}var cr={},Ve=vr(cr),nt=vr(!1),Hr=cr;function Bs(e,t){var n=e.type.contextTypes;if(!n)return cr;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var s={},a;for(a in n)s[a]=t[a];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=s),s}function rt(e){return e=e.childContextTypes,e!=null}function zo(){de(nt),de(Ve)}function Ch(e,t,n){if(Ve.current!==cr)throw Error(j(168));le(Ve,t),le(nt,n)}function Yg(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var s in r)if(!(s in t))throw Error(j(108,n1(e)||"Unknown",s));return ye({},n,r)}function Uo(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||cr,Hr=Ve.current,le(Ve,e),le(nt,nt.current),!0}function Nh(e,t,n){var r=e.stateNode;if(!r)throw Error(j(169));n?(e=Yg(e,t,Hr),r.__reactInternalMemoizedMergedChildContext=e,de(nt),de(Ve),le(Ve,e)):de(nt),le(nt,n)}var dn=null,bl=!1,uu=!1;function Jg(e){dn===null?dn=[e]:dn.push(e)}function xw(e){bl=!0,Jg(e)}function xr(){if(!uu&&dn!==null){uu=!0;var e=0,t=ie;try{var n=dn;for(ie=1;e>=i,s-=i,gn=1<<32-Dt(t)+s|n<k?(T=R,R=null):T=R.sibling;var A=m(p,R,g[k],_);if(A===null){R===null&&(R=T);break}e&&R&&A.alternate===null&&t(p,R),h=a(A,h,k),C===null?E=A:C.sibling=A,C=A,R=T}if(k===g.length)return n(p,R),he&&_r(p,k),E;if(R===null){for(;kk?(T=R,R=null):T=R.sibling;var J=m(p,R,A.value,_);if(J===null){R===null&&(R=T);break}e&&R&&J.alternate===null&&t(p,R),h=a(J,h,k),C===null?E=J:C.sibling=J,C=J,R=T}if(A.done)return n(p,R),he&&_r(p,k),E;if(R===null){for(;!A.done;k++,A=g.next())A=d(p,A.value,_),A!==null&&(h=a(A,h,k),C===null?E=A:C.sibling=A,C=A);return he&&_r(p,k),E}for(R=r(p,R);!A.done;k++,A=g.next())A=v(R,p,k,A.value,_),A!==null&&(e&&A.alternate!==null&&R.delete(A.key===null?k:A.key),h=a(A,h,k),C===null?E=A:C.sibling=A,C=A);return e&&R.forEach(function(te){return t(p,te)}),he&&_r(p,k),E}function S(p,h,g,_){if(typeof g=="object"&&g!==null&&g.type===cs&&g.key===null&&(g=g.props.children),typeof g=="object"&&g!==null){switch(g.$$typeof){case Di:e:{for(var E=g.key,C=h;C!==null;){if(C.key===E){if(E=g.type,E===cs){if(C.tag===7){n(p,C.sibling),h=s(C,g.props.children),h.return=p,p=h;break e}}else if(C.elementType===E||typeof E=="object"&&E!==null&&E.$$typeof===Ln&&Ph(E)===C.type){n(p,C.sibling),h=s(C,g.props),h.ref=pa(p,C,g),h.return=p,p=h;break e}n(p,C);break}else t(p,C);C=C.sibling}g.type===cs?(h=Ur(g.props.children,p.mode,_,g.key),h.return=p,p=h):(_=xo(g.type,g.key,g.props,null,p.mode,_),_.ref=pa(p,h,g),_.return=p,p=_)}return i(p);case us:e:{for(C=g.key;h!==null;){if(h.key===C)if(h.tag===4&&h.stateNode.containerInfo===g.containerInfo&&h.stateNode.implementation===g.implementation){n(p,h.sibling),h=s(h,g.children||[]),h.return=p,p=h;break e}else{n(p,h);break}else t(p,h);h=h.sibling}h=yu(g,p.mode,_),h.return=p,p=h}return i(p);case Ln:return C=g._init,S(p,h,C(g._payload),_)}if(_a(g))return y(p,h,g,_);if(ua(g))return x(p,h,g,_);Gi(p,g)}return typeof g=="string"&&g!==""||typeof g=="number"?(g=""+g,h!==null&&h.tag===6?(n(p,h.sibling),h=s(h,g),h.return=p,p=h):(n(p,h),h=gu(g,p.mode,_),h.return=p,p=h),i(p)):n(p,h)}return S}var Ws=ny(!0),ry=ny(!1),Wo=vr(null),Vo=null,vs=null,Ud=null;function Bd(){Ud=vs=Vo=null}function Hd(e){var t=Wo.current;de(Wo),e._currentValue=t}function uc(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function Cs(e,t){Vo=e,Ud=vs=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(tt=!0),e.firstContext=null)}function kt(e){var t=e._currentValue;if(Ud!==e)if(e={context:e,memoizedValue:t,next:null},vs===null){if(Vo===null)throw Error(j(308));vs=e,Vo.dependencies={lanes:0,firstContext:e}}else vs=vs.next=e;return t}var Cr=null;function Wd(e){Cr===null?Cr=[e]:Cr.push(e)}function sy(e,t,n,r){var s=t.interleaved;return s===null?(n.next=n,Wd(t)):(n.next=s.next,s.next=n),t.interleaved=n,Sn(e,r)}function Sn(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var Fn=!1;function Vd(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function ay(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function vn(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function rr(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,X&2){var s=r.pending;return s===null?t.next=t:(t.next=s.next,s.next=t),r.pending=t,Sn(e,n)}return s=r.interleaved,s===null?(t.next=t,Wd(r)):(t.next=s.next,s.next=t),r.interleaved=t,Sn(e,n)}function ho(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Pd(e,n)}}function Oh(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var s=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var i={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};a===null?s=a=i:a=a.next=i,n=n.next}while(n!==null);a===null?s=a=t:a=a.next=t}else s=a=t;n={baseState:r.baseState,firstBaseUpdate:s,lastBaseUpdate:a,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function Qo(e,t,n,r){var s=e.updateQueue;Fn=!1;var a=s.firstBaseUpdate,i=s.lastBaseUpdate,o=s.shared.pending;if(o!==null){s.shared.pending=null;var l=o,u=l.next;l.next=null,i===null?a=u:i.next=u,i=l;var c=e.alternate;c!==null&&(c=c.updateQueue,o=c.lastBaseUpdate,o!==i&&(o===null?c.firstBaseUpdate=u:o.next=u,c.lastBaseUpdate=l))}if(a!==null){var d=s.baseState;i=0,c=u=l=null,o=a;do{var m=o.lane,v=o.eventTime;if((r&m)===m){c!==null&&(c=c.next={eventTime:v,lane:0,tag:o.tag,payload:o.payload,callback:o.callback,next:null});e:{var y=e,x=o;switch(m=t,v=n,x.tag){case 1:if(y=x.payload,typeof y=="function"){d=y.call(v,d,m);break e}d=y;break e;case 3:y.flags=y.flags&-65537|128;case 0:if(y=x.payload,m=typeof y=="function"?y.call(v,d,m):y,m==null)break e;d=ye({},d,m);break e;case 2:Fn=!0}}o.callback!==null&&o.lane!==0&&(e.flags|=64,m=s.effects,m===null?s.effects=[o]:m.push(o))}else v={eventTime:v,lane:m,tag:o.tag,payload:o.payload,callback:o.callback,next:null},c===null?(u=c=v,l=d):c=c.next=v,i|=m;if(o=o.next,o===null){if(o=s.shared.pending,o===null)break;m=o,o=m.next,m.next=null,s.lastBaseUpdate=m,s.shared.pending=null}}while(!0);if(c===null&&(l=d),s.baseState=l,s.firstBaseUpdate=u,s.lastBaseUpdate=c,t=s.shared.interleaved,t!==null){s=t;do i|=s.lane,s=s.next;while(s!==t)}else a===null&&(s.shared.lanes=0);Qr|=i,e.lanes=i,e.memoizedState=d}}function Th(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=du.transition;du.transition={};try{e(!1),t()}finally{ie=n,du.transition=r}}function Sy(){return Et().memoizedState}function _w(e,t,n){var r=ar(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},_y(e))ky(t,n);else if(n=sy(e,t,n,r),n!==null){var s=Ze();Mt(n,e,r,s),Ey(n,t,r)}}function kw(e,t,n){var r=ar(e),s={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(_y(e))ky(t,s);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var i=t.lastRenderedState,o=a(i,n);if(s.hasEagerState=!0,s.eagerState=o,$t(o,i)){var l=t.interleaved;l===null?(s.next=s,Wd(t)):(s.next=l.next,l.next=s),t.interleaved=s;return}}catch{}finally{}n=sy(e,t,s,r),n!==null&&(s=Ze(),Mt(n,e,r,s),Ey(n,t,r))}}function _y(e){var t=e.alternate;return e===me||t!==null&&t===me}function ky(e,t){Fa=Go=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Ey(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Pd(e,n)}}var Zo={readContext:kt,useCallback:$e,useContext:$e,useEffect:$e,useImperativeHandle:$e,useInsertionEffect:$e,useLayoutEffect:$e,useMemo:$e,useReducer:$e,useRef:$e,useState:$e,useDebugValue:$e,useDeferredValue:$e,useTransition:$e,useMutableSource:$e,useSyncExternalStore:$e,useId:$e,unstable_isNewReconciler:!1},Ew={readContext:kt,useCallback:function(e,t){return Ht().memoizedState=[e,t===void 0?null:t],e},useContext:kt,useEffect:Ih,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,mo(4194308,4,yy.bind(null,t,e),n)},useLayoutEffect:function(e,t){return mo(4194308,4,e,t)},useInsertionEffect:function(e,t){return mo(4,2,e,t)},useMemo:function(e,t){var n=Ht();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ht();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=_w.bind(null,me,e),[r.memoizedState,e]},useRef:function(e){var t=Ht();return e={current:e},t.memoizedState=e},useState:Ah,useDebugValue:Xd,useDeferredValue:function(e){return Ht().memoizedState=e},useTransition:function(){var e=Ah(!1),t=e[0];return e=Sw.bind(null,e[1]),Ht().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=me,s=Ht();if(he){if(n===void 0)throw Error(j(407));n=n()}else{if(n=t(),Ae===null)throw Error(j(349));Vr&30||uy(r,t,n)}s.memoizedState=n;var a={value:n,getSnapshot:t};return s.queue=a,Ih(dy.bind(null,r,a,e),[e]),r.flags|=2048,si(9,cy.bind(null,r,a,n,t),void 0,null),n},useId:function(){var e=Ht(),t=Ae.identifierPrefix;if(he){var n=yn,r=gn;n=(r&~(1<<32-Dt(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=ni++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[qt]=t,e[Xa]=r,Ly(e,t,!1,!1),t.stateNode=e;e:{switch(i=Vu(n,r),n){case"dialog":ce("cancel",e),ce("close",e),s=r;break;case"iframe":case"object":case"embed":ce("load",e),s=r;break;case"video":case"audio":for(s=0;sqs&&(t.flags|=128,r=!0,ma(a,!1),t.lanes=4194304)}else{if(!r)if(e=qo(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),ma(a,!0),a.tail===null&&a.tailMode==="hidden"&&!i.alternate&&!he)return ze(t),null}else 2*Se()-a.renderingStartTime>qs&&n!==1073741824&&(t.flags|=128,r=!0,ma(a,!1),t.lanes=4194304);a.isBackwards?(i.sibling=t.child,t.child=i):(n=a.last,n!==null?n.sibling=i:t.child=i,a.last=i)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=Se(),t.sibling=null,n=pe.current,le(pe,r?n&1|2:n&1),t):(ze(t),null);case 22:case 23:return af(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?lt&1073741824&&(ze(t),t.subtreeFlags&6&&(t.flags|=8192)):ze(t),null;case 24:return null;case 25:return null}throw Error(j(156,t.tag))}function Aw(e,t){switch($d(t),t.tag){case 1:return rt(t.type)&&zo(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Vs(),de(nt),de(Ve),Gd(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return qd(t),null;case 13:if(de(pe),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(j(340));Hs()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return de(pe),null;case 4:return Vs(),null;case 10:return Hd(t.type._context),null;case 22:case 23:return af(),null;case 24:return null;default:return null}}var Ki=!1,Be=!1,Iw=typeof WeakSet=="function"?WeakSet:Set,D=null;function xs(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){xe(e,t,r)}else n.current=null}function vc(e,t,n){try{n()}catch(r){xe(e,t,r)}}var Vh=!1;function Lw(e,t){if(tc=Fo,e=Bg(),Dd(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var s=r.anchorOffset,a=r.focusNode;r=r.focusOffset;try{n.nodeType,a.nodeType}catch{n=null;break e}var i=0,o=-1,l=-1,u=0,c=0,d=e,m=null;t:for(;;){for(var v;d!==n||s!==0&&d.nodeType!==3||(o=i+s),d!==a||r!==0&&d.nodeType!==3||(l=i+r),d.nodeType===3&&(i+=d.nodeValue.length),(v=d.firstChild)!==null;)m=d,d=v;for(;;){if(d===e)break t;if(m===n&&++u===s&&(o=i),m===a&&++c===r&&(l=i),(v=d.nextSibling)!==null)break;d=m,m=d.parentNode}d=v}n=o===-1||l===-1?null:{start:o,end:l}}else n=null}n=n||{start:0,end:0}}else n=null;for(nc={focusedElem:e,selectionRange:n},Fo=!1,D=t;D!==null;)if(t=D,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,D=e;else for(;D!==null;){t=D;try{var y=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(y!==null){var x=y.memoizedProps,S=y.memoizedState,p=t.stateNode,h=p.getSnapshotBeforeUpdate(t.elementType===t.type?x:jt(t.type,x),S);p.__reactInternalSnapshotBeforeUpdate=h}break;case 3:var g=t.stateNode.containerInfo;g.nodeType===1?g.textContent="":g.nodeType===9&&g.documentElement&&g.removeChild(g.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(j(163))}}catch(_){xe(t,t.return,_)}if(e=t.sibling,e!==null){e.return=t.return,D=e;break}D=t.return}return y=Vh,Vh=!1,y}function Da(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var s=r=r.next;do{if((s.tag&e)===e){var a=s.destroy;s.destroy=void 0,a!==void 0&&vc(t,n,a)}s=s.next}while(s!==r)}}function kl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function xc(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function My(e){var t=e.alternate;t!==null&&(e.alternate=null,My(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[qt],delete t[Xa],delete t[ac],delete t[yw],delete t[vw])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function $y(e){return e.tag===5||e.tag===3||e.tag===4}function Qh(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||$y(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function wc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=$o));else if(r!==4&&(e=e.child,e!==null))for(wc(e,t,n),e=e.sibling;e!==null;)wc(e,t,n),e=e.sibling}function bc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(bc(e,t,n),e=e.sibling;e!==null;)bc(e,t,n),e=e.sibling}var Le=null,Tt=!1;function An(e,t,n){for(n=n.child;n!==null;)zy(e,t,n),n=n.sibling}function zy(e,t,n){if(Kt&&typeof Kt.onCommitFiberUnmount=="function")try{Kt.onCommitFiberUnmount(gl,n)}catch{}switch(n.tag){case 5:Be||xs(n,t);case 6:var r=Le,s=Tt;Le=null,An(e,t,n),Le=r,Tt=s,Le!==null&&(Tt?(e=Le,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Le.removeChild(n.stateNode));break;case 18:Le!==null&&(Tt?(e=Le,n=n.stateNode,e.nodeType===8?lu(e.parentNode,n):e.nodeType===1&&lu(e,n),Ga(e)):lu(Le,n.stateNode));break;case 4:r=Le,s=Tt,Le=n.stateNode.containerInfo,Tt=!0,An(e,t,n),Le=r,Tt=s;break;case 0:case 11:case 14:case 15:if(!Be&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){s=r=r.next;do{var a=s,i=a.destroy;a=a.tag,i!==void 0&&(a&2||a&4)&&vc(n,t,i),s=s.next}while(s!==r)}An(e,t,n);break;case 1:if(!Be&&(xs(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(o){xe(n,t,o)}An(e,t,n);break;case 21:An(e,t,n);break;case 22:n.mode&1?(Be=(r=Be)||n.memoizedState!==null,An(e,t,n),Be=r):An(e,t,n);break;default:An(e,t,n)}}function qh(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Iw),t.forEach(function(r){var s=Ww.bind(null,e,r);n.has(r)||(n.add(r),r.then(s,s))})}}function Rt(e,t){var n=t.deletions;if(n!==null)for(var r=0;rs&&(s=i),r&=~a}if(r=s,r=Se()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Dw(r/1960))-r,10e?16:e,Yn===null)var r=!1;else{if(e=Yn,Yn=null,Jo=0,X&6)throw Error(j(331));var s=X;for(X|=4,D=e.current;D!==null;){var a=D,i=a.child;if(D.flags&16){var o=a.deletions;if(o!==null){for(var l=0;lSe()-rf?zr(e,0):nf|=n),st(e,t)}function Gy(e,t){t===0&&(e.mode&1?(t=Ui,Ui<<=1,!(Ui&130023424)&&(Ui=4194304)):t=1);var n=Ze();e=Sn(e,t),e!==null&&(vi(e,t,n),st(e,n))}function Hw(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Gy(e,n)}function Ww(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,s=e.memoizedState;s!==null&&(n=s.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(j(314))}r!==null&&r.delete(t),Gy(e,n)}var Zy;Zy=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||nt.current)tt=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return tt=!1,Ow(e,t,n);tt=!!(e.flags&131072)}else tt=!1,he&&t.flags&1048576&&Xg(t,Ho,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;go(e,t),e=t.pendingProps;var s=Bs(t,Ve.current);Cs(t,n),s=Kd(null,t,r,e,s,n);var a=Yd();return t.flags|=1,typeof s=="object"&&s!==null&&typeof s.render=="function"&&s.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,rt(r)?(a=!0,Uo(t)):a=!1,t.memoizedState=s.state!==null&&s.state!==void 0?s.state:null,Vd(t),s.updater=_l,t.stateNode=s,s._reactInternals=t,dc(t,r,e,n),t=pc(null,t,r,!0,a,n)):(t.tag=0,he&&a&&Md(t),qe(null,t,s,n),t=t.child),t;case 16:r=t.elementType;e:{switch(go(e,t),e=t.pendingProps,s=r._init,r=s(r._payload),t.type=r,s=t.tag=Qw(r),e=jt(r,e),s){case 0:t=hc(null,t,r,e,n);break e;case 1:t=Bh(null,t,r,e,n);break e;case 11:t=zh(null,t,r,e,n);break e;case 14:t=Uh(null,t,r,jt(r.type,e),n);break e}throw Error(j(306,r,""))}return t;case 0:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:jt(r,s),hc(e,t,r,s,n);case 1:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:jt(r,s),Bh(e,t,r,s,n);case 3:e:{if(Ty(t),e===null)throw Error(j(387));r=t.pendingProps,a=t.memoizedState,s=a.element,ay(e,t),Qo(t,r,null,n);var i=t.memoizedState;if(r=i.element,a.isDehydrated)if(a={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=a,t.memoizedState=a,t.flags&256){s=Qs(Error(j(423)),t),t=Hh(e,t,r,n,s);break e}else if(r!==s){s=Qs(Error(j(424)),t),t=Hh(e,t,r,n,s);break e}else for(ut=nr(t.stateNode.containerInfo.firstChild),ct=t,he=!0,It=null,n=ry(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Hs(),r===s){t=_n(e,t,n);break e}qe(e,t,r,n)}t=t.child}return t;case 5:return iy(t),e===null&&lc(t),r=t.type,s=t.pendingProps,a=e!==null?e.memoizedProps:null,i=s.children,rc(r,s)?i=null:a!==null&&rc(r,a)&&(t.flags|=32),Oy(e,t),qe(e,t,i,n),t.child;case 6:return e===null&&lc(t),null;case 13:return Ay(e,t,n);case 4:return Qd(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=Ws(t,null,r,n):qe(e,t,r,n),t.child;case 11:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:jt(r,s),zh(e,t,r,s,n);case 7:return qe(e,t,t.pendingProps,n),t.child;case 8:return qe(e,t,t.pendingProps.children,n),t.child;case 12:return qe(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,s=t.pendingProps,a=t.memoizedProps,i=s.value,le(Wo,r._currentValue),r._currentValue=i,a!==null)if($t(a.value,i)){if(a.children===s.children&&!nt.current){t=_n(e,t,n);break e}}else for(a=t.child,a!==null&&(a.return=t);a!==null;){var o=a.dependencies;if(o!==null){i=a.child;for(var l=o.firstContext;l!==null;){if(l.context===r){if(a.tag===1){l=vn(-1,n&-n),l.tag=2;var u=a.updateQueue;if(u!==null){u=u.shared;var c=u.pending;c===null?l.next=l:(l.next=c.next,c.next=l),u.pending=l}}a.lanes|=n,l=a.alternate,l!==null&&(l.lanes|=n),uc(a.return,n,t),o.lanes|=n;break}l=l.next}}else if(a.tag===10)i=a.type===t.type?null:a.child;else if(a.tag===18){if(i=a.return,i===null)throw Error(j(341));i.lanes|=n,o=i.alternate,o!==null&&(o.lanes|=n),uc(i,n,t),i=a.sibling}else i=a.child;if(i!==null)i.return=a;else for(i=a;i!==null;){if(i===t){i=null;break}if(a=i.sibling,a!==null){a.return=i.return,i=a;break}i=i.return}a=i}qe(e,t,s.children,n),t=t.child}return t;case 9:return s=t.type,r=t.pendingProps.children,Cs(t,n),s=kt(s),r=r(s),t.flags|=1,qe(e,t,r,n),t.child;case 14:return r=t.type,s=jt(r,t.pendingProps),s=jt(r.type,s),Uh(e,t,r,s,n);case 15:return jy(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:jt(r,s),go(e,t),t.tag=1,rt(r)?(e=!0,Uo(t)):e=!1,Cs(t,n),Cy(t,r,s),dc(t,r,s,n),pc(null,t,r,!0,e,n);case 19:return Iy(e,t,n);case 22:return Py(e,t,n)}throw Error(j(156,t.tag))};function Ky(e,t){return _g(e,t)}function Vw(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function St(e,t,n,r){return new Vw(e,t,n,r)}function lf(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Qw(e){if(typeof e=="function")return lf(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Cd)return 11;if(e===Nd)return 14}return 2}function ir(e,t){var n=e.alternate;return n===null?(n=St(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function xo(e,t,n,r,s,a){var i=2;if(r=e,typeof e=="function")lf(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case cs:return Ur(n.children,s,a,t);case Ed:i=8,s|=8;break;case Lu:return e=St(12,n,t,s|2),e.elementType=Lu,e.lanes=a,e;case Fu:return e=St(13,n,t,s),e.elementType=Fu,e.lanes=a,e;case Du:return e=St(19,n,t,s),e.elementType=Du,e.lanes=a,e;case ig:return Cl(n,s,a,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case sg:i=10;break e;case ag:i=9;break e;case Cd:i=11;break e;case Nd:i=14;break e;case Ln:i=16,r=null;break e}throw Error(j(130,e==null?e:typeof e,""))}return t=St(i,n,t,s),t.elementType=e,t.type=r,t.lanes=a,t}function Ur(e,t,n,r){return e=St(7,e,r,t),e.lanes=n,e}function Cl(e,t,n,r){return e=St(22,e,r,t),e.elementType=ig,e.lanes=n,e.stateNode={isHidden:!1},e}function gu(e,t,n){return e=St(6,e,null,t),e.lanes=n,e}function yu(e,t,n){return t=St(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function qw(e,t,n,r,s){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Yl(0),this.expirationTimes=Yl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Yl(0),this.identifierPrefix=r,this.onRecoverableError=s,this.mutableSourceEagerHydrationData=null}function uf(e,t,n,r,s,a,i,o,l){return e=new qw(e,t,n,o,l),t===1?(t=1,a===!0&&(t|=8)):t=0,a=St(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Vd(a),e}function Gw(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(ev)}catch(e){console.error(e)}}ev(),eg.exports=ht;var Xw=eg.exports,tp=Xw;Au.createRoot=tp.createRoot,Au.hydrateRoot=tp.hydrateRoot;var sa=class{constructor(){this.listeners=new Set,this.subscribe=this.subscribe.bind(this)}subscribe(e){return this.listeners.add(e),this.onSubscribe(),()=>{this.listeners.delete(e),this.onUnsubscribe()}}hasListeners(){return this.listeners.size>0}onSubscribe(){}onUnsubscribe(){}},eb={setTimeout:(e,t)=>setTimeout(e,t),clearTimeout:e=>clearTimeout(e),setInterval:(e,t)=>setInterval(e,t),clearInterval:e=>clearInterval(e)},Un,gd,Tm,tb=(Tm=class{constructor(){z(this,Un,eb);z(this,gd,!1)}setTimeoutProvider(e){L(this,Un,e)}setTimeout(e,t){return b(this,Un).setTimeout(e,t)}clearTimeout(e){b(this,Un).clearTimeout(e)}setInterval(e,t){return b(this,Un).setInterval(e,t)}clearInterval(e){b(this,Un).clearInterval(e)}},Un=new WeakMap,gd=new WeakMap,Tm),Rr=new tb;function nb(e){setTimeout(e,0)}var Gr=typeof window>"u"||"Deno"in globalThis;function Ge(){}function rb(e,t){return typeof e=="function"?e(t):e}function Cc(e){return typeof e=="number"&&e>=0&&e!==1/0}function tv(e,t){return Math.max(e+(t||0)-Date.now(),0)}function or(e,t){return typeof e=="function"?e(t):e}function xt(e,t){return typeof e=="function"?e(t):e}function np(e,t){const{type:n="all",exact:r,fetchStatus:s,predicate:a,queryKey:i,stale:o}=e;if(i){if(r){if(t.queryHash!==hf(i,t.options))return!1}else if(!ii(t.queryKey,i))return!1}if(n!=="all"){const l=t.isActive();if(n==="active"&&!l||n==="inactive"&&l)return!1}return!(typeof o=="boolean"&&t.isStale()!==o||s&&s!==t.state.fetchStatus||a&&!a(t))}function rp(e,t){const{exact:n,status:r,predicate:s,mutationKey:a}=e;if(a){if(!t.options.mutationKey)return!1;if(n){if(Zr(t.options.mutationKey)!==Zr(a))return!1}else if(!ii(t.options.mutationKey,a))return!1}return!(r&&t.state.status!==r||s&&!s(t))}function hf(e,t){return((t==null?void 0:t.queryKeyHashFn)||Zr)(e)}function Zr(e){return JSON.stringify(e,(t,n)=>Nc(n)?Object.keys(n).sort().reduce((r,s)=>(r[s]=n[s],r),{}):n)}function ii(e,t){return e===t?!0:typeof e!=typeof t?!1:e&&t&&typeof e=="object"&&typeof t=="object"?Object.keys(t).every(n=>ii(e[n],t[n])):!1}var sb=Object.prototype.hasOwnProperty;function nv(e,t,n=0){if(e===t)return e;if(n>500)return t;const r=sp(e)&&sp(t);if(!r&&!(Nc(e)&&Nc(t)))return t;const a=(r?e:Object.keys(e)).length,i=r?t:Object.keys(t),o=i.length,l=r?new Array(o):{};let u=0;for(let c=0;c{Rr.setTimeout(t,e)})}function Rc(e,t,n){return typeof n.structuralSharing=="function"?n.structuralSharing(e,t):n.structuralSharing!==!1?nv(e,t):t}function ib(e,t,n=0){const r=[...e,t];return n&&r.length>n?r.slice(1):r}function ob(e,t,n=0){const r=[t,...e];return n&&r.length>n?r.slice(0,-1):r}var pf=Symbol();function rv(e,t){return!e.queryFn&&(t!=null&&t.initialPromise)?()=>t.initialPromise:!e.queryFn||e.queryFn===pf?()=>Promise.reject(new Error(`Missing queryFn: '${e.queryHash}'`)):e.queryFn}function mf(e,t){return typeof e=="function"?e(...t):!!e}function lb(e,t,n){let r=!1,s;return Object.defineProperty(e,"signal",{enumerable:!0,get:()=>(s??(s=t()),r||(r=!0,s.aborted?n():s.addEventListener("abort",n,{once:!0})),s)}),e}var Or,Bn,js,Am,ub=(Am=class extends sa{constructor(){super();z(this,Or);z(this,Bn);z(this,js);L(this,js,t=>{if(!Gr&&window.addEventListener){const n=()=>t();return window.addEventListener("visibilitychange",n,!1),()=>{window.removeEventListener("visibilitychange",n)}}})}onSubscribe(){b(this,Bn)||this.setEventListener(b(this,js))}onUnsubscribe(){var t;this.hasListeners()||((t=b(this,Bn))==null||t.call(this),L(this,Bn,void 0))}setEventListener(t){var n;L(this,js,t),(n=b(this,Bn))==null||n.call(this),L(this,Bn,t(r=>{typeof r=="boolean"?this.setFocused(r):this.onFocus()}))}setFocused(t){b(this,Or)!==t&&(L(this,Or,t),this.onFocus())}onFocus(){const t=this.isFocused();this.listeners.forEach(n=>{n(t)})}isFocused(){var t;return typeof b(this,Or)=="boolean"?b(this,Or):((t=globalThis.document)==null?void 0:t.visibilityState)!=="hidden"}},Or=new WeakMap,Bn=new WeakMap,js=new WeakMap,Am),gf=new ub;function jc(){let e,t;const n=new Promise((s,a)=>{e=s,t=a});n.status="pending",n.catch(()=>{});function r(s){Object.assign(n,s),delete n.resolve,delete n.reject}return n.resolve=s=>{r({status:"fulfilled",value:s}),e(s)},n.reject=s=>{r({status:"rejected",reason:s}),t(s)},n}var cb=nb;function db(){let e=[],t=0,n=o=>{o()},r=o=>{o()},s=cb;const a=o=>{t?e.push(o):s(()=>{n(o)})},i=()=>{const o=e;e=[],o.length&&s(()=>{r(()=>{o.forEach(l=>{n(l)})})})};return{batch:o=>{let l;t++;try{l=o()}finally{t--,t||i()}return l},batchCalls:o=>(...l)=>{a(()=>{o(...l)})},schedule:a,setNotifyFunction:o=>{n=o},setBatchNotifyFunction:o=>{r=o},setScheduler:o=>{s=o}}}var Re=db(),Ps,Hn,Os,Im,fb=(Im=class extends sa{constructor(){super();z(this,Ps,!0);z(this,Hn);z(this,Os);L(this,Os,t=>{if(!Gr&&window.addEventListener){const n=()=>t(!0),r=()=>t(!1);return window.addEventListener("online",n,!1),window.addEventListener("offline",r,!1),()=>{window.removeEventListener("online",n),window.removeEventListener("offline",r)}}})}onSubscribe(){b(this,Hn)||this.setEventListener(b(this,Os))}onUnsubscribe(){var t;this.hasListeners()||((t=b(this,Hn))==null||t.call(this),L(this,Hn,void 0))}setEventListener(t){var n;L(this,Os,t),(n=b(this,Hn))==null||n.call(this),L(this,Hn,t(this.setOnline.bind(this)))}setOnline(t){b(this,Ps)!==t&&(L(this,Ps,t),this.listeners.forEach(r=>{r(t)}))}isOnline(){return b(this,Ps)}},Ps=new WeakMap,Hn=new WeakMap,Os=new WeakMap,Im),nl=new fb;function hb(e){return Math.min(1e3*2**e,3e4)}function sv(e){return(e??"online")==="online"?nl.isOnline():!0}var Pc=class extends Error{constructor(e){super("CancelledError"),this.revert=e==null?void 0:e.revert,this.silent=e==null?void 0:e.silent}};function av(e){let t=!1,n=0,r;const s=jc(),a=()=>s.status!=="pending",i=x=>{var S;if(!a()){const p=new Pc(x);m(p),(S=e.onCancel)==null||S.call(e,p)}},o=()=>{t=!0},l=()=>{t=!1},u=()=>gf.isFocused()&&(e.networkMode==="always"||nl.isOnline())&&e.canRun(),c=()=>sv(e.networkMode)&&e.canRun(),d=x=>{a()||(r==null||r(),s.resolve(x))},m=x=>{a()||(r==null||r(),s.reject(x))},v=()=>new Promise(x=>{var S;r=p=>{(a()||u())&&x(p)},(S=e.onPause)==null||S.call(e)}).then(()=>{var x;r=void 0,a()||(x=e.onContinue)==null||x.call(e)}),y=()=>{if(a())return;let x;const S=n===0?e.initialPromise:void 0;try{x=S??e.fn()}catch(p){x=Promise.reject(p)}Promise.resolve(x).then(d).catch(p=>{var C;if(a())return;const h=e.retry??(Gr?0:3),g=e.retryDelay??hb,_=typeof g=="function"?g(n,p):g,E=h===!0||typeof h=="number"&&nu()?void 0:v()).then(()=>{t?m(p):y()})})};return{promise:s,status:()=>s.status,cancel:i,continue:()=>(r==null||r(),s),cancelRetry:o,continueRetry:l,canStart:c,start:()=>(c()?y():v().then(y),s)}}var Tr,Lm,iv=(Lm=class{constructor(){z(this,Tr)}destroy(){this.clearGcTimeout()}scheduleGc(){this.clearGcTimeout(),Cc(this.gcTime)&&L(this,Tr,Rr.setTimeout(()=>{this.optionalRemove()},this.gcTime))}updateGcTime(e){this.gcTime=Math.max(this.gcTime||0,e??(Gr?1/0:5*60*1e3))}clearGcTimeout(){b(this,Tr)&&(Rr.clearTimeout(b(this,Tr)),L(this,Tr,void 0))}},Tr=new WeakMap,Lm),Ar,Ts,vt,Ir,Oe,fi,Lr,Pt,on,Fm,pb=(Fm=class extends iv{constructor(t){super();z(this,Pt);z(this,Ar);z(this,Ts);z(this,vt);z(this,Ir);z(this,Oe);z(this,fi);z(this,Lr);L(this,Lr,!1),L(this,fi,t.defaultOptions),this.setOptions(t.options),this.observers=[],L(this,Ir,t.client),L(this,vt,b(this,Ir).getQueryCache()),this.queryKey=t.queryKey,this.queryHash=t.queryHash,L(this,Ar,op(this.options)),this.state=t.state??b(this,Ar),this.scheduleGc()}get meta(){return this.options.meta}get promise(){var t;return(t=b(this,Oe))==null?void 0:t.promise}setOptions(t){if(this.options={...b(this,fi),...t},this.updateGcTime(this.options.gcTime),this.state&&this.state.data===void 0){const n=op(this.options);n.data!==void 0&&(this.setState(ip(n.data,n.dataUpdatedAt)),L(this,Ar,n))}}optionalRemove(){!this.observers.length&&this.state.fetchStatus==="idle"&&b(this,vt).remove(this)}setData(t,n){const r=Rc(this.state.data,t,this.options);return G(this,Pt,on).call(this,{data:r,type:"success",dataUpdatedAt:n==null?void 0:n.updatedAt,manual:n==null?void 0:n.manual}),r}setState(t,n){G(this,Pt,on).call(this,{type:"setState",state:t,setStateOptions:n})}cancel(t){var r,s;const n=(r=b(this,Oe))==null?void 0:r.promise;return(s=b(this,Oe))==null||s.cancel(t),n?n.then(Ge).catch(Ge):Promise.resolve()}destroy(){super.destroy(),this.cancel({silent:!0})}reset(){this.destroy(),this.setState(b(this,Ar))}isActive(){return this.observers.some(t=>xt(t.options.enabled,this)!==!1)}isDisabled(){return this.getObserversCount()>0?!this.isActive():this.options.queryFn===pf||this.state.dataUpdateCount+this.state.errorUpdateCount===0}isStatic(){return this.getObserversCount()>0?this.observers.some(t=>or(t.options.staleTime,this)==="static"):!1}isStale(){return this.getObserversCount()>0?this.observers.some(t=>t.getCurrentResult().isStale):this.state.data===void 0||this.state.isInvalidated}isStaleByTime(t=0){return this.state.data===void 0?!0:t==="static"?!1:this.state.isInvalidated?!0:!tv(this.state.dataUpdatedAt,t)}onFocus(){var n;const t=this.observers.find(r=>r.shouldFetchOnWindowFocus());t==null||t.refetch({cancelRefetch:!1}),(n=b(this,Oe))==null||n.continue()}onOnline(){var n;const t=this.observers.find(r=>r.shouldFetchOnReconnect());t==null||t.refetch({cancelRefetch:!1}),(n=b(this,Oe))==null||n.continue()}addObserver(t){this.observers.includes(t)||(this.observers.push(t),this.clearGcTimeout(),b(this,vt).notify({type:"observerAdded",query:this,observer:t}))}removeObserver(t){this.observers.includes(t)&&(this.observers=this.observers.filter(n=>n!==t),this.observers.length||(b(this,Oe)&&(b(this,Lr)?b(this,Oe).cancel({revert:!0}):b(this,Oe).cancelRetry()),this.scheduleGc()),b(this,vt).notify({type:"observerRemoved",query:this,observer:t}))}getObserversCount(){return this.observers.length}invalidate(){this.state.isInvalidated||G(this,Pt,on).call(this,{type:"invalidate"})}async fetch(t,n){var l,u,c,d,m,v,y,x,S,p,h,g;if(this.state.fetchStatus!=="idle"&&((l=b(this,Oe))==null?void 0:l.status())!=="rejected"){if(this.state.data!==void 0&&(n!=null&&n.cancelRefetch))this.cancel({silent:!0});else if(b(this,Oe))return b(this,Oe).continueRetry(),b(this,Oe).promise}if(t&&this.setOptions(t),!this.options.queryFn){const _=this.observers.find(E=>E.options.queryFn);_&&this.setOptions(_.options)}const r=new AbortController,s=_=>{Object.defineProperty(_,"signal",{enumerable:!0,get:()=>(L(this,Lr,!0),r.signal)})},a=()=>{const _=rv(this.options,n),C=(()=>{const R={client:b(this,Ir),queryKey:this.queryKey,meta:this.meta};return s(R),R})();return L(this,Lr,!1),this.options.persister?this.options.persister(_,C,this):_(C)},o=(()=>{const _={fetchOptions:n,options:this.options,queryKey:this.queryKey,client:b(this,Ir),state:this.state,fetchFn:a};return s(_),_})();(u=this.options.behavior)==null||u.onFetch(o,this),L(this,Ts,this.state),(this.state.fetchStatus==="idle"||this.state.fetchMeta!==((c=o.fetchOptions)==null?void 0:c.meta))&&G(this,Pt,on).call(this,{type:"fetch",meta:(d=o.fetchOptions)==null?void 0:d.meta}),L(this,Oe,av({initialPromise:n==null?void 0:n.initialPromise,fn:o.fetchFn,onCancel:_=>{_ instanceof Pc&&_.revert&&this.setState({...b(this,Ts),fetchStatus:"idle"}),r.abort()},onFail:(_,E)=>{G(this,Pt,on).call(this,{type:"failed",failureCount:_,error:E})},onPause:()=>{G(this,Pt,on).call(this,{type:"pause"})},onContinue:()=>{G(this,Pt,on).call(this,{type:"continue"})},retry:o.options.retry,retryDelay:o.options.retryDelay,networkMode:o.options.networkMode,canRun:()=>!0}));try{const _=await b(this,Oe).start();if(_===void 0)throw new Error(`${this.queryHash} data is undefined`);return this.setData(_),(v=(m=b(this,vt).config).onSuccess)==null||v.call(m,_,this),(x=(y=b(this,vt).config).onSettled)==null||x.call(y,_,this.state.error,this),_}catch(_){if(_ instanceof Pc){if(_.silent)return b(this,Oe).promise;if(_.revert){if(this.state.data===void 0)throw _;return this.state.data}}throw G(this,Pt,on).call(this,{type:"error",error:_}),(p=(S=b(this,vt).config).onError)==null||p.call(S,_,this),(g=(h=b(this,vt).config).onSettled)==null||g.call(h,this.state.data,_,this),_}finally{this.scheduleGc()}}},Ar=new WeakMap,Ts=new WeakMap,vt=new WeakMap,Ir=new WeakMap,Oe=new WeakMap,fi=new WeakMap,Lr=new WeakMap,Pt=new WeakSet,on=function(t){const n=r=>{switch(t.type){case"failed":return{...r,fetchFailureCount:t.failureCount,fetchFailureReason:t.error};case"pause":return{...r,fetchStatus:"paused"};case"continue":return{...r,fetchStatus:"fetching"};case"fetch":return{...r,...ov(r.data,this.options),fetchMeta:t.meta??null};case"success":const s={...r,...ip(t.data,t.dataUpdatedAt),dataUpdateCount:r.dataUpdateCount+1,...!t.manual&&{fetchStatus:"idle",fetchFailureCount:0,fetchFailureReason:null}};return L(this,Ts,t.manual?s:void 0),s;case"error":const a=t.error;return{...r,error:a,errorUpdateCount:r.errorUpdateCount+1,errorUpdatedAt:Date.now(),fetchFailureCount:r.fetchFailureCount+1,fetchFailureReason:a,fetchStatus:"idle",status:"error",isInvalidated:!0};case"invalidate":return{...r,isInvalidated:!0};case"setState":return{...r,...t.state}}};this.state=n(this.state),Re.batch(()=>{this.observers.forEach(r=>{r.onQueryUpdate()}),b(this,vt).notify({query:this,type:"updated",action:t})})},Fm);function ov(e,t){return{fetchFailureCount:0,fetchFailureReason:null,fetchStatus:sv(t.networkMode)?"fetching":"paused",...e===void 0&&{error:null,status:"pending"}}}function ip(e,t){return{data:e,dataUpdatedAt:t??Date.now(),error:null,isInvalidated:!1,status:"success"}}function op(e){const t=typeof e.initialData=="function"?e.initialData():e.initialData,n=t!==void 0,r=n?typeof e.initialDataUpdatedAt=="function"?e.initialDataUpdatedAt():e.initialDataUpdatedAt:0;return{data:t,dataUpdateCount:0,dataUpdatedAt:n?r??Date.now():0,error:null,errorUpdateCount:0,errorUpdatedAt:0,fetchFailureCount:0,fetchFailureReason:null,fetchMeta:null,isInvalidated:!1,status:n?"success":"pending",fetchStatus:"idle"}}var Je,Y,hi,Qe,Fr,As,fn,Wn,pi,Is,Ls,Dr,Mr,Vn,Fs,ae,Ca,Oc,Tc,Ac,Ic,Lc,Fc,Dc,lv,Dm,mb=(Dm=class extends sa{constructor(t,n){super();z(this,ae);z(this,Je);z(this,Y);z(this,hi);z(this,Qe);z(this,Fr);z(this,As);z(this,fn);z(this,Wn);z(this,pi);z(this,Is);z(this,Ls);z(this,Dr);z(this,Mr);z(this,Vn);z(this,Fs,new Set);this.options=n,L(this,Je,t),L(this,Wn,null),L(this,fn,jc()),this.bindMethods(),this.setOptions(n)}bindMethods(){this.refetch=this.refetch.bind(this)}onSubscribe(){this.listeners.size===1&&(b(this,Y).addObserver(this),lp(b(this,Y),this.options)?G(this,ae,Ca).call(this):this.updateResult(),G(this,ae,Ic).call(this))}onUnsubscribe(){this.hasListeners()||this.destroy()}shouldFetchOnReconnect(){return Mc(b(this,Y),this.options,this.options.refetchOnReconnect)}shouldFetchOnWindowFocus(){return Mc(b(this,Y),this.options,this.options.refetchOnWindowFocus)}destroy(){this.listeners=new Set,G(this,ae,Lc).call(this),G(this,ae,Fc).call(this),b(this,Y).removeObserver(this)}setOptions(t){const n=this.options,r=b(this,Y);if(this.options=b(this,Je).defaultQueryOptions(t),this.options.enabled!==void 0&&typeof this.options.enabled!="boolean"&&typeof this.options.enabled!="function"&&typeof xt(this.options.enabled,b(this,Y))!="boolean")throw new Error("Expected enabled to be a boolean or a callback that returns a boolean");G(this,ae,Dc).call(this),b(this,Y).setOptions(this.options),n._defaulted&&!tl(this.options,n)&&b(this,Je).getQueryCache().notify({type:"observerOptionsUpdated",query:b(this,Y),observer:this});const s=this.hasListeners();s&&up(b(this,Y),r,this.options,n)&&G(this,ae,Ca).call(this),this.updateResult(),s&&(b(this,Y)!==r||xt(this.options.enabled,b(this,Y))!==xt(n.enabled,b(this,Y))||or(this.options.staleTime,b(this,Y))!==or(n.staleTime,b(this,Y)))&&G(this,ae,Oc).call(this);const a=G(this,ae,Tc).call(this);s&&(b(this,Y)!==r||xt(this.options.enabled,b(this,Y))!==xt(n.enabled,b(this,Y))||a!==b(this,Vn))&&G(this,ae,Ac).call(this,a)}getOptimisticResult(t){const n=b(this,Je).getQueryCache().build(b(this,Je),t),r=this.createResult(n,t);return yb(this,r)&&(L(this,Qe,r),L(this,As,this.options),L(this,Fr,b(this,Y).state)),r}getCurrentResult(){return b(this,Qe)}trackResult(t,n){return new Proxy(t,{get:(r,s)=>(this.trackProp(s),n==null||n(s),s==="promise"&&(this.trackProp("data"),!this.options.experimental_prefetchInRender&&b(this,fn).status==="pending"&&b(this,fn).reject(new Error("experimental_prefetchInRender feature flag is not enabled"))),Reflect.get(r,s))})}trackProp(t){b(this,Fs).add(t)}getCurrentQuery(){return b(this,Y)}refetch({...t}={}){return this.fetch({...t})}fetchOptimistic(t){const n=b(this,Je).defaultQueryOptions(t),r=b(this,Je).getQueryCache().build(b(this,Je),n);return r.fetch().then(()=>this.createResult(r,n))}fetch(t){return G(this,ae,Ca).call(this,{...t,cancelRefetch:t.cancelRefetch??!0}).then(()=>(this.updateResult(),b(this,Qe)))}createResult(t,n){var T;const r=b(this,Y),s=this.options,a=b(this,Qe),i=b(this,Fr),o=b(this,As),u=t!==r?t.state:b(this,hi),{state:c}=t;let d={...c},m=!1,v;if(n._optimisticResults){const A=this.hasListeners(),J=!A&&lp(t,n),te=A&&up(t,r,n,s);(J||te)&&(d={...d,...ov(c.data,t.options)}),n._optimisticResults==="isRestoring"&&(d.fetchStatus="idle")}let{error:y,errorUpdatedAt:x,status:S}=d;v=d.data;let p=!1;if(n.placeholderData!==void 0&&v===void 0&&S==="pending"){let A;a!=null&&a.isPlaceholderData&&n.placeholderData===(o==null?void 0:o.placeholderData)?(A=a.data,p=!0):A=typeof n.placeholderData=="function"?n.placeholderData((T=b(this,Ls))==null?void 0:T.state.data,b(this,Ls)):n.placeholderData,A!==void 0&&(S="success",v=Rc(a==null?void 0:a.data,A,n),m=!0)}if(n.select&&v!==void 0&&!p)if(a&&v===(i==null?void 0:i.data)&&n.select===b(this,pi))v=b(this,Is);else try{L(this,pi,n.select),v=n.select(v),v=Rc(a==null?void 0:a.data,v,n),L(this,Is,v),L(this,Wn,null)}catch(A){L(this,Wn,A)}b(this,Wn)&&(y=b(this,Wn),v=b(this,Is),x=Date.now(),S="error");const h=d.fetchStatus==="fetching",g=S==="pending",_=S==="error",E=g&&h,C=v!==void 0,k={status:S,fetchStatus:d.fetchStatus,isPending:g,isSuccess:S==="success",isError:_,isInitialLoading:E,isLoading:E,data:v,dataUpdatedAt:d.dataUpdatedAt,error:y,errorUpdatedAt:x,failureCount:d.fetchFailureCount,failureReason:d.fetchFailureReason,errorUpdateCount:d.errorUpdateCount,isFetched:d.dataUpdateCount>0||d.errorUpdateCount>0,isFetchedAfterMount:d.dataUpdateCount>u.dataUpdateCount||d.errorUpdateCount>u.errorUpdateCount,isFetching:h,isRefetching:h&&!g,isLoadingError:_&&!C,isPaused:d.fetchStatus==="paused",isPlaceholderData:m,isRefetchError:_&&C,isStale:yf(t,n),refetch:this.refetch,promise:b(this,fn),isEnabled:xt(n.enabled,t)!==!1};if(this.options.experimental_prefetchInRender){const A=k.data!==void 0,J=k.status==="error"&&!A,te=be=>{J?be.reject(k.error):A&&be.resolve(k.data)},oe=()=>{const be=L(this,fn,k.promise=jc());te(be)},we=b(this,fn);switch(we.status){case"pending":t.queryHash===r.queryHash&&te(we);break;case"fulfilled":(J||k.data!==we.value)&&oe();break;case"rejected":(!J||k.error!==we.reason)&&oe();break}}return k}updateResult(){const t=b(this,Qe),n=this.createResult(b(this,Y),this.options);if(L(this,Fr,b(this,Y).state),L(this,As,this.options),b(this,Fr).data!==void 0&&L(this,Ls,b(this,Y)),tl(n,t))return;L(this,Qe,n);const r=()=>{if(!t)return!0;const{notifyOnChangeProps:s}=this.options,a=typeof s=="function"?s():s;if(a==="all"||!a&&!b(this,Fs).size)return!0;const i=new Set(a??b(this,Fs));return this.options.throwOnError&&i.add("error"),Object.keys(b(this,Qe)).some(o=>{const l=o;return b(this,Qe)[l]!==t[l]&&i.has(l)})};G(this,ae,lv).call(this,{listeners:r()})}onQueryUpdate(){this.updateResult(),this.hasListeners()&&G(this,ae,Ic).call(this)}},Je=new WeakMap,Y=new WeakMap,hi=new WeakMap,Qe=new WeakMap,Fr=new WeakMap,As=new WeakMap,fn=new WeakMap,Wn=new WeakMap,pi=new WeakMap,Is=new WeakMap,Ls=new WeakMap,Dr=new WeakMap,Mr=new WeakMap,Vn=new WeakMap,Fs=new WeakMap,ae=new WeakSet,Ca=function(t){G(this,ae,Dc).call(this);let n=b(this,Y).fetch(this.options,t);return t!=null&&t.throwOnError||(n=n.catch(Ge)),n},Oc=function(){G(this,ae,Lc).call(this);const t=or(this.options.staleTime,b(this,Y));if(Gr||b(this,Qe).isStale||!Cc(t))return;const r=tv(b(this,Qe).dataUpdatedAt,t)+1;L(this,Dr,Rr.setTimeout(()=>{b(this,Qe).isStale||this.updateResult()},r))},Tc=function(){return(typeof this.options.refetchInterval=="function"?this.options.refetchInterval(b(this,Y)):this.options.refetchInterval)??!1},Ac=function(t){G(this,ae,Fc).call(this),L(this,Vn,t),!(Gr||xt(this.options.enabled,b(this,Y))===!1||!Cc(b(this,Vn))||b(this,Vn)===0)&&L(this,Mr,Rr.setInterval(()=>{(this.options.refetchIntervalInBackground||gf.isFocused())&&G(this,ae,Ca).call(this)},b(this,Vn)))},Ic=function(){G(this,ae,Oc).call(this),G(this,ae,Ac).call(this,G(this,ae,Tc).call(this))},Lc=function(){b(this,Dr)&&(Rr.clearTimeout(b(this,Dr)),L(this,Dr,void 0))},Fc=function(){b(this,Mr)&&(Rr.clearInterval(b(this,Mr)),L(this,Mr,void 0))},Dc=function(){const t=b(this,Je).getQueryCache().build(b(this,Je),this.options);if(t===b(this,Y))return;const n=b(this,Y);L(this,Y,t),L(this,hi,t.state),this.hasListeners()&&(n==null||n.removeObserver(this),t.addObserver(this))},lv=function(t){Re.batch(()=>{t.listeners&&this.listeners.forEach(n=>{n(b(this,Qe))}),b(this,Je).getQueryCache().notify({query:b(this,Y),type:"observerResultsUpdated"})})},Dm);function gb(e,t){return xt(t.enabled,e)!==!1&&e.state.data===void 0&&!(e.state.status==="error"&&t.retryOnMount===!1)}function lp(e,t){return gb(e,t)||e.state.data!==void 0&&Mc(e,t,t.refetchOnMount)}function Mc(e,t,n){if(xt(t.enabled,e)!==!1&&or(t.staleTime,e)!=="static"){const r=typeof n=="function"?n(e):n;return r==="always"||r!==!1&&yf(e,t)}return!1}function up(e,t,n,r){return(e!==t||xt(r.enabled,e)===!1)&&(!n.suspense||e.state.status!=="error")&&yf(e,n)}function yf(e,t){return xt(t.enabled,e)!==!1&&e.isStaleByTime(or(t.staleTime,e))}function yb(e,t){return!tl(e.getCurrentResult(),t)}function cp(e){return{onFetch:(t,n)=>{var c,d,m,v,y;const r=t.options,s=(m=(d=(c=t.fetchOptions)==null?void 0:c.meta)==null?void 0:d.fetchMore)==null?void 0:m.direction,a=((v=t.state.data)==null?void 0:v.pages)||[],i=((y=t.state.data)==null?void 0:y.pageParams)||[];let o={pages:[],pageParams:[]},l=0;const u=async()=>{let x=!1;const S=g=>{lb(g,()=>t.signal,()=>x=!0)},p=rv(t.options,t.fetchOptions),h=async(g,_,E)=>{if(x)return Promise.reject();if(_==null&&g.pages.length)return Promise.resolve(g);const R=(()=>{const J={client:t.client,queryKey:t.queryKey,pageParam:_,direction:E?"backward":"forward",meta:t.options.meta};return S(J),J})(),k=await p(R),{maxPages:T}=t.options,A=E?ob:ib;return{pages:A(g.pages,k,T),pageParams:A(g.pageParams,_,T)}};if(s&&a.length){const g=s==="backward",_=g?vb:dp,E={pages:a,pageParams:i},C=_(r,E);o=await h(E,C,g)}else{const g=e??a.length;do{const _=l===0?i[0]??r.initialPageParam:dp(r,o);if(l>0&&_==null)break;o=await h(o,_),l++}while(l{var x,S;return(S=(x=t.options).persister)==null?void 0:S.call(x,u,{client:t.client,queryKey:t.queryKey,meta:t.options.meta,signal:t.signal},n)}:t.fetchFn=u}}}function dp(e,{pages:t,pageParams:n}){const r=t.length-1;return t.length>0?e.getNextPageParam(t[r],t,n[r],n):void 0}function vb(e,{pages:t,pageParams:n}){var r;return t.length>0?(r=e.getPreviousPageParam)==null?void 0:r.call(e,t[0],t,n[0],n):void 0}var mi,Wt,Ue,$r,Vt,In,Mm,xb=(Mm=class extends iv{constructor(t){super();z(this,Vt);z(this,mi);z(this,Wt);z(this,Ue);z(this,$r);L(this,mi,t.client),this.mutationId=t.mutationId,L(this,Ue,t.mutationCache),L(this,Wt,[]),this.state=t.state||uv(),this.setOptions(t.options),this.scheduleGc()}setOptions(t){this.options=t,this.updateGcTime(this.options.gcTime)}get meta(){return this.options.meta}addObserver(t){b(this,Wt).includes(t)||(b(this,Wt).push(t),this.clearGcTimeout(),b(this,Ue).notify({type:"observerAdded",mutation:this,observer:t}))}removeObserver(t){L(this,Wt,b(this,Wt).filter(n=>n!==t)),this.scheduleGc(),b(this,Ue).notify({type:"observerRemoved",mutation:this,observer:t})}optionalRemove(){b(this,Wt).length||(this.state.status==="pending"?this.scheduleGc():b(this,Ue).remove(this))}continue(){var t;return((t=b(this,$r))==null?void 0:t.continue())??this.execute(this.state.variables)}async execute(t){var i,o,l,u,c,d,m,v,y,x,S,p,h,g,_,E,C,R;const n=()=>{G(this,Vt,In).call(this,{type:"continue"})},r={client:b(this,mi),meta:this.options.meta,mutationKey:this.options.mutationKey};L(this,$r,av({fn:()=>this.options.mutationFn?this.options.mutationFn(t,r):Promise.reject(new Error("No mutationFn found")),onFail:(k,T)=>{G(this,Vt,In).call(this,{type:"failed",failureCount:k,error:T})},onPause:()=>{G(this,Vt,In).call(this,{type:"pause"})},onContinue:n,retry:this.options.retry??0,retryDelay:this.options.retryDelay,networkMode:this.options.networkMode,canRun:()=>b(this,Ue).canRun(this)}));const s=this.state.status==="pending",a=!b(this,$r).canStart();try{if(s)n();else{G(this,Vt,In).call(this,{type:"pending",variables:t,isPaused:a}),b(this,Ue).config.onMutate&&await b(this,Ue).config.onMutate(t,this,r);const T=await((o=(i=this.options).onMutate)==null?void 0:o.call(i,t,r));T!==this.state.context&&G(this,Vt,In).call(this,{type:"pending",context:T,variables:t,isPaused:a})}const k=await b(this,$r).start();return await((u=(l=b(this,Ue).config).onSuccess)==null?void 0:u.call(l,k,t,this.state.context,this,r)),await((d=(c=this.options).onSuccess)==null?void 0:d.call(c,k,t,this.state.context,r)),await((v=(m=b(this,Ue).config).onSettled)==null?void 0:v.call(m,k,null,this.state.variables,this.state.context,this,r)),await((x=(y=this.options).onSettled)==null?void 0:x.call(y,k,null,t,this.state.context,r)),G(this,Vt,In).call(this,{type:"success",data:k}),k}catch(k){try{await((p=(S=b(this,Ue).config).onError)==null?void 0:p.call(S,k,t,this.state.context,this,r))}catch(T){Promise.reject(T)}try{await((g=(h=this.options).onError)==null?void 0:g.call(h,k,t,this.state.context,r))}catch(T){Promise.reject(T)}try{await((E=(_=b(this,Ue).config).onSettled)==null?void 0:E.call(_,void 0,k,this.state.variables,this.state.context,this,r))}catch(T){Promise.reject(T)}try{await((R=(C=this.options).onSettled)==null?void 0:R.call(C,void 0,k,t,this.state.context,r))}catch(T){Promise.reject(T)}throw G(this,Vt,In).call(this,{type:"error",error:k}),k}finally{b(this,Ue).runNext(this)}}},mi=new WeakMap,Wt=new WeakMap,Ue=new WeakMap,$r=new WeakMap,Vt=new WeakSet,In=function(t){const n=r=>{switch(t.type){case"failed":return{...r,failureCount:t.failureCount,failureReason:t.error};case"pause":return{...r,isPaused:!0};case"continue":return{...r,isPaused:!1};case"pending":return{...r,context:t.context,data:void 0,failureCount:0,failureReason:null,error:null,isPaused:t.isPaused,status:"pending",variables:t.variables,submittedAt:Date.now()};case"success":return{...r,data:t.data,failureCount:0,failureReason:null,error:null,status:"success",isPaused:!1};case"error":return{...r,data:void 0,error:t.error,failureCount:r.failureCount+1,failureReason:t.error,isPaused:!1,status:"error"}}};this.state=n(this.state),Re.batch(()=>{b(this,Wt).forEach(r=>{r.onMutationUpdate(t)}),b(this,Ue).notify({mutation:this,type:"updated",action:t})})},Mm);function uv(){return{context:void 0,data:void 0,error:null,failureCount:0,failureReason:null,isPaused:!1,status:"idle",variables:void 0,submittedAt:0}}var hn,Ot,gi,$m,wb=($m=class extends sa{constructor(t={}){super();z(this,hn);z(this,Ot);z(this,gi);this.config=t,L(this,hn,new Set),L(this,Ot,new Map),L(this,gi,0)}build(t,n,r){const s=new xb({client:t,mutationCache:this,mutationId:++Li(this,gi)._,options:t.defaultMutationOptions(n),state:r});return this.add(s),s}add(t){b(this,hn).add(t);const n=Xi(t);if(typeof n=="string"){const r=b(this,Ot).get(n);r?r.push(t):b(this,Ot).set(n,[t])}this.notify({type:"added",mutation:t})}remove(t){if(b(this,hn).delete(t)){const n=Xi(t);if(typeof n=="string"){const r=b(this,Ot).get(n);if(r)if(r.length>1){const s=r.indexOf(t);s!==-1&&r.splice(s,1)}else r[0]===t&&b(this,Ot).delete(n)}}this.notify({type:"removed",mutation:t})}canRun(t){const n=Xi(t);if(typeof n=="string"){const r=b(this,Ot).get(n),s=r==null?void 0:r.find(a=>a.state.status==="pending");return!s||s===t}else return!0}runNext(t){var r;const n=Xi(t);if(typeof n=="string"){const s=(r=b(this,Ot).get(n))==null?void 0:r.find(a=>a!==t&&a.state.isPaused);return(s==null?void 0:s.continue())??Promise.resolve()}else return Promise.resolve()}clear(){Re.batch(()=>{b(this,hn).forEach(t=>{this.notify({type:"removed",mutation:t})}),b(this,hn).clear(),b(this,Ot).clear()})}getAll(){return Array.from(b(this,hn))}find(t){const n={exact:!0,...t};return this.getAll().find(r=>rp(n,r))}findAll(t={}){return this.getAll().filter(n=>rp(t,n))}notify(t){Re.batch(()=>{this.listeners.forEach(n=>{n(t)})})}resumePausedMutations(){const t=this.getAll().filter(n=>n.state.isPaused);return Re.batch(()=>Promise.all(t.map(n=>n.continue().catch(Ge))))}},hn=new WeakMap,Ot=new WeakMap,gi=new WeakMap,$m);function Xi(e){var t;return(t=e.options.scope)==null?void 0:t.id}var pn,Qn,Xe,mn,xn,wo,$c,zm,bb=(zm=class extends sa{constructor(n,r){super();z(this,xn);z(this,pn);z(this,Qn);z(this,Xe);z(this,mn);L(this,pn,n),this.setOptions(r),this.bindMethods(),G(this,xn,wo).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(n){var s;const r=this.options;this.options=b(this,pn).defaultMutationOptions(n),tl(this.options,r)||b(this,pn).getMutationCache().notify({type:"observerOptionsUpdated",mutation:b(this,Xe),observer:this}),r!=null&&r.mutationKey&&this.options.mutationKey&&Zr(r.mutationKey)!==Zr(this.options.mutationKey)?this.reset():((s=b(this,Xe))==null?void 0:s.state.status)==="pending"&&b(this,Xe).setOptions(this.options)}onUnsubscribe(){var n;this.hasListeners()||(n=b(this,Xe))==null||n.removeObserver(this)}onMutationUpdate(n){G(this,xn,wo).call(this),G(this,xn,$c).call(this,n)}getCurrentResult(){return b(this,Qn)}reset(){var n;(n=b(this,Xe))==null||n.removeObserver(this),L(this,Xe,void 0),G(this,xn,wo).call(this),G(this,xn,$c).call(this)}mutate(n,r){var s;return L(this,mn,r),(s=b(this,Xe))==null||s.removeObserver(this),L(this,Xe,b(this,pn).getMutationCache().build(b(this,pn),this.options)),b(this,Xe).addObserver(this),b(this,Xe).execute(n)}},pn=new WeakMap,Qn=new WeakMap,Xe=new WeakMap,mn=new WeakMap,xn=new WeakSet,wo=function(){var r;const n=((r=b(this,Xe))==null?void 0:r.state)??uv();L(this,Qn,{...n,isPending:n.status==="pending",isSuccess:n.status==="success",isError:n.status==="error",isIdle:n.status==="idle",mutate:this.mutate,reset:this.reset})},$c=function(n){Re.batch(()=>{var r,s,a,i,o,l,u,c;if(b(this,mn)&&this.hasListeners()){const d=b(this,Qn).variables,m=b(this,Qn).context,v={client:b(this,pn),meta:this.options.meta,mutationKey:this.options.mutationKey};if((n==null?void 0:n.type)==="success"){try{(s=(r=b(this,mn)).onSuccess)==null||s.call(r,n.data,d,m,v)}catch(y){Promise.reject(y)}try{(i=(a=b(this,mn)).onSettled)==null||i.call(a,n.data,null,d,m,v)}catch(y){Promise.reject(y)}}else if((n==null?void 0:n.type)==="error"){try{(l=(o=b(this,mn)).onError)==null||l.call(o,n.error,d,m,v)}catch(y){Promise.reject(y)}try{(c=(u=b(this,mn)).onSettled)==null||c.call(u,void 0,n.error,d,m,v)}catch(y){Promise.reject(y)}}}this.listeners.forEach(d=>{d(b(this,Qn))})})},zm),Qt,Um,Sb=(Um=class extends sa{constructor(t={}){super();z(this,Qt);this.config=t,L(this,Qt,new Map)}build(t,n,r){const s=n.queryKey,a=n.queryHash??hf(s,n);let i=this.get(a);return i||(i=new pb({client:t,queryKey:s,queryHash:a,options:t.defaultQueryOptions(n),state:r,defaultOptions:t.getQueryDefaults(s)}),this.add(i)),i}add(t){b(this,Qt).has(t.queryHash)||(b(this,Qt).set(t.queryHash,t),this.notify({type:"added",query:t}))}remove(t){const n=b(this,Qt).get(t.queryHash);n&&(t.destroy(),n===t&&b(this,Qt).delete(t.queryHash),this.notify({type:"removed",query:t}))}clear(){Re.batch(()=>{this.getAll().forEach(t=>{this.remove(t)})})}get(t){return b(this,Qt).get(t)}getAll(){return[...b(this,Qt).values()]}find(t){const n={exact:!0,...t};return this.getAll().find(r=>np(n,r))}findAll(t={}){const n=this.getAll();return Object.keys(t).length>0?n.filter(r=>np(t,r)):n}notify(t){Re.batch(()=>{this.listeners.forEach(n=>{n(t)})})}onFocus(){Re.batch(()=>{this.getAll().forEach(t=>{t.onFocus()})})}onOnline(){Re.batch(()=>{this.getAll().forEach(t=>{t.onOnline()})})}},Qt=new WeakMap,Um),ve,qn,Gn,Ds,Ms,Zn,$s,zs,Bm,_b=(Bm=class{constructor(e={}){z(this,ve);z(this,qn);z(this,Gn);z(this,Ds);z(this,Ms);z(this,Zn);z(this,$s);z(this,zs);L(this,ve,e.queryCache||new Sb),L(this,qn,e.mutationCache||new wb),L(this,Gn,e.defaultOptions||{}),L(this,Ds,new Map),L(this,Ms,new Map),L(this,Zn,0)}mount(){Li(this,Zn)._++,b(this,Zn)===1&&(L(this,$s,gf.subscribe(async e=>{e&&(await this.resumePausedMutations(),b(this,ve).onFocus())})),L(this,zs,nl.subscribe(async e=>{e&&(await this.resumePausedMutations(),b(this,ve).onOnline())})))}unmount(){var e,t;Li(this,Zn)._--,b(this,Zn)===0&&((e=b(this,$s))==null||e.call(this),L(this,$s,void 0),(t=b(this,zs))==null||t.call(this),L(this,zs,void 0))}isFetching(e){return b(this,ve).findAll({...e,fetchStatus:"fetching"}).length}isMutating(e){return b(this,qn).findAll({...e,status:"pending"}).length}getQueryData(e){var n;const t=this.defaultQueryOptions({queryKey:e});return(n=b(this,ve).get(t.queryHash))==null?void 0:n.state.data}ensureQueryData(e){const t=this.defaultQueryOptions(e),n=b(this,ve).build(this,t),r=n.state.data;return r===void 0?this.fetchQuery(e):(e.revalidateIfStale&&n.isStaleByTime(or(t.staleTime,n))&&this.prefetchQuery(t),Promise.resolve(r))}getQueriesData(e){return b(this,ve).findAll(e).map(({queryKey:t,state:n})=>{const r=n.data;return[t,r]})}setQueryData(e,t,n){const r=this.defaultQueryOptions({queryKey:e}),s=b(this,ve).get(r.queryHash),a=s==null?void 0:s.state.data,i=rb(t,a);if(i!==void 0)return b(this,ve).build(this,r).setData(i,{...n,manual:!0})}setQueriesData(e,t,n){return Re.batch(()=>b(this,ve).findAll(e).map(({queryKey:r})=>[r,this.setQueryData(r,t,n)]))}getQueryState(e){var n;const t=this.defaultQueryOptions({queryKey:e});return(n=b(this,ve).get(t.queryHash))==null?void 0:n.state}removeQueries(e){const t=b(this,ve);Re.batch(()=>{t.findAll(e).forEach(n=>{t.remove(n)})})}resetQueries(e,t){const n=b(this,ve);return Re.batch(()=>(n.findAll(e).forEach(r=>{r.reset()}),this.refetchQueries({type:"active",...e},t)))}cancelQueries(e,t={}){const n={revert:!0,...t},r=Re.batch(()=>b(this,ve).findAll(e).map(s=>s.cancel(n)));return Promise.all(r).then(Ge).catch(Ge)}invalidateQueries(e,t={}){return Re.batch(()=>(b(this,ve).findAll(e).forEach(n=>{n.invalidate()}),(e==null?void 0:e.refetchType)==="none"?Promise.resolve():this.refetchQueries({...e,type:(e==null?void 0:e.refetchType)??(e==null?void 0:e.type)??"active"},t)))}refetchQueries(e,t={}){const n={...t,cancelRefetch:t.cancelRefetch??!0},r=Re.batch(()=>b(this,ve).findAll(e).filter(s=>!s.isDisabled()&&!s.isStatic()).map(s=>{let a=s.fetch(void 0,n);return n.throwOnError||(a=a.catch(Ge)),s.state.fetchStatus==="paused"?Promise.resolve():a}));return Promise.all(r).then(Ge)}fetchQuery(e){const t=this.defaultQueryOptions(e);t.retry===void 0&&(t.retry=!1);const n=b(this,ve).build(this,t);return n.isStaleByTime(or(t.staleTime,n))?n.fetch(t):Promise.resolve(n.state.data)}prefetchQuery(e){return this.fetchQuery(e).then(Ge).catch(Ge)}fetchInfiniteQuery(e){return e.behavior=cp(e.pages),this.fetchQuery(e)}prefetchInfiniteQuery(e){return this.fetchInfiniteQuery(e).then(Ge).catch(Ge)}ensureInfiniteQueryData(e){return e.behavior=cp(e.pages),this.ensureQueryData(e)}resumePausedMutations(){return nl.isOnline()?b(this,qn).resumePausedMutations():Promise.resolve()}getQueryCache(){return b(this,ve)}getMutationCache(){return b(this,qn)}getDefaultOptions(){return b(this,Gn)}setDefaultOptions(e){L(this,Gn,e)}setQueryDefaults(e,t){b(this,Ds).set(Zr(e),{queryKey:e,defaultOptions:t})}getQueryDefaults(e){const t=[...b(this,Ds).values()],n={};return t.forEach(r=>{ii(e,r.queryKey)&&Object.assign(n,r.defaultOptions)}),n}setMutationDefaults(e,t){b(this,Ms).set(Zr(e),{mutationKey:e,defaultOptions:t})}getMutationDefaults(e){const t=[...b(this,Ms).values()],n={};return t.forEach(r=>{ii(e,r.mutationKey)&&Object.assign(n,r.defaultOptions)}),n}defaultQueryOptions(e){if(e._defaulted)return e;const t={...b(this,Gn).queries,...this.getQueryDefaults(e.queryKey),...e,_defaulted:!0};return t.queryHash||(t.queryHash=hf(t.queryKey,t)),t.refetchOnReconnect===void 0&&(t.refetchOnReconnect=t.networkMode!=="always"),t.throwOnError===void 0&&(t.throwOnError=!!t.suspense),!t.networkMode&&t.persister&&(t.networkMode="offlineFirst"),t.queryFn===pf&&(t.enabled=!1),t}defaultMutationOptions(e){return e!=null&&e._defaulted?e:{...b(this,Gn).mutations,...(e==null?void 0:e.mutationKey)&&this.getMutationDefaults(e.mutationKey),...e,_defaulted:!0}}clear(){b(this,ve).clear(),b(this,qn).clear()}},ve=new WeakMap,qn=new WeakMap,Gn=new WeakMap,Ds=new WeakMap,Ms=new WeakMap,Zn=new WeakMap,$s=new WeakMap,zs=new WeakMap,Bm),cv=w.createContext(void 0),vf=e=>{const t=w.useContext(cv);if(!t)throw new Error("No QueryClient set, use QueryClientProvider to set one");return t},kb=({client:e,children:t})=>(w.useEffect(()=>(e.mount(),()=>{e.unmount()}),[e]),f.jsx(cv.Provider,{value:e,children:t})),dv=w.createContext(!1),Eb=()=>w.useContext(dv);dv.Provider;function Cb(){let e=!1;return{clearReset:()=>{e=!1},reset:()=>{e=!0},isReset:()=>e}}var Nb=w.createContext(Cb()),Rb=()=>w.useContext(Nb),jb=(e,t,n)=>{const r=n!=null&&n.state.error&&typeof e.throwOnError=="function"?mf(e.throwOnError,[n.state.error,n]):e.throwOnError;(e.suspense||e.experimental_prefetchInRender||r)&&(t.isReset()||(e.retryOnMount=!1))},Pb=e=>{w.useEffect(()=>{e.clearReset()},[e])},Ob=({result:e,errorResetBoundary:t,throwOnError:n,query:r,suspense:s})=>e.isError&&!t.isReset()&&!e.isFetching&&r&&(s&&e.data===void 0||mf(n,[e.error,r])),Tb=e=>{if(e.suspense){const n=s=>s==="static"?s:Math.max(s??1e3,1e3),r=e.staleTime;e.staleTime=typeof r=="function"?(...s)=>n(r(...s)):n(r),typeof e.gcTime=="number"&&(e.gcTime=Math.max(e.gcTime,1e3))}},Ab=(e,t)=>e.isLoading&&e.isFetching&&!t,Ib=(e,t)=>(e==null?void 0:e.suspense)&&t.isPending,fp=(e,t,n)=>t.fetchOptimistic(e).catch(()=>{n.clearReset()});function Lb(e,t,n){var m,v,y,x;const r=Eb(),s=Rb(),a=vf(),i=a.defaultQueryOptions(e);(v=(m=a.getDefaultOptions().queries)==null?void 0:m._experimental_beforeQuery)==null||v.call(m,i);const o=a.getQueryCache().get(i.queryHash);i._optimisticResults=r?"isRestoring":"optimistic",Tb(i),jb(i,s,o),Pb(s);const l=!a.getQueryCache().get(i.queryHash),[u]=w.useState(()=>new t(a,i)),c=u.getOptimisticResult(i),d=!r&&e.subscribed!==!1;if(w.useSyncExternalStore(w.useCallback(S=>{const p=d?u.subscribe(Re.batchCalls(S)):Ge;return u.updateResult(),p},[u,d]),()=>u.getCurrentResult(),()=>u.getCurrentResult()),w.useEffect(()=>{u.setOptions(i)},[i,u]),Ib(i,c))throw fp(i,u,s);if(Ob({result:c,errorResetBoundary:s,throwOnError:i.throwOnError,query:o,suspense:i.suspense}))throw c.error;if((x=(y=a.getDefaultOptions().queries)==null?void 0:y._experimental_afterQuery)==null||x.call(y,i,c),i.experimental_prefetchInRender&&!Gr&&Ab(c,r)){const S=l?fp(i,u,s):o==null?void 0:o.promise;S==null||S.catch(Ge).finally(()=>{u.updateResult()})}return i.notifyOnChangeProps?c:u.trackResult(c)}function rl(e,t){return Lb(e,mb)}function xf(e,t){const n=vf(),[r]=w.useState(()=>new bb(n,e));w.useEffect(()=>{r.setOptions(e)},[r,e]);const s=w.useSyncExternalStore(w.useCallback(i=>r.subscribe(Re.batchCalls(i)),[r]),()=>r.getCurrentResult(),()=>r.getCurrentResult()),a=w.useCallback((i,o)=>{r.mutate(i,o).catch(Ge)},[r]);if(s.error&&mf(r.options.throwOnError,[s.error]))throw s.error;return{...s,mutate:a,mutateAsync:s.mutate}}/** + * react-router v7.13.1 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */var hp="popstate";function pp(e){return typeof e=="object"&&e!=null&&"pathname"in e&&"search"in e&&"hash"in e&&"state"in e&&"key"in e}function Fb(e={}){function t(r,s){var u;let a=(u=s.state)==null?void 0:u.masked,{pathname:i,search:o,hash:l}=a||r.location;return zc("",{pathname:i,search:o,hash:l},s.state&&s.state.usr||null,s.state&&s.state.key||"default",a?{pathname:r.location.pathname,search:r.location.search,hash:r.location.hash}:void 0)}function n(r,s){return typeof s=="string"?s:oi(s)}return Mb(t,n,null,e)}function ge(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function tn(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function Db(){return Math.random().toString(36).substring(2,10)}function mp(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.unstable_mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function zc(e,t,n=null,r,s){return{pathname:typeof e=="string"?e:e.pathname,search:"",hash:"",...typeof t=="string"?aa(t):t,state:n,key:t&&t.key||r||Db(),unstable_mask:s}}function oi({pathname:e="/",search:t="",hash:n=""}){return t&&t!=="?"&&(e+=t.charAt(0)==="?"?t:"?"+t),n&&n!=="#"&&(e+=n.charAt(0)==="#"?n:"#"+n),e}function aa(e){let t={};if(e){let n=e.indexOf("#");n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf("?");r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function Mb(e,t,n,r={}){let{window:s=document.defaultView,v5Compat:a=!1}=r,i=s.history,o="POP",l=null,u=c();u==null&&(u=0,i.replaceState({...i.state,idx:u},""));function c(){return(i.state||{idx:null}).idx}function d(){o="POP";let S=c(),p=S==null?null:S-u;u=S,l&&l({action:o,location:x.location,delta:p})}function m(S,p){o="PUSH";let h=pp(S)?S:zc(x.location,S,p);u=c()+1;let g=mp(h,u),_=x.createHref(h.unstable_mask||h);try{i.pushState(g,"",_)}catch(E){if(E instanceof DOMException&&E.name==="DataCloneError")throw E;s.location.assign(_)}a&&l&&l({action:o,location:x.location,delta:1})}function v(S,p){o="REPLACE";let h=pp(S)?S:zc(x.location,S,p);u=c();let g=mp(h,u),_=x.createHref(h.unstable_mask||h);i.replaceState(g,"",_),a&&l&&l({action:o,location:x.location,delta:0})}function y(S){return $b(S)}let x={get action(){return o},get location(){return e(s,i)},listen(S){if(l)throw new Error("A history only accepts one active listener");return s.addEventListener(hp,d),l=S,()=>{s.removeEventListener(hp,d),l=null}},createHref(S){return t(s,S)},createURL:y,encodeLocation(S){let p=y(S);return{pathname:p.pathname,search:p.search,hash:p.hash}},push:m,replace:v,go(S){return i.go(S)}};return x}function $b(e,t=!1){let n="http://localhost";typeof window<"u"&&(n=window.location.origin!=="null"?window.location.origin:window.location.href),ge(n,"No window.location.(origin|href) available to create URL");let r=typeof e=="string"?e:oi(e);return r=r.replace(/ $/,"%20"),!t&&r.startsWith("//")&&(r=n+r),new URL(r,n)}function fv(e,t,n="/"){return zb(e,t,n,!1)}function zb(e,t,n,r){let s=typeof t=="string"?aa(t):t,a=kn(s.pathname||"/",n);if(a==null)return null;let i=hv(e);Ub(i);let o=null;for(let l=0;o==null&&l{let c={relativePath:u===void 0?i.path||"":u,caseSensitive:i.caseSensitive===!0,childrenIndex:o,route:i};if(c.relativePath.startsWith("/")){if(!c.relativePath.startsWith(r)&&l)return;ge(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let d=Jt([r,c.relativePath]),m=n.concat(c);i.children&&i.children.length>0&&(ge(i.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${d}".`),hv(i.children,t,m,d,l)),!(i.path==null&&!i.index)&&t.push({path:d,score:Gb(d,i.index),routesMeta:m})};return e.forEach((i,o)=>{var l;if(i.path===""||!((l=i.path)!=null&&l.includes("?")))a(i,o);else for(let u of pv(i.path))a(i,o,!0,u)}),t}function pv(e){let t=e.split("/");if(t.length===0)return[];let[n,...r]=t,s=n.endsWith("?"),a=n.replace(/\?$/,"");if(r.length===0)return s?[a,""]:[a];let i=pv(r.join("/")),o=[];return o.push(...i.map(l=>l===""?a:[a,l].join("/"))),s&&o.push(...i),o.map(l=>e.startsWith("/")&&l===""?"/":l)}function Ub(e){e.sort((t,n)=>t.score!==n.score?n.score-t.score:Zb(t.routesMeta.map(r=>r.childrenIndex),n.routesMeta.map(r=>r.childrenIndex)))}var Bb=/^:[\w-]+$/,Hb=3,Wb=2,Vb=1,Qb=10,qb=-2,gp=e=>e==="*";function Gb(e,t){let n=e.split("/"),r=n.length;return n.some(gp)&&(r+=qb),t&&(r+=Wb),n.filter(s=>!gp(s)).reduce((s,a)=>s+(Bb.test(a)?Hb:a===""?Vb:Qb),r)}function Zb(e,t){return e.length===t.length&&e.slice(0,-1).every((r,s)=>r===t[s])?e[e.length-1]-t[t.length-1]:0}function Kb(e,t,n=!1){let{routesMeta:r}=e,s={},a="/",i=[];for(let o=0;o{if(c==="*"){let y=o[m]||"";i=a.slice(0,a.length-y.length).replace(/(.)\/+$/,"$1")}const v=o[m];return d&&!v?u[c]=void 0:u[c]=(v||"").replace(/%2F/g,"/"),u},{}),pathname:a,pathnameBase:i,pattern:e}}function Yb(e,t=!1,n=!0){tn(e==="*"||!e.endsWith("*")||e.endsWith("/*"),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,"/*")}".`);let r=[],s="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(i,o,l,u,c)=>{if(r.push({paramName:o,isOptional:l!=null}),l){let d=c.charAt(u+i.length);return d&&d!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return e.endsWith("*")?(r.push({paramName:"*"}),s+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):n?s+="\\/*$":e!==""&&e!=="/"&&(s+="(?:(?=\\/|$))"),[new RegExp(s,t?void 0:"i"),r]}function Jb(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return tn(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function kn(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith("/")?t.length-1:t.length,r=e.charAt(n);return r&&r!=="/"?null:e.slice(n)||"/"}var Xb=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function e2(e,t="/"){let{pathname:n,search:r="",hash:s=""}=typeof e=="string"?aa(e):e,a;return n?(n=n.replace(/\/\/+/g,"/"),n.startsWith("/")?a=yp(n.substring(1),"/"):a=yp(n,t)):a=t,{pathname:a,search:r2(r),hash:s2(s)}}function yp(e,t){let n=t.replace(/\/+$/,"").split("/");return e.split("/").forEach(s=>{s===".."?n.length>1&&n.pop():s!=="."&&n.push(s)}),n.length>1?n.join("/"):"/"}function vu(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function t2(e){return e.filter((t,n)=>n===0||t.route.path&&t.route.path.length>0)}function mv(e){let t=t2(e);return t.map((n,r)=>r===t.length-1?n.pathname:n.pathnameBase)}function wf(e,t,n,r=!1){let s;typeof e=="string"?s=aa(e):(s={...e},ge(!s.pathname||!s.pathname.includes("?"),vu("?","pathname","search",s)),ge(!s.pathname||!s.pathname.includes("#"),vu("#","pathname","hash",s)),ge(!s.search||!s.search.includes("#"),vu("#","search","hash",s)));let a=e===""||s.pathname==="",i=a?"/":s.pathname,o;if(i==null)o=n;else{let d=t.length-1;if(!r&&i.startsWith("..")){let m=i.split("/");for(;m[0]==="..";)m.shift(),d-=1;s.pathname=m.join("/")}o=d>=0?t[d]:"/"}let l=e2(s,o),u=i&&i!=="/"&&i.endsWith("/"),c=(a||i===".")&&n.endsWith("/");return!l.pathname.endsWith("/")&&(u||c)&&(l.pathname+="/"),l}var Jt=e=>e.join("/").replace(/\/\/+/g,"/"),n2=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),r2=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,s2=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e,a2=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||"",this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function i2(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}function o2(e){return e.map(t=>t.route.path).filter(Boolean).join("/").replace(/\/\/*/g,"/")||"/"}var gv=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function yv(e,t){let n=e;if(typeof n!="string"||!Xb.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,s=!1;if(gv)try{let a=new URL(window.location.href),i=n.startsWith("//")?new URL(a.protocol+n):new URL(n),o=kn(i.pathname,t);i.origin===a.origin&&o!=null?n=o+i.search+i.hash:s=!0}catch{tn(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:s,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var vv=["POST","PUT","PATCH","DELETE"];new Set(vv);var l2=["GET",...vv];new Set(l2);var ia=w.createContext(null);ia.displayName="DataRouter";var Ol=w.createContext(null);Ol.displayName="DataRouterState";var u2=w.createContext(!1),xv=w.createContext({isTransitioning:!1});xv.displayName="ViewTransition";var c2=w.createContext(new Map);c2.displayName="Fetchers";var d2=w.createContext(null);d2.displayName="Await";var Ct=w.createContext(null);Ct.displayName="Navigation";var Si=w.createContext(null);Si.displayName="Location";var nn=w.createContext({outlet:null,matches:[],isDataRoute:!1});nn.displayName="Route";var bf=w.createContext(null);bf.displayName="RouteError";var wv="REACT_ROUTER_ERROR",f2="REDIRECT",h2="ROUTE_ERROR_RESPONSE";function p2(e){if(e.startsWith(`${wv}:${f2}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t=="object"&&t&&typeof t.status=="number"&&typeof t.statusText=="string"&&typeof t.location=="string"&&typeof t.reloadDocument=="boolean"&&typeof t.replace=="boolean")return t}catch{}}function m2(e){if(e.startsWith(`${wv}:${h2}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t=="object"&&t&&typeof t.status=="number"&&typeof t.statusText=="string")return new a2(t.status,t.statusText,t.data)}catch{}}function g2(e,{relative:t}={}){ge(_i(),"useHref() may be used only in the context of a component.");let{basename:n,navigator:r}=w.useContext(Ct),{hash:s,pathname:a,search:i}=ki(e,{relative:t}),o=a;return n!=="/"&&(o=a==="/"?n:Jt([n,a])),r.createHref({pathname:o,search:i,hash:s})}function _i(){return w.useContext(Si)!=null}function Pn(){return ge(_i(),"useLocation() may be used only in the context of a component."),w.useContext(Si).location}var bv="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function Sv(e){w.useContext(Ct).static||w.useLayoutEffect(e)}function _v(){let{isDataRoute:e}=w.useContext(nn);return e?O2():y2()}function y2(){ge(_i(),"useNavigate() may be used only in the context of a component.");let e=w.useContext(ia),{basename:t,navigator:n}=w.useContext(Ct),{matches:r}=w.useContext(nn),{pathname:s}=Pn(),a=JSON.stringify(mv(r)),i=w.useRef(!1);return Sv(()=>{i.current=!0}),w.useCallback((l,u={})=>{if(tn(i.current,bv),!i.current)return;if(typeof l=="number"){n.go(l);return}let c=wf(l,JSON.parse(a),s,u.relative==="path");e==null&&t!=="/"&&(c.pathname=c.pathname==="/"?t:Jt([t,c.pathname])),(u.replace?n.replace:n.push)(c,u.state,u)},[t,n,a,s,e])}var v2=w.createContext(null);function x2(e){let t=w.useContext(nn).outlet;return w.useMemo(()=>t&&w.createElement(v2.Provider,{value:e},t),[t,e])}function ki(e,{relative:t}={}){let{matches:n}=w.useContext(nn),{pathname:r}=Pn(),s=JSON.stringify(mv(n));return w.useMemo(()=>wf(e,JSON.parse(s),r,t==="path"),[e,s,r,t])}function w2(e,t){return kv(e,t)}function kv(e,t,n){var S;ge(_i(),"useRoutes() may be used only in the context of a component.");let{navigator:r}=w.useContext(Ct),{matches:s}=w.useContext(nn),a=s[s.length-1],i=a?a.params:{},o=a?a.pathname:"/",l=a?a.pathnameBase:"/",u=a&&a.route;{let p=u&&u.path||"";Cv(o,!u||p.endsWith("*")||p.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${o}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let c=Pn(),d;if(t){let p=typeof t=="string"?aa(t):t;ge(l==="/"||((S=p.pathname)==null?void 0:S.startsWith(l)),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${l}" but pathname "${p.pathname}" was given in the \`location\` prop.`),d=p}else d=c;let m=d.pathname||"/",v=m;if(l!=="/"){let p=l.replace(/^\//,"").split("/");v="/"+m.replace(/^\//,"").split("/").slice(p.length).join("/")}let y=fv(e,{pathname:v});tn(u||y!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),tn(y==null||y[y.length-1].route.element!==void 0||y[y.length-1].route.Component!==void 0||y[y.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let x=E2(y&&y.map(p=>Object.assign({},p,{params:Object.assign({},i,p.params),pathname:Jt([l,r.encodeLocation?r.encodeLocation(p.pathname.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:p.pathname]),pathnameBase:p.pathnameBase==="/"?l:Jt([l,r.encodeLocation?r.encodeLocation(p.pathnameBase.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:p.pathnameBase])})),s,n);return t&&x?w.createElement(Si.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",unstable_mask:void 0,...d},navigationType:"POP"}},x):x}function b2(){let e=P2(),t=i2(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r="rgba(200,200,200, 0.5)",s={padding:"0.5rem",backgroundColor:r},a={padding:"2px 4px",backgroundColor:r},i=null;return console.error("Error handled by React Router default ErrorBoundary:",e),i=w.createElement(w.Fragment,null,w.createElement("p",null,"💿 Hey developer 👋"),w.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",w.createElement("code",{style:a},"ErrorBoundary")," or"," ",w.createElement("code",{style:a},"errorElement")," prop on your route.")),w.createElement(w.Fragment,null,w.createElement("h2",null,"Unexpected Application Error!"),w.createElement("h3",{style:{fontStyle:"italic"}},t),n?w.createElement("pre",{style:s},n):null,i)}var S2=w.createElement(b2,null),Ev=class extends w.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!=="idle"&&e.revalidation==="idle"?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error!==void 0?e.error:t.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error("React Router caught the following error during render",e)}render(){let e=this.state.error;if(this.context&&typeof e=="object"&&e&&"digest"in e&&typeof e.digest=="string"){const n=m2(e.digest);n&&(e=n)}let t=e!==void 0?w.createElement(nn.Provider,{value:this.props.routeContext},w.createElement(bf.Provider,{value:e,children:this.props.component})):this.props.children;return this.context?w.createElement(_2,{error:e},t):t}};Ev.contextType=u2;var xu=new WeakMap;function _2({children:e,error:t}){let{basename:n}=w.useContext(Ct);if(typeof t=="object"&&t&&"digest"in t&&typeof t.digest=="string"){let r=p2(t.digest);if(r){let s=xu.get(t);if(s)throw s;let a=yv(r.location,n);if(gv&&!xu.get(t))if(a.isExternal||r.reloadDocument)window.location.href=a.absoluteURL||a.to;else{const i=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(a.to,{replace:r.replace}));throw xu.set(t,i),i}return w.createElement("meta",{httpEquiv:"refresh",content:`0;url=${a.absoluteURL||a.to}`})}}return e}function k2({routeContext:e,match:t,children:n}){let r=w.useContext(ia);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),w.createElement(nn.Provider,{value:e},n)}function E2(e,t=[],n){let r=n==null?void 0:n.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let s=e,a=r==null?void 0:r.errors;if(a!=null){let c=s.findIndex(d=>d.route.id&&(a==null?void 0:a[d.route.id])!==void 0);ge(c>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(",")}`),s=s.slice(0,Math.min(s.length,c+1))}let i=!1,o=-1;if(n&&r){i=r.renderFallback;for(let c=0;c=0?s=s.slice(0,o+1):s=[s[0]];break}}}}let l=n==null?void 0:n.onError,u=r&&l?(c,d)=>{var m,v;l(c,{location:r.location,params:((v=(m=r.matches)==null?void 0:m[0])==null?void 0:v.params)??{},unstable_pattern:o2(r.matches),errorInfo:d})}:void 0;return s.reduceRight((c,d,m)=>{let v,y=!1,x=null,S=null;r&&(v=a&&d.route.id?a[d.route.id]:void 0,x=d.route.errorElement||S2,i&&(o<0&&m===0?(Cv("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),y=!0,S=null):o===m&&(y=!0,S=d.route.hydrateFallbackElement||null)));let p=t.concat(s.slice(0,m+1)),h=()=>{let g;return v?g=x:y?g=S:d.route.Component?g=w.createElement(d.route.Component,null):d.route.element?g=d.route.element:g=c,w.createElement(k2,{match:d,routeContext:{outlet:c,matches:p,isDataRoute:r!=null},children:g})};return r&&(d.route.ErrorBoundary||d.route.errorElement||m===0)?w.createElement(Ev,{location:r.location,revalidation:r.revalidation,component:x,error:v,children:h(),routeContext:{outlet:null,matches:p,isDataRoute:!0},onError:u}):h()},null)}function Sf(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function C2(e){let t=w.useContext(ia);return ge(t,Sf(e)),t}function N2(e){let t=w.useContext(Ol);return ge(t,Sf(e)),t}function R2(e){let t=w.useContext(nn);return ge(t,Sf(e)),t}function _f(e){let t=R2(e),n=t.matches[t.matches.length-1];return ge(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function j2(){return _f("useRouteId")}function P2(){var r;let e=w.useContext(bf),t=N2("useRouteError"),n=_f("useRouteError");return e!==void 0?e:(r=t.errors)==null?void 0:r[n]}function O2(){let{router:e}=C2("useNavigate"),t=_f("useNavigate"),n=w.useRef(!1);return Sv(()=>{n.current=!0}),w.useCallback(async(s,a={})=>{tn(n.current,bv),n.current&&(typeof s=="number"?await e.navigate(s):await e.navigate(s,{fromRouteId:t,...a}))},[e,t])}var vp={};function Cv(e,t,n){!t&&!vp[e]&&(vp[e]=!0,tn(!1,n))}w.memo(T2);function T2({routes:e,future:t,state:n,isStatic:r,onError:s}){return kv(e,void 0,{state:n,isStatic:r,onError:s})}function A2(e){return x2(e.context)}function Na(e){ge(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function I2({basename:e="/",children:t=null,location:n,navigationType:r="POP",navigator:s,static:a=!1,unstable_useTransitions:i}){ge(!_i(),"You cannot render a inside another . You should never have more than one in your app.");let o=e.replace(/^\/*/,"/"),l=w.useMemo(()=>({basename:o,navigator:s,static:a,unstable_useTransitions:i,future:{}}),[o,s,a,i]);typeof n=="string"&&(n=aa(n));let{pathname:u="/",search:c="",hash:d="",state:m=null,key:v="default",unstable_mask:y}=n,x=w.useMemo(()=>{let S=kn(u,o);return S==null?null:{location:{pathname:S,search:c,hash:d,state:m,key:v,unstable_mask:y},navigationType:r}},[o,u,c,d,m,v,r,y]);return tn(x!=null,` is not able to match the URL "${u}${c}${d}" because it does not start with the basename, so the won't render anything.`),x==null?null:w.createElement(Ct.Provider,{value:l},w.createElement(Si.Provider,{children:t,value:x}))}function L2({children:e,location:t}){return w2(Uc(e),t)}function Uc(e,t=[]){let n=[];return w.Children.forEach(e,(r,s)=>{if(!w.isValidElement(r))return;let a=[...t,s];if(r.type===w.Fragment){n.push.apply(n,Uc(r.props.children,a));return}ge(r.type===Na,`[${typeof r.type=="string"?r.type:r.type.name}] is not a component. All component children of must be a or `),ge(!r.props.index||!r.props.children,"An index route cannot have child routes.");let i={id:r.props.id||a.join("-"),caseSensitive:r.props.caseSensitive,element:r.props.element,Component:r.props.Component,index:r.props.index,path:r.props.path,middleware:r.props.middleware,loader:r.props.loader,action:r.props.action,hydrateFallbackElement:r.props.hydrateFallbackElement,HydrateFallback:r.props.HydrateFallback,errorElement:r.props.errorElement,ErrorBoundary:r.props.ErrorBoundary,hasErrorBoundary:r.props.hasErrorBoundary===!0||r.props.ErrorBoundary!=null||r.props.errorElement!=null,shouldRevalidate:r.props.shouldRevalidate,handle:r.props.handle,lazy:r.props.lazy};r.props.children&&(i.children=Uc(r.props.children,a)),n.push(i)}),n}var bo="get",So="application/x-www-form-urlencoded";function Tl(e){return typeof HTMLElement<"u"&&e instanceof HTMLElement}function F2(e){return Tl(e)&&e.tagName.toLowerCase()==="button"}function D2(e){return Tl(e)&&e.tagName.toLowerCase()==="form"}function M2(e){return Tl(e)&&e.tagName.toLowerCase()==="input"}function $2(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function z2(e,t){return e.button===0&&(!t||t==="_self")&&!$2(e)}var eo=null;function U2(){if(eo===null)try{new FormData(document.createElement("form"),0),eo=!1}catch{eo=!0}return eo}var B2=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function wu(e){return e!=null&&!B2.has(e)?(tn(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${So}"`),null):e}function H2(e,t){let n,r,s,a,i;if(D2(e)){let o=e.getAttribute("action");r=o?kn(o,t):null,n=e.getAttribute("method")||bo,s=wu(e.getAttribute("enctype"))||So,a=new FormData(e)}else if(F2(e)||M2(e)&&(e.type==="submit"||e.type==="image")){let o=e.form;if(o==null)throw new Error('Cannot submit a + + ))} +
  • + + + + + {expanded && Activation} + +
  • + + + {/* Settings at bottom of nav */} +
    + +
    + + ); +}; + +export default LeftNav; diff --git a/adtech_series_sp26/segment_builder/frontend/src/components/LoadingSpinner.tsx b/adtech_series_sp26/segment_builder/frontend/src/components/LoadingSpinner.tsx new file mode 100644 index 0000000..dd90feb --- /dev/null +++ b/adtech_series_sp26/segment_builder/frontend/src/components/LoadingSpinner.tsx @@ -0,0 +1,59 @@ +/** + * Shared loading spinner for consistent loading UX across the app. + */ + +import React from 'react'; + +interface LoadingSpinnerProps { + /** Optional label shown next to the spinner */ + label?: string; + /** Size: 'sm' | 'md' | 'lg' */ + size?: 'sm' | 'md' | 'lg'; + className?: string; +} + +const sizeClasses = { + sm: 'w-4 h-4', + md: 'w-6 h-6', + lg: 'w-8 h-8', +}; + +export const LoadingSpinner: React.FC = ({ + label, + size = 'md', + className = '', +}) => { + return ( +
    + + + + + {label && ( + {label} + )} +
    + ); +}; + +export default LoadingSpinner; diff --git a/adtech_series_sp26/segment_builder/frontend/src/components/PageHeader.tsx b/adtech_series_sp26/segment_builder/frontend/src/components/PageHeader.tsx new file mode 100644 index 0000000..aa9acf3 --- /dev/null +++ b/adtech_series_sp26/segment_builder/frontend/src/components/PageHeader.tsx @@ -0,0 +1,43 @@ +/** + * Shared page header with MegaCorp logo (left), title, optional description, and optional right-side content. + * Logo size is fixed (h-10); whitespace around it is consistent across all pages. + */ + +import React from 'react'; + +const LOGO_SRC = '/MegaCorp_Logo_-_Transparent.png'; + +export interface PageHeaderProps { + title: string; + description?: string; + children?: React.ReactNode; +} + +export const PageHeader: React.FC = ({ + title, + description, + children, +}) => { + return ( +
    +
    +
    + MegaCorp +
    +
    +

    {title}

    + {description && ( +

    {description}

    + )} +
    +
    + {children != null &&
    {children}
    } +
    + ); +}; + +export default PageHeader; diff --git a/adtech_series_sp26/segment_builder/frontend/src/components/SettingsPage.tsx b/adtech_series_sp26/segment_builder/frontend/src/components/SettingsPage.tsx new file mode 100644 index 0000000..b0f88f4 --- /dev/null +++ b/adtech_series_sp26/segment_builder/frontend/src/components/SettingsPage.tsx @@ -0,0 +1,499 @@ +/** + * Settings page. + */ + +import React, { useState, useEffect } from 'react'; +import { + settingsApi, + DEFAULT_COLUMN_CONFIGS, + type TablesSettings, + type ColumnConfigs, + type GrantsSqlResponse, +} from '@/api/settingsApi'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faChevronDown, faChevronRight, faCopy } from '@fortawesome/free-solid-svg-icons'; + +const DEFAULT_TABLES: TablesSettings = { + profiles_table: 'media_advertising.profiles.megacorp_audience_census_profile', + campaigns_table: 'media_advertising.segments.megacorp_campaigns', + definitions_table: 'media_advertising.segments.megacorp_segment_definitions', + column_configs: DEFAULT_COLUMN_CONFIGS, +}; + +/** UI label (fixed) → default table column name (editable). Count columns use identity columns, not derived "count". */ +const SEGMENT_INFO_SLOTS: { label: string; defaultColumn: string }[] = [ + { label: 'Segment Name', defaultColumn: 'segment_name' }, + { label: 'Segment Definition', defaultColumn: 'segment_definition' }, + { label: 'Quarter', defaultColumn: 'quarter' }, + { label: 'Flight Start Date', defaultColumn: 'start_date' }, + { label: 'Flight End Date', defaultColumn: 'end_date' }, + { label: 'Individual Identifier', defaultColumn: 'megacorp_indid' }, + { label: 'Household Identifier', defaultColumn: 'megacorp_hhid' }, +]; + +function ensureColumnConfigs(t: TablesSettings): TablesSettings { + if (t.column_configs) return t; + return { ...t, column_configs: DEFAULT_COLUMN_CONFIGS }; +} + +export const SettingsPage: React.FC = () => { + const [tables, setTables] = useState(DEFAULT_TABLES); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + const [configOpen, setConfigOpen] = useState<{ + profile: boolean; + segment_list: boolean; + segment_info: boolean; + permissions: boolean; + }>({ profile: false, segment_list: false, segment_info: false, permissions: false }); + const [grantsSql, setGrantsSql] = useState(null); + const [grantsLoading, setGrantsLoading] = useState(false); + const [copyFeedback, setCopyFeedback] = useState(null); + + useEffect(() => { + let cancelled = false; + setLoading(true); + settingsApi + .getTables() + .then((data) => { + if (!cancelled) setTables(ensureColumnConfigs(data)); + }) + .catch(() => { + if (!cancelled) setTables(DEFAULT_TABLES); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, []); + + const handleSaveTables = async () => { + setSaving(true); + setMessage(null); + try { + await settingsApi.putTables(tables); + setMessage({ type: 'success', text: 'Table settings saved.' }); + setGrantsSql(null); + } catch { + setMessage({ type: 'error', text: 'Failed to save table settings.' }); + } finally { + setSaving(false); + } + }; + + const togglePermissionsSection = () => { + const next = !configOpen.permissions; + setConfigOpen((o) => ({ ...o, permissions: next })); + if (next && grantsSql === null) { + setGrantsLoading(true); + settingsApi + .getGrantsSql() + .then(setGrantsSql) + .catch(() => setGrantsSql({ sql: '-- Failed to load. Save table settings and try again.', principal_placeholder: '' })) + .finally(() => setGrantsLoading(false)); + } + }; + + const copyGrantsSql = async () => { + if (!grantsSql?.sql) return; + setCopyFeedback(null); + try { + await navigator.clipboard.writeText(grantsSql.sql); + setCopyFeedback('Copied!'); + setTimeout(() => setCopyFeedback(null), 2000); + } catch { + setCopyFeedback('Copy failed'); + } + }; + + const configs: ColumnConfigs = tables.column_configs ?? DEFAULT_COLUMN_CONFIGS; + + const setProfileConfig = (updates: Partial) => { + setTables((prev) => ({ + ...prev, + column_configs: { + ...(prev.column_configs ?? DEFAULT_COLUMN_CONFIGS), + profile: { ...(prev.column_configs?.profile ?? DEFAULT_COLUMN_CONFIGS.profile), ...updates }, + }, + })); + }; + + const setSegmentListConfig = (updates: Partial) => { + setTables((prev) => ({ + ...prev, + column_configs: { + ...(prev.column_configs ?? DEFAULT_COLUMN_CONFIGS), + segment_list: { + ...(prev.column_configs?.segment_list ?? DEFAULT_COLUMN_CONFIGS.segment_list), + ...updates, + }, + }, + })); + }; + + /** Get the table column name that currently maps to this UI label. */ + const getColumnNameForLabel = (label: string, defaultColumn: string): string => { + const labels = configs.segment_info_labels; + const entry = Object.entries(labels).find(([, v]) => v === label); + return entry ? entry[0] : defaultColumn; + }; + + /** Set the table column name for a UI label (user edits the column name, not the label). */ + const setSegmentInfoColumnForLabel = (label: string, newColumnName: string) => { + setTables((prev) => { + const labels = prev.column_configs?.segment_info_labels ?? DEFAULT_COLUMN_CONFIGS.segment_info_labels; + const oldColumn = Object.entries(labels).find(([, v]) => v === label)?.[0]; + const next = { ...labels, [newColumnName]: label }; + if (oldColumn && oldColumn !== newColumnName) delete next[oldColumn]; + return { + ...prev, + column_configs: { + ...(prev.column_configs ?? DEFAULT_COLUMN_CONFIGS), + segment_info_labels: next, + }, + }; + }); + }; + + return ( +
    +
    +

    Settings

    +

    + Configure your preferences for the audience segmentation app. +

    +
    +
    +

    Tables

    +

    + Unity Catalog table names and column configs. Use fully qualified names + (catalog.schema.table) for tables. +

    + {loading ? ( +

    Loading…

    + ) : ( +
    + {/* Audience Profile Table */} +
    + + + setTables((prev) => ({ ...prev, profiles_table: e.target.value })) + } + placeholder={DEFAULT_TABLES.profiles_table} + className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-gray-500 focus:border-gray-500 mb-3" + /> +
    + + {configOpen.profile && ( +
    +
    + + Features are + +
    + + +
    +
    +
    +
    + + + setProfileConfig({ identity_household_column: e.target.value }) + } + className="w-full px-2 py-1.5 border border-gray-300 rounded text-sm" + /> +
    +
    + + + setProfileConfig({ identity_individual_column: e.target.value }) + } + className="w-full px-2 py-1.5 border border-gray-300 rounded text-sm" + /> +
    +
    +
    + )} +
    +
    + + {/* Audience Segment List Table */} +
    + + + setTables((prev) => ({ ...prev, campaigns_table: e.target.value })) + } + placeholder={DEFAULT_TABLES.campaigns_table} + className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-gray-500 focus:border-gray-500 mb-3" + /> +
    + + {configOpen.segment_list && ( +
    +
    +
    + + + setSegmentListConfig({ + identity_household_column: e.target.value, + }) + } + className="w-full px-2 py-1.5 border border-gray-300 rounded text-sm" + /> +
    +
    + + + setSegmentListConfig({ + identity_individual_column: e.target.value, + }) + } + className="w-full px-2 py-1.5 border border-gray-300 rounded text-sm" + /> +
    +
    +
    + + + setSegmentListConfig({ segment_name_column: e.target.value }) + } + placeholder="campaign_name" + className="w-full px-2 py-1.5 border border-gray-300 rounded text-sm" + /> +
    +
    + )} +
    +
    + + {/* Audience Segment Info Table */} +
    + + + setTables((prev) => ({ ...prev, definitions_table: e.target.value })) + } + placeholder={DEFAULT_TABLES.definitions_table} + className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-gray-500 focus:border-gray-500 mb-3" + /> +
    + + {configOpen.segment_info && ( +
    +

    + Map UI labels (fixed) to table column names (editable). Column names must match the Audience Segment Info table. +

    + {SEGMENT_INFO_SLOTS.map(({ label, defaultColumn }) => ( +
    + + {label} + + setSegmentInfoColumnForLabel(label, e.target.value)} + placeholder={defaultColumn} + className="flex-1 px-2 py-1.5 border border-gray-300 rounded text-sm font-mono" + /> +
    + ))} +
    + )} +
    +
    + + + {message && ( +

    + {message.text} +

    + )} +
    + )} +
    + +
    + + {configOpen.permissions && ( +
    +

    + {grantsSql?.principal_detected + ? "This app's identity was detected. Copy the SQL below and run it in a SQL warehouse to grant the required permissions." + : "Grant these to your app's service principal so it can read profiles and read/write segments. Copy and run in a SQL warehouse. When running in Databricks Apps, the identity is detected automatically."} +

    + {grantsLoading ? ( +

    Loading…

    + ) : grantsSql ? ( +
    +
    +                      {grantsSql.sql}
    +                    
    + +
    + ) : null} +
    + )} +
    +
    +
    +
    + ); +}; + +export default SettingsPage; diff --git a/adtech_series_sp26/segment_builder/frontend/src/features/segment-builder/api/schemas.ts b/adtech_series_sp26/segment_builder/frontend/src/features/segment-builder/api/schemas.ts new file mode 100644 index 0000000..1190aea --- /dev/null +++ b/adtech_series_sp26/segment_builder/frontend/src/features/segment-builder/api/schemas.ts @@ -0,0 +1,119 @@ +/** + * Zod schemas for validating API responses at the boundary. + * Provides runtime safety when the backend contract changes or returns unexpected data. + */ + +import { z } from 'zod'; + +const operatorType = z.enum(['IS', 'IN', 'NOT', 'BETWEEN', 'GT', 'LT', 'GTE', 'LTE']); +const logicType = z.enum(['AND', 'OR']); +const featureType = z.enum(['categorical', 'numeric', 'boolean']); + +export const segmentConditionSchema = z.object({ + id: z.string(), + feature: z.string(), + operator: operatorType, + values: z.array(z.union([z.string(), z.number(), z.boolean()])), +}); + +export const segmentGroupSchema = z.object({ + id: z.string(), + logic: logicType, + conditions: z.array(segmentConditionSchema), +}); + +export const segmentDefinitionSchema = z.object({ + name: z.string(), + description: z.string(), + groups: z.array(segmentGroupSchema), + group_logic: logicType, +}); + +export const featureSchema = z.object({ + name: z.string(), + display_name: z.string(), + column: z.string(), + type: featureType, + operators: z.array(operatorType), + description: z.string(), + nullable: z.boolean(), + null_rate: z.number().nullish(), + distinct_values: z.number().nullish(), + values: z.array(z.string()).nullish(), + searchable: z.boolean().optional(), + range: z.object({ min: z.number(), max: z.number() }).nullish(), + brackets: z + .array(z.object({ label: z.string(), min: z.number().optional(), max: z.number().optional() })) + .nullish(), +}); + +export const featuresListResponseSchema = z.object({ + features: z.array(featureSchema), +}); + +export const featureValuesResponseSchema = z.object({ + values: z.array(z.string()), + total: z.number(), +}); + +export const segmentPreviewResponseSchema = z.object({ + individual_count: z.number(), + household_count: z.number(), + sql: z.string().nullable(), + execution_time_ms: z.number(), +}); + +export const segmentBuildResponseSchema = z.object({ + segment_id: z.string(), + rows_inserted: z.number(), + campaign_name: z.string(), +}); + +export const segmentListItemSchema = z.object({ + segment_name: z.string(), + segment_definition: z.string(), + quarter: z.string(), + start_date: z.string(), + end_date: z.string(), +}); + +export const listSegmentsResponseSchema = z.object({ + segments: z.array(segmentListItemSchema), +}); + +export const allSegmentsOverviewRowSchema = z.object({ + segment_name: z.string(), + segment_definition: z.string(), + quarter: z.string(), + start_date: z.string(), + end_date: z.string(), + individual_count: z.number(), + household_count: z.number(), +}); + +export const allSegmentsOverviewResponseSchema = z.object({ + rows: z.array(allSegmentsOverviewRowSchema), +}); + +export const segmentUpdateResponseSchema = z.object({ + status: z.string(), + segment_name: z.string(), +}); + +export const agentParseResponseSchema = z.object({ + response_text: z.string(), + segment: segmentDefinitionSchema.nullable().optional(), + preview: z + .object({ + individual_count: z.number(), + household_count: z.number(), + }) + .nullable() + .optional(), + sql: z.string().nullable().optional(), +}); + +export const agentSummarizeResponseSchema = z.object({ + summary: z.string(), + suggested_name: z.string(), +}); diff --git a/adtech_series_sp26/segment_builder/frontend/src/features/segment-builder/api/segmentApi.ts b/adtech_series_sp26/segment_builder/frontend/src/features/segment-builder/api/segmentApi.ts new file mode 100644 index 0000000..0cc8561 --- /dev/null +++ b/adtech_series_sp26/segment_builder/frontend/src/features/segment-builder/api/segmentApi.ts @@ -0,0 +1,268 @@ +/** + * API client for segment builder. + * Validates API responses with Zod at the boundary for runtime safety. + */ + +import apiClient from '@/lib/apiClient'; +import { + featuresListResponseSchema, + featureValuesResponseSchema, + segmentPreviewResponseSchema, + segmentBuildResponseSchema, + listSegmentsResponseSchema, + allSegmentsOverviewResponseSchema, + segmentUpdateResponseSchema, + agentParseResponseSchema, + agentSummarizeResponseSchema, +} from './schemas'; +import type { + Feature, + SegmentDefinition, + SegmentPreviewResponse, +} from '../types'; +import { filterIncompleteConditions } from '../utils/segmentUtils'; + +export const segmentApi = { + /** + * Get all available features. + */ + async getFeatures(): Promise { + const response = await apiClient.get('/features'); + const parsed = featuresListResponseSchema.safeParse(response.data); + if (!parsed.success) { + throw new Error(`Invalid features response: ${parsed.error.message}`); + } + return parsed.data.features; + }, + + /** + * Get distinct values for a feature. + */ + async getFeatureValues( + featureName: string, + search?: string, + limit: number = 100 + ): Promise { + const params = new URLSearchParams(); + if (search) params.append('search', search); + params.append('limit', limit.toString()); + + const response = await apiClient.get( + `/features/${featureName}/values?${params.toString()}` + ); + const parsed = featureValuesResponseSchema.safeParse(response.data); + if (!parsed.success) { + throw new Error(`Invalid feature values response: ${parsed.error.message}`); + } + return parsed.data.values; + }, + + /** + * Preview a segment - get counts and SQL. + */ + async previewSegment( + segment: SegmentDefinition, + includeSql: boolean = true + ): Promise { + // Filter out incomplete conditions before sending + const cleanSegment = filterIncompleteConditions(segment); + + // If no valid conditions after filtering, return zeros + if (cleanSegment.groups.length === 0) { + return { + individual_count: 0, + household_count: 0, + sql: null, + execution_time_ms: 0, + }; + } + + const response = await apiClient.post('/segment/preview', { + segment: { + name: cleanSegment.name, + description: cleanSegment.description, + groups: cleanSegment.groups, + group_logic: cleanSegment.groupLogic, + }, + include_sql: includeSql, + }); + const parsed = segmentPreviewResponseSchema.safeParse(response.data); + if (!parsed.success) { + throw new Error(`Invalid preview response: ${parsed.error.message}`); + } + return parsed.data; + }, + + /** + * Build and save a segment. + */ + async buildSegment( + segment: SegmentDefinition, + name: string, + quarter: string, + startDate: string, + endDate: string + ): Promise<{ segment_id: string; rows_inserted: number; campaign_name: string }> { + // Filter out incomplete conditions before sending + const cleanSegment = filterIncompleteConditions(segment); + + if (cleanSegment.groups.length === 0) { + throw new Error('Cannot build segment with no valid conditions'); + } + + const response = await apiClient.post('/segment/build', { + segment: { + name: cleanSegment.name, + description: cleanSegment.description, + groups: cleanSegment.groups, + group_logic: cleanSegment.groupLogic, + }, + name, + quarter, + start_date: startDate, + end_date: endDate, + }); + const parsed = segmentBuildResponseSchema.safeParse(response.data); + if (!parsed.success) { + throw new Error(`Invalid build response: ${parsed.error.message}`); + } + return parsed.data; + }, + + /** + * List existing segments. + */ + async listSegments(): Promise> { + const response = await apiClient.get('/segment'); + const parsed = listSegmentsResponseSchema.safeParse(response.data); + if (!parsed.success) { + throw new Error(`Invalid list segments response: ${parsed.error.message}`); + } + return parsed.data.segments; + }, + + /** + * All segments overview: definitions joined with campaigns, grouped by campaign with counts. + */ + async getAllSegmentsOverview(): Promise> { + const response = await apiClient.get('/segment/all'); + const parsed = allSegmentsOverviewResponseSchema.safeParse(response.data); + if (!parsed.success) { + throw new Error(`Invalid all segments response: ${parsed.error.message}`); + } + return parsed.data.rows; + }, + + /** + * Update segment metadata (definition, quarter, flight dates) in the Delta table. + */ + async updateSegmentMetadata( + segmentName: string, + payload: { + segment_definition: string; + quarter: string; + start_date: string; + end_date: string; + } + ): Promise<{ status: string; segment_name: string }> { + const response = await apiClient.patch( + `/segment/${encodeURIComponent(segmentName)}`, + payload + ); + const parsed = segmentUpdateResponseSchema.safeParse(response.data); + if (!parsed.success) { + throw new Error(`Invalid update response: ${parsed.error.message}`); + } + return parsed.data; + }, + + /** + * Parse natural language input using LLM agent. + */ + async parseAgentInput( + input: string, + conversationHistory: Array<{ role: string; content: string }>, + currentSegment?: SegmentDefinition + ): Promise<{ + response_text: string; + segment: SegmentDefinition | null; + preview: { individual_count: number; household_count: number } | null; + sql: string | null; + }> { + // Filter incomplete conditions from current segment if provided + const cleanSegment = currentSegment + ? filterIncompleteConditions(currentSegment) + : null; + + const response = await apiClient.post('/agent/parse', { + input, + conversation_history: conversationHistory, + current_segment: cleanSegment && cleanSegment.groups.length > 0 ? { + name: cleanSegment.name, + description: cleanSegment.description, + groups: cleanSegment.groups, + group_logic: cleanSegment.groupLogic, + } : null, + }); + + const parsed = agentParseResponseSchema.safeParse(response.data); + if (!parsed.success) { + throw new Error(`Invalid agent parse response: ${parsed.error.message}`); + } + const data = parsed.data; + + // Convert snake_case to camelCase for segment in return value + return { + response_text: data.response_text, + segment: data.segment + ? { + name: data.segment.name, + description: data.segment.description, + groups: data.segment.groups, + groupLogic: data.segment.group_logic, + } + : null, + preview: data.preview ?? null, + sql: data.sql ?? null, + }; + }, + + /** + * Generate a 1-2 sentence segment description and suggested name from chat context using the LLM. + */ + async summarizeSegment( + segment: SegmentDefinition, + conversationHistory: Array<{ role: string; content: string }> + ): Promise<{ summary: string; suggested_name: string }> { + const response = await apiClient.post('/agent/summarize', { + segment: { + name: segment.name, + description: segment.description, + groups: segment.groups, + group_logic: segment.groupLogic, + }, + conversation_history: conversationHistory, + }); + const parsed = agentSummarizeResponseSchema.safeParse(response.data); + if (!parsed.success) { + throw new Error(`Invalid agent summarize response: ${parsed.error.message}`); + } + return parsed.data; + }, +}; + +export default segmentApi; diff --git a/adtech_series_sp26/segment_builder/frontend/src/features/segment-builder/components/AllSegmentsPage.tsx b/adtech_series_sp26/segment_builder/frontend/src/features/segment-builder/components/AllSegmentsPage.tsx new file mode 100644 index 0000000..5ac1300 --- /dev/null +++ b/adtech_series_sp26/segment_builder/frontend/src/features/segment-builder/components/AllSegmentsPage.tsx @@ -0,0 +1,254 @@ +/** + * All Segments page: definitions joined with campaigns, grouped by campaign with counts. + */ + +import React, { useMemo, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; +import { PageHeader } from '../../../components/PageHeader'; +import { LoadingSpinner } from '../../../components/LoadingSpinner'; +import { settingsApi } from '../../../api/settingsApi'; +import { segmentApi } from '../api/segmentApi'; +import { EditSegmentDialog, type SegmentOverviewRow } from './EditSegmentDialog'; + +const DEFAULT_LABELS: Record = { + segment_name: 'Segment Name', + segment_definition: 'Segment Definition', + quarter: 'Quarter', + start_date: 'Flight Start Date', + end_date: 'Flight End Date', + megacorp_hhid: 'Household Identifier', + megacorp_indid: 'Individual Identifier', +}; + +export const AllSegmentsPage: React.FC = () => { + const [editRow, setEditRow] = useState(null); + const [quarterFilter, setQuarterFilter] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [expandedDefinitions, setExpandedDefinitions] = useState>(new Set()); + + const toggleDefinition = (segmentName: string) => { + setExpandedDefinitions((prev) => { + const next = new Set(prev); + if (next.has(segmentName)) next.delete(segmentName); + else next.add(segmentName); + return next; + }); + }; + + const { data: settings } = useQuery({ + queryKey: ['settingsTables'], + queryFn: () => settingsApi.getTables(), + }); + const columnLabels = useMemo( + () => ({ ...DEFAULT_LABELS, ...settings?.column_configs?.segment_info_labels }), + [settings?.column_configs?.segment_info_labels] + ); + const segmentListConfig = settings?.column_configs?.segment_list; + const householdCountHeader = + columnLabels[segmentListConfig?.identity_household_column ?? 'megacorp_hhid'] ?? 'Household Identifier'; + const individualCountHeader = + columnLabels[segmentListConfig?.identity_individual_column ?? 'megacorp_indid'] ?? 'Individual Identifier'; + + const { data: rows, isLoading, error } = useQuery({ + queryKey: ['allSegmentsOverview'], + queryFn: () => segmentApi.getAllSegmentsOverview(), + }); + + const rawList = rows ?? []; + + const quarters = useMemo(() => { + const set = new Set(); + rawList.forEach((row) => { + if (row.quarter) set.add(row.quarter); + }); + return Array.from(set).sort(); + }, [rawList]); + + const list = useMemo(() => { + return rawList.filter((row) => { + if (quarterFilter && row.quarter !== quarterFilter) return false; + const q = searchQuery.trim().toLowerCase(); + if (!q) return true; + const name = (row.segment_name ?? '').toLowerCase(); + const def = (row.segment_definition ?? '').toLowerCase(); + return name.includes(q) || def.includes(q); + }); + }, [rawList, quarterFilter, searchQuery]); + + if (isLoading) { + return ( +
    + +
    + ); + } + + if (error) { + return ( +
    +

    + Failed to load segments: {error instanceof Error ? error.message : 'Unknown error'} +

    +
    + ); + } + + return ( +
    + + +
    +
    +
    + + +
    +
    + + setSearchQuery(e.target.value)} + placeholder="Search by segment name or definition…" + className="block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-500 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" + /> +
    + {(quarterFilter || searchQuery.trim()) && ( + + )} +
    +
    +
    + + + + + + + + + + + + + + {list.length === 0 ? ( + + + + ) : ( + list.map((row) => ( + + + + + + + + + + )) + )} + +
    + {columnLabels.segment_name} + + {columnLabels.segment_definition} + + {columnLabels.quarter} + + {columnLabels.start_date} + + {columnLabels.end_date} + + {householdCountHeader} + + {individualCountHeader} +
    + No segments found. +
    + + +
    + + + {row.segment_definition ?? '—'} + +
    +
    + {row.quarter ?? '—'} + + {row.start_date != null ? String(row.start_date) : '—'} + + {row.end_date != null ? String(row.end_date) : '—'} + + {Number(row.household_count ?? 0).toLocaleString()} + + {Number(row.individual_count ?? 0).toLocaleString()} +
    +
    +
    +
    + + setEditRow(null)} + row={editRow} + /> +
    + ); +}; + +export default AllSegmentsPage; diff --git a/adtech_series_sp26/segment_builder/frontend/src/features/segment-builder/components/BuildSegmentDialog.tsx b/adtech_series_sp26/segment_builder/frontend/src/features/segment-builder/components/BuildSegmentDialog.tsx new file mode 100644 index 0000000..18791a9 --- /dev/null +++ b/adtech_series_sp26/segment_builder/frontend/src/features/segment-builder/components/BuildSegmentDialog.tsx @@ -0,0 +1,345 @@ +/** + * Dialog for building and saving a segment. + */ + +import React, { useState, useEffect } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { segmentApi } from '../api/segmentApi'; +import { useSegmentContext } from '../context/SegmentContext'; + +interface BuildResult { + campaign_name: string; + rows_inserted: number; +} + +interface BuildSegmentDialogProps { + isOpen: boolean; + onClose: () => void; + individualCount: number; + householdCount: number; +} + +const currentYY = new Date().getFullYear() % 100; +const currentQuarter = `${currentYY}Q${Math.ceil((new Date().getMonth() + 1) / 3)}`; + +const quarterOptions = [ + { value: `${currentYY}Q1`, label: `${currentYY}Q1 (Jan-Mar)` }, + { value: `${currentYY}Q2`, label: `${currentYY}Q2 (Apr-Jun)` }, + { value: `${currentYY}Q3`, label: `${currentYY}Q3 (Jul-Sep)` }, + { value: `${currentYY}Q4`, label: `${currentYY}Q4 (Oct-Dec)` }, +]; + +/** Return start and end dates (YYYY-MM-DD) for a quarter string e.g. "25Q1". */ +function getQuarterDateRange(quarter: string): { startDate: string; endDate: string } { + const yy = parseInt(quarter.slice(0, 2), 10); + const year = 2000 + yy; + const q = parseInt(quarter.replace(/^\d+Q/, ''), 10); + const startMonth = (q - 1) * 3 + 1; + const endMonth = q * 3; + const startDate = new Date(year, startMonth - 1, 1); + const endDate = new Date(year, endMonth, 0); // last day of endMonth + const fmt = (d: Date) => d.toISOString().slice(0, 10); + return { startDate: fmt(startDate), endDate: fmt(endDate) }; +} + +export const BuildSegmentDialog: React.FC = ({ + isOpen, + onClose, + individualCount, + householdCount, +}) => { + const { state } = useSegmentContext(); + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [quarter, setQuarter] = useState(currentQuarter); + const [startDate, setStartDate] = useState(() => getQuarterDateRange(currentQuarter).startDate); + const [endDate, setEndDate] = useState(() => getQuarterDateRange(currentQuarter).endDate); + const [buildResult, setBuildResult] = useState(null); + const [descriptionExpanded, setDescriptionExpanded] = useState(false); + + // When dialog opens, auto-populate description and suggested name from LLM summary of segment + chat + useEffect(() => { + if (!isOpen) return; + const segment = state.segment; + const hasConditions = segment.groups.some((g) => + g.conditions.some((c) => c.feature && c.values.length > 0) + ); + if (!hasConditions) return; + setName(''); + setDescription(''); + let cancelled = false; + const history = state.chatHistory.map((m) => ({ + role: m.role === 'agent' ? 'assistant' : m.role, + content: m.content, + })); + segmentApi + .summarizeSegment(segment, history) + .then(({ summary, suggested_name }) => { + if (cancelled) return; + if (summary) setDescription(summary); + if (suggested_name) setName(suggested_name); + }) + .catch(() => {}); + return () => { + cancelled = true; + }; + }, [isOpen]); + + const resetForm = () => { + setName(''); + setDescription(''); + setDescriptionExpanded(false); + setQuarter(currentQuarter); + const range = getQuarterDateRange(currentQuarter); + setStartDate(range.startDate); + setEndDate(range.endDate); + setBuildResult(null); + buildMutation.reset(); + }; + + const handleClose = () => { + resetForm(); + onClose(); + }; + + const buildMutation = useMutation({ + mutationFn: async () => { + return segmentApi.buildSegment( + { ...state.segment, description }, + name, + quarter, + startDate, + endDate + ); + }, + onSuccess: (data) => { + setBuildResult({ + campaign_name: data.campaign_name, + rows_inserted: data.rows_inserted, + }); + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + buildMutation.mutate(); + }; + + if (!isOpen) return null; + + return ( +
    + {/* Backdrop */} +
    + + {/* Dialog */} +
    + {buildResult ? ( + /* Success Summary */ +
    +
    + + + +
    +

    + Segment Built Successfully +

    +
    +
    + Segment Name + {buildResult.campaign_name} +
    +
    + Rows Inserted + {buildResult.rows_inserted.toLocaleString()} +
    +
    + Quarter + {quarter} +
    +
    + Date Range + {startDate} — {endDate} +
    + {description && ( +
    + Description + {description} +
    + )} +
    + +
    + ) : ( + /* Build Form */ + <> +

    + Build Segment +

    + + {/* Preview Summary */} +
    +
    + Individuals: + {individualCount.toLocaleString()} +
    +
    + Households: + {householdCount.toLocaleString()} +
    +
    + + {/* Inline Error Banner */} + {buildMutation.isError && ( +
    + {buildMutation.error?.message ?? 'An error occurred while building the segment.'} +
    + )} + + + {/* Segment Name */} +
    + + setName(e.target.value)} + placeholder="e.g., CA_Dog_Owners_25_54" + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + required + /> +
    + + {/* Description */} +
    + +
    +