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 */}
+ setMobileSidebarOpen(false)}
+ className="absolute top-4 right-4 p-1.5 bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground rounded-lg transition-colors cursor-pointer"
+ >
+
+
+
+ {/* 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.
+
+
selectTabFromNav('dashboard')}
+ className="px-4 py-2 bg-primary/10 hover:bg-primary/20 border border-primary/30 text-primary text-xs font-bold rounded-xl cursor-pointer transition-all"
+ >
+ Back to Dashboard
+
+
+ )}
+
+
+
+
+
+ );
+};
+
+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}
+
+
+ handleDownload(code, filename)}
+ className="flex items-center gap-1 hover:text-foreground transition-colors p-1"
+ title="Download File"
+ >
+
+
+ handleCopy(code, blockId)}
+ className="flex items-center gap-1.5 hover:text-foreground transition-colors p-1"
+ >
+ {copiedId === blockId ? (
+ <>
+
+ COPIED
+ >
+ ) : (
+ <>
+
+ COPY
+ >
+ )}
+
+
+
+
+ {/* 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 */}
+
+
sendMessage('Start a new clean chat session.')}
+ className="w-full flex items-center justify-center gap-2 py-2.5 bg-[#6344d5] hover:bg-[#6344d5]/90 text-xs text-white font-bold rounded-xl cursor-pointer shadow-sm transition-all active:scale-[0.98] glow-primary"
+ >
+
+ New Chat
+
+
+ {/* 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) => (
+
+ {item.title}
+
+ ))}
+
+
+
+ {/* Yesterday */}
+
+
Yesterday
+
+ {conversationsMock.yesterday.map((item) => (
+
+ {item.title}
+
+ ))}
+
+
+
+ {/* Last 7 days */}
+
+
Last 7 days
+
+ {conversationsMock.last7days.map((item) => (
+
+ {item.title}
+
+ ))}
+
+
+
+
+
+
+
+ {/* 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 && (
+
+ )}
+
+ {/* Input panel bottom container */}
+
+
+
+ {/* Model Status info subtext */}
+
+
+ {modelName}
+ •
+ Local Inference
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/CodePlayground.tsx b/frontend/src/components/CodePlayground.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0f33885cafacfeebffb1ddecc63fc728cb513c3c
--- /dev/null
+++ b/frontend/src/components/CodePlayground.tsx
@@ -0,0 +1,271 @@
+import React, { useState } from 'react';
+import { motion } from 'framer-motion';
+import {
+ Play, Trash2, Save, Terminal, Code2, Copy, Check, ChevronDown,
+ Maximize2, RefreshCw
+} from 'lucide-react';
+import { MonacoEditorWrapper } from './MonacoEditorWrapper';
+
+interface CodePlaygroundProps {
+ executeCodeAction: (
+ action: 'explain' | 'debug' | 'refactor' | 'generate-tests' | 'summarize',
+ code: string,
+ language: string,
+ context?: string
+ ) => Promise;
+ isLoading: boolean;
+}
+
+const LANGUAGES = [
+ { id: 'python', name: 'Python' },
+ { id: 'typescript', name: 'TypeScript' },
+ { id: 'javascript', name: 'JavaScript' },
+ { id: 'rust', name: 'Rust' },
+ { id: 'go', name: 'Go' },
+ { id: 'cpp', name: 'C++' },
+ { id: 'html', name: 'HTML' },
+ { id: 'css', name: 'CSS' },
+];
+
+const THEMES = [
+ { id: 'dracula', name: 'Dracula' },
+ { id: 'monokai', name: 'Monokai' },
+ { id: 'vs-dark', name: 'VS Dark' },
+ { id: 'github-dark', name: 'GitHub Dark' },
+];
+
+export const CodePlayground: React.FC = ({
+ executeCodeAction,
+ isLoading,
+}) => {
+ const [code, setCode] = useState(`async def fetch_data(url: str):
+ import aiohttp
+ async with aiohttp.ClientSession() as session:
+ async with session.get(url) as response:
+ return await response.json()
+
+async def main():
+ urls = [
+ "https://api.github.com/users/octocat",
+ "https://api.github.com/users/torvalds",
+ "https://api.github.com/users/gaearon"
+ ]
+
+ results = await asyncio.gather(*(fetch_data(url) for url in urls))
+
+ for user in results:
+ print(f"User: {user['login']} - {user['name']}")
+
+if __name__ == "__main__":
+ import asyncio
+ asyncio.run(main())`);
+ const [language, setLanguage] = useState('python');
+ const [theme, setTheme] = useState('dracula');
+ const [output, setOutput] = useState(`octocat - The Octocat
+torvalds - Linus Torvalds
+gaearon - Dan Abramov
+
+Process finished with exit code 0`);
+ const [activeConsoleTab, setActiveConsoleTab] = useState<'output' | 'console' | 'logs' | 'errors'>('output');
+ const [copied, setCopied] = useState(false);
+ const [statusText, setStatusText] = useState('Running main.py ...\nSuccess');
+
+ const handleRun = async () => {
+ if (!code.trim() || isLoading) return;
+ setStatusText('Running main.py ...\nExecuting local environment...');
+ try {
+ // Direct call to refactor/explain to act as Run execution
+ const result = await executeCodeAction('explain', code, language);
+ setOutput(result);
+ setStatusText('Running main.py ...\nSuccess');
+ } catch (e: any) {
+ setOutput(`Error during execution: ${e.message}`);
+ setStatusText('Running main.py ...\nFailed');
+ }
+ };
+
+ const handleClear = () => {
+ setCode('');
+ };
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(output);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ return (
+
+
+ {/* Playground Top Toolbar */}
+
+
+ {/* Left: Dropdown selectors */}
+
+
+
Language
+
+ setLanguage(e.target.value)}
+ className="bg-[#131121] border border-[#1d1b2e] text-xs text-white rounded-lg pl-3 pr-8 py-1.5 outline-none cursor-pointer focus:border-primary/50 appearance-none font-semibold min-w-[120px]"
+ >
+ {LANGUAGES.map((lang) => (
+ {lang.name}
+ ))}
+
+
+
+
+
+
+
Theme
+
+ setTheme(e.target.value)}
+ className="bg-[#131121] border border-[#1d1b2e] text-xs text-white rounded-lg pl-3 pr-8 py-1.5 outline-none cursor-pointer focus:border-primary/50 appearance-none font-semibold min-w-[120px]"
+ >
+ {THEMES.map((t) => (
+ {t.name}
+ ))}
+
+
+
+
+
+
+ {/* Right: Actions */}
+
+
+
+ Clear
+
+
alert('Code saved successfully.')}
+ >
+
+ Save
+
+
+
+ Run
+
+
+
+
+
+ {/* Main Split Layout: Editor & Outputs */}
+
+
+ {/* Left: Code Editor Container */}
+
+ {/* Tab Header bar */}
+
+
+ {/* Code Editor body */}
+
+
+
+
+ {/* Editor Status Bar Footer */}
+
+
+ Ln 1, Col 1
+ Spaces: 4
+ UTF-8
+ LF
+ {language}
+
+
+ Execution time: {isLoading ? 'Running...' : '1.24s'}
+ Memory: 45.6MB
+
+
+
+
+ {/* Right: Output Review Panel */}
+
+ {/* Output console tab headers */}
+
+
+ {(['output', 'console', 'logs', 'errors'] as const).map((tab) => (
+ setActiveConsoleTab(tab)}
+ className={`px-3.5 h-full text-xs font-semibold uppercase tracking-wider border-b-2 transition-all cursor-pointer ${
+ activeConsoleTab === tab
+ ? 'border-primary text-white bg-[#090810]/40'
+ : 'border-transparent text-muted-foreground hover:text-foreground'
+ }`}
+ >
+ {tab}
+
+ ))}
+
+
+
+
+ {copied ? : }
+
+
+
+
+ {/* Console Text display */}
+
+ {activeConsoleTab === 'output' && (
+
+
+ {output}
+
+
+ {/* Diagnostics Status line indicator */}
+
+
Console output
+
+ {statusText}
+
+
+
+ )}
+
+ {activeConsoleTab === 'console' && (
+
# Interactive local interpreter session is active
+ )}
+ {activeConsoleTab === 'logs' && (
+
[INFO] 172.18.0.3 - "GET /metrics HTTP/1.1" 200 OK
+ )}
+ {activeConsoleTab === 'errors' && (
+
No compile execution errors reported.
+ )}
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..44753237702194fc59b8d0d5490b352a37e771a6
--- /dev/null
+++ b/frontend/src/components/Dashboard.tsx
@@ -0,0 +1,257 @@
+import React from 'react';
+import { motion } from 'framer-motion';
+import {
+ MessageSquare, Code, FileText, ShieldAlert, List, RefreshCw,
+ Cpu, HardDrive, Zap, BarChart2, ChevronRight, HelpCircle, Database
+} from 'lucide-react';
+import type { ModelInfo, ModelMetrics } from '../types';
+
+interface DashboardProps {
+ modelInfo: ModelInfo | null;
+ metrics: ModelMetrics | null;
+ setActiveTab: (tab: string) => void;
+ createNewConversation: (title?: string) => void;
+ sendMessage: (msg: string) => void;
+}
+
+export const Dashboard: React.FC = ({
+ modelInfo,
+ metrics,
+ setActiveTab,
+ createNewConversation,
+ sendMessage,
+}) => {
+ const quickActions = [
+ { id: 'chat', title: 'AI Chat', desc: 'Chat with your AI assistant', icon: MessageSquare },
+ { id: 'playground', title: 'Playground', desc: 'Run code and experiments', icon: Code },
+ { id: 'explain', title: 'Explain Code', desc: 'Get detailed explanations', icon: FileText, prompt: 'Explain how this code block functions and list edge cases:\n\n' },
+ { id: 'debug', title: 'Debug Code', desc: 'Find and fix issues', icon: ShieldAlert, prompt: 'Find bugs and syntax errors in this code block:\n\n' },
+ { id: 'tests', title: 'Generate Tests', desc: 'Create unit tests', icon: List, prompt: 'Write comprehensive unit tests for this code block:\n\n' },
+ { id: 'refactor', title: 'Refactor Code', desc: 'Improve code quality', icon: RefreshCw, prompt: 'Refactor this code block to improve readability and complexity:\n\n' },
+ ];
+
+ const recentConversations = [
+ { title: 'Python async queue implementation', time: '2 mins ago' },
+ { title: 'React custom hook optimization', time: '28 mins ago' },
+ { title: 'SQL query performance tuning', time: '1 hour ago' },
+ { title: 'Debug memory leak in Node.js', time: '3 hours ago' },
+ { title: 'Implement auth with JWT', time: '5 hours ago' },
+ ];
+
+ const examplePrompts = [
+ { label: 'Explain this code', prompt: 'Explain the runtime complexity of this function:\n\n', icon: FileText },
+ { label: 'Write a Python function', prompt: 'Write a Python function to check if a string is a palindrome.', icon: Code },
+ { label: 'Debug this error', prompt: 'Explain how to fix this TypeError exception:\n\n', icon: ShieldAlert },
+ { label: 'Optimize this SQL query', prompt: 'Optimize this slow SQL query using proper indexes:\n\n', icon: Database },
+ { label: 'Generate unit tests', prompt: 'Generate unit tests for a standard user authentication class.', icon: List },
+ ];
+
+ const handleActionClick = (action: typeof quickActions[0]) => {
+ if (action.id === 'chat') {
+ createNewConversation();
+ setActiveTab('chat');
+ } else if (action.id === 'playground') {
+ setActiveTab('playground');
+ } else if (action.prompt) {
+ createNewConversation(action.title);
+ setActiveTab('chat');
+ setTimeout(() => {
+ sendMessage(action.prompt || '');
+ }, 100);
+ }
+ };
+
+ const handlePromptClick = (label: string, promptText: string) => {
+ createNewConversation(label);
+ setActiveTab('chat');
+ setTimeout(() => {
+ sendMessage(promptText);
+ }, 100);
+ };
+
+ // Safe formatting variables matching image values
+ const modelName = modelInfo?.model_name.split('/').pop() || 'SmolLM2-360M';
+ const memoryUsed = modelInfo?.memory_usage_gb || 1.53;
+ const memoryTotal = modelInfo?.total_memory_gb || 7.65;
+ const memoryPercent = Math.min(100, Math.round((memoryUsed / memoryTotal) * 100)) || 20;
+
+ const latency = metrics?.average_latency_seconds || 22.13;
+ const totalTokens = metrics?.total_tokens || 816;
+
+ return (
+
+
+ {/* Welcome Row with status badges */}
+
+
+
Welcome back, Developer 👋
+
Your AI Coding Assistant is ready to help you build, debug and ship faster.
+
+
+
+ LOCAL MODEL:
+ {modelName}
+
+
+ SYSTEM STATUS:
+
+
+ Healthy
+
+
+
+
+
+ {/* Grid: System Status & Metrics */}
+
+
+ {/* Model Status Card */}
+
+
+
+
{modelName}
+
Local Inference
+
+
+
+ {/* Memory Allocation Card with Progress Bar */}
+
+
+
+
+
{memoryUsed.toFixed(2)} GB / {memoryTotal.toFixed(2)} GB
+ {memoryPercent}%
+
+
+
+
+
+ {/* Latency Card */}
+
+
+
+
{latency.toFixed(2)} s
+
Per request
+
+
+
+ {/* Tokens Card */}
+
+
+
+
{totalTokens.toLocaleString()}
+
Total (Input + Output)
+
+
+
+
+
+ {/* Main Layout Split */}
+
+
+ {/* Left: Quick Actions Grid */}
+
+
+
Quick Actions
+
Choose a tool to get started
+
+
+
+ {quickActions.map((action, idx) => {
+ const Icon = action.icon;
+ return (
+
handleActionClick(action)}
+ className="flex items-center gap-3.5 p-4 bg-[#131121] hover:bg-[#1c1a30] border border-[#1d1b2e] hover:border-primary/40 rounded-xl text-left transition-all duration-200 cursor-pointer shadow-sm group"
+ >
+
+
+
+
+
{action.title}
+
{action.desc}
+
+
+ );
+ })}
+
+
+
+ {/* Right: Recent Conversations */}
+
+
+
Recent Conversations
+ setActiveTab('chat')}
+ className="text-[10px] font-bold text-primary hover:underline cursor-pointer"
+ >
+ View all
+
+
+
+
+ {recentConversations.map((conv, idx) => (
+
{ createNewConversation(conv.title); setActiveTab('chat'); }}
+ >
+ {conv.title}
+ {conv.time}
+
+ ))}
+
+
+
+
+
+ {/* Bottom: Example Prompts */}
+
+
+
Example Prompts
+
Try these examples to get started
+
+
+
+ {examplePrompts.map((ep, idx) => {
+ const Icon = ep.icon;
+ return (
+ handlePromptClick(ep.label, ep.prompt)}
+ className="flex items-center gap-2 px-3.5 py-2.5 bg-[#131121] hover:bg-[#1c1a30] border border-[#1d1b2e] hover:border-primary/40 text-xs text-[#eae9fc] font-medium rounded-xl transition-all cursor-pointer shadow-sm"
+ >
+
+ {ep.label}
+
+ );
+ })}
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/MonacoEditorWrapper.tsx b/frontend/src/components/MonacoEditorWrapper.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4ae77e04c3f79beffd08dcf87de8b106017d2796
--- /dev/null
+++ b/frontend/src/components/MonacoEditorWrapper.tsx
@@ -0,0 +1,88 @@
+import React, { useRef } from 'react';
+import Editor from '@monaco-editor/react';
+import type { Monaco } from '@monaco-editor/react';
+
+interface MonacoEditorWrapperProps {
+ value: string;
+ onChange: (val: string) => void;
+ language: string;
+ theme?: 'vs-dark' | 'light';
+ height?: string;
+ readOnly?: boolean;
+}
+
+export const MonacoEditorWrapper: React.FC = ({
+ value,
+ onChange,
+ language,
+ theme = 'vs-dark',
+ height = '100%',
+ readOnly = false,
+}) => {
+ const editorRef = useRef(null);
+
+ const handleEditorDidMount = (editor: any, monaco: Monaco) => {
+ editorRef.current = editor;
+
+ monaco.editor.defineTheme('antigravity-dark', {
+ base: 'vs-dark',
+ inherit: true,
+ rules: [
+ { token: 'comment', foreground: '6272a4', fontStyle: 'italic' },
+ { token: 'keyword', foreground: 'ff79c6' },
+ { token: 'string', foreground: 'f1fa8c' },
+ { token: 'number', foreground: 'bd93f9' },
+ { token: 'regexp', foreground: 'ffb86c' },
+ { token: 'type', foreground: '8be9fd' },
+ ],
+ colors: {
+ 'editor.background': '#0b0f19',
+ 'editor.foreground': '#f8f8f2',
+ 'editorLineNumber.foreground': '#4b5563',
+ 'editorLineNumber.activeForeground': '#3b82f6',
+ 'editor.lineHighlightBackground': '#1e293b50',
+ 'editorCursor.foreground': '#3b82f6',
+ },
+ });
+
+ monaco.editor.setTheme(theme === 'vs-dark' ? 'antigravity-dark' : 'light');
+ };
+
+ const handleEditorChange = (value: string | undefined) => {
+ if (value !== undefined) {
+ onChange(value);
+ }
+ };
+
+ return (
+
+
+
+ Loading editor assets...
+
+ }
+ options={{
+ readOnly,
+ minimap: { enabled: true },
+ fontSize: 14,
+ fontFamily: "'Fira Code', 'Courier New', monospace",
+ fontLigatures: true,
+ lineNumbers: 'on',
+ roundedSelection: true,
+ scrollBeyondLastLine: false,
+ automaticLayout: true,
+ tabSize: 4,
+ padding: { top: 12, bottom: 12 },
+ wordWrap: 'on',
+ }}
+ />
+
+ );
+};
diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9e3269ebc4bee5b388e9acfa4230c45350e1a757
--- /dev/null
+++ b/frontend/src/components/Navbar.tsx
@@ -0,0 +1,141 @@
+import React from 'react';
+import { motion } from 'framer-motion';
+import {
+ Bell, Search, Sun, Moon, Cpu, Globe,
+ CheckCircle2, AlertCircle, HelpCircle
+} from 'lucide-react';
+import type { ModelInfo, AppSettings } from '../types';
+
+interface NavbarProps {
+ activeTab: string;
+ modelInfo: ModelInfo | null;
+ settings: AppSettings;
+ updateSettings: (newSettings: Partial) => void;
+ onSearchClick?: () => void;
+ onOpenMobileSidebar?: () => void;
+}
+
+export const Navbar: React.FC = ({
+ activeTab,
+ modelInfo,
+ settings,
+ updateSettings,
+ onSearchClick,
+ onOpenMobileSidebar,
+}) => {
+ const getTabTitle = () => {
+ switch (activeTab) {
+ case 'dashboard': return 'Dashboard';
+ case 'chat': return 'AI Chat Console';
+ case 'playground': return 'Code Playground';
+ case 'history': return 'History';
+ case 'documents': return 'Documents';
+ case 'snippets': return 'Snippets';
+ case 'models': return 'Models';
+ case 'settings': return 'System Settings';
+ case 'about': return 'About';
+ default: return 'Dashboard';
+ }
+ };
+
+ const toggleTheme = () => {
+ updateSettings({ theme: settings.theme === 'dark' ? 'light' : 'dark' });
+ };
+
+ return (
+
+
+
+ {/* Left Section: Breadcrumb/Page Title */}
+
+ {onOpenMobileSidebar && (
+
+
+
+ )}
+ ‹
+
{getTabTitle()}
+
+
+ {/* Middle Section: Mock Search Bar */}
+
+
+
+
+ Quick search prompts...
+
+
+ ⌘ K
+
+
+
+
+ {/* Right Section: Diagnostics Status Controls */}
+
+
+ {/* Connection badge */}
+
+ {modelInfo?.status === 'loaded' ? (
+ <>
+
+
Online
+ >
+ ) : modelInfo?.status === 'loading' ? (
+ <>
+
+
Syncing
+ >
+ ) : (
+ <>
+
+
Offline
+ >
+ )}
+
+
+ {/* Model info indicator */}
+
+
+
+ {modelInfo?.model_name.split('/').pop() || 'SmolLM2-360M'}
+
+
+
+
+
+ {/* Notifications Trigger */}
+
+
+
+
+
+ {/* Theme Selector */}
+
+ {settings.theme === 'dark' ? : }
+
+
+ {/* User Profile avatar */}
+
+ AC
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/Settings.tsx b/frontend/src/components/Settings.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4b2501cc45f5c085a6abe1abc961bfd91e8dec7f
--- /dev/null
+++ b/frontend/src/components/Settings.tsx
@@ -0,0 +1,337 @@
+import React, { useState } from 'react';
+import {
+ Cpu, Sliders, AlertTriangle, ShieldCheck, Save, CheckCircle2,
+ HardDrive, Info, Activity, RefreshCw
+} from 'lucide-react';
+import type { AppSettings, ModelInfo } from '../types';
+
+interface SettingsProps {
+ settings: AppSettings;
+ updateSettings: (newSettings: Partial) => void;
+ clearHistory: () => void;
+ modelInfo: ModelInfo | null;
+}
+
+export const Settings: React.FC = ({
+ settings,
+ updateSettings,
+ clearHistory,
+ modelInfo,
+}) => {
+ const [saveSuccess, setSaveSuccess] = useState(false);
+ const [resetting, setResetting] = useState(false);
+ const [confirmClear, setConfirmClear] = useState(false);
+
+ const handleSave = () => {
+ setSaveSuccess(true);
+ setTimeout(() => setSaveSuccess(false), 2000);
+ };
+
+ const handleClearHistory = () => {
+ if (confirmClear) {
+ setResetting(true);
+ setTimeout(() => {
+ clearHistory();
+ setConfirmClear(false);
+ setResetting(false);
+ alert('Local database log history has been successfully reset.');
+ }, 800);
+ } else {
+ setConfirmClear(true);
+ }
+ };
+
+ // Safe formatting variables matching image specifications
+ const memoryUsed = modelInfo?.memory_usage_gb || 1.53;
+ const memoryTotal = modelInfo?.total_memory_gb || 7.65;
+ const memoryPercent = Math.min(100, Math.round((memoryUsed / memoryTotal) * 100)) || 20;
+
+ return (
+
+
+ {/* Settings Split Layout grid */}
+
+
+ {/* Left Column Form (3/5 width) */}
+
+
+
+
Model & Inference
+
Configure how the AI model runs and generates responses.
+
+
+
+
+ {/* Inference Mode selector */}
+
+ Inference Mode
+
+ Local Inference
+
+
+
+ {/* Target model selector */}
+
+
+ Model
+
+
+ Loaded
+
+
+
+ {modelInfo?.local_model_path.split('/').pop() || 'SmolLM2-360M-Instruct-Q4_K_M.gguf'}
+
+
+
+
+
+ {/* Context length slider */}
+
+
+ Context Length
+ {settings.contextLength}
+
+
updateSettings({ contextLength: parseInt(e.target.value) })}
+ className="w-full accent-primary h-1 bg-[#090810] rounded-lg appearance-none cursor-pointer focus:outline-none"
+ />
+
Maximum context length constraints for the model.
+
+
+ {/* Temperature Slider */}
+
+
+ Temperature
+ {settings.temperature}
+
+
updateSettings({ temperature: parseFloat(e.target.value) })}
+ className="w-full accent-primary h-1 bg-[#090810] rounded-lg appearance-none cursor-pointer focus:outline-none"
+ />
+
Controls randomness in responses.
+
+
+ {/* Max Output Tokens Slider */}
+
+
+ Max Tokens
+ {settings.maxTokens}
+
+
updateSettings({ maxTokens: parseInt(e.target.value) })}
+ className="w-full accent-primary h-1 bg-[#090810] rounded-lg appearance-none cursor-pointer focus:outline-none"
+ />
+
Maximum tokens in the response.
+
+
+ {/* Top P Slider */}
+
+
+ Top P
+ {settings.topP}
+
+
updateSettings({ topP: parseFloat(e.target.value) })}
+ className="w-full accent-primary h-1 bg-[#090810] rounded-lg appearance-none cursor-pointer focus:outline-none"
+ />
+
Nucleus sampling probability parameter.
+
+
+
+
+ {/* Stream response switch */}
+
+
+
+ Stream Response
+
+
Stream tokens as they are generated.
+
+
updateSettings({ streaming: e.target.checked })}
+ className="w-4.5 h-4.5 rounded text-primary focus:ring-primary accent-primary bg-background border-border cursor-pointer focus-ring"
+ />
+
+
+ {/* Keep history switch */}
+
+
+
+ Keep Conversation History
+
+
Maintain conversation history log records in memory.
+
+
+
+
+ {/* Save Button */}
+
+
+ {saveSuccess ? (
+ <>
+
+ Settings Saved
+ >
+ ) : (
+ <>
+
+ Save Settings
+ >
+ )}
+
+
+
+
+
+
+ {/* Right Column Stats (2/5 width) */}
+
+
+ {/* Model Information metadata */}
+
+
+
+ Model Information
+
+
+
+ Model Name
+ {modelInfo?.model_name.split('/').pop() || 'SmolLM2-360M-Instruct'}
+
+
+ Quantization
+ Q4_K_M
+
+
+ Model Size
+ 1.6 GB
+
+
+ Context Length
+ 4096
+
+
+ Vocabulary Size
+ 151,936
+
+
+ Architecture
+ Transformers
+
+
+
+
+ {/* System resource bars */}
+
+
+
+ System Resources
+
+
+ {/* RAM Usage progress bar */}
+
+
+ RAM Usage
+ {memoryPercent}%
+
+
+
{memoryUsed.toFixed(2)} GB / {memoryTotal.toFixed(2)} GB
+
+
+ {/* CPU Usage progress bar */}
+
+
+ CPU Usage
+ 12%
+
+
+
+
+
+ {/* Danger zone log clear button */}
+
+
+
+ Reset Cache
+
+
+
+ {resetting ? (
+ <>
+
+ Reseting...
+ >
+ ) : (
+ <>
+ {confirmClear ? 'Confirm Reset' : 'Reset Database Records'}
+ >
+ )}
+
+ {confirmClear && !resetting && (
+
setConfirmClear(false)}
+ className="w-full text-center text-[10px] hover:underline text-muted-foreground block py-1 cursor-pointer"
+ >
+ Cancel
+
+ )}
+
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..42cac7f583447ef2da080dbfd5a4014343514691
--- /dev/null
+++ b/frontend/src/components/Sidebar.tsx
@@ -0,0 +1,178 @@
+import React from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import {
+ MessageSquare, Code2, Settings2, Info, Plus,
+ Trash2, ChevronLeft, ChevronRight, LayoutDashboard, Cpu,
+ User, Database, Sparkles, HelpCircle, History, FileText,
+ Terminal, ShieldCheck, Settings
+} from 'lucide-react';
+import type { Conversation, ModelInfo } from '../types';
+
+interface SidebarProps {
+ conversations: Conversation[];
+ activeConversationId: string | null;
+ setActiveConversationId: (id: string) => void;
+ createNewConversation: () => void;
+ deleteConversation: (id: string) => void;
+ activeTab: string;
+ setActiveTab: (tab: string) => void;
+ modelInfo: ModelInfo | null;
+ collapsed: boolean;
+ setCollapsed: (c: boolean) => void;
+}
+
+export const Sidebar: React.FC = ({
+ conversations,
+ activeConversationId,
+ setActiveConversationId,
+ createNewConversation,
+ deleteConversation,
+ activeTab,
+ setActiveTab,
+ modelInfo,
+ collapsed,
+ setCollapsed,
+}) => {
+ const navItems = [
+ { id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
+ { id: 'chat', label: 'AI Chat', icon: MessageSquare },
+ { id: 'playground', label: 'Playground', icon: Code2 },
+ { id: 'history', label: 'History', icon: History },
+ { id: 'documents', label: 'Documents', icon: FileText },
+ { id: 'snippets', label: 'Snippets', icon: Terminal },
+ { id: 'models', label: 'Models', icon: Cpu },
+ { id: 'settings', label: 'Settings', icon: Settings2 },
+ ];
+
+ return (
+
+ {/* Brand Header */}
+
+
+
+
+
+
+ {!collapsed && (
+
+
+ Antigravity
+
+
+ Coder
+
+
+ )}
+
+
+
+ {!collapsed && (
+
setCollapsed(true)}
+ className="text-muted-foreground hover:text-foreground p-1 hover:bg-muted/50 rounded-lg transition-colors cursor-pointer shrink-0"
+ title="Collapse Sidebar"
+ >
+
+
+ )}
+
+
+ {/* Navigation Links */}
+
+ {navItems.map((item) => {
+ const Icon = item.icon;
+ const isActive = activeTab === item.id;
+ return (
+ setActiveTab(item.id)}
+ className={`w-full flex items-center gap-3 p-2.5 rounded-xl text-xs font-semibold transition-all group relative cursor-pointer ${
+ isActive
+ ? 'bg-[#2b1854] text-white border border-[#6344d5]/30'
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted/20'
+ }`}
+ >
+
+
+
+ {!collapsed && (
+
+ {item.label}
+
+ )}
+
+
+ {collapsed && (
+
+ {item.label}
+
+ )}
+
+ );
+ })}
+
+
+ {/* Expand trigger when collapsed */}
+ {collapsed && (
+
+ setCollapsed(false)}
+ className="text-muted-foreground hover:text-foreground p-2 hover:bg-muted/50 rounded-xl transition-colors cursor-pointer"
+ title="Expand Sidebar"
+ >
+
+
+
+ )}
+
+ {/* Mock User Profile Area */}
+
+
+ {!collapsed && (
+
+
+
+
+ D
+
+
+ Developer
+ Pro Plan
+
+
+
setActiveTab('settings')}
+ className="text-muted-foreground hover:text-foreground cursor-pointer p-1 rounded-md hover:bg-muted/40 shrink-0"
+ title="Settings"
+ >
+
+
+
+
+ )}
+
+
+
+ );
+};
diff --git a/frontend/src/hooks/useChat.ts b/frontend/src/hooks/useChat.ts
new file mode 100644
index 0000000000000000000000000000000000000000..30b8bc711699fc7c4c184f8bc58e970eff779589
--- /dev/null
+++ b/frontend/src/hooks/useChat.ts
@@ -0,0 +1,382 @@
+import { useState, useEffect, useCallback } from 'react';
+import type { ChatMessage, Conversation, AppSettings, ModelInfo, ModelMetrics } from '../types';
+
+const LOCAL_STORAGE_CONVS_KEY = 'antigravity_conversations';
+const LOCAL_STORAGE_SETTINGS_KEY = 'antigravity_settings';
+
+const DEFAULT_SETTINGS: AppSettings = {
+ inferenceMode: 'auto',
+ temperature: 0.7,
+ maxTokens: 1024,
+ topP: 0.9,
+ contextLength: 4096,
+ streaming: true,
+ theme: 'dark',
+ hfToken: '',
+ hfModelId: 'Qwen/Qwen2.5-Coder-0.5B-Instruct',
+};
+
+export const useChat = () => {
+ const [conversations, setConversations] = useState([]);
+ const [activeConversationId, setActiveConversationId] = useState(null);
+ const [settings, setSettings] = useState(DEFAULT_SETTINGS);
+ const [modelInfo, setModelInfo] = useState(null);
+ const [metrics, setMetrics] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ // Initialize Settings
+ useEffect(() => {
+ const savedSettings = localStorage.getItem(LOCAL_STORAGE_SETTINGS_KEY);
+ if (savedSettings) {
+ try {
+ const parsed = JSON.parse(savedSettings);
+ setSettings({ ...DEFAULT_SETTINGS, ...parsed });
+ } catch (e) {
+ setSettings(DEFAULT_SETTINGS);
+ }
+ }
+ }, []);
+
+ // Initialize Conversations
+ useEffect(() => {
+ const savedConvs = localStorage.getItem(LOCAL_STORAGE_CONVS_KEY);
+ if (savedConvs) {
+ try {
+ const parsed = JSON.parse(savedConvs);
+ setConversations(parsed);
+ if (parsed.length > 0) {
+ setActiveConversationId(parsed[0].id);
+ }
+ } catch (e) {
+ setConversations([]);
+ }
+ }
+ }, []);
+
+ // Save Conversations on change
+ useEffect(() => {
+ if (conversations.length > 0) {
+ localStorage.setItem(LOCAL_STORAGE_CONVS_KEY, JSON.stringify(conversations));
+ } else {
+ localStorage.removeItem(LOCAL_STORAGE_CONVS_KEY);
+ }
+ }, [conversations]);
+
+ // Apply CSS Class for Theme
+ useEffect(() => {
+ const root = window.document.documentElement;
+ if (settings.theme === 'dark') {
+ root.classList.add('dark');
+ root.classList.remove('light');
+ } else {
+ root.classList.add('light');
+ root.classList.remove('dark');
+ }
+ localStorage.setItem(LOCAL_STORAGE_SETTINGS_KEY, JSON.stringify(settings));
+ }, [settings]);
+
+ // Fetch model metadata
+ const fetchModelInfo = useCallback(async () => {
+ try {
+ const response = await fetch('/api/model-info');
+ if (response.ok) {
+ const data = await response.json();
+ setModelInfo(data);
+ }
+ } catch (e) {
+ console.error('Failed to load model info', e);
+ }
+ }, []);
+
+ // Fetch performance metrics
+ const fetchMetrics = useCallback(async () => {
+ try {
+ const response = await fetch('/api/metrics');
+ if (response.ok) {
+ const data = await response.json();
+ setMetrics(data);
+ }
+ } catch (e) {
+ console.error('Failed to load performance metrics', e);
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchModelInfo();
+ fetchMetrics();
+ // Poll metrics and model status every 10 seconds
+ const interval = setInterval(() => {
+ fetchModelInfo();
+ fetchMetrics();
+ }, 10000);
+ return () => clearInterval(interval);
+ }, [fetchModelInfo, fetchMetrics]);
+
+ const updateSettings = (newSettings: Partial) => {
+ setSettings((prev) => {
+ const updated = { ...prev, ...newSettings };
+ return updated;
+ });
+ };
+
+ const createNewConversation = (title: any = 'New Chat') => {
+ const cleanTitle = typeof title === 'string' && title.trim() ? title : 'New Chat';
+ const newConv: Conversation = {
+ id: Math.random().toString(36).substring(7),
+ title: cleanTitle,
+ messages: [],
+ activeModel: modelInfo?.model_name || 'Qwen2.5-Coder',
+ timestamp: Date.now(),
+ };
+ setConversations((prev) => [newConv, ...prev]);
+ setActiveConversationId(newConv.id);
+ return newConv;
+ };
+
+ const deleteConversation = (id: string) => {
+ setConversations((prev) => prev.filter((c) => c.id !== id));
+ if (activeConversationId === id) {
+ const remaining = conversations.filter((c) => c.id !== id);
+ if (remaining.length > 0) {
+ setActiveConversationId(remaining[0].id);
+ } else {
+ setActiveConversationId(null);
+ }
+ }
+ };
+
+ const activeConversation = conversations.find((c) => c.id === activeConversationId) || null;
+
+ const sendMessage = async (content: string) => {
+ if (!content.trim()) return;
+
+ let currentConv = activeConversation;
+ if (!currentConv) {
+ currentConv = createNewConversation(content.substring(0, 30));
+ }
+
+ const userMsg: ChatMessage = {
+ id: Math.random().toString(36).substring(7),
+ role: 'user',
+ content,
+ timestamp: Date.now(),
+ };
+
+ // Add user message to conversation list
+ const updatedMessages = [...currentConv.messages, userMsg];
+
+ // Set a quick temporary assistant message for streaming
+ const assistantMsgId = Math.random().toString(36).substring(7);
+ const tempAssistantMsg: ChatMessage = {
+ id: assistantMsgId,
+ role: 'assistant',
+ content: '',
+ timestamp: Date.now(),
+ };
+
+ setConversations((prev) =>
+ prev.map((c) =>
+ c.id === currentConv!.id
+ ? { ...c, messages: [...updatedMessages, tempAssistantMsg], timestamp: Date.now() }
+ : c
+ )
+ );
+
+ setIsLoading(true);
+ setError(null);
+
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ };
+
+ try {
+ const response = await fetch('/api/chat', {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({
+ messages: updatedMessages.map(({ role, content }) => ({ role, content })),
+ temperature: settings.temperature,
+ max_tokens: settings.maxTokens,
+ top_p: settings.topP,
+ stream: settings.streaming,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Server returned error: ${response.statusText}`);
+ }
+
+ if (settings.streaming && response.body) {
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let assistantContent = '';
+ let buffer = '';
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ buffer += decoder.decode(value, { stream: true });
+ const lines = buffer.split('\n');
+ buffer = lines.pop() || '';
+
+ for (const line of lines) {
+ const cleanLine = line.trim();
+ if (cleanLine.startsWith('data: ')) {
+ try {
+ const chunkData = JSON.parse(cleanLine.slice(6));
+
+ if (chunkData.content) {
+ assistantContent += chunkData.content;
+ setConversations((prev) =>
+ prev.map((c) =>
+ c.id === currentConv!.id
+ ? {
+ ...c,
+ messages: c.messages.map((m) =>
+ m.id === assistantMsgId ? { ...m, content: assistantContent } : m
+ ),
+ }
+ : c
+ )
+ );
+ }
+
+ if (chunkData.done && chunkData.usage) {
+ fetchMetrics();
+ fetchModelInfo();
+ }
+ } catch (e) {
+ // Ignore incomplete JSON chunks
+ }
+ }
+ }
+ }
+ } else {
+ const result = await response.json();
+ setConversations((prev) =>
+ prev.map((c) =>
+ c.id === currentConv!.id
+ ? {
+ ...c,
+ messages: c.messages.map((m) =>
+ m.id === assistantMsgId ? { ...m, content: result.content } : m
+ ),
+ }
+ : c
+ )
+ );
+ fetchMetrics();
+ fetchModelInfo();
+ }
+ } catch (e: any) {
+ console.error(e);
+ setError(e.message || 'Something went wrong');
+ setConversations((prev) =>
+ prev.map((c) =>
+ c.id === currentConv!.id
+ ? {
+ ...c,
+ messages: c.messages.map((m) =>
+ m.id === assistantMsgId
+ ? { ...m, content: `Error: Could not retrieve response from server. Detail: ${e.message}` }
+ : m
+ ),
+ }
+ : c
+ )
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const executeCodeAction = async (action: 'explain' | 'debug' | 'refactor' | 'generate-tests' | 'summarize', code: string, language: string, context?: string): Promise => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ const response = await fetch(`/api/${action}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ code,
+ language,
+ context,
+ temperature: settings.temperature,
+ max_tokens: settings.maxTokens,
+ stream: false,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`API failed: ${response.statusText}`);
+ }
+
+ const result = await response.json();
+ fetchMetrics();
+ return result.content;
+ } catch (e: any) {
+ console.error(e);
+ setError(e.message || 'Execution failed');
+ throw e;
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const getCompletion = async (prefix: string, suffix: string, language: string): Promise => {
+ try {
+ const response = await fetch('/api/complete', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ prefix,
+ suffix,
+ language,
+ max_tokens: 64,
+ temperature: 0.1,
+ stream: false,
+ }),
+ });
+
+ if (response.ok) {
+ const result = await response.json();
+ return result.content;
+ }
+ return '';
+ } catch (e) {
+ console.error('Completion request failed', e);
+ return '';
+ }
+ };
+
+ const clearHistory = () => {
+ setConversations([]);
+ setActiveConversationId(null);
+ };
+
+ return {
+ conversations,
+ activeConversationId,
+ setActiveConversationId,
+ activeConversation,
+ settings,
+ modelInfo,
+ metrics,
+ isLoading,
+ error,
+ updateSettings,
+ createNewConversation,
+ deleteConversation,
+ sendMessage,
+ executeCodeAction,
+ getCompletion,
+ clearHistory,
+ fetchModelInfo,
+ };
+};
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000000000000000000000000000000000000..454b5489c294d0077b140290f662a119335d2698
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,221 @@
+@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&family=Inter:wght@300;400;500;600;700&display=swap');
+
+@import "tailwindcss";
+
+@theme {
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ --font-mono: 'Fira Code', 'Courier New', monospace;
+
+ --color-border: hsl(var(--border));
+ --color-input: hsl(var(--input));
+ --color-ring: hsl(var(--ring));
+ --color-background: hsl(var(--background));
+ --color-foreground: hsl(var(--foreground));
+
+ --color-primary: hsl(var(--primary));
+ --color-primary-foreground: hsl(var(--primary-foreground));
+
+ --color-secondary: hsl(var(--secondary));
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
+
+ --color-muted: hsl(var(--muted));
+ --color-muted-foreground: hsl(var(--muted-foreground));
+
+ --color-accent: hsl(var(--accent));
+ --color-accent-foreground: hsl(var(--accent-foreground));
+
+ --color-popover: hsl(var(--popover));
+ --color-popover-foreground: hsl(var(--popover-foreground));
+
+ --color-card: hsl(var(--card));
+ --color-card-foreground: hsl(var(--card-foreground));
+
+ --radius-lg: var(--radius);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-sm: calc(var(--radius) - 4px);
+}
+
+:root {
+ /* High Fidelity Deep Purple Reference Theme */
+ --background: 250 33% 5%; /* #090810 */
+ --foreground: 250 20% 98%; /* #eae9fc */
+
+ --muted: 250 15% 12%; /* #1a1829 */
+ --muted-foreground: 250 12% 65%;/* #9c97b5 */
+
+ --popover: 250 33% 5%; /* #090810 */
+ --popover-foreground: 250 20% 98%;
+
+ --card: 250 32% 10%; /* #131121 */
+ --card-foreground: 250 20% 98%;
+
+ --border: 247 25% 15%; /* #1d1b2e */
+ --input: 247 25% 15%;
+
+ --primary: 252 65% 55%; /* #6344d5 - Violet Accent */
+ --primary-foreground: 0 0% 100%;
+
+ --secondary: 250 28% 18%; /* #2b1854 - Soft active capsule/bubble background */
+ --secondary-foreground: 250 20% 98%;
+
+ --accent: 252 50% 20%; /* #251b47 */
+ --accent-foreground: 250 20% 98%;
+
+ --destructive: 0 85% 60%;
+ --destructive-foreground: 0 0% 100%;
+
+ --ring: 252 65% 55%;
+
+ --radius: 0.75rem;
+}
+
+.light {
+ /* Light fallback matching the scale */
+ --background: 0 0% 100%;
+ --foreground: 224 71% 4%;
+
+ --muted: 220 14% 96%;
+ --muted-foreground: 220 9% 46%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 224 71% 4%;
+
+ --card: 0 0% 99%;
+ --card-foreground: 224 71% 4%;
+
+ --border: 220 13% 91%;
+ --input: 220 13% 91%;
+
+ --primary: 252 65% 45%;
+ --primary-foreground: 0 0% 100%;
+
+ --secondary: 220 14% 96%;
+ --secondary-foreground: 224 71% 4%;
+
+ --accent: 252 40% 94%;
+ --accent-foreground: 252 65% 45%;
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: var(--font-sans);
+ font-size: 14px;
+ background-color: hsl(var(--background));
+ color: hsl(var(--foreground));
+ overflow: hidden;
+ transition: background-color 0.2s ease, color 0.2s ease;
+ line-height: 1.5;
+ -webkit-font-smoothing: antialiased;
+}
+
+/* Custom Scrollbars */
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: hsl(var(--border));
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: hsl(var(--muted-foreground));
+}
+
+/* Shimmer Loader Effect */
+@keyframes shimmer {
+ 0% {
+ background-position: -200% 0;
+ }
+ 100% {
+ background-position: 200% 0;
+ }
+}
+
+.animate-shimmer {
+ background: linear-gradient(
+ 90deg,
+ hsl(var(--card)) 25%,
+ color-mix(in srgb, hsl(var(--muted)) 60%, transparent) 50%,
+ hsl(var(--card)) 75%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite linear;
+}
+
+/* Custom interactive outline ring */
+.focus-ring {
+ outline: none;
+ transition: box-shadow 0.2s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.2s cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+.focus-ring:focus-visible {
+ box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(var(--primary));
+ border-color: hsl(var(--primary));
+}
+
+/* Glassmorphic Blur panel */
+.glass-panel {
+ background: color-mix(in srgb, hsl(var(--card)) 70%, transparent);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+}
+
+/* Subtle Glowing Borders & Accents */
+.glow-primary {
+ box-shadow: 0 0 15px -3px color-mix(in srgb, hsl(var(--primary)) 40%, transparent);
+}
+
+.border-glow-hover:hover {
+ border-color: color-mix(in srgb, hsl(var(--primary)) 50%, transparent);
+ box-shadow: 0 0 12px -5px color-mix(in srgb, hsl(var(--primary)) 30%, transparent);
+}
+
+/* Premium Selection Colors */
+::selection {
+ background-color: color-mix(in srgb, hsl(var(--primary)) 30%, transparent);
+ color: hsl(var(--foreground));
+}
+
+/* Micro Animations */
+@keyframes pulse-glow {
+ 0%, 100% {
+ opacity: 0.15;
+ transform: scale(1);
+ }
+ 50% {
+ opacity: 0.35;
+ transform: scale(1.05);
+ }
+}
+
+.animate-pulse-glow {
+ animation: pulse-glow 4s ease-in-out infinite;
+}
+
+/* Custom buttons */
+.btn-primary {
+ background-color: hsl(var(--primary));
+ color: hsl(var(--primary-foreground));
+ border-radius: var(--radius);
+ transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+.btn-primary:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px color-mix(in srgb, hsl(var(--primary)) 40%, transparent);
+}
+
+.btn-primary:active {
+ transform: translateY(0);
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bef5202a32cbd0632c43de40f6e908532903fd42
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fce0a93be86941ab102f33abd23c227694fca73a
--- /dev/null
+++ b/frontend/src/types.ts
@@ -0,0 +1,49 @@
+export interface ChatMessage {
+ id?: string;
+ role: 'system' | 'user' | 'assistant';
+ content: string;
+ timestamp?: number;
+}
+
+export interface Conversation {
+ id: string;
+ title: string;
+ messages: ChatMessage[];
+ activeModel: string;
+ timestamp: number;
+}
+
+export interface ModelInfo {
+ model_name: string;
+ inference_mode: string;
+ status: string;
+ memory_usage_gb: number;
+ total_memory_gb: number;
+ local_model_exists: boolean;
+ local_model_path: string;
+ device: string;
+}
+
+export interface ModelMetrics {
+ total_requests: number;
+ total_prompt_tokens: number;
+ total_completion_tokens: number;
+ total_tokens: number;
+ average_latency_seconds: number;
+ total_generation_time_seconds: number;
+ active_mode: string;
+ system_ram_gb: number;
+ device: string;
+}
+
+export interface AppSettings {
+ inferenceMode: 'auto' | 'local' | 'huggingface';
+ temperature: number;
+ maxTokens: number;
+ topP: number;
+ contextLength: number;
+ streaming: boolean;
+ theme: 'dark' | 'light';
+ hfToken: string;
+ hfModelId: string;
+}
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
new file mode 100644
index 0000000000000000000000000000000000000000..6830b6f759f55aec32f0a2fd367ee63cf68db8b7
--- /dev/null
+++ b/frontend/tsconfig.app.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023", "DOM"],
+ "module": "esnext",
+ "types": ["vite/client"],
+ "allowArbitraryExtensions": true,
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ffef600d959ec9e396d5a260bd3f5b927b2cef8
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 0000000000000000000000000000000000000000..8455dcbc2c947322adef8783a42741878edaea9c
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "module": "nodenext",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f780c7883e5eb20912b9918a04c4f4d002bdec2f
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,27 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
+import path from 'path'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [
+ react(),
+ tailwindcss()
+ ],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+ server: {
+ port: 5173,
+ proxy: {
+ '/api': {
+ target: process.env.VITE_API_URL || 'http://localhost:8000',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, '')
+ }
+ }
+ }
+})
diff --git a/models/.gitkeep b/models/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..1c11b97e6802669872c51abae4a5a2e9b8cccf5c
--- /dev/null
+++ b/models/.gitkeep
@@ -0,0 +1 @@
+# Keep this folder so that models can be downloaded here
diff --git a/models/Qwen2.5-Coder-0.5B-Instruct-Q4_K_M.gguf b/models/Qwen2.5-Coder-0.5B-Instruct-Q4_K_M.gguf
new file mode 100644
index 0000000000000000000000000000000000000000..a5aac6d55df658e79819ca05616330158c5829bf
--- /dev/null
+++ b/models/Qwen2.5-Coder-0.5B-Instruct-Q4_K_M.gguf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0128e77564e43d40682f82d7ebe8a9abdf0c24c8f55fa85629f8cc156b1b6560
+size 397808288
diff --git a/models/SmolLM2-360M-Instruct-Q4_K_M.gguf b/models/SmolLM2-360M-Instruct-Q4_K_M.gguf
new file mode 100644
index 0000000000000000000000000000000000000000..8a5714b7af3ce45e72a46d47840ce0b80c567f88
--- /dev/null
+++ b/models/SmolLM2-360M-Instruct-Q4_K_M.gguf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2fa3f013dcdd7b99f9b237717fa0b12d75bbb89984cc1274be1471a465bac9c2
+size 270590880
diff --git a/railway.json b/railway.json
new file mode 100644
index 0000000000000000000000000000000000000000..43e1f5f1b2f242d0025459529a9f42be643a72f9
--- /dev/null
+++ b/railway.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://railway.app/railway.schema.json",
+ "build": {
+ "builder": "DOCKERFILE",
+ "dockerfilePath": "Dockerfile"
+ },
+ "deploy": {
+ "numReplicas": 1,
+ "restartPolicyType": "ON_FAILURE",
+ "healthcheckPath": "/health",
+ "healthcheckTimeout": 120
+ }
+}
diff --git a/render.yaml b/render.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..746a26f595a1f4a4f29ef5d0e60242e5b3096f85
--- /dev/null
+++ b/render.yaml
@@ -0,0 +1,31 @@
+services:
+ - type: web
+ name: antigravity-ai-coder
+ env: docker
+ dockerfilePath: Dockerfile
+ plan: free # Can be upgraded for RAM/volumes
+ envVars:
+ - key: PORT
+ value: 8000
+ - key: HOST
+ value: 0.0.0.0
+ - key: INFERENCE_MODE
+ value: auto
+ - key: LOCAL_MODEL_PATH
+ value: models/Qwen2.5-Coder-0.5B-Instruct-Q4_K_M.gguf
+ - key: HF_MODEL_ID
+ value: Qwen/Qwen2.5-Coder-0.5B-Instruct
+ - key: DEFAULT_TEMPERATURE
+ value: 0.7
+ - key: DEFAULT_MAX_TOKENS
+ value: 1024
+ - key: RATE_LIMIT_PER_MINUTE
+ value: 60
+ - key: SECRET_KEY
+ generateValue: true
+ - key: HF_API_TOKEN
+ sync: false # Set in Render UI
+ disk:
+ name: models-storage
+ mountPath: /app/models
+ sizeGB: 10
diff --git a/scripts/download_model.py b/scripts/download_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..cc96d1aa9daab3b75786a0795c0b945723dfe5c5
--- /dev/null
+++ b/scripts/download_model.py
@@ -0,0 +1,40 @@
+import os
+import sys
+
+def download_model():
+ # Read settings from environment variables or use default
+ repo_id = os.getenv("LOCAL_MODEL_REPO", "bartowski/Qwen2.5-Coder-0.5B-Instruct-GGUF")
+ filename = os.getenv("LOCAL_MODEL_FILE", "Qwen2.5-Coder-0.5B-Instruct-Q4_K_M.gguf")
+ model_path = os.getenv("LOCAL_MODEL_PATH", "models/Qwen2.5-Coder-0.5B-Instruct-Q4_K_M.gguf")
+
+ target_dir = os.path.dirname(os.path.abspath(model_path))
+ os.makedirs(target_dir, exist_ok=True)
+
+ print(f"============================================================")
+ print(f"Starting Qwen GGUF model downloader...")
+ print(f"Source HF Repo: {repo_id}")
+ print(f"Target Filename: {filename}")
+ print(f"Destination Dir: {target_dir}")
+ print(f"============================================================")
+
+ try:
+ from huggingface_hub import hf_hub_download
+
+ print("Downloading GGUF file (approx. 397MB)...")
+ hf_hub_download(
+ repo_id=repo_id,
+ filename=filename,
+ local_dir=target_dir,
+ local_dir_use_symlinks=False
+ )
+ print("Model file downloaded successfully!")
+ print(f"Located at: {os.path.abspath(model_path)}")
+ except ImportError:
+ print("Error: 'huggingface_hub' package is not installed. Please run: pip install huggingface_hub")
+ sys.exit(1)
+ except Exception as e:
+ print(f"Error downloading model: {e}")
+ sys.exit(1)
+
+if __name__ == "__main__":
+ download_model()
diff --git a/scripts/setup.sh b/scripts/setup.sh
new file mode 100644
index 0000000000000000000000000000000000000000..3c00e6cfb81eca96be8fe9fe74b4e611521b727f
--- /dev/null
+++ b/scripts/setup.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+# Antigravity Coder Setup Script
+
+set -e
+
+echo "=== Starting Antigravity Coder Installation ==="
+
+# Check requirements
+if ! command -v python3 &> /dev/null; then
+ echo "Error: Python 3 is not installed. Please install Python 3.11+."
+ exit 1
+fi
+
+if ! command -v node &> /dev/null; then
+ echo "Error: Node.js is not installed. Please install Node.js 20+."
+ exit 1
+fi
+
+# 1. Setup Python Virtual Environment
+echo "Setting up Python virtual environment..."
+python3 -m venv venv
+source venv/bin/activate
+
+# 2. Install Python Dependencies
+echo "Installing backend dependencies (this may compile llama-cpp-python)..."
+pip install --upgrade pip
+pip install -r backend/requirements.txt
+
+# 3. Install Frontend Dependencies
+echo "Installing frontend dependencies..."
+cd frontend
+npm install
+cd ..
+
+# 4. Build Frontend Assets
+echo "Compiling frontend assets..."
+cd frontend
+npm run build
+cd ..
+
+# 5. Pre-download local model
+echo "Downloading Qwen2.5-Coder model..."
+source venv/bin/activate
+python scripts/download_model.py
+
+echo "============================================="
+echo "Setup Complete!"
+echo "To start the backend: source venv/bin/activate && python backend/run.py"
+echo "To start frontend dev server: cd frontend && npm run dev"
+echo "============================================="
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..66173aec46f4872ef3626ad8b9abd0a177334f34
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+# Test package
diff --git a/tests/test_backend.py b/tests/test_backend.py
new file mode 100644
index 0000000000000000000000000000000000000000..9fbcd61a1ab3c06c8dca67c4d9565b947d4b722d
--- /dev/null
+++ b/tests/test_backend.py
@@ -0,0 +1,61 @@
+import os
+import sys
+import unittest
+from fastapi.testclient import TestClient
+
+# Ensure python paths are mapped correctly
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
+
+from backend.app.main import app
+from backend.app.utils import estimate_tokens, get_code_prompt, get_completion_prompt
+from backend.app.config import settings
+
+class TestBackendUtilities(unittest.TestCase):
+ def test_token_estimation(self):
+ # Verify basic estimation boundaries
+ self.assertEqual(estimate_tokens(""), 0)
+
+ sample_text = "def hello_world():\n print('Hello World')"
+ tokens = estimate_tokens(sample_text)
+ self.assertGreater(tokens, 0)
+ self.assertLess(tokens, len(sample_text))
+
+ def test_prompt_constructors(self):
+ # Test code prompt builder
+ prompt = get_code_prompt("explain", "print(10)", "python")
+ self.assertIn("print(10)", prompt)
+ self.assertIn("explain", prompt.lower())
+
+ # Test code completion prompt builder
+ completion_prompt = get_completion_prompt("def add(a, b):", "return a + b", "python")
+ self.assertIn("def add(a, b):", completion_prompt)
+ self.assertIn("return a + b", completion_prompt)
+
+class TestAPIEndpoints(unittest.TestCase):
+ def setUp(self):
+ self.client = TestClient(app)
+
+ def test_health_check(self):
+ response = self.client.get("/health")
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+ self.assertEqual(data["status"], "healthy")
+ self.assertIn("active_mode", data)
+
+ def test_metrics_endpoints(self):
+ response = self.client.get("/metrics")
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+ self.assertIn("total_requests", data)
+ self.assertIn("average_latency_seconds", data)
+
+ def test_model_info(self):
+ response = self.client.get("/model-info")
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+ self.assertIn("model_name", data)
+ self.assertIn("inference_mode", data)
+ self.assertIn("device", data)
+
+if __name__ == "__main__":
+ unittest.main()