diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..1ae550590cc1c526faba623a4c2fe09be1d85075 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# Ignore Node.js dependencies +frontend/node_modules/ +frontend/dist/ +frontend/.vite/ + +# Ignore Python environments and caches +**/__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +.venv/ +pip-log.txt +pip-delete-this-directory.txt + +# Ignore models directory (it is mounted as a volume at runtime) +models/ + +# Ignore logs and dev tools +.git/ +.gitignore +.dockerignore +.agents/ +.gemini/ +*.log +docker-compose.override.yml diff --git a/.env b/.env new file mode 100644 index 0000000000000000000000000000000000000000..cdb6ae4f4bf181e60e650197fe2f5701e340b118 --- /dev/null +++ b/.env @@ -0,0 +1,21 @@ +# AI Coding Assistant - Active Environment Configuration + +PORT=8000 +HOST=0.0.0.0 +DEBUG=true +CORS_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost:8000 + +# SmolLM2 Model Settings +LOCAL_MODEL_PATH=models/SmolLM2-360M-Instruct-Q4_K_M.gguf +LOCAL_MODEL_REPO=bartowski/SmolLM2-360M-Instruct-GGUF +LOCAL_MODEL_FILE=SmolLM2-360M-Instruct-Q4_K_M.gguf + +# Generation Parameters +DEFAULT_TEMPERATURE=0.7 +DEFAULT_MAX_TOKENS=1024 +DEFAULT_TOP_P=0.9 +DEFAULT_CONTEXT_LENGTH=2048 + +# Security & Limits +RATE_LIMIT_PER_MINUTE=100 +SECRET_KEY=dev-secret-key-32-chars-long-minimum-required! diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..dbb8d53bf53d23425eed1d5a6a89fb56132e1a1e --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +# AI Coding Assistant - Environment Configuration + +# Backend Server Configuration +PORT=8000 +HOST=0.0.0.0 +DEBUG=false +CORS_ORIGINS=http://localhost:5173,http://localhost:3000,https://*.railway.app,https://*.render.com + +# SmolLM2 Model Settings +LOCAL_MODEL_PATH=models/SmolLM2-360M-Instruct-Q4_K_M.gguf +LOCAL_MODEL_REPO=bartowski/SmolLM2-360M-Instruct-GGUF +LOCAL_MODEL_FILE=SmolLM2-360M-Instruct-Q4_K_M.gguf + +# Generation Parameters +DEFAULT_TEMPERATURE=0.7 +DEFAULT_MAX_TOKENS=1024 +DEFAULT_TOP_P=0.9 +DEFAULT_CONTEXT_LENGTH=2048 + +# Security & Limits +RATE_LIMIT_PER_MINUTE=60 +SECRET_KEY=generate_a_secure_random_key_here diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..30cf57ed7cba5157ad5f7c05ede08adde945ada6 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/Levi.iml b/.idea/Levi.iml new file mode 100644 index 0000000000000000000000000000000000000000..d0876a78d06ac03b5d78c8dcdb95570281c6f1d6 --- /dev/null +++ b/.idea/Levi.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000000000000000000000000000000000000..5626ca9841470e59f3f23d031d72e5d10d47ca80 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000000000000000000000000000000000000..105ce2da2d6447d11dfe32bfb846c3d5b199fc99 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000000000000000000000000000000000..4c5045a599734bd4a871a18ce34887002a08e42b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/new.iml b/.idea/new.iml new file mode 100644 index 0000000000000000000000000000000000000000..8b8c395472a5a6b3598af42086e590417ace9933 --- /dev/null +++ b/.idea/new.iml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000000000000000000000000000000000..35eb1ddfbbc029bcab630581847471d7f238ec53 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6fea97ba2c2aafb4ad38f0d17d95d25df00c71aa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +# Stage 1: Build React Frontend +FROM node:20-slim as frontend-builder + +WORKDIR /frontend + +# Copy package config and lock files +COPY frontend/package*.json ./ + +# Install packages +RUN npm ci + +# Copy frontend source files +COPY frontend/ ./ + +# Build production static bundle +RUN npm run build + +# Stage 2: Build Python Backend & Package app +FROM python:3.11-slim + +WORKDIR /app + +# Install compilation tools for building llama-cpp-python in backend +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + gcc \ + g++ \ + make \ + python3-dev \ + git \ + libgomp1 \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements and install dependencies +COPY backend/requirements.txt ./backend/ +RUN pip install --no-cache-dir -r ./backend/requirements.txt + +# Copy backend source code +COPY backend/ ./backend/ + +# Copy static frontend build from Stage 1 into frontend/dist +COPY --from=frontend-builder /frontend/dist ./frontend/dist + +# Setup environments +ENV PORT=8000 +ENV HOST=0.0.0.0 +ENV PYTHONPATH=/app + +EXPOSE 8000 + +# Start unified server +CMD ["python", "backend/run.py"] diff --git a/README.md b/README.md index d54bdd20f4211fca988461ae936c5312668a67cc..a6ab1cc1f3475600340567ad4fc87c50bf3f0a06 100644 --- a/README.md +++ b/README.md @@ -1 +1,107 @@ -# Levi \ No newline at end of file +# Antigravity AI Coding Assistant ⚡ + +A production-ready, resource-optimized, containerized AI Coding Assistant using **Qwen2.5-Coder-0.5B-Instruct** as its core engine. Built with a Python FastAPI backend and a stunning dark-theme React + Vite + TypeScript frontend. + +```mermaid +graph TD + User([User]) <--> |HTTP / SSE| FE[React SPA - Vite + TS] + subgraph Backend [FastAPI Server] + API[API Endpoints] <--> MGR[LLM Manager] + MGR --> |Check RAM / Models| Decision{Load Local GGUF?} + Decision -->|Yes: RAM >= 1.5GB| GGUF[Local llama-cpp-python] + Decision -->|No: Low memory / Failed| HF[Hugging Face Cloud API] + end + GGUF <--> |Read / Write| Models[(models/ folder)] + HF <--> |HTTPS Request| HFHub[Hugging Face Inference Hub] +``` + +--- + +## 🌟 Key Features + +* **Cascading Fallback Pipeline:** Automatically runs local Qwen GGUF inference. If RAM is constrained (e.g. Render Free, Railway Starter), it transparently falls back to Hugging Face serverless API. +* **Interactive Code Playground:** Features Monaco Editor (VS Code core) with language syntax highlight selectors and quick AI commands (`Explain`, `Find Bugs`, `Refactor`, `Generate Tests`, `Summarize`). +* **Advanced Chat Window:** Smooth response streaming (Server-Sent Events), code block copy buttons, and drag-and-drop file imports. +* **Production Deployment Ready:** Pre-configured Dockerfiles, Docker Compose, Railway config, and Render deployment specifications. +* **Performance Telemetry:** Live dashboards displaying generation speed (tokens/sec), latency, request tallies, and RAM footprint. + +--- + +## 🛠️ Tech Stack + +* **Frontend:** React 19, Vite, TypeScript, Tailwind CSS, Monaco Editor, Lucide Icons, Framer Motion +* **Backend:** FastAPI, Python 3.11+, Uvicorn, llama-cpp-python, Hugging Face Hub Client, Psutil + +--- + +## 🚀 Quick Start (Local Setup) + +### Prerequisites +* Python 3.11+ +* Node.js 20+ + +### Step 1: Clone and Setup Workspace +Clone this repository and navigate to the project directory: +```bash +git clone https://github.com/your-repo/antigravity-coder.git +cd antigravity-coder +``` + +### Step 2: Install and Download GGUF Model +Use our automated installer script: +```bash +# On Linux/macOS +chmod +x scripts/setup.sh +./scripts/setup.sh + +# On Windows (PowerShell) +pip install -r backend/requirements.txt +cd frontend; npm install; npm run build; cd .. +python scripts/download_model.py +``` + +### Step 3: Run the Application +Start the backend server: +```bash +# Run backend (activates virtual env if created) +python backend/run.py +``` +This runs the API server on `http://localhost:8000` and automatically compiles & hosts the React frontend assets. Navigate to [http://localhost:8000](http://localhost:8000) to view the application! + +For hot-reloading frontend development: +```bash +cd frontend +npm run dev +``` +Open [http://localhost:5173](http://localhost:5173) in your browser. + +--- + +## 🐳 Running with Docker + +Run both services in hot-reloading development mode using Docker Compose: +```bash +docker compose -f docker/docker-compose.yml up --build +``` +Build and run the production-ready unified container (hosting both frontend and API on port 8000): +```bash +docker build -t antigravity-coder . +docker run -p 8000:8000 antigravity-coder +``` + +--- + +## 🌐 Cloud Deployment + +Detailed step-by-step guides for deployment configurations: +* [Railway Deployment Guide](docs/DEPLOYMENT.md#railway) +* [Render Deployment Guide](docs/DEPLOYMENT.md#render) +* [API Reference Documentation](docs/API.md) + +--- + +## 🔒 Security + +* Standard CORS origin protections. +* Secure in-memory sliding rate limiter per client IP. +* Configuration parsing using Pydantic Settings from `.env` files. Secrets are never hardcoded. diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..60510c76ffb9ca0dd77574d95a170537a2c80f78 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ +# AI Coding Assistant Backend Package diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000000000000000000000000000000000000..bf5b730808d461581898a6aa0fbc5e8fe978e12c --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,57 @@ +import os +from typing import List +from pydantic_settings import BaseSettings, SettingsConfigDict +from pydantic import Field + +class Settings(BaseSettings): + # Server settings + PORT: int = Field(default=8000, validation_alias="PORT") + HOST: str = Field(default="0.0.0.0", validation_alias="HOST") + DEBUG: bool = Field(default=False, validation_alias="DEBUG") + CORS_ORIGINS: str = Field( + default="http://localhost:5173,http://localhost:3000,http://localhost:8000", + validation_alias="CORS_ORIGINS" + ) + + # LLM Settings + INFERENCE_MODE: str = Field(default="local", validation_alias="INFERENCE_MODE") + + # Local GGUF Settings + LOCAL_MODEL_PATH: str = Field( + default="models/SmolLM2-360M-Instruct-Q4_K_M.gguf", + validation_alias="LOCAL_MODEL_PATH" + ) + LOCAL_MODEL_REPO: str = Field( + default="bartowski/SmolLM2-360M-Instruct-GGUF", + validation_alias="LOCAL_MODEL_REPO" + ) + LOCAL_MODEL_FILE: str = Field( + default="SmolLM2-360M-Instruct-Q4_K_M.gguf", + validation_alias="LOCAL_MODEL_FILE" + ) + + # Generation Defaults + DEFAULT_TEMPERATURE: float = Field(default=0.7, validation_alias="DEFAULT_TEMPERATURE") + DEFAULT_MAX_TOKENS: int = Field(default=1024, validation_alias="DEFAULT_MAX_TOKENS") + DEFAULT_TOP_P: float = Field(default=0.9, validation_alias="DEFAULT_TOP_P") + DEFAULT_CONTEXT_LENGTH: int = Field(default=2048, validation_alias="DEFAULT_CONTEXT_LENGTH") + + # Security / Limits + RATE_LIMIT_PER_MINUTE: int = Field(default=60, validation_alias="RATE_LIMIT_PER_MINUTE") + SECRET_KEY: str = Field( + default="dev-secret-key-must-be-changed-in-production-environments!", + validation_alias="SECRET_KEY" + ) + + @property + def cors_origins_list(self) -> List[str]: + return [origin.strip() for origin in self.CORS_ORIGINS.split(",") if origin.strip()] + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + extra="ignore" + ) + +# Instantiate singleton settings +settings = Settings() diff --git a/backend/app/llm/__init__.py b/backend/app/llm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e9fcdc416532b96e4b576bf545e13e4a7914fd3b --- /dev/null +++ b/backend/app/llm/__init__.py @@ -0,0 +1 @@ +# LLM Provider Adapters Package diff --git a/backend/app/llm/base.py b/backend/app/llm/base.py new file mode 100644 index 0000000000000000000000000000000000000000..fd09d3e07cb03a3250de7aa4239dea1358cc672b --- /dev/null +++ b/backend/app/llm/base.py @@ -0,0 +1,45 @@ +from abc import ABC, abstractmethod +from typing import AsyncIterator, List, Dict, Any, Optional + +class BaseLLMProvider(ABC): + @abstractmethod + async def initialize(self) -> bool: + """Initialize the LLM provider. Returns True if successful.""" + pass + + @abstractmethod + async def generate( + self, + prompt: str, + system_prompt: Optional[str] = None, + messages: Optional[List[Dict[str, str]]] = None, + temperature: float = 0.7, + max_tokens: int = 1024, + top_p: float = 0.9, + ) -> Dict[str, Any]: + """ + Execute non-streaming completion. + Returns a dict with format: { "content": str, "usage": dict } + """ + pass + + @abstractmethod + async def generate_stream( + self, + prompt: str, + system_prompt: Optional[str] = None, + messages: Optional[List[Dict[str, str]]] = None, + temperature: float = 0.7, + max_tokens: int = 1024, + top_p: float = 0.9, + ) -> AsyncIterator[str]: + """ + Execute streaming completion. + Yields text chunks. + """ + pass + + @abstractmethod + def get_info(self) -> Dict[str, Any]: + """Return diagnostic and config information for the provider.""" + pass diff --git a/backend/app/llm/local.py b/backend/app/llm/local.py new file mode 100644 index 0000000000000000000000000000000000000000..abafc4683c1e4943ec2695781266d406f950abf1 --- /dev/null +++ b/backend/app/llm/local.py @@ -0,0 +1,168 @@ +import os +import sys +import logging +import asyncio +from typing import AsyncIterator, List, Dict, Any, Optional +from backend.app.llm.base import BaseLLMProvider +from backend.app.config import settings + +logger = logging.getLogger(__name__) + +# Safe imports for environment portability +try: + from llama_cpp import Llama +except ImportError: + Llama = None + logger.warning("llama-cpp-python is not installed. Local inference will be unavailable.") + +class LocalLLMProvider(BaseLLMProvider): + def __init__(self): + self.llm = None + self.initialized = False + self.model_path = os.path.abspath(settings.LOCAL_MODEL_PATH) + self.context_length = settings.DEFAULT_CONTEXT_LENGTH + + async def initialize(self) -> bool: + if self.initialized: + return True + + if Llama is None: + logger.error("Cannot initialize LocalLLMProvider: llama-cpp-python not installed.") + return False + + if not os.path.exists(self.model_path): + logger.error(f"Local GGUF model file not found at: {self.model_path}") + return False + + try: + # Determine thread count (default to CPU cores minus 1, min 1) + threads = max(1, (os.cpu_count() or 2) - 1) + + logger.info(f"Loading local model from {self.model_path} with {threads} threads and context {self.context_length}...") + + # Since loading the model blocks, run it in a separate thread to keep event loop active + def load_model(): + return Llama( + model_path=self.model_path, + n_ctx=self.context_length, + n_threads=threads, + verbose=settings.DEBUG + ) + + self.llm = await asyncio.to_thread(load_model) + self.initialized = True + logger.info("Local model successfully loaded!") + return True + except Exception as e: + logger.error(f"Failed to load local model: {e}", exc_info=True) + self.initialized = False + self.llm = None + return False + + def _format_prompt(self, prompt: str, system_prompt: Optional[str] = None, messages: Optional[List[Dict[str, str]]] = None) -> str: + # Build standard Qwen ChatML prompt format + formatted = "" + + # Determine system prompt + sys_p = system_prompt or "You are Qwen, a helpful, precise, and state-of-the-art AI programming assistant." + + # Check if messages already contain a system prompt + has_system = any(m.get("role") == "system" for m in messages) if messages else False + if not has_system: + formatted += f"<|im_start|>system\n{sys_p}<|im_end|>\n" + + if messages: + for msg in messages: + role = msg.get("role", "user") + content = msg.get("content", "") + formatted += f"<|im_start|>{role}\n{content}<|im_end|>\n" + else: + # If no chat history is provided, construct a simple message pair + formatted += f"<|im_start|>user\n{prompt}<|im_end|>\n" + + formatted += "<|im_start|>assistant\n" + return formatted + + async def generate( + self, + prompt: str, + system_prompt: Optional[str] = None, + messages: Optional[List[Dict[str, str]]] = None, + temperature: float = 0.7, + max_tokens: int = 1024, + top_p: float = 0.9, + ) -> Dict[str, Any]: + if not await self.initialize(): + raise RuntimeError("Local LLM provider is not initialized.") + + formatted_prompt = self._format_prompt(prompt, system_prompt, messages) + + def run_inference(): + return self.llm( + prompt=formatted_prompt, + max_tokens=max_tokens, + temperature=temperature, + top_p=top_p, + stop=["<|im_end|>", "<|im_start|>", "im_end", "im_start"], + ) + + response = await asyncio.to_thread(run_inference) + + content = response["choices"][0]["text"] + prompt_tokens = response["usage"]["prompt_tokens"] + completion_tokens = response["usage"]["completion_tokens"] + + return { + "content": content, + "usage": { + "prompt_tokens": prompt_tokens, + "completion_tokens": completion_tokens, + "total_tokens": prompt_tokens + completion_tokens + } + } + + async def generate_stream( + self, + prompt: str, + system_prompt: Optional[str] = None, + messages: Optional[List[Dict[str, str]]] = None, + temperature: float = 0.7, + max_tokens: int = 1024, + top_p: float = 0.9, + ) -> AsyncIterator[str]: + if not await self.initialize(): + raise RuntimeError("Local LLM provider is not initialized.") + + formatted_prompt = self._format_prompt(prompt, system_prompt, messages) + + # Generator for streaming + def run_stream(): + return self.llm( + prompt=formatted_prompt, + max_tokens=max_tokens, + temperature=temperature, + top_p=top_p, + stop=["<|im_end|>", "<|im_start|>", "im_end", "im_start"], + stream=True + ) + + stream = await asyncio.to_thread(run_stream) + + async def async_generator(): + for chunk in stream: + text = chunk["choices"][0]["text"] + if text: + yield text + # Yield CPU control to event loop + await asyncio.sleep(0) + + return async_generator() + + def get_info(self) -> Dict[str, Any]: + return { + "provider_name": "local", + "initialized": self.initialized, + "model_path": self.model_path, + "context_length": self.context_length, + "device": "CPU" # Llama.cpp runs on CPU in basic config, can use GPU via CUDA wrappers + } diff --git a/backend/app/llm/manager.py b/backend/app/llm/manager.py new file mode 100644 index 0000000000000000000000000000000000000000..519e6e2e20f47387167b31176b63d78c33107f0c --- /dev/null +++ b/backend/app/llm/manager.py @@ -0,0 +1,89 @@ +import os +import logging +import asyncio +from typing import AsyncIterator, Dict, Any, Optional +from backend.app.config import settings +from backend.app.llm.base import BaseLLMProvider +from backend.app.llm.local import LocalLLMProvider + +logger = logging.getLogger(__name__) + +class LLMManager: + def __init__(self): + self.provider: Optional[BaseLLMProvider] = None + self.is_downloading: bool = False + + async def ensure_model_downloaded(self) -> bool: + """Downloads the GGUF model file if not exists.""" + model_path = os.path.abspath(settings.LOCAL_MODEL_PATH) + if os.path.exists(model_path): + logger.info(f"Model file already exists at: {model_path}") + return True + + # Ensure directory exists + os.makedirs(os.path.dirname(model_path), exist_ok=True) + + self.is_downloading = True + logger.info(f"Downloading model {settings.LOCAL_MODEL_FILE} from repo {settings.LOCAL_MODEL_REPO}...") + + try: + from huggingface_hub import hf_hub_download + + def download_job(): + return hf_hub_download( + repo_id=settings.LOCAL_MODEL_REPO, + filename=settings.LOCAL_MODEL_FILE, + local_dir=os.path.dirname(model_path), + local_dir_use_symlinks=False + ) + + # Download model in background thread to not block the main application loop + await asyncio.to_thread(download_job) + logger.info("Model download complete!") + self.is_downloading = False + return True + except Exception as e: + logger.error(f"Failed to download model automatically: {e}", exc_info=True) + self.is_downloading = False + return False + + async def setup_provider(self) -> BaseLLMProvider: + """ + Initializes the local LLM provider strictly. + """ + logger.info("Setting up local provider for SmolLM2...") + await self.ensure_model_downloaded() + + local = LocalLLMProvider() + # Initialize in background, do not raise exception on failure to support diagnostics + await local.initialize() + self.provider = local + return local + + async def get_active_provider(self) -> BaseLLMProvider: + """Returns the active provider. Lazily initializes if needed.""" + if not self.provider: + await self.setup_provider() + return self.provider + + async def generate(self, *args, **kwargs) -> Dict[str, Any]: + p = await self.get_active_provider() + return await p.generate(*args, **kwargs) + + async def generate_stream(self, *args, **kwargs) -> AsyncIterator[str]: + p = await self.get_active_provider() + return await p.generate_stream(*args, **kwargs) + + async def get_status_info(self) -> Dict[str, Any]: + """Returns diagnostic and current provider stats.""" + p = await self.get_active_provider() + info = p.get_info() + info.update({ + "configured_mode": "local", + "active_mode": "local", + "is_downloading": self.is_downloading + }) + return info + +# Instantiate global singleton manager +llm_manager = LLMManager() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..664ccf178cd112c1f18a5b2dceb9cd7a1c17d8bc --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,317 @@ +import os +import time +import uuid +import logging +import asyncio +import psutil +from contextlib import asynccontextmanager +from typing import AsyncGenerator, Dict, Any + +from fastapi import FastAPI, Depends, HTTPException, status +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse, JSONResponse +from fastapi.staticfiles import StaticFiles + +from backend.app.config import settings +from backend.app.models import ( + ChatRequest, ChatResponse, ChatResponseChunk, + CodeRequest, CompletionRequest, ModelInfoResponse +) +from backend.app.middleware import RateLimitMiddleware, LoggingMiddleware +from backend.app.llm.manager import llm_manager +from backend.app.utils import ( + estimate_tokens, format_sse_chunk, + get_code_prompt, get_completion_prompt +) + +# Setup logging configuration +logging.basicConfig( + level=logging.INFO if not settings.DEBUG else logging.DEBUG, + format="%(asctime)s [%(levelname)s] %(name)s - %(message)s" +) +logger = logging.getLogger("backend.app.main") + +# Metrics memory store +class SystemMetrics: + def __init__(self): + self.total_requests = 0 + self.total_prompt_tokens = 0 + self.total_completion_tokens = 0 + self.total_generation_time_sec = 0.0 + + def add(self, prompt_tokens: int, completion_tokens: int, duration: float): + self.total_requests += 1 + self.total_prompt_tokens += prompt_tokens + self.total_completion_tokens += completion_tokens + self.total_generation_time_sec += duration + + def get_metrics_report(self) -> Dict[str, Any]: + avg_latency = ( + self.total_generation_time_sec / self.total_requests + if self.total_requests > 0 else 0.0 + ) + return { + "total_requests": self.total_requests, + "total_prompt_tokens": self.total_prompt_tokens, + "total_completion_tokens": self.total_completion_tokens, + "total_tokens": self.total_prompt_tokens + self.total_completion_tokens, + "average_latency_seconds": round(avg_latency, 4), + "total_generation_time_seconds": round(self.total_generation_time_sec, 2) + } + +metrics = SystemMetrics() + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Model warm-up on startup (non-blocking for fast server boot) + logger.info("Initializing LLM Manager and warming up model in the background...") + asyncio.create_task(llm_manager.setup_provider()) + yield + # Shutdown operations + logger.info("Server shutting down.") + +# Main app instantiation +app = FastAPI( + title="AI Coding Assistant API", + version="1.0.0", + lifespan=lifespan +) + +# Add Middleware +app.add_middleware(LoggingMiddleware) +app.add_middleware(RateLimitMiddleware, limit=settings.RATE_LIMIT_PER_MINUTE) + +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins_list, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Stream Generator Helper +async def run_stream_generator( + prompt: str, + system_prompt: str = None, + messages: list = None, + temperature: float = 0.7, + max_tokens: int = 1024, +) -> AsyncGenerator[str, None]: + start_time = time.time() + generated_content = "" + prompt_tokens = estimate_tokens(prompt) + if messages: + for m in messages: + prompt_tokens += estimate_tokens(m.get("content", "")) + + try: + stream = await llm_manager.generate_stream( + prompt=prompt, + system_prompt=system_prompt, + messages=messages, + temperature=temperature, + max_tokens=max_tokens + ) + + async for chunk in stream: + generated_content += chunk + # Format SSE yield + yield format_sse_chunk(content=chunk, done=False) + + duration = time.time() - start_time + completion_tokens = estimate_tokens(generated_content) + metrics.add(prompt_tokens, completion_tokens, duration) + + # Send final chunk with usage metrics + usage_data = { + "prompt_tokens": prompt_tokens, + "completion_tokens": completion_tokens, + "total_tokens": prompt_tokens + completion_tokens, + "duration_ms": int(duration * 1000), + "tokens_per_second": round(completion_tokens / duration, 2) if duration > 0 else 0 + } + yield format_sse_chunk(content="", done=True, usage=usage_data) + + except Exception as e: + logger.error(f"Error in running streaming generator: {e}") + yield format_sse_chunk(content=f"\n[Generation Error: {e}]", done=True) + +# Standard Non-Stream Helper +async def run_standard_generation( + prompt: str, + system_prompt: str = None, + messages: list = None, + temperature: float = 0.7, + max_tokens: int = 1024, +) -> ChatResponse: + start_time = time.time() + try: + response_data = await llm_manager.generate( + prompt=prompt, + system_prompt=system_prompt, + messages=messages, + temperature=temperature, + max_tokens=max_tokens + ) + + duration = time.time() - start_time + usage = response_data.get("usage", {}) + + prompt_tokens = usage.get("prompt_tokens", estimate_tokens(prompt)) + completion_tokens = usage.get("completion_tokens", estimate_tokens(response_data["content"])) + metrics.add(prompt_tokens, completion_tokens, duration) + + # Build enriched usage dict + metrics_dict = { + "prompt_tokens": prompt_tokens, + "completion_tokens": completion_tokens, + "total_tokens": prompt_tokens + completion_tokens, + "duration_ms": int(duration * 1000), + "tokens_per_second": round(completion_tokens / duration, 2) if duration > 0 else 0 + } + + status_info = await llm_manager.get_status_info() + + return ChatResponse( + id=str(uuid.uuid4()), + content=response_data["content"], + usage=metrics_dict, + model=status_info.get("model_id", status_info.get("model_path", "qwen-0.5b")) + ) + except Exception as e: + logger.error(f"Error in standard generation: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e) + ) + +# API ENDPOINTS +@app.post("/chat") +async def chat(request: ChatRequest): + messages_list = [m.model_dump() for m in request.messages] + + # Extract system prompt if present in payload + system_prompt = None + user_messages = [] + for msg in messages_list: + if msg["role"] == "system": + system_prompt = msg["content"] + else: + user_messages.append(msg) + + # The last user message is the primary prompt + last_prompt = user_messages[-1]["content"] if user_messages else "" + + if request.stream: + return StreamingResponse( + run_stream_generator( + prompt=last_prompt, + system_prompt=system_prompt, + messages=messages_list, + temperature=request.temperature or settings.DEFAULT_TEMPERATURE, + max_tokens=request.max_tokens or settings.DEFAULT_MAX_TOKENS + ), + media_type="text/event-stream" + ) + else: + return await run_standard_generation( + prompt=last_prompt, + system_prompt=system_prompt, + messages=messages_list, + temperature=request.temperature or settings.DEFAULT_TEMPERATURE, + max_tokens=request.max_tokens or settings.DEFAULT_MAX_TOKENS + ) + +@app.post("/complete") +async def complete(request: CompletionRequest): + prompt = get_completion_prompt(request.prefix, request.suffix, request.language or "python") + + # We want low temperature for completion tasks + temp = request.temperature or 0.2 + max_t = request.max_tokens or 128 + + if request.stream: + return StreamingResponse( + run_stream_generator(prompt=prompt, temperature=temp, max_tokens=max_t), + media_type="text/event-stream" + ) + else: + return await run_standard_generation(prompt=prompt, temperature=temp, max_tokens=max_t) + +# Helper decorator for modular code API endpoints +def make_code_endpoint(action: str): + async def endpoint(request: CodeRequest): + prompt = get_code_prompt(action, request.code, request.language or "python", request.context) + temp = request.temperature or settings.DEFAULT_TEMPERATURE + max_t = request.max_tokens or settings.DEFAULT_MAX_TOKENS + + if request.stream: + return StreamingResponse( + run_stream_generator(prompt=prompt, temperature=temp, max_tokens=max_t), + media_type="text/event-stream" + ) + else: + return await run_standard_generation(prompt=prompt, temperature=temp, max_tokens=max_t) + return endpoint + +# Register code utility endpoints +app.post("/explain")(make_code_endpoint("explain")) +app.post("/debug")(make_code_endpoint("debug")) +app.post("/refactor")(make_code_endpoint("refactor")) +app.post("/generate-tests")(make_code_endpoint("generate-tests")) +app.post("/summarize")(make_code_endpoint("summarize")) + +@app.get("/health") +async def health(): + # Health check responds instantly for cloud pingers + status_info = await llm_manager.get_status_info() + return { + "status": "healthy", + "timestamp": time.time(), + "active_mode": status_info.get("active_mode"), + "local_model_loaded": status_info.get("initialized", False), + "is_downloading": status_info.get("is_downloading", False) + } + +@app.get("/metrics") +async def get_metrics(): + # Enriches metrics with current server state + report = metrics.get_metrics_report() + status_info = await llm_manager.get_status_info() + ram_stats = psutil.virtual_memory() + report.update({ + "active_mode": "local", + "system_ram_gb": round(ram_stats.total / (1024 ** 3), 2), + "device": status_info.get("device", "CPU") + }) + return report + +@app.get("/model-info", response_model=ModelInfoResponse) +async def get_model_info(): + status_info = await llm_manager.get_status_info() + + # Calculate memory stats + ram_stats = psutil.virtual_memory() + total_gb = ram_stats.total / (1024 ** 3) + used_gb = ram_stats.used / (1024 ** 3) + + local_exists = os.path.exists(settings.LOCAL_MODEL_PATH) + + return ModelInfoResponse( + model_name=os.path.basename(settings.LOCAL_MODEL_PATH), + inference_mode="local", + status="loaded" if status_info.get("initialized") else "loading", + memory_usage_gb=round(used_gb, 2), + total_memory_gb=round(total_gb, 2), + local_model_exists=local_exists, + local_model_path=settings.LOCAL_MODEL_PATH, + device=status_info.get("device", "CPU") + ) + +# Serve Frontend static assets if available (production mode) +frontend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "frontend", "dist")) +if os.path.exists(frontend_dir): + app.mount("/", StaticFiles(directory=frontend_dir, html=True), name="frontend") + logger.info(f"Frontend dist found. Serving frontend from {frontend_dir}") +else: + logger.warning(f"Frontend dist not found at {frontend_dir}. Running API-only server mode.") diff --git a/backend/app/middleware.py b/backend/app/middleware.py new file mode 100644 index 0000000000000000000000000000000000000000..f7bb9a44f6ec5719fb0e55cc3fd407b711233df0 --- /dev/null +++ b/backend/app/middleware.py @@ -0,0 +1,73 @@ +import time +import logging +from collections import defaultdict +from fastapi import Request, Response, status +from fastapi.responses import JSONResponse +from starlette.middleware.base import BaseHTTPMiddleware +from backend.app.config import settings + +logger = logging.getLogger(__name__) + +class RateLimiter: + def __init__(self, requests_per_minute: int): + self.limit = requests_per_minute + self.clients = defaultdict(list) + + def is_allowed(self, ip: str) -> bool: + now = time.time() + # Clean up requests older than 60 seconds + self.clients[ip] = [req_time for req_time in self.clients[ip] if now - req_time < 60] + + if len(self.clients[ip]) >= self.limit: + return False + + self.clients[ip].append(now) + return True + +class RateLimitMiddleware(BaseHTTPMiddleware): + def __init__(self, app, limit: int = None): + super().__init__(app) + self.limiter = RateLimiter(limit or settings.RATE_LIMIT_PER_MINUTE) + + async def dispatch(self, request: Request, call_next) -> Response: + # Bypass rate limits for health and metric endpoints + if request.url.path in ["/health", "/metrics", "/model-info"]: + return await call_next(request) + + # Get client IP + client_ip = request.client.host if request.client else "unknown" + + if not self.limiter.is_allowed(client_ip): + logger.warning(f"Rate limit exceeded for client: {client_ip} on path {request.url.path}") + return JSONResponse( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + content={"detail": "Too many requests. Please try again in a minute."} + ) + + return await call_next(request) + +class LoggingMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next) -> Response: + start_time = time.time() + client_ip = request.client.host if request.client else "unknown" + logger.info(f"Incoming: {request.method} {request.url.path} from {client_ip}") + + try: + response = await call_next(request) + duration = time.time() - start_time + logger.info( + f"Outgoing: {request.method} {request.url.path} - " + f"Status: {response.status_code} - Duration: {duration:.4f}s" + ) + return response + except Exception as e: + duration = time.time() - start_time + logger.error( + f"Exception: {request.method} {request.url.path} failed - " + f"Error: {e} - Duration: {duration:.4f}s", + exc_info=True + ) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={"detail": "Internal server error occurred."} + ) diff --git a/backend/app/models.py b/backend/app/models.py new file mode 100644 index 0000000000000000000000000000000000000000..1e3dd53a44dc2444088d099b096f2d4a33004b49 --- /dev/null +++ b/backend/app/models.py @@ -0,0 +1,50 @@ +from typing import List, Optional, Dict, Any +from pydantic import BaseModel, Field + +class ChatMessage(BaseModel): + role: str = Field(..., description="Role of the message author (system, user, assistant)") + content: str = Field(..., description="Content of the message") + +class ChatRequest(BaseModel): + messages: List[ChatMessage] = Field(..., description="Conversation history including current prompt") + temperature: Optional[float] = Field(None, description="Controls randomness (0.0 to 1.0)") + max_tokens: Optional[int] = Field(None, description="Maximum number of tokens to generate") + top_p: Optional[float] = Field(None, description="Nucleus sampling threshold") + stream: Optional[bool] = Field(True, description="Whether to stream responses") + +class CodeRequest(BaseModel): + code: str = Field(..., description="The code snippet to process") + language: Optional[str] = Field("python", description="Programming language of the code") + context: Optional[str] = Field(None, description="Additional developer instructions or query context") + temperature: Optional[float] = Field(None) + max_tokens: Optional[int] = Field(None) + stream: Optional[bool] = Field(True) + +class CompletionRequest(BaseModel): + prefix: str = Field(..., description="Code before the cursor position") + suffix: Optional[str] = Field("", description="Code after the cursor position") + language: Optional[str] = Field("python") + max_tokens: Optional[int] = Field(128) + temperature: Optional[float] = Field(0.2) + stream: Optional[bool] = Field(False) + +class ChatResponseChunk(BaseModel): + content: str + done: bool = False + usage: Optional[Dict[str, Any]] = None + +class ChatResponse(BaseModel): + id: str + content: str + usage: Dict[str, Any] + model: str + +class ModelInfoResponse(BaseModel): + model_name: str + inference_mode: str # "local" or "huggingface" + status: str # "loaded", "error", "fallback" + memory_usage_gb: float + total_memory_gb: float + local_model_exists: bool + local_model_path: str + device: str # "cpu", "cuda", etc. diff --git a/backend/app/utils.py b/backend/app/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..277799583510752437b2274693ea5be419be9b33 --- /dev/null +++ b/backend/app/utils.py @@ -0,0 +1,83 @@ +import time +from typing import Dict, Any, List, Optional + +def estimate_tokens(text: str) -> int: + """ + Estimates token count for Qwen/GPT-like models without heavy tokenizers. + Rule of thumb: 1 token ≈ 4 characters, or ~1.3 tokens per word. + """ + if not text: + return 0 + # Average of char-based and word-based estimations + char_est = len(text) / 4.0 + word_est = len(text.split()) * 1.3 + return int((char_est + word_est) / 2.0) + 1 + +def format_sse_chunk(content: str, done: bool = False, usage: Optional[Dict[str, Any]] = None) -> str: + """Format data for Server-Sent Events transmission.""" + data = { + "content": content, + "done": done, + "usage": usage + } + return f"data: {json_dumps(data)}\n\n" + +def json_dumps(obj: Any) -> str: + # A safe helper for JSON serialization + import json + return json.dumps(obj) + +def get_code_prompt(action: str, code: str, language: str, context: Optional[str] = None) -> str: + """Returns specialized system/user prompts for code tasks.""" + prompt_templates = { + "explain": ( + "You are an expert software engineer. Explain this code step-by-step. " + "Highlight the flow of execution, core algorithms, and potential performance implications. " + "Write the explanation in clear, readable markdown.\n\n" + f"Language: {language}\n" + f"Code:\n```\n{code}\n```" + ), + "debug": ( + "You are a senior debugger. Identify errors, logical bugs, edge cases, memory leaks, " + "or security vulnerabilities in the code below. Explain each issue found and " + "provide a corrected version of the code, indicating what changes were made.\n\n" + f"Language: {language}\n" + f"Code:\n```\n{code}\n```" + ), + "refactor": ( + "You are a principal engineer. Refactor this code to improve its readability, " + "efficiency, and modularity. Adhere to SOLID principles and industry design patterns. " + "Provide the refactored code and list the improvements.\n\n" + f"Language: {language}\n" + f"Code:\n```\n{code}\n```" + ), + "generate-tests": ( + "You are a QA automation lead. Write comprehensive unit tests for the following code snippet. " + "Cover typical inputs, edge cases, and error conditions. Use standard testing libraries.\n\n" + f"Language: {language}\n" + f"Code:\n```\n{code}\n```" + ), + "summarize": ( + "Provide a brief, high-level summary of what this code does in 2-3 sentences. " + "Focus on the main inputs, transformations, and outputs.\n\n" + f"Language: {language}\n" + f"Code:\n```\n{code}\n```" + ) + } + + prompt = prompt_templates.get(action, f"Analyze the following code:\n\n```{language}\n{code}\n```") + if context: + prompt = f"Context/Instructions: {context}\n\n{prompt}" + return prompt + +def get_completion_prompt(prefix: str, suffix: str, language: str) -> str: + """Constructs instructions for code completion.""" + return ( + f"You are a code auto-completion utility. Continue the code for the {language} language. " + "Your task is to fill in the missing code between the Prefix and Suffix. " + "Return ONLY the code that should be inserted directly at the transition point. " + "Do NOT write any explanations. Do NOT wrap your response in markdown code blocks.\n\n" + f"--- PREFIX ---\n{prefix}\n" + f"--- SUFFIX ---\n{suffix}\n" + "--- INSERT COMPLETED CODE BELOW ---" + ) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..83d16c2251c1c6bb9abca1bde0073df70d02ce39 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,10 @@ +fastapi>=0.100.0 +uvicorn>=0.22.0 +pydantic>=2.0 +pydantic-settings>=2.0 +python-dotenv>=1.0.0 +requests>=2.31.0 +httpx>=0.24.0 +huggingface_hub>=0.16.0 +psutil>=5.9.0 +llama-cpp-python>=0.2.0 diff --git a/backend/run.py b/backend/run.py new file mode 100644 index 0000000000000000000000000000000000000000..376371ccf948d2331317ca6f427247ff65eb7503 --- /dev/null +++ b/backend/run.py @@ -0,0 +1,11 @@ +import uvicorn +from backend.app.config import settings + +if __name__ == "__main__": + print(f"Starting server on http://{settings.HOST}:{settings.PORT}") + uvicorn.run( + "backend.app.main:app", + host=settings.HOST, + port=settings.PORT, + reload=settings.DEBUG + ) diff --git a/docker/Dockerfile.backend b/docker/Dockerfile.backend new file mode 100644 index 0000000000000000000000000000000000000000..91e2f891f66b59c5b6f18aefb892b557ec80a7b9 --- /dev/null +++ b/docker/Dockerfile.backend @@ -0,0 +1,45 @@ +# Multi-stage build to reduce final image size +FROM python:3.11-slim as builder + +WORKDIR /app + +# Install compilation tools needed for compiling llama-cpp-python +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + gcc \ + g++ \ + make \ + python3-dev \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Copy backend requirements +COPY backend/requirements.txt . + +# Install dependencies and build llama-cpp-python +RUN pip install --no-cache-dir --user -r requirements.txt + +# Final production stage +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libgomp1 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy installed site-packages from builder stage +COPY --from=builder /root/.local /root/.local +ENV PATH=/root/.local/bin:$PATH + +# Copy backend codebase +COPY backend/ /app/backend/ + +ENV PORT=8000 +ENV HOST=0.0.0.0 +ENV PYTHONPATH=/app + +EXPOSE 8000 + +# Start command +CMD ["python", "backend/run.py"] diff --git a/docker/Dockerfile.frontend b/docker/Dockerfile.frontend new file mode 100644 index 0000000000000000000000000000000000000000..0de6b83e398dc633e83817087829e0e7f7928908 --- /dev/null +++ b/docker/Dockerfile.frontend @@ -0,0 +1,13 @@ +FROM node:20-slim + +WORKDIR /app + +COPY frontend/package*.json ./ + +RUN npm install + +COPY frontend/ ./ + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..97694ea396f7e218b3972d8da86d6da6ad8cfe04 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,36 @@ +version: '3.8' + +services: + backend: + build: + context: ../ + dockerfile: docker/Dockerfile.backend + ports: + - "8000:8000" + volumes: + - ../backend:/app/backend + - ../models:/app/models + environment: + - PORT=8000 + - HOST=0.0.0.0 + - DEBUG=true + - INFERENCE_MODE=local + - LOCAL_MODEL_PATH=models/SmolLM2-360M-Instruct-Q4_K_M.gguf + - CORS_ORIGINS=http://localhost:5173,http://localhost:8000 + restart: unless-stopped + + frontend: + build: + context: ../ + dockerfile: docker/Dockerfile.frontend + ports: + - "5173:5173" + volumes: + - ../frontend:/app + - /app/node_modules + environment: + - VITE_API_URL=http://backend:8000 + command: npm run dev -- --host --force + depends_on: + - backend + restart: unless-stopped diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000000000000000000000000000000000000..9e1a86e9991c40b762d832d5e3c388770127ea73 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,109 @@ +# API Reference Documentation 📖 + +This document details the REST API specifications for the Antigravity Coding Assistant. + +All APIs use JSON request bodies. Streaming endpoints support Server-Sent Events (`text/event-stream`). + +--- + +## Endpoints Summary + +| Method | Path | Description | Streaming Support | +| :--- | :--- | :--- | :--- | +| `POST` | `/chat` | Chat message handler | Yes | +| `POST` | `/complete` | Code auto-completion | Yes | +| `POST` | `/explain` | Generates explanation for a code block | Yes | +| `POST` | `/debug` | Locates bugs and provides fixes | Yes | +| `POST` | `/refactor` | Refactors code for quality & SOLID rules | Yes | +| `POST` | `/generate-tests`| Creates automated unit tests | Yes | +| `POST` | `/summarize` | Summarizes code in 2-3 sentences | Yes | +| `GET` | `/health` | Check backend service health | No | +| `GET` | `/metrics` | Get performance metrics telemetry | No | +| `GET` | `/model-info` | Check active LLM provider specifications| No | + +--- + +## Detailed Specifications + +### 1. POST `/chat` +Generates conversational assistance based on prompt history. + +**Request Payload:** +```json +{ + "messages": [ + { "role": "system", "content": "You are a coding assistant." }, + { "role": "user", "content": "Write a bubble sort in Python." } + ], + "temperature": 0.7, + "max_tokens": 1024, + "top_p": 0.9, + "stream": true +} +``` + +--- + +### 2. POST `/complete` +Fills in missing code between a prefix and suffix (Fill-in-the-Middle). + +**Request Payload:** +```json +{ + "prefix": "def add_numbers(a, b):\n ", + "suffix": "\n\nprint(add_numbers(5, 10))", + "language": "python", + "max_tokens": 64, + "temperature": 0.1, + "stream": false +} +``` + +--- + +### 3. POST `/explain` | `/debug` | `/refactor` | `/generate-tests` | `/summarize` +Specialized endpoints for code-focused instructions. + +**Request Payload:** +```json +{ + "code": "def double(x): return x * 2", + "language": "python", + "context": "Optimize this code", + "temperature": 0.7, + "max_tokens": 1024, + "stream": true +} +``` + +--- + +## Response Formats + +### Non-Streaming Response +Returned when `stream` parameter is `false`: +```json +{ + "id": "uuid-string-here", + "content": "Generated text or code review block here", + "usage": { + "prompt_tokens": 42, + "completion_tokens": 128, + "total_tokens": 170, + "duration_ms": 1240, + "tokens_per_second": 103.2 + }, + "model": "bartowski/Qwen2.5-Coder-0.5B-Instruct-GGUF" +} +``` + +### Streaming Response (SSE) +Returned when `stream` parameter is `true`. Standard SSE format: +``` +data: {"content": "Hello", "done": false} + +data: {"content": " World", "done": false} + +data: {"content": "", "done": true, "usage": {"prompt_tokens": 12, "completion_tokens": 2, "total_tokens": 14, "duration_ms": 230, "tokens_per_second": 8.7}} +``` +Each data line represents a single generated text token. The final SSE chunk contains `done: true` and the generation metrics. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000000000000000000000000000000000000..05994ffaba114d2d499e8bdb4513ee581749969f --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,47 @@ +# Cloud Deployment Guide 🌐 + +This document covers instructions on deploying the unified **Antigravity AI Coder** container to Railway and Render. + +--- + +## 🚄 Railway Deployment (Preferred) + +Railway is the recommended host because of its native support for Dockerfile builds, fast container deployments, and reliable persistent volumes. + +### Step 1: Create a Railway Project +1. Log into your [Railway Console](https://railway.app/). +2. Click **New Project** -> **Deploy from GitHub repo**. +3. Select your repository. + +### Step 2: Configure Environment Variables +Add the following variables in Railway's **Variables** tab: +* `PORT` = `8000` +* `HOST` = `0.0.0.0` +* `INFERENCE_MODE` = `auto` (Routes to Hugging Face Cloud if container RAM is limited) +* `HF_API_TOKEN` = `your_huggingface_api_token` (Strongly recommended to avoid rate limits) +* `HF_MODEL_ID` = `Qwen/Qwen2.5-Coder-0.5B-Instruct` +* `SECRET_KEY` = `generate-a-long-random-string` + +### Step 3: Mount a Persistent Volume (Optional but Recommended) +To prevent the container from re-downloading the 397MB local model file on every restart: +1. In the service settings, click **Volume** -> **Add Volume**. +2. Mount the volume to: `/app/models`. +3. Save the changes. Railway will now persist the downloaded GGUF file inside this volume. + +--- + +## 💎 Render Deployment + +Render supports Docker builds out of the box using our blueprint `render.yaml` configuration. + +### Step 1: Deploy using render.yaml Blueprint +1. Log into your [Render Dashboard](https://dashboard.render.com/). +2. Click **New** -> **Blueprint**. +3. Link your repository. Render will automatically read the `render.yaml` file from your repo root. +4. Render will prompt you for: + * **Service Name:** `antigravity-ai-coder` + * **HF_API_TOKEN:** Provide your Hugging Face API key. + +### Step 2: Custom Scaling & Memory Fallback +* **On Free Instances:** Render's free tier provides 512MB RAM. Since 512MB is below our local inference threshold, the backend will boot up, detect the system memory limit, and automatically switch to `huggingface` cloud fallback mode. +* **On Paid Instances:** If you upgrade to a Starter or Standard instance (>= 2GB RAM) and keep `INFERENCE_MODE=auto`, it will download and run the GGUF model locally. The blueprint allocates a 10GB persistent disk mounted to `/app/models` to store the model file. diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a547bf36d8d11a4f89c59c144f24795749086dd1 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..6fa991dad24b57eb19c9cefba8ea12fd1870ed07 --- /dev/null +++ b/frontend/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d6af7e39ffac6183b891838d6ab1d6eb7ac70dda --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,32 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the Oxlint configuration + +If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`: + +```json +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "options": { + "typeAware": true + }, + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} +``` + +See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories. diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000000000000000000000000000000000000..ce91b4a63e470aa98f206d6a3904034288734320 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + + Antigravity AI Coder | Qwen2.5-Coder Assistant + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..79c8a45252536dd28e1fbb1a1a4f797bb354ffa5 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1909 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@tailwindcss/vite": "^4.3.2", + "@tanstack/react-query": "^5.101.2", + "clsx": "^2.1.1", + "framer-motion": "^12.42.2", + "lucide-react": "^1.23.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.1", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4.3.2" + }, + "devDependencies": { + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "oxlint": "^1.71.0", + "typescript": "~6.0.2", + "vite": "^8.1.1" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.72.0.tgz", + "integrity": "sha512-zhCmvn+1Mj3UchAc/90i99S0t7jJUsHmFVSPg4UWrjO8b8eaSGwscgO6QAUtvHBstkjQwBttQNswEnAF1mIQdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.72.0.tgz", + "integrity": "sha512-mtH+aY/ozv1eZoCUC2owjFAtyNBKHpJHygKeEu9zXXnQGW1Q2/qOpvx+I+Lf23+TvTz66F4iiXUbl2cGvoLPCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.72.0.tgz", + "integrity": "sha512-EvnajNPDtfknB3ZieeOOyDTwJn9QXDiwfnF4ZDQqART6RG6hjY4WigQcZdGoK2dkB3e1vrmEzN9aYbQCUkh/gQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.72.0.tgz", + "integrity": "sha512-ZkCdEa/G80A7vEHfeCDz/+L3m33DE73v32mDKhgOIgz8Uwf0DFcK7+uu6qC+7LEhmz5fpOe1osWKyjSNMydFIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.72.0.tgz", + "integrity": "sha512-NroXv2vh+sxVY1uya/rM5pjhx1hm8BzlYpx9q67QP0Xhw5MH2bf5GJylpvLEC+781p1Xli/317EoV9AlGwViag==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.72.0.tgz", + "integrity": "sha512-0NDywYgfj279Ou/BcQuCYSj7NJwBfmWn5qc5uGO/Ny7fUWmXyIpvawqX/8acQlWG6IXelJsJhj+JAy6sjsKj0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.72.0.tgz", + "integrity": "sha512-4vpXB06h65Ezsy4hRyrGjGrfa1SkVPii09yaajiYhmVpgsFiLD+KNxIx/BNAY+XiO+i1yqp9HHdwqM8VTqa5XQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.72.0.tgz", + "integrity": "sha512-immaN4g2ZGFiOkKrvRX9LvzZdd2GkQM5wR+UyzYyUuyhUTXGQ4HKUJH18xp4G8OfhCVaVAJfKZxwE1r8+4hhaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.72.0.tgz", + "integrity": "sha512-JGHS9Mnr7iWyyLDxgCv1MhzVpAckgptg00F2gnxt/GD7lQ2SW1BRcxHqhSTaSdDpjWRrBkBxMMh4+Hn3aVtExg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.72.0.tgz", + "integrity": "sha512-AOYgBZqxNshrg83P9v0RYv+m8s10Cqkj4/PxXFDhcS3k7FqsIG5+CxErshZCIN7G8iy4Y+VGfAsuEdar8AcbBg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.72.0.tgz", + "integrity": "sha512-QMybPS5ij3/vrKG67mqzHwW++91sYxK/PPUVi6SBtNCEzW4niS52fVBdXbQ6nou0wWbUPEpx8Sl/ZjtgE3clXA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.72.0.tgz", + "integrity": "sha512-gOc3W7JV0PXRpIL7stUlLe3Wa9Gp0Kdlup87IT3gHDvPKck2xNgMIl/Gs2lldYY2lyXZDC4rWi3hmoLUobkgbQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.72.0.tgz", + "integrity": "sha512-rpGxph+FjjHcYI5q6uxB3Az+tnfmEnDbSA8+PK9ZE/VzyUAkvBOMeuY7ZQMhu5mpZH7YQDsTdW6Cx4kV/msc6w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.72.0.tgz", + "integrity": "sha512-WND+uhf/Ko13SLqQMWQUgsZuLvYYEvL0ZKgg0tgGYfLqxG7l8Ju123fHDMJyYSDl5E3bUbpFUuii/OvMreFQzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.72.0.tgz", + "integrity": "sha512-SrpbrUL70nG9vh6zP4/oKHWgLuHquwsr7MW9XOn0olBVgh10Uqr8qscKhQoBGEn6olK/IUpn5GSKcdQ5AjUhGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.72.0.tgz", + "integrity": "sha512-qkrsEn6NmgFKr7U/QnezQMb+q/vzAy0Dd9Y95gQGQTyjzDLN+HRZMuM5u70iyH4nBLCfKBzhjMsYCehKay2jyg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.72.0.tgz", + "integrity": "sha512-LWR6ZlFZph+KPjXv8opgZsXRDCdrdQe8VL8Cg9zxCoBS73h6znzZpydVgmdnwj8mB9AuSM5jxEgDJDpQkjboeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.72.0.tgz", + "integrity": "sha512-yt6HEh7IsHvtjRWtmeZRX134eaXKHq5Gnqlf1xBJdJl1JtdoRUEJw3nAxpZoUDS860cX/foKbztO441anVBtVQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.72.0.tgz", + "integrity": "sha512-b2eKFD2hX7tIwmo/cyH6TDq8vzWRZ2qNHrzoGntUTmq0h3zQh/uX3eTSHCwI8OB/ADQfJCRelLItK8BsxuucDA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", + "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "tailwindcss": "4.3.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz", + "integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.2.tgz", + "integrity": "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/framer-motion": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", + "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.42.2", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lucide-react": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz", + "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, + "node_modules/motion-dom": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", + "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oxlint": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.72.0.tgz", + "integrity": "sha512-1rhdZIP/EvoI91ABIwNU5Q8+bWf8mjrS5UzIOZld4d4bXxJvtlUhlQvaoTogIGin/qdErMOrwaIJvCSIAKTLhA==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.72.0", + "@oxlint/binding-android-arm64": "1.72.0", + "@oxlint/binding-darwin-arm64": "1.72.0", + "@oxlint/binding-darwin-x64": "1.72.0", + "@oxlint/binding-freebsd-x64": "1.72.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.72.0", + "@oxlint/binding-linux-arm-musleabihf": "1.72.0", + "@oxlint/binding-linux-arm64-gnu": "1.72.0", + "@oxlint/binding-linux-arm64-musl": "1.72.0", + "@oxlint/binding-linux-ppc64-gnu": "1.72.0", + "@oxlint/binding-linux-riscv64-gnu": "1.72.0", + "@oxlint/binding-linux-riscv64-musl": "1.72.0", + "@oxlint/binding-linux-s390x-gnu": "1.72.0", + "@oxlint/binding-linux-x64-gnu": "1.72.0", + "@oxlint/binding-linux-x64-musl": "1.72.0", + "@oxlint/binding-openharmony-arm64": "1.72.0", + "@oxlint/binding-win32-arm64-msvc": "1.72.0", + "@oxlint/binding-win32-ia32-msvc": "1.72.0", + "@oxlint/binding-win32-x64-msvc": "1.72.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=0.22.1", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-router": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rolldown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.138.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000000000000000000000000000000000000..1810b9c364b091dbcc2e168d042271e3f477e390 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,34 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@tailwindcss/vite": "^4.3.2", + "@tanstack/react-query": "^5.101.2", + "clsx": "^2.1.1", + "framer-motion": "^12.42.2", + "lucide-react": "^1.23.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.1", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4.3.2" + }, + "devDependencies": { + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "oxlint": "^1.71.0", + "typescript": "~6.0.2", + "vite": "^8.1.1" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..6893eb13237060adc0c968a690149a49faa2d7d3 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000000000000000000000000000000000000..e9522193d9f796a9748e9ad8c952a5df73c87db9 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000000000000000000000000000000000000..f90339d8f765fa2c69d9a341959a8ddb9fff5720 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1ecf1816180f058bce43e2ad613d61bc5264850e --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,193 @@ +import React, { useState } from 'react'; +import { AnimatePresence, motion } from 'framer-motion'; +import { useChat } from './hooks/useChat'; +import { Sidebar } from './components/Sidebar'; +import { Navbar } from './components/Navbar'; +import { Dashboard } from './components/Dashboard'; +import { ChatInterface } from './components/ChatInterface'; +import { CodePlayground } from './components/CodePlayground'; +import { Settings } from './components/Settings'; +import { AboutPage } from './components/AboutPage'; + +export const App: React.FC = () => { + const { + conversations, + activeConversationId, + setActiveConversationId, + activeConversation, + settings, + modelInfo, + metrics, + isLoading, + error, + updateSettings, + createNewConversation, + deleteConversation, + sendMessage, + executeCodeAction, + clearHistory, + } = useChat(); + + const [activeTab, setActiveTab] = useState('dashboard'); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); + + const selectConversationFromSidebar = (id: string) => { + setActiveConversationId(id); + setActiveTab('chat'); + setMobileSidebarOpen(false); + }; + + const selectTabFromNav = (tab: string) => { + setActiveTab(tab); + setMobileSidebarOpen(false); + }; + + return ( +
+ + {/* 1. Sidebar Navigation (Desktop only, hidden on lg screens down) */} +
+ +
+ + {/* 2. Responsive Overlay Drawer Sidebar (Mobile / Tablet) */} + + {mobileSidebarOpen && ( + + + {}} + /> + {/* Close trigger overlay inside the sidebar */} + + + {/* Backdrop shadow space clicking closer */} +
setMobileSidebarOpen(false)}>
+
+ )} +
+ + {/* 3. Main content frame (Contains sticky header and tabs) */} +
+ setMobileSidebarOpen(true)} + /> + +
+ + + {activeTab === 'dashboard' && ( + + )} + + {activeTab === 'chat' && ( + + )} + + {activeTab === 'playground' && ( + + )} + + {activeTab === 'settings' && ( + + )} + + {activeTab === 'about' && ( + + )} + + {/* Placeholder tabs for nav items not yet implemented */} + {['history', 'documents', 'snippets', 'models'].includes(activeTab) && ( +
+
+ +
+
+

{activeTab}

+

This section is coming soon. Core chat and playground features are fully available.

+
+ +
+ )} +
+
+
+
+
+ ); +}; + +export default App; diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb Binary files /dev/null and b/frontend/src/assets/hero.png differ diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000000000000000000000000000000000000..6c87de9bb3358469122cc991d5cf578927246184 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 0000000000000000000000000000000000000000..5101b674df391399da71c767aa5c976426c9dc7a --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/components/AboutPage.tsx b/frontend/src/components/AboutPage.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fe1aaac22fa4412a64311158eabf31ce3d371708 --- /dev/null +++ b/frontend/src/components/AboutPage.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { Info, Cpu, Code, BookOpen, HardDrive, Share2 } from 'lucide-react'; + +export const AboutPage: React.FC = () => { + return ( +
+
+ + + +
+

About Antigravity AI Coder

+

Learn about the architecture and technologies running under the hood.

+
+
+ +
+
+

+ + Hybrid Local/Cloud Architecture +

+

+ Antigravity Coder is built on a cascading inference pipeline. It is configured to run the state-of-the-art Qwen2.5-Coder-0.5B-Instruct model locally in GGUF format using llama-cpp-python. +

+

+ If the application is deployed on cloud hosts with restricted memory boundaries (e.g. Render Free, Railway Starter), the backend automatically and transparently switches to the Hugging Face Serverless Inference API, guaranteeing high-performance execution without needing a local GPU or extensive memory budgets. +

+
+ +
+
+

+ + Qwen2.5-Coder Engine +

+

+ Leverages the Qwen2.5-Coder 0.5B Instruct model, specifically fine-tuned for code generation, mathematical logic, bug fixing, and language formatting in dozens of programming languages. +

+
+ +
+

+ + GGUF Quantization +

+

+ Quantized in Q4_K_M (4-bit quantization). Reduces the weights size to under 400MB and fits comfortably within 1.5GB system RAM, providing blazing-fast local processing speed. +

+
+ +
+

+ + Monaco Code Editor +

+

+ Integrates the VS Code core editor (Monaco), delivering built-in code formatting, syntax highlighting, search/replace, and advanced editing features directly inside the Playground. +

+
+ +
+

+ + Cloud Container Ready +

+

+ Dockerized with independent backend and frontend packages, fully prepared for automated pipeline deployment on Railway, Render, and Fly.io. +

+
+
+ +
+ Antigravity AI Coding Assistant © 2026. Made with Google DeepMind Advanced Agentic Coding. +
+
+
+ ); +}; diff --git a/frontend/src/components/ChatInterface.tsx b/frontend/src/components/ChatInterface.tsx new file mode 100644 index 0000000000000000000000000000000000000000..29fec6450f8d7a02e75a63c2fdae4e856e66db0c --- /dev/null +++ b/frontend/src/components/ChatInterface.tsx @@ -0,0 +1,460 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Send, Sparkles, AlertCircle, Copy, Check, + Upload, Terminal, HelpCircle, FileText, Download, + CornerDownLeft, Paperclip, RotateCcw, + User, Shield, MessageSquare, Plus, Search, Code, Mic, Trash2 +} from 'lucide-react'; +import type { ChatMessage, Conversation, ModelInfo } from '../types'; + +interface ChatInterfaceProps { + activeConversation: Conversation | null; + sendMessage: (msg: string) => void; + isLoading: boolean; + error: string | null; + modelInfo: ModelInfo | null; +} + +// Markdown & Code Renderer block +const MarkdownRenderer: React.FC<{ text: string }> = ({ text }) => { + const [copiedId, setCopiedId] = useState(null); + + const handleCopy = (code: string, blockId: string) => { + navigator.clipboard.writeText(code); + setCopiedId(blockId); + setTimeout(() => setCopiedId(null), 2000); + }; + + const handleDownload = (code: string, filename: string) => { + const blob = new Blob([code], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + }; + + if (!text) return null; + + const parts = text.split(/(```[\s\S]*?```)/g); + + return ( +
+ {parts.map((part, idx) => { + if (part.startsWith('```')) { + const lines = part.split('\n'); + const firstLine = lines[0].replace('```', '').trim(); + const language = firstLine || 'code'; + const code = lines.slice(1, -1).join('\n'); + const blockId = `${idx}-${language}`; + const filename = `code-snippet.${language === 'python' ? 'py' : language === 'javascript' ? 'js' : language === 'typescript' ? 'ts' : 'txt'}`; + + const codeLines = code.split('\n'); + + return ( +
+ {/* Header Titlebar */} +
+ + + {language} + +
+ + +
+
+ + {/* Editor area with line numbers */} +
+
+ {codeLines.map((_, i) => ( +
{i + 1}
+ ))} +
+
+                  {code}
+                
+
+
+ ); + } else { + const lines = part.split('\n'); + return ( +
+ {lines.map((line, lIdx) => { + const trimmed = line.trim(); + + if (trimmed.startsWith('* ') || trimmed.startsWith('- ')) { + return ( +
    +
  • {renderInlineStyles(trimmed.substring(2))}
  • +
+ ); + } + + if (trimmed.startsWith('#')) { + const level = (trimmed.match(/^#+/) || [''])[0].length; + const headerText = trimmed.replace(/^#+\s*/, ''); + const headerClasses = level === 1 + ? 'text-lg font-bold my-3 text-foreground tracking-tight border-b border-[#1d1b2e] pb-1.5' + : level === 2 + ? 'text-base font-bold my-2 text-foreground tracking-tight' + : 'text-sm font-semibold my-2 text-foreground'; + return
{renderInlineStyles(headerText)}
; + } + + return line ?

{renderInlineStyles(line)}

:
; + })} +
+ ); + } + })} +
+ ); +}; + +const renderInlineStyles = (text: string) => { + const parts = text.split(/(\*\*.*?\*\*|`.*?`)/g); + return parts.map((part, idx) => { + if (part.startsWith('**') && part.endsWith('**')) { + return {part.slice(2, -2)}; + } else if (part.startsWith('`') && part.endsWith('`')) { + return {part.slice(1, -1)}; + } + return part; + }); +}; + +export const ChatInterface: React.FC = ({ + activeConversation, + sendMessage, + isLoading, + error, + modelInfo, +}) => { + const [input, setInput] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const messagesEndRef = useRef(null); + const textareaRef = useRef(null); + + // Hardcoded mock categories matching the reference screen design + const conversationsMock = { + today: [ + { id: '1', title: 'Explain asyncio.gather', active: true }, + { id: '2', title: 'React useMemo vs useCallback' }, + ], + yesterday: [ + { id: '3', title: 'SQL indexes best practices' }, + { id: '4', title: 'Python list comprehension' }, + ], + last7days: [ + { id: '5', title: 'Fix Memory Leak' }, + { id: '6', title: 'JWT authentication flow' }, + { id: '7', title: 'Docker multi-stage build' }, + ] + }; + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [activeConversation?.messages, isLoading]); + + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = `${Math.min(120, textareaRef.current.scrollHeight)}px`; + } + }, [input]); + + const handleSend = () => { + if (!input.trim() || isLoading) return; + sendMessage(input); + setInput(''); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + const modelName = modelInfo?.model_name.split('/').pop() || 'SmolLM2-360M-Instruct'; + + return ( +
+ + {/* Split column 1: Conversation History List (320px width) */} +
+ + {/* New Chat trigger button */} +
+ + + {/* Search bar */} +
+ setSearchQuery(e.target.value)} + placeholder="Search conversations..." + className="w-full bg-[#131121] border border-[#1d1b2e] text-xs rounded-xl pl-9.5 pr-4 py-2 outline-none text-foreground placeholder:text-[#58556f] focus:border-primary/50 focus-ring" + /> + +
+
+ + {/* Categories list */} +
+ + {/* Today */} +
+

Today

+
+ {conversationsMock.today.map((item) => ( + + ))} +
+
+ + {/* Yesterday */} +
+

Yesterday

+
+ {conversationsMock.yesterday.map((item) => ( + + ))} +
+
+ + {/* Last 7 days */} +
+

Last 7 days

+
+ {conversationsMock.last7days.map((item) => ( + + ))} +
+
+ +
+ +
+ + {/* Split column 2: Main chat scope */} +
+ + {/* Chat Header title */} +
+
+

+ {activeConversation?.title && activeConversation.messages.length > 0 + ? activeConversation.title + : 'Explain asyncio.gather'} +

+
+
+ {new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +
+
+ + {/* Messages Feed area */} +
+ {!activeConversation || activeConversation.messages.length === 0 ? ( + // Preload a clean reference conversation when empty to match screen 3 +
+ + {/* User Bubble (Right) */} +
+
+ +
+
+
+ Can you explain how asyncio.gather() works in Python? +
+
10:30 AM
+
+
+ + {/* AI Bubble (Left) */} +
+
+ +
+
+
+

+ `asyncio.gather()` is a powerful function in Python's `asyncio` library that allows you to run multiple coroutines concurrently and wait for all of them to complete. +

+

Here's how it works:

+ + + +

Key points:

+
    +
  • It runs all coroutines concurrently.
  • +
  • Returns results in the same order as the input list.
  • +
  • If `return_exceptions=True`, exceptions are returned instead of raised.
  • +
+
+
10:30 AM
+
+
+ +
+ ) : ( +
+ {activeConversation.messages.map((msg, idx) => { + const isUser = msg.role === 'user'; + return ( +
+
+ {isUser ? : } +
+ +
+
+ + + {!msg.content && isLoading && !isUser && ( +
+
+
+
+
+ )} +
+
+
+ ); + })} +
+ )} +
+
+ + {/* Error notification */} + {error && ( +
+ + Error: {error} +
+ )} + + {/* Input panel bottom container */} +
+
+ + + + +