From 71d3730423ae9bb139404b989c647197e2586965 Mon Sep 17 00:00:00 2001 From: Utkarsh Date: Sat, 30 May 2026 19:02:30 +0530 Subject: [PATCH] Regigigas is the MVP --- README.md | 52 + cli/website.py | 97 ++ gui/__init__.py | 1 + gui/backend.py | 441 +++++++ gui/dashboard.py | 1119 +++++++++++++++++ gui/main.py | 144 +++ gui/wizard.py | 964 ++++++++++++++ pyproject.toml | 4 +- website/dist/assets/index-BRByxZiy.css | 1 - website/dist/assets/index-DLKHxB3n.js | 88 -- website/dist/index.html | 6 +- website/index.html | 2 +- website/package-lock.json | 161 +-- website/src/App.tsx | 14 +- .../src/features/dashboard/DashboardPage.tsx | 1 + .../dashboard/components/AgentStatusBadge.tsx | 79 ++ .../dashboard/components/PanelShell.tsx | 86 ++ .../dashboard/components/SidebarNavItem.tsx | 58 + website/src/features/dashboard/dashboard.css | 18 + .../dashboard/hooks/useDashboardStatus.ts | 15 + .../dashboard/layout/DashboardShell.tsx | 64 + .../src/features/dashboard/layout/Sidebar.tsx | 75 ++ .../src/features/dashboard/layout/TopBar.tsx | 89 ++ website/src/features/dashboard/nav.ts | 55 + .../features/dashboard/panels/AskPanel.tsx | 49 + .../dashboard/panels/DashboardPanel.tsx | 43 + .../features/dashboard/panels/LogsPanel.tsx | 41 + .../dashboard/panels/SettingsPanel.tsx | 53 + .../dashboard/panels/SourcesPanel.tsx | 84 ++ website/src/features/dashboard/types.ts | 15 + website/tailwind.config.js | 6 +- 31 files changed, 3689 insertions(+), 236 deletions(-) create mode 100644 cli/website.py create mode 100644 gui/__init__.py create mode 100644 gui/backend.py create mode 100644 gui/dashboard.py create mode 100644 gui/main.py create mode 100644 gui/wizard.py delete mode 100644 website/dist/assets/index-BRByxZiy.css delete mode 100644 website/dist/assets/index-DLKHxB3n.js create mode 100644 website/src/features/dashboard/DashboardPage.tsx create mode 100644 website/src/features/dashboard/components/AgentStatusBadge.tsx create mode 100644 website/src/features/dashboard/components/PanelShell.tsx create mode 100644 website/src/features/dashboard/components/SidebarNavItem.tsx create mode 100644 website/src/features/dashboard/dashboard.css create mode 100644 website/src/features/dashboard/hooks/useDashboardStatus.ts create mode 100644 website/src/features/dashboard/layout/DashboardShell.tsx create mode 100644 website/src/features/dashboard/layout/Sidebar.tsx create mode 100644 website/src/features/dashboard/layout/TopBar.tsx create mode 100644 website/src/features/dashboard/nav.ts create mode 100644 website/src/features/dashboard/panels/AskPanel.tsx create mode 100644 website/src/features/dashboard/panels/DashboardPanel.tsx create mode 100644 website/src/features/dashboard/panels/LogsPanel.tsx create mode 100644 website/src/features/dashboard/panels/SettingsPanel.tsx create mode 100644 website/src/features/dashboard/panels/SourcesPanel.tsx create mode 100644 website/src/features/dashboard/types.ts diff --git a/README.md b/README.md index 9baebbf..208d521 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,60 @@ hermes gateway # start the scheduler (keep running) hermes cron list # verify "Sorelax 6h Context Refresh" ``` +## GUI (Desktop App) + +Sorelax ships a **PyQt6 desktop interface** — a dark, Tokyo Night themed dashboard with +live log tailing, interactive Q&A, a context explorer, and a credential manager. + +### Install + +PyQt6 is an optional dependency (not pulled in by the base `pip install -e .`): + +```bash +pip install PyQt6 +pip install -e . # re-installs to register the sorelax-gui script +``` + +### Launch + +```bash +sorelax-gui +``` + +Or directly: + +```bash +python -m gui.main +``` + +### First-run wizard + +If no `.env` is found the app opens the **Setup Wizard** automatically: + +| Step | What happens | +|------|-------------| +| **1 — Dependency Check** | Verifies `curl`, `pip`, and `coral` are on PATH. Shows install command if Coral is missing. | +| **2 — Credentials** | Collects all 7 tokens (GitHub, Linear, Slack, Notion, Gemini, …) with password echo. | +| **3 — Install Progress** | Runs the full install pipeline in a background thread with colour-coded live output. | +| **4 — Done** | Displays copyable `hermes model / gateway` commands and opens the main dashboard. | + +### Dashboard panels + +| Panel | Description | +|-------|-------------| +| **Dashboard** | Coloured source health dots, last-sync time, countdown timer, Refresh Now + Start/Stop scheduler buttons, live console | +| **Ask Sorelax** | Natural-language Q&A streamed live from `sorelax ask`, with session history sidebar | +| **Context Explorer** | Scrollable cards built from `~/.sorelax/project_context.json`, grouped by key | +| **Live Logs** | Auto-scrolling tail of `~/.sorelax/project_log.jsonl` with Pause/Resume | +| **Settings** | 7 credential fields (password mode, Show/Hide toggle), Save applies Coral sources off-thread | + +> **Screenshot** +> +> *(Add a screenshot here once the app is running — e.g. `![Dashboard](docs/screenshot_dashboard.png)`)* + ## Usage + ### Sorelax CLI ```bash diff --git a/cli/website.py b/cli/website.py new file mode 100644 index 0000000..7f83c3d --- /dev/null +++ b/cli/website.py @@ -0,0 +1,97 @@ +"""Launch the Sorelax dashboard website (Vite dev server).""" + +from __future__ import annotations + +import shutil +import signal +import subprocess +import sys +import threading +import time +import webbrowser +from pathlib import Path + +import typer + + +def _repo_root() -> Path: + return Path(__file__).resolve().parent.parent + + +def _website_dir() -> Path: + website = _repo_root() / "website" + if not website.is_dir(): + typer.echo( + f"Cannot find website/ at {website}. " + "Run sorelax-website from an installed Sorelax checkout.", + err=True, + ) + raise typer.Exit(1) + return website + + +def _ensure_deps(website: Path) -> None: + if (website / "node_modules").is_dir(): + return + if not shutil.which("npm"): + typer.echo("npm is required. Install Node.js and try again.", err=True) + raise typer.Exit(1) + typer.echo("Installing frontend dependencies (first run)...") + subprocess.run(["npm", "install"], cwd=website, check=True) + + +def _launch( + port: int = typer.Option(5173, "--port", "-p", help="Vite dev server port"), + host: str = typer.Option("127.0.0.1", "--host", help="Bind address"), + no_open: bool = typer.Option(False, "--no-open", help="Do not open the browser"), +) -> None: + """Start the dashboard and open it in your default browser.""" + website = _website_dir() + _ensure_deps(website) + + if not shutil.which("npm"): + typer.echo("npm is required. Install Node.js and try again.", err=True) + raise typer.Exit(1) + + url = f"http://{host}:{port}/dashboard" + typer.echo(f"\n Sorelax Website → {url}\n") + typer.echo(" Press Ctrl+C to stop.\n") + + if not no_open: + + def _open_browser() -> None: + time.sleep(1.8) + webbrowser.open(url) + + threading.Thread(target=_open_browser, daemon=True).start() + + proc = subprocess.Popen( + [ + "npm", + "run", + "dev", + "--", + "--host", + host, + "--port", + str(port), + "--strictPort", + ], + cwd=website, + ) + + def _handle_signal(_signum: int, _frame: object) -> None: + proc.send_signal(signal.SIGINT) + + signal.signal(signal.SIGINT, _handle_signal) + signal.signal(signal.SIGTERM, _handle_signal) + + sys.exit(proc.wait()) + + +def main() -> None: + typer.run(_launch) + + +if __name__ == "__main__": + main() diff --git a/gui/__init__.py b/gui/__init__.py new file mode 100644 index 0000000..3b40e49 --- /dev/null +++ b/gui/__init__.py @@ -0,0 +1 @@ +"""Sorelax GUI package — PyQt6 desktop interface.""" diff --git a/gui/backend.py b/gui/backend.py new file mode 100644 index 0000000..85ebe6c --- /dev/null +++ b/gui/backend.py @@ -0,0 +1,441 @@ +"""GUI Backend for Sorelax — Standalone Python module wrapping Sorelax CLI and setup actions. + +This module is pure Python, fully testable standalone, and exposes callbacks for +stdout/stderr streaming of long-running operations. +""" + +from __future__ import annotations + +import json +import os +import shutil +import signal +import subprocess +import sys +import time +from pathlib import Path +from typing import Any, Callable + +# Robust imports supporting both direct execution and package imports +try: + from sorelax_cli.scheduler import read_pid, _pid_running, next_refresh_iso, INTERVAL_HOURS +except ImportError: + try: + from cli.scheduler import read_pid, _pid_running, next_refresh_iso, INTERVAL_HOURS + except ImportError: + # Fallback values if CLI scheduler package is not in python path + INTERVAL_HOURS = 6 + def read_pid() -> int | None: + pid_file = Path.home() / ".sorelax" / "sorelax.pid" + if not pid_file.exists(): + return None + try: + return int(pid_file.read_text().strip()) + except ValueError: + return None + def _pid_running(pid: int) -> bool: + try: + os.kill(pid, 0) + return True + except OSError: + return False + def next_refresh_iso() -> str | None: + pid = read_pid() + if pid and _pid_running(pid): + return f"~{INTERVAL_HOURS}h from last daemon tick (PID {pid})" + return None + +try: + from agent.refresh import CONTEXT_PATH, LOG_PATH +except ImportError: + CONTEXT_PATH = Path.home() / ".sorelax" / "project_context.json" + LOG_PATH = Path.home() / ".sorelax" / "project_log.jsonl" + + +def _repo_root() -> Path: + """Returns the absolute path to the repository root directory.""" + return Path(__file__).resolve().parent.parent + + +def _run_cmd_stream( + cmd: list[str] | str, + line_callback: Callable[[str], None], + cwd: Path | None = None, + env: dict[str, str] | None = None, +) -> int: + """Spawns a subprocess with stdout/stderr merged and streams lines to callback.""" + shell = isinstance(cmd, str) + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + shell=shell, + cwd=cwd, + env=env, + bufsize=1, # Line buffered + ) + + if proc.stdout: + for line in proc.stdout: + line_callback(line.rstrip("\n")) + + return proc.wait() + + +def check_dependencies() -> dict[str, dict[str, Any]]: + """Checks for curl, pip, and coral presence and their versions. + + Returns: + dict: A dictionary containing the status (installed, path, version) for each tool. + """ + tools = ["curl", "pip", "coral"] + result = {} + for tool in tools: + path = shutil.which(tool) + installed = path is not None + version = None + if installed: + try: + proc = subprocess.run( + [tool, "--version"], + capture_output=True, + text=True, + timeout=5, + ) + if proc.returncode == 0: + version = proc.stdout.strip().splitlines()[0] + except Exception: + pass + result[tool] = { + "installed": installed, + "path": path, + "version": version, + } + return result + + +def save_env(tokens: dict[str, str]) -> None: + """Rewrites the .env file in the repository root and re-runs Coral source registration. + + Args: + tokens: Dictionary of keys and values to save. + """ + root = _repo_root() + env_file = root / ".env" + + keys = [ + "GITHUB_OWNER", + "GITHUB_REPO", + "GITHUB_TOKEN", + "LINEAR_API_KEY", + "SLACK_TOKEN", + "NOTION_TOKEN", + "GEMINI_API_KEY", + ] + + content = "" + for key in keys: + val = tokens.get(key, "") + content += f"{key}={val}\n" + + env_file.write_text(content, encoding="utf-8") + env_file.chmod(0o600) + + # Re-runs coral source add for the 4 key integrations + sources = ["github", "linear", "slack", "notion"] + token_mapping = { + "github": "GITHUB_TOKEN", + "linear": "LINEAR_API_KEY", + "slack": "SLACK_TOKEN", + "notion": "NOTION_TOKEN", + } + + env = os.environ.copy() + env.update(tokens) + + for name in sources: + token = tokens.get(token_mapping[name], "") + if token: + try: + res = subprocess.run( + ["coral", "source", "add", name, "--token", token], + capture_output=True, + text=True, + env=env, + ) + if res.returncode == 0: + continue + except Exception: + pass + try: + subprocess.run( + ["coral", "source", "add", name], + capture_output=True, + text=True, + env=env, + ) + except Exception: + pass + + +def run_install(tokens: dict[str, str], line_callback: Callable[[str], None]) -> bool: + """Performs the full installation/setup pipeline mirroring install.sh. + + Steps: + 1. Installs the Python package in editable mode (pip install -e .) + 2. Writes the .env file with supplied tokens + 3. Adds Coral sources (github, linear, slack, notion) + 4. Sets up Hermes Agent (setup_hermes.sh) + 5. Registers Sorelax cron in Hermes (register_cron.sh) + 6. Runs the first context refresh + + Args: + tokens: Dictionary of Sorelax integration tokens. + line_callback: Callback function receiving stdout/stderr lines. + + Returns: + bool: True if installation was successful, False otherwise. + """ + root = _repo_root() + env = os.environ.copy() + env.update(tokens) + + # 1. pip install -e . + line_callback(">>> [1/6] Installing Sorelax Python package in editable mode...") + code = _run_cmd_stream( + [sys.executable, "-m", "pip", "install", "-e", "."], + line_callback, + cwd=root, + ) + if code != 0: + line_callback(f">>> Error: pip install failed with exit code {code}") + return False + + # 2. Writes .env + line_callback(">>> [2/6] Writing .env configuration file...") + try: + save_env(tokens) + line_callback(">>> Successfully wrote .env configuration.") + except Exception as exc: + line_callback(f">>> Error writing .env: {exc}") + return False + + # 3. Add Coral sources + line_callback(">>> [3/6] Configuring Coral sources...") + sources = ["github", "linear", "slack", "notion"] + token_mapping = { + "github": "GITHUB_TOKEN", + "linear": "LINEAR_API_KEY", + "slack": "SLACK_TOKEN", + "notion": "NOTION_TOKEN", + } + for name in sources: + token = tokens.get(token_mapping[name], "") + line_callback(f">>> Registering Coral source: {name}...") + if token: + cmd = ["coral", "source", "add", name, "--token", token] + code = _run_cmd_stream(cmd, line_callback, env=env) + if code != 0: + line_callback(f">>> Warning: Failed to add {name} with token (code {code}), trying fallback...") + _run_cmd_stream(["coral", "source", "add", name], line_callback, env=env) + else: + _run_cmd_stream(["coral", "source", "add", name], line_callback, env=env) + + # 4. setup_hermes.sh + line_callback(">>> [4/6] Setting up Hermes Agent...") + hermes_script = root / "hermes" / "setup_hermes.sh" + if hermes_script.exists(): + code = _run_cmd_stream( + ["bash", str(hermes_script)], + line_callback, + cwd=root, + env=env, + ) + if code != 0: + line_callback(f">>> Warning: setup_hermes.sh completed with non-zero exit code {code}") + else: + line_callback(">>> Warning: setup_hermes.sh not found, skipping Hermes setup.") + + # 5. register_cron.sh + line_callback(">>> [5/6] Registering Sorelax cron job inside Hermes Agent...") + register_script = root / "hermes" / "register_cron.sh" + if register_script.exists(): + code = _run_cmd_stream( + ["bash", str(register_script)], + line_callback, + cwd=root, + env=env, + ) + if code != 0: + line_callback(f">>> Warning: register_cron.sh failed with exit code {code}") + else: + line_callback(">>> Warning: register_cron.sh not found, skipping cron registration.") + + # 6. Run first refresh + line_callback(">>> [6/6] Running first context refresh pipeline...") + refresh_code = run_refresh(line_callback) + if refresh_code == 0: + line_callback(">>> Sorelax installation and setup completed successfully!") + return True + else: + line_callback(f">>> Warning: First context refresh finished with non-zero code {refresh_code}") + return True + + +def run_refresh(line_callback: Callable[[str], None]) -> int: + """Runs the full refresh pipeline via the Sorelax CLI. + + Args: + line_callback: Callback function receiving stdout/stderr lines. + + Returns: + int: Subprocess exit code. + """ + root = _repo_root() + env = os.environ.copy() + env["PYTHONPATH"] = str(root) + return _run_cmd_stream( + [sys.executable, "-m", "sorelax_cli.main", "refresh"], + line_callback, + cwd=root, + env=env, + ) + + +def run_ask(query: str, line_callback: Callable[[str], None]) -> int: + """Runs the Sorelax ask command for a given natural-language question. + + Args: + query: Question to ask Sorelax. + line_callback: Callback function receiving stdout/stderr lines. + + Returns: + int: Subprocess exit code. + """ + root = _repo_root() + env = os.environ.copy() + env["PYTHONPATH"] = str(root) + return _run_cmd_stream( + [sys.executable, "-m", "sorelax_cli.main", "ask", query], + line_callback, + cwd=root, + env=env, + ) + + +def get_status() -> dict[str, Any]: + """Retrieves and parses current status, scheduler, and source health. + + Returns: + dict: Replicates Sorelax CLI status info as parsed key-value pairs. + """ + ctx = read_context() + last = ctx.get("updated_at", "never") if ctx else "never" + + pid = read_pid() + sched = f"running (PID {pid})" if (pid and _pid_running(pid)) else "stopped" + next_r = next_refresh_iso() if pid else f"manual / every {INTERVAL_HOURS}h when daemon runs" + + # Direct source health check + health = {} + for name in ("github", "linear", "slack", "notion"): + try: + result = subprocess.run( + ["coral", "source", "list"], + capture_output=True, + text=True, + timeout=15, + ) + out = (result.stdout + result.stderr).lower() + health[name.capitalize()] = name in out and result.returncode == 0 + except (subprocess.TimeoutExpired, FileNotFoundError): + health[name.capitalize()] = False + + return { + "last_refresh": last, + "next_refresh": next_r, + "scheduler": sched, + "context_path": str(CONTEXT_PATH), + "sources": health, + } + + +def get_logs(line_callback: Callable[[str], None]) -> None: + """Continuously tails the Sorelax log file in a `tail -f` style. + + Runs in the current thread and can be terminated by having the callback return False. + + Args: + line_callback: Callback receiving log lines. + """ + # First, yield existing logs + if LOG_PATH.exists(): + with LOG_PATH.open("r", encoding="utf-8") as f: + lines = f.readlines() + for line in lines: + if line_callback(line.strip()) is False: + return + + # Seek to end and tail + f.seek(0, os.SEEK_END) + while True: + line = f.readline() + if not line: + time.sleep(0.5) + continue + if line_callback(line.strip()) is False: + break + + +def read_context() -> dict[str, Any]: + """Reads and parses Sorelax's project_context.json. + + Returns: + dict: Parsed project context JSON or empty dict. + """ + if not CONTEXT_PATH.exists(): + return {} + try: + return json.loads(CONTEXT_PATH.read_text(encoding="utf-8")) + except Exception: + return {} + + +def start_scheduler(line_callback: Callable[[str], None]) -> int: + """Starts Sorelax background scheduler daemon and streams output. + + Args: + line_callback: Callback function receiving stdout/stderr lines. + + Returns: + int: Subprocess exit code. + """ + root = _repo_root() + env = os.environ.copy() + env["PYTHONPATH"] = str(root) + return _run_cmd_stream( + [sys.executable, "-m", "sorelax_cli.main", "start"], + line_callback, + cwd=root, + env=env, + ) + + +def stop_scheduler() -> int: + """Stops Sorelax background scheduler daemon. + + Returns: + int: Subprocess exit code. + """ + root = _repo_root() + env = os.environ.copy() + env["PYTHONPATH"] = str(root) + res = subprocess.run( + [sys.executable, "-m", "sorelax_cli.main", "stop"], + capture_output=True, + text=True, + cwd=root, + env=env, + ) + return res.returncode diff --git a/gui/dashboard.py b/gui/dashboard.py new file mode 100644 index 0000000..0370354 --- /dev/null +++ b/gui/dashboard.py @@ -0,0 +1,1119 @@ +"""Sorelax Developer Dashboard — PyQt6 Developer Panel. + +Exposes status monitoring, on-demand Coral ask Q&A, context explore cards, +live log tailing, and secure credentials management. + +All long-running operations execute in QThread workers — the main thread +is never blocked. +""" + +from __future__ import annotations + +import json +import os +import sys +import time +from pathlib import Path +from typing import Any + +from PyQt6.QtCore import QSize, QThread, QTimer, Qt, pyqtSignal +from PyQt6.QtGui import QColor, QFont, QPalette, QTextCursor +from PyQt6.QtWidgets import ( + QApplication, + QFormLayout, + QFrame, + QHBoxLayout, + QLabel, + QLineEdit, + QListWidget, + QListWidgetItem, + QMainWindow, + QPushButton, + QScrollArea, + QStackedWidget, + QTextEdit, + QVBoxLayout, + QWidget, +) + +# Robust import of gui/backend.py +try: + import gui.backend as backend +except ImportError: + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + import gui.backend as backend + +# --------------------------------------------------------------------------- +# Tokyo Night Dark Theme Stylesheet +# --------------------------------------------------------------------------- +TOKYO_NIGHT_STYLE = """ +/* Shell */ +QMainWindow, QWidget { + background-color: #1a1b26; + color: #a9b1d6; + font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; + font-size: 13px; +} + +/* Sidebar Navigation */ +QListWidget#sidebar { + background-color: #16161e; + border: none; + border-right: 1px solid #3b4261; + padding-top: 10px; +} + +QListWidget#sidebar::item { + padding: 12px 20px; + color: #787c99; + font-weight: bold; + border-radius: 0px; +} + +QListWidget#sidebar::item:hover { + background-color: #24283b; + color: #89ddff; +} + +QListWidget#sidebar::item:selected { + background-color: #1f2335; + color: #7aa2f7; + border-left: 4px solid #7aa2f7; +} + +/* Ask History List */ +QListWidget#historyList { + background-color: #16161e; + border: 1px solid #3b4261; + border-radius: 6px; + color: #c0caf5; +} + +QListWidget#historyList::item { + padding: 8px 12px; + border-bottom: 1px solid #24283b; +} + +QListWidget#historyList::item:hover { + background-color: #24283b; + color: #89ddff; +} + +/* Section titles */ +QLabel#sectionTitle { + font-size: 22px; + font-weight: bold; + color: #7aa2f7; + padding-bottom: 5px; +} + +QLabel#sectionSubtitle { + font-size: 13px; + color: #565f89; + margin-bottom: 20px; +} + +/* Inputs */ +QLineEdit { + background-color: #24283b; + border: 1px solid #3b4261; + border-radius: 6px; + padding: 8px 12px; + color: #c0caf5; +} + +QLineEdit:focus { + border: 1px solid #7aa2f7; +} + +/* Buttons */ +QPushButton { + background-color: #7aa2f7; + color: #1a1b26; + border: none; + border-radius: 6px; + padding: 8px 16px; + font-weight: bold; +} + +QPushButton:hover { + background-color: #89ddff; +} + +QPushButton:pressed { + background-color: #3d59a1; +} + +QPushButton:disabled { + background-color: #414868; + color: #565f89; +} + +QPushButton#toggleBtn { + background-color: #24283b; + color: #7aa2f7; + border: 1px solid #3b4261; + border-radius: 6px; + padding: 8px 16px; +} + +QPushButton#toggleBtn:hover { + background-color: #3b4261; +} + +/* Cards */ +QFrame#card { + background-color: #1f2335; + border: 1px solid #3b4261; + border-radius: 8px; + padding: 15px; +} + +/* Terminal consoles */ +QTextEdit { + background-color: #16161e; + color: #c0caf5; + border: 1px solid #3b4261; + border-radius: 8px; + padding: 10px; + font-family: 'Consolas', 'Fira Code', 'Courier New', monospace; + font-size: 12px; +} + +/* Scroll areas */ +QScrollArea { + border: none; + background-color: transparent; +} +""" + + +# --------------------------------------------------------------------------- +# QThread workers — every subprocess lives here, never in the main thread +# --------------------------------------------------------------------------- + +class RefreshThread(QThread): + """Runs backend.run_refresh() off-thread and streams output lines.""" + + line_received = pyqtSignal(str) + finished_code = pyqtSignal(int) + + def run(self) -> None: + try: + code = backend.run_refresh(self._emit) + self.finished_code.emit(code) + except Exception as exc: + self._emit(f"\n>>> ERROR: {exc}\n") + self.finished_code.emit(1) + + def _emit(self, line: str) -> None: + self.line_received.emit(line) + + +class AskThread(QThread): + """Runs backend.run_ask() off-thread and streams answer lines live.""" + + line_received = pyqtSignal(str) + finished_code = pyqtSignal(int) + + def __init__(self, query: str) -> None: + super().__init__() + self.query = query + + def run(self) -> None: + try: + code = backend.run_ask(self.query, self._emit) + self.finished_code.emit(code) + except Exception as exc: + self._emit(f"\n>>> ERROR: {exc}\n") + self.finished_code.emit(1) + + def _emit(self, line: str) -> None: + self.line_received.emit(line) + + +class SchedulerStartThread(QThread): + """Starts the background scheduler daemon off-thread.""" + + line_received = pyqtSignal(str) + finished_code = pyqtSignal(int) + + def run(self) -> None: + try: + code = backend.start_scheduler(self._emit) + self.finished_code.emit(code) + except Exception as exc: + self._emit(f"\n>>> ERROR: {exc}\n") + self.finished_code.emit(1) + + def _emit(self, line: str) -> None: + self.line_received.emit(line) + + +class SchedulerStopThread(QThread): + """Stops the background scheduler daemon off-thread.""" + + finished_code = pyqtSignal(int) + + def run(self) -> None: + try: + code = backend.stop_scheduler() + self.finished_code.emit(code) + except Exception as exc: + self.finished_code.emit(1) + + +class StatusPollThread(QThread): + """Polls backend.get_status() off-thread to avoid blocking on coral calls.""" + + status_ready = pyqtSignal(dict) + + def run(self) -> None: + try: + status = backend.get_status() + self.status_ready.emit(status) + except Exception: + self.status_ready.emit({}) + + +class SaveEnvThread(QThread): + """Calls backend.save_env() off-thread (coral source add can be slow).""" + + finished_ok = pyqtSignal() + finished_err = pyqtSignal(str) + + def __init__(self, tokens: dict[str, str]) -> None: + super().__init__() + self.tokens = tokens + + def run(self) -> None: + try: + backend.save_env(self.tokens) + self.finished_ok.emit() + except Exception as exc: + self.finished_err.emit(str(exc)) + + +class LogsThread(QThread): + """Tails the Sorelax log file continuously, emitting each new line.""" + + line_received = pyqtSignal(str) + + def __init__(self) -> None: + super().__init__() + self._active = True + + def run(self) -> None: + try: + from gui.backend import LOG_PATH # re-import for path resolution + except ImportError: + LOG_PATH = Path.home() / ".sorelax" / "project_log.jsonl" + + try: + if not LOG_PATH.exists(): + self.line_received.emit(">>> Log file not found yet. Run a refresh first.") + return + + with LOG_PATH.open("r", encoding="utf-8") as f: + # Seed with last 50 lines + existing = f.readlines() + for ln in existing[-50:]: + if not self._active: + return + self.line_received.emit(ln.strip()) + + # Tail new entries + f.seek(0, os.SEEK_END) + while self._active: + ln = f.readline() + if not ln: + time.sleep(0.4) + continue + self.line_received.emit(ln.strip()) + except Exception as exc: + self.line_received.emit(f">>> Logs stream error: {exc}") + + def stop(self) -> None: + self._active = False + + +# --------------------------------------------------------------------------- +# Main Window +# --------------------------------------------------------------------------- + +class SorelaxDashboard(QMainWindow): + """QMainWindow with a QStackedWidget + sidebar for 5-panel navigation.""" + + def __init__(self) -> None: + super().__init__() + self.setWindowTitle("Sorelax Developer Workspace") + self.setStyleSheet(TOKYO_NIGHT_STYLE) + self.resize(1060, 720) + + # Active thread references (kept to avoid GC and for lifecycle control) + self._refresh_thread: RefreshThread | None = None + self._ask_thread: AskThread | None = None + self._sched_start_thread: SchedulerStartThread | None = None + self._sched_stop_thread: SchedulerStopThread | None = None + self._logs_thread: LogsThread | None = None + self._status_thread: StatusPollThread | None = None + self._save_thread: SaveEnvThread | None = None + + # Countdown: seconds remaining until next scheduled refresh. + # Only ticks down when scheduler is running; reset on manual refresh. + self._countdown_secs: int = backend.INTERVAL_HOURS * 3600 + self._sched_running: bool = False + + # --- Central layout --- + central = QWidget() + self.setCentralWidget(central) + root_layout = QHBoxLayout(central) + root_layout.setContentsMargins(0, 0, 0, 0) + root_layout.setSpacing(0) + + # Sidebar + self.sidebar = QListWidget() + self.sidebar.setObjectName("sidebar") + self.sidebar.setFixedWidth(220) + self.sidebar.currentRowChanged.connect(self._switch_panel) + root_layout.addWidget(self.sidebar) + + # Stacked viewport + self.viewport = QStackedWidget() + root_layout.addWidget(self.viewport) + + # Populate sidebar items + nav_items = [ + ("Dashboard", "Overview & status metrics"), + ("Ask Sorelax", "On-demand source query"), + ("Context Explorer", "Sorelax knowledge database"), + ("Live Logs", "Real-time daemon streams"), + ("Settings", "Tokens & database configuration"), + ] + for name, tip in nav_items: + item = QListWidgetItem(name) + item.setToolTip(tip) + self.sidebar.addItem(item) + + # Build all panels + self._init_dashboard_panel() + self._init_ask_panel() + self._init_context_panel() + self._init_logs_panel() + self._init_settings_panel() + + # Select Dashboard by default + self.sidebar.setCurrentRow(0) + + # 1-second countdown ticker (cheap — no subprocess) + self._tick_timer = QTimer(self) + self._tick_timer.timeout.connect(self._tick_countdown) + self._tick_timer.start(1000) + + # Initial status poll (off-thread) + self._poll_status() + + # ----------------------------------------------------------------------- + # Panel switching + # ----------------------------------------------------------------------- + + def _switch_panel(self, index: int) -> None: + self.viewport.setCurrentIndex(index) + + if index == 3: # Live Logs + self._start_logs_thread() + else: + self._stop_logs_thread() + + if index == 2: # Context Explorer + self._load_context_data() + + # ----------------------------------------------------------------------- + # Panel 1 — Dashboard + # ----------------------------------------------------------------------- + + def _init_dashboard_panel(self) -> None: + panel = QWidget() + layout = QVBoxLayout(panel) + layout.setContentsMargins(30, 30, 30, 30) + + # Header + title = QLabel("System Dashboard") + title.setObjectName("sectionTitle") + subtitle = QLabel("Live connection state, scheduler controls, and manual synchronisation.") + subtitle.setObjectName("sectionSubtitle") + layout.addWidget(title) + layout.addWidget(subtitle) + + # Connection health row + health_frame = QFrame() + health_frame.setObjectName("card") + health_layout = QHBoxLayout(health_frame) + health_layout.addWidget(QLabel("Source Integrity:")) + health_layout.addSpacing(20) + + self._health_dots: dict[str, QLabel] = {} + for src in ("GitHub", "Linear", "Slack", "Notion"): + dot = QLabel(f"● {src}") + health_layout.addWidget(dot) + self._health_dots[src] = dot + + health_layout.addStretch() + layout.addWidget(health_frame) + layout.addSpacing(10) + + # Status cards row + cards_row = QHBoxLayout() + + card_a = QFrame() + card_a.setObjectName("card") + la = QVBoxLayout(card_a) + la.addWidget(QLabel("LAST SYNCHRONISATION")) + self._lbl_last_refresh = QLabel("—") + self._lbl_last_refresh.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold)) + la.addWidget(self._lbl_last_refresh) + cards_row.addWidget(card_a) + + card_b = QFrame() + card_b.setObjectName("card") + lb = QVBoxLayout(card_b) + lb.addWidget(QLabel("NEXT SCHEDULED REFRESH")) + self._lbl_countdown = QLabel("—") + self._lbl_countdown.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold)) + lb.addWidget(self._lbl_countdown) + cards_row.addWidget(card_b) + + layout.addLayout(cards_row) + layout.addSpacing(20) + + # Controls & output + ctrl_frame = QFrame() + ctrl_frame.setObjectName("card") + ctrl_layout = QVBoxLayout(ctrl_frame) + + btn_row = QHBoxLayout() + btn_row.addWidget(QLabel("Actions & Scheduler Controls:")) + btn_row.addStretch() + + self._btn_refresh = QPushButton("Refresh Now") + self._btn_refresh.clicked.connect(self._trigger_refresh) + btn_row.addWidget(self._btn_refresh) + + self._btn_scheduler = QPushButton("Start Scheduler") + self._btn_scheduler.setObjectName("toggleBtn") + self._btn_scheduler.clicked.connect(self._toggle_scheduler) + btn_row.addWidget(self._btn_scheduler) + + ctrl_layout.addLayout(btn_row) + + self._dash_console = QTextEdit() + self._dash_console.setReadOnly(True) + self._dash_console.setPlaceholderText("Console output for active refresh operations…") + ctrl_layout.addWidget(self._dash_console) + + layout.addWidget(ctrl_frame) + self.viewport.addWidget(panel) + + # -- Status polling (off-thread) ----------------------------------------- + + def _poll_status(self) -> None: + """Fire a one-shot off-thread status poll; apply results via signal.""" + if self._status_thread and self._status_thread.isRunning(): + return + self._status_thread = StatusPollThread() + self._status_thread.status_ready.connect(self._apply_status) + self._status_thread.start() + + def _apply_status(self, status: dict) -> None: + # Connection dots + sources = status.get("sources", {}) + for name, healthy in sources.items(): + if name in self._health_dots: + color = "#9ece6a" if healthy else "#f7768e" + self._health_dots[name].setText( + f"● {name}" + ) + + # Last refresh label + self._lbl_last_refresh.setText(status.get("last_refresh", "never")) + + # Scheduler button + state flag + sched = status.get("scheduler", "stopped") + self._sched_running = "running" in sched + if self._sched_running: + self._btn_scheduler.setText("Stop Scheduler") + self._btn_scheduler.setStyleSheet("background-color:#f7768e;color:#1a1b26;") + else: + self._btn_scheduler.setText("Start Scheduler") + self._btn_scheduler.setStyleSheet("") + + # -- Countdown tick (purely local arithmetic — no subprocess) ------------ + + def _tick_countdown(self) -> None: + if self._sched_running: + self._countdown_secs -= 1 + if self._countdown_secs < 0: + self._countdown_secs = backend.INTERVAL_HOURS * 3600 + h = self._countdown_secs // 3600 + m = (self._countdown_secs % 3600) // 60 + s = self._countdown_secs % 60 + self._lbl_countdown.setText(f"{h:02d}:{m:02d}:{s:02d}") + else: + self._lbl_countdown.setText("Paused") + + # -- Refresh action ------------------------------------------------------- + + def _trigger_refresh(self) -> None: + if self._refresh_thread and self._refresh_thread.isRunning(): + return + + self._dash_console.clear() + self._dash_console.append( + "Triggering manual context refresh…\n" + ) + self._btn_refresh.setEnabled(False) + + self._refresh_thread = RefreshThread() + self._refresh_thread.line_received.connect(self._dash_append) + self._refresh_thread.finished_code.connect(self._on_refresh_done) + self._refresh_thread.start() + + def _dash_append(self, line: str) -> None: + if line.startswith(">>>"): + self._dash_console.append(f"{line}") + elif any(kw in line.lower() for kw in ("error", "failed", "exception")): + self._dash_console.append(f"{line}") + elif any(kw in line.lower() for kw in ("success", "completed", "done")): + self._dash_console.append(f"{line}") + else: + self._dash_console.append(f"{line}") + self._dash_console.moveCursor(QTextCursor.MoveOperation.End) + + def _on_refresh_done(self, code: int) -> None: + self._btn_refresh.setEnabled(True) + if code == 0: + self._dash_console.append( + "\n✓ Context refresh completed!" + ) + self._countdown_secs = backend.INTERVAL_HOURS * 3600 + else: + self._dash_console.append( + f"\n✗ Refresh finished with code {code}" + ) + self._poll_status() + + # -- Scheduler toggle (off-thread) ---------------------------------------- + + def _toggle_scheduler(self) -> None: + self._btn_scheduler.setEnabled(False) + self._dash_console.clear() + + if self._sched_running: + self._dash_console.append( + "Stopping Sorelax daemon…" + ) + self._sched_stop_thread = SchedulerStopThread() + self._sched_stop_thread.finished_code.connect(self._on_sched_stop_done) + self._sched_stop_thread.start() + else: + self._dash_console.append( + "Starting Sorelax daemon…" + ) + self._sched_start_thread = SchedulerStartThread() + self._sched_start_thread.line_received.connect(self._dash_append) + self._sched_start_thread.finished_code.connect(self._on_sched_start_done) + self._sched_start_thread.start() + + def _on_sched_stop_done(self, code: int) -> None: + self._btn_scheduler.setEnabled(True) + if code == 0: + self._dash_console.append( + "✓ Scheduler stopped." + ) + else: + self._dash_console.append( + "✗ Stop command returned non-zero. Check PID." + ) + self._poll_status() + + def _on_sched_start_done(self, code: int) -> None: + self._btn_scheduler.setEnabled(True) + if code == 0: + self._dash_console.append( + "✓ Scheduler started." + ) + self._countdown_secs = backend.INTERVAL_HOURS * 3600 + else: + self._dash_console.append( + f"✗ Scheduler start finished with code {code}" + ) + self._poll_status() + + # ----------------------------------------------------------------------- + # Panel 2 — Ask + # ----------------------------------------------------------------------- + + def _init_ask_panel(self) -> None: + panel = QWidget() + layout = QHBoxLayout(panel) + layout.setContentsMargins(30, 30, 30, 30) + + # History sidebar + history_box = QVBoxLayout() + history_box.addWidget(QLabel("Q&A Session History")) + self._history_list = QListWidget() + self._history_list.setObjectName("historyList") + self._history_list.setFixedWidth(200) + self._history_list.itemClicked.connect(self._on_history_clicked) + history_box.addWidget(self._history_list) + layout.addLayout(history_box) + + # Main Q&A area + main_qa = QVBoxLayout() + + title = QLabel("Ask Sorelax") + title.setObjectName("sectionTitle") + subtitle = QLabel( + "Query cross-source knowledge graphs on demand using natural language." + ) + subtitle.setObjectName("sectionSubtitle") + main_qa.addWidget(title) + main_qa.addWidget(subtitle) + + search_row = QHBoxLayout() + self._ask_input = QLineEdit() + self._ask_input.setPlaceholderText("Ask a question about the project state…") + self._ask_input.returnPressed.connect(self._trigger_ask) + self._btn_ask = QPushButton("Submit") + self._btn_ask.clicked.connect(self._trigger_ask) + search_row.addWidget(self._ask_input) + search_row.addWidget(self._btn_ask) + main_qa.addLayout(search_row) + + self._ask_console = QTextEdit() + self._ask_console.setReadOnly(True) + self._ask_console.setPlaceholderText("Answer will stream here…") + main_qa.addWidget(self._ask_console) + + layout.addLayout(main_qa) + self.viewport.addWidget(panel) + + # Session cache: query -> accumulated answer text + self._qa_cache: dict[str, str] = {} + self._current_query: str = "" + + def _trigger_ask(self) -> None: + query = self._ask_input.text().strip() + if not query: + return + if self._ask_thread and self._ask_thread.isRunning(): + return + + self._ask_console.clear() + self._ask_console.append( + f"Querying: '{query}'…\n" + ) + self._btn_ask.setEnabled(False) + self._current_query = query + + # Add to history sidebar + if query not in self._qa_cache: + self._history_list.insertItem(0, QListWidgetItem(query)) + self._qa_cache[query] = "" + + self._ask_thread = AskThread(query) + self._ask_thread.line_received.connect(self._on_ask_line) + self._ask_thread.finished_code.connect(self._on_ask_done) + self._ask_thread.start() + + def _on_ask_line(self, line: str) -> None: + self._ask_console.append(f"{line}") + self._ask_console.moveCursor(QTextCursor.MoveOperation.End) + self._qa_cache[self._current_query] += line + "\n" + + def _on_ask_done(self, code: int) -> None: + self._btn_ask.setEnabled(True) + self._ask_input.clear() + if code != 0: + self._ask_console.append( + f"\n✗ Query failed with code {code}" + ) + + def _on_history_clicked(self, item: QListWidgetItem) -> None: + query = item.text() + cached = self._qa_cache.get(query, "No cached answer.") + self._ask_console.clear() + self._ask_console.append( + f"Session query: '{query}'\n" + ) + self._ask_console.append(cached) + + # ----------------------------------------------------------------------- + # Panel 3 — Context Explorer + # ----------------------------------------------------------------------- + + def _init_context_panel(self) -> None: + panel = QWidget() + layout = QVBoxLayout(panel) + layout.setContentsMargins(30, 30, 30, 30) + + header = QHBoxLayout() + title_box = QVBoxLayout() + title = QLabel("Context Explorer") + title.setObjectName("sectionTitle") + subtitle = QLabel("Interactive view of the Sorelax synchronised project state.") + subtitle.setObjectName("sectionSubtitle") + title_box.addWidget(title) + title_box.addWidget(subtitle) + header.addLayout(title_box) + header.addStretch() + + reload_btn = QPushButton("Reload") + reload_btn.clicked.connect(self._load_context_data) + header.addWidget(reload_btn) + layout.addLayout(header) + + self._ctx_scroll = QScrollArea() + self._ctx_scroll.setWidgetResizable(True) + self._ctx_content = QWidget() + self._ctx_layout = QVBoxLayout(self._ctx_content) + self._ctx_scroll.setWidget(self._ctx_content) + layout.addWidget(self._ctx_scroll) + + self.viewport.addWidget(panel) + + def _clear_ctx_layout(self) -> None: + """Safely remove all child widgets from the context scroll layout.""" + while self._ctx_layout.count(): + item = self._ctx_layout.takeAt(0) + if item is not None: + w = item.widget() + if w is not None: + w.deleteLater() + + def _load_context_data(self) -> None: + self._clear_ctx_layout() + ctx = backend.read_context() + + if not ctx: + placeholder = QLabel( + "Sorelax context database is empty.\n" + "Synchronise using the Dashboard refresh first." + ) + placeholder.setStyleSheet("color:#565f89;font-style:italic;padding:20px;") + placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter) + self._ctx_layout.addWidget(placeholder) + return + + # Cards grouped by common top-level context keys + card_defs = [ + ("sprint_goal", "🎯 Sprint Objective", lambda v: str(v)), + ("key_actions", "⚡ Priority Actions", lambda v: "\n".join(f"• {a}" for a in v)), + ("recent_progress", "📈 Recent Developments", lambda v: "\n".join(f"• {p}" for p in v)), + ("pr_status", "🐙 PR Status", lambda v: str(v)), + ("architecture_docs", "📂 Architecture Docs", + lambda v: "\n".join(f"• {d.get('title', str(d))}" for d in v)), + ] + + shown = 0 + for key, card_title, formatter in card_defs: + value = ctx.get(key) + if value: + try: + self._add_context_card(card_title, formatter(value)) + shown += 1 + except Exception: + pass + + # Fallback: render all top-level string/int fields as a catch-all card + if shown == 0: + known_keys = {k for k, _, _ in card_defs} + extra_lines = [] + for k, v in ctx.items(): + if k not in known_keys and isinstance(v, (str, int, float, bool)): + extra_lines.append(f"{k}: {v}") + if extra_lines: + self._add_context_card("📋 Raw Context Fields", "\n".join(extra_lines)) + else: + placeholder = QLabel("Context data present but in an unrecognised format.") + placeholder.setStyleSheet("color:#565f89;padding:20px;") + self._ctx_layout.addWidget(placeholder) + + self._ctx_layout.addStretch() + + def _add_context_card(self, title: str, content: str) -> None: + card = QFrame() + card.setObjectName("card") + card_layout = QVBoxLayout(card) + + lbl_title = QLabel(f"{title}") + lbl_title.setStyleSheet("font-size:15px;color:#7aa2f7;") + card_layout.addWidget(lbl_title) + + sep = QFrame() + sep.setFrameShape(QFrame.Shape.HLine) + sep.setStyleSheet("background-color:#3b4261;") + card_layout.addWidget(sep) + + lbl_content = QLabel(content) + lbl_content.setWordWrap(True) + lbl_content.setStyleSheet("color:#c0caf5;line-height:1.5;") + card_layout.addWidget(lbl_content) + + self._ctx_layout.addWidget(card) + + # ----------------------------------------------------------------------- + # Panel 4 — Live Logs + # ----------------------------------------------------------------------- + + def _init_logs_panel(self) -> None: + panel = QWidget() + layout = QVBoxLayout(panel) + layout.setContentsMargins(30, 30, 30, 30) + + header = QHBoxLayout() + title_box = QVBoxLayout() + title = QLabel("Live Tail Daemon Logs") + title.setObjectName("sectionTitle") + subtitle = QLabel("Real-time telemetry stream from the Sorelax daemon scheduler.") + subtitle.setObjectName("sectionSubtitle") + title_box.addWidget(title) + title_box.addWidget(subtitle) + header.addLayout(title_box) + header.addStretch() + + self._btn_logs_toggle = QPushButton("Pause") + self._btn_logs_toggle.clicked.connect(self._toggle_logs) + header.addWidget(self._btn_logs_toggle) + layout.addLayout(header) + + self._logs_terminal = QTextEdit() + self._logs_terminal.setReadOnly(True) + self._logs_terminal.setPlaceholderText("Log entries will appear here…") + layout.addWidget(self._logs_terminal) + + self.viewport.addWidget(panel) + self._logs_active = False + + def _start_logs_thread(self) -> None: + if self._logs_active: + return + + self._logs_active = True + self._logs_terminal.clear() + self._logs_terminal.append( + "Connecting to log stream…\n" + ) + self._btn_logs_toggle.setText("Pause") + self._btn_logs_toggle.setStyleSheet("") + + self._logs_thread = LogsThread() + self._logs_thread.line_received.connect(self._on_log_line) + self._logs_thread.start() + + def _stop_logs_thread(self) -> None: + if not self._logs_active: + return + self._logs_active = False + if self._logs_thread: + self._logs_thread.stop() + self._logs_thread.wait(2000) + self._logs_thread = None + self._btn_logs_toggle.setText("Resume") + self._btn_logs_toggle.setStyleSheet( + "background-color:#24283b;color:#7aa2f7;border:1px solid #3b4261;" + ) + + def _toggle_logs(self) -> None: + if self._logs_active: + self._stop_logs_thread() + self._logs_terminal.append( + "\nStream paused." + ) + else: + self._start_logs_thread() + + def _on_log_line(self, line: str) -> None: + try: + entry = json.loads(line) + ts = entry.get("timestamp", "?") + status = entry.get("status", "ok") + rows = entry.get("row_count", 0) + color = "#9ece6a" if status == "ok" else "#f7768e" + self._logs_terminal.append( + f"[{ts}] " + f"Sync: {status.upper()} · " + f"Rows: {rows}" + ) + except Exception: + self._logs_terminal.append(f"{line}") + self._logs_terminal.moveCursor(QTextCursor.MoveOperation.End) + + # ----------------------------------------------------------------------- + # Panel 5 — Settings + # ----------------------------------------------------------------------- + + def _init_settings_panel(self) -> None: + panel = QWidget() + layout = QVBoxLayout(panel) + layout.setContentsMargins(30, 30, 30, 30) + + title = QLabel("Settings") + title.setObjectName("sectionTitle") + subtitle = QLabel("Manage integration credentials and repository tokens.") + subtitle.setObjectName("sectionSubtitle") + layout.addWidget(title) + layout.addWidget(subtitle) + + form_frame = QFrame() + form_frame.setObjectName("card") + form_layout = QFormLayout(form_frame) + form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight) + form_layout.setSpacing(12) + + tokens = self._load_tokens() + + self._settings_fields: dict[str, QLineEdit] = {} + fields_def = [ + ("GITHUB_TOKEN", "GitHub Token"), + ("GITHUB_OWNER", "GitHub Owner"), + ("GITHUB_REPO", "GitHub Repo"), + ("LINEAR_API_KEY", "Linear API Key"), + ("SLACK_TOKEN", "Slack Bot Token"), + ("NOTION_TOKEN", "Notion Token"), + ("GEMINI_API_KEY", "Gemini API Key"), + ] + + for env_name, label in fields_def: + row = QHBoxLayout() + edit = QLineEdit(tokens.get(env_name, "")) + edit.setEchoMode(QLineEdit.EchoMode.Password) + row.addWidget(edit) + + vis_btn = QPushButton("Show") + vis_btn.setFixedWidth(60) + vis_btn.setStyleSheet( + "background-color:#24283b;color:#7aa2f7;" + "border:1px solid #3b4261;border-radius:4px;padding:4px;" + ) + vis_btn.clicked.connect( + lambda _checked, e=edit, b=vis_btn: self._toggle_vis(e, b) + ) + row.addWidget(vis_btn) + + form_layout.addRow(QLabel(label), row) + self._settings_fields[env_name] = edit + + layout.addWidget(form_frame) + layout.addSpacing(10) + + self._lbl_settings_status = QLabel("") + self._lbl_settings_status.setStyleSheet("font-weight:bold;font-size:13px;") + layout.addWidget(self._lbl_settings_status) + + layout.addSpacing(10) + + self._btn_save = QPushButton("Save Settings & Apply Sources") + self._btn_save.clicked.connect(self._save_settings) + layout.addWidget(self._btn_save) + + layout.addStretch() + self.viewport.addWidget(panel) + + def _load_tokens(self) -> dict[str, str]: + try: + from dotenv import dotenv_values + root = backend._repo_root() + env_path = root / ".env" + if not env_path.exists(): + env_path = Path.home() / ".sorelax" / ".env" + if env_path.exists(): + return dict(dotenv_values(str(env_path))) + except Exception: + pass + return {} + + def _toggle_vis(self, edit: QLineEdit, btn: QPushButton) -> None: + if edit.echoMode() == QLineEdit.EchoMode.Password: + edit.setEchoMode(QLineEdit.EchoMode.Normal) + btn.setText("Hide") + else: + edit.setEchoMode(QLineEdit.EchoMode.Password) + btn.setText("Show") + + def _save_settings(self) -> None: + if self._save_thread and self._save_thread.isRunning(): + return + + tokens = {name: edit.text().strip() for name, edit in self._settings_fields.items()} + self._lbl_settings_status.setText( + "Saving credentials and registering Coral sources…" + ) + self._btn_save.setEnabled(False) + + self._save_thread = SaveEnvThread(tokens) + self._save_thread.finished_ok.connect(self._on_save_ok) + self._save_thread.finished_err.connect(self._on_save_err) + self._save_thread.start() + + def _on_save_ok(self) -> None: + self._btn_save.setEnabled(True) + self._lbl_settings_status.setText( + "✓ Configuration saved and Coral sources registered!" + ) + self._poll_status() + + def _on_save_err(self, msg: str) -> None: + self._btn_save.setEnabled(True) + self._lbl_settings_status.setText( + f"✗ Failed: {msg}" + ) + + # ----------------------------------------------------------------------- + # Cleanup + # ----------------------------------------------------------------------- + + def closeEvent(self, event) -> None: # noqa: N802 + self._stop_logs_thread() + # Ask any running thread to stop (best-effort) + for attr in ( + "_refresh_thread", "_ask_thread", + "_sched_start_thread", "_sched_stop_thread", + "_status_thread", "_save_thread", + ): + thread = getattr(self, attr, None) + if thread and thread.isRunning(): + thread.wait(1000) + event.accept() + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main() -> None: + app = QApplication(sys.argv) + app.setStyle("Fusion") + + palette = QPalette() + palette.setColor(QPalette.ColorRole.Window, QColor("#1a1b26")) + palette.setColor(QPalette.ColorRole.WindowText, QColor("#a9b1d6")) + palette.setColor(QPalette.ColorRole.Base, QColor("#16161e")) + palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#1f2335")) + palette.setColor(QPalette.ColorRole.ToolTipBase, QColor("#16161e")) + palette.setColor(QPalette.ColorRole.ToolTipText, QColor("#a9b1d6")) + palette.setColor(QPalette.ColorRole.Text, QColor("#c0caf5")) + palette.setColor(QPalette.ColorRole.Button, QColor("#1f2335")) + palette.setColor(QPalette.ColorRole.ButtonText, QColor("#c0caf5")) + palette.setColor(QPalette.ColorRole.BrightText, QColor("#f7768e")) + palette.setColor(QPalette.ColorRole.Highlight, QColor("#7aa2f7")) + palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#1a1b26")) + app.setPalette(palette) + + window = SorelaxDashboard() + window.show() + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/gui/main.py b/gui/main.py new file mode 100644 index 0000000..eff800c --- /dev/null +++ b/gui/main.py @@ -0,0 +1,144 @@ +"""gui/main.py — Sorelax GUI entry point. + +Checks whether a valid `.env` is present: + - If yes → opens SorelaxDashboard directly. + - If no → opens SorelaxWizard; on wizard completion the dashboard is shown + in-process (no subprocess restart required). + +The dark Tokyo Night QApplication palette and global stylesheet are applied +here so both windows share exactly the same look. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from PyQt6.QtGui import QColor, QPalette +from PyQt6.QtWidgets import QApplication + +# --------------------------------------------------------------------------- +# Tokyo Night global palette (applied before any window is constructed) +# --------------------------------------------------------------------------- +_TN_PALETTE_ROLES: list[tuple] = [ + ("Window", "#1a1b26"), + ("WindowText", "#a9b1d6"), + ("Base", "#16161e"), + ("AlternateBase", "#1f2335"), + ("ToolTipBase", "#16161e"), + ("ToolTipText", "#a9b1d6"), + ("Text", "#c0caf5"), + ("Button", "#1f2335"), + ("ButtonText", "#c0caf5"), + ("BrightText", "#f7768e"), + ("Highlight", "#7aa2f7"), + ("HighlightedText", "#1a1b26"), +] + +# Shared base stylesheet — enough for unstyled widgets that miss the per-module +# TOKYO_NIGHT_STYLE sheets (e.g. QToolTip, QMenu). +_GLOBAL_STYLE = """ +QToolTip { + background-color: #1f2335; + color: #c0caf5; + border: 1px solid #3b4261; + border-radius: 4px; + padding: 4px 8px; +} +QMenu { + background-color: #1f2335; + color: #c0caf5; + border: 1px solid #3b4261; +} +QMenu::item:selected { + background-color: #364a82; +} +""" + + +def _build_app() -> QApplication: + """Create and configure the QApplication with the dark palette.""" + app = QApplication.instance() or QApplication(sys.argv) + app.setStyle("Fusion") + app.setApplicationName("Sorelax") + app.setApplicationDisplayName("Sorelax Developer Workspace") + + palette = QPalette() + role_map = {name: QColor(hex_) for name, hex_ in _TN_PALETTE_ROLES} + for role_name, color in role_map.items(): + role = getattr(QPalette.ColorRole, role_name) + palette.setColor(role, color) + app.setPalette(palette) + app.setStyleSheet(_GLOBAL_STYLE) + return app + + +def _env_configured() -> bool: + """Return True if a non-trivially-empty .env file is found.""" + try: + import gui.backend as _backend # noqa: F401 — just trigger path bootstrap + except ImportError: + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + import gui.backend as _backend # noqa: F401 + + from gui.backend import _repo_root + candidates = [ + _repo_root() / ".env", + Path.home() / ".sorelax" / ".env", + ] + for path in candidates: + if path.exists() and path.stat().st_size > 20: + return True + return False + + +def _open_dashboard() -> None: + """Show the main SorelaxDashboard window (import deferred for startup speed).""" + from gui.dashboard import SorelaxDashboard + win = SorelaxDashboard() + win.show() + # Keep a module-level reference so the window is not garbage-collected + _open_dashboard._instance = win # type: ignore[attr-defined] + + +def _open_wizard() -> None: + """Show the SorelaxWizard; on accept() it hands off to the dashboard.""" + from gui.wizard import SorelaxWizard + + wizard = SorelaxWizard() + + # Override accept() so the wizard opens the full dashboard instead of + # wizard.py's internal lightweight MainWindow. + def _on_accept() -> None: + # Let QWizard close itself first + from PyQt6.QtWidgets import QDialog + QDialog.accept(wizard) # bypasses SorelaxWizard.accept() + _open_dashboard() + + wizard.accepted.connect(_on_accept) + + # Disconnect the original accept override that opens the wrong window + try: + wizard.accepted.disconnect() + except TypeError: + pass + wizard.accepted.connect(_on_accept) + + wizard.show() + _open_wizard._instance = wizard # type: ignore[attr-defined] + + +def launch() -> None: + """Entry point called by the ``sorelax-gui`` console script.""" + app = _build_app() + + if _env_configured(): + _open_dashboard() + else: + _open_wizard() + + sys.exit(app.exec()) + + +if __name__ == "__main__": + launch() diff --git a/gui/wizard.py b/gui/wizard.py new file mode 100644 index 0000000..b66be9b --- /dev/null +++ b/gui/wizard.py @@ -0,0 +1,964 @@ +"""PyQt6 Setup Wizard for Sorelax. + +Provides a Tokyo Night styled 4-page wizard that: + 1. Checks system dependencies (curl, pip, coral) + 2. Collects integration tokens + 3. Runs the install pipeline in a QThread with live output + 4. Shows a "Done" page with copyable commands + +When the wizard completes, it emits ``SorelaxWizard.setup_complete`` so the +caller (gui/main.py) can open the real dashboard without restarting the process. + +NOTE: The old lightweight ``MainWindow`` class that previously lived here is +kept for import-compatibility but no longer opened by the wizard itself. +""" + +from __future__ import annotations + +import json +import os +import sys +from pathlib import Path +from typing import Any, Callable + +from PyQt6.QtCore import QSize, QThread, Qt, pyqtSignal +from PyQt6.QtGui import QColor, QFont, QPalette, QTextCursor +from PyQt6.QtWidgets import ( + QApplication, + QFormLayout, + QFrame, + QGridLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QMainWindow, + QMessageBox, + QPushButton, + QScrollArea, + QTabWidget, + QTextEdit, + QVBoxLayout, + QWidget, + QWizard, + QWizardPage, +) + +# Robust import of gui/backend.py +try: + import gui.backend as backend +except ImportError: + # If run directly from gui/ folder, insert parent to sys.path + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + import gui.backend as backend + +# Stylesheet for high-end Tokyo Night aesthetics +TOKYO_NIGHT_STYLE = """ +/* Core Shell */ +QWizard, QMainWindow, QWizardPage, QWidget { + background-color: #1a1b26; + color: #a9b1d6; + font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; +} + +QTabWidget::pane { + border: 1px solid #3b4261; + background-color: #1f2335; + border-radius: 8px; +} + +QTabBar::tab { + background-color: #16161e; + color: #787c99; + border: 1px solid #3b4261; + border-bottom: none; + border-top-left-radius: 6px; + border-top-right-radius: 6px; + padding: 10px 16px; + margin-right: 2px; +} + +QTabBar::tab:selected, QTabBar::tab:hover { + background-color: #1f2335; + color: #7aa2f7; + border-bottom: 2px solid #7aa2f7; +} + +/* Headers */ +QLabel#title { + font-size: 22px; + font-weight: bold; + color: #7aa2f7; + padding-bottom: 5px; +} + +QLabel#subtitle { + font-size: 13px; + color: #565f89; + margin-bottom: 15px; +} + +/* Forms & Inputs */ +QLineEdit { + background-color: #24283b; + border: 1px solid #3b4261; + border-radius: 6px; + padding: 8px 12px; + color: #c0caf5; + selection-background-color: #364a82; +} + +QLineEdit:focus { + border: 1px solid #7aa2f7; +} + +/* Buttons */ +QPushButton { + background-color: #7aa2f7; + color: #1a1b26; + border: none; + border-radius: 6px; + padding: 8px 16px; + font-weight: bold; + font-size: 13px; +} + +QPushButton:hover { + background-color: #89ddff; +} + +QPushButton:pressed { + background-color: #3d59a1; +} + +QPushButton:disabled { + background-color: #414868; + color: #565f89; +} + +QPushButton#copyBtn { + background-color: #24283b; + color: #7aa2f7; + border: 1px solid #3b4261; + border-radius: 4px; + padding: 4px 8px; + font-size: 11px; +} + +QPushButton#copyBtn:hover { + background-color: #3b4261; + color: #89ddff; +} + +/* Cards & Frames */ +QFrame#card { + background-color: #1f2335; + border: 1px solid #3b4261; + border-radius: 8px; + padding: 12px; +} + +QFrame#terminalFrame { + border: 1px solid #414868; + border-radius: 8px; + background-color: #16161e; +} + +/* Scroll Area */ +QScrollArea { + border: none; + background-color: transparent; +} + +QScrollBar:vertical { + border: none; + background-color: #16161e; + width: 10px; + margin: 0px; +} + +QScrollBar::handle:vertical { + background-color: #414868; + min-height: 20px; + border-radius: 5px; +} + +QScrollBar::handle:vertical:hover { + background-color: #7aa2f7; +} + +/* QTextEdit Console */ +QTextEdit { + background-color: #16161e; + color: #a9b1d6; + border: none; + font-family: 'Consolas', 'Fira Code', 'Courier New', monospace; + font-size: 12px; +} +""" + + +class InstallThread(QThread): + """Runs backend.run_install() off-thread and streams output lines.""" + + line_received = pyqtSignal(str) + finished_success = pyqtSignal(bool) + + def __init__(self, tokens: dict[str, str]) -> None: + super().__init__() + self.tokens = tokens + + def run(self) -> None: + try: + success = backend.run_install(self.tokens, self._emit) + self.finished_success.emit(success) + except Exception as exc: + self._emit(f"\n>>> CRITICAL ERROR in install thread: {exc}\n") + self.finished_success.emit(False) + + def _emit(self, line: str) -> None: + self.line_received.emit(line) + + +class RefreshThread(QThread): + """Runs backend.run_refresh() off-thread and streams output lines.""" + + line_received = pyqtSignal(str) + finished_code = pyqtSignal(int) + + def run(self) -> None: + try: + code = backend.run_refresh(self._emit) + self.finished_code.emit(code) + except Exception as exc: + self._emit(f"\n>>> ERROR: {exc}\n") + self.finished_code.emit(1) + + def _emit(self, line: str) -> None: + self.line_received.emit(line) + + +class AskThread(QThread): + """Runs backend.run_ask() off-thread and streams answer lines live.""" + + line_received = pyqtSignal(str) + finished_code = pyqtSignal(int) + + def __init__(self, query: str) -> None: + super().__init__() + self.query = query + + def run(self) -> None: + try: + code = backend.run_ask(self.query, self._emit) + self.finished_code.emit(code) + except Exception as exc: + self._emit(f"\n>>> ERROR: {exc}\n") + self.finished_code.emit(1) + + def _emit(self, line: str) -> None: + self.line_received.emit(line) + + +class SchedulerStartThread(QThread): + """Starts the Sorelax daemon off-thread.""" + + line_received = pyqtSignal(str) + finished_code = pyqtSignal(int) + + def run(self) -> None: + try: + code = backend.start_scheduler(self._emit) + self.finished_code.emit(code) + except Exception as exc: + self._emit(f"\n>>> ERROR: {exc}\n") + self.finished_code.emit(1) + + def _emit(self, line: str) -> None: + self.line_received.emit(line) + + +class SchedulerStopThread(QThread): + """Stops the Sorelax daemon off-thread.""" + + finished_code = pyqtSignal(int) + + def run(self) -> None: + try: + code = backend.stop_scheduler() + self.finished_code.emit(code) + except Exception: + self.finished_code.emit(1) + + +class DependencyPage(QWizardPage): + """Page 1: Check dependencies and block unless curl, pip, and coral are installed.""" + + def __init__(self): + super().__init__() + self.curl_ok = False + self.pip_ok = False + self.coral_ok = False + + self.setTitle("Sorelax Installation Wizard") + + layout = QVBoxLayout(self) + + # Header + title = QLabel("System Dependency Check") + title.setObjectName("title") + subtitle = QLabel("Checking for required tools on your system path.") + subtitle.setObjectName("subtitle") + layout.addWidget(title) + layout.addWidget(subtitle) + + # Status Cards Container + self.cards_layout = QVBoxLayout() + layout.addLayout(self.cards_layout) + + # Coral Install Box (Only shown if Coral is missing) + self.install_frame = QFrame() + self.install_frame.setObjectName("card") + self.install_frame.setVisible(False) + inst_layout = QVBoxLayout(self.install_frame) + + inst_label = QLabel("Coral SQL is missing! Install it using the command below:") + inst_label.setStyleSheet("color: #bb9af3;") + inst_layout.addWidget(inst_label) + + cmd_row = QHBoxLayout() + self.cmd_line = QLineEdit("curl -fsSL https://withcoral.com/install.sh | sh") + self.cmd_line.setReadOnly(True) + + copy_btn = QPushButton("Copy") + copy_btn.setObjectName("copyBtn") + copy_btn.clicked.connect(self.copy_install_command) + + cmd_row.addWidget(self.cmd_line) + cmd_row.addWidget(copy_btn) + inst_layout.addLayout(cmd_row) + layout.addWidget(self.install_frame) + + # Bottom space + layout.addStretch() + + # Retry button + self.retry_btn = QPushButton("Re-check Dependencies") + self.retry_btn.clicked.connect(self.run_checks) + layout.addWidget(self.retry_btn) + + def initializePage(self): + self.run_checks() + + def run_checks(self) -> None: + # Safely remove all child widgets (guard against spacer items) + while self.cards_layout.count(): + item = self.cards_layout.takeAt(0) + if item is not None: + w = item.widget() + if w is not None: + w.deleteLater() + + deps = backend.check_dependencies() + + self.curl_ok = deps.get("curl", {}).get("installed", False) + self.pip_ok = deps.get("pip", {}).get("installed", False) + self.coral_ok = deps.get("coral", {}).get("installed", False) + + # Render labels for each dependency + self.add_dep_row("curl (HTTP Client)", self.curl_ok, deps.get("curl", {}).get("version")) + self.add_dep_row("pip (Python Package Manager)", self.pip_ok, deps.get("pip", {}).get("version")) + self.add_dep_row("coral (Coral SQL engine)", self.coral_ok, deps.get("coral", {}).get("version")) + + # Toggle install instructions frame + self.install_frame.setVisible(not self.coral_ok) + + # Notify wizard to refresh Next button status + self.completeChanged.emit() + + def add_dep_row(self, name: str, success: bool, version: str | None): + row = QFrame() + row.setObjectName("card") + row_layout = QHBoxLayout(row) + + indicator = QLabel("✓" if success else "✗") + indicator.setFont(QFont("Segoe UI", 16, QFont.Weight.Bold)) + indicator.setStyleSheet("color: #9ece6a;" if success else "color: #f7768e;") + row_layout.addWidget(indicator) + + details = QVBoxLayout() + lbl_name = QLabel(f"{name}") + lbl_name.setStyleSheet("color: #c0caf5;") + + version_str = version if version else "Not installed" + lbl_version = QLabel(version_str) + lbl_version.setStyleSheet("color: #565f89; font-size: 11px;") + + details.addWidget(lbl_name) + details.addWidget(lbl_version) + row_layout.addLayout(details) + row_layout.addStretch() + + self.cards_layout.addWidget(row) + + def isComplete(self) -> bool: + return self.curl_ok and self.pip_ok and self.coral_ok + + def copy_install_command(self): + clipboard = QApplication.clipboard() + clipboard.setText(self.cmd_line.text()) + QMessageBox.information(self, "Copied", "Coral installation command copied to clipboard!") + + +class TokenPage(QWizardPage): + """Page 2: Collect all Sorelax integration credentials.""" + + def __init__(self): + super().__init__() + self.setTitle("Sorelax Credentials Configuration") + + layout = QVBoxLayout(self) + + title = QLabel("API & Source Tokens") + title.setObjectName("title") + subtitle = QLabel("Configure Sorelax environment credentials. All fields are required.") + subtitle.setObjectName("subtitle") + layout.addWidget(title) + layout.addWidget(subtitle) + + form_frame = QFrame() + form_frame.setObjectName("card") + form_layout = QFormLayout(form_frame) + form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight) + + # Form fields + self.fields = {} + fields_def = [ + ("github_token", "GitHub Token"), + ("github_owner", "GitHub Owner"), + ("github_repo", "GitHub Repo Name"), + ("linear_api_key", "Linear API Key"), + ("slack_token", "Slack Bot Token"), + ("notion_token", "Notion Integration Token"), + ("gemini_api_key", "Gemini API Key"), + ] + + for internal_name, label_text in fields_def: + edit = QLineEdit() + edit.setEchoMode(QLineEdit.EchoMode.Password) + form_layout.addRow(QLabel(label_text), edit) + self.fields[internal_name] = edit + self.registerField(internal_name, edit) + + layout.addWidget(form_frame) + layout.addStretch() + + def validatePage(self) -> bool: + has_empty = False + for name, edit in self.fields.items(): + if not edit.text().strip(): + edit.setStyleSheet("border: 1px solid #f7768e; background-color: #2e243b;") + has_empty = True + else: + edit.setStyleSheet("") + + if has_empty: + QMessageBox.warning( + self, + "Required Fields", + "Please fill in all Sorelax configuration fields before starting installation.", + ) + return False + return True + + +class ProgressPage(QWizardPage): + """Page 3: Run the install thread while displaying active console output.""" + + def __init__(self): + super().__init__() + self.setTitle("Installing Sorelax Components") + self.is_running = False + self.install_success = False + + layout = QVBoxLayout(self) + + title = QLabel("System Configuration Progress") + title.setObjectName("title") + subtitle = QLabel("Executing Python setup, writing local .env database, registering Hermes skills...") + subtitle.setObjectName("subtitle") + layout.addWidget(title) + layout.addWidget(subtitle) + + # Monospace terminal log output + term_frame = QFrame() + term_frame.setObjectName("terminalFrame") + term_layout = QVBoxLayout(term_frame) + term_layout.setContentsMargins(1, 1, 1, 1) + + self.terminal = QTextEdit() + self.terminal.setReadOnly(True) + term_layout.addWidget(self.terminal) + layout.addWidget(term_frame) + + layout.addStretch() + + def initializePage(self): + if self.is_running: + return + + self.is_running = True + self.install_success = False + self.terminal.clear() + + # Block user navigation during install + self.wizard().button(QWizard.WizardButton.BackButton).setEnabled(False) + self.wizard().button(QWizard.WizardButton.NextButton).setEnabled(False) + + # Gather token dict from previous page fields + tokens = { + "GITHUB_TOKEN": self.field("github_token"), + "GITHUB_OWNER": self.field("github_owner"), + "GITHUB_REPO": self.field("github_repo"), + "LINEAR_API_KEY": self.field("linear_api_key"), + "SLACK_TOKEN": self.field("slack_token"), + "NOTION_TOKEN": self.field("notion_token"), + "GEMINI_API_KEY": self.field("gemini_api_key"), + } + + # Spawn installer worker thread + self.thread = InstallThread(tokens) + self.thread.line_received.connect(self.append_terminal_line) + self.thread.finished_success.connect(self.on_install_finish) + self.thread.start() + + def append_terminal_line(self, line: str): + # Color coding logic for elegant output styling + if line.startswith(">>>"): + # Step header lines -> cyan/blue + self.terminal.append(f"{line}") + elif "error" in line.lower() or "failed" in line.lower() or "✗" in line.lower(): + # Error lines -> red + self.terminal.append(f"{line}") + elif "success" in line.lower() or "✓" in line.lower() or "completed" in line.lower(): + # Success lines -> green + self.terminal.append(f"{line}") + else: + # Regular output -> neutral gray-blue + self.terminal.append(f"{line}") + + self.terminal.moveCursor(QTextCursor.MoveOperation.End) + + def on_install_finish(self, success: bool): + self.is_running = False + self.install_success = success + + # Enable Back button so user can fix credentials if failure occurs + self.wizard().button(QWizard.WizardButton.BackButton).setEnabled(True) + + if success: + # Enable Next page button + self.wizard().button(QWizard.WizardButton.NextButton).setEnabled(True) + self.terminal.append( + "\n✓ Sorelax setup pipeline completed successfully! Click Next." + ) + else: + self.terminal.append( + "\n✗ Setup pipeline failed. Click Back to check entered credentials." + ) + QMessageBox.critical( + self, + "Installation Failed", + "Sorelax setup did not complete successfully. Check the terminal logs for details.", + ) + + self.terminal.moveCursor(QTextCursor.MoveOperation.End) + self.completeChanged.emit() + + def isComplete(self) -> bool: + return not self.is_running and self.install_success + + +class DonePage(QWizardPage): + """Page 4: Done page displaying copyable terminal actions for manual execution.""" + + def __init__(self): + super().__init__() + self.setTitle("Sorelax Setup Completed") + + layout = QVBoxLayout(self) + + title = QLabel("Initialization Completed Successfully!") + title.setObjectName("title") + title.setStyleSheet("color: #9ece6a;") + subtitle = QLabel("Your environment is configured. Use the following Hermes commands to start scheduling:") + subtitle.setObjectName("subtitle") + layout.addWidget(title) + layout.addWidget(subtitle) + + # Instructions panel + inst_frame = QFrame() + inst_frame.setObjectName("card") + inst_layout = QVBoxLayout(inst_frame) + + instructions = [ + ("1. Select & configure LLM provider for Hermes scheduler:", "hermes model"), + ("2. Launch active background gateway scheduler:", "hermes gateway"), + ("3. Perform context synchronization manually:", "sorelax refresh"), + ] + + for title_text, cmd_text in instructions: + inst_layout.addWidget(QLabel(f"{title_text}")) + + row = QHBoxLayout() + cmd_line = QLineEdit(cmd_text) + cmd_line.setReadOnly(True) + + copy_btn = QPushButton("Copy") + copy_btn.setObjectName("copyBtn") + # Safe binding with lambda capturing current cmd_text + copy_btn.clicked.connect(lambda checked, text=cmd_text: self.copy_text(text)) + + row.addWidget(cmd_line) + row.addWidget(copy_btn) + inst_layout.addLayout(row) + inst_layout.addSpacing(5) + + layout.addWidget(inst_frame) + layout.addStretch() + + def copy_text(self, text: str): + clipboard = QApplication.clipboard() + clipboard.setText(text) + QMessageBox.information(self, "Copied", f"Command copied to clipboard: '{text}'") + + +class SorelaxWizard(QWizard): + """4-page setup wizard with Tokyo Night styling. + + On completion the wizard emits ``setup_complete`` instead of opening any + window itself. The caller (gui/main.py) connects that signal to open the + real ``SorelaxDashboard``. + """ + + # Emitted after the user clicks Finish on the Done page. + setup_complete = pyqtSignal() + + def __init__(self) -> None: + super().__init__() + self.setWindowTitle("Sorelax Setup & Install Wizard") + self.setStyleSheet(TOKYO_NIGHT_STYLE) + self.setWizardStyle(QWizard.WizardStyle.ModernStyle) + self.setOption(QWizard.WizardOption.NoCancelButton, True) + self.setFixedSize(650, 520) + + self.dep_page = DependencyPage() + self.token_page = TokenPage() + self.progress_page = ProgressPage() + self.done_page = DonePage() + + self.addPage(self.dep_page) + self.addPage(self.token_page) + self.addPage(self.progress_page) + self.addPage(self.done_page) + + def accept(self) -> None: + """Emit setup_complete then close — the caller opens the dashboard.""" + super().accept() + self.setup_complete.emit() + + +class MainWindow(QMainWindow): + """Main dashboard application displayed once Sorelax is configured.""" + + def __init__(self): + super().__init__() + self.setWindowTitle("Sorelax Developer Dashboard") + self.setStyleSheet(TOKYO_NIGHT_STYLE) + self.resize(800, 600) + + self.refresh_thread = None + self.ask_thread = None + + # Base Widget + central = QWidget() + self.setCentralWidget(central) + layout = QVBoxLayout(central) + + # Header Row + header = QHBoxLayout() + title = QLabel("Sorelax Context Manager") + title.setStyleSheet("font-size: 24px; font-weight: bold; color: #7aa2f7;") + header.addWidget(title) + + self.status_badge = QLabel("CONFIGURED") + self.status_badge.setStyleSheet( + "background-color: #1f2335; border: 1px solid #9ece6a; " + "color: #9ece6a; border-radius: 4px; padding: 4px 8px; font-weight: bold;" + ) + header.addWidget(self.status_badge) + header.addStretch() + layout.addLayout(header) + + # Tabs Layout + self.tabs = QTabWidget() + layout.addWidget(self.tabs) + + # Render internal tabs + self.init_status_tab() + self.init_ask_tab() + self.init_logs_tab() + + self.update_status_metrics() + + def init_status_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + # Health grid card container + grid_frame = QFrame() + grid_frame.setObjectName("card") + grid = QGridLayout(grid_frame) + + self.lbl_last_ref = QLabel("Last Context Refresh: Loading...") + self.lbl_sched_status = QLabel("Scheduler Process: Loading...") + self.lbl_context_path = QLabel("Context Path: Loading...") + grid.addWidget(self.lbl_last_ref, 0, 0) + grid.addWidget(self.lbl_sched_status, 1, 0) + grid.addWidget(self.lbl_context_path, 2, 0) + + layout.addWidget(grid_frame) + + # Sources status container + sources_frame = QFrame() + sources_frame.setObjectName("card") + src_layout = QVBoxLayout(sources_frame) + src_layout.addWidget(QLabel("Integration Source Health:")) + + self.sources_health_lbl = QLabel("Fetching source health...") + self.sources_health_lbl.setStyleSheet("color: #787c99; padding-left: 10px;") + src_layout.addWidget(self.sources_health_lbl) + layout.addWidget(sources_frame) + + # Interactive controls frame + btn_layout = QHBoxLayout() + refresh_btn = QPushButton("Refresh Context Now") + refresh_btn.clicked.connect(self.trigger_refresh) + + start_sched_btn = QPushButton("Start Background Scheduler") + start_sched_btn.clicked.connect(self.trigger_start_scheduler) + + stop_sched_btn = QPushButton("Stop Background Scheduler") + stop_sched_btn.clicked.connect(self.trigger_stop_scheduler) + + btn_layout.addWidget(refresh_btn) + btn_layout.addWidget(start_sched_btn) + btn_layout.addWidget(stop_sched_btn) + layout.addLayout(btn_layout) + + # Output console for status tab + self.status_console = QTextEdit() + self.status_console.setReadOnly(True) + self.status_console.setPlaceholderText("Subprocess tasks logs will output here...") + layout.addWidget(self.status_console) + + self.tabs.addTab(tab, "Overview & Status") + + def init_ask_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + layout.addWidget(QLabel("Ask Sorelax Q&A")) + layout.addWidget(QLabel("Query your connected sources (GitHub, Slack, Notion, Linear) via on-demand Coral SQL:")) + + row = QHBoxLayout() + self.ask_input = QLineEdit() + self.ask_input.setPlaceholderText("e.g. what changes were made in auth?") + self.ask_input.returnPressed.connect(self.trigger_ask) + + self.ask_btn = QPushButton("Ask Sorelax") + self.ask_btn.clicked.connect(self.trigger_ask) + + row.addWidget(self.ask_input) + row.addWidget(self.ask_btn) + layout.addLayout(row) + + self.ask_console = QTextEdit() + self.ask_console.setReadOnly(True) + layout.addWidget(self.ask_console) + + self.tabs.addTab(tab, "Interactive Ask (Q&A)") + + def init_logs_tab(self): + tab = QWidget() + layout = QVBoxLayout(tab) + + row = QHBoxLayout() + row.addWidget(QLabel("Database Action Logs")) + row.addStretch() + + refresh_logs_btn = QPushButton("Reload Logs") + refresh_logs_btn.setObjectName("copyBtn") + refresh_logs_btn.clicked.connect(self.load_logs) + row.addWidget(refresh_logs_btn) + layout.addLayout(row) + + self.logs_terminal = QTextEdit() + self.logs_terminal.setReadOnly(True) + layout.addWidget(self.logs_terminal) + + self.tabs.addTab(tab, "Action Logs") + self.load_logs() + + def update_status_metrics(self): + try: + status = backend.get_status() + self.lbl_last_ref.setText(f"Last Context Refresh: {status.get('last_refresh', 'never')}") + self.lbl_sched_status.setText(f"Scheduler Process: {status.get('scheduler', 'stopped')}") + self.lbl_context_path.setText(f"Context Path: {status.get('context_path', 'unknown')}") + + # Parse and render sources health nicely + sources = status.get("sources", {}) + src_text = [] + for src_name, healthy in sources.items(): + indicator = "✓ Active" if healthy else "✗ Inactive" + src_text.append(f"{src_name}: {indicator}") + + self.sources_health_lbl.setText(" · ".join(src_text)) + except Exception as exc: + self.sources_health_lbl.setText(f"Error fetching status: {exc}") + + def load_logs(self): + self.logs_terminal.clear() + try: + # Safely fetch log entries + from agent.refresh import load_logs + logs = load_logs(25) + if not logs: + self.logs_terminal.append("No logs found in Sorelax log database.") + return + for entry in logs: + ts = entry.get("timestamp", "unknown") + status = entry.get("status", "ok") + rows = entry.get("row_count", 0) + status_color = "#9ece6a" if status == "ok" else "#f7768e" + + self.logs_terminal.append( + f"[{ts}] " + f"Refresh status: {status.upper()} · " + f"Parsed Rows: {rows}" + ) + except Exception as exc: + self.logs_terminal.append(f"Failed to load logs: {exc}") + + def trigger_refresh(self): + if self.refresh_thread and self.refresh_thread.isRunning(): + return + + self.status_console.clear() + self.status_console.append("Starting context refresh process in background...\n") + + self.refresh_thread = RefreshThread() + self.refresh_thread.line_received.connect(self.append_status_line) + self.refresh_thread.finished_code.connect(self.on_refresh_finished) + self.refresh_thread.start() + + def append_status_line(self, line: str): + if line.startswith(">>>"): + self.status_console.append(f"{line}") + elif "error" in line.lower() or "failed" in line.lower(): + self.status_console.append(f"{line}") + else: + self.status_console.append(f"{line}") + + self.status_console.moveCursor(QTextCursor.MoveOperation.End) + + def on_refresh_finished(self, code: int): + if code == 0: + self.status_console.append("\n✓ Context refresh finished successfully!") + else: + self.status_console.append(f"\n✗ Context refresh finished with code {code}") + self.update_status_metrics() + self.load_logs() + + def trigger_start_scheduler(self) -> None: + self.status_console.clear() + self.status_console.append( + "Starting Sorelax scheduler daemon…\n" + ) + # Stop first (off-thread), then start — chained via signals + self._sched_prestop = SchedulerStopThread() + self._sched_prestop.finished_code.connect(self._on_prestop_done) + self._sched_prestop.start() + + def _on_prestop_done(self, _code: int) -> None: + """Callback after pre-stop; now launch the scheduler.""" + self._sched_start = SchedulerStartThread() + self._sched_start.line_received.connect(self.append_status_line) + self._sched_start.finished_code.connect(self._on_sched_start_done) + self._sched_start.start() + + def _on_sched_start_done(self, code: int) -> None: + if code == 0: + self.status_console.append( + "✓ Scheduler started." + ) + else: + self.status_console.append( + f"✗ Scheduler start returned code {code}" + ) + self.update_status_metrics() + + def trigger_stop_scheduler(self) -> None: + self.status_console.clear() + self.status_console.append( + "Stopping Sorelax scheduler daemon…\n" + ) + self._sched_stop = SchedulerStopThread() + self._sched_stop.finished_code.connect(self._on_sched_stop_done) + self._sched_stop.start() + + def _on_sched_stop_done(self, code: int) -> None: + if code == 0: + self.status_console.append( + "✓ Scheduler stopped." + ) + else: + self.status_console.append( + "✗ Stop returned non-zero. Check PID file." + ) + self.update_status_metrics() + + def trigger_ask(self): + query = self.ask_input.text().strip() + if not query: + return + + if self.ask_thread and self.ask_thread.isRunning(): + return + + self.ask_console.clear() + self.ask_console.append(f"Searching local knowledge & Q&A for: '{query}'...\n") + + self.ask_thread = AskThread(query) + self.ask_thread.line_received.connect(self.append_ask_line) + self.ask_thread.finished_code.connect(self.on_ask_finished) + self.ask_thread.start() + + def append_ask_line(self, line: str): + self.ask_console.append(f"{line}") + self.ask_console.moveCursor(QTextCursor.MoveOperation.End) + + def on_ask_finished(self, code: int): + if code != 0: + self.ask_console.append(f"\n✗ Ask query completed with code {code}") + self.ask_console.moveCursor(QTextCursor.MoveOperation.End) + + +def main() -> None: + """Standalone entry point — delegates to gui.main.launch for consistency.""" + from gui.main import launch + launch() + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index f7d9095..76c738b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,9 +19,11 @@ dependencies = [ [project.scripts] sorelax = "sorelax_cli.main:main" +sorelax-website = "sorelax_cli.website:main" +sorelax-gui = "gui.main:launch" [tool.setuptools] -packages = ["agent", "sorelax_cli"] +packages = ["agent", "sorelax_cli", "gui"] [tool.setuptools.package-dir] sorelax_cli = "cli" diff --git a/website/dist/assets/index-BRByxZiy.css b/website/dist/assets/index-BRByxZiy.css deleted file mode 100644 index 1c2d885..0000000 --- a/website/dist/assets/index-BRByxZiy.css +++ /dev/null @@ -1 +0,0 @@ -*,: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:Inter,system-ui,sans-serif;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:JetBrains Mono,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}:root{--background: 0 0% 4%;--foreground: 0 0% 100%;--card: 0 0% 7%;--card-foreground: 0 0% 100%;--popover: 0 0% 7%;--popover-foreground: 0 0% 100%;--primary: 0 0% 100%;--primary-foreground: 0 0% 0%;--secondary: 0 0% 7%;--secondary-foreground: 0 0% 53%;--muted: 0 0% 7%;--muted-foreground: 0 0% 33%;--accent: 217 91% 60%;--accent-foreground: 0 0% 100%;--destructive: 0 84% 60%;--destructive-foreground: 0 0% 98%;--border: 0 0% 12%;--input: 0 0% 12%;--ring: 217 91% 60%;--radius: 8px}html{scroll-behavior:smooth}body{font-family:Inter,system-ui,sans-serif;background:#0a0a0a;color:#fff;margin:0;padding:0;overflow-x:hidden;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-size:16px;line-height:1.6}::-moz-selection{background:#3b82f64d;color:#fff}::selection{background:#3b82f64d;color:#fff}::-webkit-scrollbar{width:8px}::-webkit-scrollbar-track{background:#0a0a0a}::-webkit-scrollbar-thumb{background:#1f1f1f;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#2a2a2a}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.static{position:static}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-12{right:3rem}.right-3{right:.75rem}.top-0{top:0}.top-3{top:.75rem}.top-4{top:1rem}.top-\[72px\]{top:72px}.z-10{z-index:10}.z-50{z-index:50}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.mb-14{margin-bottom:3.5rem}.mb-16{margin-bottom:4rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-0\.5{margin-left:.125rem}.ml-2{margin-left:.5rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1\.5{height:.375rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-6{height:1.5rem}.max-h-\[calc\(100vh-100px\)\]{max-height:calc(100vh - 100px)}.min-h-\[400px\]{min-height:400px}.min-h-\[calc\(100vh-56px\)\]{min-height:calc(100vh - 56px)}.min-h-screen{min-height:100vh}.w-1\.5{width:.375rem}.w-16{width:4rem}.w-2{width:.5rem}.w-3{width:.75rem}.w-\[240px\]{width:240px}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-3xl{max-width:48rem}.max-w-\[1000px\]{max-width:1000px}.max-w-\[1200px\]{max-width:1200px}.max-w-\[520px\]{max-width:520px}.max-w-\[560px\]{max-width:560px}.max-w-\[800px\]{max-width:800px}.max-w-command{max-width:480px}.max-w-content{max-width:680px}.max-w-lg{max-width:32rem}.max-w-terminal{max-width:680px}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x: -50%;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))}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.scroll-mt-\[88px\]{scroll-margin-top:88px}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,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-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-20{gap:5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.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))}.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{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-badge{border-radius:4px}.rounded-button{border-radius:6px}.rounded-card{border-radius:8px}.rounded-full{border-radius:9999px}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-t{border-top-width:1px}.border-accent\/30{border-color:#3b82f64d}.border-border{--tw-border-opacity: 1;border-color:rgb(31 31 31 / var(--tw-border-opacity, 1))}.border-yellow-500\/30{border-color:#eab3084d}.border-l-accent{--tw-border-opacity: 1;border-left-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-l-transparent{border-left-color:transparent}.bg-\[\#22c55e\]{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-\[\#ef4444\]{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-\[\#f59e0b\]{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-accent{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-accent\/5{background-color:#3b82f60d}.bg-background{--tw-bg-opacity: 1;background-color:rgb(10 10 10 / var(--tw-bg-opacity, 1))}.bg-border{--tw-bg-opacity: 1;background-color:rgb(31 31 31 / var(--tw-bg-opacity, 1))}.bg-code-bg{--tw-bg-opacity: 1;background-color:rgb(13 13 13 / var(--tw-bg-opacity, 1))}.bg-surface{--tw-bg-opacity: 1;background-color:rgb(17 17 17 / var(--tw-bg-opacity, 1))}.bg-surface\/30{background-color:#1111114d}.bg-surface\/50{background-color:#11111180}.bg-surface\/80{background-color:#111c}.bg-text-secondary{--tw-bg-opacity: 1;background-color:rgb(136 136 136 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-500\/5{background-color:#eab3080d}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.from-accent\/5{--tw-gradient-from: rgb(59 130 246 / .05) var(--tw-gradient-from-position);--tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-transparent{--tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), transparent var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-transparent{--tw-gradient-to: transparent var(--tw-gradient-to-position)}.p-2{padding:.5rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.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-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-20{padding-top:5rem;padding-bottom:5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pr-4{padding-right:1rem}.text-left{text-align:left}.text-center{text-align:center}.align-middle{vertical-align:middle}.font-mono{font-family:JetBrains Mono,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-5xl{font-size:3rem;line-height:1}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[15px\]{font-size:15px}.text-base{font-size:1rem;line-height:1.5rem}.text-hero-mobile{font-size:36px;line-height:1.1;letter-spacing:-.02em;font-weight:700}.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}.leading-6{line-height:1.5rem}.leading-relaxed{line-height:1.625}.leading-tight{line-height:1.25}.tracking-\[0\.12em\]{letter-spacing:.12em}.tracking-\[0\.15em\]{letter-spacing:.15em}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#333333\]{--tw-text-opacity: 1;color:rgb(51 51 51 / var(--tw-text-opacity, 1))}.text-accent{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-border{--tw-text-opacity: 1;color:rgb(31 31 31 / var(--tw-text-opacity, 1))}.text-orange-400{--tw-text-opacity: 1;color:rgb(251 146 60 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-success{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-text-primary{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-text-secondary{--tw-text-opacity: 1;color:rgb(136 136 136 / var(--tw-text-opacity, 1))}.text-text-tertiary{--tw-text-opacity: 1;color:rgb(85 85 85 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.opacity-0{opacity:0}.opacity-\[0\.03\]{opacity:.03}.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{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;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-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}@keyframes enter{0%{opacity:var(--tw-enter-opacity, 1);transform:translate3d(var(--tw-enter-translate-x, 0),var(--tw-enter-translate-y, 0),0) scale3d(var(--tw-enter-scale, 1),var(--tw-enter-scale, 1),var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity, 1);transform:translate3d(var(--tw-exit-translate-x, 0),var(--tw-exit-translate-y, 0),0) scale3d(var(--tw-exit-scale, 1),var(--tw-exit-scale, 1),var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))}}.duration-150{animation-duration:.15s}.duration-200{animation-duration:.2s}.hover\:border-\[\#2a2a2a\]:hover{--tw-border-opacity: 1;border-color:rgb(42 42 42 / var(--tw-border-opacity, 1))}.hover\:border-text-tertiary:hover{--tw-border-opacity: 1;border-color:rgb(85 85 85 / var(--tw-border-opacity, 1))}.hover\:bg-\[\#e5e5e5\]:hover{--tw-bg-opacity: 1;background-color:rgb(229 229 229 / var(--tw-bg-opacity, 1))}.hover\:text-text-primary:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:text-text-secondary:hover{--tw-text-opacity: 1;color:rgb(136 136 136 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:opacity-80:hover{opacity:.8}.focus\:border-text-tertiary:focus{--tw-border-opacity: 1;border-color:rgb(85 85 85 / var(--tw-border-opacity, 1))}.focus\:opacity-100:focus{opacity:1}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.group:hover .group-hover\:text-text-primary{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:opacity-100{opacity:1}@media(min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}}@media(min-width:768px){.md\:block{display:block}.md\:hidden{display:none}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:text-3xl{font-size:1.875rem;line-height:2.25rem}.md\:text-hero-tablet{font-size:40px;line-height:1.1;letter-spacing:-.02em;font-weight:700}}@media(min-width:1024px){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:1280px){.xl\:text-hero{font-size:72px;line-height:1.05;letter-spacing:-.03em;font-weight:700}} diff --git a/website/dist/assets/index-DLKHxB3n.js b/website/dist/assets/index-DLKHxB3n.js deleted file mode 100644 index 3e79d46..0000000 --- a/website/dist/assets/index-DLKHxB3n.js +++ /dev/null @@ -1,88 +0,0 @@ -(function(){const i=document.createElement("link").relList;if(i&&i.supports&&i.supports("modulepreload"))return;for(const c of document.querySelectorAll('link[rel="modulepreload"]'))r(c);new MutationObserver(c=>{for(const d of c)if(d.type==="childList")for(const f of d.addedNodes)f.tagName==="LINK"&&f.rel==="modulepreload"&&r(f)}).observe(document,{childList:!0,subtree:!0});function s(c){const d={};return c.integrity&&(d.integrity=c.integrity),c.referrerPolicy&&(d.referrerPolicy=c.referrerPolicy),c.crossOrigin==="use-credentials"?d.credentials="include":c.crossOrigin==="anonymous"?d.credentials="omit":d.credentials="same-origin",d}function r(c){if(c.ep)return;c.ep=!0;const d=s(c);fetch(c.href,d)}})();var uc={exports:{}},bl={};var Jp;function lb(){if(Jp)return bl;Jp=1;var a=Symbol.for("react.transitional.element"),i=Symbol.for("react.fragment");function s(r,c,d){var f=null;if(d!==void 0&&(f=""+d),c.key!==void 0&&(f=""+c.key),"key"in c){d={};for(var m in c)m!=="key"&&(d[m]=c[m])}else d=c;return c=d.ref,{$$typeof:a,type:r,key:f,ref:c!==void 0?c:null,props:d}}return bl.Fragment=i,bl.jsx=s,bl.jsxs=s,bl}var Fp;function sb(){return Fp||(Fp=1,uc.exports=lb()),uc.exports}var x=sb(),cc={exports:{}},lt={};var Wp;function rb(){if(Wp)return lt;Wp=1;var a=Symbol.for("react.transitional.element"),i=Symbol.for("react.portal"),s=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),c=Symbol.for("react.profiler"),d=Symbol.for("react.consumer"),f=Symbol.for("react.context"),m=Symbol.for("react.forward_ref"),y=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),g=Symbol.for("react.lazy"),v=Symbol.for("react.activity"),S=Symbol.iterator;function R(C){return C===null||typeof C!="object"?null:(C=S&&C[S]||C["@@iterator"],typeof C=="function"?C:null)}var M={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},D=Object.assign,w={};function V(C,q,Z){this.props=C,this.context=q,this.refs=w,this.updater=Z||M}V.prototype.isReactComponent={},V.prototype.setState=function(C,q){if(typeof C!="object"&&typeof C!="function"&&C!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,C,q,"setState")},V.prototype.forceUpdate=function(C){this.updater.enqueueForceUpdate(this,C,"forceUpdate")};function G(){}G.prototype=V.prototype;function B(C,q,Z){this.props=C,this.context=q,this.refs=w,this.updater=Z||M}var k=B.prototype=new G;k.constructor=B,D(k,V.prototype),k.isPureReactComponent=!0;var P=Array.isArray;function et(){}var X={H:null,A:null,T:null,S:null},Q=Object.prototype.hasOwnProperty;function at(C,q,Z){var $=Z.ref;return{$$typeof:a,type:C,key:q,ref:$!==void 0?$:null,props:Z}}function I(C,q){return at(C.type,q,C.props)}function ct(C){return typeof C=="object"&&C!==null&&C.$$typeof===a}function xt(C){var q={"=":"=0",":":"=2"};return"$"+C.replace(/[=:]/g,function(Z){return q[Z]})}var Xt=/\/+/g;function _t(C,q){return typeof C=="object"&&C!==null&&C.key!=null?xt(""+C.key):q.toString(36)}function Et(C){switch(C.status){case"fulfilled":return C.value;case"rejected":throw C.reason;default:switch(typeof C.status=="string"?C.then(et,et):(C.status="pending",C.then(function(q){C.status==="pending"&&(C.status="fulfilled",C.value=q)},function(q){C.status==="pending"&&(C.status="rejected",C.reason=q)})),C.status){case"fulfilled":return C.value;case"rejected":throw C.reason}}throw C}function _(C,q,Z,$,st){var ft=typeof C;(ft==="undefined"||ft==="boolean")&&(C=null);var At=!1;if(C===null)At=!0;else switch(ft){case"bigint":case"string":case"number":At=!0;break;case"object":switch(C.$$typeof){case a:case i:At=!0;break;case g:return At=C._init,_(At(C._payload),q,Z,$,st)}}if(At)return st=st(C),At=$===""?"."+_t(C,0):$,P(st)?(Z="",At!=null&&(Z=At.replace(Xt,"$&/")+"/"),_(st,q,Z,"",function(wi){return wi})):st!=null&&(ct(st)&&(st=I(st,Z+(st.key==null||C&&C.key===st.key?"":(""+st.key).replace(Xt,"$&/")+"/")+At)),q.push(st)),1;At=0;var oe=$===""?".":$+":";if(P(C))for(var Yt=0;Yt>>1,yt=_[ot];if(0>>1;otc(Z,J))$c(st,Z)?(_[ot]=st,_[$]=J,ot=$):(_[ot]=Z,_[q]=J,ot=q);else if($c(st,J))_[ot]=st,_[$]=J,ot=$;else break t}}return K}function c(_,K){var J=_.sortIndex-K.sortIndex;return J!==0?J:_.id-K.id}if(a.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var d=performance;a.unstable_now=function(){return d.now()}}else{var f=Date,m=f.now();a.unstable_now=function(){return f.now()-m}}var y=[],p=[],g=1,v=null,S=3,R=!1,M=!1,D=!1,w=!1,V=typeof setTimeout=="function"?setTimeout:null,G=typeof clearTimeout=="function"?clearTimeout:null,B=typeof setImmediate<"u"?setImmediate:null;function k(_){for(var K=s(p);K!==null;){if(K.callback===null)r(p);else if(K.startTime<=_)r(p),K.sortIndex=K.expirationTime,i(y,K);else break;K=s(p)}}function P(_){if(D=!1,k(_),!M)if(s(y)!==null)M=!0,et||(et=!0,xt());else{var K=s(p);K!==null&&Et(P,K.startTime-_)}}var et=!1,X=-1,Q=5,at=-1;function I(){return w?!0:!(a.unstable_now()-at_&&I());){var ot=v.callback;if(typeof ot=="function"){v.callback=null,S=v.priorityLevel;var yt=ot(v.expirationTime<=_);if(_=a.unstable_now(),typeof yt=="function"){v.callback=yt,k(_),K=!0;break e}v===s(y)&&r(y),k(_)}else r(y);v=s(y)}if(v!==null)K=!0;else{var C=s(p);C!==null&&Et(P,C.startTime-_),K=!1}}break t}finally{v=null,S=J,R=!1}K=void 0}}finally{K?xt():et=!1}}}var xt;if(typeof B=="function")xt=function(){B(ct)};else if(typeof MessageChannel<"u"){var Xt=new MessageChannel,_t=Xt.port2;Xt.port1.onmessage=ct,xt=function(){_t.postMessage(null)}}else xt=function(){V(ct,0)};function Et(_,K){X=V(function(){_(a.unstable_now())},K)}a.unstable_IdlePriority=5,a.unstable_ImmediatePriority=1,a.unstable_LowPriority=4,a.unstable_NormalPriority=3,a.unstable_Profiling=null,a.unstable_UserBlockingPriority=2,a.unstable_cancelCallback=function(_){_.callback=null},a.unstable_forceFrameRate=function(_){0>_||125<_?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):Q=0<_?Math.floor(1e3/_):5},a.unstable_getCurrentPriorityLevel=function(){return S},a.unstable_next=function(_){switch(S){case 1:case 2:case 3:var K=3;break;default:K=S}var J=S;S=K;try{return _()}finally{S=J}},a.unstable_requestPaint=function(){w=!0},a.unstable_runWithPriority=function(_,K){switch(_){case 1:case 2:case 3:case 4:case 5:break;default:_=3}var J=S;S=_;try{return K()}finally{S=J}},a.unstable_scheduleCallback=function(_,K,J){var ot=a.unstable_now();switch(typeof J=="object"&&J!==null?(J=J.delay,J=typeof J=="number"&&0ot?(_.sortIndex=J,i(p,_),s(y)===null&&_===s(p)&&(D?(G(X),X=-1):D=!0,Et(P,J-ot))):(_.sortIndex=yt,i(y,_),M||R||(M=!0,et||(et=!0,xt()))),_},a.unstable_shouldYield=I,a.unstable_wrapCallback=function(_){var K=S;return function(){var J=S;S=K;try{return _.apply(this,arguments)}finally{S=J}}}})(hc)),hc}var ty;function ub(){return ty||(ty=1,dc.exports=ob()),dc.exports}var mc={exports:{}},re={};var ey;function cb(){if(ey)return re;ey=1;var a=ff();function i(y){var p="https://react.dev/errors/"+y;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(a)}catch(i){console.error(i)}}return a(),mc.exports=cb(),mc.exports}var ay;function db(){if(ay)return Sl;ay=1;var a=ub(),i=ff(),s=fb();function r(t){var e="https://react.dev/errors/"+t;if(1yt||(t.current=ot[yt],ot[yt]=null,yt--)}function Z(t,e){yt++,ot[yt]=t.current,t.current=e}var $=C(null),st=C(null),ft=C(null),At=C(null);function oe(t,e){switch(Z(ft,e),Z(st,t),Z($,null),e.nodeType){case 9:case 11:t=(t=e.documentElement)&&(t=t.namespaceURI)?vp(t):0;break;default:if(t=e.tagName,e=e.namespaceURI)e=vp(e),t=xp(e,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}q($),Z($,t)}function Yt(){q($),q(st),q(ft)}function wi(t){t.memoizedState!==null&&Z(At,t);var e=$.current,n=xp(e,t.type);e!==n&&(Z(st,t),Z($,n))}function kl(t){st.current===t&&(q($),q(st)),At.current===t&&(q(At),yl._currentValue=J)}var Xr,Pf;function ra(t){if(Xr===void 0)try{throw Error()}catch(n){var e=n.stack.trim().match(/\n( *(at )?)/);Xr=e&&e[1]||"",Pf=-1)":-1o||T[l]!==O[o]){var U=` -`+T[l].replace(" at new "," at ");return t.displayName&&U.includes("")&&(U=U.replace("",t.displayName)),U}while(1<=l&&0<=o);break}}}finally{Qr=!1,Error.prepareStackTrace=n}return(n=t?t.displayName||t.name:"")?ra(n):""}function _v(t,e){switch(t.tag){case 26:case 27:case 5:return ra(t.type);case 16:return ra("Lazy");case 13:return t.child!==e&&e!==null?ra("Suspense Fallback"):ra("Suspense");case 19:return ra("SuspenseList");case 0:case 15:return Kr(t.type,!1);case 11:return Kr(t.type.render,!1);case 1:return Kr(t.type,!0);case 31:return ra("Activity");default:return""}}function Jf(t){try{var e="",n=null;do e+=_v(t,n),n=t,t=t.return;while(t);return e}catch(l){return` -Error generating stack: `+l.message+` -`+l.stack}}var Zr=Object.prototype.hasOwnProperty,Pr=a.unstable_scheduleCallback,Jr=a.unstable_cancelCallback,Uv=a.unstable_shouldYield,Bv=a.unstable_requestPaint,xe=a.unstable_now,Hv=a.unstable_getCurrentPriorityLevel,Ff=a.unstable_ImmediatePriority,Wf=a.unstable_UserBlockingPriority,Xl=a.unstable_NormalPriority,Gv=a.unstable_LowPriority,$f=a.unstable_IdlePriority,Yv=a.log,qv=a.unstable_setDisableYieldValue,Mi=null,be=null;function jn(t){if(typeof Yv=="function"&&qv(t),be&&typeof be.setStrictMode=="function")try{be.setStrictMode(Mi,t)}catch{}}var Se=Math.clz32?Math.clz32:Qv,kv=Math.log,Xv=Math.LN2;function Qv(t){return t>>>=0,t===0?32:31-(kv(t)/Xv|0)|0}var Ql=256,Kl=262144,Zl=4194304;function oa(t){var e=t&42;if(e!==0)return e;switch(t&-t){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:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function Pl(t,e,n){var l=t.pendingLanes;if(l===0)return 0;var o=0,u=t.suspendedLanes,h=t.pingedLanes;t=t.warmLanes;var b=l&134217727;return b!==0?(l=b&~u,l!==0?o=oa(l):(h&=b,h!==0?o=oa(h):n||(n=b&~t,n!==0&&(o=oa(n))))):(b=l&~u,b!==0?o=oa(b):h!==0?o=oa(h):n||(n=l&~t,n!==0&&(o=oa(n)))),o===0?0:e!==0&&e!==o&&(e&u)===0&&(u=o&-o,n=e&-e,u>=n||u===32&&(n&4194048)!==0)?e:o}function Ni(t,e){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&e)===0}function Kv(t,e){switch(t){case 1:case 2:case 4:case 8:case 64:return e+250;case 16:case 32: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+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function If(){var t=Zl;return Zl<<=1,(Zl&62914560)===0&&(Zl=4194304),t}function Fr(t){for(var e=[],n=0;31>n;n++)e.push(t);return e}function Di(t,e){t.pendingLanes|=e,e!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function Zv(t,e,n,l,o,u){var h=t.pendingLanes;t.pendingLanes=n,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=n,t.entangledLanes&=n,t.errorRecoveryDisabledLanes&=n,t.shellSuspendCounter=0;var b=t.entanglements,T=t.expirationTimes,O=t.hiddenUpdates;for(n=h&~n;0"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var Iv=/[\n"\\]/g;function Oe(t){return t.replace(Iv,function(e){return"\\"+e.charCodeAt(0).toString(16)+" "})}function no(t,e,n,l,o,u,h,b){t.name="",h!=null&&typeof h!="function"&&typeof h!="symbol"&&typeof h!="boolean"?t.type=h:t.removeAttribute("type"),e!=null?h==="number"?(e===0&&t.value===""||t.value!=e)&&(t.value=""+je(e)):t.value!==""+je(e)&&(t.value=""+je(e)):h!=="submit"&&h!=="reset"||t.removeAttribute("value"),e!=null?ao(t,h,je(e)):n!=null?ao(t,h,je(n)):l!=null&&t.removeAttribute("value"),o==null&&u!=null&&(t.defaultChecked=!!u),o!=null&&(t.checked=o&&typeof o!="function"&&typeof o!="symbol"),b!=null&&typeof b!="function"&&typeof b!="symbol"&&typeof b!="boolean"?t.name=""+je(b):t.removeAttribute("name")}function dd(t,e,n,l,o,u,h,b){if(u!=null&&typeof u!="function"&&typeof u!="symbol"&&typeof u!="boolean"&&(t.type=u),e!=null||n!=null){if(!(u!=="submit"&&u!=="reset"||e!=null)){eo(t);return}n=n!=null?""+je(n):"",e=e!=null?""+je(e):n,b||e===t.value||(t.value=e),t.defaultValue=e}l=l??o,l=typeof l!="function"&&typeof l!="symbol"&&!!l,t.checked=b?t.checked:!!l,t.defaultChecked=!!l,h!=null&&typeof h!="function"&&typeof h!="symbol"&&typeof h!="boolean"&&(t.name=h),eo(t)}function ao(t,e,n){e==="number"&&Wl(t.ownerDocument)===t||t.defaultValue===""+n||(t.defaultValue=""+n)}function Ua(t,e,n,l){if(t=t.options,e){e={};for(var o=0;o"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),oo=!1;if(fn)try{var Li={};Object.defineProperty(Li,"passive",{get:function(){oo=!0}}),window.addEventListener("test",Li,Li),window.removeEventListener("test",Li,Li)}catch{oo=!1}var zn=null,uo=null,Il=null;function xd(){if(Il)return Il;var t,e=uo,n=e.length,l,o="value"in zn?zn.value:zn.textContent,u=o.length;for(t=0;t=Ui),Cd=" ",Rd=!1;function wd(t,e){switch(t){case"keyup":return wx.indexOf(e.keyCode)!==-1;case"keydown":return e.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Md(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var Ya=!1;function Nx(t,e){switch(t){case"compositionend":return Md(e);case"keypress":return e.which!==32?null:(Rd=!0,Cd);case"textInput":return t=e.data,t===Cd&&Rd?null:t;default:return null}}function Dx(t,e){if(Ya)return t==="compositionend"||!po&&wd(t,e)?(t=xd(),Il=uo=zn=null,Ya=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(e.ctrlKey||e.altKey||e.metaKey)||e.ctrlKey&&e.altKey){if(e.char&&1=e)return{node:n,offset:e-t};t=l}t:{for(;n;){if(n.nextSibling){n=n.nextSibling;break t}n=n.parentNode}n=void 0}n=_d(n)}}function Bd(t,e){return t&&e?t===e?!0:t&&t.nodeType===3?!1:e&&e.nodeType===3?Bd(t,e.parentNode):"contains"in t?t.contains(e):t.compareDocumentPosition?!!(t.compareDocumentPosition(e)&16):!1:!1}function Hd(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var e=Wl(t.document);e instanceof t.HTMLIFrameElement;){try{var n=typeof e.contentWindow.location.href=="string"}catch{n=!1}if(n)t=e.contentWindow;else break;e=Wl(t.document)}return e}function vo(t){var e=t&&t.nodeName&&t.nodeName.toLowerCase();return e&&(e==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||e==="textarea"||t.contentEditable==="true")}var Bx=fn&&"documentMode"in document&&11>=document.documentMode,qa=null,xo=null,Yi=null,bo=!1;function Gd(t,e,n){var l=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;bo||qa==null||qa!==Wl(l)||(l=qa,"selectionStart"in l&&vo(l)?l={start:l.selectionStart,end:l.selectionEnd}:(l=(l.ownerDocument&&l.ownerDocument.defaultView||window).getSelection(),l={anchorNode:l.anchorNode,anchorOffset:l.anchorOffset,focusNode:l.focusNode,focusOffset:l.focusOffset}),Yi&&Gi(Yi,l)||(Yi=l,l=Ks(xo,"onSelect"),0>=h,o-=h,$e=1<<32-Se(e)+o|n<ut?(pt=tt,tt=null):pt=tt.sibling;var St=z(N,tt,j[ut],H);if(St===null){tt===null&&(tt=pt);break}t&&tt&&St.alternate===null&&e(N,tt),A=u(St,A,ut),bt===null?nt=St:bt.sibling=St,bt=St,tt=pt}if(ut===j.length)return n(N,tt),gt&&hn(N,ut),nt;if(tt===null){for(;utut?(pt=tt,tt=null):pt=tt.sibling;var ea=z(N,tt,St.value,H);if(ea===null){tt===null&&(tt=pt);break}t&&tt&&ea.alternate===null&&e(N,tt),A=u(ea,A,ut),bt===null?nt=ea:bt.sibling=ea,bt=ea,tt=pt}if(St.done)return n(N,tt),gt&&hn(N,ut),nt;if(tt===null){for(;!St.done;ut++,St=j.next())St=Y(N,St.value,H),St!==null&&(A=u(St,A,ut),bt===null?nt=St:bt.sibling=St,bt=St);return gt&&hn(N,ut),nt}for(tt=l(tt);!St.done;ut++,St=j.next())St=L(tt,N,ut,St.value,H),St!==null&&(t&&St.alternate!==null&&tt.delete(St.key===null?ut:St.key),A=u(St,A,ut),bt===null?nt=St:bt.sibling=St,bt=St);return t&&tt.forEach(function(ib){return e(N,ib)}),gt&&hn(N,ut),nt}function Nt(N,A,j,H){if(typeof j=="object"&&j!==null&&j.type===D&&j.key===null&&(j=j.props.children),typeof j=="object"&&j!==null){switch(j.$$typeof){case R:t:{for(var nt=j.key;A!==null;){if(A.key===nt){if(nt=j.type,nt===D){if(A.tag===7){n(N,A.sibling),H=o(A,j.props.children),H.return=N,N=H;break t}}else if(A.elementType===nt||typeof nt=="object"&&nt!==null&&nt.$$typeof===Q&&xa(nt)===A.type){n(N,A.sibling),H=o(A,j.props),Zi(H,j),H.return=N,N=H;break t}n(N,A);break}else e(N,A);A=A.sibling}j.type===D?(H=ma(j.props.children,N.mode,H,j.key),H.return=N,N=H):(H=us(j.type,j.key,j.props,null,N.mode,H),Zi(H,j),H.return=N,N=H)}return h(N);case M:t:{for(nt=j.key;A!==null;){if(A.key===nt)if(A.tag===4&&A.stateNode.containerInfo===j.containerInfo&&A.stateNode.implementation===j.implementation){n(N,A.sibling),H=o(A,j.children||[]),H.return=N,N=H;break t}else{n(N,A);break}else e(N,A);A=A.sibling}H=wo(j,N.mode,H),H.return=N,N=H}return h(N);case Q:return j=xa(j),Nt(N,A,j,H)}if(Et(j))return F(N,A,j,H);if(xt(j)){if(nt=xt(j),typeof nt!="function")throw Error(r(150));return j=nt.call(j),it(N,A,j,H)}if(typeof j.then=="function")return Nt(N,A,ys(j),H);if(j.$$typeof===B)return Nt(N,A,ds(N,j),H);gs(N,j)}return typeof j=="string"&&j!==""||typeof j=="number"||typeof j=="bigint"?(j=""+j,A!==null&&A.tag===6?(n(N,A.sibling),H=o(A,j),H.return=N,N=H):(n(N,A),H=Ro(j,N.mode,H),H.return=N,N=H),h(N)):n(N,A)}return function(N,A,j,H){try{Ki=0;var nt=Nt(N,A,j,H);return Ia=null,nt}catch(tt){if(tt===$a||tt===ms)throw tt;var bt=Ee(29,tt,null,N.mode);return bt.lanes=H,bt.return=N,bt}}}var Sa=uh(!0),ch=uh(!1),Bn=!1;function Ho(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Go(t,e){t=t.updateQueue,e.updateQueue===t&&(e.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function Hn(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function Gn(t,e,n){var l=t.updateQueue;if(l===null)return null;if(l=l.shared,(Tt&2)!==0){var o=l.pending;return o===null?e.next=e:(e.next=o.next,o.next=e),l.pending=e,e=os(t),Zd(t,null,n),e}return rs(t,l,e,n),os(t)}function Pi(t,e,n){if(e=e.updateQueue,e!==null&&(e=e.shared,(n&4194048)!==0)){var l=e.lanes;l&=t.pendingLanes,n|=l,e.lanes=n,ed(t,n)}}function Yo(t,e){var n=t.updateQueue,l=t.alternate;if(l!==null&&(l=l.updateQueue,n===l)){var o=null,u=null;if(n=n.firstBaseUpdate,n!==null){do{var h={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};u===null?o=u=h:u=u.next=h,n=n.next}while(n!==null);u===null?o=u=e:u=u.next=e}else o=u=e;n={baseState:l.baseState,firstBaseUpdate:o,lastBaseUpdate:u,shared:l.shared,callbacks:l.callbacks},t.updateQueue=n;return}t=n.lastBaseUpdate,t===null?n.firstBaseUpdate=e:t.next=e,n.lastBaseUpdate=e}var qo=!1;function Ji(){if(qo){var t=Wa;if(t!==null)throw t}}function Fi(t,e,n,l){qo=!1;var o=t.updateQueue;Bn=!1;var u=o.firstBaseUpdate,h=o.lastBaseUpdate,b=o.shared.pending;if(b!==null){o.shared.pending=null;var T=b,O=T.next;T.next=null,h===null?u=O:h.next=O,h=T;var U=t.alternate;U!==null&&(U=U.updateQueue,b=U.lastBaseUpdate,b!==h&&(b===null?U.firstBaseUpdate=O:b.next=O,U.lastBaseUpdate=T))}if(u!==null){var Y=o.baseState;h=0,U=O=T=null,b=u;do{var z=b.lane&-536870913,L=z!==b.lane;if(L?(mt&z)===z:(l&z)===z){z!==0&&z===Fa&&(qo=!0),U!==null&&(U=U.next={lane:0,tag:b.tag,payload:b.payload,callback:null,next:null});t:{var F=t,it=b;z=e;var Nt=n;switch(it.tag){case 1:if(F=it.payload,typeof F=="function"){Y=F.call(Nt,Y,z);break t}Y=F;break t;case 3:F.flags=F.flags&-65537|128;case 0:if(F=it.payload,z=typeof F=="function"?F.call(Nt,Y,z):F,z==null)break t;Y=v({},Y,z);break t;case 2:Bn=!0}}z=b.callback,z!==null&&(t.flags|=64,L&&(t.flags|=8192),L=o.callbacks,L===null?o.callbacks=[z]:L.push(z))}else L={lane:z,tag:b.tag,payload:b.payload,callback:b.callback,next:null},U===null?(O=U=L,T=Y):U=U.next=L,h|=z;if(b=b.next,b===null){if(b=o.shared.pending,b===null)break;L=b,b=L.next,L.next=null,o.lastBaseUpdate=L,o.shared.pending=null}}while(!0);U===null&&(T=Y),o.baseState=T,o.firstBaseUpdate=O,o.lastBaseUpdate=U,u===null&&(o.shared.lanes=0),Qn|=h,t.lanes=h,t.memoizedState=Y}}function fh(t,e){if(typeof t!="function")throw Error(r(191,t));t.call(e)}function dh(t,e){var n=t.callbacks;if(n!==null)for(t.callbacks=null,t=0;tu?u:8;var h=_.T,b={};_.T=b,ru(t,!1,e,n);try{var T=o(),O=_.S;if(O!==null&&O(b,T),T!==null&&typeof T=="object"&&typeof T.then=="function"){var U=Zx(T,l);Ii(t,e,U,Me(t))}else Ii(t,e,l,Me(t))}catch(Y){Ii(t,e,{then:function(){},status:"rejected",reason:Y},Me())}finally{K.p=u,h!==null&&b.types!==null&&(h.types=b.types),_.T=h}}function Ix(){}function lu(t,e,n,l){if(t.tag!==5)throw Error(r(476));var o=Xh(t).queue;kh(t,o,e,J,n===null?Ix:function(){return Qh(t),n(l)})}function Xh(t){var e=t.memoizedState;if(e!==null)return e;e={memoizedState:J,baseState:J,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:gn,lastRenderedState:J},next:null};var n={};return e.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:gn,lastRenderedState:n},next:null},t.memoizedState=e,t=t.alternate,t!==null&&(t.memoizedState=e),e}function Qh(t){var e=Xh(t);e.next===null&&(e=t.alternate.memoizedState),Ii(t,e.next.queue,{},Me())}function su(){return ne(yl)}function Kh(){return kt().memoizedState}function Zh(){return kt().memoizedState}function t1(t){for(var e=t.return;e!==null;){switch(e.tag){case 24:case 3:var n=Me();t=Hn(n);var l=Gn(e,t,n);l!==null&&(ve(l,e,n),Pi(l,e,n)),e={cache:Vo()},t.payload=e;return}e=e.return}}function e1(t,e,n){var l=Me();n={lane:l,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},ws(t)?Jh(e,n):(n=Ao(t,e,n,l),n!==null&&(ve(n,t,l),Fh(n,e,l)))}function Ph(t,e,n){var l=Me();Ii(t,e,n,l)}function Ii(t,e,n,l){var o={lane:l,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(ws(t))Jh(e,o);else{var u=t.alternate;if(t.lanes===0&&(u===null||u.lanes===0)&&(u=e.lastRenderedReducer,u!==null))try{var h=e.lastRenderedState,b=u(h,n);if(o.hasEagerState=!0,o.eagerState=b,Te(b,h))return rs(t,e,o,0),Dt===null&&ss(),!1}catch{}if(n=Ao(t,e,o,l),n!==null)return ve(n,t,l),Fh(n,e,l),!0}return!1}function ru(t,e,n,l){if(l={lane:2,revertLane:Hu(),gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},ws(t)){if(e)throw Error(r(479))}else e=Ao(t,n,l,2),e!==null&&ve(e,t,2)}function ws(t){var e=t.alternate;return t===rt||e!==null&&e===rt}function Jh(t,e){ei=bs=!0;var n=t.pending;n===null?e.next=e:(e.next=n.next,n.next=e),t.pending=e}function Fh(t,e,n){if((n&4194048)!==0){var l=e.lanes;l&=t.pendingLanes,n|=l,e.lanes=n,ed(t,n)}}var tl={readContext:ne,use:Es,useCallback:Bt,useContext:Bt,useEffect:Bt,useImperativeHandle:Bt,useLayoutEffect:Bt,useInsertionEffect:Bt,useMemo:Bt,useReducer:Bt,useRef:Bt,useState:Bt,useDebugValue:Bt,useDeferredValue:Bt,useTransition:Bt,useSyncExternalStore:Bt,useId:Bt,useHostTransitionStatus:Bt,useFormState:Bt,useActionState:Bt,useOptimistic:Bt,useMemoCache:Bt,useCacheRefresh:Bt};tl.useEffectEvent=Bt;var Wh={readContext:ne,use:Es,useCallback:function(t,e){return ue().memoizedState=[t,e===void 0?null:e],t},useContext:ne,useEffect:Lh,useImperativeHandle:function(t,e,n){n=n!=null?n.concat([t]):null,Cs(4194308,4,Bh.bind(null,e,t),n)},useLayoutEffect:function(t,e){return Cs(4194308,4,t,e)},useInsertionEffect:function(t,e){Cs(4,2,t,e)},useMemo:function(t,e){var n=ue();e=e===void 0?null:e;var l=t();if(Ta){jn(!0);try{t()}finally{jn(!1)}}return n.memoizedState=[l,e],l},useReducer:function(t,e,n){var l=ue();if(n!==void 0){var o=n(e);if(Ta){jn(!0);try{n(e)}finally{jn(!1)}}}else o=e;return l.memoizedState=l.baseState=o,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:o},l.queue=t,t=t.dispatch=e1.bind(null,rt,t),[l.memoizedState,t]},useRef:function(t){var e=ue();return t={current:t},e.memoizedState=t},useState:function(t){t=tu(t);var e=t.queue,n=Ph.bind(null,rt,e);return e.dispatch=n,[t.memoizedState,n]},useDebugValue:au,useDeferredValue:function(t,e){var n=ue();return iu(n,t,e)},useTransition:function(){var t=tu(!1);return t=kh.bind(null,rt,t.queue,!0,!1),ue().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,e,n){var l=rt,o=ue();if(gt){if(n===void 0)throw Error(r(407));n=n()}else{if(n=e(),Dt===null)throw Error(r(349));(mt&127)!==0||vh(l,e,n)}o.memoizedState=n;var u={value:n,getSnapshot:e};return o.queue=u,Lh(bh.bind(null,l,u,t),[t]),l.flags|=2048,ai(9,{destroy:void 0},xh.bind(null,l,u,n,e),null),n},useId:function(){var t=ue(),e=Dt.identifierPrefix;if(gt){var n=Ie,l=$e;n=(l&~(1<<32-Se(l)-1)).toString(32)+n,e="_"+e+"R_"+n,n=Ss++,0<\/script>",u=u.removeChild(u.firstChild);break;case"select":u=typeof l.is=="string"?h.createElement("select",{is:l.is}):h.createElement("select"),l.multiple?u.multiple=!0:l.size&&(u.size=l.size);break;default:u=typeof l.is=="string"?h.createElement(o,{is:l.is}):h.createElement(o)}}u[te]=e,u[de]=l;t:for(h=e.child;h!==null;){if(h.tag===5||h.tag===6)u.appendChild(h.stateNode);else if(h.tag!==4&&h.tag!==27&&h.child!==null){h.child.return=h,h=h.child;continue}if(h===e)break t;for(;h.sibling===null;){if(h.return===null||h.return===e)break t;h=h.return}h.sibling.return=h.return,h=h.sibling}e.stateNode=u;t:switch(ie(u,o,l),o){case"button":case"input":case"select":case"textarea":l=!!l.autoFocus;break t;case"img":l=!0;break t;default:l=!1}l&&xn(e)}}return Lt(e),Su(e,e.type,t===null?null:t.memoizedProps,e.pendingProps,n),null;case 6:if(t&&e.stateNode!=null)t.memoizedProps!==l&&xn(e);else{if(typeof l!="string"&&e.stateNode===null)throw Error(r(166));if(t=ft.current,Pa(e)){if(t=e.stateNode,n=e.memoizedProps,l=null,o=ee,o!==null)switch(o.tag){case 27:case 5:l=o.memoizedProps}t[te]=e,t=!!(t.nodeValue===n||l!==null&&l.suppressHydrationWarning===!0||yp(t.nodeValue,n)),t||_n(e,!0)}else t=Zs(t).createTextNode(l),t[te]=e,e.stateNode=t}return Lt(e),null;case 31:if(n=e.memoizedState,t===null||t.memoizedState!==null){if(l=Pa(e),n!==null){if(t===null){if(!l)throw Error(r(318));if(t=e.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(r(557));t[te]=e}else pa(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;Lt(e),t=!1}else n=jo(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=n),t=!0;if(!t)return e.flags&256?(Ce(e),e):(Ce(e),null);if((e.flags&128)!==0)throw Error(r(558))}return Lt(e),null;case 13:if(l=e.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(o=Pa(e),l!==null&&l.dehydrated!==null){if(t===null){if(!o)throw Error(r(318));if(o=e.memoizedState,o=o!==null?o.dehydrated:null,!o)throw Error(r(317));o[te]=e}else pa(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;Lt(e),o=!1}else o=jo(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=o),o=!0;if(!o)return e.flags&256?(Ce(e),e):(Ce(e),null)}return Ce(e),(e.flags&128)!==0?(e.lanes=n,e):(n=l!==null,t=t!==null&&t.memoizedState!==null,n&&(l=e.child,o=null,l.alternate!==null&&l.alternate.memoizedState!==null&&l.alternate.memoizedState.cachePool!==null&&(o=l.alternate.memoizedState.cachePool.pool),u=null,l.memoizedState!==null&&l.memoizedState.cachePool!==null&&(u=l.memoizedState.cachePool.pool),u!==o&&(l.flags|=2048)),n!==t&&n&&(e.child.flags|=8192),Os(e,e.updateQueue),Lt(e),null);case 4:return Yt(),t===null&&ku(e.stateNode.containerInfo),Lt(e),null;case 10:return pn(e.type),Lt(e),null;case 19:if(q(qt),l=e.memoizedState,l===null)return Lt(e),null;if(o=(e.flags&128)!==0,u=l.rendering,u===null)if(o)nl(l,!1);else{if(Ht!==0||t!==null&&(t.flags&128)!==0)for(t=e.child;t!==null;){if(u=xs(t),u!==null){for(e.flags|=128,nl(l,!1),t=u.updateQueue,e.updateQueue=t,Os(e,t),e.subtreeFlags=0,t=n,n=e.child;n!==null;)Pd(n,t),n=n.sibling;return Z(qt,qt.current&1|2),gt&&hn(e,l.treeForkCount),e.child}t=t.sibling}l.tail!==null&&xe()>Us&&(e.flags|=128,o=!0,nl(l,!1),e.lanes=4194304)}else{if(!o)if(t=xs(u),t!==null){if(e.flags|=128,o=!0,t=t.updateQueue,e.updateQueue=t,Os(e,t),nl(l,!0),l.tail===null&&l.tailMode==="hidden"&&!u.alternate&&!gt)return Lt(e),null}else 2*xe()-l.renderingStartTime>Us&&n!==536870912&&(e.flags|=128,o=!0,nl(l,!1),e.lanes=4194304);l.isBackwards?(u.sibling=e.child,e.child=u):(t=l.last,t!==null?t.sibling=u:e.child=u,l.last=u)}return l.tail!==null?(t=l.tail,l.rendering=t,l.tail=t.sibling,l.renderingStartTime=xe(),t.sibling=null,n=qt.current,Z(qt,o?n&1|2:n&1),gt&&hn(e,l.treeForkCount),t):(Lt(e),null);case 22:case 23:return Ce(e),Xo(),l=e.memoizedState!==null,t!==null?t.memoizedState!==null!==l&&(e.flags|=8192):l&&(e.flags|=8192),l?(n&536870912)!==0&&(e.flags&128)===0&&(Lt(e),e.subtreeFlags&6&&(e.flags|=8192)):Lt(e),n=e.updateQueue,n!==null&&Os(e,n.retryQueue),n=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(n=t.memoizedState.cachePool.pool),l=null,e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(l=e.memoizedState.cachePool.pool),l!==n&&(e.flags|=2048),t!==null&&q(va),null;case 24:return n=null,t!==null&&(n=t.memoizedState.cache),e.memoizedState.cache!==n&&(e.flags|=2048),pn(Qt),Lt(e),null;case 25:return null;case 30:return null}throw Error(r(156,e.tag))}function s1(t,e){switch(No(e),e.tag){case 1:return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 3:return pn(Qt),Yt(),t=e.flags,(t&65536)!==0&&(t&128)===0?(e.flags=t&-65537|128,e):null;case 26:case 27:case 5:return kl(e),null;case 31:if(e.memoizedState!==null){if(Ce(e),e.alternate===null)throw Error(r(340));pa()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 13:if(Ce(e),t=e.memoizedState,t!==null&&t.dehydrated!==null){if(e.alternate===null)throw Error(r(340));pa()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 19:return q(qt),null;case 4:return Yt(),null;case 10:return pn(e.type),null;case 22:case 23:return Ce(e),Xo(),t!==null&&q(va),t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 24:return pn(Qt),null;case 25:return null;default:return null}}function Sm(t,e){switch(No(e),e.tag){case 3:pn(Qt),Yt();break;case 26:case 27:case 5:kl(e);break;case 4:Yt();break;case 31:e.memoizedState!==null&&Ce(e);break;case 13:Ce(e);break;case 19:q(qt);break;case 10:pn(e.type);break;case 22:case 23:Ce(e),Xo(),t!==null&&q(va);break;case 24:pn(Qt)}}function al(t,e){try{var n=e.updateQueue,l=n!==null?n.lastEffect:null;if(l!==null){var o=l.next;n=o;do{if((n.tag&t)===t){l=void 0;var u=n.create,h=n.inst;l=u(),h.destroy=l}n=n.next}while(n!==o)}}catch(b){Rt(e,e.return,b)}}function kn(t,e,n){try{var l=e.updateQueue,o=l!==null?l.lastEffect:null;if(o!==null){var u=o.next;l=u;do{if((l.tag&t)===t){var h=l.inst,b=h.destroy;if(b!==void 0){h.destroy=void 0,o=e;var T=n,O=b;try{O()}catch(U){Rt(o,T,U)}}}l=l.next}while(l!==u)}}catch(U){Rt(e,e.return,U)}}function Tm(t){var e=t.updateQueue;if(e!==null){var n=t.stateNode;try{dh(e,n)}catch(l){Rt(t,t.return,l)}}}function Em(t,e,n){n.props=Ea(t.type,t.memoizedProps),n.state=t.memoizedState;try{n.componentWillUnmount()}catch(l){Rt(t,e,l)}}function il(t,e){try{var n=t.ref;if(n!==null){switch(t.tag){case 26:case 27:case 5:var l=t.stateNode;break;case 30:l=t.stateNode;break;default:l=t.stateNode}typeof n=="function"?t.refCleanup=n(l):n.current=l}}catch(o){Rt(t,e,o)}}function tn(t,e){var n=t.ref,l=t.refCleanup;if(n!==null)if(typeof l=="function")try{l()}catch(o){Rt(t,e,o)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof n=="function")try{n(null)}catch(o){Rt(t,e,o)}else n.current=null}function Am(t){var e=t.type,n=t.memoizedProps,l=t.stateNode;try{t:switch(e){case"button":case"input":case"select":case"textarea":n.autoFocus&&l.focus();break t;case"img":n.src?l.src=n.src:n.srcSet&&(l.srcset=n.srcSet)}}catch(o){Rt(t,t.return,o)}}function Tu(t,e,n){try{var l=t.stateNode;M1(l,t.type,n,e),l[de]=e}catch(o){Rt(t,t.return,o)}}function Cm(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&Fn(t.type)||t.tag===4}function Eu(t){t:for(;;){for(;t.sibling===null;){if(t.return===null||Cm(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&Fn(t.type)||t.flags&2||t.child===null||t.tag===4)continue t;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function Au(t,e,n){var l=t.tag;if(l===5||l===6)t=t.stateNode,e?(n.nodeType===9?n.body:n.nodeName==="HTML"?n.ownerDocument.body:n).insertBefore(t,e):(e=n.nodeType===9?n.body:n.nodeName==="HTML"?n.ownerDocument.body:n,e.appendChild(t),n=n._reactRootContainer,n!=null||e.onclick!==null||(e.onclick=cn));else if(l!==4&&(l===27&&Fn(t.type)&&(n=t.stateNode,e=null),t=t.child,t!==null))for(Au(t,e,n),t=t.sibling;t!==null;)Au(t,e,n),t=t.sibling}function zs(t,e,n){var l=t.tag;if(l===5||l===6)t=t.stateNode,e?n.insertBefore(t,e):n.appendChild(t);else if(l!==4&&(l===27&&Fn(t.type)&&(n=t.stateNode),t=t.child,t!==null))for(zs(t,e,n),t=t.sibling;t!==null;)zs(t,e,n),t=t.sibling}function Rm(t){var e=t.stateNode,n=t.memoizedProps;try{for(var l=t.type,o=e.attributes;o.length;)e.removeAttributeNode(o[0]);ie(e,l,n),e[te]=t,e[de]=n}catch(u){Rt(t,t.return,u)}}var bn=!1,Pt=!1,Cu=!1,wm=typeof WeakSet=="function"?WeakSet:Set,It=null;function r1(t,e){if(t=t.containerInfo,Ku=tr,t=Hd(t),vo(t)){if("selectionStart"in t)var n={start:t.selectionStart,end:t.selectionEnd};else t:{n=(n=t.ownerDocument)&&n.defaultView||window;var l=n.getSelection&&n.getSelection();if(l&&l.rangeCount!==0){n=l.anchorNode;var o=l.anchorOffset,u=l.focusNode;l=l.focusOffset;try{n.nodeType,u.nodeType}catch{n=null;break t}var h=0,b=-1,T=-1,O=0,U=0,Y=t,z=null;e:for(;;){for(var L;Y!==n||o!==0&&Y.nodeType!==3||(b=h+o),Y!==u||l!==0&&Y.nodeType!==3||(T=h+l),Y.nodeType===3&&(h+=Y.nodeValue.length),(L=Y.firstChild)!==null;)z=Y,Y=L;for(;;){if(Y===t)break e;if(z===n&&++O===o&&(b=h),z===u&&++U===l&&(T=h),(L=Y.nextSibling)!==null)break;Y=z,z=Y.parentNode}Y=L}n=b===-1||T===-1?null:{start:b,end:T}}else n=null}n=n||{start:0,end:0}}else n=null;for(Zu={focusedElem:t,selectionRange:n},tr=!1,It=e;It!==null;)if(e=It,t=e.child,(e.subtreeFlags&1028)!==0&&t!==null)t.return=e,It=t;else for(;It!==null;){switch(e=It,u=e.alternate,t=e.flags,e.tag){case 0:if((t&4)!==0&&(t=e.updateQueue,t=t!==null?t.events:null,t!==null))for(n=0;n title"))),ie(u,l,n),u[te]=t,$t(u),l=u;break t;case"link":var h=zp("link","href",o).get(l+(n.href||""));if(h){for(var b=0;bNt&&(h=Nt,Nt=it,it=h);var N=Ud(b,it),A=Ud(b,Nt);if(N&&A&&(L.rangeCount!==1||L.anchorNode!==N.node||L.anchorOffset!==N.offset||L.focusNode!==A.node||L.focusOffset!==A.offset)){var j=Y.createRange();j.setStart(N.node,N.offset),L.removeAllRanges(),it>Nt?(L.addRange(j),L.extend(A.node,A.offset)):(j.setEnd(A.node,A.offset),L.addRange(j))}}}}for(Y=[],L=b;L=L.parentNode;)L.nodeType===1&&Y.push({element:L,left:L.scrollLeft,top:L.scrollTop});for(typeof b.focus=="function"&&b.focus(),b=0;bn?32:n,_.T=null,n=Ou,Ou=null;var u=Zn,h=Cn;if(Ft=0,oi=Zn=null,Cn=0,(Tt&6)!==0)throw Error(r(331));var b=Tt;if(Tt|=4,Bm(u.current),Vm(u,u.current,h,n),Tt=b,cl(0,!1),be&&typeof be.onPostCommitFiberRoot=="function")try{be.onPostCommitFiberRoot(Mi,u)}catch{}return!0}finally{K.p=o,_.T=l,np(t,e)}}function ip(t,e,n){e=Le(n,e),e=fu(t.stateNode,e,2),t=Gn(t,e,2),t!==null&&(Di(t,2),en(t))}function Rt(t,e,n){if(t.tag===3)ip(t,t,n);else for(;e!==null;){if(e.tag===3){ip(e,t,n);break}else if(e.tag===1){var l=e.stateNode;if(typeof e.type.getDerivedStateFromError=="function"||typeof l.componentDidCatch=="function"&&(Kn===null||!Kn.has(l))){t=Le(n,t),n=lm(2),l=Gn(e,n,2),l!==null&&(sm(n,l,e,t),Di(l,2),en(l));break}}e=e.return}}function _u(t,e,n){var l=t.pingCache;if(l===null){l=t.pingCache=new c1;var o=new Set;l.set(e,o)}else o=l.get(e),o===void 0&&(o=new Set,l.set(e,o));o.has(n)||(Mu=!0,o.add(n),t=p1.bind(null,t,e,n),e.then(t,t))}function p1(t,e,n){var l=t.pingCache;l!==null&&l.delete(e),t.pingedLanes|=t.suspendedLanes&n,t.warmLanes&=~n,Dt===t&&(mt&n)===n&&(Ht===4||Ht===3&&(mt&62914560)===mt&&300>xe()-_s?(Tt&2)===0&&ui(t,0):Nu|=n,ri===mt&&(ri=0)),en(t)}function lp(t,e){e===0&&(e=If()),t=ha(t,e),t!==null&&(Di(t,e),en(t))}function y1(t){var e=t.memoizedState,n=0;e!==null&&(n=e.retryLane),lp(t,n)}function g1(t,e){var n=0;switch(t.tag){case 31:case 13:var l=t.stateNode,o=t.memoizedState;o!==null&&(n=o.retryLane);break;case 19:l=t.stateNode;break;case 22:l=t.stateNode._retryCache;break;default:throw Error(r(314))}l!==null&&l.delete(e),lp(t,n)}function v1(t,e){return Pr(t,e)}var ks=null,fi=null,Uu=!1,Xs=!1,Bu=!1,Jn=0;function en(t){t!==fi&&t.next===null&&(fi===null?ks=fi=t:fi=fi.next=t),Xs=!0,Uu||(Uu=!0,b1())}function cl(t,e){if(!Bu&&Xs){Bu=!0;do for(var n=!1,l=ks;l!==null;){if(t!==0){var o=l.pendingLanes;if(o===0)var u=0;else{var h=l.suspendedLanes,b=l.pingedLanes;u=(1<<31-Se(42|t)+1)-1,u&=o&~(h&~b),u=u&201326741?u&201326741|1:u?u|2:0}u!==0&&(n=!0,up(l,u))}else u=mt,u=Pl(l,l===Dt?u:0,l.cancelPendingCommit!==null||l.timeoutHandle!==-1),(u&3)===0||Ni(l,u)||(n=!0,up(l,u));l=l.next}while(n);Bu=!1}}function x1(){sp()}function sp(){Xs=Uu=!1;var t=0;Jn!==0&&D1()&&(t=Jn);for(var e=xe(),n=null,l=ks;l!==null;){var o=l.next,u=rp(l,e);u===0?(l.next=null,n===null?ks=o:n.next=o,o===null&&(fi=n)):(n=l,(t!==0||(u&3)!==0)&&(Xs=!0)),l=o}Ft!==0&&Ft!==5||cl(t),Jn!==0&&(Jn=0)}function rp(t,e){for(var n=t.suspendedLanes,l=t.pingedLanes,o=t.expirationTimes,u=t.pendingLanes&-62914561;0b)break;var U=T.transferSize,Y=T.initiatorType;U&&gp(Y)&&(T=T.responseEnd,h+=U*(T"u"?null:document;function Np(t,e,n){var l=di;if(l&&typeof e=="string"&&e){var o=Oe(e);o='link[rel="'+t+'"][href="'+o+'"]',typeof n=="string"&&(o+='[crossorigin="'+n+'"]'),Mp.has(o)||(Mp.add(o),t={rel:t,crossOrigin:n,href:e},l.querySelector(o)===null&&(e=l.createElement("link"),ie(e,"link",t),$t(e),l.head.appendChild(e)))}}function H1(t){Rn.D(t),Np("dns-prefetch",t,null)}function G1(t,e){Rn.C(t,e),Np("preconnect",t,e)}function Y1(t,e,n){Rn.L(t,e,n);var l=di;if(l&&t&&e){var o='link[rel="preload"][as="'+Oe(e)+'"]';e==="image"&&n&&n.imageSrcSet?(o+='[imagesrcset="'+Oe(n.imageSrcSet)+'"]',typeof n.imageSizes=="string"&&(o+='[imagesizes="'+Oe(n.imageSizes)+'"]')):o+='[href="'+Oe(t)+'"]';var u=o;switch(e){case"style":u=hi(t);break;case"script":u=mi(t)}Ge.has(u)||(t=v({rel:"preload",href:e==="image"&&n&&n.imageSrcSet?void 0:t,as:e},n),Ge.set(u,t),l.querySelector(o)!==null||e==="style"&&l.querySelector(ml(u))||e==="script"&&l.querySelector(pl(u))||(e=l.createElement("link"),ie(e,"link",t),$t(e),l.head.appendChild(e)))}}function q1(t,e){Rn.m(t,e);var n=di;if(n&&t){var l=e&&typeof e.as=="string"?e.as:"script",o='link[rel="modulepreload"][as="'+Oe(l)+'"][href="'+Oe(t)+'"]',u=o;switch(l){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":u=mi(t)}if(!Ge.has(u)&&(t=v({rel:"modulepreload",href:t},e),Ge.set(u,t),n.querySelector(o)===null)){switch(l){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(n.querySelector(pl(u)))return}l=n.createElement("link"),ie(l,"link",t),$t(l),n.head.appendChild(l)}}}function k1(t,e,n){Rn.S(t,e,n);var l=di;if(l&&t){var o=Va(l).hoistableStyles,u=hi(t);e=e||"default";var h=o.get(u);if(!h){var b={loading:0,preload:null};if(h=l.querySelector(ml(u)))b.loading=5;else{t=v({rel:"stylesheet",href:t,"data-precedence":e},n),(n=Ge.get(u))&&tc(t,n);var T=h=l.createElement("link");$t(T),ie(T,"link",t),T._p=new Promise(function(O,U){T.onload=O,T.onerror=U}),T.addEventListener("load",function(){b.loading|=1}),T.addEventListener("error",function(){b.loading|=2}),b.loading|=4,Js(h,e,l)}h={type:"stylesheet",instance:h,count:1,state:b},o.set(u,h)}}}function X1(t,e){Rn.X(t,e);var n=di;if(n&&t){var l=Va(n).hoistableScripts,o=mi(t),u=l.get(o);u||(u=n.querySelector(pl(o)),u||(t=v({src:t,async:!0},e),(e=Ge.get(o))&&ec(t,e),u=n.createElement("script"),$t(u),ie(u,"link",t),n.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},l.set(o,u))}}function Q1(t,e){Rn.M(t,e);var n=di;if(n&&t){var l=Va(n).hoistableScripts,o=mi(t),u=l.get(o);u||(u=n.querySelector(pl(o)),u||(t=v({src:t,async:!0,type:"module"},e),(e=Ge.get(o))&&ec(t,e),u=n.createElement("script"),$t(u),ie(u,"link",t),n.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},l.set(o,u))}}function Dp(t,e,n,l){var o=(o=ft.current)?Ps(o):null;if(!o)throw Error(r(446));switch(t){case"meta":case"title":return null;case"style":return typeof n.precedence=="string"&&typeof n.href=="string"?(e=hi(n.href),n=Va(o).hoistableStyles,l=n.get(e),l||(l={type:"style",instance:null,count:0,state:null},n.set(e,l)),l):{type:"void",instance:null,count:0,state:null};case"link":if(n.rel==="stylesheet"&&typeof n.href=="string"&&typeof n.precedence=="string"){t=hi(n.href);var u=Va(o).hoistableStyles,h=u.get(t);if(h||(o=o.ownerDocument||o,h={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},u.set(t,h),(u=o.querySelector(ml(t)))&&!u._p&&(h.instance=u,h.state.loading=5),Ge.has(t)||(n={rel:"preload",as:"style",href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},Ge.set(t,n),u||K1(o,t,n,h.state))),e&&l===null)throw Error(r(528,""));return h}if(e&&l!==null)throw Error(r(529,""));return null;case"script":return e=n.async,n=n.src,typeof n=="string"&&e&&typeof e!="function"&&typeof e!="symbol"?(e=mi(n),n=Va(o).hoistableScripts,l=n.get(e),l||(l={type:"script",instance:null,count:0,state:null},n.set(e,l)),l):{type:"void",instance:null,count:0,state:null};default:throw Error(r(444,t))}}function hi(t){return'href="'+Oe(t)+'"'}function ml(t){return'link[rel="stylesheet"]['+t+"]"}function jp(t){return v({},t,{"data-precedence":t.precedence,precedence:null})}function K1(t,e,n,l){t.querySelector('link[rel="preload"][as="style"]['+e+"]")?l.loading=1:(e=t.createElement("link"),l.preload=e,e.addEventListener("load",function(){return l.loading|=1}),e.addEventListener("error",function(){return l.loading|=2}),ie(e,"link",n),$t(e),t.head.appendChild(e))}function mi(t){return'[src="'+Oe(t)+'"]'}function pl(t){return"script[async]"+t}function Op(t,e,n){if(e.count++,e.instance===null)switch(e.type){case"style":var l=t.querySelector('style[data-href~="'+Oe(n.href)+'"]');if(l)return e.instance=l,$t(l),l;var o=v({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return l=(t.ownerDocument||t).createElement("style"),$t(l),ie(l,"style",o),Js(l,n.precedence,t),e.instance=l;case"stylesheet":o=hi(n.href);var u=t.querySelector(ml(o));if(u)return e.state.loading|=4,e.instance=u,$t(u),u;l=jp(n),(o=Ge.get(o))&&tc(l,o),u=(t.ownerDocument||t).createElement("link"),$t(u);var h=u;return h._p=new Promise(function(b,T){h.onload=b,h.onerror=T}),ie(u,"link",l),e.state.loading|=4,Js(u,n.precedence,t),e.instance=u;case"script":return u=mi(n.src),(o=t.querySelector(pl(u)))?(e.instance=o,$t(o),o):(l=n,(o=Ge.get(u))&&(l=v({},n),ec(l,o)),t=t.ownerDocument||t,o=t.createElement("script"),$t(o),ie(o,"link",l),t.head.appendChild(o),e.instance=o);case"void":return null;default:throw Error(r(443,e.type))}else e.type==="stylesheet"&&(e.state.loading&4)===0&&(l=e.instance,e.state.loading|=4,Js(l,n.precedence,t));return e.instance}function Js(t,e,n){for(var l=n.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),o=l.length?l[l.length-1]:null,u=o,h=0;h title"):null)}function Z1(t,e,n){if(n===1||e.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof e.precedence!="string"||typeof e.href!="string"||e.href==="")break;return!0;case"link":if(typeof e.rel!="string"||typeof e.href!="string"||e.href===""||e.onLoad||e.onError)break;return e.rel==="stylesheet"?(t=e.disabled,typeof e.precedence=="string"&&t==null):!0;case"script":if(e.async&&typeof e.async!="function"&&typeof e.async!="symbol"&&!e.onLoad&&!e.onError&&e.src&&typeof e.src=="string")return!0}return!1}function Vp(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function P1(t,e,n,l){if(n.type==="stylesheet"&&(typeof l.media!="string"||matchMedia(l.media).matches!==!1)&&(n.state.loading&4)===0){if(n.instance===null){var o=hi(l.href),u=e.querySelector(ml(o));if(u){e=u._p,e!==null&&typeof e=="object"&&typeof e.then=="function"&&(t.count++,t=Ws.bind(t),e.then(t,t)),n.state.loading|=4,n.instance=u,$t(u);return}u=e.ownerDocument||e,l=jp(l),(o=Ge.get(o))&&tc(l,o),u=u.createElement("link"),$t(u);var h=u;h._p=new Promise(function(b,T){h.onload=b,h.onerror=T}),ie(u,"link",l),n.instance=u}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(n,e),(e=n.state.preload)&&(n.state.loading&3)===0&&(t.count++,n=Ws.bind(t),e.addEventListener("load",n),e.addEventListener("error",n))}}var nc=0;function J1(t,e){return t.stylesheets&&t.count===0&&Is(t,t.stylesheets),0nc?50:800)+e);return t.unsuspend=n,function(){t.unsuspend=null,clearTimeout(l),clearTimeout(o)}}:null}function Ws(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Is(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var $s=null;function Is(t,e){t.stylesheets=null,t.unsuspend!==null&&(t.count++,$s=new Map,e.forEach(F1,t),$s=null,Ws.call(t))}function F1(t,e){if(!(e.state.loading&4)){var n=$s.get(t);if(n)var l=n.get(null);else{n=new Map,$s.set(t,n);for(var o=t.querySelectorAll("link[data-precedence],style[data-precedence]"),u=0;u"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(a)}catch(i){console.error(i)}}return a(),fc.exports=db(),fc.exports}var mb=hb();var ly="popstate";function sy(a){return typeof a=="object"&&a!=null&&"pathname"in a&&"search"in a&&"hash"in a&&"state"in a&&"key"in a}function pb(a={}){function i(r,c){let d=c.state?.masked,{pathname:f,search:m,hash:y}=d||r.location;return _c("",{pathname:f,search:m,hash:y},c.state&&c.state.usr||null,c.state&&c.state.key||"default",d?{pathname:r.location.pathname,search:r.location.search,hash:r.location.hash}:void 0)}function s(r,c){return typeof c=="string"?c:jl(c)}return gb(i,s,null,a)}function Ut(a,i){if(a===!1||a===null||typeof a>"u")throw new Error(i)}function sn(a,i){if(!a){typeof console<"u"&&console.warn(i);try{throw new Error(i)}catch{}}}function yb(){return Math.random().toString(36).substring(2,10)}function ry(a,i){return{usr:a.state,key:a.key,idx:i,masked:a.mask?{pathname:a.pathname,search:a.search,hash:a.hash}:void 0}}function _c(a,i,s=null,r,c){return{pathname:typeof a=="string"?a:a.pathname,search:"",hash:"",...typeof i=="string"?Ti(i):i,state:s,key:i&&i.key||r||yb(),mask:c}}function jl({pathname:a="/",search:i="",hash:s=""}){return i&&i!=="?"&&(a+=i.charAt(0)==="?"?i:"?"+i),s&&s!=="#"&&(a+=s.charAt(0)==="#"?s:"#"+s),a}function Ti(a){let i={};if(a){let s=a.indexOf("#");s>=0&&(i.hash=a.substring(s),a=a.substring(0,s));let r=a.indexOf("?");r>=0&&(i.search=a.substring(r),a=a.substring(0,r)),a&&(i.pathname=a)}return i}function gb(a,i,s,r={}){let{window:c=document.defaultView,v5Compat:d=!1}=r,f=c.history,m="POP",y=null,p=g();p==null&&(p=0,f.replaceState({...f.state,idx:p},""));function g(){return(f.state||{idx:null}).idx}function v(){m="POP";let w=g(),V=w==null?null:w-p;p=w,y&&y({action:m,location:D.location,delta:V})}function S(w,V){m="PUSH";let G=sy(w)?w:_c(D.location,w,V);p=g()+1;let B=ry(G,p),k=D.createHref(G.mask||G);try{f.pushState(B,"",k)}catch(P){if(P instanceof DOMException&&P.name==="DataCloneError")throw P;c.location.assign(k)}d&&y&&y({action:m,location:D.location,delta:1})}function R(w,V){m="REPLACE";let G=sy(w)?w:_c(D.location,w,V);p=g();let B=ry(G,p),k=D.createHref(G.mask||G);f.replaceState(B,"",k),d&&y&&y({action:m,location:D.location,delta:0})}function M(w){return vb(w)}let D={get action(){return m},get location(){return a(c,f)},listen(w){if(y)throw new Error("A history only accepts one active listener");return c.addEventListener(ly,v),y=w,()=>{c.removeEventListener(ly,v),y=null}},createHref(w){return i(c,w)},createURL:M,encodeLocation(w){let V=M(w);return{pathname:V.pathname,search:V.search,hash:V.hash}},push:S,replace:R,go(w){return f.go(w)}};return D}function vb(a,i=!1){let s="http://localhost";typeof window<"u"&&(s=window.location.origin!=="null"?window.location.origin:window.location.href),Ut(s,"No window.location.(origin|href) available to create URL");let r=typeof a=="string"?a:jl(a);return r=r.replace(/ $/,"%20"),!i&&r.startsWith("//")&&(r=s+r),new URL(r,s)}function D0(a,i,s="/"){return xb(a,i,s,!1)}function xb(a,i,s,r,c){let d=typeof i=="string"?Ti(i):i,f=Nn(d.pathname||"/",s);if(f==null)return null;let m=bb(a),y=null,p=Ob(f);for(let g=0;y==null&&g{let g={relativePath:p===void 0?f.path||"":p,caseSensitive:f.caseSensitive===!0,childrenIndex:m,route:f};if(g.relativePath.startsWith("/")){if(!g.relativePath.startsWith(r)&&y)return;Ut(g.relativePath.startsWith(r),`Absolute route path "${g.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),g.relativePath=g.relativePath.slice(r.length)}let v=Fe([r,g.relativePath]),S=s.concat(g);f.children&&f.children.length>0&&(Ut(f.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${v}".`),j0(f.children,i,S,v,y)),!(f.path==null&&!f.index)&&i.push({path:v,score:Mb(v,f.index),routesMeta:S})};return a.forEach((f,m)=>{if(f.path===""||!f.path?.includes("?"))d(f,m);else for(let y of O0(f.path))d(f,m,!0,y)}),i}function O0(a){let i=a.split("/");if(i.length===0)return[];let[s,...r]=i,c=s.endsWith("?"),d=s.replace(/\?$/,"");if(r.length===0)return c?[d,""]:[d];let f=O0(r.join("/")),m=[];return m.push(...f.map(y=>y===""?d:[d,y].join("/"))),c&&m.push(...f),m.map(y=>a.startsWith("/")&&y===""?"/":y)}function Sb(a){a.sort((i,s)=>i.score!==s.score?s.score-i.score:Nb(i.routesMeta.map(r=>r.childrenIndex),s.routesMeta.map(r=>r.childrenIndex)))}var Tb=/^:[\w-]+$/,Eb=3,Ab=2,Cb=1,Rb=10,wb=-2,oy=a=>a==="*";function Mb(a,i){let s=a.split("/"),r=s.length;return s.some(oy)&&(r+=wb),i&&(r+=Ab),s.filter(c=>!oy(c)).reduce((c,d)=>c+(Tb.test(d)?Eb:d===""?Cb:Rb),r)}function Nb(a,i){return a.length===i.length&&a.slice(0,-1).every((r,c)=>r===i[c])?a[a.length-1]-i[i.length-1]:0}function Db(a,i,s=!1){let{routesMeta:r}=a,c={},d="/",f=[];for(let m=0;m{if(g==="*"){let M=m[S]||"";f=d.slice(0,d.length-M.length).replace(/(.)\/+$/,"$1")}const R=m[S];return v&&!R?p[g]=void 0:p[g]=(R||"").replace(/%2F/g,"/"),p},{}),pathname:d,pathnameBase:f,pattern:a}}function jb(a,i=!1,s=!0){sn(a==="*"||!a.endsWith("*")||a.endsWith("/*"),`Route path "${a}" will be treated as if it were "${a.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${a.replace(/\*$/,"/*")}".`);let r=[],c="^"+a.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(f,m,y,p,g)=>{if(r.push({paramName:m,isOptional:y!=null}),y){let v=g.charAt(p+f.length);return v&&v!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return a.endsWith("*")?(r.push({paramName:"*"}),c+=a==="*"||a==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):s?c+="\\/*$":a!==""&&a!=="/"&&(c+="(?:(?=\\/|$))"),[new RegExp(c,i?void 0:"i"),r]}function Ob(a){try{return a.split("/").map(i=>decodeURIComponent(i).replace(/\//g,"%2F")).join("/")}catch(i){return sn(!1,`The URL path "${a}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${i}).`),a}}function Nn(a,i){if(i==="/")return a;if(!a.toLowerCase().startsWith(i.toLowerCase()))return null;let s=i.endsWith("/")?i.length-1:i.length,r=a.charAt(s);return r&&r!=="/"?null:a.slice(s)||"/"}var zb=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function Lb(a,i="/"){let{pathname:s,search:r="",hash:c=""}=typeof a=="string"?Ti(a):a,d;return s?(s=L0(s),s.startsWith("/")?d=uy(s.substring(1),"/"):d=uy(s,i)):d=i,{pathname:d,search:Ub(r),hash:Bb(c)}}function uy(a,i){let s=Rr(i).split("/");return a.split("/").forEach(c=>{c===".."?s.length>1&&s.pop():c!=="."&&s.push(c)}),s.length>1?s.join("/"):"/"}function pc(a,i,s,r){return`Cannot include a '${a}' character in a manually specified \`to.${i}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${s}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function Vb(a){return a.filter((i,s)=>s===0||i.route.path&&i.route.path.length>0)}function z0(a){let i=Vb(a);return i.map((s,r)=>r===i.length-1?s.pathname:s.pathnameBase)}function df(a,i,s,r=!1){let c;typeof a=="string"?c=Ti(a):(c={...a},Ut(!c.pathname||!c.pathname.includes("?"),pc("?","pathname","search",c)),Ut(!c.pathname||!c.pathname.includes("#"),pc("#","pathname","hash",c)),Ut(!c.search||!c.search.includes("#"),pc("#","search","hash",c)));let d=a===""||c.pathname==="",f=d?"/":c.pathname,m;if(f==null)m=s;else{let v=i.length-1;if(!r&&f.startsWith("..")){let S=f.split("/");for(;S[0]==="..";)S.shift(),v-=1;c.pathname=S.join("/")}m=v>=0?i[v]:"/"}let y=Lb(c,m),p=f&&f!=="/"&&f.endsWith("/"),g=(d||f===".")&&s.endsWith("/");return!y.pathname.endsWith("/")&&(p||g)&&(y.pathname+="/"),y}var L0=a=>a.replace(/\/\/+/g,"/"),Fe=a=>L0(a.join("/")),Rr=a=>a.replace(/\/+$/,""),_b=a=>Rr(a).replace(/^\/*/,"/"),Ub=a=>!a||a==="?"?"":a.startsWith("?")?a:"?"+a,Bb=a=>!a||a==="#"?"":a.startsWith("#")?a:"#"+a,Hb=class{constructor(a,i,s,r=!1){this.status=a,this.statusText=i||"",this.internal=r,s instanceof Error?(this.data=s.toString(),this.error=s):this.data=s}};function Gb(a){return a!=null&&typeof a.status=="number"&&typeof a.statusText=="string"&&typeof a.internal=="boolean"&&"data"in a}function Yb(a){let i=a.map(s=>s.route.path).filter(Boolean);return Fe(i)||"/"}var V0=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function _0(a,i){let s=a;if(typeof s!="string"||!zb.test(s))return{absoluteURL:void 0,isExternal:!1,to:s};let r=s,c=!1;if(V0)try{let d=new URL(window.location.href),f=s.startsWith("//")?new URL(d.protocol+s):new URL(s),m=Nn(f.pathname,i);f.origin===d.origin&&m!=null?s=m+f.search+f.hash:c=!0}catch{sn(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:c,to:s}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var U0=["POST","PUT","PATCH","DELETE"];new Set(U0);var qb=["GET",...U0];new Set(qb);var Ei=E.createContext(null);Ei.displayName="DataRouter";var Ur=E.createContext(null);Ur.displayName="DataRouterState";var B0=E.createContext(!1);function kb(){return E.useContext(B0)}var H0=E.createContext({isTransitioning:!1});H0.displayName="ViewTransition";var Xb=E.createContext(new Map);Xb.displayName="Fetchers";var Qb=E.createContext(null);Qb.displayName="Await";var Xe=E.createContext(null);Xe.displayName="Navigation";var _l=E.createContext(null);_l.displayName="Location";var Dn=E.createContext({outlet:null,matches:[],isDataRoute:!1});Dn.displayName="Route";var hf=E.createContext(null);hf.displayName="RouteError";var G0="REACT_ROUTER_ERROR",Kb="REDIRECT",Zb="ROUTE_ERROR_RESPONSE";function Pb(a){if(a.startsWith(`${G0}:${Kb}:{`))try{let i=JSON.parse(a.slice(28));if(typeof i=="object"&&i&&typeof i.status=="number"&&typeof i.statusText=="string"&&typeof i.location=="string"&&typeof i.reloadDocument=="boolean"&&typeof i.replace=="boolean")return i}catch{}}function Jb(a){if(a.startsWith(`${G0}:${Zb}:{`))try{let i=JSON.parse(a.slice(40));if(typeof i=="object"&&i&&typeof i.status=="number"&&typeof i.statusText=="string")return new Hb(i.status,i.statusText,i.data)}catch{}}function Fb(a,{relative:i}={}){Ut(Ul(),"useHref() may be used only in the context of a component.");let{basename:s,navigator:r}=E.useContext(Xe),{hash:c,pathname:d,search:f}=Bl(a,{relative:i}),m=d;return s!=="/"&&(m=d==="/"?s:Fe([s,d])),r.createHref({pathname:m,search:f,hash:c})}function Ul(){return E.useContext(_l)!=null}function on(){return Ut(Ul(),"useLocation() may be used only in the context of a component."),E.useContext(_l).location}var Y0="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function q0(a){E.useContext(Xe).static||E.useLayoutEffect(a)}function Wb(){let{isDataRoute:a}=E.useContext(Dn);return a?c2():$b()}function $b(){Ut(Ul(),"useNavigate() may be used only in the context of a component.");let a=E.useContext(Ei),{basename:i,navigator:s}=E.useContext(Xe),{matches:r}=E.useContext(Dn),{pathname:c}=on(),d=JSON.stringify(z0(r)),f=E.useRef(!1);return q0(()=>{f.current=!0}),E.useCallback((y,p={})=>{if(sn(f.current,Y0),!f.current)return;if(typeof y=="number"){s.go(y);return}let g=df(y,JSON.parse(d),c,p.relative==="path");a==null&&i!=="/"&&(g.pathname=g.pathname==="/"?i:Fe([i,g.pathname])),(p.replace?s.replace:s.push)(g,p.state,p)},[i,s,d,c,a])}E.createContext(null);function Bl(a,{relative:i}={}){let{matches:s}=E.useContext(Dn),{pathname:r}=on(),c=JSON.stringify(z0(s));return E.useMemo(()=>df(a,JSON.parse(c),r,i==="path"),[a,c,r,i])}function Ib(a,i){return k0(a,i)}function k0(a,i,s){Ut(Ul(),"useRoutes() may be used only in the context of a component.");let{navigator:r}=E.useContext(Xe),{matches:c}=E.useContext(Dn),d=c[c.length-1],f=d?d.params:{},m=d?d.pathname:"/",y=d?d.pathnameBase:"/",p=d&&d.route;{let w=p&&p.path||"";Q0(m,!p||w.endsWith("*")||w.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${m}" (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 g=on(),v;if(i){let w=typeof i=="string"?Ti(i):i;Ut(y==="/"||w.pathname?.startsWith(y),`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 "${y}" but pathname "${w.pathname}" was given in the \`location\` prop.`),v=w}else v=g;let S=v.pathname||"/",R=S;if(y!=="/"){let w=y.replace(/^\//,"").split("/");R="/"+S.replace(/^\//,"").split("/").slice(w.length).join("/")}let M=s&&s.state.matches.length?s.state.matches.map(w=>Object.assign(w,{route:s.manifest[w.route.id]||w.route})):D0(a,{pathname:R});sn(p||M!=null,`No routes matched location "${v.pathname}${v.search}${v.hash}" `),sn(M==null||M[M.length-1].route.element!==void 0||M[M.length-1].route.Component!==void 0||M[M.length-1].route.lazy!==void 0,`Matched leaf route at location "${v.pathname}${v.search}${v.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 D=i2(M&&M.map(w=>Object.assign({},w,{params:Object.assign({},f,w.params),pathname:Fe([y,r.encodeLocation?r.encodeLocation(w.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:w.pathname]),pathnameBase:w.pathnameBase==="/"?y:Fe([y,r.encodeLocation?r.encodeLocation(w.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:w.pathnameBase])})),c,s);return i&&D?E.createElement(_l.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",mask:void 0,...v},navigationType:"POP"}},D):D}function t2(){let a=u2(),i=Gb(a)?`${a.status} ${a.statusText}`:a instanceof Error?a.message:JSON.stringify(a),s=a instanceof Error?a.stack:null,r="rgba(200,200,200, 0.5)",c={padding:"0.5rem",backgroundColor:r},d={padding:"2px 4px",backgroundColor:r},f=null;return console.error("Error handled by React Router default ErrorBoundary:",a),f=E.createElement(E.Fragment,null,E.createElement("p",null,"💿 Hey developer 👋"),E.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",E.createElement("code",{style:d},"ErrorBoundary")," or"," ",E.createElement("code",{style:d},"errorElement")," prop on your route.")),E.createElement(E.Fragment,null,E.createElement("h2",null,"Unexpected Application Error!"),E.createElement("h3",{style:{fontStyle:"italic"}},i),s?E.createElement("pre",{style:c},s):null,f)}var e2=E.createElement(t2,null),X0=class extends E.Component{constructor(a){super(a),this.state={location:a.location,revalidation:a.revalidation,error:a.error}}static getDerivedStateFromError(a){return{error:a}}static getDerivedStateFromProps(a,i){return i.location!==a.location||i.revalidation!=="idle"&&a.revalidation==="idle"?{error:a.error,location:a.location,revalidation:a.revalidation}:{error:a.error!==void 0?a.error:i.error,location:i.location,revalidation:a.revalidation||i.revalidation}}componentDidCatch(a,i){this.props.onError?this.props.onError(a,i):console.error("React Router caught the following error during render",a)}render(){let a=this.state.error;if(this.context&&typeof a=="object"&&a&&"digest"in a&&typeof a.digest=="string"){const s=Jb(a.digest);s&&(a=s)}let i=a!==void 0?E.createElement(Dn.Provider,{value:this.props.routeContext},E.createElement(hf.Provider,{value:a,children:this.props.component})):this.props.children;return this.context?E.createElement(n2,{error:a},i):i}};X0.contextType=B0;var yc=new WeakMap;function n2({children:a,error:i}){let{basename:s}=E.useContext(Xe);if(typeof i=="object"&&i&&"digest"in i&&typeof i.digest=="string"){let r=Pb(i.digest);if(r){let c=yc.get(i);if(c)throw c;let d=_0(r.location,s);if(V0&&!yc.get(i))if(d.isExternal||r.reloadDocument)window.location.href=d.absoluteURL||d.to;else{const f=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(d.to,{replace:r.replace}));throw yc.set(i,f),f}return E.createElement("meta",{httpEquiv:"refresh",content:`0;url=${d.absoluteURL||d.to}`})}}return a}function a2({routeContext:a,match:i,children:s}){let r=E.useContext(Ei);return r&&r.static&&r.staticContext&&(i.route.errorElement||i.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=i.route.id),E.createElement(Dn.Provider,{value:a},s)}function i2(a,i=[],s){let r=s?.state;if(a==null){if(!r)return null;if(r.errors)a=r.matches;else if(i.length===0&&!r.initialized&&r.matches.length>0)a=r.matches;else return null}let c=a,d=r?.errors;if(d!=null){let g=c.findIndex(v=>v.route.id&&d?.[v.route.id]!==void 0);Ut(g>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(d).join(",")}`),c=c.slice(0,Math.min(c.length,g+1))}let f=!1,m=-1;if(s&&r){f=r.renderFallback;for(let g=0;g=0?c=c.slice(0,m+1):c=[c[0]];break}}}}let y=s?.onError,p=r&&y?(g,v)=>{y(g,{location:r.location,params:r.matches?.[0]?.params??{},pattern:Yb(r.matches),errorInfo:v})}:void 0;return c.reduceRight((g,v,S)=>{let R,M=!1,D=null,w=null;r&&(R=d&&v.route.id?d[v.route.id]:void 0,D=v.route.errorElement||e2,f&&(m<0&&S===0?(Q0("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),M=!0,w=null):m===S&&(M=!0,w=v.route.hydrateFallbackElement||null)));let V=i.concat(c.slice(0,S+1)),G=()=>{let B;return R?B=D:M?B=w:v.route.Component?B=E.createElement(v.route.Component,null):v.route.element?B=v.route.element:B=g,E.createElement(a2,{match:v,routeContext:{outlet:g,matches:V,isDataRoute:r!=null},children:B})};return r&&(v.route.ErrorBoundary||v.route.errorElement||S===0)?E.createElement(X0,{location:r.location,revalidation:r.revalidation,component:D,error:R,children:G(),routeContext:{outlet:null,matches:V,isDataRoute:!0},onError:p}):G()},null)}function mf(a){return`${a} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function l2(a){let i=E.useContext(Ei);return Ut(i,mf(a)),i}function s2(a){let i=E.useContext(Ur);return Ut(i,mf(a)),i}function r2(a){let i=E.useContext(Dn);return Ut(i,mf(a)),i}function pf(a){let i=r2(a),s=i.matches[i.matches.length-1];return Ut(s.route.id,`${a} can only be used on routes that contain a unique "id"`),s.route.id}function o2(){return pf("useRouteId")}function u2(){let a=E.useContext(hf),i=s2("useRouteError"),s=pf("useRouteError");return a!==void 0?a:i.errors?.[s]}function c2(){let{router:a}=l2("useNavigate"),i=pf("useNavigate"),s=E.useRef(!1);return q0(()=>{s.current=!0}),E.useCallback(async(c,d={})=>{sn(s.current,Y0),s.current&&(typeof c=="number"?await a.navigate(c):await a.navigate(c,{fromRouteId:i,...d}))},[a,i])}var cy={};function Q0(a,i,s){!i&&!cy[a]&&(cy[a]=!0,sn(!1,s))}E.memo(f2);function f2({routes:a,manifest:i,future:s,state:r,isStatic:c,onError:d}){return k0(a,void 0,{manifest:i,state:r,isStatic:c,onError:d})}function hr(a){Ut(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function d2({basename:a="/",children:i=null,location:s,navigationType:r="POP",navigator:c,static:d=!1,useTransitions:f}){Ut(!Ul(),"You cannot render a inside another . You should never have more than one in your app.");let m=a.replace(/^\/*/,"/"),y=E.useMemo(()=>({basename:m,navigator:c,static:d,useTransitions:f,future:{}}),[m,c,d,f]);typeof s=="string"&&(s=Ti(s));let{pathname:p="/",search:g="",hash:v="",state:S=null,key:R="default",mask:M}=s,D=E.useMemo(()=>{let w=Nn(p,m);return w==null?null:{location:{pathname:w,search:g,hash:v,state:S,key:R,mask:M},navigationType:r}},[m,p,g,v,S,R,r,M]);return sn(D!=null,` is not able to match the URL "${p}${g}${v}" because it does not start with the basename, so the won't render anything.`),D==null?null:E.createElement(Xe.Provider,{value:y},E.createElement(_l.Provider,{children:i,value:D}))}function h2({children:a,location:i}){return Ib(Uc(a),i)}function Uc(a,i=[]){let s=[];return E.Children.forEach(a,(r,c)=>{if(!E.isValidElement(r))return;let d=[...i,c];if(r.type===E.Fragment){s.push.apply(s,Uc(r.props.children,d));return}Ut(r.type===hr,`[${typeof r.type=="string"?r.type:r.type.name}] is not a component. All component children of must be a or `),Ut(!r.props.index||!r.props.children,"An index route cannot have child routes.");let f={id:r.props.id||d.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&&(f.children=Uc(r.props.children,d)),s.push(f)}),s}var mr="get",pr="application/x-www-form-urlencoded";function Br(a){return typeof HTMLElement<"u"&&a instanceof HTMLElement}function m2(a){return Br(a)&&a.tagName.toLowerCase()==="button"}function p2(a){return Br(a)&&a.tagName.toLowerCase()==="form"}function y2(a){return Br(a)&&a.tagName.toLowerCase()==="input"}function g2(a){return!!(a.metaKey||a.altKey||a.ctrlKey||a.shiftKey)}function v2(a,i){return a.button===0&&(!i||i==="_self")&&!g2(a)}var rr=null;function x2(){if(rr===null)try{new FormData(document.createElement("form"),0),rr=!1}catch{rr=!0}return rr}var b2=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function gc(a){return a!=null&&!b2.has(a)?(sn(!1,`"${a}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${pr}"`),null):a}function S2(a,i){let s,r,c,d,f;if(p2(a)){let m=a.getAttribute("action");r=m?Nn(m,i):null,s=a.getAttribute("method")||mr,c=gc(a.getAttribute("enctype"))||pr,d=new FormData(a)}else if(m2(a)||y2(a)&&(a.type==="submit"||a.type==="image")){let m=a.form;if(m==null)throw new Error('Cannot submit a + ); +} diff --git a/website/src/features/dashboard/dashboard.css b/website/src/features/dashboard/dashboard.css new file mode 100644 index 0000000..0332407 --- /dev/null +++ b/website/src/features/dashboard/dashboard.css @@ -0,0 +1,18 @@ +/* Sorelax dashboard — industrial telemetry aesthetic */ + +.dashboard-root { + --coral: #38bdb0; + --coral-dim: rgba(56, 189, 176, 0.12); +} + +.dashboard-grid { + background-image: + linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px); + background-size: 24px 24px; + mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, black 20%, transparent 70%); +} + +.dashboard-root ::selection { + background: rgba(56, 189, 176, 0.25); +} diff --git a/website/src/features/dashboard/hooks/useDashboardStatus.ts b/website/src/features/dashboard/hooks/useDashboardStatus.ts new file mode 100644 index 0000000..e40911a --- /dev/null +++ b/website/src/features/dashboard/hooks/useDashboardStatus.ts @@ -0,0 +1,15 @@ +import { useMemo } from 'react'; +import type { DashboardStatus } from '../types'; + +/** Placeholder status until the FastAPI layer is wired. */ +export function useDashboardStatus(): DashboardStatus { + return useMemo( + () => ({ + agentState: 'active', + schedulerRunning: true, + lastRefresh: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), + nextRefresh: new Date(Date.now() + 4 * 60 * 60 * 1000).toISOString(), + }), + [], + ); +} diff --git a/website/src/features/dashboard/layout/DashboardShell.tsx b/website/src/features/dashboard/layout/DashboardShell.tsx new file mode 100644 index 0000000..d123a91 --- /dev/null +++ b/website/src/features/dashboard/layout/DashboardShell.tsx @@ -0,0 +1,64 @@ +import { useCallback, useEffect, useState, type ComponentType } from 'react'; +import { AnimatePresence, motion } from 'framer-motion'; +import { Sidebar } from './Sidebar'; +import { TopBar } from './TopBar'; +import { DashboardPanel } from '../panels/DashboardPanel'; +import { AskPanel } from '../panels/AskPanel'; +import { LogsPanel } from '../panels/LogsPanel'; +import { SourcesPanel } from '../panels/SourcesPanel'; +import { SettingsPanel } from '../panels/SettingsPanel'; +import { NAV_ITEMS } from '../nav'; +import type { DashboardSection } from '../types'; +import '../dashboard.css'; + +const PANELS: Record = { + dashboard: DashboardPanel, + ask: AskPanel, + logs: LogsPanel, + sources: SourcesPanel, + settings: SettingsPanel, +}; + +export function DashboardShell() { + const [active, setActive] = useState('dashboard'); + + const handleKeyDown = useCallback((e: KeyboardEvent) => { + if (e.metaKey || e.ctrlKey || e.altKey) return; + const match = NAV_ITEMS.find((item) => item.shortcut === e.key); + if (match) { + e.preventDefault(); + setActive(match.id); + } + }, []); + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [handleKeyDown]); + + const ActivePanel = PANELS[active]; + + return ( +
+
+ + +
+ +
+ + + + + +
+
+
+ ); +} diff --git a/website/src/features/dashboard/layout/Sidebar.tsx b/website/src/features/dashboard/layout/Sidebar.tsx new file mode 100644 index 0000000..2286b2f --- /dev/null +++ b/website/src/features/dashboard/layout/Sidebar.tsx @@ -0,0 +1,75 @@ +import { NAV_ITEMS } from '../nav'; +import { SidebarNavItem } from '../components/SidebarNavItem'; +import { GitHubIcon, LinearIcon, NotionIcon, SlackIcon } from '@/lib/icons'; +import type { DashboardSection } from '../types'; + +interface SidebarProps { + active: DashboardSection; + onNavigate: (section: DashboardSection) => void; +} + +const SOURCE_ICONS = [ + { name: 'GitHub', Icon: GitHubIcon, connected: true }, + { name: 'Linear', Icon: LinearIcon, connected: true }, + { name: 'Slack', Icon: SlackIcon, connected: true }, + { name: 'Notion', Icon: NotionIcon, connected: false }, +] as const; + +export function Sidebar({ active, onNavigate }: SidebarProps) { + return ( + + ); +} diff --git a/website/src/features/dashboard/layout/TopBar.tsx b/website/src/features/dashboard/layout/TopBar.tsx new file mode 100644 index 0000000..1a9dffe --- /dev/null +++ b/website/src/features/dashboard/layout/TopBar.tsx @@ -0,0 +1,89 @@ +import { RefreshCw } from 'lucide-react'; +import { AgentStatusBadge } from '../components/AgentStatusBadge'; +import { useDashboardStatus } from '../hooks/useDashboardStatus'; +import type { DashboardSection } from '../types'; +import { NAV_ITEMS } from '../nav'; + +interface TopBarProps { + activeSection: DashboardSection; +} + +function formatRelativeTime(iso: string | null): string { + if (!iso) return 'never'; + const diff = Date.now() - new Date(iso).getTime(); + const hours = Math.floor(diff / (1000 * 60 * 60)); + const minutes = Math.floor(diff / (1000 * 60)); + if (hours >= 1) return `${hours}h ago`; + if (minutes >= 1) return `${minutes}m ago`; + return 'just now'; +} + +function formatAbsoluteTime(iso: string | null): string { + if (!iso) return '—'; + return new Date(iso).toLocaleString(undefined, { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +} + +export function TopBar({ activeSection }: TopBarProps) { + const status = useDashboardStatus(); + const section = NAV_ITEMS.find((n) => n.id === activeSection); + + return ( +
+
+
+

{section?.label ?? 'Dashboard'}

+

{section?.description}

+
+
+ +
+
+
+

+ Last refresh +

+

+ {formatRelativeTime(status.lastRefresh)} +

+
+
+
+

+ Next refresh +

+

+ {status.schedulerRunning && status.nextRefresh + ? formatAbsoluteTime(status.nextRefresh) + : 'manual'} +

+
+
+ + + + +
+
+ ); +} diff --git a/website/src/features/dashboard/nav.ts b/website/src/features/dashboard/nav.ts new file mode 100644 index 0000000..1f2a28d --- /dev/null +++ b/website/src/features/dashboard/nav.ts @@ -0,0 +1,55 @@ +import { + LayoutDashboard, + MessageSquareText, + ScrollText, + Plug, + Settings, + type LucideIcon, +} from 'lucide-react'; +import type { DashboardSection } from './types'; + +export interface NavItem { + id: DashboardSection; + label: string; + description: string; + icon: LucideIcon; + shortcut?: string; +} + +export const NAV_ITEMS: NavItem[] = [ + { + id: 'dashboard', + label: 'Dashboard', + description: 'Unified project context feed', + icon: LayoutDashboard, + shortcut: '1', + }, + { + id: 'ask', + label: 'Ask', + description: 'Coral SQL on-demand search', + icon: MessageSquareText, + shortcut: '2', + }, + { + id: 'logs', + label: 'Logs', + description: 'Refresh history & diagnostics', + icon: ScrollText, + shortcut: '3', + }, + { + id: 'sources', + label: 'Sources', + description: 'GitHub, Linear, Slack, Notion', + icon: Plug, + shortcut: '4', + }, + { + id: 'settings', + label: 'Settings', + description: 'Agent & scheduler config', + icon: Settings, + shortcut: '5', + }, +]; diff --git a/website/src/features/dashboard/panels/AskPanel.tsx b/website/src/features/dashboard/panels/AskPanel.tsx new file mode 100644 index 0000000..0b6b660 --- /dev/null +++ b/website/src/features/dashboard/panels/AskPanel.tsx @@ -0,0 +1,49 @@ +import { Search } from 'lucide-react'; +import { PanelShell, PlaceholderBlock } from '../components/PanelShell'; + +export function AskPanel() { + return ( +
+ +
+ + +

+ Press Enter to run on_demand.sql · keyword extracted automatically +

+
+
+ + +
+
+ Source + Title + Detail + Extra +
+ {[1, 2, 3].map((row) => ( +
+ + + + +
+ ))} +
+
+
+ ); +} diff --git a/website/src/features/dashboard/panels/DashboardPanel.tsx b/website/src/features/dashboard/panels/DashboardPanel.tsx new file mode 100644 index 0000000..b3b2d25 --- /dev/null +++ b/website/src/features/dashboard/panels/DashboardPanel.tsx @@ -0,0 +1,43 @@ +import { PanelShell, PlaceholderBlock, EmptyMetric } from '../components/PanelShell'; + +const CONTEXT_SECTIONS = [ + { key: 'active_work', label: 'Active work' }, + { key: 'recent_commits', label: 'Recent commits' }, + { key: 'open_prs', label: 'Open PRs' }, + { key: 'sprint_goal', label: 'Sprint goal' }, + { key: 'key_decisions', label: 'Key decisions' }, + { key: 'slack_threads', label: 'Slack threads' }, + { key: 'architecture', label: 'Architecture docs' }, +] as const; + +export function DashboardPanel() { + return ( +
+
+ + + +
+ + +
+ {CONTEXT_SECTIONS.map((section) => ( +
+

+ {section.label} +

+ +
+ ))} +
+
+
+ ); +} diff --git a/website/src/features/dashboard/panels/LogsPanel.tsx b/website/src/features/dashboard/panels/LogsPanel.tsx new file mode 100644 index 0000000..d4ff667 --- /dev/null +++ b/website/src/features/dashboard/panels/LogsPanel.tsx @@ -0,0 +1,41 @@ +import { PanelShell, PlaceholderBlock } from '../components/PanelShell'; + +const MOCK_LOGS = [ + { time: '2026-05-30 08:14', rows: 847, status: 'ok' as const }, + { time: '2026-05-30 02:14', rows: 831, status: 'ok' as const }, + { time: '2026-05-29 20:14', rows: 819, status: 'ok' as const }, + { time: '2026-05-29 14:14', rows: 0, status: 'error' as const }, + { time: '2026-05-29 08:14', rows: 802, status: 'ok' as const }, +]; + +export function LogsPanel() { + return ( + +
+
+ Timestamp + Rows + Status + Source counts +
+ {MOCK_LOGS.map((log, i) => ( +
+ {log.time} + {log.rows || '—'} + + {log.status} + + +
+ ))} +
+
+ ); +} diff --git a/website/src/features/dashboard/panels/SettingsPanel.tsx b/website/src/features/dashboard/panels/SettingsPanel.tsx new file mode 100644 index 0000000..3442fbf --- /dev/null +++ b/website/src/features/dashboard/panels/SettingsPanel.tsx @@ -0,0 +1,53 @@ +import { PanelShell } from '../components/PanelShell'; + +const SETTINGS_GROUPS = [ + { + title: 'Repository', + fields: [ + { label: 'GITHUB_OWNER', value: '—', hint: 'from .env' }, + { label: 'GITHUB_REPO', value: '—', hint: 'from .env' }, + ], + }, + { + title: 'Scheduler', + fields: [ + { label: 'Interval', value: '6 hours', hint: 'APScheduler daemon' }, + { label: 'PID file', value: '~/.sorelax/sorelax.pid', hint: '' }, + ], + }, + { + title: 'Paths', + fields: [ + { label: 'Context', value: '~/.sorelax/project_context.json', hint: '' }, + { label: 'Logs', value: '~/.sorelax/project_log.jsonl', hint: '' }, + { label: 'Output', value: './CLAUDE.md', hint: 'repo root' }, + ], + }, +] as const; + +export function SettingsPanel() { + return ( +
+ {SETTINGS_GROUPS.map((group) => ( + +
+ {group.fields.map((field) => ( +
+
{field.label}
+
+ {field.value} + {field.hint && ( + {field.hint} + )} +
+
+ ))} +
+
+ ))} +
+ ); +} diff --git a/website/src/features/dashboard/panels/SourcesPanel.tsx b/website/src/features/dashboard/panels/SourcesPanel.tsx new file mode 100644 index 0000000..18d0132 --- /dev/null +++ b/website/src/features/dashboard/panels/SourcesPanel.tsx @@ -0,0 +1,84 @@ +import { GitHubIcon, LinearIcon, NotionIcon, SlackIcon } from '@/lib/icons'; +import { PanelShell } from '../components/PanelShell'; + +const SOURCES = [ + { + name: 'GitHub', + Icon: GitHubIcon, + connected: true, + tables: ['github.commits', 'github.pull_requests'], + count: '312 commits · 4 PRs', + }, + { + name: 'Linear', + Icon: LinearIcon, + connected: true, + tables: ['linear.issues'], + count: '28 open issues', + }, + { + name: 'Slack', + Icon: SlackIcon, + connected: true, + tables: ['slack.messages'], + count: '1.2k messages (7d)', + }, + { + name: 'Notion', + Icon: NotionIcon, + connected: false, + tables: ['notion.pages'], + count: 'Not connected', + }, +] as const; + +export function SourcesPanel() { + return ( +
+ {SOURCES.map(({ name, Icon, connected, tables, count }) => ( + +
+
+ +
+
+

{count}

+
+ {tables.map((table) => ( + + {table} + + ))} +
+
+ + + {connected ? 'Coral source healthy' : 'Run coral source add notion'} + +
+
+
+
+ ))} +
+ ); +} diff --git a/website/src/features/dashboard/types.ts b/website/src/features/dashboard/types.ts new file mode 100644 index 0000000..bc546dc --- /dev/null +++ b/website/src/features/dashboard/types.ts @@ -0,0 +1,15 @@ +export type DashboardSection = + | 'dashboard' + | 'ask' + | 'logs' + | 'sources' + | 'settings'; + +export type AgentState = 'active' | 'idle' | 'stopped' | 'error'; + +export interface DashboardStatus { + agentState: AgentState; + schedulerRunning: boolean; + lastRefresh: string | null; + nextRefresh: string | null; +} diff --git a/website/tailwind.config.js b/website/tailwind.config.js index acc7a15..52cec2c 100644 --- a/website/tailwind.config.js +++ b/website/tailwind.config.js @@ -5,7 +5,7 @@ module.exports = { theme: { extend: { fontFamily: { - sans: ['Inter', 'system-ui', 'sans-serif'], + sans: ['IBM Plex Sans', 'Inter', 'system-ui', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'], }, colors: { @@ -19,6 +19,10 @@ module.exports = { 'accent-hover': '#2563eb', success: '#22c55e', 'code-bg': '#0d0d0d', + coral: { + 400: '#38bdb0', + 500: '#2da89c', + }, }, borderRadius: { card: '8px',