From 33a6d0be074adc9d48eaeec0f49864f605e460ab Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:15:10 +0530 Subject: [PATCH 01/43] feat: implement environment-based configuration --- backend/.env.example | 9 +++++++++ backend/config.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 backend/.env.example create mode 100644 backend/config.py diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..86a59a5 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,9 @@ +API_PORT=3001 +CORS_ORIGINS=http://localhost:5173 +REDIS_URL= +LOG_LEVEL=INFO +MAX_SESSIONS=1000 +SESSION_TTL_HOURS=24 +CPP_SERVER_URL=http://localhost:8080 +TORCH_CHECKPOINT_PATH=../engine/best_model .pt +REQUEST_TIMEOUT_SECONDS=60 diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..07ab4d9 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,28 @@ +from functools import lru_cache +from typing import List + +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + api_port: int = Field(default=3001, alias="API_PORT") + cors_origins: str = Field(default="http://localhost:5173", alias="CORS_ORIGINS") + redis_url: str = Field(default="", alias="REDIS_URL") + log_level: str = Field(default="INFO", alias="LOG_LEVEL") + max_sessions: int = Field(default=1000, alias="MAX_SESSIONS") + session_ttl_hours: int = Field(default=24, alias="SESSION_TTL_HOURS") + cpp_server_url: str = Field(default="http://localhost:8080", alias="CPP_SERVER_URL") + torch_checkpoint_path: str = Field(default="../engine/best_model .pt", alias="TORCH_CHECKPOINT_PATH") + request_timeout_seconds: float = Field(default=60.0, alias="REQUEST_TIMEOUT_SECONDS") + + model_config = SettingsConfigDict(env_file=".env", extra="ignore", populate_by_name=True) + + @property + def cors_origin_list(self) -> List[str]: + return [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()] + + +@lru_cache +def get_settings() -> Settings: + return Settings() From eb3b930d7b033a4480c9d67a28e04981e50a8ec0 Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:18:20 +0530 Subject: [PATCH 02/43] chore: update dependencies in requirements.txt .add initial requirements.txt --- backend/requirements.txt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 backend/requirements.txt diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..cec57cc --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,8 @@ +fastapi==0.115.6 +uvicorn[standard]==0.34.0 +pydantic==2.10.4 +pydantic-settings==2.7.1 +httpx==0.28.1 +redis==5.2.1 +torch +tiktoken From ac750431cc0617fed4611a52eeaec53e7a229640 Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:23:20 +0530 Subject: [PATCH 03/43] feat: integrate error logging in global middleware added error handling middleware --- backend/middleware/error_handler.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 backend/middleware/error_handler.py diff --git a/backend/middleware/error_handler.py b/backend/middleware/error_handler.py new file mode 100644 index 0000000..4d7cbe6 --- /dev/null +++ b/backend/middleware/error_handler.py @@ -0,0 +1,28 @@ +from fastapi import FastAPI, HTTPException, Request +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse + + +def register_error_handlers(app: FastAPI) -> None: + @app.exception_handler(HTTPException) + async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse: + if isinstance(exc.detail, dict) and {"error", "message", "code"}.issubset(exc.detail.keys()): + return JSONResponse(status_code=exc.status_code, content=exc.detail) + return JSONResponse( + status_code=exc.status_code, + content={"error": "request_error", "message": str(exc.detail), "code": exc.status_code}, + ) + + @app.exception_handler(RequestValidationError) + async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse: + return JSONResponse( + status_code=422, + content={"error": "validation_error", "message": "Request validation failed", "code": 422}, + ) + + @app.exception_handler(Exception) + async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse: + return JSONResponse( + status_code=500, + content={"error": "internal_error", "message": "Unexpected server error", "code": 500}, + ) From 6594633825c80692194eaba08fc3c34cc60bfd86 Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:26:00 +0530 Subject: [PATCH 04/43] feat: add middleware for request and response logging middleware --- backend/middleware/logging.py | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 backend/middleware/logging.py diff --git a/backend/middleware/logging.py b/backend/middleware/logging.py new file mode 100644 index 0000000..8931721 --- /dev/null +++ b/backend/middleware/logging.py @@ -0,0 +1,45 @@ +import json +import logging +import time +from datetime import datetime, timezone +from typing import Callable + +from fastapi import Request, Response +from starlette.middleware.base import BaseHTTPMiddleware + + +class JsonFormatter(logging.Formatter): + def format(self, record: logging.LogRecord) -> str: + payload = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "level": record.levelname, + "message": record.getMessage(), + } + for key in ("method", "path", "status_code", "latency_ms", "session_id", "prompt_length", "latency", "chars"): + value = getattr(record, key, None) + if value is not None: + payload[key] = value + return json.dumps(payload, separators=(",", ":")) + + +def configure_logging(level: str) -> None: + handler = logging.StreamHandler() + handler.setFormatter(JsonFormatter()) + logging.basicConfig(level=level.upper(), handlers=[handler], force=True) + + +class RequestLoggingMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next: Callable[[Request], Response]) -> Response: + started = time.monotonic() + response = await call_next(request) + latency_ms = round((time.monotonic() - started) * 1000, 2) + logging.getLogger("quadtrix.api").info( + "http_request", + extra={ + "method": request.method, + "path": request.url.path, + "status_code": response.status_code, + "latency_ms": latency_ms, + }, + ) + return response From 3a31631472a90748c9e7af5e3783ea2af428d8a5 Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:26:31 +0530 Subject: [PATCH 05/43] yeeeh --- backend/middleware/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 backend/middleware/__init__.py diff --git a/backend/middleware/__init__.py b/backend/middleware/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/middleware/__init__.py @@ -0,0 +1 @@ + From 95db5ec0a787c965beffd15bc188849f61e07e30 Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:31:21 +0530 Subject: [PATCH 06/43] feat: implement chat router endpoints add endpoint to fetch user conversation list --- backend/router/chat.py | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 backend/router/chat.py diff --git a/backend/router/chat.py b/backend/router/chat.py new file mode 100644 index 0000000..538ee76 --- /dev/null +++ b/backend/router/chat.py @@ -0,0 +1,79 @@ +import logging +import time + +from fastapi import APIRouter, HTTPException, Request + +from inference import InferenceClient, InferenceUnavailableError +from models import ChatRequest, ChatResponse, Message, Role, new_id, utc_now +from session_store import SessionStore + +router = APIRouter() +logger = logging.getLogger("quadtrix.api") + + +@router.post("/api/chat", response_model=ChatResponse) +async def chat(payload: ChatRequest, request: Request) -> ChatResponse: + started = time.monotonic() + store: SessionStore = request.app.state.session_store + client: InferenceClient = request.app.state.inference_client + title = payload.prompt[:40] + session = store.get_or_create_session(payload.session_id, title=title) + user_message = Message(session_id=session.id, role=Role.user, text=payload.prompt) + store.add_message(user_message) + + try: + generated = await client.generate( + prompt=payload.prompt, + max_tokens=payload.max_tokens, + temperature=payload.temperature, + model_backend=payload.model_backend, + ) + except InferenceUnavailableError as exc: + error_message = Message( + session_id=session.id, + role=Role.assistant, + text="Could not reach the selected model. Check the C++ server or engine checkpoint.", + error="model_unavailable", + ) + store.add_message(error_message) + raise HTTPException( + status_code=503, + detail={ + "error": "model_unavailable", + "message": exc.message, + "code": 503, + }, + ) from exc + + assistant_message = Message( + id=new_id("msg"), + session_id=session.id, + role=Role.assistant, + text=generated.text, + prompt=payload.prompt, + chars=generated.chars, + seconds=generated.seconds, + ) + store.add_message(assistant_message) + latency = round(time.monotonic() - started, 3) + logger.info( + "chat_request", + extra={ + "session_id": session.id, + "prompt_length": len(payload.prompt), + "latency": latency, + "chars": generated.chars, + "model_backend": payload.model_backend, + }, + ) + return ChatResponse( + id=assistant_message.id, + session_id=session.id, + prompt=payload.prompt, + text=generated.text, + chars=generated.chars, + seconds=generated.seconds, + model="quadtrix-v1.0-pt" if payload.model_backend == "torch" else "quadtrix-v1.0", + model_backend=payload.model_backend, + created_at=assistant_message.created_at, + ) From 8e357cf1566b37ce785dcd806ec7dc65d69f61f4 Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:32:57 +0530 Subject: [PATCH 07/43] refactor: simplify processing logic feedback.py --- backend/router/feedback.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 backend/router/feedback.py diff --git a/backend/router/feedback.py b/backend/router/feedback.py new file mode 100644 index 0000000..94ee3ed --- /dev/null +++ b/backend/router/feedback.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter + +from models import FeedbackRequest, FeedbackResponse, new_id, utc_now + +router = APIRouter() + + +@router.post("/api/feedback", response_model=FeedbackResponse) +async def feedback(payload: FeedbackRequest) -> FeedbackResponse: + return FeedbackResponse(ok=True, id=new_id("feedback"), created_at=utc_now()) From ff0247d0b19159fd84eb3577b5e49242bc065c87 Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:37:48 +0530 Subject: [PATCH 08/43] feat: add health check router for system backend respond --- backend/router/health.py | 56 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 backend/router/health.py diff --git a/backend/router/health.py b/backend/router/health.py new file mode 100644 index 0000000..928271a --- /dev/null +++ b/backend/router/health.py @@ -0,0 +1,56 @@ +import time + +from fastapi import APIRouter, Request + +from inference import InferenceClient, InferenceUnavailableError +from models import HealthResponse, StatsResponse + +router = APIRouter() +START_TIME = time.monotonic() + + +def uptime_seconds() -> float: + return round(time.monotonic() - START_TIME, 3) + + +@router.get("/api/health", response_model=HealthResponse) +async def health(request: Request) -> HealthResponse: + client: InferenceClient = request.app.state.inference_client + torch_status = "ok" if client.torch_health() else "unavailable" + try: + data = await client.health() + return HealthResponse( + status="ok", + api="ok", + cpp_server="ok", + torch_model=torch_status, + model=str(data.get("model", "quadtrix-v1.0")), + vocab=int(data.get("vocab", 105)), + params=int(data.get("params", 826985)), + uptime_seconds=uptime_seconds(), + ) + except InferenceUnavailableError: + return HealthResponse( + status="degraded", + api="ok", + cpp_server="unreachable", + torch_model=torch_status, + uptime_seconds=uptime_seconds(), + ) + + +@router.get("/api/stats", response_model=StatsResponse) +async def stats(request: Request) -> StatsResponse: + client: InferenceClient = request.app.state.inference_client + online = True + try: + await client.health() + except InferenceUnavailableError: + online = False + return StatsResponse( + backend=client.settings.cpp_server_url, + backend_online=online, + torch_checkpoint=str(client.torch_runner.checkpoint_path()), + torch_online=client.torch_health(), + uptime_seconds=uptime_seconds(), + ) From 60816c538ba052e2492f733950c5c7b93d276e96 Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:43:09 +0530 Subject: [PATCH 09/43] suuuuuuu --- backend/router/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 backend/router/__init__.py diff --git a/backend/router/__init__.py b/backend/router/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/router/__init__.py @@ -0,0 +1 @@ + From b57bb02a9c09733f8d6c3d7f48a30941f65d319c Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:43:31 +0530 Subject: [PATCH 10/43] feat: implement session management and message retrieval routes Add GET /api/sessions to list recent sessions Add POST /api/sessions to create new sessions Add DELETE /api/sessions/{id} for session removal Add GET/POST message endpoints for session-specific history --- backend/router/sessions.py | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 backend/router/sessions.py diff --git a/backend/router/sessions.py b/backend/router/sessions.py new file mode 100644 index 0000000..c2b7a16 --- /dev/null +++ b/backend/router/sessions.py @@ -0,0 +1,44 @@ +from typing import List + +from fastapi import APIRouter, HTTPException, Request, status + +from models import AddMessageRequest, CreateSessionRequest, Message, Session +from session_store import SessionStore + +router = APIRouter() + + +def store_from_request(request: Request) -> SessionStore: + return request.app.state.session_store + + +@router.get("/api/sessions", response_model=List[Session]) +async def list_sessions(request: Request) -> List[Session]: + return store_from_request(request).list_sessions()[:50] + + +@router.post("/api/sessions", response_model=Session, status_code=status.HTTP_201_CREATED) +async def create_session(payload: CreateSessionRequest, request: Request) -> Session: + return store_from_request(request).create_session(title=payload.title) + + +@router.delete("/api/sessions/{session_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_session(session_id: str, request: Request) -> None: + deleted = store_from_request(request).delete_session(session_id) + if not deleted: + raise HTTPException(status_code=404, detail="Session not found") + + +@router.get("/api/sessions/{session_id}/messages", response_model=List[Message]) +async def get_messages(session_id: str, request: Request) -> List[Message]: + store = store_from_request(request) + if store.get_session(session_id) is None: + raise HTTPException(status_code=404, detail="Session not found") + return store.get_messages(session_id) + + +@router.post("/api/sessions/{session_id}/messages", response_model=Message, status_code=status.HTTP_201_CREATED) +async def add_message(session_id: str, payload: AddMessageRequest, request: Request) -> Message: + store = store_from_request(request) + store.get_or_create_session(session_id) + return store.add_message(Message(session_id=session_id, role=payload.role, text=payload.text)) From 8878e6291dd44e4d3eaeac7e63f6a2cf2e028e5b Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:48:01 +0530 Subject: [PATCH 11/43] feat: implement Inferencewith Torch and C++ backend support Add InferenceClient to orchestrate model generation. Implement generate_cpp using httpx for remote inference. Add InferenceUnavailableError for error handling of timeouts and connection issues. --- backend/inference.py | 131 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 backend/inference.py diff --git a/backend/inference.py b/backend/inference.py new file mode 100644 index 0000000..6f26a0e --- /dev/null +++ b/backend/inference.py @@ -0,0 +1,131 @@ +import asyncio +import importlib.util +import sys +import time +from pathlib import Path +from types import ModuleType +from typing import Any, Dict, Optional + +import httpx + +from config import Settings +from models import CppGenerateResponse + + +class InferenceUnavailableError(Exception): + def __init__(self, message: str) -> None: + self.message = message + super().__init__(message) + + +class InferenceClient: + def __init__(self, settings: Settings) -> None: + self.settings = settings + self.torch_runner = TorchInferenceRunner(settings) + + async def generate(self, prompt: str, max_tokens: int, temperature: float, model_backend: str) -> CppGenerateResponse: + if model_backend == "torch": + return await self.torch_runner.generate(prompt=prompt, max_tokens=max_tokens, temperature=temperature) + return await self.generate_cpp(prompt=prompt, max_tokens=max_tokens) + + async def generate_cpp(self, prompt: str, max_tokens: int) -> CppGenerateResponse: + url = f"{self.settings.cpp_server_url.rstrip('/')}/generate" + try: + async with httpx.AsyncClient(timeout=self.settings.request_timeout_seconds) as client: + response = await client.post(url, json={"prompt": prompt, "max_tokens": max_tokens}) + response.raise_for_status() + except httpx.TimeoutException as exc: + raise InferenceUnavailableError("The C++ inference server timed out") from exc + except httpx.HTTPError as exc: + raise InferenceUnavailableError( + f"The C++ inference server is not reachable at {self.settings.cpp_server_url}" + ) from exc + + try: + return CppGenerateResponse.model_validate(response.json()) + except ValueError as exc: + raise InferenceUnavailableError("The C++ inference server returned invalid JSON") from exc + + async def health(self) -> Dict[str, Any]: + url = f"{self.settings.cpp_server_url.rstrip('/')}/health" + try: + async with httpx.AsyncClient(timeout=5.0) as client: + response = await client.get(url) + response.raise_for_status() + return response.json() + except httpx.HTTPError as exc: + raise InferenceUnavailableError( + f"The C++ inference server is not reachable at {self.settings.cpp_server_url}" + ) from exc + + def torch_health(self) -> bool: + return self.torch_runner.is_available() + + +class TorchInferenceRunner: + def __init__(self, settings: Settings) -> None: + self.settings = settings + self._module: Optional[ModuleType] = None + self._model: Optional[Any] = None + + def checkpoint_path(self) -> Path: + path = Path(self.settings.torch_checkpoint_path) + if path.is_absolute(): + return path + return (Path(__file__).resolve().parent / path).resolve() + + def engine_inference_path(self) -> Path: + return (Path(__file__).resolve().parents[1] / "engine" / "inference.py").resolve() + + def is_available(self) -> bool: + return self.checkpoint_path().exists() and self.engine_inference_path().exists() + + def _load_module(self) -> ModuleType: + if self._module is not None: + return self._module + module_path = self.engine_inference_path() + if not module_path.exists(): + raise InferenceUnavailableError(f"PyTorch inference file not found at {module_path}") + spec = importlib.util.spec_from_file_location("quadtrix_engine_inference", module_path) + if spec is None or spec.loader is None: + raise InferenceUnavailableError("Could not load engine/inference.py") + module = importlib.util.module_from_spec(spec) + sys.modules["quadtrix_engine_inference"] = module + spec.loader.exec_module(module) + self._module = module + return module + + def _load_model(self) -> Any: + if self._model is not None: + return self._model + checkpoint = self.checkpoint_path() + if not checkpoint.exists(): + raise InferenceUnavailableError(f"PyTorch checkpoint not found at {checkpoint}") + module = self._load_module() + self._model = module.load_model(checkpoint) + return self._model + + def _generate_sync(self, prompt: str, max_tokens: int, temperature: float) -> CppGenerateResponse: + started = time.monotonic() + module = self._load_module() + model = self._load_model() + text = module.generate_response( + model=model, + prompt=prompt, + max_new_tokens=max_tokens, + temperature=temperature, + top_k=None, + ) + seconds = round(time.monotonic() - started, 3) + return CppGenerateResponse(text=text, chars=len(text), seconds=seconds) + + async def generate(self, prompt: str, max_tokens: int, temperature: float) -> CppGenerateResponse: + try: + return await asyncio.wait_for( + asyncio.to_thread(self._generate_sync, prompt, max_tokens, temperature), + timeout=self.settings.request_timeout_seconds, + ) + except asyncio.TimeoutError as exc: + raise InferenceUnavailableError("The PyTorch model timed out") from exc + except (RuntimeError, FileNotFoundError, ImportError, AttributeError) as exc: + raise InferenceUnavailableError(f"The PyTorch model is unavailable: {exc}") from exc From 858d991d1c563e6d1d22ab79169383f105d7a1a6 Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:50:05 +0530 Subject: [PATCH 12/43] feat: implement main entry point with centralized state and middleware --- backend/main.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 backend/main.py diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..ee33d0d --- /dev/null +++ b/backend/main.py @@ -0,0 +1,46 @@ +import uvicorn +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from config import get_settings +from inference import InferenceClient +from middleware.error_handler import register_error_handlers +from middleware.logging import RequestLoggingMiddleware, configure_logging +from router import chat, feedback, health, sessions +from session_store import build_session_store + + +settings = get_settings() +configure_logging(settings.log_level) + +app = FastAPI(title="Quadtrix API", version="1.0.0") +app.state.session_store = build_session_store( + max_sessions=settings.max_sessions, + ttl_hours=settings.session_ttl_hours, + redis_url=settings.redis_url, +) +app.state.inference_client = InferenceClient(settings) + +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origin_list or ["http://localhost:5173"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) +app.add_middleware(RequestLoggingMiddleware) +register_error_handlers(app) + +app.include_router(health.router) +app.include_router(sessions.router) +app.include_router(chat.router) +app.include_router(feedback.router) + + +@app.get("/") +async def root() -> dict[str, str]: + return {"status": "ok", "service": "quadtrix-api"} + + +if __name__ == "__main__": + uvicorn.run("main:app", host="0.0.0.0", port=settings.api_port, reload=True) From d0acd4a82cd38b1c7e6c79f339106505c2697dc9 Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:52:37 +0530 Subject: [PATCH 13/43] feat: define core models for chat and session management - best_model.bin -best_model.pt --- backend/models.py | 128 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 backend/models.py diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..a8f466b --- /dev/null +++ b/backend/models.py @@ -0,0 +1,128 @@ +from datetime import datetime, timezone +from enum import Enum +from typing import Literal, Optional +from uuid import uuid4 + +from pydantic import BaseModel, ConfigDict, Field + + +def utc_now() -> datetime: + return datetime.now(timezone.utc) + + +def new_id(prefix: str) -> str: + return f"{prefix}-{uuid4()}" + + +class Role(str, Enum): + user = "user" + assistant = "assistant" + system = "system" + + +class ErrorResponse(BaseModel): + error: str + message: str + code: int + + +class ChatRequest(BaseModel): + session_id: Optional[str] = None + prompt: str = Field(min_length=1, max_length=500) + max_tokens: int = Field(default=200, ge=1, le=500) + temperature: float = Field(default=1.0, ge=0.1, le=2.0) + stream: bool = False + model_backend: Literal["cpp", "torch"] = "cpp" + + +class ChatResponse(BaseModel): + id: str + session_id: str + prompt: str + text: str + chars: int + seconds: float + model: str = "quadtrix-v1.0" + model_backend: Literal["cpp", "torch"] = "cpp" + created_at: datetime + + +class Message(BaseModel): + id: str = Field(default_factory=lambda: new_id("msg")) + session_id: str + role: Role + text: str + prompt: Optional[str] = None + chars: int = 0 + seconds: float = 0.0 + error: Optional[str] = None + created_at: datetime = Field(default_factory=utc_now) + + +class Session(BaseModel): + id: str = Field(default_factory=lambda: str(uuid4())) + title: str = "New conversation" + created_at: datetime = Field(default_factory=utc_now) + updated_at: datetime = Field(default_factory=utc_now) + message_count: int = 0 + + +class CreateSessionRequest(BaseModel): + title: Optional[str] = Field(default=None, max_length=80) + + +class AddMessageRequest(BaseModel): + role: Role + text: str = Field(min_length=1) + + +class FeedbackRequest(BaseModel): + session_id: str + message_id: str + rating: Literal["up", "down"] + comment: Optional[str] = Field(default=None, max_length=1000) + + +class FeedbackResponse(BaseModel): + ok: bool + id: str + created_at: datetime + + +class HealthResponse(BaseModel): + status: Literal["ok", "degraded"] + api: Literal["ok"] + cpp_server: Literal["ok", "unreachable"] + torch_model: Literal["ok", "unavailable"] + model: str = "quadtrix-v1.0" + vocab: int = 105 + params: int = 826985 + uptime_seconds: float + + +class StatsResponse(BaseModel): + model: str = "quadtrix-v1.0" + architecture: str = "4L x 4H x 200d" + parameters: int = 826985 + vocabulary: int = 105 + val_loss: float = 1.6371 + context: int = 128 + training: str = "76.2 min CPU" + backend: str + backend_online: bool + torch_checkpoint: str + torch_online: bool + uptime_seconds: float + + +class CppGenerateRequest(BaseModel): + prompt: str + max_tokens: int + + +class CppGenerateResponse(BaseModel): + text: str + chars: int + seconds: float + + model_config = ConfigDict(extra="ignore") From cc68ad9af4705ab9b319b289ff95e921aa9e25e5 Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:55:16 +0530 Subject: [PATCH 14/43] feat: add repository-level run instructions to backend entry point --- backend/server.py | 517 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 backend/server.py diff --git a/backend/server.py b/backend/server.py new file mode 100644 index 0000000..03835fe --- /dev/null +++ b/backend/server.py @@ -0,0 +1,517 @@ +""" +Quadtrix web backend. + +Run from the repository root: + python server.py + +Or directly: + python backend/server.py +""" + +from __future__ import annotations + +import codecs +import os +import queue +import re +import subprocess +import sys +import threading +import time +from pathlib import Path +from typing import Generator + +USER_SITE = ( + Path.home() + / "AppData" + / "Roaming" + / "Python" + / f"Python{sys.version_info.major}{sys.version_info.minor}" + / "site-packages" +) +if str(USER_SITE) not in sys.path: + sys.path.append(str(USER_SITE)) + +from flask import Flask, Response, jsonify, request, send_from_directory +from flask_cors import CORS + + +BACKEND_DIR = Path(__file__).resolve().parent +ROOT_DIR = BACKEND_DIR.parent +FRONTEND_DIR = ROOT_DIR / "frontend" + +EXE_PATH = Path(os.environ.get("QUADTRIX_EXE", ROOT_DIR / "Quadtrix.exe")).resolve() +DATA_PATH = Path(os.environ.get("QUADTRIX_DATA", ROOT_DIR / "data" / "input.txt")).resolve() +DEFAULT_MODEL_PATH = Path(os.environ.get("MODEL_PATH", ROOT_DIR / "best_model.bin")).resolve() + +MODEL_MARKER = "Quadtrix>" +PROMPT_MARKERS = ("\r\n\r\nYou>", "\n\nYou>", "\r\rYou>") +ANSI_RE = re.compile(r"\x1b\[[0-9;?]*[a-zA-Z]") +SAFE_MODEL_ROOTS = (ROOT_DIR, BACKEND_DIR) +MODEL_LOAD_TIMEOUT_SECONDS = 180 +GENERATION_IDLE_TIMEOUT_SECONDS = 180 + +app = Flask(__name__, static_folder=str(FRONTEND_DIR), static_url_path="") +CORS(app) + +procs: dict[str, subprocess.Popen] = {} +procs_lock = threading.Lock() + + +def _is_inside(path: Path, root: Path) -> bool: + try: + path.resolve().relative_to(root.resolve()) + return True + except ValueError: + return False + + +def _public_path(path: Path) -> str: + try: + return str(path.resolve().relative_to(ROOT_DIR.resolve())) + except ValueError: + return str(path) + + +def _discover_models() -> list[dict[str, str | bool]]: + """Find practical .bin model locations without walking the whole repo.""" + search_dirs = [ROOT_DIR, ROOT_DIR / "models", BACKEND_DIR / "models"] + seen: set[Path] = set() + models: list[dict[str, str | bool]] = [] + + for directory in search_dirs: + if not directory.exists() or not directory.is_dir(): + continue + for model in sorted(directory.glob("*.bin")): + resolved = model.resolve() + if resolved in seen: + continue + seen.add(resolved) + models.append( + { + "name": model.stem, + "path": _public_path(resolved), + "exists": resolved.exists(), + } + ) + + if DEFAULT_MODEL_PATH not in seen: + models.insert( + 0, + { + "name": DEFAULT_MODEL_PATH.stem or "best_model", + "path": _public_path(DEFAULT_MODEL_PATH), + "exists": DEFAULT_MODEL_PATH.exists(), + }, + ) + + return models + + +def _resolve_model(selection: str | None) -> tuple[Path | None, str | None]: + raw = (selection or "").strip() + candidate = Path(raw) if raw else DEFAULT_MODEL_PATH + + if not candidate.is_absolute(): + candidate = ROOT_DIR / candidate + + candidate = candidate.resolve() + + if candidate.suffix.lower() != ".bin": + return None, "selected model must be a .bin file" + + if not any(_is_inside(candidate, root) for root in SAFE_MODEL_ROOTS): + return None, "selected model must be inside this project" + + if not candidate.exists(): + return None, f"model not found: {candidate}" + + return candidate, None + + +def _drain_pipe(pipe) -> None: + try: + while True: + chunk = pipe.read(4096) + if not chunk: + break + except Exception: + pass + finally: + try: + pipe.close() + except Exception: + pass + + +def _split_ansi(text: str) -> tuple[str, str]: + out: list[str] = [] + i = 0 + n = len(text) + + while i < n: + char = text[i] + if char == "\x1b": + match = ANSI_RE.match(text, i) + if match: + i = match.end() + continue + return "".join(out), text[i:] + out.append(char) + i += 1 + + return "".join(out), "" + + +def _sse_char(char: str) -> str | None: + if char == "\n": + return "data: __NL__\n\n" + if char == "\r": + return None + return f"data: {char}\n\n" + + +def _sse_event(name: str, data: str) -> str: + return f"event: {name}\ndata: {data}\n\n" + + +def _ends_like_prompt_marker(text: str) -> bool: + tail = text[-8:] + return any(marker.startswith(tail) for marker in PROMPT_MARKERS if tail) + + +def stream_exe( + prompt: str, + max_tokens: int, + session_id: str, + model_path: Path, +) -> Generator[str, None, None]: + """Run Quadtrix.exe and stream only the model reply as SSE events.""" + env = os.environ.copy() + env["MODEL_PATH"] = str(model_path) + + cmd = [ + str(EXE_PATH), + "--chat", + "--chat-tokens", + str(max_tokens), + str(DATA_PATH), + ] + + try: + proc = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env, + cwd=str(ROOT_DIR), + bufsize=0, + ) + except FileNotFoundError: + yield f"data: [ERROR] exe not found: {EXE_PATH}\n\n" + yield "data: [DONE]\n\n" + return + except Exception as exc: + yield f"data: [ERROR] {exc}\n\n" + yield "data: [DONE]\n\n" + return + + with procs_lock: + procs[session_id] = proc + + threading.Thread(target=_drain_pipe, args=(proc.stderr,), daemon=True).start() + + try: + assert proc.stdin is not None + proc.stdin.write((prompt + "\n").encode("utf-8")) + proc.stdin.flush() + except Exception as exc: + yield f"data: [ERROR] stdin write failed: {exc}\n\n" + try: + proc.kill() + except Exception: + pass + with procs_lock: + procs.pop(session_id, None) + yield "data: [DONE]\n\n" + return + + decoder = codecs.getincrementaldecoder("utf-8")(errors="replace") + pending = "" + pre_buffer = "" + output_hold = "" + started = False + first_emit = True + stopped_by_prompt = False + start_time = time.monotonic() + last_byte_time = start_time + last_status_time = start_time + stdout_queue: queue.Queue[bytes | None] = queue.Queue() + + def read_stdout() -> None: + try: + assert proc.stdout is not None + while True: + chunk = proc.stdout.read(1) + if not chunk: + break + stdout_queue.put(chunk) + finally: + stdout_queue.put(None) + + threading.Thread(target=read_stdout, daemon=True).start() + yield _sse_event("status", "Starting Quadtrix and loading the model...") + + def drain(final: bool) -> Generator[str, None, None]: + nonlocal pending, pre_buffer, output_hold, started, first_emit, stopped_by_prompt + + clean, remainder = _split_ansi(pending) + pending = "" if final else remainder + + if not started: + pre_buffer += clean + marker_index = pre_buffer.find(MODEL_MARKER) + if marker_index == -1: + # Some builds do not print the exact marker, or print it very late. + # After a short wait, stream from the latest known prompt boundary + # instead of leaving the browser blank forever. + if time.monotonic() - start_time > 5 and len(pre_buffer) > 0: + fallback_index = max(pre_buffer.rfind("\n"), pre_buffer.rfind("\r")) + clean = pre_buffer[fallback_index + 1 :] if fallback_index != -1 else pre_buffer + pre_buffer = "" + started = True + else: + if len(pre_buffer) > 16384: + pre_buffer = pre_buffer[-4096:] + return + else: + clean = pre_buffer[marker_index + len(MODEL_MARKER) :] + pre_buffer = "" + started = True + + def emit_text(text: str) -> Generator[str, None, None]: + nonlocal first_emit + for item in text: + if first_emit and item in (" ", "\t", "\n", "\r"): + continue + first_emit = False + event = _sse_char(item) + if event: + yield event + + max_marker_length = max(len(marker) for marker in PROMPT_MARKERS) + + for char in clean: + output_hold += char + + for marker in PROMPT_MARKERS: + marker_index = output_hold.find(marker) + if marker_index != -1: + yield from emit_text(output_hold[:marker_index].rstrip()) + output_hold = "" + stopped_by_prompt = True + return + + while len(output_hold) > max_marker_length - 1: + if _ends_like_prompt_marker(output_hold): + break + yield from emit_text(output_hold[0]) + output_hold = output_hold[1:] + + if final and output_hold: + yield from emit_text(output_hold) + output_hold = "" + + try: + while True: + try: + byte = stdout_queue.get(timeout=0.25) + except queue.Empty: + now = time.monotonic() + if not started and now - start_time > MODEL_LOAD_TIMEOUT_SECONDS: + yield "data: [ERROR] model did not become ready within 180 seconds\n\n" + try: + proc.kill() + except Exception: + pass + break + + if started and now - last_byte_time > GENERATION_IDLE_TIMEOUT_SECONDS: + yield "data: [ERROR] generation stalled for 180 seconds\n\n" + try: + proc.kill() + except Exception: + pass + break + + if now - last_status_time >= 3 and proc.poll() is None: + last_status_time = now + if not started: + yield _sse_event("status", "Still loading the model. First token will appear automatically...") + else: + yield _sse_event("status", "Generating tokens...") + continue + + if byte is None: + tail = decoder.decode(b"", final=True) + if tail: + pending += tail + yield from drain(final=True) + break + + last_byte_time = time.monotonic() + text = decoder.decode(byte) + if not text: + continue + + pending += text + yield from drain(final=False) + if stopped_by_prompt: + break + except GeneratorExit: + try: + proc.kill() + except Exception: + pass + with procs_lock: + procs.pop(session_id, None) + return + + try: + assert proc.stdin is not None + proc.stdin.write(b"quit\n") + proc.stdin.flush() + except Exception: + pass + + try: + proc.wait(timeout=3) + except subprocess.TimeoutExpired: + proc.kill() + + with procs_lock: + procs.pop(session_id, None) + + yield "data: [DONE]\n\n" + + +@app.route("/") +def index(): + return send_from_directory(FRONTEND_DIR, "index.html") + + +@app.route("/") +def frontend_file(path: str): + return send_from_directory(FRONTEND_DIR, path) + + +@app.route("/models") +def models(): + return jsonify({"default": _public_path(DEFAULT_MODEL_PATH), "models": _discover_models()}) + + +@app.route("/status") +def status(): + selected_model, model_error = _resolve_model(request.args.get("model")) + model_path = selected_model or DEFAULT_MODEL_PATH + exe_ok = EXE_PATH.exists() + data_ok = DATA_PATH.exists() + model_ok = selected_model is not None + + error = None + if not exe_ok: + error = f"Quadtrix.exe not found: {EXE_PATH}" + elif not data_ok: + error = f"data input not found: {DATA_PATH}" + elif model_error: + error = model_error + + return jsonify( + { + "exe": exe_ok, + "data": data_ok, + "model": model_ok, + "ready": exe_ok and data_ok and model_ok, + "error": error, + "exe_path": str(EXE_PATH), + "data_path": str(DATA_PATH), + "model_path": str(model_path), + "selected_model": _public_path(model_path), + } + ) + + +@app.route("/health") +def health(): + return jsonify({"ok": True}) + + +@app.route("/generate") +def generate(): + prompt = request.args.get("prompt", "").strip() + session_id = request.args.get("sid", "default").strip() or "default" + + try: + max_tokens = int(request.args.get("max_tokens", 200)) + except ValueError: + return jsonify({"error": "max_tokens must be a number"}), 400 + + max_tokens = max(1, min(max_tokens, 2000)) + model_path, model_error = _resolve_model(request.args.get("model")) + + if not prompt: + return jsonify({"error": "empty prompt"}), 400 + if not EXE_PATH.exists(): + return jsonify({"error": f"exe not found: {EXE_PATH}"}), 500 + if not DATA_PATH.exists(): + return jsonify({"error": f"data input not found: {DATA_PATH}"}), 500 + if model_error or model_path is None: + return jsonify({"error": model_error or "model not found"}), 500 + + return Response( + stream_exe(prompt, max_tokens, session_id, model_path), + mimetype="text/event-stream", + headers={ + "Cache-Control": "no-cache, no-store", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + }, + ) + + +@app.route("/stop", methods=["POST"]) +def stop(): + session_id = "default" + if request.is_json: + body = request.get_json(silent=True) or {} + session_id = str(body.get("sid") or "default") + + with procs_lock: + proc = procs.pop(session_id, None) + + if proc and proc.poll() is None: + proc.kill() + return jsonify({"status": "stopped"}) + return jsonify({"status": "idle"}) + + +def main() -> None: + print() + print("=" * 56) + print(" Quadtrix Web Interface") + print("=" * 56) + print(f" exe {EXE_PATH}") + print(f" {'found' if EXE_PATH.exists() else 'NOT FOUND'}") + print(f" data {DATA_PATH}") + print(f" {'found' if DATA_PATH.exists() else 'NOT FOUND'}") + print(f" model {DEFAULT_MODEL_PATH}") + print(f" {'found' if DEFAULT_MODEL_PATH.exists() else 'NOT FOUND'}") + print(" open http://localhost:5000") + print("=" * 56) + print() + app.run(host="0.0.0.0", port=5000, threaded=True, debug=False) + + +if __name__ == "__main__": + main() From 093f47732721c6d49f0569a5df69eeb3e29a44b1 Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:55:41 +0530 Subject: [PATCH 15/43] feat: add repository-level run instructions to backend entry point --- backend/session_store.py | 176 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 backend/session_store.py diff --git a/backend/session_store.py b/backend/session_store.py new file mode 100644 index 0000000..5d46973 --- /dev/null +++ b/backend/session_store.py @@ -0,0 +1,176 @@ +from collections import OrderedDict +from datetime import datetime, timedelta, timezone +from typing import Dict, List, Optional + +import redis + +from models import Message, Session, utc_now + + +class SessionStore: + def __init__(self, max_sessions: int, ttl_hours: int) -> None: + self.max_sessions = max_sessions + self.ttl = timedelta(hours=ttl_hours) + self.sessions: "OrderedDict[str, Session]" = OrderedDict() + self.messages: Dict[str, List[Message]] = {} + + def _is_expired(self, session: Session, now: datetime) -> bool: + return now - session.updated_at > self.ttl + + def prune(self) -> None: + now = utc_now() + expired = [session_id for session_id, session in self.sessions.items() if self._is_expired(session, now)] + for session_id in expired: + self.delete_session(session_id) + while len(self.sessions) > self.max_sessions: + session_id, _ = self.sessions.popitem(last=False) + self.messages.pop(session_id, None) + + def create_session(self, title: Optional[str] = None, session_id: Optional[str] = None) -> Session: + self.prune() + session = Session(title=title or "New conversation") + if session_id: + session.id = session_id + self.sessions[session.id] = session + self.messages[session.id] = [] + self.prune() + return session + + def get_or_create_session(self, session_id: Optional[str], title: Optional[str] = None) -> Session: + self.prune() + if session_id and session_id in self.sessions: + session = self.sessions[session_id] + self.sessions.move_to_end(session_id) + return session + return self.create_session(title=title, session_id=session_id) + + def list_sessions(self) -> List[Session]: + self.prune() + return list(reversed(self.sessions.values())) + + def get_session(self, session_id: str) -> Optional[Session]: + self.prune() + session = self.sessions.get(session_id) + if session: + self.sessions.move_to_end(session_id) + return session + + def delete_session(self, session_id: str) -> bool: + existed = session_id in self.sessions + self.sessions.pop(session_id, None) + self.messages.pop(session_id, None) + return existed + + def add_message(self, message: Message) -> Message: + session = self.get_or_create_session(message.session_id) + if session.title == "New conversation" and message.role.value == "user": + session.title = message.text[:40] + session.updated_at = utc_now() + items = self.messages.setdefault(session.id, []) + items.append(message) + session.message_count = len(items) + self.sessions[session.id] = session + self.sessions.move_to_end(session.id) + return message + + def get_messages(self, session_id: str) -> List[Message]: + self.prune() + return list(self.messages.get(session_id, [])) + + +class RedisSessionStore(SessionStore): + def __init__(self, max_sessions: int, ttl_hours: int, redis_url: str) -> None: + super().__init__(max_sessions=max_sessions, ttl_hours=ttl_hours) + self.redis_url = redis_url + self.client = redis.Redis.from_url(redis_url, decode_responses=True) + self.session_index = "quadtrix:sessions" + + def _ttl_seconds(self) -> int: + return int(self.ttl.total_seconds()) + + def _score(self, session: Session) -> float: + return session.updated_at.timestamp() + + def _session_key(self, session_id: str) -> str: + return f"quadtrix:session:{session_id}" + + def _messages_key(self, session_id: str) -> str: + return f"quadtrix:messages:{session_id}" + + def prune(self) -> None: + cutoff = (utc_now() - self.ttl).timestamp() + expired = self.client.zrangebyscore(self.session_index, "-inf", cutoff) + for session_id in expired: + self.delete_session(str(session_id)) + count = self.client.zcard(self.session_index) + if count > self.max_sessions: + overflow = int(count - self.max_sessions) + oldest = self.client.zrange(self.session_index, 0, overflow - 1) + for session_id in oldest: + self.delete_session(str(session_id)) + + def create_session(self, title: Optional[str] = None, session_id: Optional[str] = None) -> Session: + self.prune() + session = Session(title=title or "New conversation") + if session_id: + session.id = session_id + self.client.setex(self._session_key(session.id), self._ttl_seconds(), session.model_dump_json()) + self.client.delete(self._messages_key(session.id)) + self.client.expire(self._messages_key(session.id), self._ttl_seconds()) + self.client.zadd(self.session_index, {session.id: self._score(session)}) + self.prune() + return session + + def get_or_create_session(self, session_id: Optional[str], title: Optional[str] = None) -> Session: + self.prune() + if session_id: + session = self.get_session(session_id) + if session: + return session + return self.create_session(title=title, session_id=session_id) + + def list_sessions(self) -> List[Session]: + self.prune() + session_ids = self.client.zrevrange(self.session_index, 0, self.max_sessions - 1) + sessions: List[Session] = [] + for session_id in session_ids: + session = self.get_session(str(session_id)) + if session: + sessions.append(session) + return sessions + + def get_session(self, session_id: str) -> Optional[Session]: + raw = self.client.get(self._session_key(session_id)) + if not raw: + self.client.zrem(self.session_index, session_id) + return None + session = Session.model_validate_json(str(raw)) + self.client.zadd(self.session_index, {session.id: self._score(session)}) + return session + + def delete_session(self, session_id: str) -> bool: + deleted = bool(self.client.delete(self._session_key(session_id), self._messages_key(session_id))) + self.client.zrem(self.session_index, session_id) + return deleted + + def add_message(self, message: Message) -> Message: + session = self.get_or_create_session(message.session_id) + if session.title == "New conversation" and message.role.value == "user": + session.title = message.text[:40] + session.updated_at = utc_now() + session.message_count = int(self.client.rpush(self._messages_key(session.id), message.model_dump_json())) + self.client.setex(self._session_key(session.id), self._ttl_seconds(), session.model_dump_json()) + self.client.expire(self._messages_key(session.id), self._ttl_seconds()) + self.client.zadd(self.session_index, {session.id: self._score(session)}) + return message + + def get_messages(self, session_id: str) -> List[Message]: + self.prune() + rows = self.client.lrange(self._messages_key(session_id), 0, -1) + return [Message.model_validate_json(str(row)) for row in rows] + + +def build_session_store(max_sessions: int, ttl_hours: int, redis_url: str) -> SessionStore: + if redis_url: + return RedisSessionStore(max_sessions=max_sessions, ttl_hours=ttl_hours, redis_url=redis_url) + return SessionStore(max_sessions=max_sessions, ttl_hours=ttl_hours) From 3908ba1fb84ed7ee661856015e5a8978c9027fe3 Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 08:57:03 +0530 Subject: [PATCH 16/43] docs: create README with backend setup and execution guide --- backend/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 backend/README.md diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..ee3c735 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,16 @@ +# Quadtrix Middleware + +FastAPI middleware for the Quadtrix.cpp C++ inference server. + +## Run + +```bash +pip install -r requirements.txt +uvicorn main:app --port 3001 --reload +``` + +Start the C++ server first: + +```bash +./Quadtrix data/input.txt --server --port 8080 +``` From 332fcee6e41580de934c6a47da1dbfadae0c5407 Mon Sep 17 00:00:00 2001 From: Eamon Date: Wed, 6 May 2026 09:32:36 +0530 Subject: [PATCH 17/43] Add frontend chat application --- frontend/.env.example | 1 + frontend/index.html | 8 + frontend/manifest.webmanifest | 20 + frontend/package-lock.json | 2933 +++++++++++++++++ frontend/package.json | 29 + frontend/postcss.config.js | 6 + frontend/public/icon.svg | 75 + frontend/public/manifest.webmanifest | 20 + frontend/public/sw.js | 52 + frontend/src/App.tsx | 39 + frontend/src/components/chat/ChatView.tsx | 107 + frontend/src/components/chat/EmptyState.tsx | 15 + .../src/components/chat/MessageAvatar.tsx | 20 + frontend/src/components/chat/MessageList.tsx | 20 + frontend/src/components/chat/MessageRow.tsx | 64 + .../src/components/chat/StarterPrompts.tsx | 22 + .../src/components/chat/ThinkingIndicator.tsx | 12 + frontend/src/components/input/CharCounter.tsx | 9 + frontend/src/components/input/InputBar.tsx | 77 + frontend/src/components/layout/AppLayout.tsx | 22 + frontend/src/components/layout/Sidebar.tsx | 71 + frontend/src/components/layout/Topbar.tsx | 66 + frontend/src/components/panels/ModelBadge.tsx | 20 + .../src/components/panels/SettingsPanel.tsx | 75 + frontend/src/components/panels/StatsPanel.tsx | 57 + .../src/components/sidebar/NewChatButton.tsx | 14 + .../src/components/sidebar/SessionItem.tsx | 35 + .../src/components/sidebar/SessionList.tsx | 28 + frontend/src/components/ui/Badge.tsx | 16 + frontend/src/components/ui/Button.tsx | 22 + frontend/src/components/ui/Input.tsx | 23 + frontend/src/components/ui/Slider.tsx | 18 + frontend/src/components/ui/Tooltip.tsx | 17 + frontend/src/hooks/useAutoScroll.ts | 13 + frontend/src/hooks/useConnectionStatus.ts | 10 + frontend/src/hooks/useKeyboardShortcut.ts | 25 + frontend/src/index.css | 56 + frontend/src/main.tsx | 26 + frontend/src/registerServiceWorker.ts | 11 + frontend/src/store/sessionStore.ts | 44 + frontend/src/store/settingsStore.ts | 45 + frontend/src/types/index.ts | 76 + frontend/src/utils/text.ts | 15 + frontend/src/utils/time.ts | 18 + frontend/sw.js | 27 +- frontend/tailwind.config.ts | 24 + frontend/tsconfig.json | 22 + run.md | 492 +++ 48 files changed, 4912 insertions(+), 5 deletions(-) create mode 100644 frontend/.env.example create mode 100644 frontend/manifest.webmanifest create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/icon.svg create mode 100644 frontend/public/manifest.webmanifest create mode 100644 frontend/public/sw.js create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/components/chat/ChatView.tsx create mode 100644 frontend/src/components/chat/EmptyState.tsx create mode 100644 frontend/src/components/chat/MessageAvatar.tsx create mode 100644 frontend/src/components/chat/MessageList.tsx create mode 100644 frontend/src/components/chat/MessageRow.tsx create mode 100644 frontend/src/components/chat/StarterPrompts.tsx create mode 100644 frontend/src/components/chat/ThinkingIndicator.tsx create mode 100644 frontend/src/components/input/CharCounter.tsx create mode 100644 frontend/src/components/input/InputBar.tsx create mode 100644 frontend/src/components/layout/AppLayout.tsx create mode 100644 frontend/src/components/layout/Sidebar.tsx create mode 100644 frontend/src/components/layout/Topbar.tsx create mode 100644 frontend/src/components/panels/ModelBadge.tsx create mode 100644 frontend/src/components/panels/SettingsPanel.tsx create mode 100644 frontend/src/components/panels/StatsPanel.tsx create mode 100644 frontend/src/components/sidebar/NewChatButton.tsx create mode 100644 frontend/src/components/sidebar/SessionItem.tsx create mode 100644 frontend/src/components/sidebar/SessionList.tsx create mode 100644 frontend/src/components/ui/Badge.tsx create mode 100644 frontend/src/components/ui/Button.tsx create mode 100644 frontend/src/components/ui/Input.tsx create mode 100644 frontend/src/components/ui/Slider.tsx create mode 100644 frontend/src/components/ui/Tooltip.tsx create mode 100644 frontend/src/hooks/useAutoScroll.ts create mode 100644 frontend/src/hooks/useConnectionStatus.ts create mode 100644 frontend/src/hooks/useKeyboardShortcut.ts create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/registerServiceWorker.ts create mode 100644 frontend/src/store/sessionStore.ts create mode 100644 frontend/src/store/settingsStore.ts create mode 100644 frontend/src/types/index.ts create mode 100644 frontend/src/utils/text.ts create mode 100644 frontend/src/utils/time.ts create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json create mode 100644 run.md diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..4558365 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:3001 diff --git a/frontend/index.html b/frontend/index.html index c92d184..c694ea2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,6 +3,14 @@ + + + + + Quadtrix.cpp Chat diff --git a/frontend/manifest.webmanifest b/frontend/manifest.webmanifest new file mode 100644 index 0000000..882922b --- /dev/null +++ b/frontend/manifest.webmanifest @@ -0,0 +1,20 @@ +{ + "name": "Quadtrix.cpp Chat", + "short_name": "Quadtrix", + "description": "Installable local chat interface for Quadtrix C++ and PyTorch model backends.", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#0a0a0a", + "theme_color": "#0a0a0a", + "orientation": "any", + "categories": ["developer", "productivity", "utilities"], + "icons": [ + { + "src": "/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ] +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..e91afb1 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2933 @@ +{ + "name": "quadtrix-chat", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "quadtrix-chat", + "version": "1.0.0", + "dependencies": { + "@tanstack/react-query": "^5.62.11", + "date-fns": "^4.1.0", + "framer-motion": "^11.15.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "zustand": "^5.0.2" + }, + "devDependencies": { + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite": "^6.0.7" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/query-core": { + "version": "5.100.6", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.6.tgz", + "integrity": "sha512-Os2CPUr98to98RYm+D4qGqGkiffn7MGSyl2547a4MljVkHE30AMJRqTiyCqBfMwzAx/I91vCkAxp5tHSla6Twg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.6.tgz", + "integrity": "sha512-uVSrps0PV16Cxmcn2rvL+dUhwTpTUtiRW347AEeYxMZXO2pZe9ja7E24PAMGoQ5u2g89DD8u4QhOviBk+RN8RA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.100.6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz", + "integrity": "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..37aa936 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "quadtrix-chat", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/react-query": "^5.62.11", + "date-fns": "^4.1.0", + "framer-motion": "^11.15.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "zustand": "^5.0.2" + }, + "devDependencies": { + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite": "^6.0.7" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/public/icon.svg b/frontend/public/icon.svg new file mode 100644 index 0000000..b010998 --- /dev/null +++ b/frontend/public/icon.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + > + + + + + + + + C + + + + + ++ + + + + + \ No newline at end of file diff --git a/frontend/public/manifest.webmanifest b/frontend/public/manifest.webmanifest new file mode 100644 index 0000000..882922b --- /dev/null +++ b/frontend/public/manifest.webmanifest @@ -0,0 +1,20 @@ +{ + "name": "Quadtrix.cpp Chat", + "short_name": "Quadtrix", + "description": "Installable local chat interface for Quadtrix C++ and PyTorch model backends.", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#0a0a0a", + "theme_color": "#0a0a0a", + "orientation": "any", + "categories": ["developer", "productivity", "utilities"], + "icons": [ + { + "src": "/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ] +} diff --git a/frontend/public/sw.js b/frontend/public/sw.js new file mode 100644 index 0000000..4f5d599 --- /dev/null +++ b/frontend/public/sw.js @@ -0,0 +1,52 @@ +const CACHE_NAME = 'quadtrix-chat-v1'; +const APP_SHELL = ['/', '/manifest.webmanifest', '/icon.svg']; + +self.addEventListener('install', (event) => { + event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL))); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))) + ) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', (event) => { + if (event.request.method !== 'GET') { + return; + } + + const url = new URL(event.request.url); + + const isApiRequest = + url.pathname.startsWith('/api/') || + url.pathname === '/generate' || + url.pathname === '/health' || + url.pathname === '/status'; + + if (isApiRequest) { + return; + } + + event.respondWith( + caches.match(event.request).then((cached) => { + if (cached) { + return cached; + } + return fetch(event.request) + .then((response) => { + if (!response || response.status !== 200 || response.type === 'opaque') { + return response; + } + const copy = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(event.request, copy)); + return response; + }) + .catch(() => caches.match('/')); + }) + ); +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..6141dcf --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,39 @@ +import { useCallback } from "react"; + +import { ChatView } from "./components/chat/ChatView"; +import { AppLayout } from "./components/layout/AppLayout"; +import { useKeyboardShortcut } from "./hooks/useKeyboardShortcut"; +import { useCreateSession } from "./api/sessions"; +import { useSessionStore } from "./store/sessionStore"; +import { useSettingsStore } from "./store/settingsStore"; + +export default function App() { + const clearMessages = useSessionStore((state) => state.clearMessages); + const setActiveSession = useSessionStore((state) => state.setActiveSession); + const setMessages = useSessionStore((state) => state.setMessages); + const setSettingsOpen = useSettingsStore((state) => state.setSettingsOpen); + const createSession = useCreateSession(); + + const newConversation = useCallback(() => { + createSession.mutate(undefined, { + onSuccess: (session) => { + setActiveSession(session.id); + setMessages([]); + }, + }); + }, [createSession, setActiveSession, setMessages]); + + const openSettings = useCallback(() => setSettingsOpen(true), [setSettingsOpen]); + const closeSettings = useCallback(() => setSettingsOpen(false), [setSettingsOpen]); + + useKeyboardShortcut(["ctrl", "l"], clearMessages); + useKeyboardShortcut(["ctrl", "n"], newConversation); + useKeyboardShortcut(["ctrl", ","], openSettings); + useKeyboardShortcut(["escape"], closeSettings); + + return ( + + + + ); +} diff --git a/frontend/src/components/chat/ChatView.tsx b/frontend/src/components/chat/ChatView.tsx new file mode 100644 index 0000000..68c29eb --- /dev/null +++ b/frontend/src/components/chat/ChatView.tsx @@ -0,0 +1,107 @@ +import { useEffect, useState } from "react"; + +import { ApiClientError } from "../../api/client"; +import { useSendMessage } from "../../api/chat"; +import { useSessionMessages } from "../../api/sessions"; +import { useHealth } from "../../api/health"; +import { useSessionStore } from "../../store/sessionStore"; +import { useSettingsStore } from "../../store/settingsStore"; +import type { Message } from "../../types"; +import { EmptyState } from "./EmptyState"; +import { MessageList } from "./MessageList"; +import { InputBar } from "../input/InputBar"; + +export function ChatView() { + const [draft, setDraft] = useState(""); + const activeSessionId = useSessionStore((state) => state.activeSessionId); + const messages = useSessionStore((state) => state.messages); + const setActiveSession = useSessionStore((state) => state.setActiveSession); + const setMessages = useSessionStore((state) => state.setMessages); + const addMessage = useSessionStore((state) => state.addMessage); + const replaceMessage = useSessionStore((state) => state.replaceMessage); + const maxTokens = useSettingsStore((state) => state.maxTokens); + const temperature = useSettingsStore((state) => state.temperature); + const modelBackend = useSettingsStore((state) => state.modelBackend); + const sendMessage = useSendMessage(); + const { data: health } = useHealth(); + const { data: fetchedMessages } = useSessionMessages(activeSessionId); + const online = modelBackend === "cpp" ? health?.cpp_server === "ok" : health?.torch_model === "ok"; + + useEffect(() => { + if (fetchedMessages) { + setMessages(fetchedMessages); + } + }, [fetchedMessages, setMessages]); + + const now = (): string => new Date().toISOString(); + + const handleSend = (prompt: string): void => { + const sessionId = activeSessionId ?? crypto.randomUUID(); + setActiveSession(sessionId); + const userMessage: Message = { + id: `local-${crypto.randomUUID()}`, + session_id: sessionId, + role: "user", + text: prompt, + chars: 0, + seconds: 0, + created_at: now(), + }; + const pendingId = `pending-${crypto.randomUUID()}`; + const pendingMessage: Message = { + id: pendingId, + session_id: sessionId, + role: "assistant", + text: "", + chars: 0, + seconds: 0, + created_at: now(), + pending: true, + }; + addMessage(userMessage); + addMessage(pendingMessage); + sendMessage.mutate( + { session_id: sessionId, prompt, max_tokens: maxTokens, temperature, stream: false, model_backend: modelBackend }, + { + onSuccess: (response) => { + setActiveSession(response.session_id); + replaceMessage(pendingId, { + id: response.id, + session_id: response.session_id, + role: "assistant", + text: response.text, + prompt: response.prompt, + chars: response.chars, + seconds: response.seconds, + created_at: response.created_at, + }); + }, + onError: (error) => { + const message = + error instanceof ApiClientError + ? error.apiError.message + : "Could not reach the selected model. Check the C++ server or engine checkpoint."; + replaceMessage(pendingId, { + ...pendingMessage, + text: message, + pending: false, + error: "model_unavailable", + }); + }, + }, + ); + }; + + return ( +
+ {messages.length === 0 ? : } + +
+ ); +} diff --git a/frontend/src/components/chat/EmptyState.tsx b/frontend/src/components/chat/EmptyState.tsx new file mode 100644 index 0000000..19fdf7a --- /dev/null +++ b/frontend/src/components/chat/EmptyState.tsx @@ -0,0 +1,15 @@ +export function EmptyState() { + return ( +
+
+
+ Quadtrix.cpp icon +
+
+

Quadtrix.cpp

+

Minimal local chat interface. Start typing below to begin.

+
+
+
+ ); +} diff --git a/frontend/src/components/chat/MessageAvatar.tsx b/frontend/src/components/chat/MessageAvatar.tsx new file mode 100644 index 0000000..25373d5 --- /dev/null +++ b/frontend/src/components/chat/MessageAvatar.tsx @@ -0,0 +1,20 @@ +import type { Role } from "../../types"; + +interface MessageAvatarProps { + role: Role; +} + +export function MessageAvatar({ role }: MessageAvatarProps) { + const isUser = role === "user"; + return ( +
+ {isUser ? "You" : "Q"} +
+ ); +} diff --git a/frontend/src/components/chat/MessageList.tsx b/frontend/src/components/chat/MessageList.tsx new file mode 100644 index 0000000..e38a0af --- /dev/null +++ b/frontend/src/components/chat/MessageList.tsx @@ -0,0 +1,20 @@ +import { useAutoScroll } from "../../hooks/useAutoScroll"; +import type { Message } from "../../types"; +import { MessageRow } from "./MessageRow"; + +interface MessageListProps { + messages: Message[]; +} + +export function MessageList({ messages }: MessageListProps) { + const scrollRef = useAutoScroll(messages.length); + return ( +
+
+ {messages.map((message) => ( + + ))} +
+
+ ); +} diff --git a/frontend/src/components/chat/MessageRow.tsx b/frontend/src/components/chat/MessageRow.tsx new file mode 100644 index 0000000..372d585 --- /dev/null +++ b/frontend/src/components/chat/MessageRow.tsx @@ -0,0 +1,64 @@ +import { motion } from "framer-motion"; +import { useState } from "react"; + +import { formatRelativeTime } from "../../utils/time"; +import type { Message } from "../../types"; +import { MessageAvatar } from "./MessageAvatar"; +import { ThinkingIndicator } from "./ThinkingIndicator"; + +interface MessageRowProps { + message: Message; +} + +export function MessageRow({ message }: MessageRowProps) { + const [copied, setCopied] = useState(false); + const isUser = message.role === "user"; + + const copyText = async (): Promise => { + try { + await navigator.clipboard.writeText(message.text); + setCopied(true); + window.setTimeout(() => setCopied(false), 1200); + } catch (error) { + setCopied(false); + } + }; + + return ( + + {!isUser && } +
+
+ {isUser ? "You" : "Quadtrix"} + {formatRelativeTime(message.created_at)} + {!isUser && !message.pending && ( + + )} +
+
+ {message.pending ? : {message.text}} +
+
+ {isUser && } +
+ ); +} diff --git a/frontend/src/components/chat/StarterPrompts.tsx b/frontend/src/components/chat/StarterPrompts.tsx new file mode 100644 index 0000000..fb10a22 --- /dev/null +++ b/frontend/src/components/chat/StarterPrompts.tsx @@ -0,0 +1,22 @@ +interface StarterPromptsProps { + onSelect: (prompt: string) => void; +} + +const prompts = ["Once upon a time", "Timmy is a", "hi how are you", "The little door opened"]; + +export function StarterPrompts({ onSelect }: StarterPromptsProps) { + return ( +
+ {prompts.map((prompt) => ( + + ))} +
+ ); +} diff --git a/frontend/src/components/chat/ThinkingIndicator.tsx b/frontend/src/components/chat/ThinkingIndicator.tsx new file mode 100644 index 0000000..e83d0f5 --- /dev/null +++ b/frontend/src/components/chat/ThinkingIndicator.tsx @@ -0,0 +1,12 @@ +export function ThinkingIndicator() { + return ( +
+ Quadtrix is thinking + + + + + +
+ ); +} diff --git a/frontend/src/components/input/CharCounter.tsx b/frontend/src/components/input/CharCounter.tsx new file mode 100644 index 0000000..5ddf619 --- /dev/null +++ b/frontend/src/components/input/CharCounter.tsx @@ -0,0 +1,9 @@ +interface CharCounterProps { + count: number; + max: number; +} + +export function CharCounter({ count, max }: CharCounterProps) { + const over = count > max; + return {count}/{max}; +} diff --git a/frontend/src/components/input/InputBar.tsx b/frontend/src/components/input/InputBar.tsx new file mode 100644 index 0000000..bcfc22b --- /dev/null +++ b/frontend/src/components/input/InputBar.tsx @@ -0,0 +1,77 @@ +import { useEffect, useRef, useState } from "react"; + +import { CharCounter } from "./CharCounter"; + +interface InputBarProps { + disabled: boolean; + isSending: boolean; + onSend: (prompt: string) => void; + initialValue?: string; + onDraftChange?: (value: string) => void; +} + +export function InputBar({ disabled, isSending, onSend, initialValue = "", onDraftChange }: InputBarProps) { + const [value, setValue] = useState(initialValue); + const ref = useRef(null); + + useEffect(() => { + setValue(initialValue); + }, [initialValue]); + + useEffect(() => { + onDraftChange?.(value); + }, [onDraftChange, value]); + + useEffect(() => { + if (!ref.current) { + return; + } + ref.current.style.height = "auto"; + ref.current.style.height = `${Math.min(ref.current.scrollHeight, 120)}px`; + }, [value]); + + const submit = (): void => { + const prompt = value.trim(); + if (!prompt || disabled || isSending) { + return; + } + setValue(""); + onSend(prompt); + }; + + return ( +
+
+
+