diff --git a/.dockerignore b/.dockerignore index cc482b31..08e0db2e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,63 +1,35 @@ -notebooks/ -data/ -.uploads/ -.venv/ -.env -sqlite-db/ -temp/ -google-credentials.json -docker-compose* -.docker_data/ -docs/ -surreal_data/ -surreal-data/ -notebook_data/ -temp/ -*.env -.git/ -.github/ +# Git +.git +.gitignore -# Frontend build artifacts and dependencies -frontend/node_modules/ -frontend/.next/ -frontend/.env.local +# Python +__pycache__ +*.pyc +*.pyo +*.pyd +.venv +venv +ENV +env +.pytest_cache +.mypy_cache +.ruff_cache -# Cache directories (recursive patterns) -**/__pycache__/ -**/.mypy_cache/ -**/.ruff_cache/ -**/.pytest_cache/ -**/*.pyc -**/*.pyo -**/*.pyd -.coverage -.coverage.* -htmlcov/ -.tox/ -.nox/ -.cache/ -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ +# Frontend +frontend/node_modules +frontend/.next +frontend/dist +frontend/out +frontend/.env* +frontend/*.log -# IDE and editor files -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - - -.quarentena/ -surreal_single_data/ \ No newline at end of file +# Project +.antigravity +.gemini +tmp +data +mydata +*.db +*.log +docker.env +.env \ No newline at end of file diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index eb0e4c11..4d51cc1d 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -22,8 +22,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - pull-requests: read - issues: read + pull-requests: write + issues: write id-token: write steps: @@ -38,6 +38,7 @@ jobs: id: claude-review uses: anthropics/claude-code-action@v1 with: + github_token: ${{ secrets.GITHUB_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' plugins: 'code-review@claude-code-plugins' diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index d300267f..a285620a 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -20,8 +20,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - pull-requests: read - issues: read + pull-requests: write + issues: write id-token: write actions: read # Required for Claude to read CI results on PRs steps: @@ -34,6 +34,7 @@ jobs: id: claude uses: anthropics/claude-code-action@v1 with: + github_token: ${{ secrets.GITHUB_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # This is an optional setting that allows Claude to read CI results on PRs diff --git a/Dockerfile b/Dockerfile index ab6aeb0e..57e4ba91 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,9 +6,11 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ # Install system dependencies required for building certain Python packages # Add Node.js 20.x LTS for building frontend -RUN apt-get update && apt-get upgrade -y && apt-get install -y \ - gcc g++ git make \ +# NOTE: gcc/g++/make removed - uv should download pre-built wheels. Add back if build fails. +# NOTE: gcc/g++/make required for some python dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ + build-essential \ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y nodejs \ && rm -rf /var/lib/apt/lists/* @@ -35,7 +37,11 @@ COPY . /app # Install frontend dependencies and build WORKDIR /app/frontend +ARG NPM_REGISTRY=https://registry.npmjs.org/ +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm config set registry ${NPM_REGISTRY} RUN npm ci +COPY frontend/ ./ RUN npm run build # Return to app root @@ -46,7 +52,7 @@ FROM python:3.12-slim-bookworm AS runtime # Install only runtime system dependencies (no build tools) # Add Node.js 20.x LTS for running frontend -RUN apt-get update && apt-get upgrade -y && apt-get install -y \ +RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \ ffmpeg \ supervisor \ curl \ @@ -63,8 +69,8 @@ WORKDIR /app # Copy the virtual environment from builder stage COPY --from=builder /app/.venv /app/.venv -# Copy the application code -COPY --from=builder /app /app +# Copy the source code (the rest) +COPY . /app # Ensure uv uses the existing venv without attempting network operations ENV UV_NO_SYNC=1 diff --git a/Dockerfile.single b/Dockerfile.single index 86f98b57..901d7637 100644 --- a/Dockerfile.single +++ b/Dockerfile.single @@ -1,51 +1,39 @@ -# Build stage -FROM python:3.12-slim-bookworm AS builder - -# Install uv using the official method -COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ +# Stage 1: Frontend Builder +FROM node:20-slim AS frontend-builder +WORKDIR /app/frontend -# Install system dependencies required for building certain Python packages -# Add Node.js 20.x LTS for building frontend -RUN apt-get update && apt-get upgrade -y && apt-get install -y \ - gcc g++ git make \ - curl \ - && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ - && apt-get install -y nodejs \ - && rm -rf /var/lib/apt/lists/* +# Copy dependency files first to leverage cache +COPY frontend/package.json frontend/package-lock.json ./ +ARG NPM_REGISTRY=https://registry.npmjs.org/ +RUN npm config set registry ${NPM_REGISTRY} +RUN npm ci -# Set build optimization environment variables -ENV MAKEFLAGS="-j$(nproc)" -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 -ENV UV_COMPILE_BYTECODE=1 -ENV UV_LINK_MODE=copy +# Copy the rest of the frontend source +COPY frontend/ ./ +# Build the frontend +RUN npm run build -# Set the working directory in the container to /app +# Stage 2: Backend Builder +FROM python:3.12-slim-bookworm AS backend-builder +# Install build dependencies +RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends build-essential && rm -rf /var/lib/apt/lists/* +# Install uv +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ WORKDIR /app -# Copy dependency files and minimal package structure first for better layer caching +# Set build optimization environment variables +ENV UV_HTTP_TIMEOUT=120 + +# Copy dependency files first COPY pyproject.toml uv.lock ./ COPY open_notebook/__init__.py ./open_notebook/__init__.py - -# Install dependencies with optimizations (this layer will be cached unless dependencies change) +# Install dependencies RUN uv sync --frozen --no-dev -# Copy the rest of the application code -COPY . /app - -# Install frontend dependencies and build -WORKDIR /app/frontend -RUN npm ci -RUN npm run build - -# Return to app root -WORKDIR /app - -# Runtime stage +# Stage 3: Runtime FROM python:3.12-slim-bookworm AS runtime -# Install runtime system dependencies including curl for SurrealDB installation -# Add Node.js 20.x LTS for running frontend +# Install runtime dependencies RUN apt-get update && apt-get upgrade -y && apt-get install -y \ ffmpeg \ supervisor \ @@ -57,47 +45,34 @@ RUN apt-get update && apt-get upgrade -y && apt-get install -y \ # Install SurrealDB RUN curl --proto '=https' --tlsv1.2 -sSf https://install.surrealdb.com | sh -# Install uv using the official method +# Install uv (optional but helpful for some scripts) COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ -# Set the working directory in the container to /app WORKDIR /app -# Copy the virtual environment from builder stage -COPY --from=builder /app/.venv /app/.venv - -# Copy the application code -COPY --from=builder /app /app +# Copy backend virtualenv and source code +COPY --from=backend-builder /app/.venv /app/.venv +COPY . /app/ -# Copy built frontend from builder stage -COPY --from=builder /app/frontend/.next/standalone /app/frontend/ -COPY --from=builder /app/frontend/.next/static /app/frontend/.next/static -COPY --from=builder /app/frontend/public /app/frontend/public +# Copy built frontend from standalone output +COPY --from=frontend-builder /app/frontend/.next/standalone /app/frontend/ +COPY --from=frontend-builder /app/frontend/.next/static /app/frontend/.next/static +COPY --from=frontend-builder /app/frontend/public /app/frontend/public -# Create directories for data persistence +# Setup directories and permissions RUN mkdir -p /app/data /mydata -# Copy and make executable the wait-for-api script -COPY scripts/wait-for-api.sh /app/scripts/wait-for-api.sh +# Ensure wait-for-api script is executable RUN chmod +x /app/scripts/wait-for-api.sh -# Expose ports for Frontend and API -EXPOSE 8502 5055 - -# Copy single-container supervisord configuration +# Copy supervisord configuration COPY supervisord.single.conf /etc/supervisor/conf.d/supervisord.conf # Create log directories RUN mkdir -p /var/log/supervisor -# Runtime API URL Configuration -# The API_URL environment variable can be set at container runtime to configure -# where the frontend should connect to the API. This allows the same Docker image -# to work in different deployment scenarios without rebuilding. -# -# If not set, the system will auto-detect based on incoming requests. -# Set API_URL when using reverse proxies or custom domains. -# -# Example: docker run -e API_URL=https://your-domain.com/api ... +# Expose ports +EXPOSE 8502 5055 +# Set startup command CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] \ No newline at end of file diff --git a/api/auth.py b/api/auth.py index 04895c80..0ad23786 100644 --- a/api/auth.py +++ b/api/auth.py @@ -1,7 +1,7 @@ import os from typing import Optional -from fastapi import HTTPException, Request +from fastapi import Depends, HTTPException, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from starlette.middleware.base import BaseHTTPMiddleware from starlette.responses import JSONResponse @@ -12,35 +12,41 @@ class PasswordAuthMiddleware(BaseHTTPMiddleware): Middleware to check password authentication for all API requests. Only active when OPEN_NOTEBOOK_PASSWORD environment variable is set. """ - + def __init__(self, app, excluded_paths: Optional[list] = None): super().__init__(app) self.password = os.environ.get("OPEN_NOTEBOOK_PASSWORD") - self.excluded_paths = excluded_paths or ["/", "/health", "/docs", "/openapi.json", "/redoc"] - + self.excluded_paths = excluded_paths or [ + "/", + "/health", + "/docs", + "/openapi.json", + "/redoc", + ] + async def dispatch(self, request: Request, call_next): # Skip authentication if no password is set if not self.password: return await call_next(request) - + # Skip authentication for excluded paths if request.url.path in self.excluded_paths: return await call_next(request) - + # Skip authentication for CORS preflight requests (OPTIONS) if request.method == "OPTIONS": return await call_next(request) - + # Check authorization header auth_header = request.headers.get("Authorization") - + if not auth_header: return JSONResponse( status_code=401, content={"detail": "Missing authorization header"}, - headers={"WWW-Authenticate": "Bearer"} + headers={"WWW-Authenticate": "Bearer"}, ) - + # Expected format: "Bearer {password}" try: scheme, credentials = auth_header.split(" ", 1) @@ -50,17 +56,17 @@ async def dispatch(self, request: Request, call_next): return JSONResponse( status_code=401, content={"detail": "Invalid authorization header format"}, - headers={"WWW-Authenticate": "Bearer"} + headers={"WWW-Authenticate": "Bearer"}, ) - + # Check password if credentials != self.password: return JSONResponse( status_code=401, content={"detail": "Invalid password"}, - headers={"WWW-Authenticate": "Bearer"} + headers={"WWW-Authenticate": "Bearer"}, ) - + # Password is correct, proceed with the request response = await call_next(request) return response @@ -70,17 +76,19 @@ async def dispatch(self, request: Request, call_next): security = HTTPBearer(auto_error=False) -def check_api_password(credentials: Optional[HTTPAuthorizationCredentials] = None) -> bool: +def check_api_password( + credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), +) -> bool: """ Utility function to check API password. Can be used as a dependency in individual routes if needed. """ password = os.environ.get("OPEN_NOTEBOOK_PASSWORD") - + # No password set, allow access if not password: return True - + # No credentials provided if not credentials: raise HTTPException( @@ -88,7 +96,7 @@ def check_api_password(credentials: Optional[HTTPAuthorizationCredentials] = Non detail="Missing authorization", headers={"WWW-Authenticate": "Bearer"}, ) - + # Check password if credentials.credentials != password: raise HTTPException( @@ -96,5 +104,5 @@ def check_api_password(credentials: Optional[HTTPAuthorizationCredentials] = Non detail="Invalid password", headers={"WWW-Authenticate": "Bearer"}, ) - - return True \ No newline at end of file + + return True diff --git a/api/chat_service.py b/api/chat_service.py index 34bc3cf7..fa446b8b 100644 --- a/api/chat_service.py +++ b/api/chat_service.py @@ -2,6 +2,7 @@ Chat service for API operations. Provides async interface for chat functionality. """ + import os from typing import Any, Dict, List, Optional @@ -11,7 +12,7 @@ class ChatService: """Service for chat-related API operations""" - + def __init__(self): self.base_url = os.getenv("API_BASE_URL", "http://127.0.0.1:5055") # Add authentication header if password is set @@ -19,7 +20,7 @@ def __init__(self): password = os.getenv("OPEN_NOTEBOOK_PASSWORD") if password: self.headers["Authorization"] = f"Bearer {password}" - + async def get_sessions(self, notebook_id: str) -> List[Dict[str, Any]]: """Get all chat sessions for a notebook""" try: @@ -27,14 +28,14 @@ async def get_sessions(self, notebook_id: str) -> List[Dict[str, Any]]: response = await client.get( f"{self.base_url}/api/chat/sessions", params={"notebook_id": notebook_id}, - headers=self.headers + headers=self.headers, ) response.raise_for_status() return response.json() except Exception as e: logger.error(f"Error fetching chat sessions: {str(e)}") raise - + async def create_session( self, notebook_id: str, @@ -48,33 +49,33 @@ async def create_session( data["title"] = title if model_override is not None: data["model_override"] = model_override - + async with httpx.AsyncClient() as client: response = await client.post( f"{self.base_url}/api/chat/sessions", json=data, - headers=self.headers + headers=self.headers, ) response.raise_for_status() return response.json() except Exception as e: logger.error(f"Error creating chat session: {str(e)}") raise - + async def get_session(self, session_id: str) -> Dict[str, Any]: """Get a specific session with messages""" try: async with httpx.AsyncClient() as client: response = await client.get( f"{self.base_url}/api/chat/sessions/{session_id}", - headers=self.headers + headers=self.headers, ) response.raise_for_status() return response.json() except Exception as e: logger.error(f"Error fetching session: {str(e)}") raise - + async def update_session( self, session_id: str, @@ -90,34 +91,36 @@ async def update_session( data["model_override"] = model_override if not data: - raise ValueError("At least one field must be provided to update a session") + raise ValueError( + "At least one field must be provided to update a session" + ) async with httpx.AsyncClient() as client: response = await client.put( f"{self.base_url}/api/chat/sessions/{session_id}", json=data, - headers=self.headers + headers=self.headers, ) response.raise_for_status() return response.json() except Exception as e: logger.error(f"Error updating session: {str(e)}") raise - + async def delete_session(self, session_id: str) -> Dict[str, Any]: """Delete a chat session""" try: async with httpx.AsyncClient() as client: response = await client.delete( f"{self.base_url}/api/chat/sessions/{session_id}", - headers=self.headers + headers=self.headers, ) response.raise_for_status() return response.json() except Exception as e: logger.error(f"Error deleting session: {str(e)}") raise - + async def execute_chat( self, session_id: str, @@ -127,41 +130,32 @@ async def execute_chat( ) -> Dict[str, Any]: """Execute a chat request""" try: - data = { - "session_id": session_id, - "message": message, - "context": context - } + data = {"session_id": session_id, "message": message, "context": context} if model_override is not None: data["model_override"] = model_override - + # Short connect timeout (10s), long read timeout (10 min) for Ollama/local LLMs timeout = httpx.Timeout(connect=10.0, read=600.0, write=30.0, pool=10.0) async with httpx.AsyncClient(timeout=timeout) as client: response = await client.post( - f"{self.base_url}/api/chat/execute", - json=data, - headers=self.headers + f"{self.base_url}/api/chat/execute", json=data, headers=self.headers ) response.raise_for_status() return response.json() except Exception as e: logger.error(f"Error executing chat: {str(e)}") raise - - async def build_context(self, notebook_id: str, context_config: Dict[str, Any]) -> Dict[str, Any]: + + async def build_context( + self, notebook_id: str, context_config: Dict[str, Any] + ) -> Dict[str, Any]: """Build context for a notebook""" try: - data = { - "notebook_id": notebook_id, - "context_config": context_config - } - + data = {"notebook_id": notebook_id, "context_config": context_config} + async with httpx.AsyncClient() as client: response = await client.post( - f"{self.base_url}/api/chat/context", - json=data, - headers=self.headers + f"{self.base_url}/api/chat/context", json=data, headers=self.headers ) response.raise_for_status() return response.json() diff --git a/api/client.py b/api/client.py index 016d31ce..91978c06 100644 --- a/api/client.py +++ b/api/client.py @@ -23,14 +23,20 @@ def __init__(self, base_url: Optional[str] = None): timeout_value = float(timeout_str) # Validate timeout is within reasonable bounds (30s - 3600s / 1 hour) if timeout_value < 30: - logger.warning(f"API_CLIENT_TIMEOUT={timeout_value}s is too low, using minimum of 30s") + logger.warning( + f"API_CLIENT_TIMEOUT={timeout_value}s is too low, using minimum of 30s" + ) timeout_value = 30.0 elif timeout_value > 3600: - logger.warning(f"API_CLIENT_TIMEOUT={timeout_value}s is too high, using maximum of 3600s") + logger.warning( + f"API_CLIENT_TIMEOUT={timeout_value}s is too high, using maximum of 3600s" + ) timeout_value = 3600.0 self.timeout = timeout_value except ValueError: - logger.error(f"Invalid API_CLIENT_TIMEOUT value '{timeout_str}', using default 300s") + logger.error( + f"Invalid API_CLIENT_TIMEOUT value '{timeout_str}', using default 300s" + ) self.timeout = 300.0 # Add authentication header if password is set @@ -45,7 +51,7 @@ def _make_request( """Make HTTP request to the API.""" url = f"{self.base_url}{endpoint}" request_timeout = timeout if timeout is not None else self.timeout - + # Merge headers headers = kwargs.get("headers", {}) headers.update(self.headers) @@ -82,20 +88,28 @@ def get_notebooks( result = self._make_request("GET", "/api/notebooks", params=params) return result if isinstance(result, list) else [result] - def create_notebook(self, name: str, description: str = "") -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def create_notebook( + self, name: str, description: str = "" + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Create a new notebook.""" data = {"name": name, "description": description} return self._make_request("POST", "/api/notebooks", json=data) - def get_notebook(self, notebook_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def get_notebook( + self, notebook_id: str + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Get a specific notebook.""" return self._make_request("GET", f"/api/notebooks/{notebook_id}") - def update_notebook(self, notebook_id: str, **updates) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def update_notebook( + self, notebook_id: str, **updates + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Update a notebook.""" return self._make_request("PUT", f"/api/notebooks/{notebook_id}", json=updates) - def delete_notebook(self, notebook_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def delete_notebook( + self, notebook_id: str + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Delete a notebook.""" return self._make_request("DELETE", f"/api/notebooks/{notebook_id}") @@ -148,7 +162,9 @@ def get_models(self, model_type: Optional[str] = None) -> List[Dict[Any, Any]]: result = self._make_request("GET", "/api/models", params=params) return result if isinstance(result, list) else [result] - def create_model(self, name: str, provider: str, model_type: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def create_model( + self, name: str, provider: str, model_type: str + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Create a new model.""" data = { "name": name, @@ -157,7 +173,9 @@ def create_model(self, name: str, provider: str, model_type: str) -> Union[Dict[ } return self._make_request("POST", "/api/models", json=data) - def delete_model(self, model_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def delete_model( + self, model_id: str + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Delete a model.""" return self._make_request("DELETE", f"/api/models/{model_id}") @@ -165,7 +183,9 @@ def get_default_models(self) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Get default model assignments.""" return self._make_request("GET", "/api/models/defaults") - def update_default_models(self, **defaults) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def update_default_models( + self, **defaults + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Update default model assignments.""" return self._make_request("PUT", "/api/models/defaults", json=defaults) @@ -193,17 +213,23 @@ def create_transformation( } return self._make_request("POST", "/api/transformations", json=data) - def get_transformation(self, transformation_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def get_transformation( + self, transformation_id: str + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Get a specific transformation.""" return self._make_request("GET", f"/api/transformations/{transformation_id}") - def update_transformation(self, transformation_id: str, **updates) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def update_transformation( + self, transformation_id: str, **updates + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Update a transformation.""" return self._make_request( "PUT", f"/api/transformations/{transformation_id}", json=updates ) - def delete_transformation(self, transformation_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def delete_transformation( + self, transformation_id: str + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Delete a transformation.""" return self._make_request("DELETE", f"/api/transformations/{transformation_id}") @@ -252,7 +278,9 @@ def get_note(self, note_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Get a specific note.""" return self._make_request("GET", f"/api/notes/{note_id}") - def update_note(self, note_id: str, **updates) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def update_note( + self, note_id: str, **updates + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Update a note.""" return self._make_request("PUT", f"/api/notes/{note_id}", json=updates) @@ -261,7 +289,9 @@ def delete_note(self, note_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any] return self._make_request("DELETE", f"/api/notes/{note_id}") # Embedding API methods - def embed_content(self, item_id: str, item_type: str, async_processing: bool = False) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def embed_content( + self, item_id: str, item_type: str, async_processing: bool = False + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Embed content for vector search.""" data = { "item_id": item_id, @@ -276,7 +306,7 @@ def rebuild_embeddings( mode: str = "existing", include_sources: bool = True, include_notes: bool = True, - include_insights: bool = True + include_insights: bool = True, ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Rebuild embeddings in bulk. @@ -291,9 +321,13 @@ def rebuild_embeddings( } # Use double the configured timeout for bulk rebuild operations (or configured value if already high) rebuild_timeout = max(self.timeout, min(self.timeout * 2, 3600.0)) - return self._make_request("POST", "/api/embeddings/rebuild", json=data, timeout=rebuild_timeout) + return self._make_request( + "POST", "/api/embeddings/rebuild", json=data, timeout=rebuild_timeout + ) - def get_rebuild_status(self, command_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def get_rebuild_status( + self, command_id: str + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Get status of a rebuild operation.""" return self._make_request("GET", f"/api/embeddings/rebuild/{command_id}/status") @@ -302,7 +336,9 @@ def get_settings(self) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Get all application settings.""" return self._make_request("GET", "/api/settings") - def update_settings(self, **settings) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def update_settings( + self, **settings + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Update application settings.""" return self._make_request("PUT", "/api/settings", json=settings) @@ -370,21 +406,29 @@ def create_source( data["transformations"] = transformations # Use configured timeout for source creation (especially PDF processing with OCR) - return self._make_request("POST", "/api/sources/json", json=data, timeout=self.timeout) + return self._make_request( + "POST", "/api/sources/json", json=data, timeout=self.timeout + ) def get_source(self, source_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Get a specific source.""" return self._make_request("GET", f"/api/sources/{source_id}") - def get_source_status(self, source_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def get_source_status( + self, source_id: str + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Get processing status for a source.""" return self._make_request("GET", f"/api/sources/{source_id}/status") - def update_source(self, source_id: str, **updates) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def update_source( + self, source_id: str, **updates + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Update a source.""" return self._make_request("PUT", f"/api/sources/{source_id}", json=updates) - def delete_source(self, source_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def delete_source( + self, source_id: str + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Delete a source.""" return self._make_request("DELETE", f"/api/sources/{source_id}") @@ -394,11 +438,15 @@ def get_source_insights(self, source_id: str) -> List[Dict[Any, Any]]: result = self._make_request("GET", f"/api/sources/{source_id}/insights") return result if isinstance(result, list) else [result] - def get_insight(self, insight_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def get_insight( + self, insight_id: str + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Get a specific insight.""" return self._make_request("GET", f"/api/insights/{insight_id}") - def delete_insight(self, insight_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def delete_insight( + self, insight_id: str + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Delete a specific insight.""" return self._make_request("DELETE", f"/api/insights/{insight_id}") @@ -430,7 +478,9 @@ def get_episode_profiles(self) -> List[Dict[Any, Any]]: result = self._make_request("GET", "/api/episode-profiles") return result if isinstance(result, list) else [result] - def get_episode_profile(self, profile_name: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def get_episode_profile( + self, profile_name: str + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Get a specific episode profile by name.""" return self._make_request("GET", f"/api/episode-profiles/{profile_name}") @@ -460,11 +510,17 @@ def create_episode_profile( } return self._make_request("POST", "/api/episode-profiles", json=data) - def update_episode_profile(self, profile_id: str, **updates) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def update_episode_profile( + self, profile_id: str, **updates + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Update an episode profile.""" - return self._make_request("PUT", f"/api/episode-profiles/{profile_id}", json=updates) + return self._make_request( + "PUT", f"/api/episode-profiles/{profile_id}", json=updates + ) - def delete_episode_profile(self, profile_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def delete_episode_profile( + self, profile_id: str + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Delete an episode profile.""" return self._make_request("DELETE", f"/api/episode-profiles/{profile_id}") diff --git a/api/context_service.py b/api/context_service.py index 3f6f63fa..87f766fb 100644 --- a/api/context_service.py +++ b/api/context_service.py @@ -16,17 +16,14 @@ def __init__(self): logger.info("Using API for context operations") def get_notebook_context( - self, - notebook_id: str, - context_config: Optional[Dict] = None + self, notebook_id: str, context_config: Optional[Dict] = None ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Get context for a notebook.""" result = api_client.get_notebook_context( - notebook_id=notebook_id, - context_config=context_config + notebook_id=notebook_id, context_config=context_config ) return result # Global service instance -context_service = ContextService() \ No newline at end of file +context_service = ContextService() diff --git a/api/embedding_service.py b/api/embedding_service.py index b3d4d8ec..1378d73b 100644 --- a/api/embedding_service.py +++ b/api/embedding_service.py @@ -15,11 +15,13 @@ class EmbeddingService: def __init__(self): logger.info("Using API for embedding operations") - def embed_content(self, item_id: str, item_type: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: + def embed_content( + self, item_id: str, item_type: str + ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Embed content for vector search.""" result = api_client.embed_content(item_id=item_id, item_type=item_type) return result # Global service instance -embedding_service = EmbeddingService() \ No newline at end of file +embedding_service = EmbeddingService() diff --git a/api/episode_profiles_service.py b/api/episode_profiles_service.py index 22deb31c..f2072e42 100644 --- a/api/episode_profiles_service.py +++ b/api/episode_profiles_service.py @@ -12,10 +12,10 @@ class EpisodeProfilesService: """Service layer for episode profiles operations using API.""" - + def __init__(self): logger.info("Using API for episode profiles operations") - + def get_all_episode_profiles(self) -> List[EpisodeProfile]: """Get all episode profiles.""" profiles_data = api_client.get_episode_profiles() @@ -31,16 +31,20 @@ def get_all_episode_profiles(self) -> List[EpisodeProfile]: transcript_provider=profile_data["transcript_provider"], transcript_model=profile_data["transcript_model"], default_briefing=profile_data["default_briefing"], - num_segments=profile_data["num_segments"] + num_segments=profile_data["num_segments"], ) profile.id = profile_data["id"] profiles.append(profile) return profiles - + def get_episode_profile(self, profile_name: str) -> EpisodeProfile: """Get a specific episode profile by name.""" profile_response = api_client.get_episode_profile(profile_name) - profile_data = profile_response if isinstance(profile_response, dict) else profile_response[0] + profile_data = ( + profile_response + if isinstance(profile_response, dict) + else profile_response[0] + ) profile = EpisodeProfile( name=profile_data["name"], description=profile_data.get("description", ""), @@ -50,11 +54,11 @@ def get_episode_profile(self, profile_name: str) -> EpisodeProfile: transcript_provider=profile_data["transcript_provider"], transcript_model=profile_data["transcript_model"], default_briefing=profile_data["default_briefing"], - num_segments=profile_data["num_segments"] + num_segments=profile_data["num_segments"], ) profile.id = profile_data["id"] return profile - + def create_episode_profile( self, name: str, @@ -79,7 +83,11 @@ def create_episode_profile( default_briefing=default_briefing, num_segments=num_segments, ) - profile_data = profile_response if isinstance(profile_response, dict) else profile_response[0] + profile_data = ( + profile_response + if isinstance(profile_response, dict) + else profile_response[0] + ) profile = EpisodeProfile( name=profile_data["name"], description=profile_data.get("description", ""), @@ -89,11 +97,11 @@ def create_episode_profile( transcript_provider=profile_data["transcript_provider"], transcript_model=profile_data["transcript_model"], default_briefing=profile_data["default_briefing"], - num_segments=profile_data["num_segments"] + num_segments=profile_data["num_segments"], ) profile.id = profile_data["id"] return profile - + def delete_episode_profile(self, profile_id: str) -> bool: """Delete an episode profile.""" api_client.delete_episode_profile(profile_id) @@ -101,4 +109,4 @@ def delete_episode_profile(self, profile_id: str) -> bool: # Global service instance -episode_profiles_service = EpisodeProfilesService() \ No newline at end of file +episode_profiles_service = EpisodeProfilesService() diff --git a/api/insights_service.py b/api/insights_service.py index b435519e..6ae87622 100644 --- a/api/insights_service.py +++ b/api/insights_service.py @@ -12,10 +12,10 @@ class InsightsService: """Service layer for insights operations using API.""" - + def __init__(self): logger.info("Using API for insights operations") - + def get_source_insights(self, source_id: str) -> List[SourceInsight]: """Get all insights for a specific source.""" insights_data = api_client.get_source_insights(source_id) @@ -31,11 +31,15 @@ def get_source_insights(self, source_id: str) -> List[SourceInsight]: insight.updated = insight_data["updated"] insights.append(insight) return insights - + def get_insight(self, insight_id: str) -> SourceInsight: """Get a specific insight.""" insight_response = api_client.get_insight(insight_id) - insight_data = insight_response if isinstance(insight_response, dict) else insight_response[0] + insight_data = ( + insight_response + if isinstance(insight_response, dict) + else insight_response[0] + ) insight = SourceInsight( insight_type=insight_data["insight_type"], content=insight_data["content"], @@ -45,16 +49,20 @@ def get_insight(self, insight_id: str) -> SourceInsight: insight.updated = insight_data["updated"] # Note: source_id from API response is not stored; use await insight.get_source() if needed return insight - + def delete_insight(self, insight_id: str) -> bool: """Delete a specific insight.""" api_client.delete_insight(insight_id) return True - - def save_insight_as_note(self, insight_id: str, notebook_id: Optional[str] = None) -> Note: + + def save_insight_as_note( + self, insight_id: str, notebook_id: Optional[str] = None + ) -> Note: """Convert an insight to a note.""" note_response = api_client.save_insight_as_note(insight_id, notebook_id) - note_data = note_response if isinstance(note_response, dict) else note_response[0] + note_data = ( + note_response if isinstance(note_response, dict) else note_response[0] + ) note = Note( title=note_data["title"], content=note_data["content"], @@ -64,11 +72,19 @@ def save_insight_as_note(self, insight_id: str, notebook_id: Optional[str] = Non note.created = note_data["created"] note.updated = note_data["updated"] return note - - def create_source_insight(self, source_id: str, transformation_id: str, model_id: Optional[str] = None) -> SourceInsight: + + def create_source_insight( + self, source_id: str, transformation_id: str, model_id: Optional[str] = None + ) -> SourceInsight: """Create a new insight for a source by running a transformation.""" - insight_response = api_client.create_source_insight(source_id, transformation_id, model_id) - insight_data = insight_response if isinstance(insight_response, dict) else insight_response[0] + insight_response = api_client.create_source_insight( + source_id, transformation_id, model_id + ) + insight_data = ( + insight_response + if isinstance(insight_response, dict) + else insight_response[0] + ) insight = SourceInsight( insight_type=insight_data["insight_type"], content=insight_data["content"], @@ -81,4 +97,4 @@ def create_source_insight(self, source_id: str, transformation_id: str, model_id # Global service instance -insights_service = InsightsService() \ No newline at end of file +insights_service = InsightsService() diff --git a/api/main.py b/api/main.py index d3efa8c0..b48db0ca 100644 --- a/api/main.py +++ b/api/main.py @@ -37,7 +37,6 @@ # Import commands to register them in the API process try: - logger.info("Commands imported in API process") except Exception as e: logger.error(f"Failed to import commands in API process: {e}") @@ -61,9 +60,13 @@ async def lifespan(app: FastAPI): logger.warning("Database migrations are pending. Running migrations...") await migration_manager.run_migration_up() new_version = await migration_manager.get_current_version() - logger.success(f"Migrations completed successfully. Database is now at version {new_version}") + logger.success( + f"Migrations completed successfully. Database is now at version {new_version}" + ) else: - logger.info("Database is already at the latest version. No migrations needed.") + logger.info( + "Database is already at the latest version. No migrations needed." + ) except Exception as e: logger.error(f"CRITICAL: Database migration failed: {str(e)}") logger.exception(e) @@ -88,7 +91,18 @@ async def lifespan(app: FastAPI): # Add password authentication middleware first # Exclude /api/auth/status and /api/config from authentication -app.add_middleware(PasswordAuthMiddleware, excluded_paths=["/", "/health", "/docs", "/openapi.json", "/redoc", "/api/auth/status", "/api/config"]) +app.add_middleware( + PasswordAuthMiddleware, + excluded_paths=[ + "/", + "/health", + "/docs", + "/openapi.json", + "/redoc", + "/api/auth/status", + "/api/config", + ], +) # Add CORS middleware last (so it processes first) app.add_middleware( @@ -119,7 +133,7 @@ async def custom_http_exception_handler(request: Request, exc: StarletteHTTPExce status_code=exc.status_code, content={"detail": exc.detail}, headers={ - "Access-Control-Allow-Origin": origin, + **(exc.headers or {}), "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true", "Access-Control-Allow-Methods": "*", "Access-Control-Allow-Headers": "*", @@ -136,7 +150,9 @@ async def custom_http_exception_handler(request: Request, exc: StarletteHTTPExce app.include_router(transformations.router, prefix="/api", tags=["transformations"]) app.include_router(notes.router, prefix="/api", tags=["notes"]) app.include_router(embedding.router, prefix="/api", tags=["embedding"]) -app.include_router(embedding_rebuild.router, prefix="/api/embeddings", tags=["embeddings"]) +app.include_router( + embedding_rebuild.router, prefix="/api/embeddings", tags=["embeddings"] +) app.include_router(settings.router, prefix="/api", tags=["settings"]) app.include_router(context.router, prefix="/api", tags=["context"]) app.include_router(sources.router, prefix="/api", tags=["sources"]) diff --git a/api/models_service.py b/api/models_service.py index 956c8fd4..ab857fa5 100644 --- a/api/models_service.py +++ b/api/models_service.py @@ -12,10 +12,10 @@ class ModelsService: """Service layer for models operations using API.""" - + def __init__(self): logger.info("Using API for models operations") - + def get_all_models(self, model_type: Optional[str] = None) -> List[Model]: """Get all models with optional type filtering.""" models_data = api_client.get_models(model_type=model_type) @@ -32,7 +32,7 @@ def get_all_models(self, model_type: Optional[str] = None) -> List[Model]: model.updated = model_data["updated"] models.append(model) return models - + def create_model(self, name: str, provider: str, model_type: str) -> Model: """Create a new model.""" response = api_client.create_model(name, provider, model_type) @@ -46,12 +46,12 @@ def create_model(self, name: str, provider: str, model_type: str) -> Model: model.created = model_data["created"] model.updated = model_data["updated"] return model - + def delete_model(self, model_id: str) -> bool: """Delete a model.""" api_client.delete_model(model_id) return True - + def get_default_models(self) -> DefaultModels: """Get default model assignments.""" response = api_client.get_default_models() @@ -60,15 +60,21 @@ def get_default_models(self) -> DefaultModels: # Set the values from API response defaults.default_chat_model = defaults_data.get("default_chat_model") - defaults.default_transformation_model = defaults_data.get("default_transformation_model") + defaults.default_transformation_model = defaults_data.get( + "default_transformation_model" + ) defaults.large_context_model = defaults_data.get("large_context_model") - defaults.default_text_to_speech_model = defaults_data.get("default_text_to_speech_model") - defaults.default_speech_to_text_model = defaults_data.get("default_speech_to_text_model") + defaults.default_text_to_speech_model = defaults_data.get( + "default_text_to_speech_model" + ) + defaults.default_speech_to_text_model = defaults_data.get( + "default_speech_to_text_model" + ) defaults.default_embedding_model = defaults_data.get("default_embedding_model") defaults.default_tools_model = defaults_data.get("default_tools_model") return defaults - + def update_default_models(self, defaults: DefaultModels) -> DefaultModels: """Update default model assignments.""" updates = { @@ -86,10 +92,16 @@ def update_default_models(self, defaults: DefaultModels) -> DefaultModels: # Update the defaults object with the response defaults.default_chat_model = defaults_data.get("default_chat_model") - defaults.default_transformation_model = defaults_data.get("default_transformation_model") + defaults.default_transformation_model = defaults_data.get( + "default_transformation_model" + ) defaults.large_context_model = defaults_data.get("large_context_model") - defaults.default_text_to_speech_model = defaults_data.get("default_text_to_speech_model") - defaults.default_speech_to_text_model = defaults_data.get("default_speech_to_text_model") + defaults.default_text_to_speech_model = defaults_data.get( + "default_text_to_speech_model" + ) + defaults.default_speech_to_text_model = defaults_data.get( + "default_speech_to_text_model" + ) defaults.default_embedding_model = defaults_data.get("default_embedding_model") defaults.default_tools_model = defaults_data.get("default_tools_model") @@ -97,4 +109,4 @@ def update_default_models(self, defaults: DefaultModels) -> DefaultModels: # Global service instance -models_service = ModelsService() \ No newline at end of file +models_service = ModelsService() diff --git a/api/notebook_service.py b/api/notebook_service.py index 340f35e9..ab1ff738 100644 --- a/api/notebook_service.py +++ b/api/notebook_service.py @@ -12,10 +12,10 @@ class NotebookService: """Service layer for notebook operations using API.""" - + def __init__(self): logger.info("Using API for notebook operations") - + def get_all_notebooks(self, order_by: str = "updated desc") -> List[Notebook]: """Get all notebooks.""" notebooks_data = api_client.get_notebooks(order_by=order_by) @@ -32,7 +32,7 @@ def get_all_notebooks(self, order_by: str = "updated desc") -> List[Notebook]: nb.updated = nb_data["updated"] notebooks.append(nb) return notebooks - + def get_notebook(self, notebook_id: str) -> Optional[Notebook]: """Get a specific notebook.""" response = api_client.get_notebook(notebook_id) @@ -60,7 +60,7 @@ def create_notebook(self, name: str, description: str = "") -> Notebook: nb.created = nb_data["created"] nb.updated = nb_data["updated"] return nb - + def update_notebook(self, notebook: Notebook) -> Notebook: """Update a notebook.""" updates = { @@ -76,7 +76,7 @@ def update_notebook(self, notebook: Notebook) -> Notebook: notebook.archived = nb_data["archived"] notebook.updated = nb_data["updated"] return notebook - + def delete_notebook(self, notebook: Notebook) -> bool: """Delete a notebook.""" api_client.delete_notebook(notebook.id or "") @@ -84,4 +84,4 @@ def delete_notebook(self, notebook: Notebook) -> bool: # Global service instance -notebook_service = NotebookService() \ No newline at end of file +notebook_service = NotebookService() diff --git a/api/notes_service.py b/api/notes_service.py index d47a37be..d336b88d 100644 --- a/api/notes_service.py +++ b/api/notes_service.py @@ -12,10 +12,10 @@ class NotesService: """Service layer for notes operations using API.""" - + def __init__(self): logger.info("Using API for notes operations") - + def get_all_notes(self, notebook_id: Optional[str] = None) -> List[Note]: """Get all notes with optional notebook filtering.""" notes_data = api_client.get_notes(notebook_id=notebook_id) @@ -32,11 +32,13 @@ def get_all_notes(self, notebook_id: Optional[str] = None) -> List[Note]: note.updated = note_data["updated"] notes.append(note) return notes - + def get_note(self, note_id: str) -> Note: """Get a specific note.""" note_response = api_client.get_note(note_id) - note_data = note_response if isinstance(note_response, dict) else note_response[0] + note_data = ( + note_response if isinstance(note_response, dict) else note_response[0] + ) note = Note( title=note_data["title"], content=note_data["content"], @@ -46,22 +48,21 @@ def get_note(self, note_id: str) -> Note: note.created = note_data["created"] note.updated = note_data["updated"] return note - + def create_note( self, content: str, title: Optional[str] = None, note_type: str = "human", - notebook_id: Optional[str] = None + notebook_id: Optional[str] = None, ) -> Note: """Create a new note.""" note_response = api_client.create_note( - content=content, - title=title, - note_type=note_type, - notebook_id=notebook_id + content=content, title=title, note_type=note_type, notebook_id=notebook_id + ) + note_data = ( + note_response if isinstance(note_response, dict) else note_response[0] ) - note_data = note_response if isinstance(note_response, dict) else note_response[0] note = Note( title=note_data["title"], content=note_data["content"], @@ -71,7 +72,7 @@ def create_note( note.created = note_data["created"] note.updated = note_data["updated"] return note - + def update_note(self, note: Note) -> Note: """Update a note.""" updates = { @@ -80,7 +81,9 @@ def update_note(self, note: Note) -> Note: "note_type": note.note_type, } note_response = api_client.update_note(note.id or "", **updates) - note_data = note_response if isinstance(note_response, dict) else note_response[0] + note_data = ( + note_response if isinstance(note_response, dict) else note_response[0] + ) # Update the note object with the response note.title = note_data["title"] @@ -89,7 +92,7 @@ def update_note(self, note: Note) -> Note: note.updated = note_data["updated"] return note - + def delete_note(self, note_id: str) -> bool: """Delete a note.""" api_client.delete_note(note_id) @@ -97,4 +100,4 @@ def delete_note(self, note_id: str) -> bool: # Global service instance -notes_service = NotesService() \ No newline at end of file +notes_service = NotesService() diff --git a/api/routers/auth.py b/api/routers/auth.py index 5c35c389..1bcd842f 100644 --- a/api/routers/auth.py +++ b/api/routers/auth.py @@ -20,5 +20,7 @@ async def get_auth_status(): return { "auth_enabled": auth_enabled, - "message": "Authentication is required" if auth_enabled else "Authentication is disabled" + "message": "Authentication is required" + if auth_enabled + else "Authentication is disabled", } diff --git a/api/routers/chat.py b/api/routers/chat.py index 61e1468b..76c4e9ef 100644 --- a/api/routers/chat.py +++ b/api/routers/chat.py @@ -15,6 +15,7 @@ router = APIRouter() + # Request/Response models class CreateSessionRequest(BaseModel): notebook_id: str = Field(..., description="Notebook ID to create session for") @@ -134,7 +135,8 @@ async def create_session(request: CreateSessionRequest): # Create new session session = ChatSession( - title=request.title or f"Chat Session {asyncio.get_event_loop().time():.0f}", + title=request.title + or f"Chat Session {asyncio.get_event_loop().time():.0f}", model_override=request.model_override, ) await session.save() @@ -334,9 +336,7 @@ async def execute_chat(request: ExecuteChatRequest): # Get current state current_state = chat_graph.get_state( - config=RunnableConfig( - configurable={"thread_id": request.session_id} - ) + config=RunnableConfig(configurable={"thread_id": request.session_id}) ) # Prepare state for execution diff --git a/api/routers/commands.py b/api/routers/commands.py index 264e0d34..8b2d41b2 100644 --- a/api/routers/commands.py +++ b/api/routers/commands.py @@ -9,16 +9,21 @@ router = APIRouter() + class CommandExecutionRequest(BaseModel): - command: str = Field(..., description="Command function name (e.g., 'process_text')") + command: str = Field( + ..., description="Command function name (e.g., 'process_text')" + ) app: str = Field(..., description="Application name (e.g., 'open_notebook')") input: Dict[str, Any] = Field(..., description="Arguments to pass to the command") + class CommandJobResponse(BaseModel): job_id: str status: str message: str + class CommandJobStatusResponse(BaseModel): job_id: str status: str @@ -28,19 +33,20 @@ class CommandJobStatusResponse(BaseModel): updated: Optional[str] = None progress: Optional[Dict[str, Any]] = None + @router.post("/commands/jobs", response_model=CommandJobResponse) async def execute_command(request: CommandExecutionRequest): """ Submit a command for background processing. Returns immediately with job ID for status tracking. - + Example request: { "command": "process_text", - "app": "open_notebook", - "input": { - "text": "Hello world", - "operation": "uppercase" + "app": "open_notebook", + "input": { + "text": "Hello world", + "operation": "uppercase" } } """ @@ -49,91 +55,91 @@ async def execute_command(request: CommandExecutionRequest): job_id = await CommandService.submit_command_job( module_name=request.app, # This should be "open_notebook" command_name=request.command, - command_args=request.input + command_args=request.input, ) - + return CommandJobResponse( job_id=job_id, status="submitted", - message=f"Command '{request.command}' submitted successfully" + message=f"Command '{request.command}' submitted successfully", ) - + except Exception as e: logger.error(f"Error submitting command: {str(e)}") raise HTTPException( - status_code=500, - detail=f"Failed to submit command: {str(e)}" + status_code=500, detail="Failed to submit command" ) + @router.get("/commands/jobs/{job_id}", response_model=CommandJobStatusResponse) async def get_command_job_status(job_id: str): """Get the status of a specific command job""" try: status_data = await CommandService.get_command_status(job_id) return CommandJobStatusResponse(**status_data) - + except Exception as e: logger.error(f"Error fetching job status: {str(e)}") raise HTTPException( - status_code=500, - detail=f"Failed to fetch job status: {str(e)}" + status_code=500, detail="Failed to fetch job status" ) + @router.get("/commands/jobs", response_model=List[Dict[str, Any]]) async def list_command_jobs( command_filter: Optional[str] = Query(None, description="Filter by command name"), status_filter: Optional[str] = Query(None, description="Filter by status"), - limit: int = Query(50, description="Maximum number of jobs to return") + limit: int = Query(50, description="Maximum number of jobs to return"), ): """List command jobs with optional filtering""" try: jobs = await CommandService.list_command_jobs( - command_filter=command_filter, - status_filter=status_filter, - limit=limit + command_filter=command_filter, status_filter=status_filter, limit=limit ) return jobs - + except Exception as e: logger.error(f"Error listing command jobs: {str(e)}") raise HTTPException( - status_code=500, - detail=f"Failed to list command jobs: {str(e)}" + status_code=500, detail="Failed to list command jobs" ) + @router.delete("/commands/jobs/{job_id}") async def cancel_command_job(job_id: str): """Cancel a running command job""" try: success = await CommandService.cancel_command_job(job_id) return {"job_id": job_id, "cancelled": success} - + except Exception as e: logger.error(f"Error cancelling command job: {str(e)}") raise HTTPException( - status_code=500, - detail=f"Failed to cancel command job: {str(e)}" + status_code=500, detail="Failed to cancel command job" ) + @router.get("/commands/registry/debug") async def debug_registry(): """Debug endpoint to see what commands are registered""" try: # Get all registered commands all_items = registry.get_all_commands() - + # Create JSON-serializable data command_items = [] for item in all_items: try: - command_items.append({ - "app_id": item.app_id, - "name": item.name, - "full_id": f"{item.app_id}.{item.name}" - }) + command_items.append( + { + "app_id": item.app_id, + "name": item.name, + "full_id": f"{item.app_id}.{item.name}", + } + ) except Exception as item_error: logger.error(f"Error processing item: {item_error}") - + # Get the basic command structure try: commands_dict: dict[str, list[str]] = {} @@ -143,18 +149,18 @@ async def debug_registry(): commands_dict[item.app_id].append(item.name) except Exception: commands_dict = {} - + return { "total_commands": len(all_items), "commands_by_app": commands_dict, - "command_items": command_items + "command_items": command_items, } - + except Exception as e: logger.error(f"Error debugging registry: {str(e)}") return { "error": str(e), "total_commands": 0, "commands_by_app": {}, - "command_items": [] - } \ No newline at end of file + "command_items": [], + } diff --git a/api/routers/config.py b/api/routers/config.py index 3a9e5d59..0f5424d1 100644 --- a/api/routers/config.py +++ b/api/routers/config.py @@ -11,7 +11,7 @@ from open_notebook.database.repository import repo_query from open_notebook.utils.version_utils import ( compare_versions, - get_version_from_github, + get_version_from_github_async, ) router = APIRouter() @@ -40,7 +40,7 @@ def get_version() -> str: return "unknown" -def get_latest_version_cached(current_version: str) -> tuple[Optional[str], bool]: +async def get_latest_version_cached(current_version: str) -> tuple[Optional[str], bool]: """ Check for the latest version from GitHub with caching. @@ -66,12 +66,13 @@ def get_latest_version_cached(current_version: str) -> tuple[Optional[str], bool logger.info("Checking for latest version from GitHub...") # Fetch latest version from GitHub with 10-second timeout - latest_version = get_version_from_github( - "https://github.com/lfnovo/open-notebook", - "main" + latest_version = await get_version_from_github_async( + "https://github.com/lfnovo/open-notebook", "main" ) - logger.info(f"Latest version from GitHub: {latest_version}, Current version: {current_version}") + logger.info( + f"Latest version from GitHub: {latest_version}, Current version: {current_version}" + ) # Compare versions has_update = compare_versions(current_version, latest_version) < 0 @@ -107,10 +108,7 @@ async def check_database_health() -> dict: """ try: # 2-second timeout for database health check - result = await asyncio.wait_for( - repo_query("RETURN 1"), - timeout=2.0 - ) + result = await asyncio.wait_for(repo_query("RETURN 1"), timeout=2.0) if result: return {"status": "online"} return {"status": "offline", "error": "Empty result"} @@ -142,7 +140,7 @@ async def get_config(request: Request): has_update = False try: - latest_version, has_update = get_latest_version_cached(current_version) + latest_version, has_update = await get_latest_version_cached(current_version) except Exception as e: # Extra safety: ensure version check never breaks the config endpoint logger.error(f"Unexpected error during version check: {e}") diff --git a/api/routers/context.py b/api/routers/context.py index 70cd70f6..92dd723b 100644 --- a/api/routers/context.py +++ b/api/routers/context.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, HTTPException from loguru import logger diff --git a/api/routers/embedding.py b/api/routers/embedding.py index 40b70d9b..63b9dd98 100644 --- a/api/routers/embedding.py +++ b/api/routers/embedding.py @@ -88,7 +88,11 @@ async def embed_content(embed_request: EmbedRequest): message = "Note embedded successfully" return EmbedResponse( - success=True, message=message, item_id=item_id, item_type=item_type, command_id=command_id + success=True, + message=message, + item_id=item_id, + item_type=item_type, + command_id=command_id, ) except HTTPException: diff --git a/api/routers/embedding_rebuild.py b/api/routers/embedding_rebuild.py index 8a0f9a10..36978919 100644 --- a/api/routers/embedding_rebuild.py +++ b/api/routers/embedding_rebuild.py @@ -173,10 +173,12 @@ async def get_rebuild_status(command_id: str): response.completed_at = str(status.updated) # Add error message if failed - if status.status == "failed" and status.result and isinstance(status.result, dict): - response.error_message = status.result.get( - "error_message", "Unknown error" - ) + if ( + status.status == "failed" + and status.result + and isinstance(status.result, dict) + ): + response.error_message = status.result.get("error_message", "Unknown error") return response diff --git a/api/routers/episode_profiles.py b/api/routers/episode_profiles.py index e35aa4ec..da0baff2 100644 --- a/api/routers/episode_profiles.py +++ b/api/routers/episode_profiles.py @@ -27,7 +27,7 @@ async def list_episode_profiles(): """List all available episode profiles""" try: profiles = await EpisodeProfile.get_all(order_by="name asc") - + return [ EpisodeProfileResponse( id=str(profile.id), @@ -39,16 +39,15 @@ async def list_episode_profiles(): transcript_provider=profile.transcript_provider, transcript_model=profile.transcript_model, default_briefing=profile.default_briefing, - num_segments=profile.num_segments + num_segments=profile.num_segments, ) for profile in profiles ] - + except Exception as e: logger.error(f"Failed to fetch episode profiles: {e}") raise HTTPException( - status_code=500, - detail=f"Failed to fetch episode profiles: {str(e)}" + status_code=500, detail="Failed to fetch episode profiles" ) @@ -57,13 +56,12 @@ async def get_episode_profile(profile_name: str): """Get a specific episode profile by name""" try: profile = await EpisodeProfile.get_by_name(profile_name) - + if not profile: raise HTTPException( - status_code=404, - detail=f"Episode profile '{profile_name}' not found" + status_code=404, detail=f"Episode profile '{profile_name}' not found" ) - + return EpisodeProfileResponse( id=str(profile.id), name=profile.name, @@ -74,16 +72,15 @@ async def get_episode_profile(profile_name: str): transcript_provider=profile.transcript_provider, transcript_model=profile.transcript_model, default_briefing=profile.default_briefing, - num_segments=profile.num_segments + num_segments=profile.num_segments, ) - + except HTTPException: raise except Exception as e: logger.error(f"Failed to fetch episode profile '{profile_name}': {e}") raise HTTPException( - status_code=500, - detail=f"Failed to fetch episode profile: {str(e)}" + status_code=500, detail="Failed to fetch episode profile" ) @@ -93,7 +90,9 @@ class EpisodeProfileCreate(BaseModel): speaker_config: str = Field(..., description="Reference to speaker profile name") outline_provider: str = Field(..., description="AI provider for outline generation") outline_model: str = Field(..., description="AI model for outline generation") - transcript_provider: str = Field(..., description="AI provider for transcript generation") + transcript_provider: str = Field( + ..., description="AI provider for transcript generation" + ) transcript_model: str = Field(..., description="AI model for transcript generation") default_briefing: str = Field(..., description="Default briefing template") num_segments: int = Field(default=5, description="Number of podcast segments") @@ -112,11 +111,11 @@ async def create_episode_profile(profile_data: EpisodeProfileCreate): transcript_provider=profile_data.transcript_provider, transcript_model=profile_data.transcript_model, default_briefing=profile_data.default_briefing, - num_segments=profile_data.num_segments + num_segments=profile_data.num_segments, ) - + await profile.save() - + return EpisodeProfileResponse( id=str(profile.id), name=profile.name, @@ -127,14 +126,13 @@ async def create_episode_profile(profile_data: EpisodeProfileCreate): transcript_provider=profile.transcript_provider, transcript_model=profile.transcript_model, default_briefing=profile.default_briefing, - num_segments=profile.num_segments + num_segments=profile.num_segments, ) - + except Exception as e: logger.error(f"Failed to create episode profile: {e}") raise HTTPException( - status_code=500, - detail=f"Failed to create episode profile: {str(e)}" + status_code=500, detail="Failed to create episode profile" ) @@ -143,13 +141,12 @@ async def update_episode_profile(profile_id: str, profile_data: EpisodeProfileCr """Update an existing episode profile""" try: profile = await EpisodeProfile.get(profile_id) - + if not profile: raise HTTPException( - status_code=404, - detail=f"Episode profile '{profile_id}' not found" + status_code=404, detail=f"Episode profile '{profile_id}' not found" ) - + # Update fields profile.name = profile_data.name profile.description = profile_data.description @@ -160,9 +157,9 @@ async def update_episode_profile(profile_id: str, profile_data: EpisodeProfileCr profile.transcript_model = profile_data.transcript_model profile.default_briefing = profile_data.default_briefing profile.num_segments = profile_data.num_segments - + await profile.save() - + return EpisodeProfileResponse( id=str(profile.id), name=profile.name, @@ -173,16 +170,15 @@ async def update_episode_profile(profile_id: str, profile_data: EpisodeProfileCr transcript_provider=profile.transcript_provider, transcript_model=profile.transcript_model, default_briefing=profile.default_briefing, - num_segments=profile.num_segments + num_segments=profile.num_segments, ) - + except HTTPException: raise except Exception as e: logger.error(f"Failed to update episode profile: {e}") raise HTTPException( - status_code=500, - detail=f"Failed to update episode profile: {str(e)}" + status_code=500, detail="Failed to update episode profile" ) @@ -191,39 +187,38 @@ async def delete_episode_profile(profile_id: str): """Delete an episode profile""" try: profile = await EpisodeProfile.get(profile_id) - + if not profile: raise HTTPException( - status_code=404, - detail=f"Episode profile '{profile_id}' not found" + status_code=404, detail=f"Episode profile '{profile_id}' not found" ) - + await profile.delete() - + return {"message": "Episode profile deleted successfully"} - + except HTTPException: raise except Exception as e: logger.error(f"Failed to delete episode profile: {e}") raise HTTPException( - status_code=500, - detail=f"Failed to delete episode profile: {str(e)}" + status_code=500, detail="Failed to delete episode profile" ) -@router.post("/episode-profiles/{profile_id}/duplicate", response_model=EpisodeProfileResponse) +@router.post( + "/episode-profiles/{profile_id}/duplicate", response_model=EpisodeProfileResponse +) async def duplicate_episode_profile(profile_id: str): """Duplicate an episode profile""" try: original = await EpisodeProfile.get(profile_id) - + if not original: raise HTTPException( - status_code=404, - detail=f"Episode profile '{profile_id}' not found" + status_code=404, detail=f"Episode profile '{profile_id}' not found" ) - + # Create duplicate with modified name duplicate = EpisodeProfile( name=f"{original.name} - Copy", @@ -234,11 +229,11 @@ async def duplicate_episode_profile(profile_id: str): transcript_provider=original.transcript_provider, transcript_model=original.transcript_model, default_briefing=original.default_briefing, - num_segments=original.num_segments + num_segments=original.num_segments, ) - + await duplicate.save() - + return EpisodeProfileResponse( id=str(duplicate.id), name=duplicate.name, @@ -249,14 +244,13 @@ async def duplicate_episode_profile(profile_id: str): transcript_provider=duplicate.transcript_provider, transcript_model=duplicate.transcript_model, default_briefing=duplicate.default_briefing, - num_segments=duplicate.num_segments + num_segments=duplicate.num_segments, ) - + except HTTPException: raise except Exception as e: logger.error(f"Failed to duplicate episode profile: {e}") raise HTTPException( - status_code=500, - detail=f"Failed to duplicate episode profile: {str(e)}" - ) \ No newline at end of file + status_code=500, detail="Failed to duplicate episode profile" + ) diff --git a/api/routers/insights.py b/api/routers/insights.py index b651e709..b9e2c714 100644 --- a/api/routers/insights.py +++ b/api/routers/insights.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, HTTPException from loguru import logger @@ -16,10 +15,10 @@ async def get_insight(insight_id: str): insight = await SourceInsight.get(insight_id) if not insight: raise HTTPException(status_code=404, detail="Insight not found") - + # Get source ID from the insight relationship source = await insight.get_source() - + return SourceInsightResponse( id=insight.id or "", source_id=source.id or "", @@ -32,7 +31,7 @@ async def get_insight(insight_id: str): raise except Exception as e: logger.error(f"Error fetching insight {insight_id}: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error fetching insight: {str(e)}") + raise HTTPException(status_code=500, detail="Error fetching insight") @router.delete("/insights/{insight_id}") @@ -42,15 +41,15 @@ async def delete_insight(insight_id: str): insight = await SourceInsight.get(insight_id) if not insight: raise HTTPException(status_code=404, detail="Insight not found") - + await insight.delete() - + return {"message": "Insight deleted successfully"} except HTTPException: raise except Exception as e: logger.error(f"Error deleting insight {insight_id}: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error deleting insight: {str(e)}") + raise HTTPException(status_code=500, detail="Error deleting insight") @router.post("/insights/{insight_id}/save-as-note", response_model=NoteResponse) @@ -60,10 +59,10 @@ async def save_insight_as_note(insight_id: str, request: SaveAsNoteRequest): insight = await SourceInsight.get(insight_id) if not insight: raise HTTPException(status_code=404, detail="Insight not found") - + # Use the existing save_as_note method from the domain model note = await insight.save_as_note(request.notebook_id) - + return NoteResponse( id=note.id or "", title=note.title, @@ -78,4 +77,6 @@ async def save_insight_as_note(insight_id: str, request: SaveAsNoteRequest): raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error(f"Error saving insight {insight_id} as note: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error saving insight as note: {str(e)}") \ No newline at end of file + raise HTTPException( + status_code=500, detail="Error saving insight as note" + ) diff --git a/api/routers/models.py b/api/routers/models.py index 6574dacb..3744bf9f 100644 --- a/api/routers/models.py +++ b/api/routers/models.py @@ -61,7 +61,7 @@ def _check_azure_support(mode: str) -> bool: @router.get("/models", response_model=List[ModelResponse]) async def get_models( - type: Optional[str] = Query(None, description="Filter by model type") + type: Optional[str] = Query(None, description="Filter by model type"), ): """Get all configured models with optional type filtering.""" try: @@ -69,7 +69,7 @@ async def get_models( models = await Model.get_models_by_type(type) else: models = await Model.get_all() - + return [ ModelResponse( id=model.id, @@ -95,19 +95,24 @@ async def create_model(model_data: ModelCreate): if model_data.type not in valid_types: raise HTTPException( status_code=400, - detail=f"Invalid model type. Must be one of: {valid_types}" + detail=f"Invalid model type. Must be one of: {valid_types}", ) # Check for duplicate model name under the same provider and type (case-insensitive) from open_notebook.database.repository import repo_query + existing = await repo_query( "SELECT * FROM model WHERE string::lowercase(provider) = $provider AND string::lowercase(name) = $name AND string::lowercase(type) = $type LIMIT 1", - {"provider": model_data.provider.lower(), "name": model_data.name.lower(), "type": model_data.type.lower()} + { + "provider": model_data.provider.lower(), + "name": model_data.name.lower(), + "type": model_data.type.lower(), + }, ) if existing: raise HTTPException( status_code=400, - detail=f"Model '{model_data.name}' already exists for provider '{model_data.provider}' with type '{model_data.type}'" + detail=f"Model '{model_data.name}' already exists for provider '{model_data.provider}' with type '{model_data.type}'", ) new_model = Model( @@ -141,9 +146,9 @@ async def delete_model(model_id: str): model = await Model.get(model_id) if not model: raise HTTPException(status_code=404, detail="Model not found") - + await model.delete() - + return {"message": "Model deleted successfully"} except HTTPException: raise @@ -169,7 +174,9 @@ async def get_default_models(): ) except Exception as e: logger.error(f"Error fetching default models: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error fetching default models: {str(e)}") + raise HTTPException( + status_code=500, detail=f"Error fetching default models: {str(e)}" + ) @router.put("/models/defaults", response_model=DefaultModelsResponse) @@ -177,23 +184,29 @@ async def update_default_models(defaults_data: DefaultModelsResponse): """Update default model assignments.""" try: defaults = await DefaultModels.get_instance() - + # Update only provided fields if defaults_data.default_chat_model is not None: defaults.default_chat_model = defaults_data.default_chat_model # type: ignore[attr-defined] if defaults_data.default_transformation_model is not None: - defaults.default_transformation_model = defaults_data.default_transformation_model # type: ignore[attr-defined] + defaults.default_transformation_model = ( + defaults_data.default_transformation_model + ) # type: ignore[attr-defined] if defaults_data.large_context_model is not None: defaults.large_context_model = defaults_data.large_context_model # type: ignore[attr-defined] if defaults_data.default_text_to_speech_model is not None: - defaults.default_text_to_speech_model = defaults_data.default_text_to_speech_model # type: ignore[attr-defined] + defaults.default_text_to_speech_model = ( + defaults_data.default_text_to_speech_model + ) # type: ignore[attr-defined] if defaults_data.default_speech_to_text_model is not None: - defaults.default_speech_to_text_model = defaults_data.default_speech_to_text_model # type: ignore[attr-defined] + defaults.default_speech_to_text_model = ( + defaults_data.default_speech_to_text_model + ) # type: ignore[attr-defined] if defaults_data.default_embedding_model is not None: defaults.default_embedding_model = defaults_data.default_embedding_model # type: ignore[attr-defined] if defaults_data.default_tools_model is not None: defaults.default_tools_model = defaults_data.default_tools_model # type: ignore[attr-defined] - + await defaults.update() # No cache refresh needed - next access will fetch fresh data from DB @@ -211,7 +224,9 @@ async def update_default_models(defaults_data: DefaultModelsResponse): raise except Exception as e: logger.error(f"Error updating default models: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error updating default models: {str(e)}") + raise HTTPException( + status_code=500, detail=f"Error updating default models: {str(e)}" + ) @router.get("/models/providers", response_model=ProviderAvailabilityResponse) @@ -252,7 +267,7 @@ async def get_provider_availability(): or _check_openai_compatible_support("TTS") ), } - + available_providers = [k for k, v in provider_status.items() if v] unavailable_providers = [k for k, v in provider_status.items() if not v] @@ -275,13 +290,19 @@ async def get_provider_availability(): # Special handling for openai-compatible to check mode-specific availability if provider == "openai-compatible": for model_type, mode in mode_mapping.items(): - if model_type in esperanto_available and provider in esperanto_available[model_type]: + if ( + model_type in esperanto_available + and provider in esperanto_available[model_type] + ): if _check_openai_compatible_support(mode): supported_types[provider].append(model_type) # Special handling for azure to check mode-specific availability elif provider == "azure": for model_type, mode in mode_mapping.items(): - if model_type in esperanto_available and provider in esperanto_available[model_type]: + if ( + model_type in esperanto_available + and provider in esperanto_available[model_type] + ): if _check_azure_support(mode): supported_types[provider].append(model_type) else: @@ -289,12 +310,14 @@ async def get_provider_availability(): for model_type, providers in esperanto_available.items(): if provider in providers: supported_types[provider].append(model_type) - + return ProviderAvailabilityResponse( available=available_providers, unavailable=unavailable_providers, - supported_types=supported_types + supported_types=supported_types, ) except Exception as e: logger.error(f"Error checking provider availability: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error checking provider availability: {str(e)}") \ No newline at end of file + raise HTTPException( + status_code=500, detail=f"Error checking provider availability: {str(e)}" + ) diff --git a/api/routers/notes.py b/api/routers/notes.py index 1eed228a..d07cd340 100644 --- a/api/routers/notes.py +++ b/api/routers/notes.py @@ -12,13 +12,14 @@ @router.get("/notes", response_model=List[NoteResponse]) async def get_notes( - notebook_id: Optional[str] = Query(None, description="Filter by notebook ID") + notebook_id: Optional[str] = Query(None, description="Filter by notebook ID"), ): """Get all notes with optional notebook filtering.""" try: if notebook_id: # Get notes for a specific notebook from open_notebook.domain.notebook import Notebook + notebook = await Notebook.get(notebook_id) if not notebook: raise HTTPException(status_code=404, detail="Notebook not found") @@ -26,7 +27,7 @@ async def get_notes( else: # Get all notes notes = await Note.get_all(order_by="updated desc") - + return [ NoteResponse( id=note.id or "", @@ -53,21 +54,24 @@ async def create_note(note_data: NoteCreate): title = note_data.title if not title and note_data.note_type == "ai" and note_data.content: from open_notebook.graphs.prompt import graph as prompt_graph + prompt = "Based on the Note below, please provide a Title for this content, with max 15 words" result = await prompt_graph.ainvoke( { # type: ignore[arg-type] "input_text": note_data.content, - "prompt": prompt + "prompt": prompt, } ) title = result.get("output", "Untitled Note") - + # Validate note_type note_type: Optional[Literal["human", "ai"]] = None if note_data.note_type in ("human", "ai"): note_type = note_data.note_type # type: ignore[assignment] elif note_data.note_type is not None: - raise HTTPException(status_code=400, detail="note_type must be 'human' or 'ai'") + raise HTTPException( + status_code=400, detail="note_type must be 'human' or 'ai'" + ) new_note = Note( title=title, @@ -75,15 +79,16 @@ async def create_note(note_data: NoteCreate): note_type=note_type, ) await new_note.save() - + # Add to notebook if specified if note_data.notebook_id: from open_notebook.domain.notebook import Notebook + notebook = await Notebook.get(note_data.notebook_id) if not notebook: raise HTTPException(status_code=404, detail="Notebook not found") await new_note.add_to_notebook(note_data.notebook_id) - + return NoteResponse( id=new_note.id or "", title=new_note.title, @@ -108,7 +113,7 @@ async def get_note(note_id: str): note = await Note.get(note_id) if not note: raise HTTPException(status_code=404, detail="Note not found") - + return NoteResponse( id=note.id or "", title=note.title, @@ -131,7 +136,7 @@ async def update_note(note_id: str, note_update: NoteUpdate): note = await Note.get(note_id) if not note: raise HTTPException(status_code=404, detail="Note not found") - + # Update only provided fields if note_update.title is not None: note.title = note_update.title @@ -141,7 +146,9 @@ async def update_note(note_id: str, note_update: NoteUpdate): if note_update.note_type in ("human", "ai"): note.note_type = note_update.note_type # type: ignore[assignment] else: - raise HTTPException(status_code=400, detail="note_type must be 'human' or 'ai'") + raise HTTPException( + status_code=400, detail="note_type must be 'human' or 'ai'" + ) await note.save() @@ -169,12 +176,12 @@ async def delete_note(note_id: str): note = await Note.get(note_id) if not note: raise HTTPException(status_code=404, detail="Note not found") - + await note.delete() - + return {"message": "Note deleted successfully"} except HTTPException: raise except Exception as e: logger.error(f"Error deleting note {note_id}: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error deleting note: {str(e)}") \ No newline at end of file + raise HTTPException(status_code=500, detail=f"Error deleting note: {str(e)}") diff --git a/api/routers/podcasts.py b/api/routers/podcasts.py index 9f833bca..b7168083 100644 --- a/api/routers/podcasts.py +++ b/api/routers/podcasts.py @@ -64,7 +64,7 @@ async def generate_podcast(request: PodcastGenerationRequest): except Exception as e: logger.error(f"Error generating podcast: {str(e)}") raise HTTPException( - status_code=500, detail=f"Failed to generate podcast: {str(e)}" + status_code=500, detail="Failed to generate podcast" ) @@ -78,7 +78,7 @@ async def get_podcast_job_status(job_id: str): except Exception as e: logger.error(f"Error fetching podcast job status: {str(e)}") raise HTTPException( - status_code=500, detail=f"Failed to fetch job status: {str(e)}" + status_code=500, detail="Failed to fetch job status" ) @@ -93,7 +93,7 @@ async def list_podcast_episodes(): # Skip incomplete episodes without command or audio if not episode.command and not episode.audio_file: continue - + # Get job status if available job_status = None if episode.command: @@ -132,7 +132,7 @@ async def list_podcast_episodes(): except Exception as e: logger.error(f"Error listing podcast episodes: {str(e)}") raise HTTPException( - status_code=500, detail=f"Failed to list podcast episodes: {str(e)}" + status_code=500, detail="Failed to list podcast episodes" ) @@ -175,7 +175,7 @@ async def get_podcast_episode(episode_id: str): except Exception as e: logger.error(f"Error fetching podcast episode: {str(e)}") - raise HTTPException(status_code=404, detail=f"Episode not found: {str(e)}") + raise HTTPException(status_code=404, detail="Episode not found") @router.get("/podcasts/episodes/{episode_id}/audio") @@ -187,7 +187,7 @@ async def stream_podcast_episode_audio(episode_id: str): raise except Exception as e: logger.error(f"Error fetching podcast episode for audio: {str(e)}") - raise HTTPException(status_code=404, detail=f"Episode not found: {str(e)}") + raise HTTPException(status_code=404, detail="Episode not found") if not episode.audio_file: raise HTTPException(status_code=404, detail="Episode has no audio file") @@ -209,7 +209,7 @@ async def delete_podcast_episode(episode_id: str): try: # Get the episode first to check if it exists and get the audio file path episode = await PodcastService.get_episode(episode_id) - + # Delete the physical audio file if it exists if episode.audio_file: audio_path = _resolve_audio_path(episode.audio_file) @@ -219,13 +219,15 @@ async def delete_podcast_episode(episode_id: str): logger.info(f"Deleted audio file: {audio_path}") except Exception as e: logger.warning(f"Failed to delete audio file {audio_path}: {e}") - + # Delete the episode from the database await episode.delete() - + logger.info(f"Deleted podcast episode: {episode_id}") return {"message": "Episode deleted successfully", "episode_id": episode_id} - + except Exception as e: logger.error(f"Error deleting podcast episode: {str(e)}") - raise HTTPException(status_code=500, detail=f"Failed to delete episode: {str(e)}") + raise HTTPException( + status_code=500, detail="Failed to delete episode" + ) diff --git a/api/routers/settings.py b/api/routers/settings.py index c5eabeb2..5e992619 100644 --- a/api/routers/settings.py +++ b/api/routers/settings.py @@ -23,7 +23,9 @@ async def get_settings(): ) except Exception as e: logger.error(f"Error fetching settings: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error fetching settings: {str(e)}") + raise HTTPException( + status_code=500, detail="Error fetching settings" + ) @router.put("/settings", response_model=SettingsResponse) @@ -36,30 +38,35 @@ async def update_settings(settings_update: SettingsUpdate): if settings_update.default_content_processing_engine_doc is not None: # Cast to proper literal type from typing import Literal, cast + settings.default_content_processing_engine_doc = cast( Literal["auto", "docling", "simple"], - settings_update.default_content_processing_engine_doc + settings_update.default_content_processing_engine_doc, ) if settings_update.default_content_processing_engine_url is not None: from typing import Literal, cast + settings.default_content_processing_engine_url = cast( Literal["auto", "firecrawl", "jina", "simple"], - settings_update.default_content_processing_engine_url + settings_update.default_content_processing_engine_url, ) if settings_update.default_embedding_option is not None: from typing import Literal, cast + settings.default_embedding_option = cast( Literal["ask", "always", "never"], - settings_update.default_embedding_option + settings_update.default_embedding_option, ) if settings_update.auto_delete_files is not None: from typing import Literal, cast + settings.auto_delete_files = cast( - Literal["yes", "no"], - settings_update.auto_delete_files + Literal["yes", "no"], settings_update.auto_delete_files ) if settings_update.youtube_preferred_languages is not None: - settings.youtube_preferred_languages = settings_update.youtube_preferred_languages + settings.youtube_preferred_languages = ( + settings_update.youtube_preferred_languages + ) await settings.update() @@ -76,4 +83,6 @@ async def update_settings(settings_update: SettingsUpdate): raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error(f"Error updating settings: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error updating settings: {str(e)}") \ No newline at end of file + raise HTTPException( + status_code=500, detail="Error updating settings" + ) diff --git a/api/routers/source_chat.py b/api/routers/source_chat.py index 38d31d2c..ddda4e16 100644 --- a/api/routers/source_chat.py +++ b/api/routers/source_chat.py @@ -18,15 +18,22 @@ router = APIRouter() + # Request/Response models class CreateSourceChatSessionRequest(BaseModel): source_id: str = Field(..., description="Source ID to create chat session for") title: Optional[str] = Field(None, description="Optional session title") - model_override: Optional[str] = Field(None, description="Optional model override for this session") + model_override: Optional[str] = Field( + None, description="Optional model override for this session" + ) + class UpdateSourceChatSessionRequest(BaseModel): title: Optional[str] = Field(None, description="New session title") - model_override: Optional[str] = Field(None, description="Model override for this session") + model_override: Optional[str] = Field( + None, description="Model override for this session" + ) + class ChatMessage(BaseModel): id: str = Field(..., description="Message ID") @@ -34,56 +41,81 @@ class ChatMessage(BaseModel): content: str = Field(..., description="Message content") timestamp: Optional[str] = Field(None, description="Message timestamp") + class ContextIndicator(BaseModel): - sources: List[str] = Field(default_factory=list, description="Source IDs used in context") - insights: List[str] = Field(default_factory=list, description="Insight IDs used in context") - notes: List[str] = Field(default_factory=list, description="Note IDs used in context") + sources: List[str] = Field( + default_factory=list, description="Source IDs used in context" + ) + insights: List[str] = Field( + default_factory=list, description="Insight IDs used in context" + ) + notes: List[str] = Field( + default_factory=list, description="Note IDs used in context" + ) + class SourceChatSessionResponse(BaseModel): id: str = Field(..., description="Session ID") title: str = Field(..., description="Session title") source_id: str = Field(..., description="Source ID") - model_override: Optional[str] = Field(None, description="Model override for this session") + model_override: Optional[str] = Field( + None, description="Model override for this session" + ) created: str = Field(..., description="Creation timestamp") updated: str = Field(..., description="Last update timestamp") - message_count: Optional[int] = Field(None, description="Number of messages in session") + message_count: Optional[int] = Field( + None, description="Number of messages in session" + ) + class SourceChatSessionWithMessagesResponse(SourceChatSessionResponse): - messages: List[ChatMessage] = Field(default_factory=list, description="Session messages") - context_indicators: Optional[ContextIndicator] = Field(None, description="Context indicators from last response") + messages: List[ChatMessage] = Field( + default_factory=list, description="Session messages" + ) + context_indicators: Optional[ContextIndicator] = Field( + None, description="Context indicators from last response" + ) + class SendMessageRequest(BaseModel): message: str = Field(..., description="User message content") - model_override: Optional[str] = Field(None, description="Optional model override for this message") + model_override: Optional[str] = Field( + None, description="Optional model override for this message" + ) + class SuccessResponse(BaseModel): success: bool = Field(True, description="Operation success status") message: str = Field(..., description="Success message") -@router.post("/sources/{source_id}/chat/sessions", response_model=SourceChatSessionResponse) +@router.post( + "/sources/{source_id}/chat/sessions", response_model=SourceChatSessionResponse +) async def create_source_chat_session( request: CreateSourceChatSessionRequest, - source_id: str = Path(..., description="Source ID") + source_id: str = Path(..., description="Source ID"), ): """Create a new chat session for a source.""" try: # Verify source exists - full_source_id = source_id if source_id.startswith("source:") else f"source:{source_id}" + full_source_id = ( + source_id if source_id.startswith("source:") else f"source:{source_id}" + ) source = await Source.get(full_source_id) if not source: raise HTTPException(status_code=404, detail="Source not found") - + # Create new session with model_override support session = ChatSession( title=request.title or f"Source Chat {asyncio.get_event_loop().time():.0f}", - model_override=request.model_override + model_override=request.model_override, ) await session.save() - + # Relate session to source using "refers_to" relation await session.relate("refers_to", full_source_id) - + return SourceChatSessionResponse( id=session.id or "", title=session.title or "Untitled Session", @@ -91,33 +123,37 @@ async def create_source_chat_session( model_override=session.model_override, created=str(session.created), updated=str(session.updated), - message_count=0 + message_count=0, ) except NotFoundError: raise HTTPException(status_code=404, detail="Source not found") except Exception as e: logger.error(f"Error creating source chat session: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error creating source chat session: {str(e)}") + raise HTTPException( + status_code=500, detail=f"Error creating source chat session: {str(e)}" + ) -@router.get("/sources/{source_id}/chat/sessions", response_model=List[SourceChatSessionResponse]) -async def get_source_chat_sessions( - source_id: str = Path(..., description="Source ID") -): +@router.get( + "/sources/{source_id}/chat/sessions", response_model=List[SourceChatSessionResponse] +) +async def get_source_chat_sessions(source_id: str = Path(..., description="Source ID")): """Get all chat sessions for a source.""" try: # Verify source exists - full_source_id = source_id if source_id.startswith("source:") else f"source:{source_id}" + full_source_id = ( + source_id if source_id.startswith("source:") else f"source:{source_id}" + ) source = await Source.get(full_source_id) if not source: raise HTTPException(status_code=404, detail="Source not found") - + # Get sessions that refer to this source - first get relations, then sessions relations = await repo_query( "SELECT in FROM refers_to WHERE out = $source_id", - {"source_id": ensure_record_id(full_source_id)} + {"source_id": ensure_record_id(full_source_id)}, ) - + sessions = [] for relation in relations: session_id = relation.get("in") @@ -125,16 +161,18 @@ async def get_source_chat_sessions( session_result = await repo_query(f"SELECT * FROM {session_id}") if session_result and len(session_result) > 0: session_data = session_result[0] - sessions.append(SourceChatSessionResponse( - id=session_data.get("id") or "", - title=session_data.get("title") or "Untitled Session", - source_id=source_id, - model_override=session_data.get("model_override"), - created=str(session_data.get("created")), - updated=str(session_data.get("updated")), - message_count=0 # TODO: Add message count if needed - )) - + sessions.append( + SourceChatSessionResponse( + id=session_data.get("id") or "", + title=session_data.get("title") or "Untitled Session", + source_id=source_id, + model_override=session_data.get("model_override"), + created=str(session_data.get("created")), + updated=str(session_data.get("updated")), + message_count=0, # TODO: Add message count if needed + ) + ) + # Sort sessions by created date (newest first) sessions.sort(key=lambda x: x.created, reverse=True) return sessions @@ -142,183 +180,232 @@ async def get_source_chat_sessions( raise HTTPException(status_code=404, detail="Source not found") except Exception as e: logger.error(f"Error fetching source chat sessions: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error fetching source chat sessions: {str(e)}") + raise HTTPException( + status_code=500, detail=f"Error fetching source chat sessions: {str(e)}" + ) -@router.get("/sources/{source_id}/chat/sessions/{session_id}", response_model=SourceChatSessionWithMessagesResponse) +@router.get( + "/sources/{source_id}/chat/sessions/{session_id}", + response_model=SourceChatSessionWithMessagesResponse, +) async def get_source_chat_session( source_id: str = Path(..., description="Source ID"), - session_id: str = Path(..., description="Session ID") + session_id: str = Path(..., description="Session ID"), ): """Get a specific source chat session with its messages.""" try: # Verify source exists - full_source_id = source_id if source_id.startswith("source:") else f"source:{source_id}" + full_source_id = ( + source_id if source_id.startswith("source:") else f"source:{source_id}" + ) source = await Source.get(full_source_id) if not source: raise HTTPException(status_code=404, detail="Source not found") - + # Get session - full_session_id = session_id if session_id.startswith("chat_session:") else f"chat_session:{session_id}" + full_session_id = ( + session_id + if session_id.startswith("chat_session:") + else f"chat_session:{session_id}" + ) session = await ChatSession.get(full_session_id) if not session: raise HTTPException(status_code=404, detail="Session not found") - + # Verify session is related to this source relation_query = await repo_query( "SELECT * FROM refers_to WHERE in = $session_id AND out = $source_id", - {"session_id": ensure_record_id(full_session_id), "source_id": ensure_record_id(full_source_id)} + { + "session_id": ensure_record_id(full_session_id), + "source_id": ensure_record_id(full_source_id), + }, ) - + if not relation_query: - raise HTTPException(status_code=404, detail="Session not found for this source") - + raise HTTPException( + status_code=404, detail="Session not found for this source" + ) + # Get session state from LangGraph to retrieve messages thread_state = source_chat_graph.get_state( config=RunnableConfig(configurable={"thread_id": session_id}) ) - + # Extract messages from state messages: list[ChatMessage] = [] context_indicators = None - + if thread_state and thread_state.values: # Extract messages if "messages" in thread_state.values: for msg in thread_state.values["messages"]: - messages.append(ChatMessage( - id=getattr(msg, 'id', f"msg_{len(messages)}"), - type=msg.type if hasattr(msg, 'type') else 'unknown', - content=msg.content if hasattr(msg, 'content') else str(msg), - timestamp=None # LangChain messages don't have timestamps by default - )) - + messages.append( + ChatMessage( + id=getattr(msg, "id", f"msg_{len(messages)}"), + type=msg.type if hasattr(msg, "type") else "unknown", + content=msg.content + if hasattr(msg, "content") + else str(msg), + timestamp=None, # LangChain messages don't have timestamps by default + ) + ) + # Extract context indicators from the last state if "context_indicators" in thread_state.values: context_data = thread_state.values["context_indicators"] context_indicators = ContextIndicator( sources=context_data.get("sources", []), insights=context_data.get("insights", []), - notes=context_data.get("notes", []) + notes=context_data.get("notes", []), ) - + return SourceChatSessionWithMessagesResponse( id=session.id or "", title=session.title or "Untitled Session", source_id=source_id, - model_override=getattr(session, 'model_override', None), + model_override=getattr(session, "model_override", None), created=str(session.created), updated=str(session.updated), message_count=len(messages), messages=messages, - context_indicators=context_indicators + context_indicators=context_indicators, ) except NotFoundError: raise HTTPException(status_code=404, detail="Source or session not found") except Exception as e: logger.error(f"Error fetching source chat session: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error fetching source chat session: {str(e)}") + raise HTTPException( + status_code=500, detail=f"Error fetching source chat session: {str(e)}" + ) -@router.put("/sources/{source_id}/chat/sessions/{session_id}", response_model=SourceChatSessionResponse) +@router.put( + "/sources/{source_id}/chat/sessions/{session_id}", + response_model=SourceChatSessionResponse, +) async def update_source_chat_session( request: UpdateSourceChatSessionRequest, source_id: str = Path(..., description="Source ID"), - session_id: str = Path(..., description="Session ID") + session_id: str = Path(..., description="Session ID"), ): """Update source chat session title and/or model override.""" try: # Verify source exists - full_source_id = source_id if source_id.startswith("source:") else f"source:{source_id}" + full_source_id = ( + source_id if source_id.startswith("source:") else f"source:{source_id}" + ) source = await Source.get(full_source_id) if not source: raise HTTPException(status_code=404, detail="Source not found") - + # Get session - full_session_id = session_id if session_id.startswith("chat_session:") else f"chat_session:{session_id}" + full_session_id = ( + session_id + if session_id.startswith("chat_session:") + else f"chat_session:{session_id}" + ) session = await ChatSession.get(full_session_id) if not session: raise HTTPException(status_code=404, detail="Session not found") - + # Verify session is related to this source relation_query = await repo_query( "SELECT * FROM refers_to WHERE in = $session_id AND out = $source_id", - {"session_id": ensure_record_id(full_session_id), "source_id": ensure_record_id(full_source_id)} + { + "session_id": ensure_record_id(full_session_id), + "source_id": ensure_record_id(full_source_id), + }, ) - + if not relation_query: - raise HTTPException(status_code=404, detail="Session not found for this source") - + raise HTTPException( + status_code=404, detail="Session not found for this source" + ) + # Update session fields if request.title is not None: session.title = request.title if request.model_override is not None: session.model_override = request.model_override - + await session.save() - + return SourceChatSessionResponse( id=session.id or "", title=session.title or "Untitled Session", source_id=source_id, - model_override=getattr(session, 'model_override', None), + model_override=getattr(session, "model_override", None), created=str(session.created), updated=str(session.updated), - message_count=0 + message_count=0, ) except NotFoundError: raise HTTPException(status_code=404, detail="Source or session not found") except Exception as e: logger.error(f"Error updating source chat session: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error updating source chat session: {str(e)}") + raise HTTPException( + status_code=500, detail=f"Error updating source chat session: {str(e)}" + ) -@router.delete("/sources/{source_id}/chat/sessions/{session_id}", response_model=SuccessResponse) +@router.delete( + "/sources/{source_id}/chat/sessions/{session_id}", response_model=SuccessResponse +) async def delete_source_chat_session( source_id: str = Path(..., description="Source ID"), - session_id: str = Path(..., description="Session ID") + session_id: str = Path(..., description="Session ID"), ): """Delete a source chat session.""" try: # Verify source exists - full_source_id = source_id if source_id.startswith("source:") else f"source:{source_id}" + full_source_id = ( + source_id if source_id.startswith("source:") else f"source:{source_id}" + ) source = await Source.get(full_source_id) if not source: raise HTTPException(status_code=404, detail="Source not found") - + # Get session - full_session_id = session_id if session_id.startswith("chat_session:") else f"chat_session:{session_id}" + full_session_id = ( + session_id + if session_id.startswith("chat_session:") + else f"chat_session:{session_id}" + ) session = await ChatSession.get(full_session_id) if not session: raise HTTPException(status_code=404, detail="Session not found") - + # Verify session is related to this source relation_query = await repo_query( "SELECT * FROM refers_to WHERE in = $session_id AND out = $source_id", - {"session_id": ensure_record_id(full_session_id), "source_id": ensure_record_id(full_source_id)} + { + "session_id": ensure_record_id(full_session_id), + "source_id": ensure_record_id(full_source_id), + }, ) - + if not relation_query: - raise HTTPException(status_code=404, detail="Session not found for this source") - + raise HTTPException( + status_code=404, detail="Session not found for this source" + ) + await session.delete() - + return SuccessResponse( - success=True, - message="Source chat session deleted successfully" + success=True, message="Source chat session deleted successfully" ) except NotFoundError: raise HTTPException(status_code=404, detail="Source or session not found") except Exception as e: logger.error(f"Error deleting source chat session: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error deleting source chat session: {str(e)}") + raise HTTPException( + status_code=500, detail=f"Error deleting source chat session: {str(e)}" + ) async def stream_source_chat_response( - session_id: str, - source_id: str, - message: str, - model_override: Optional[str] = None + session_id: str, source_id: str, message: str, model_override: Optional[str] = None ) -> AsyncGenerator[str, None]: """Stream the source chat response as Server-Sent Events.""" try: @@ -326,59 +413,52 @@ async def stream_source_chat_response( current_state = source_chat_graph.get_state( config=RunnableConfig(configurable={"thread_id": session_id}) ) - + # Prepare state for execution state_values = current_state.values if current_state else {} state_values["messages"] = state_values.get("messages", []) state_values["source_id"] = source_id state_values["model_override"] = model_override - + # Add user message to state user_message = HumanMessage(content=message) state_values["messages"].append(user_message) - + # Send user message event - user_event = { - "type": "user_message", - "content": message, - "timestamp": None - } + user_event = {"type": "user_message", "content": message, "timestamp": None} yield f"data: {json.dumps(user_event)}\n\n" - + # Execute source chat graph synchronously (like notebook chat does) result = source_chat_graph.invoke( input=state_values, # type: ignore[arg-type] config=RunnableConfig( - configurable={ - "thread_id": session_id, - "model_id": model_override - } - ) + configurable={"thread_id": session_id, "model_id": model_override} + ), ) - + # Stream the complete AI response if "messages" in result: for msg in result["messages"]: - if hasattr(msg, 'type') and msg.type == 'ai': + if hasattr(msg, "type") and msg.type == "ai": ai_event = { - "type": "ai_message", - "content": msg.content if hasattr(msg, 'content') else str(msg), - "timestamp": None + "type": "ai_message", + "content": msg.content if hasattr(msg, "content") else str(msg), + "timestamp": None, } yield f"data: {json.dumps(ai_event)}\n\n" - + # Stream context indicators if "context_indicators" in result: context_event = { "type": "context_indicators", - "data": result["context_indicators"] + "data": result["context_indicators"], } yield f"data: {json.dumps(context_event)}\n\n" - + # Send completion signal completion_event = {"type": "complete"} yield f"data: {json.dumps(completion_event)}\n\n" - + except Exception as e: logger.error(f"Error in source chat streaming: {str(e)}") error_event = {"type": "error", "message": str(e)} @@ -389,58 +469,71 @@ async def stream_source_chat_response( async def send_message_to_source_chat( request: SendMessageRequest, source_id: str = Path(..., description="Source ID"), - session_id: str = Path(..., description="Session ID") + session_id: str = Path(..., description="Session ID"), ): """Send a message to source chat session with SSE streaming response.""" try: # Verify source exists - full_source_id = source_id if source_id.startswith("source:") else f"source:{source_id}" + full_source_id = ( + source_id if source_id.startswith("source:") else f"source:{source_id}" + ) source = await Source.get(full_source_id) if not source: raise HTTPException(status_code=404, detail="Source not found") - + # Verify session exists and is related to source - full_session_id = session_id if session_id.startswith("chat_session:") else f"chat_session:{session_id}" + full_session_id = ( + session_id + if session_id.startswith("chat_session:") + else f"chat_session:{session_id}" + ) session = await ChatSession.get(full_session_id) if not session: raise HTTPException(status_code=404, detail="Session not found") - + # Verify session is related to this source relation_query = await repo_query( "SELECT * FROM refers_to WHERE in = $session_id AND out = $source_id", - {"session_id": ensure_record_id(full_session_id), "source_id": ensure_record_id(full_source_id)} + { + "session_id": ensure_record_id(full_session_id), + "source_id": ensure_record_id(full_source_id), + }, ) - + if not relation_query: - raise HTTPException(status_code=404, detail="Session not found for this source") - + raise HTTPException( + status_code=404, detail="Session not found for this source" + ) + if not request.message: raise HTTPException(status_code=400, detail="Message content is required") - + # Determine model override (request override takes precedence over session override) - model_override = request.model_override or getattr(session, 'model_override', None) - + model_override = request.model_override or getattr( + session, "model_override", None + ) + # Update session timestamp await session.save() - + # Return streaming response return StreamingResponse( stream_source_chat_response( session_id=session_id, source_id=full_source_id, message=request.message, - model_override=model_override + model_override=model_override, ), media_type="text/plain", headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", - "Content-Type": "text/plain; charset=utf-8" - } + "Content-Type": "text/plain; charset=utf-8", + }, ) - + except HTTPException: raise except Exception as e: logger.error(f"Error sending message to source chat: {str(e)}") - raise HTTPException(status_code=500, detail=f"Error sending message: {str(e)}") \ No newline at end of file + raise HTTPException(status_code=500, detail=f"Error sending message: {str(e)}") diff --git a/api/routers/sources.py b/api/routers/sources.py index 30b4e3e6..3544cf54 100644 --- a/api/routers/sources.py +++ b/api/routers/sources.py @@ -121,9 +121,7 @@ def str_to_bool(value: str) -> bool: try: transformations_list = json.loads(transformations) except json.JSONDecodeError: - logger.error( - f"Invalid JSON in transformations field: {transformations}" - ) + logger.error(f"Invalid JSON in transformations field: {transformations}") raise ValueError("Invalid JSON in transformations field") # Create SourceCreate instance @@ -152,18 +150,26 @@ def str_to_bool(value: str) -> bool: @router.get("/sources", response_model=List[SourceListResponse]) async def get_sources( notebook_id: Optional[str] = Query(None, description="Filter by notebook ID"), - limit: int = Query(50, ge=1, le=100, description="Number of sources to return (1-100)"), + limit: int = Query( + 50, ge=1, le=100, description="Number of sources to return (1-100)" + ), offset: int = Query(0, ge=0, description="Number of sources to skip"), - sort_by: str = Query("updated", description="Field to sort by (created or updated)"), + sort_by: str = Query( + "updated", description="Field to sort by (created or updated)" + ), sort_order: str = Query("desc", description="Sort order (asc or desc)"), ): """Get sources with pagination and sorting support.""" try: # Validate sort parameters if sort_by not in ["created", "updated"]: - raise HTTPException(status_code=400, detail="sort_by must be 'created' or 'updated'") + raise HTTPException( + status_code=400, detail="sort_by must be 'created' or 'updated'" + ) if sort_order.lower() not in ["asc", "desc"]: - raise HTTPException(status_code=400, detail="sort_order must be 'asc' or 'desc'") + raise HTTPException( + status_code=400, detail="sort_order must be 'asc' or 'desc'" + ) # Build ORDER BY clause order_clause = f"ORDER BY {sort_by} {sort_order.upper()}" @@ -185,11 +191,12 @@ async def get_sources( LIMIT $limit START $offset """ result = await repo_query( - query, { + query, + { "notebook_id": ensure_record_id(notebook_id), "limit": limit, - "offset": offset - } + "offset": offset, + }, ) else: # Query all sources - include command field @@ -272,8 +279,14 @@ async def get_status_with_limit(command_id: str): if status_obj: status = status_obj.status # Extract execution metadata from nested result structure - result_data: dict[str, Any] | None = getattr(status_obj, "result", None) - execution_metadata: dict[str, Any] = result_data.get("execution_metadata", {}) if isinstance(result_data, dict) else {} + result_data: dict[str, Any] | None = getattr( + status_obj, "result", None + ) + execution_metadata: dict[str, Any] = ( + result_data.get("execution_metadata", {}) + if isinstance(result_data, dict) + else {} + ) processing_info = { "started_at": execution_metadata.get("started_at"), "completed_at": execution_metadata.get("completed_at"), @@ -327,7 +340,7 @@ async def create_source( try: # Verify all specified notebooks exist (backward compatibility support) - for notebook_id in (source_data.notebooks or []): + for notebook_id in source_data.notebooks or []: notebook = await Notebook.get(notebook_id) if not notebook: raise HTTPException( @@ -399,7 +412,7 @@ async def create_source( # Add source to notebooks immediately so it appears in the UI # The source_graph will skip adding duplicates - for notebook_id in (source_data.notebooks or []): + for notebook_id in source_data.notebooks or []: await source.add_to_notebook(notebook_id) try: @@ -478,7 +491,7 @@ async def create_source( # Add source to notebooks immediately so it appears in the UI # The source_graph will skip adding duplicates - for notebook_id in (source_data.notebooks or []): + for notebook_id in source_data.notebooks or []: await source.add_to_notebook(notebook_id) # Execute command synchronously @@ -517,9 +530,7 @@ async def create_source( # Get the processed source if not source.id: - raise HTTPException( - status_code=500, detail="Source ID is missing" - ) + raise HTTPException(status_code=500, detail="Source ID is missing") processed_source = await Source.get(source.id) if not processed_source: raise HTTPException( @@ -657,9 +668,11 @@ async def get_source(source_id: str): # Get associated notebooks notebooks_query = await repo_query( "SELECT VALUE out FROM reference WHERE in = $source_id", - {"source_id": ensure_record_id(source.id or source_id)} + {"source_id": ensure_record_id(source.id or source_id)}, + ) + notebook_ids = ( + [str(nb_id) for nb_id in notebooks_query] if notebooks_query else [] ) - notebook_ids = [str(nb_id) for nb_id in notebooks_query] if notebooks_query else [] return SourceResponse( id=source.id or "", diff --git a/api/routers/speaker_profiles.py b/api/routers/speaker_profiles.py index e8611fdf..3ce88862 100644 --- a/api/routers/speaker_profiles.py +++ b/api/routers/speaker_profiles.py @@ -23,7 +23,7 @@ async def list_speaker_profiles(): """List all available speaker profiles""" try: profiles = await SpeakerProfile.get_all(order_by="name asc") - + return [ SpeakerProfileResponse( id=str(profile.id), @@ -31,16 +31,15 @@ async def list_speaker_profiles(): description=profile.description or "", tts_provider=profile.tts_provider, tts_model=profile.tts_model, - speakers=profile.speakers + speakers=profile.speakers, ) for profile in profiles ] - + except Exception as e: logger.error(f"Failed to fetch speaker profiles: {e}") raise HTTPException( - status_code=500, - detail=f"Failed to fetch speaker profiles: {str(e)}" + status_code=500, detail="Failed to fetch speaker profiles" ) @@ -49,29 +48,27 @@ async def get_speaker_profile(profile_name: str): """Get a specific speaker profile by name""" try: profile = await SpeakerProfile.get_by_name(profile_name) - + if not profile: raise HTTPException( - status_code=404, - detail=f"Speaker profile '{profile_name}' not found" + status_code=404, detail=f"Speaker profile '{profile_name}' not found" ) - + return SpeakerProfileResponse( id=str(profile.id), name=profile.name, description=profile.description or "", tts_provider=profile.tts_provider, tts_model=profile.tts_model, - speakers=profile.speakers + speakers=profile.speakers, ) - + except HTTPException: raise except Exception as e: logger.error(f"Failed to fetch speaker profile '{profile_name}': {e}") raise HTTPException( - status_code=500, - detail=f"Failed to fetch speaker profile: {str(e)}" + status_code=500, detail="Failed to fetch speaker profile" ) @@ -80,7 +77,9 @@ class SpeakerProfileCreate(BaseModel): description: str = Field("", description="Profile description") tts_provider: str = Field(..., description="TTS provider") tts_model: str = Field(..., description="TTS model name") - speakers: List[Dict[str, Any]] = Field(..., description="Array of speaker configurations") + speakers: List[Dict[str, Any]] = Field( + ..., description="Array of speaker configurations" + ) @router.post("/speaker-profiles", response_model=SpeakerProfileResponse) @@ -92,25 +91,24 @@ async def create_speaker_profile(profile_data: SpeakerProfileCreate): description=profile_data.description, tts_provider=profile_data.tts_provider, tts_model=profile_data.tts_model, - speakers=profile_data.speakers + speakers=profile_data.speakers, ) - + await profile.save() - + return SpeakerProfileResponse( id=str(profile.id), name=profile.name, description=profile.description or "", tts_provider=profile.tts_provider, tts_model=profile.tts_model, - speakers=profile.speakers + speakers=profile.speakers, ) - + except Exception as e: logger.error(f"Failed to create speaker profile: {e}") raise HTTPException( - status_code=500, - detail=f"Failed to create speaker profile: {str(e)}" + status_code=500, detail="Failed to create speaker profile" ) @@ -119,38 +117,36 @@ async def update_speaker_profile(profile_id: str, profile_data: SpeakerProfileCr """Update an existing speaker profile""" try: profile = await SpeakerProfile.get(profile_id) - + if not profile: raise HTTPException( - status_code=404, - detail=f"Speaker profile '{profile_id}' not found" + status_code=404, detail=f"Speaker profile '{profile_id}' not found" ) - + # Update fields profile.name = profile_data.name profile.description = profile_data.description profile.tts_provider = profile_data.tts_provider profile.tts_model = profile_data.tts_model profile.speakers = profile_data.speakers - + await profile.save() - + return SpeakerProfileResponse( id=str(profile.id), name=profile.name, description=profile.description or "", tts_provider=profile.tts_provider, tts_model=profile.tts_model, - speakers=profile.speakers + speakers=profile.speakers, ) - + except HTTPException: raise except Exception as e: logger.error(f"Failed to update speaker profile: {e}") raise HTTPException( - status_code=500, - detail=f"Failed to update speaker profile: {str(e)}" + status_code=500, detail="Failed to update speaker profile" ) @@ -159,64 +155,62 @@ async def delete_speaker_profile(profile_id: str): """Delete a speaker profile""" try: profile = await SpeakerProfile.get(profile_id) - + if not profile: raise HTTPException( - status_code=404, - detail=f"Speaker profile '{profile_id}' not found" + status_code=404, detail=f"Speaker profile '{profile_id}' not found" ) - + await profile.delete() - + return {"message": "Speaker profile deleted successfully"} - + except HTTPException: raise except Exception as e: logger.error(f"Failed to delete speaker profile: {e}") raise HTTPException( - status_code=500, - detail=f"Failed to delete speaker profile: {str(e)}" + status_code=500, detail="Failed to delete speaker profile" ) -@router.post("/speaker-profiles/{profile_id}/duplicate", response_model=SpeakerProfileResponse) +@router.post( + "/speaker-profiles/{profile_id}/duplicate", response_model=SpeakerProfileResponse +) async def duplicate_speaker_profile(profile_id: str): """Duplicate a speaker profile""" try: original = await SpeakerProfile.get(profile_id) - + if not original: raise HTTPException( - status_code=404, - detail=f"Speaker profile '{profile_id}' not found" + status_code=404, detail=f"Speaker profile '{profile_id}' not found" ) - + # Create duplicate with modified name duplicate = SpeakerProfile( name=f"{original.name} - Copy", description=original.description, tts_provider=original.tts_provider, tts_model=original.tts_model, - speakers=original.speakers + speakers=original.speakers, ) - + await duplicate.save() - + return SpeakerProfileResponse( id=str(duplicate.id), name=duplicate.name, description=duplicate.description or "", tts_provider=duplicate.tts_provider, tts_model=duplicate.tts_model, - speakers=duplicate.speakers + speakers=duplicate.speakers, ) - + except HTTPException: raise except Exception as e: logger.error(f"Failed to duplicate speaker profile: {e}") raise HTTPException( - status_code=500, - detail=f"Failed to duplicate speaker profile: {str(e)}" - ) \ No newline at end of file + status_code=500, detail="Failed to duplicate speaker profile" + ) diff --git a/api/routers/transformations.py b/api/routers/transformations.py index 6e3d455a..88871bba 100644 --- a/api/routers/transformations.py +++ b/api/routers/transformations.py @@ -123,7 +123,8 @@ async def get_default_prompt(): default_prompts: DefaultPrompts = await DefaultPrompts.get_instance() # type: ignore[assignment] return DefaultPromptResponse( - transformation_instructions=default_prompts.transformation_instructions or "" + transformation_instructions=default_prompts.transformation_instructions + or "" ) except Exception as e: logger.error(f"Error fetching default prompt: {str(e)}") @@ -138,7 +139,9 @@ async def update_default_prompt(prompt_update: DefaultPromptUpdate): try: default_prompts: DefaultPrompts = await DefaultPrompts.get_instance() # type: ignore[assignment] - default_prompts.transformation_instructions = prompt_update.transformation_instructions + default_prompts.transformation_instructions = ( + prompt_update.transformation_instructions + ) await default_prompts.update() return DefaultPromptResponse( diff --git a/api/search_service.py b/api/search_service.py index 07d7b6fa..57e0580c 100644 --- a/api/search_service.py +++ b/api/search_service.py @@ -22,7 +22,7 @@ def search( limit: int = 100, search_sources: bool = True, search_notes: bool = True, - minimum_score: float = 0.2 + minimum_score: float = 0.2, ) -> List[Dict[str, Any]]: """Search the knowledge base.""" response = api_client.search( @@ -31,7 +31,7 @@ def search( limit=limit, search_sources=search_sources, search_notes=search_notes, - minimum_score=minimum_score + minimum_score=minimum_score, ) if isinstance(response, dict): return response.get("results", []) @@ -42,17 +42,17 @@ def ask_knowledge_base( question: str, strategy_model: str, answer_model: str, - final_answer_model: str + final_answer_model: str, ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Ask the knowledge base a question.""" response = api_client.ask_simple( question=question, strategy_model=strategy_model, answer_model=answer_model, - final_answer_model=final_answer_model + final_answer_model=final_answer_model, ) return response # Global service instance -search_service = SearchService() \ No newline at end of file +search_service = SearchService() diff --git a/api/settings_service.py b/api/settings_service.py index ed84e02e..e8a43f56 100644 --- a/api/settings_service.py +++ b/api/settings_service.py @@ -2,7 +2,6 @@ Settings service layer using API. """ - from loguru import logger from api.client import api_client @@ -11,26 +10,36 @@ class SettingsService: """Service layer for settings operations using API.""" - + def __init__(self): logger.info("Using API for settings operations") - + def get_settings(self) -> ContentSettings: """Get application settings.""" settings_response = api_client.get_settings() - settings_data = settings_response if isinstance(settings_response, dict) else settings_response[0] + settings_data = ( + settings_response + if isinstance(settings_response, dict) + else settings_response[0] + ) # Create ContentSettings object from API response settings = ContentSettings( - default_content_processing_engine_doc=settings_data.get("default_content_processing_engine_doc"), - default_content_processing_engine_url=settings_data.get("default_content_processing_engine_url"), + default_content_processing_engine_doc=settings_data.get( + "default_content_processing_engine_doc" + ), + default_content_processing_engine_url=settings_data.get( + "default_content_processing_engine_url" + ), default_embedding_option=settings_data.get("default_embedding_option"), auto_delete_files=settings_data.get("auto_delete_files"), - youtube_preferred_languages=settings_data.get("youtube_preferred_languages"), + youtube_preferred_languages=settings_data.get( + "youtube_preferred_languages" + ), ) return settings - + def update_settings(self, settings: ContentSettings) -> ContentSettings: """Update application settings.""" updates = { @@ -42,17 +51,29 @@ def update_settings(self, settings: ContentSettings) -> ContentSettings: } settings_response = api_client.update_settings(**updates) - settings_data = settings_response if isinstance(settings_response, dict) else settings_response[0] + settings_data = ( + settings_response + if isinstance(settings_response, dict) + else settings_response[0] + ) # Update the settings object with the response - settings.default_content_processing_engine_doc = settings_data.get("default_content_processing_engine_doc") - settings.default_content_processing_engine_url = settings_data.get("default_content_processing_engine_url") - settings.default_embedding_option = settings_data.get("default_embedding_option") + settings.default_content_processing_engine_doc = settings_data.get( + "default_content_processing_engine_doc" + ) + settings.default_content_processing_engine_url = settings_data.get( + "default_content_processing_engine_url" + ) + settings.default_embedding_option = settings_data.get( + "default_embedding_option" + ) settings.auto_delete_files = settings_data.get("auto_delete_files") - settings.youtube_preferred_languages = settings_data.get("youtube_preferred_languages") + settings.youtube_preferred_languages = settings_data.get( + "youtube_preferred_languages" + ) return settings # Global service instance -settings_service = SettingsService() \ No newline at end of file +settings_service = SettingsService() diff --git a/api/sources_service.py b/api/sources_service.py index 6e3fa3b2..ae03375c 100644 --- a/api/sources_service.py +++ b/api/sources_service.py @@ -14,6 +14,7 @@ @dataclass class SourceProcessingResult: """Result of source creation with optional async processing info.""" + source: Source is_async: bool = False command_id: Optional[str] = None @@ -24,38 +25,39 @@ class SourceProcessingResult: @dataclass class SourceWithMetadata: """Source object with additional metadata from API.""" + source: Source embedded_chunks: int - + # Expose common source properties for easy access @property def id(self): return self.source.id - - @property + + @property def title(self): return self.source.title - + @title.setter def title(self, value): self.source.title = value - + @property def topics(self): return self.source.topics - + @property def asset(self): return self.source.asset - + @property def full_text(self): return self.source.full_text - + @property def created(self): return self.source.created - + @property def updated(self): return self.source.updated @@ -67,7 +69,9 @@ class SourcesService: def __init__(self): logger.info("Using API for sources operations") - def get_all_sources(self, notebook_id: Optional[str] = None) -> List[SourceWithMetadata]: + def get_all_sources( + self, notebook_id: Optional[str] = None + ) -> List[SourceWithMetadata]: """Get all sources with optional notebook filtering.""" sources_data = api_client.get_sources(notebook_id=notebook_id) # Convert API response to SourceWithMetadata objects @@ -88,11 +92,10 @@ def get_all_sources(self, notebook_id: Optional[str] = None) -> List[SourceWithM source.id = source_data["id"] source.created = source_data["created"] source.updated = source_data["updated"] - + # Wrap in SourceWithMetadata source_with_metadata = SourceWithMetadata( - source=source, - embedded_chunks=source_data.get("embedded_chunks", 0) + source=source, embedded_chunks=source_data.get("embedded_chunks", 0) ) sources.append(source_with_metadata) return sources @@ -119,8 +122,7 @@ def get_source(self, source_id: str) -> SourceWithMetadata: source.updated = source_data["updated"] return SourceWithMetadata( - source=source, - embedded_chunks=source_data.get("embedded_chunks", 0) + source=source, embedded_chunks=source_data.get("embedded_chunks", 0) ) def create_source( @@ -139,7 +141,7 @@ def create_source( ) -> Union[Source, SourceProcessingResult]: """ Create a new source with support for async processing. - + Args: notebook_id: Single notebook ID (deprecated, use notebooks parameter) source_type: Type of source (link, upload, text) @@ -152,7 +154,7 @@ def create_source( delete_source: Whether to delete uploaded file after processing notebooks: List of notebook IDs to add source to (preferred over notebook_id) async_processing: Whether to process source asynchronously - + Returns: Source object for sync processing (backward compatibility) SourceProcessingResult for async processing (contains additional metadata) @@ -193,9 +195,15 @@ def create_source( source.updated = response_data["updated"] # Check if this is an async processing response - if response_data.get("command_id") or response_data.get("status") or response_data.get("processing_info"): + if ( + response_data.get("command_id") + or response_data.get("status") + or response_data.get("processing_info") + ): # Ensure source_data is a dict for accessing attributes - source_data_dict = source_data if isinstance(source_data, dict) else source_data[0] + source_data_dict = ( + source_data if isinstance(source_data, dict) else source_data[0] + ) # Return enhanced result for async processing return SourceProcessingResult( source=source, @@ -228,7 +236,7 @@ def create_source_async( ) -> SourceProcessingResult: """ Create a new source with async processing enabled. - + This is a convenience method that always uses async processing. Returns a SourceProcessingResult with processing status information. """ @@ -245,7 +253,7 @@ def create_source_async( delete_source=delete_source, async_processing=True, ) - + # Since we forced async_processing=True, this should always be a SourceProcessingResult if isinstance(result, SourceProcessingResult): return result @@ -259,14 +267,18 @@ def create_source_async( def is_source_processing_complete(self, source_id: str) -> bool: """ Check if a source's async processing is complete. - + Returns True if processing is complete (success or failure), False if still processing or queued. """ try: status_data = self.get_source_status(source_id) status = status_data.get("status") - return status in ["completed", "failed", None] # None indicates legacy/sync source + return status in [ + "completed", + "failed", + None, + ] # None indicates legacy/sync source except Exception as e: logger.error(f"Error checking source processing status: {e}") return True # Assume complete on error @@ -275,7 +287,7 @@ def update_source(self, source: Source) -> Source: """Update a source.""" if not source.id: raise ValueError("Source ID is required for update") - + updates = { "title": source.title, "topics": source.topics, @@ -283,7 +295,9 @@ def update_source(self, source: Source) -> Source: source_data = api_client.update_source(source.id, **updates) # Ensure source_data is a dict - source_data_dict = source_data if isinstance(source_data, dict) else source_data[0] + source_data_dict = ( + source_data if isinstance(source_data, dict) else source_data[0] + ) # Update the source object with the response source.title = source_data_dict["title"] @@ -302,4 +316,9 @@ def delete_source(self, source_id: str) -> bool: sources_service = SourcesService() # Export important classes for easy importing -__all__ = ["SourcesService", "SourceWithMetadata", "SourceProcessingResult", "sources_service"] +__all__ = [ + "SourcesService", + "SourceWithMetadata", + "SourceProcessingResult", + "sources_service", +] diff --git a/api/transformations_service.py b/api/transformations_service.py index 876b9a92..bc81c08c 100644 --- a/api/transformations_service.py +++ b/api/transformations_service.py @@ -13,10 +13,10 @@ class TransformationsService: """Service layer for transformations operations using API.""" - + def __init__(self): logger.info("Using API for transformations operations") - + def get_all_transformations(self) -> List[Transformation]: """Get all transformations.""" transformations_data = api_client.get_transformations() @@ -31,11 +31,15 @@ def get_all_transformations(self) -> List[Transformation]: apply_default=trans_data["apply_default"], ) transformation.id = trans_data["id"] - transformation.created = datetime.fromisoformat(trans_data["created"].replace('Z', '+00:00')) - transformation.updated = datetime.fromisoformat(trans_data["updated"].replace('Z', '+00:00')) + transformation.created = datetime.fromisoformat( + trans_data["created"].replace("Z", "+00:00") + ) + transformation.updated = datetime.fromisoformat( + trans_data["updated"].replace("Z", "+00:00") + ) transformations.append(transformation) return transformations - + def get_transformation(self, transformation_id: str) -> Transformation: """Get a specific transformation.""" response = api_client.get_transformation(transformation_id) @@ -48,17 +52,21 @@ def get_transformation(self, transformation_id: str) -> Transformation: apply_default=trans_data["apply_default"], ) transformation.id = trans_data["id"] - transformation.created = datetime.fromisoformat(trans_data["created"].replace('Z', '+00:00')) - transformation.updated = datetime.fromisoformat(trans_data["updated"].replace('Z', '+00:00')) + transformation.created = datetime.fromisoformat( + trans_data["created"].replace("Z", "+00:00") + ) + transformation.updated = datetime.fromisoformat( + trans_data["updated"].replace("Z", "+00:00") + ) return transformation - + def create_transformation( self, name: str, title: str, description: str, prompt: str, - apply_default: bool = False + apply_default: bool = False, ) -> Transformation: """Create a new transformation.""" response = api_client.create_transformation( @@ -66,7 +74,7 @@ def create_transformation( title=title, description=description, prompt=prompt, - apply_default=apply_default + apply_default=apply_default, ) trans_data = response if isinstance(response, dict) else response[0] transformation = Transformation( @@ -77,10 +85,14 @@ def create_transformation( apply_default=trans_data["apply_default"], ) transformation.id = trans_data["id"] - transformation.created = datetime.fromisoformat(trans_data["created"].replace('Z', '+00:00')) - transformation.updated = datetime.fromisoformat(trans_data["updated"].replace('Z', '+00:00')) + transformation.created = datetime.fromisoformat( + trans_data["created"].replace("Z", "+00:00") + ) + transformation.updated = datetime.fromisoformat( + trans_data["updated"].replace("Z", "+00:00") + ) return transformation - + def update_transformation(self, transformation: Transformation) -> Transformation: """Update a transformation.""" if not transformation.id: @@ -102,29 +114,28 @@ def update_transformation(self, transformation: Transformation) -> Transformatio transformation.description = trans_data["description"] transformation.prompt = trans_data["prompt"] transformation.apply_default = trans_data["apply_default"] - transformation.updated = datetime.fromisoformat(trans_data["updated"].replace('Z', '+00:00')) + transformation.updated = datetime.fromisoformat( + trans_data["updated"].replace("Z", "+00:00") + ) return transformation - + def delete_transformation(self, transformation_id: str) -> bool: """Delete a transformation.""" api_client.delete_transformation(transformation_id) return True - + def execute_transformation( - self, - transformation_id: str, - input_text: str, - model_id: str + self, transformation_id: str, input_text: str, model_id: str ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Execute a transformation on input text.""" result = api_client.execute_transformation( transformation_id=transformation_id, input_text=input_text, - model_id=model_id + model_id=model_id, ) return result # Global service instance -transformations_service = TransformationsService() \ No newline at end of file +transformations_service = TransformationsService() diff --git a/commands/embedding_commands.py b/commands/embedding_commands.py index 6bf87e5f..a44dc799 100644 --- a/commands/embedding_commands.py +++ b/commands/embedding_commands.py @@ -174,7 +174,9 @@ async def embed_single_item_command( except Exception as e: processing_time = time.time() - start_time - logger.error(f"Embedding failed for {input_data.item_type} {input_data.item_id}: {e}") + logger.error( + f"Embedding failed for {input_data.item_type} {input_data.item_id}: {e}" + ) logger.exception(e) return EmbedSingleItemOutput( @@ -317,7 +319,9 @@ async def vectorize_source_command( start_time = time.time() try: - logger.info(f"Starting vectorization orchestration for source {input_data.source_id}") + logger.info( + f"Starting vectorization orchestration for source {input_data.source_id}" + ) # 1. Load source source = await Source.get(input_data.source_id) @@ -331,7 +335,7 @@ async def vectorize_source_command( logger.info(f"Deleting existing embeddings for source {input_data.source_id}") delete_result = await repo_query( "DELETE source_embedding WHERE source = $source_id", - {"source_id": ensure_record_id(input_data.source_id)} + {"source_id": ensure_record_id(input_data.source_id)}, ) deleted_count = len(delete_result) if delete_result else 0 if deleted_count > 0: @@ -354,12 +358,12 @@ async def vectorize_source_command( try: job_id = submit_command( "open_notebook", # app name - "embed_chunk", # command name + "embed_chunk", # command name { "source_id": input_data.source_id, "chunk_index": idx, "chunk_text": chunk_text, - } + }, ) jobs_submitted += 1 @@ -387,7 +391,9 @@ async def vectorize_source_command( except Exception as e: processing_time = time.time() - start_time - logger.error(f"Vectorization orchestration failed for source {input_data.source_id}: {e}") + logger.error( + f"Vectorization orchestration failed for source {input_data.source_id}: {e}" + ) logger.exception(e) return VectorizeSourceOutput( @@ -484,7 +490,9 @@ async def rebuild_embeddings_command( try: logger.info("=" * 60) logger.info(f"Starting embedding rebuild with mode={input_data.mode}") - logger.info(f"Include: sources={input_data.include_sources}, notes={input_data.include_notes}, insights={input_data.include_insights}") + logger.info( + f"Include: sources={input_data.include_sources}, notes={input_data.include_notes}, insights={input_data.include_insights}" + ) logger.info("=" * 60) # Check embedding model availability @@ -561,7 +569,9 @@ async def rebuild_embeddings_command( notes_processed += 1 if idx % 10 == 0 or idx == len(items["notes"]): - logger.info(f" Progress: {idx}/{len(items['notes'])} notes processed") + logger.info( + f" Progress: {idx}/{len(items['notes'])} notes processed" + ) except Exception as e: logger.error(f"Failed to re-embed note {note_id}: {e}") diff --git a/commands/example_commands.py b/commands/example_commands.py index c1439e60..7f26cb98 100644 --- a/commands/example_commands.py +++ b/commands/example_commands.py @@ -12,6 +12,7 @@ class TextProcessingInput(BaseModel): operation: str = "uppercase" # uppercase, lowercase, word_count, reverse delay_seconds: Optional[int] = None # For testing async behavior + class TextProcessingOutput(BaseModel): success: bool original_text: str @@ -20,11 +21,13 @@ class TextProcessingOutput(BaseModel): processing_time: float error_message: Optional[str] = None + class DataAnalysisInput(BaseModel): numbers: List[float] analysis_type: str = "basic" # basic, detailed delay_seconds: Optional[int] = None + class DataAnalysisOutput(BaseModel): success: bool analysis_type: str @@ -36,6 +39,7 @@ class DataAnalysisOutput(BaseModel): processing_time: float error_message: Optional[str] = None + @command("process_text", app="open_notebook") async def process_text_command(input_data: TextProcessingInput) -> TextProcessingOutput: """ @@ -43,17 +47,17 @@ async def process_text_command(input_data: TextProcessingInput) -> TextProcessin and demonstrates different processing types. """ start_time = time.time() - + try: logger.info(f"Processing text with operation: {input_data.operation}") - + # Simulate processing delay if specified if input_data.delay_seconds: await asyncio.sleep(input_data.delay_seconds) - + processed_text = None word_count = None - + if input_data.operation == "uppercase": processed_text = input_data.text.upper() elif input_data.operation == "lowercase": @@ -65,17 +69,17 @@ async def process_text_command(input_data: TextProcessingInput) -> TextProcessin processed_text = f"Word count: {word_count}" else: raise ValueError(f"Unknown operation: {input_data.operation}") - + processing_time = time.time() - start_time - + return TextProcessingOutput( success=True, original_text=input_data.text, processed_text=processed_text, word_count=word_count, - processing_time=processing_time + processing_time=processing_time, ) - + except Exception as e: processing_time = time.time() - start_time logger.error(f"Text processing failed: {e}") @@ -83,9 +87,10 @@ async def process_text_command(input_data: TextProcessingInput) -> TextProcessin success=False, original_text=input_data.text, processing_time=processing_time, - error_message=str(e) + error_message=str(e), ) + @command("analyze_data", app="open_notebook") async def analyze_data_command(input_data: DataAnalysisInput) -> DataAnalysisOutput: """ @@ -93,25 +98,27 @@ async def analyze_data_command(input_data: DataAnalysisInput) -> DataAnalysisOut and demonstrates error handling. """ start_time = time.time() - + try: - logger.info(f"Analyzing {len(input_data.numbers)} numbers with {input_data.analysis_type} analysis") - + logger.info( + f"Analyzing {len(input_data.numbers)} numbers with {input_data.analysis_type} analysis" + ) + # Simulate processing delay if specified if input_data.delay_seconds: await asyncio.sleep(input_data.delay_seconds) - + if not input_data.numbers: raise ValueError("No numbers provided for analysis") - + count = len(input_data.numbers) sum_value = sum(input_data.numbers) average = sum_value / count min_value = min(input_data.numbers) max_value = max(input_data.numbers) - + processing_time = time.time() - start_time - + return DataAnalysisOutput( success=True, analysis_type=input_data.analysis_type, @@ -120,9 +127,9 @@ async def analyze_data_command(input_data: DataAnalysisInput) -> DataAnalysisOut average=average, min_value=min_value, max_value=max_value, - processing_time=processing_time + processing_time=processing_time, ) - + except Exception as e: processing_time = time.time() - start_time logger.error(f"Data analysis failed: {e}") @@ -131,5 +138,5 @@ async def analyze_data_command(input_data: DataAnalysisInput) -> DataAnalysisOut analysis_type=input_data.analysis_type, count=0, processing_time=processing_time, - error_message=str(e) - ) \ No newline at end of file + error_message=str(e), + ) diff --git a/docker-compose.full.yml b/docker-compose.full.yml index 9675f05f..96d4e09c 100644 --- a/docker-compose.full.yml +++ b/docker-compose.full.yml @@ -13,6 +13,9 @@ services: restart: always open_notebook: image: lfnovo/open_notebook:v1-latest + # build: + # context: . + # dockerfile: Dockerfile ports: - "8502:8502" - "5055:5055" diff --git a/docker-compose.single.yml b/docker-compose.single.yml index ad641bd7..06ac0d50 100644 --- a/docker-compose.single.yml +++ b/docker-compose.single.yml @@ -9,6 +9,9 @@ services: - "5055:5055" # REST API env_file: - ./docker.env + environment: + # Override for single-container mode: SurrealDB runs on localhost inside the same container + - SURREAL_URL=ws://localhost:8000/rpc volumes: - ./notebook_data:/app/data # Application data - ./surreal_single_data:/mydata # SurrealDB data diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6011f4d9..2be6e94c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,12 +34,15 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "i18next": "^25.7.3", + "i18next-browser-languagedetector": "^8.2.0", "lucide-react": "^0.525.0", "next": "^16.1.1", "next-themes": "^0.4.6", "react": "^19.2.3", "react-dom": "^19.2.3", "react-hook-form": "^7.60.0", + "react-i18next": "^16.5.0", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", "sonner": "^2.0.6", @@ -51,16 +54,29 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitejs/plugin-react": "^4.3.4", + "@vitest/ui": "^3.0.0", "eslint": "^9", "eslint-config-next": "15.4.2", + "jsdom": "^26.0.0", "tailwindcss": "^4", "tw-animate-css": "^1.3.5", - "typescript": "^5" + "typescript": "^5", + "vitest": "^3.0.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmmirror.com/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -86,752 +102,1645 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", - "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "dev": true, - "optional": true, + "license": "MIT", "dependencies": { - "@emnapi/wasi-threads": "1.0.4", - "tslib": "^2.4.0" + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" } }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", - "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", "dev": true, - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@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": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" }, "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "license": "MIT", + "bin": { + "json5": "lib/cli.js" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">=6" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" + "@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": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, + "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.15" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, + "license": "MIT", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@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": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": ">=6.9.0" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", "dependencies": { - "@eslint/core": "^0.15.1", - "levn": "^0.4.1" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.9.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", - "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@floating-ui/dom": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", - "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@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": { - "@floating-ui/core": "^1.7.2", - "@floating-ui/utils": "^0.2.10" + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", - "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.2" + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@hookform/resolvers": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.1.1.tgz", - "integrity": "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==", + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@standard-schema/utils": "^0.3.0" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, - "peerDependencies": { - "react-hook-form": "^7.55.0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, "engines": { - "node": ">=18.18.0" + "node": ">=6.9.0" } }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "dev": true, + "license": "MIT", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { - "node": ">=18.18.0" + "node": ">=6.9.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "engines": { - "node": ">=18.18" + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, "engines": { - "node": ">=12.22" + "node": ">=18" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", "engines": { - "node": ">=18.18" + "node": ">=18" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "optional": true, "engines": { "node": ">=18" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "node_modules/@emnapi/core": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ - "arm64" + "ppc64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "aix" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "node": ">=18" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ - "x64" + "arm" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "android" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "android" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "android" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ - "arm" + "arm64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ - "arm64" + "x64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ - "ppc64" + "arm64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ - "riscv64" + "x64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ - "s390x" + "arm" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ - "x64" + "arm64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ - "arm64" + "ia32" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ - "x64" + "loong64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ - "arm" + "mips64el" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ - "arm64" + "ppc64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ - "ppc64" + "riscv64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ - "riscv64" + "s390x" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ - "s390x" + "x64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "netbsd" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "node": ">=18" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "cpu": [ "arm64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "openbsd" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "node": ">=18" } }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "openbsd" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "node": ">=18" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", "cpu": [ - "wasm32" + "arm64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "dev": true, + "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, + "os": [ + "openharmony" + ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", + "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", + "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", + "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "dependencies": { + "@floating-ui/core": "^1.7.2", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", + "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", + "dependencies": { + "@floating-ui/dom": "^1.7.2" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" + }, + "node_modules/@hookform/resolvers": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.1.1.tgz", + "integrity": "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "dev": true, "dependencies": { "minipass": "^7.0.4" @@ -850,6 +1759,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@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", @@ -1095,6 +2015,13 @@ "node": ">=12.4.0" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -2248,12 +3175,57 @@ } } }, - "node_modules/@radix-ui/react-use-escape-keydown": { + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" + "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2265,10 +3237,13 @@ } } }, - "node_modules/@radix-ui/react-use-layout-effect": { + "node_modules/@radix-ui/react-use-size": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -2279,80 +3254,389 @@ } } }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmmirror.com/@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.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@rtsao/scc": { "version": "1.1.0", @@ -2738,6 +4022,93 @@ "react": "^18 || ^19" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmmirror.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.1", + "resolved": "https://registry.npmmirror.com/@testing-library/react/-/react-16.3.1.tgz", + "integrity": "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", @@ -2748,6 +4119,70 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -2756,6 +4191,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3431,31 +4873,189 @@ "win32" ] }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/@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/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/@vitest/ui/-/ui-3.2.4.tgz", + "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", "dev": true, - "optional": true, - "os": [ - "win32" - ] + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.1", + "tinyglobby": "^0.2.14", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "3.2.4" + } }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, - "optional": true, - "os": [ - "win32" - ] + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, "node_modules/acorn": { "version": "8.15.0", @@ -3478,6 +5078,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3494,6 +5104,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -3687,6 +5308,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -3811,6 +5442,50 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -3867,9 +5542,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001764", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", "funding": [ { "type": "opencollective", @@ -3883,7 +5558,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/ccount": { "version": "2.0.1", @@ -3894,6 +5570,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmmirror.com/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3946,6 +5639,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -4038,6 +5741,13 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4067,6 +5777,13 @@ } ] }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4078,6 +5795,20 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -4089,6 +5820,20 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -4165,6 +5910,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -4177,6 +5929,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4284,6 +6046,14 @@ "node": ">=0.10.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4297,6 +6067,13 @@ "node": ">= 0.4" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -4438,6 +6215,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -4492,6 +6276,58 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4908,6 +6744,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -4917,6 +6763,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -4977,6 +6833,13 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5085,6 +6948,21 @@ "node": ">= 6" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/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", @@ -5122,6 +7000,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/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/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5595,6 +7483,28 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -5604,13 +7514,94 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/i18next": { + "version": "25.7.4", + "resolved": "https://registry.npmmirror.com/i18next/-/i18next-25.7.4.tgz", + "integrity": "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.0", + "resolved": "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/ignore": { @@ -5647,6 +7638,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inline-style-parser": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", @@ -5956,6 +7957,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -6149,6 +8157,59 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -6513,6 +8574,30 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/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/lru-cache/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/lucide-react": { "version": "0.525.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", @@ -6521,6 +8606,17 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -7390,6 +9486,16 @@ "node": ">= 0.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -7453,6 +9559,16 @@ "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", "peer": true }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7585,6 +9701,13 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -7596,6 +9719,13 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmmirror.com/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7850,6 +9980,23 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7925,6 +10072,44 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -8015,6 +10200,33 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-i18next": { + "version": "16.5.3", + "resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-16.5.3.tgz", + "integrity": "sha512-fo+/NNch37zqxOzlBYrWMx0uy/yInPkRfjSuy4lqKdaecR17nvCHnEUt3QyzA8XjQ2B/0iW/5BhaHR3ZmukpGw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -8047,6 +10259,16 @@ "react": ">=18" } }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-remove-scroll": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", @@ -8113,6 +10335,20 @@ } } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -8495,6 +10731,58 @@ "node": ">=0.10.0" } }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "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.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8570,6 +10858,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -8773,6 +11081,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/sonner": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.6.tgz", @@ -8805,11 +11135,25 @@ "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", "dev": true }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/state-local": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -8952,6 +11296,19 @@ "node": ">=4" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8964,6 +11321,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.17", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz", @@ -9026,6 +11403,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", @@ -9066,14 +11450,29 @@ "node": ">=18" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, + "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -9083,10 +11482,14 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/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" }, @@ -9098,9 +11501,10 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -9108,6 +11512,56 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmmirror.com/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -9120,6 +11574,42 @@ "node": ">=8.0" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -9266,7 +11756,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9424,6 +11914,37 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -9485,6 +12006,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9529,6 +12059,243 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "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": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/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.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -9538,6 +12305,53 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9638,6 +12452,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -9647,6 +12478,45 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5f9cbccb..3e0e6e99 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,10 @@ "dev": "next dev", "build": "next build", "start": "node start-server.js", - "lint": "next lint" + "lint": "next lint", + "test": "vitest run", + "test:watch": "vitest", + "test:ui": "vitest --ui" }, "dependencies": { "@hookform/resolvers": "^5.1.1", @@ -35,12 +38,15 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "i18next": "^25.7.3", + "i18next-browser-languagedetector": "^8.2.0", "lucide-react": "^0.525.0", "next": "^16.1.1", "next-themes": "^0.4.6", "react": "^19.2.3", "react-dom": "^19.2.3", "react-hook-form": "^7.60.0", + "react-i18next": "^16.5.0", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", "sonner": "^2.0.6", @@ -57,8 +63,14 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.4.2", + "jsdom": "^26.0.0", "tailwindcss": "^4", "tw-animate-css": "^1.3.5", - "typescript": "^5" + "typescript": "^5", + "vitest": "^3.0.0", + "@vitest/ui": "^3.0.0", + "@vitejs/plugin-react": "^4.3.4", + "@testing-library/react": "^16.2.0", + "@testing-library/jest-dom": "^6.6.3" } } diff --git a/frontend/src/app/(dashboard)/advanced/components/RebuildEmbeddings.tsx b/frontend/src/app/(dashboard)/advanced/components/RebuildEmbeddings.tsx index 32227546..729d870a 100644 --- a/frontend/src/app/(dashboard)/advanced/components/RebuildEmbeddings.tsx +++ b/frontend/src/app/(dashboard)/advanced/components/RebuildEmbeddings.tsx @@ -18,8 +18,10 @@ import { } from '@/components/ui/accordion' import { embeddingApi } from '@/lib/api/embedding' import type { RebuildEmbeddingsRequest, RebuildStatusResponse } from '@/lib/api/embedding' +import { useTranslation } from '@/lib/hooks/use-translation' export function RebuildEmbeddings() { + const { t } = useTranslation() const [mode, setMode] = useState<'existing' | 'all'>('existing') const [includeSources, setIncludeSources] = useState(true) const [includeNotes, setIncludeNotes] = useState(true) @@ -121,10 +123,10 @@ export function RebuildEmbeddings() { - 🔄 Rebuild Embeddings + {t.advanced.rebuildEmbeddings} - Rebuild vector embeddings for your content. Use this when switching embedding models or fixing corrupted embeddings. + {t.advanced.rebuildEmbeddingsDesc} @@ -132,25 +134,25 @@ export function RebuildEmbeddings() { {!isRebuildActive && (
- +

{mode === 'existing' - ? 'Re-embed only items that already have embeddings (faster, for model switching)' - : 'Re-embed existing items + create embeddings for items without any (slower, comprehensive)'} + ? t.advanced.rebuild.existingDesc + : t.advanced.rebuild.allDesc}

-
- +
+ {t.advanced.rebuild.include}
setIncludeSources(checked === true)} />
@@ -169,7 +171,7 @@ export function RebuildEmbeddings() { onCheckedChange={(checked) => setIncludeNotes(checked === true)} />
@@ -179,7 +181,7 @@ export function RebuildEmbeddings() { onCheckedChange={(checked) => setIncludeInsights(checked === true)} />
@@ -187,7 +189,7 @@ export function RebuildEmbeddings() { - Please select at least one item type to rebuild + {t.advanced.rebuild.selectOneError} )} @@ -201,10 +203,10 @@ export function RebuildEmbeddings() { {rebuildMutation.isPending ? ( <> - Starting Rebuild... + {t.advanced.rebuild.starting} ) : ( - '🚀 Start Rebuild' + t.advanced.rebuild.startBtn )} @@ -212,7 +214,7 @@ export function RebuildEmbeddings() { - Failed to start rebuild: {(rebuildMutation.error as Error)?.message || 'Unknown error'} + {t.advanced.rebuild.failed}: {(rebuildMutation.error as Error)?.message || t.common.error} )} @@ -230,21 +232,21 @@ export function RebuildEmbeddings() { {status.status === 'failed' && }
- {status.status === 'queued' && 'Queued'} - {status.status === 'running' && 'Running...'} - {status.status === 'completed' && 'Completed!'} - {status.status === 'failed' && 'Failed'} + {status.status === 'queued' && t.advanced.rebuild.queued} + {status.status === 'running' && t.advanced.rebuild.running} + {status.status === 'completed' && t.advanced.rebuild.completed} + {status.status === 'failed' && t.advanced.rebuild.failed} {status.status === 'running' && ( - You can leave this page as this will run in the background + {t.advanced.rebuild.leavePageHint} )}
{(status.status === 'completed' || status.status === 'failed') && ( )}
@@ -252,36 +254,39 @@ export function RebuildEmbeddings() { {progressData && (
- Progress + {t.common.progress} - {processedItems}/{totalItems} items ({progressPercent.toFixed(1)}%) + {t.advanced.rebuild.itemsProcessed + .replace('{processed}', processedItems.toString()) + .replace('{total}', totalItems.toString()) + .replace('{percent}', progressPercent.toFixed(1))}
{failedItems > 0 && (

- ⚠️ {failedItems} items failed to process + ⚠️ {t.advanced.rebuild.failedItems.replace('{count}', failedItems.toString())}

)}
)} - {stats && ( + {stats && (
-

Sources

+

{t.navigation.sources}

{sourcesProcessed}

-

Notes

+

{t.common.notes}

{notesProcessed}

-

Insights

+

{t.common.insights}

{insightsProcessed}

-

Time

+

{t.advanced.rebuild.time}

{processingTimeSeconds !== undefined ? `${processingTimeSeconds.toFixed(1)}s` : '—'}

@@ -298,9 +303,9 @@ export function RebuildEmbeddings() { {status.started_at && (
-

Started: {new Date(status.started_at).toLocaleString()}

+

{t.common.created.replace('{time}', new Date(status.started_at).toLocaleString())}

{status.completed_at && ( -

Completed: {new Date(status.completed_at).toLocaleString()}

+

{t.notebooks.updated}: {new Date(status.completed_at).toLocaleString()}

)}
)} @@ -308,51 +313,25 @@ export function RebuildEmbeddings() { )} {/* Help Section */} - + - When should I rebuild embeddings? + {t.advanced.rebuild.whenToRebuild} -

You should rebuild embeddings when:

-
    -
  • Switching embedding models: If you change from one embedding model to another, you need to rebuild all embeddings to ensure consistency.
  • -
  • Upgrading model versions: When updating to a newer version of your embedding model, rebuild to take advantage of improvements.
  • -
  • Fixing corrupted embeddings: If you suspect some embeddings are corrupted or missing, rebuilding can restore them.
  • -
  • After bulk imports: If you imported content without embeddings, use "All" mode to embed everything.
  • -
+

{t.advanced.rebuild.whenToRebuildAns}

- How long does rebuilding take? + {t.advanced.rebuild.howLong} -

Processing time depends on:

-
    -
  • Number of items to process
  • -
  • Embedding model speed
  • -
  • API rate limits (for cloud providers)
  • -
  • System resources
  • -
-

Typical rates:

-
    -
  • Local models (Ollama): Very fast, limited only by hardware
  • -
  • Cloud APIs (OpenAI, Google): Moderate speed, may hit rate limits with large datasets
  • -
  • Sources: Slower than notes/insights (creates multiple chunks per source)
  • -
-

Example: Rebuilding 200 items might take 2-5 minutes with cloud APIs, or under 1 minute with local models.

+

{t.advanced.rebuild.howLongAns}

- Is it safe to rebuild while using the app? + {t.advanced.rebuild.isSafe} -

Yes, rebuilding is safe! The rebuild process:

-
    -
  • Is idempotent: Running multiple times produces the same result
  • -
  • Doesn't delete content: Only replaces embeddings
  • -
  • Can be run anytime: No need to stop other operations
  • -
  • Handles errors gracefully: Failed items are logged and skipped
  • -
-

⚠️ However: Very large rebuilds (1000s of items) may temporarily slow down searches while processing.

+

{t.advanced.rebuild.isSafeAns}

diff --git a/frontend/src/app/(dashboard)/advanced/components/SystemInfo.tsx b/frontend/src/app/(dashboard)/advanced/components/SystemInfo.tsx index 7916eaec..2b411d4e 100644 --- a/frontend/src/app/(dashboard)/advanced/components/SystemInfo.tsx +++ b/frontend/src/app/(dashboard)/advanced/components/SystemInfo.tsx @@ -4,8 +4,10 @@ import { useEffect, useState } from 'react' import { Card } from '@/components/ui/card' import { getConfig } from '@/lib/config' import { Badge } from '@/components/ui/badge' +import { useTranslation } from '@/lib/hooks/use-translation' export function SystemInfo() { + const { t } = useTranslation() const [config, setConfig] = useState<{ version: string latestVersion?: string | null @@ -32,8 +34,8 @@ export function SystemInfo() { return (
-

System Information

-
Loading...
+

{t.advanced.systemInfo}

+
{t.common.loading}
) @@ -42,37 +44,37 @@ export function SystemInfo() { return (
-

System Information

+

{t.advanced.systemInfo}

{/* Current Version */}
- Current Version - {config?.version || 'Unknown'} + {t.advanced.currentVersion} + {config?.version || t.advanced.unknown}
{/* Latest Version */} {config?.latestVersion && (
- Latest Version + {t.advanced.latestVersion} {config.latestVersion}
)} {/* Update Status */}
- Status + {t.advanced.status} {config?.hasUpdate ? ( - Update Available + {t.advanced.updateAvailable.replace('{version}', config.latestVersion || '')} ) : config?.latestVersion ? ( - Up to Date + {t.advanced.upToDate} ) : ( - Unknown + {t.advanced.unknown} )}
@@ -86,7 +88,7 @@ export function SystemInfo() { rel="noopener noreferrer" className="text-sm text-primary hover:underline inline-flex items-center gap-1" > - View on GitHub + {t.advanced.viewOnGithub} - Unable to check for updates. GitHub may be unreachable. + {t.advanced.updateCheckFailed}
)}
diff --git a/frontend/src/app/(dashboard)/advanced/page.tsx b/frontend/src/app/(dashboard)/advanced/page.tsx index 71f56ea6..d20331d5 100644 --- a/frontend/src/app/(dashboard)/advanced/page.tsx +++ b/frontend/src/app/(dashboard)/advanced/page.tsx @@ -3,17 +3,19 @@ import { AppShell } from '@/components/layout/AppShell' import { RebuildEmbeddings } from './components/RebuildEmbeddings' import { SystemInfo } from './components/SystemInfo' +import { useTranslation } from '@/lib/hooks/use-translation' export default function AdvancedPage() { + const { t } = useTranslation() return (
-

Advanced

+

{t.advanced.title}

- Advanced tools and utilities for power users + {t.advanced.desc}

diff --git a/frontend/src/app/(dashboard)/models/components/AddModelForm.tsx b/frontend/src/app/(dashboard)/models/components/AddModelForm.tsx index a287a21b..45066db6 100644 --- a/frontend/src/app/(dashboard)/models/components/AddModelForm.tsx +++ b/frontend/src/app/(dashboard)/models/components/AddModelForm.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useId, useState } from 'react' import { useForm } from 'react-hook-form' import { CreateModelRequest, ProviderAvailability } from '@/lib/types/models' import { Button } from '@/components/ui/button' @@ -10,6 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import { useCreateModel } from '@/lib/hooks/use-models' import { Plus } from 'lucide-react' +import { useTranslation } from '@/lib/hooks/use-translation' interface AddModelFormProps { modelType: 'language' | 'embedding' | 'text_to_speech' | 'speech_to_text' @@ -17,6 +18,9 @@ interface AddModelFormProps { } export function AddModelForm({ modelType, providers }: AddModelFormProps) { + const { t } = useTranslation() + const providerSelectId = useId() + const modelNameInputId = useId() const [open, setOpen] = useState(false) const createModel = useCreateModel() const { register, handleSubmit, formState: { errors }, reset, setValue, watch } = useForm({ @@ -37,7 +41,7 @@ export function AddModelForm({ modelType, providers }: AddModelFormProps) { } const getModelTypeName = () => { - return modelType.replace(/_/g, ' ') + return (t.models as Record)[modelType] || modelType.replace(/_/g, ' ') } const getModelPlaceholder = () => { @@ -51,14 +55,14 @@ export function AddModelForm({ modelType, providers }: AddModelFormProps) { case 'speech_to_text': return 'e.g., whisper-1' default: - return 'Enter model name' + return t.models.enterModelName } } if (availableProviders.length === 0) { return (
- No providers available for {getModelTypeName()} models + {t.models.noProvidersForType.replace('{type}', getModelTypeName())}
) } @@ -73,24 +77,34 @@ export function AddModelForm({ modelType, providers }: AddModelFormProps) { return ( - - Add {getModelTypeName()} Model + + {t.models.addSpecificModel.replace('{type}', getModelTypeName())} + - Configure a new {getModelTypeName()} model from available providers. + {t.models.addSpecificModelDesc.replace('{type}', getModelTypeName())}
- - setValue('provider', value)} + required + > + + {availableProviders.map((provider) => ( @@ -101,32 +115,33 @@ export function AddModelForm({ modelType, providers }: AddModelFormProps) { {errors.provider && ( -

Provider is required

+

{t.models.providerRequired}

)}
- + {errors.name && (

{errors.name.message}

)}

{modelType === 'language' && watch('provider') === 'azure' && - 'For Azure, use the deployment name as the model name'} + t.models.azureHint}

diff --git a/frontend/src/app/(dashboard)/models/components/DefaultModelsSection.tsx b/frontend/src/app/(dashboard)/models/components/DefaultModelsSection.tsx index d3f42cf9..492f0f0d 100644 --- a/frontend/src/app/(dashboard)/models/components/DefaultModelsSection.tsx +++ b/frontend/src/app/(dashboard)/models/components/DefaultModelsSection.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useState } from 'react' +import { useEffect, useState, useId } from 'react' import { useForm } from 'react-hook-form' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' @@ -11,74 +11,86 @@ import { ModelDefaults, Model } from '@/lib/types/models' import { useUpdateModelDefaults } from '@/lib/hooks/use-models' import { AlertCircle, X } from 'lucide-react' import { EmbeddingModelChangeDialog } from './EmbeddingModelChangeDialog' +import { useTranslation } from '@/lib/hooks/use-translation' interface DefaultModelsSectionProps { models: Model[] defaults: ModelDefaults } -interface DefaultConfig { - key: keyof ModelDefaults - label: string - description: string - modelType: 'language' | 'embedding' | 'text_to_speech' | 'speech_to_text' - required?: boolean -} - -const defaultConfigs: DefaultConfig[] = [ - { - key: 'default_chat_model', - label: 'Chat Model', - description: 'Used for chat conversations', - modelType: 'language', - required: true - }, - { - key: 'default_transformation_model', - label: 'Transformation Model', - description: 'Used for summaries, insights, and transformations', - modelType: 'language', - required: true - }, - { - key: 'default_tools_model', - label: 'Tools Model', - description: 'Used for function calling - OpenAI or Anthropic recommended', - modelType: 'language' - }, - { - key: 'large_context_model', - label: 'Large Context Model', - description: 'Used for processing large documents - Gemini recommended', - modelType: 'language' - }, - { - key: 'default_embedding_model', - label: 'Embedding Model', - description: 'Used for semantic search and vector embeddings', - modelType: 'embedding', - required: true - }, - { - key: 'default_text_to_speech_model', - label: 'Text-to-Speech Model', - description: 'Used for podcast generation', - modelType: 'text_to_speech' - }, - { - key: 'default_speech_to_text_model', - label: 'Speech-to-Text Model', - description: 'Used for audio transcription', - modelType: 'speech_to_text' - } -] - export function DefaultModelsSection({ models, defaults }: DefaultModelsSectionProps) { + const { t } = useTranslation() const updateDefaults = useUpdateModelDefaults() const { setValue, watch } = useForm({ defaultValues: defaults }) + interface DefaultConfig { + key: keyof ModelDefaults + label: string + description: string + modelType: 'language' | 'embedding' | 'text_to_speech' | 'speech_to_text' + required?: boolean + id: string + } + + const generatedId = useId() + + const defaultConfigs: DefaultConfig[] = [ + { + key: 'default_chat_model', + label: t.models.chatModelLabel, + description: t.models.chatModelDesc, + modelType: 'language', + required: true, + id: `${generatedId}-chat`, + }, + { + key: 'default_transformation_model', + label: t.models.transformationModelLabel, + description: t.models.transformationModelDesc, + modelType: 'language', + required: true, + id: `${generatedId}-transformation`, + }, + { + key: 'default_tools_model', + label: t.models.toolsModelLabel, + description: t.models.toolsModelDesc, + modelType: 'language', + id: `${generatedId}-tools`, + }, + { + key: 'large_context_model', + label: t.models.largeContextModelLabel, + description: t.models.largeContextModelDesc, + modelType: 'language', + id: `${generatedId}-large-context`, + }, + { + key: 'default_embedding_model', + label: t.models.embeddingModelLabel, + description: t.models.embeddingModelDesc, + modelType: 'embedding', + required: true, + id: `${generatedId}-embedding`, + }, + { + key: 'default_text_to_speech_model', + label: t.models.ttsModelLabel, + description: t.models.ttsModelDesc, + modelType: 'text_to_speech', + id: `${generatedId}-tts`, + }, + { + key: 'default_speech_to_text_model', + label: t.models.sttModelLabel, + description: t.models.sttModelDesc, + modelType: 'speech_to_text', + id: `${generatedId}-stt`, + }, + ] + // State for embedding model change dialog const [showEmbeddingDialog, setShowEmbeddingDialog] = useState(false) const [pendingEmbeddingChange, setPendingEmbeddingChange] = useState<{ @@ -153,9 +165,9 @@ export function DefaultModelsSection({ models, defaults }: DefaultModelsSectionP return ( - Default Model Assignments + {t.models.defaultAssignments} - Configure which models to use for different purposes across Open Notebook + {t.models.defaultAssignmentsDesc} @@ -163,8 +175,7 @@ export function DefaultModelsSection({ models, defaults }: DefaultModelsSectionP - Missing required models: {missingRequired.join(', ')}. - Open Notebook may not function properly without these. + {t.models.missingRequiredModels.replace('{models}', missingRequired.join(', '))} )} @@ -179,7 +190,7 @@ export function DefaultModelsSection({ models, defaults }: DefaultModelsSectionP return (
-
diff --git a/frontend/src/app/(dashboard)/models/components/EmbeddingModelChangeDialog.tsx b/frontend/src/app/(dashboard)/models/components/EmbeddingModelChangeDialog.tsx index a81a3b93..05646d06 100644 --- a/frontend/src/app/(dashboard)/models/components/EmbeddingModelChangeDialog.tsx +++ b/frontend/src/app/(dashboard)/models/components/EmbeddingModelChangeDialog.tsx @@ -14,6 +14,7 @@ import { } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' import { AlertTriangle, ExternalLink } from 'lucide-react' +import { useTranslation } from '@/lib/hooks/use-translation' interface EmbeddingModelChangeDialogProps { open: boolean @@ -30,6 +31,7 @@ export function EmbeddingModelChangeDialog({ oldModelName, newModelName }: EmbeddingModelChangeDialogProps) { + const { t } = useTranslation() const router = useRouter() const [isConfirming, setIsConfirming] = useState(false) @@ -55,54 +57,49 @@ export function EmbeddingModelChangeDialog({
- Embedding Model Change + {t.models.embeddingChangeTitle}

- You are about to change your embedding model{' '} - {oldModelName && newModelName && ( - <> - from {oldModelName} to {newModelName} - - )} - . + {t.models.embeddingChangeConfirm + .replace('{from}', oldModelName || '...') + .replace('{to}', newModelName || '...')}

-

⚠️ Important: Rebuild Required

+

⚠️ {t.models.rebuildRequired}

- Changing your embedding model requires rebuilding all existing embeddings to maintain consistency. - Without rebuilding, your searches may return incorrect or incomplete results. + {t.models.rebuildReason}

-

What happens next:

+

{t.models.whatHappensNext}

    -
  • Your default embedding model will be updated
  • -
  • Existing embeddings will remain unchanged until rebuild
  • -
  • New content will use the new embedding model
  • -
  • You should rebuild embeddings as soon as possible
  • +
  • {t.models.step1}
  • +
  • {t.models.step2}
  • +
  • {t.models.step3}
  • +
  • {t.models.step4}

- Would you like to proceed to the Advanced page to start the rebuild now? + {t.models.proceedToRebuildPrompt}

- Cancel + {t.common.cancel} - Change & Go to Rebuild + {t.models.changeAndRebuild} diff --git a/frontend/src/app/(dashboard)/models/components/ModelTypeSection.tsx b/frontend/src/app/(dashboard)/models/components/ModelTypeSection.tsx index 3d1afe52..9810ebd8 100644 --- a/frontend/src/app/(dashboard)/models/components/ModelTypeSection.tsx +++ b/frontend/src/app/(dashboard)/models/components/ModelTypeSection.tsx @@ -10,6 +10,7 @@ import { LoadingSpinner } from '@/components/common/LoadingSpinner' import { useDeleteModel } from '@/lib/hooks/use-models' import { ConfirmDialog } from '@/components/common/ConfirmDialog' import { useState, useMemo } from 'react' +import { useTranslation } from '@/lib/hooks/use-translation' interface ModelTypeSectionProps { type: 'language' | 'embedding' | 'text_to_speech' | 'speech_to_text' @@ -21,6 +22,7 @@ interface ModelTypeSectionProps { const COLLAPSED_ITEM_COUNT = 5 export function ModelTypeSection({ type, models, providers, isLoading }: ModelTypeSectionProps) { + const { t } = useTranslation() const [deleteModel, setDeleteModel] = useState(null) const [selectedProvider, setSelectedProvider] = useState(null) const [isExpanded, setIsExpanded] = useState(false) @@ -30,32 +32,32 @@ export function ModelTypeSection({ type, models, providers, isLoading }: ModelTy switch (type) { case 'language': return { - title: 'Language Models', - description: 'Chat, transformations, and text generation', + title: t.models.language, + description: t.models.languageDesc, icon: Bot, iconColor: 'text-blue-500', bgColor: 'bg-blue-50 dark:bg-blue-950/20' } case 'embedding': return { - title: 'Embedding Models', - description: 'Semantic search and vector embeddings', + title: t.models.embedding, + description: t.models.embeddingDesc, icon: Search, iconColor: 'text-green-500', bgColor: 'bg-green-50 dark:bg-green-950/20' } case 'text_to_speech': return { - title: 'Text-to-Speech', - description: 'Generate audio from text', + title: t.models.tts, + description: t.models.ttsDesc, icon: Volume2, iconColor: 'text-purple-500', bgColor: 'bg-purple-50 dark:bg-purple-950/20' } case 'speech_to_text': return { - title: 'Speech-to-Text', - description: 'Transcribe audio to text', + title: t.models.stt, + description: t.models.sttDesc, icon: Mic, iconColor: 'text-orange-500', bgColor: 'bg-orange-50 dark:bg-orange-950/20' @@ -118,7 +120,7 @@ export function ModelTypeSection({ type, models, providers, isLoading }: ModelTy className="cursor-pointer text-xs" onClick={() => setSelectedProvider(null)} > - All + {t.models.all} {modelProviders.map(provider => ( {selectedProvider - ? `No ${selectedProvider} models configured` - : 'No models configured' + ? t.models.noProviderModelsConfigured.replace('{provider}', selectedProvider) + : t.models.noModelsConfigured }
) : ( @@ -182,12 +184,12 @@ export function ModelTypeSection({ type, models, providers, isLoading }: ModelTy {isExpanded ? ( <> - Show less + {t.models.seeLess} ) : ( <> - Show {filteredModels.length - COLLAPSED_ITEM_COUNT} more + {t.models.showMore.replace('{count}', (filteredModels.length - COLLAPSED_ITEM_COUNT).toString())} )} @@ -200,9 +202,9 @@ export function ModelTypeSection({ type, models, providers, isLoading }: ModelTy !open && setDeleteModel(null)} - title="Delete Model" - description={`Are you sure you want to delete "${deleteModel?.name}"? This action cannot be undone.`} - confirmText="Delete" + title={t.models.deleteModel} + description={t.models.deleteModelDesc.replace('{name}', deleteModel?.name || '')} + confirmText={t.common.delete} confirmVariant="destructive" onConfirm={handleDelete} /> diff --git a/frontend/src/app/(dashboard)/models/components/ProviderStatus.tsx b/frontend/src/app/(dashboard)/models/components/ProviderStatus.tsx index 4d726b97..3d29ac2b 100644 --- a/frontend/src/app/(dashboard)/models/components/ProviderStatus.tsx +++ b/frontend/src/app/(dashboard)/models/components/ProviderStatus.tsx @@ -6,12 +6,14 @@ import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Check, X } from 'lucide-react' import { ProviderAvailability } from '@/lib/types/models' +import { useTranslation } from '@/lib/hooks/use-translation' interface ProviderStatusProps { providers: ProviderAvailability } export function ProviderStatus({ providers }: ProviderStatusProps) { + const { t } = useTranslation() // Combine all providers, with available ones first const allProviders = useMemo( () => [ @@ -33,11 +35,13 @@ export function ProviderStatus({ providers }: ProviderStatusProps) { return ( - AI Providers + {t.models.aiProviders} - Configure providers through environment variables to enable their models. + {t.models.providerConfigDesc} - {providers.available.length} of {allProviders.length} configured + {t.models.configuredCount + .replace('{count}', providers.available.length.toString()) + .replace('{total}', allProviders.length.toString())} @@ -74,21 +78,21 @@ export function ProviderStatus({ providers }: ProviderStatusProps) { {provider.name} - {provider.available ? ( + {provider.available ? (
{supportedTypes.length > 0 ? ( supportedTypes.map((type) => ( - {type.replace('_', ' ')} + {(t.models as Record)[type] || type.replace('_', ' ')} )) ) : ( - No models + {t.models.noModels} )}
) : ( - Not configured + {t.models.notConfigured} )}
@@ -97,26 +101,28 @@ export function ProviderStatus({ providers }: ProviderStatusProps) { })}
- {allProviders.length > 6 ? ( + {allProviders.length > 6 ? (
) : null} -
+ diff --git a/frontend/src/app/(dashboard)/models/page.tsx b/frontend/src/app/(dashboard)/models/page.tsx index d4ab74f1..ff8e65d3 100644 --- a/frontend/src/app/(dashboard)/models/page.tsx +++ b/frontend/src/app/(dashboard)/models/page.tsx @@ -8,8 +8,10 @@ import { useModels, useModelDefaults, useProviders } from '@/lib/hooks/use-model import { LoadingSpinner } from '@/components/common/LoadingSpinner' import { RefreshCw } from 'lucide-react' import { Button } from '@/components/ui/button' +import { useTranslation } from '@/lib/hooks/use-translation' export default function ModelsPage() { + const { t } = useTranslation() const { data: models, isLoading: modelsLoading, refetch: refetchModels } = useModels() const { data: defaults, isLoading: defaultsLoading, refetch: refetchDefaults } = useModelDefaults() const { data: providers, isLoading: providersLoading, refetch: refetchProviders } = useProviders() @@ -35,7 +37,7 @@ export default function ModelsPage() {
-

Failed to load models data

+

{t.models.failedToLoad}

@@ -48,9 +50,9 @@ export default function ModelsPage() {
-

Model Management

+

{t.models.title}

- Configure AI models for different purposes across Open Notebook + {t.models.desc}

diff --git a/frontend/src/app/(dashboard)/notebooks/components/NotebookCard.tsx b/frontend/src/app/(dashboard)/notebooks/components/NotebookCard.tsx index 7cff89d2..1cfaee4f 100644 --- a/frontend/src/app/(dashboard)/notebooks/components/NotebookCard.tsx +++ b/frontend/src/app/(dashboard)/notebooks/components/NotebookCard.tsx @@ -16,12 +16,14 @@ import { import { useUpdateNotebook, useDeleteNotebook } from '@/lib/hooks/use-notebooks' import { ConfirmDialog } from '@/components/common/ConfirmDialog' import { useState } from 'react' - +import { useTranslation } from '@/lib/hooks/use-translation' +import { getDateLocale } from '@/lib/utils/date-locale' interface NotebookCardProps { notebook: NotebookResponse } export function NotebookCard({ notebook }: NotebookCardProps) { + const { t, language } = useTranslation() const [showDeleteDialog, setShowDeleteDialog] = useState(false) const router = useRouter() const updateNotebook = useUpdateNotebook() @@ -59,7 +61,7 @@ export function NotebookCard({ notebook }: NotebookCardProps) { {notebook.archived && ( - Archived + {t.notebooks.archived} )}
@@ -80,12 +82,12 @@ export function NotebookCard({ notebook }: NotebookCardProps) { {notebook.archived ? ( <> - Unarchive + {t.notebooks.unarchive} ) : ( <> - Archive + {t.notebooks.archive} )} @@ -97,7 +99,7 @@ export function NotebookCard({ notebook }: NotebookCardProps) { className="text-red-600" > - Delete + {t.common.delete} @@ -106,11 +108,14 @@ export function NotebookCard({ notebook }: NotebookCardProps) { - {notebook.description || 'No description'} + {notebook.description || t.chat.noDescription}
- Updated {formatDistanceToNow(new Date(notebook.updated), { addSuffix: true })} + {t.common.updated.replace('{time}', formatDistanceToNow(new Date(notebook.updated), { + addSuffix: true, + locale: getDateLocale(language) + }))}
{/* Item counts footer */} @@ -130,9 +135,9 @@ export function NotebookCard({ notebook }: NotebookCardProps) { diff --git a/frontend/src/app/(dashboard)/notebooks/components/NotebookHeader.tsx b/frontend/src/app/(dashboard)/notebooks/components/NotebookHeader.tsx index 148dc7be..93277689 100644 --- a/frontend/src/app/(dashboard)/notebooks/components/NotebookHeader.tsx +++ b/frontend/src/app/(dashboard)/notebooks/components/NotebookHeader.tsx @@ -8,13 +8,17 @@ import { Archive, ArchiveRestore, Trash2 } from 'lucide-react' import { useUpdateNotebook, useDeleteNotebook } from '@/lib/hooks/use-notebooks' import { ConfirmDialog } from '@/components/common/ConfirmDialog' import { formatDistanceToNow } from 'date-fns' +import { getDateLocale } from '@/lib/utils/date-locale' import { InlineEdit } from '@/components/common/InlineEdit' +import { useTranslation } from '@/lib/hooks/use-translation' interface NotebookHeaderProps { notebook: NotebookResponse } export function NotebookHeader({ notebook }: NotebookHeaderProps) { + const { t, language } = useTranslation() + const dfLocale = getDateLocale(language) const [showDeleteDialog, setShowDeleteDialog] = useState(false) const updateNotebook = useUpdateNotebook() @@ -57,14 +61,16 @@ export function NotebookHeader({ notebook }: NotebookHeaderProps) {
{notebook.archived && ( - Archived + {t.notebooks.archived} )}
@@ -76,12 +82,12 @@ export function NotebookHeader({ notebook }: NotebookHeaderProps) { {notebook.archived ? ( <> - Unarchive + {t.notebooks.unarchive} ) : ( <> - Archive + {t.notebooks.archive} )} @@ -92,24 +98,26 @@ export function NotebookHeader({ notebook }: NotebookHeaderProps) { className="text-red-600 hover:text-red-700" > - Delete + {t.common.delete}
- Created {formatDistanceToNow(new Date(notebook.created), { addSuffix: true })} • - Updated {formatDistanceToNow(new Date(notebook.updated), { addSuffix: true })} + {t.common.created.replace('{time}', formatDistanceToNow(new Date(notebook.created), { addSuffix: true, locale: dfLocale }))} • + {t.common.updated.replace('{time}', formatDistanceToNow(new Date(notebook.updated), { addSuffix: true, locale: dfLocale }))}
@@ -117,9 +125,9 @@ export function NotebookHeader({ notebook }: NotebookHeaderProps) { diff --git a/frontend/src/app/(dashboard)/notebooks/components/NotebookList.tsx b/frontend/src/app/(dashboard)/notebooks/components/NotebookList.tsx index 7e3bf322..a09efa82 100644 --- a/frontend/src/app/(dashboard)/notebooks/components/NotebookList.tsx +++ b/frontend/src/app/(dashboard)/notebooks/components/NotebookList.tsx @@ -7,6 +7,7 @@ import { EmptyState } from '@/components/common/EmptyState' import { Book, ChevronDown, ChevronRight, Plus } from 'lucide-react' import { Button } from '@/components/ui/button' import { useState } from 'react' +import { useTranslation } from '@/lib/hooks/use-translation' interface NotebookListProps { notebooks?: NotebookResponse[] @@ -29,6 +30,7 @@ export function NotebookList({ onAction, actionLabel, }: NotebookListProps) { + const { t } = useTranslation() const [isExpanded, setIsExpanded] = useState(!collapsible) if (isLoading) { @@ -43,8 +45,8 @@ export function NotebookList({ return ( diff --git a/frontend/src/app/(dashboard)/notebooks/components/NotesColumn.tsx b/frontend/src/app/(dashboard)/notebooks/components/NotesColumn.tsx index 9ffa6bc0..05e39b2b 100644 --- a/frontend/src/app/(dashboard)/notebooks/components/NotesColumn.tsx +++ b/frontend/src/app/(dashboard)/notebooks/components/NotesColumn.tsx @@ -15,6 +15,7 @@ import { LoadingSpinner } from '@/components/common/LoadingSpinner' import { EmptyState } from '@/components/common/EmptyState' import { Badge } from '@/components/ui/badge' import { NoteEditorDialog } from './NoteEditorDialog' +import { getDateLocale } from '@/lib/utils/date-locale' import { formatDistanceToNow } from 'date-fns' import { ContextToggle } from '@/components/common/ContextToggle' import { ContextMode } from '../[id]/page' @@ -22,6 +23,7 @@ import { useDeleteNote } from '@/lib/hooks/use-notes' import { ConfirmDialog } from '@/components/common/ConfirmDialog' import { CollapsibleColumn, createCollapseButton } from '@/components/notebooks/CollapsibleColumn' import { useNotebookColumnsStore } from '@/lib/stores/notebook-columns-store' +import { useTranslation } from '@/lib/hooks/use-translation' interface NotesColumnProps { notes?: NoteResponse[] @@ -38,6 +40,7 @@ export function NotesColumn({ contextSelections, onContextModeChange }: NotesColumnProps) { + const { t, language } = useTranslation() const [showAddDialog, setShowAddDialog] = useState(false) const [editingNote, setEditingNote] = useState(null) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) @@ -48,8 +51,8 @@ export function NotesColumn({ // Collapsible column state const { notesCollapsed, toggleNotes } = useNotebookColumnsStore() const collapseButton = useMemo( - () => createCollapseButton(toggleNotes, 'Notes'), - [toggleNotes] + () => createCollapseButton(toggleNotes, t.common.notes), + [toggleNotes, t.common.notes] ) const handleDeleteClick = (noteId: string) => { @@ -75,12 +78,12 @@ export function NotesColumn({ isCollapsed={notesCollapsed} onToggle={toggleNotes} collapsedIcon={StickyNote} - collapsedLabel="Notes" + collapsedLabel={t.common.notes} >
- Notes + {t.common.notes}
{collapseButton}
@@ -105,8 +108,8 @@ export function NotesColumn({ ) : !notes || notes.length === 0 ? ( ) : (
@@ -124,13 +127,16 @@ export function NotesColumn({ )} - {note.note_type === 'ai' ? 'AI Generated' : 'Human'} + {note.note_type === 'ai' ? t.common.aiGenerated : t.common.human}
- {formatDistanceToNow(new Date(note.updated), { addSuffix: true })} + {formatDistanceToNow(new Date(note.updated), { + addSuffix: true, + locale: getDateLocale(language) + })} {/* Context toggle - only show if handler provided */} @@ -165,7 +171,7 @@ export function NotesColumn({ className="text-red-600 focus:text-red-600" > - Delete Note + {t.notebooks.deleteNote} @@ -206,9 +212,9 @@ export function NotesColumn({ createCollapseButton(toggleSources, 'Sources'), - [toggleSources] + () => createCollapseButton(toggleSources, t.navigation.sources), + [toggleSources, t.navigation.sources] ) // Scroll container ref for infinite scroll @@ -149,29 +151,29 @@ export function SourcesColumn({ isCollapsed={sourcesCollapsed} onToggle={toggleSources} collapsedIcon={FileText} - collapsedLabel="Sources" + collapsedLabel={t.navigation.sources} >
- Sources + {t.navigation.sources}
{ setDropdownOpen(false); setAddDialogOpen(true); }}> - Add New Source + {t.sources.addSource} { setDropdownOpen(false); setAddExistingDialogOpen(true); }}> - Add Existing Source + {t.sources.addExistingTitle} @@ -188,8 +190,8 @@ export function SourcesColumn({ ) : !sources || sources.length === 0 ? ( ) : (
@@ -238,9 +240,9 @@ export function SourcesColumn({
-

Notebooks

+

{t.notebooks.title}

setSearchTerm(event.target.value)} - placeholder="Search notebooks..." + placeholder={t.notebooks.searchPlaceholder} + autoComplete="off" + aria-label={t.common.accessibility?.searchNotebooks || "Search notebooks"} className="w-full sm:w-64" />
@@ -74,21 +80,21 @@ export default function NotebooksPage() { setCreateDialogOpen(true) : undefined} - actionLabel={!isSearching ? "Create Notebook" : undefined} + actionLabel={!isSearching ? t.notebooks.newNotebook : undefined} /> {hasArchived && ( )}
diff --git a/frontend/src/app/(dashboard)/podcasts/page.tsx b/frontend/src/app/(dashboard)/podcasts/page.tsx index 4e32d276..fe55d866 100644 --- a/frontend/src/app/(dashboard)/podcasts/page.tsx +++ b/frontend/src/app/(dashboard)/podcasts/page.tsx @@ -7,8 +7,10 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { EpisodesTab } from '@/components/podcasts/EpisodesTab' import { TemplatesTab } from '@/components/podcasts/TemplatesTab' import { Mic, LayoutTemplate } from 'lucide-react' +import { useTranslation } from '@/lib/hooks/use-translation' export default function PodcastsPage() { + const { t } = useTranslation() const [activeTab, setActiveTab] = useState<'episodes' | 'templates'>('episodes') return ( @@ -16,9 +18,9 @@ export default function PodcastsPage() {
-

Podcasts

+

{t.podcasts.listTitle}

- Keep track of generated episodes and manage reusable templates. + {t.podcasts.listDesc}

@@ -28,15 +30,15 @@ export default function PodcastsPage() { className="space-y-6" >
-

Choose a view

- +

{t.podcasts.chooseAView}

+ - Episodes + {t.podcasts.episodesTab} - Templates + {t.podcasts.templatesTab}
diff --git a/frontend/src/app/(dashboard)/search/page.tsx b/frontend/src/app/(dashboard)/search/page.tsx index e52159e5..4ca15adb 100644 --- a/frontend/src/app/(dashboard)/search/page.tsx +++ b/frontend/src/app/(dashboard)/search/page.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useSearchParams } from 'next/navigation' +import { useTranslation } from '@/lib/hooks/use-translation' import { AppShell } from '@/components/layout/AppShell' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Input } from '@/components/ui/input' @@ -24,10 +25,11 @@ import { AdvancedModelsDialog } from '@/components/search/AdvancedModelsDialog' import { SaveToNotebooksDialog } from '@/components/search/SaveToNotebooksDialog' export default function SearchPage() { + const { t } = useTranslation() // URL params const searchParams = useSearchParams() - const urlQuery = searchParams.get('q') || '' - const rawMode = searchParams.get('mode') + const urlQuery = searchParams?.get('q') || '' + const rawMode = searchParams?.get('mode') const urlMode = rawMode === 'search' ? 'search' : 'ask' // Tab state (controlled) @@ -70,7 +72,7 @@ export default function SearchPage() { }, [availableModels]) const resolveModelName = (id?: string | null) => { - if (!id) return 'Not set' + if (!id) return t.searchPage.notSet return modelNameById.get(id) ?? id } @@ -130,8 +132,8 @@ export default function SearchPage() { // Handle URL param changes while on page (e.g., from command palette again) useEffect(() => { - const currentQ = searchParams.get('q') || '' - const rawCurrentMode = searchParams.get('mode') + const currentQ = searchParams?.get('q') || '' + const rawCurrentMode = searchParams?.get('mode') const currentMode = rawCurrentMode === 'search' ? 'search' : 'ask' // Check if URL params have changed @@ -157,19 +159,19 @@ export default function SearchPage() { return (
-

Ask and Search

+

{t.searchPage.askAndSearch}

setActiveTab(v as 'ask' | 'search')} className="w-full space-y-6">
-

Choose a mode

- +

{t.searchPage.chooseAMode}

+ - Ask (beta) + {t.searchPage.askBeta} - Search + {t.searchPage.search}
@@ -177,18 +179,19 @@ export default function SearchPage() { - Ask Your Knowledge Base (beta) + {t.searchPage.askYourKb}

- The LLM will answer your query based on the documents in your knowledge base. + {t.searchPage.askYourKbDesc}

{/* Question Input */}
- +