Manish Kumar commited on
Commit
a6ee555
·
1 Parent(s): 057f830

inicial commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +28 -0
  2. .env +21 -0
  3. .env.example +22 -0
  4. .idea/.gitignore +10 -0
  5. .idea/Levi.iml +8 -0
  6. .idea/inspectionProfiles/Project_Default.xml +13 -0
  7. .idea/inspectionProfiles/profiles_settings.xml +6 -0
  8. .idea/modules.xml +8 -0
  9. .idea/new.iml +12 -0
  10. .idea/vcs.xml +6 -0
  11. Dockerfile +52 -0
  12. README.md +107 -1
  13. backend/app/__init__.py +1 -0
  14. backend/app/config.py +57 -0
  15. backend/app/llm/__init__.py +1 -0
  16. backend/app/llm/base.py +45 -0
  17. backend/app/llm/local.py +168 -0
  18. backend/app/llm/manager.py +89 -0
  19. backend/app/main.py +317 -0
  20. backend/app/middleware.py +73 -0
  21. backend/app/models.py +50 -0
  22. backend/app/utils.py +83 -0
  23. backend/requirements.txt +10 -0
  24. backend/run.py +11 -0
  25. docker/Dockerfile.backend +45 -0
  26. docker/Dockerfile.frontend +13 -0
  27. docker/docker-compose.yml +36 -0
  28. docs/API.md +109 -0
  29. docs/DEPLOYMENT.md +47 -0
  30. frontend/.gitignore +24 -0
  31. frontend/.oxlintrc.json +8 -0
  32. frontend/README.md +32 -0
  33. frontend/index.html +14 -0
  34. frontend/package-lock.json +1909 -0
  35. frontend/package.json +34 -0
  36. frontend/public/favicon.svg +1 -0
  37. frontend/public/icons.svg +24 -0
  38. frontend/src/App.css +184 -0
  39. frontend/src/App.tsx +193 -0
  40. frontend/src/assets/hero.png +0 -0
  41. frontend/src/assets/react.svg +1 -0
  42. frontend/src/assets/vite.svg +1 -0
  43. frontend/src/components/AboutPage.tsx +79 -0
  44. frontend/src/components/ChatInterface.tsx +460 -0
  45. frontend/src/components/CodePlayground.tsx +271 -0
  46. frontend/src/components/Dashboard.tsx +257 -0
  47. frontend/src/components/MonacoEditorWrapper.tsx +88 -0
  48. frontend/src/components/Navbar.tsx +141 -0
  49. frontend/src/components/Settings.tsx +337 -0
  50. frontend/src/components/Sidebar.tsx +178 -0
.dockerignore ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignore Node.js dependencies
2
+ frontend/node_modules/
3
+ frontend/dist/
4
+ frontend/.vite/
5
+
6
+ # Ignore Python environments and caches
7
+ **/__pycache__/
8
+ *.pyc
9
+ *.pyo
10
+ *.pyd
11
+ .Python
12
+ env/
13
+ venv/
14
+ .venv/
15
+ pip-log.txt
16
+ pip-delete-this-directory.txt
17
+
18
+ # Ignore models directory (it is mounted as a volume at runtime)
19
+ models/
20
+
21
+ # Ignore logs and dev tools
22
+ .git/
23
+ .gitignore
24
+ .dockerignore
25
+ .agents/
26
+ .gemini/
27
+ *.log
28
+ docker-compose.override.yml
.env ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AI Coding Assistant - Active Environment Configuration
2
+
3
+ PORT=8000
4
+ HOST=0.0.0.0
5
+ DEBUG=true
6
+ CORS_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost:8000
7
+
8
+ # SmolLM2 Model Settings
9
+ LOCAL_MODEL_PATH=models/SmolLM2-360M-Instruct-Q4_K_M.gguf
10
+ LOCAL_MODEL_REPO=bartowski/SmolLM2-360M-Instruct-GGUF
11
+ LOCAL_MODEL_FILE=SmolLM2-360M-Instruct-Q4_K_M.gguf
12
+
13
+ # Generation Parameters
14
+ DEFAULT_TEMPERATURE=0.7
15
+ DEFAULT_MAX_TOKENS=1024
16
+ DEFAULT_TOP_P=0.9
17
+ DEFAULT_CONTEXT_LENGTH=2048
18
+
19
+ # Security & Limits
20
+ RATE_LIMIT_PER_MINUTE=100
21
+ SECRET_KEY=dev-secret-key-32-chars-long-minimum-required!
.env.example ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AI Coding Assistant - Environment Configuration
2
+
3
+ # Backend Server Configuration
4
+ PORT=8000
5
+ HOST=0.0.0.0
6
+ DEBUG=false
7
+ CORS_ORIGINS=http://localhost:5173,http://localhost:3000,https://*.railway.app,https://*.render.com
8
+
9
+ # SmolLM2 Model Settings
10
+ LOCAL_MODEL_PATH=models/SmolLM2-360M-Instruct-Q4_K_M.gguf
11
+ LOCAL_MODEL_REPO=bartowski/SmolLM2-360M-Instruct-GGUF
12
+ LOCAL_MODEL_FILE=SmolLM2-360M-Instruct-Q4_K_M.gguf
13
+
14
+ # Generation Parameters
15
+ DEFAULT_TEMPERATURE=0.7
16
+ DEFAULT_MAX_TOKENS=1024
17
+ DEFAULT_TOP_P=0.9
18
+ DEFAULT_CONTEXT_LENGTH=2048
19
+
20
+ # Security & Limits
21
+ RATE_LIMIT_PER_MINUTE=60
22
+ SECRET_KEY=generate_a_secure_random_key_here
.idea/.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
4
+ # Editor-based HTTP Client requests
5
+ /httpRequests/
6
+ # Ignored default folder with query files
7
+ /queries/
8
+ # Datasource local storage ignored files
9
+ /dataSources/
10
+ /dataSources.local.xml
.idea/Levi.iml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$" />
5
+ <orderEntry type="inheritedJdk" />
6
+ <orderEntry type="sourceFolder" forTests="false" />
7
+ </component>
8
+ </module>
.idea/inspectionProfiles/Project_Default.xml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <component name="InspectionProjectProfileManager">
2
+ <profile version="1.0">
3
+ <option name="myName" value="Project Default" />
4
+ <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
5
+ <inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
6
+ <option name="ignoredIdentifiers">
7
+ <list>
8
+ <option value="llama_index.*" />
9
+ </list>
10
+ </option>
11
+ </inspection_tool>
12
+ </profile>
13
+ </component>
.idea/inspectionProfiles/profiles_settings.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/Levi.iml" filepath="$PROJECT_DIR$/.idea/Levi.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
.idea/new.iml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$" />
5
+ <orderEntry type="inheritedJdk" />
6
+ <orderEntry type="sourceFolder" forTests="false" />
7
+ </component>
8
+ <component name="PyDocumentationSettings">
9
+ <option name="format" value="PLAIN" />
10
+ <option name="myDocStringFormat" value="Plain" />
11
+ </component>
12
+ </module>
.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
Dockerfile ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 1: Build React Frontend
2
+ FROM node:20-slim as frontend-builder
3
+
4
+ WORKDIR /frontend
5
+
6
+ # Copy package config and lock files
7
+ COPY frontend/package*.json ./
8
+
9
+ # Install packages
10
+ RUN npm ci
11
+
12
+ # Copy frontend source files
13
+ COPY frontend/ ./
14
+
15
+ # Build production static bundle
16
+ RUN npm run build
17
+
18
+ # Stage 2: Build Python Backend & Package app
19
+ FROM python:3.11-slim
20
+
21
+ WORKDIR /app
22
+
23
+ # Install compilation tools for building llama-cpp-python in backend
24
+ RUN apt-get update && apt-get install -y --no-install-recommends \
25
+ build-essential \
26
+ gcc \
27
+ g++ \
28
+ make \
29
+ python3-dev \
30
+ git \
31
+ libgomp1 \
32
+ && rm -rf /var/lib/apt/lists/*
33
+
34
+ # Copy requirements and install dependencies
35
+ COPY backend/requirements.txt ./backend/
36
+ RUN pip install --no-cache-dir -r ./backend/requirements.txt
37
+
38
+ # Copy backend source code
39
+ COPY backend/ ./backend/
40
+
41
+ # Copy static frontend build from Stage 1 into frontend/dist
42
+ COPY --from=frontend-builder /frontend/dist ./frontend/dist
43
+
44
+ # Setup environments
45
+ ENV PORT=8000
46
+ ENV HOST=0.0.0.0
47
+ ENV PYTHONPATH=/app
48
+
49
+ EXPOSE 8000
50
+
51
+ # Start unified server
52
+ CMD ["python", "backend/run.py"]
README.md CHANGED
@@ -1 +1,107 @@
1
- # Levi
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Antigravity AI Coding Assistant ⚡
2
+
3
+ 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.
4
+
5
+ ```mermaid
6
+ graph TD
7
+ User([User]) <--> |HTTP / SSE| FE[React SPA - Vite + TS]
8
+ subgraph Backend [FastAPI Server]
9
+ API[API Endpoints] <--> MGR[LLM Manager]
10
+ MGR --> |Check RAM / Models| Decision{Load Local GGUF?}
11
+ Decision -->|Yes: RAM >= 1.5GB| GGUF[Local llama-cpp-python]
12
+ Decision -->|No: Low memory / Failed| HF[Hugging Face Cloud API]
13
+ end
14
+ GGUF <--> |Read / Write| Models[(models/ folder)]
15
+ HF <--> |HTTPS Request| HFHub[Hugging Face Inference Hub]
16
+ ```
17
+
18
+ ---
19
+
20
+ ## 🌟 Key Features
21
+
22
+ * **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.
23
+ * **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`).
24
+ * **Advanced Chat Window:** Smooth response streaming (Server-Sent Events), code block copy buttons, and drag-and-drop file imports.
25
+ * **Production Deployment Ready:** Pre-configured Dockerfiles, Docker Compose, Railway config, and Render deployment specifications.
26
+ * **Performance Telemetry:** Live dashboards displaying generation speed (tokens/sec), latency, request tallies, and RAM footprint.
27
+
28
+ ---
29
+
30
+ ## 🛠️ Tech Stack
31
+
32
+ * **Frontend:** React 19, Vite, TypeScript, Tailwind CSS, Monaco Editor, Lucide Icons, Framer Motion
33
+ * **Backend:** FastAPI, Python 3.11+, Uvicorn, llama-cpp-python, Hugging Face Hub Client, Psutil
34
+
35
+ ---
36
+
37
+ ## 🚀 Quick Start (Local Setup)
38
+
39
+ ### Prerequisites
40
+ * Python 3.11+
41
+ * Node.js 20+
42
+
43
+ ### Step 1: Clone and Setup Workspace
44
+ Clone this repository and navigate to the project directory:
45
+ ```bash
46
+ git clone https://github.com/your-repo/antigravity-coder.git
47
+ cd antigravity-coder
48
+ ```
49
+
50
+ ### Step 2: Install and Download GGUF Model
51
+ Use our automated installer script:
52
+ ```bash
53
+ # On Linux/macOS
54
+ chmod +x scripts/setup.sh
55
+ ./scripts/setup.sh
56
+
57
+ # On Windows (PowerShell)
58
+ pip install -r backend/requirements.txt
59
+ cd frontend; npm install; npm run build; cd ..
60
+ python scripts/download_model.py
61
+ ```
62
+
63
+ ### Step 3: Run the Application
64
+ Start the backend server:
65
+ ```bash
66
+ # Run backend (activates virtual env if created)
67
+ python backend/run.py
68
+ ```
69
+ 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!
70
+
71
+ For hot-reloading frontend development:
72
+ ```bash
73
+ cd frontend
74
+ npm run dev
75
+ ```
76
+ Open [http://localhost:5173](http://localhost:5173) in your browser.
77
+
78
+ ---
79
+
80
+ ## 🐳 Running with Docker
81
+
82
+ Run both services in hot-reloading development mode using Docker Compose:
83
+ ```bash
84
+ docker compose -f docker/docker-compose.yml up --build
85
+ ```
86
+ Build and run the production-ready unified container (hosting both frontend and API on port 8000):
87
+ ```bash
88
+ docker build -t antigravity-coder .
89
+ docker run -p 8000:8000 antigravity-coder
90
+ ```
91
+
92
+ ---
93
+
94
+ ## 🌐 Cloud Deployment
95
+
96
+ Detailed step-by-step guides for deployment configurations:
97
+ * [Railway Deployment Guide](docs/DEPLOYMENT.md#railway)
98
+ * [Render Deployment Guide](docs/DEPLOYMENT.md#render)
99
+ * [API Reference Documentation](docs/API.md)
100
+
101
+ ---
102
+
103
+ ## 🔒 Security
104
+
105
+ * Standard CORS origin protections.
106
+ * Secure in-memory sliding rate limiter per client IP.
107
+ * Configuration parsing using Pydantic Settings from `.env` files. Secrets are never hardcoded.
backend/app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # AI Coding Assistant Backend Package
backend/app/config.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import List
3
+ from pydantic_settings import BaseSettings, SettingsConfigDict
4
+ from pydantic import Field
5
+
6
+ class Settings(BaseSettings):
7
+ # Server settings
8
+ PORT: int = Field(default=8000, validation_alias="PORT")
9
+ HOST: str = Field(default="0.0.0.0", validation_alias="HOST")
10
+ DEBUG: bool = Field(default=False, validation_alias="DEBUG")
11
+ CORS_ORIGINS: str = Field(
12
+ default="http://localhost:5173,http://localhost:3000,http://localhost:8000",
13
+ validation_alias="CORS_ORIGINS"
14
+ )
15
+
16
+ # LLM Settings
17
+ INFERENCE_MODE: str = Field(default="local", validation_alias="INFERENCE_MODE")
18
+
19
+ # Local GGUF Settings
20
+ LOCAL_MODEL_PATH: str = Field(
21
+ default="models/SmolLM2-360M-Instruct-Q4_K_M.gguf",
22
+ validation_alias="LOCAL_MODEL_PATH"
23
+ )
24
+ LOCAL_MODEL_REPO: str = Field(
25
+ default="bartowski/SmolLM2-360M-Instruct-GGUF",
26
+ validation_alias="LOCAL_MODEL_REPO"
27
+ )
28
+ LOCAL_MODEL_FILE: str = Field(
29
+ default="SmolLM2-360M-Instruct-Q4_K_M.gguf",
30
+ validation_alias="LOCAL_MODEL_FILE"
31
+ )
32
+
33
+ # Generation Defaults
34
+ DEFAULT_TEMPERATURE: float = Field(default=0.7, validation_alias="DEFAULT_TEMPERATURE")
35
+ DEFAULT_MAX_TOKENS: int = Field(default=1024, validation_alias="DEFAULT_MAX_TOKENS")
36
+ DEFAULT_TOP_P: float = Field(default=0.9, validation_alias="DEFAULT_TOP_P")
37
+ DEFAULT_CONTEXT_LENGTH: int = Field(default=2048, validation_alias="DEFAULT_CONTEXT_LENGTH")
38
+
39
+ # Security / Limits
40
+ RATE_LIMIT_PER_MINUTE: int = Field(default=60, validation_alias="RATE_LIMIT_PER_MINUTE")
41
+ SECRET_KEY: str = Field(
42
+ default="dev-secret-key-must-be-changed-in-production-environments!",
43
+ validation_alias="SECRET_KEY"
44
+ )
45
+
46
+ @property
47
+ def cors_origins_list(self) -> List[str]:
48
+ return [origin.strip() for origin in self.CORS_ORIGINS.split(",") if origin.strip()]
49
+
50
+ model_config = SettingsConfigDict(
51
+ env_file=".env",
52
+ env_file_encoding="utf-8",
53
+ extra="ignore"
54
+ )
55
+
56
+ # Instantiate singleton settings
57
+ settings = Settings()
backend/app/llm/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # LLM Provider Adapters Package
backend/app/llm/base.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import AsyncIterator, List, Dict, Any, Optional
3
+
4
+ class BaseLLMProvider(ABC):
5
+ @abstractmethod
6
+ async def initialize(self) -> bool:
7
+ """Initialize the LLM provider. Returns True if successful."""
8
+ pass
9
+
10
+ @abstractmethod
11
+ async def generate(
12
+ self,
13
+ prompt: str,
14
+ system_prompt: Optional[str] = None,
15
+ messages: Optional[List[Dict[str, str]]] = None,
16
+ temperature: float = 0.7,
17
+ max_tokens: int = 1024,
18
+ top_p: float = 0.9,
19
+ ) -> Dict[str, Any]:
20
+ """
21
+ Execute non-streaming completion.
22
+ Returns a dict with format: { "content": str, "usage": dict }
23
+ """
24
+ pass
25
+
26
+ @abstractmethod
27
+ async def generate_stream(
28
+ self,
29
+ prompt: str,
30
+ system_prompt: Optional[str] = None,
31
+ messages: Optional[List[Dict[str, str]]] = None,
32
+ temperature: float = 0.7,
33
+ max_tokens: int = 1024,
34
+ top_p: float = 0.9,
35
+ ) -> AsyncIterator[str]:
36
+ """
37
+ Execute streaming completion.
38
+ Yields text chunks.
39
+ """
40
+ pass
41
+
42
+ @abstractmethod
43
+ def get_info(self) -> Dict[str, Any]:
44
+ """Return diagnostic and config information for the provider."""
45
+ pass
backend/app/llm/local.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import logging
4
+ import asyncio
5
+ from typing import AsyncIterator, List, Dict, Any, Optional
6
+ from backend.app.llm.base import BaseLLMProvider
7
+ from backend.app.config import settings
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Safe imports for environment portability
12
+ try:
13
+ from llama_cpp import Llama
14
+ except ImportError:
15
+ Llama = None
16
+ logger.warning("llama-cpp-python is not installed. Local inference will be unavailable.")
17
+
18
+ class LocalLLMProvider(BaseLLMProvider):
19
+ def __init__(self):
20
+ self.llm = None
21
+ self.initialized = False
22
+ self.model_path = os.path.abspath(settings.LOCAL_MODEL_PATH)
23
+ self.context_length = settings.DEFAULT_CONTEXT_LENGTH
24
+
25
+ async def initialize(self) -> bool:
26
+ if self.initialized:
27
+ return True
28
+
29
+ if Llama is None:
30
+ logger.error("Cannot initialize LocalLLMProvider: llama-cpp-python not installed.")
31
+ return False
32
+
33
+ if not os.path.exists(self.model_path):
34
+ logger.error(f"Local GGUF model file not found at: {self.model_path}")
35
+ return False
36
+
37
+ try:
38
+ # Determine thread count (default to CPU cores minus 1, min 1)
39
+ threads = max(1, (os.cpu_count() or 2) - 1)
40
+
41
+ logger.info(f"Loading local model from {self.model_path} with {threads} threads and context {self.context_length}...")
42
+
43
+ # Since loading the model blocks, run it in a separate thread to keep event loop active
44
+ def load_model():
45
+ return Llama(
46
+ model_path=self.model_path,
47
+ n_ctx=self.context_length,
48
+ n_threads=threads,
49
+ verbose=settings.DEBUG
50
+ )
51
+
52
+ self.llm = await asyncio.to_thread(load_model)
53
+ self.initialized = True
54
+ logger.info("Local model successfully loaded!")
55
+ return True
56
+ except Exception as e:
57
+ logger.error(f"Failed to load local model: {e}", exc_info=True)
58
+ self.initialized = False
59
+ self.llm = None
60
+ return False
61
+
62
+ def _format_prompt(self, prompt: str, system_prompt: Optional[str] = None, messages: Optional[List[Dict[str, str]]] = None) -> str:
63
+ # Build standard Qwen ChatML prompt format
64
+ formatted = ""
65
+
66
+ # Determine system prompt
67
+ sys_p = system_prompt or "You are Qwen, a helpful, precise, and state-of-the-art AI programming assistant."
68
+
69
+ # Check if messages already contain a system prompt
70
+ has_system = any(m.get("role") == "system" for m in messages) if messages else False
71
+ if not has_system:
72
+ formatted += f"<|im_start|>system\n{sys_p}<|im_end|>\n"
73
+
74
+ if messages:
75
+ for msg in messages:
76
+ role = msg.get("role", "user")
77
+ content = msg.get("content", "")
78
+ formatted += f"<|im_start|>{role}\n{content}<|im_end|>\n"
79
+ else:
80
+ # If no chat history is provided, construct a simple message pair
81
+ formatted += f"<|im_start|>user\n{prompt}<|im_end|>\n"
82
+
83
+ formatted += "<|im_start|>assistant\n"
84
+ return formatted
85
+
86
+ async def generate(
87
+ self,
88
+ prompt: str,
89
+ system_prompt: Optional[str] = None,
90
+ messages: Optional[List[Dict[str, str]]] = None,
91
+ temperature: float = 0.7,
92
+ max_tokens: int = 1024,
93
+ top_p: float = 0.9,
94
+ ) -> Dict[str, Any]:
95
+ if not await self.initialize():
96
+ raise RuntimeError("Local LLM provider is not initialized.")
97
+
98
+ formatted_prompt = self._format_prompt(prompt, system_prompt, messages)
99
+
100
+ def run_inference():
101
+ return self.llm(
102
+ prompt=formatted_prompt,
103
+ max_tokens=max_tokens,
104
+ temperature=temperature,
105
+ top_p=top_p,
106
+ stop=["<|im_end|>", "<|im_start|>", "im_end", "im_start"],
107
+ )
108
+
109
+ response = await asyncio.to_thread(run_inference)
110
+
111
+ content = response["choices"][0]["text"]
112
+ prompt_tokens = response["usage"]["prompt_tokens"]
113
+ completion_tokens = response["usage"]["completion_tokens"]
114
+
115
+ return {
116
+ "content": content,
117
+ "usage": {
118
+ "prompt_tokens": prompt_tokens,
119
+ "completion_tokens": completion_tokens,
120
+ "total_tokens": prompt_tokens + completion_tokens
121
+ }
122
+ }
123
+
124
+ async def generate_stream(
125
+ self,
126
+ prompt: str,
127
+ system_prompt: Optional[str] = None,
128
+ messages: Optional[List[Dict[str, str]]] = None,
129
+ temperature: float = 0.7,
130
+ max_tokens: int = 1024,
131
+ top_p: float = 0.9,
132
+ ) -> AsyncIterator[str]:
133
+ if not await self.initialize():
134
+ raise RuntimeError("Local LLM provider is not initialized.")
135
+
136
+ formatted_prompt = self._format_prompt(prompt, system_prompt, messages)
137
+
138
+ # Generator for streaming
139
+ def run_stream():
140
+ return self.llm(
141
+ prompt=formatted_prompt,
142
+ max_tokens=max_tokens,
143
+ temperature=temperature,
144
+ top_p=top_p,
145
+ stop=["<|im_end|>", "<|im_start|>", "im_end", "im_start"],
146
+ stream=True
147
+ )
148
+
149
+ stream = await asyncio.to_thread(run_stream)
150
+
151
+ async def async_generator():
152
+ for chunk in stream:
153
+ text = chunk["choices"][0]["text"]
154
+ if text:
155
+ yield text
156
+ # Yield CPU control to event loop
157
+ await asyncio.sleep(0)
158
+
159
+ return async_generator()
160
+
161
+ def get_info(self) -> Dict[str, Any]:
162
+ return {
163
+ "provider_name": "local",
164
+ "initialized": self.initialized,
165
+ "model_path": self.model_path,
166
+ "context_length": self.context_length,
167
+ "device": "CPU" # Llama.cpp runs on CPU in basic config, can use GPU via CUDA wrappers
168
+ }
backend/app/llm/manager.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import asyncio
4
+ from typing import AsyncIterator, Dict, Any, Optional
5
+ from backend.app.config import settings
6
+ from backend.app.llm.base import BaseLLMProvider
7
+ from backend.app.llm.local import LocalLLMProvider
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class LLMManager:
12
+ def __init__(self):
13
+ self.provider: Optional[BaseLLMProvider] = None
14
+ self.is_downloading: bool = False
15
+
16
+ async def ensure_model_downloaded(self) -> bool:
17
+ """Downloads the GGUF model file if not exists."""
18
+ model_path = os.path.abspath(settings.LOCAL_MODEL_PATH)
19
+ if os.path.exists(model_path):
20
+ logger.info(f"Model file already exists at: {model_path}")
21
+ return True
22
+
23
+ # Ensure directory exists
24
+ os.makedirs(os.path.dirname(model_path), exist_ok=True)
25
+
26
+ self.is_downloading = True
27
+ logger.info(f"Downloading model {settings.LOCAL_MODEL_FILE} from repo {settings.LOCAL_MODEL_REPO}...")
28
+
29
+ try:
30
+ from huggingface_hub import hf_hub_download
31
+
32
+ def download_job():
33
+ return hf_hub_download(
34
+ repo_id=settings.LOCAL_MODEL_REPO,
35
+ filename=settings.LOCAL_MODEL_FILE,
36
+ local_dir=os.path.dirname(model_path),
37
+ local_dir_use_symlinks=False
38
+ )
39
+
40
+ # Download model in background thread to not block the main application loop
41
+ await asyncio.to_thread(download_job)
42
+ logger.info("Model download complete!")
43
+ self.is_downloading = False
44
+ return True
45
+ except Exception as e:
46
+ logger.error(f"Failed to download model automatically: {e}", exc_info=True)
47
+ self.is_downloading = False
48
+ return False
49
+
50
+ async def setup_provider(self) -> BaseLLMProvider:
51
+ """
52
+ Initializes the local LLM provider strictly.
53
+ """
54
+ logger.info("Setting up local provider for SmolLM2...")
55
+ await self.ensure_model_downloaded()
56
+
57
+ local = LocalLLMProvider()
58
+ # Initialize in background, do not raise exception on failure to support diagnostics
59
+ await local.initialize()
60
+ self.provider = local
61
+ return local
62
+
63
+ async def get_active_provider(self) -> BaseLLMProvider:
64
+ """Returns the active provider. Lazily initializes if needed."""
65
+ if not self.provider:
66
+ await self.setup_provider()
67
+ return self.provider
68
+
69
+ async def generate(self, *args, **kwargs) -> Dict[str, Any]:
70
+ p = await self.get_active_provider()
71
+ return await p.generate(*args, **kwargs)
72
+
73
+ async def generate_stream(self, *args, **kwargs) -> AsyncIterator[str]:
74
+ p = await self.get_active_provider()
75
+ return await p.generate_stream(*args, **kwargs)
76
+
77
+ async def get_status_info(self) -> Dict[str, Any]:
78
+ """Returns diagnostic and current provider stats."""
79
+ p = await self.get_active_provider()
80
+ info = p.get_info()
81
+ info.update({
82
+ "configured_mode": "local",
83
+ "active_mode": "local",
84
+ "is_downloading": self.is_downloading
85
+ })
86
+ return info
87
+
88
+ # Instantiate global singleton manager
89
+ llm_manager = LLMManager()
backend/app/main.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import uuid
4
+ import logging
5
+ import asyncio
6
+ import psutil
7
+ from contextlib import asynccontextmanager
8
+ from typing import AsyncGenerator, Dict, Any
9
+
10
+ from fastapi import FastAPI, Depends, HTTPException, status
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from fastapi.responses import StreamingResponse, JSONResponse
13
+ from fastapi.staticfiles import StaticFiles
14
+
15
+ from backend.app.config import settings
16
+ from backend.app.models import (
17
+ ChatRequest, ChatResponse, ChatResponseChunk,
18
+ CodeRequest, CompletionRequest, ModelInfoResponse
19
+ )
20
+ from backend.app.middleware import RateLimitMiddleware, LoggingMiddleware
21
+ from backend.app.llm.manager import llm_manager
22
+ from backend.app.utils import (
23
+ estimate_tokens, format_sse_chunk,
24
+ get_code_prompt, get_completion_prompt
25
+ )
26
+
27
+ # Setup logging configuration
28
+ logging.basicConfig(
29
+ level=logging.INFO if not settings.DEBUG else logging.DEBUG,
30
+ format="%(asctime)s [%(levelname)s] %(name)s - %(message)s"
31
+ )
32
+ logger = logging.getLogger("backend.app.main")
33
+
34
+ # Metrics memory store
35
+ class SystemMetrics:
36
+ def __init__(self):
37
+ self.total_requests = 0
38
+ self.total_prompt_tokens = 0
39
+ self.total_completion_tokens = 0
40
+ self.total_generation_time_sec = 0.0
41
+
42
+ def add(self, prompt_tokens: int, completion_tokens: int, duration: float):
43
+ self.total_requests += 1
44
+ self.total_prompt_tokens += prompt_tokens
45
+ self.total_completion_tokens += completion_tokens
46
+ self.total_generation_time_sec += duration
47
+
48
+ def get_metrics_report(self) -> Dict[str, Any]:
49
+ avg_latency = (
50
+ self.total_generation_time_sec / self.total_requests
51
+ if self.total_requests > 0 else 0.0
52
+ )
53
+ return {
54
+ "total_requests": self.total_requests,
55
+ "total_prompt_tokens": self.total_prompt_tokens,
56
+ "total_completion_tokens": self.total_completion_tokens,
57
+ "total_tokens": self.total_prompt_tokens + self.total_completion_tokens,
58
+ "average_latency_seconds": round(avg_latency, 4),
59
+ "total_generation_time_seconds": round(self.total_generation_time_sec, 2)
60
+ }
61
+
62
+ metrics = SystemMetrics()
63
+
64
+ @asynccontextmanager
65
+ async def lifespan(app: FastAPI):
66
+ # Model warm-up on startup (non-blocking for fast server boot)
67
+ logger.info("Initializing LLM Manager and warming up model in the background...")
68
+ asyncio.create_task(llm_manager.setup_provider())
69
+ yield
70
+ # Shutdown operations
71
+ logger.info("Server shutting down.")
72
+
73
+ # Main app instantiation
74
+ app = FastAPI(
75
+ title="AI Coding Assistant API",
76
+ version="1.0.0",
77
+ lifespan=lifespan
78
+ )
79
+
80
+ # Add Middleware
81
+ app.add_middleware(LoggingMiddleware)
82
+ app.add_middleware(RateLimitMiddleware, limit=settings.RATE_LIMIT_PER_MINUTE)
83
+
84
+ app.add_middleware(
85
+ CORSMiddleware,
86
+ allow_origins=settings.cors_origins_list,
87
+ allow_credentials=True,
88
+ allow_methods=["*"],
89
+ allow_headers=["*"],
90
+ )
91
+
92
+ # Stream Generator Helper
93
+ async def run_stream_generator(
94
+ prompt: str,
95
+ system_prompt: str = None,
96
+ messages: list = None,
97
+ temperature: float = 0.7,
98
+ max_tokens: int = 1024,
99
+ ) -> AsyncGenerator[str, None]:
100
+ start_time = time.time()
101
+ generated_content = ""
102
+ prompt_tokens = estimate_tokens(prompt)
103
+ if messages:
104
+ for m in messages:
105
+ prompt_tokens += estimate_tokens(m.get("content", ""))
106
+
107
+ try:
108
+ stream = await llm_manager.generate_stream(
109
+ prompt=prompt,
110
+ system_prompt=system_prompt,
111
+ messages=messages,
112
+ temperature=temperature,
113
+ max_tokens=max_tokens
114
+ )
115
+
116
+ async for chunk in stream:
117
+ generated_content += chunk
118
+ # Format SSE yield
119
+ yield format_sse_chunk(content=chunk, done=False)
120
+
121
+ duration = time.time() - start_time
122
+ completion_tokens = estimate_tokens(generated_content)
123
+ metrics.add(prompt_tokens, completion_tokens, duration)
124
+
125
+ # Send final chunk with usage metrics
126
+ usage_data = {
127
+ "prompt_tokens": prompt_tokens,
128
+ "completion_tokens": completion_tokens,
129
+ "total_tokens": prompt_tokens + completion_tokens,
130
+ "duration_ms": int(duration * 1000),
131
+ "tokens_per_second": round(completion_tokens / duration, 2) if duration > 0 else 0
132
+ }
133
+ yield format_sse_chunk(content="", done=True, usage=usage_data)
134
+
135
+ except Exception as e:
136
+ logger.error(f"Error in running streaming generator: {e}")
137
+ yield format_sse_chunk(content=f"\n[Generation Error: {e}]", done=True)
138
+
139
+ # Standard Non-Stream Helper
140
+ async def run_standard_generation(
141
+ prompt: str,
142
+ system_prompt: str = None,
143
+ messages: list = None,
144
+ temperature: float = 0.7,
145
+ max_tokens: int = 1024,
146
+ ) -> ChatResponse:
147
+ start_time = time.time()
148
+ try:
149
+ response_data = await llm_manager.generate(
150
+ prompt=prompt,
151
+ system_prompt=system_prompt,
152
+ messages=messages,
153
+ temperature=temperature,
154
+ max_tokens=max_tokens
155
+ )
156
+
157
+ duration = time.time() - start_time
158
+ usage = response_data.get("usage", {})
159
+
160
+ prompt_tokens = usage.get("prompt_tokens", estimate_tokens(prompt))
161
+ completion_tokens = usage.get("completion_tokens", estimate_tokens(response_data["content"]))
162
+ metrics.add(prompt_tokens, completion_tokens, duration)
163
+
164
+ # Build enriched usage dict
165
+ metrics_dict = {
166
+ "prompt_tokens": prompt_tokens,
167
+ "completion_tokens": completion_tokens,
168
+ "total_tokens": prompt_tokens + completion_tokens,
169
+ "duration_ms": int(duration * 1000),
170
+ "tokens_per_second": round(completion_tokens / duration, 2) if duration > 0 else 0
171
+ }
172
+
173
+ status_info = await llm_manager.get_status_info()
174
+
175
+ return ChatResponse(
176
+ id=str(uuid.uuid4()),
177
+ content=response_data["content"],
178
+ usage=metrics_dict,
179
+ model=status_info.get("model_id", status_info.get("model_path", "qwen-0.5b"))
180
+ )
181
+ except Exception as e:
182
+ logger.error(f"Error in standard generation: {e}")
183
+ raise HTTPException(
184
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
185
+ detail=str(e)
186
+ )
187
+
188
+ # API ENDPOINTS
189
+ @app.post("/chat")
190
+ async def chat(request: ChatRequest):
191
+ messages_list = [m.model_dump() for m in request.messages]
192
+
193
+ # Extract system prompt if present in payload
194
+ system_prompt = None
195
+ user_messages = []
196
+ for msg in messages_list:
197
+ if msg["role"] == "system":
198
+ system_prompt = msg["content"]
199
+ else:
200
+ user_messages.append(msg)
201
+
202
+ # The last user message is the primary prompt
203
+ last_prompt = user_messages[-1]["content"] if user_messages else ""
204
+
205
+ if request.stream:
206
+ return StreamingResponse(
207
+ run_stream_generator(
208
+ prompt=last_prompt,
209
+ system_prompt=system_prompt,
210
+ messages=messages_list,
211
+ temperature=request.temperature or settings.DEFAULT_TEMPERATURE,
212
+ max_tokens=request.max_tokens or settings.DEFAULT_MAX_TOKENS
213
+ ),
214
+ media_type="text/event-stream"
215
+ )
216
+ else:
217
+ return await run_standard_generation(
218
+ prompt=last_prompt,
219
+ system_prompt=system_prompt,
220
+ messages=messages_list,
221
+ temperature=request.temperature or settings.DEFAULT_TEMPERATURE,
222
+ max_tokens=request.max_tokens or settings.DEFAULT_MAX_TOKENS
223
+ )
224
+
225
+ @app.post("/complete")
226
+ async def complete(request: CompletionRequest):
227
+ prompt = get_completion_prompt(request.prefix, request.suffix, request.language or "python")
228
+
229
+ # We want low temperature for completion tasks
230
+ temp = request.temperature or 0.2
231
+ max_t = request.max_tokens or 128
232
+
233
+ if request.stream:
234
+ return StreamingResponse(
235
+ run_stream_generator(prompt=prompt, temperature=temp, max_tokens=max_t),
236
+ media_type="text/event-stream"
237
+ )
238
+ else:
239
+ return await run_standard_generation(prompt=prompt, temperature=temp, max_tokens=max_t)
240
+
241
+ # Helper decorator for modular code API endpoints
242
+ def make_code_endpoint(action: str):
243
+ async def endpoint(request: CodeRequest):
244
+ prompt = get_code_prompt(action, request.code, request.language or "python", request.context)
245
+ temp = request.temperature or settings.DEFAULT_TEMPERATURE
246
+ max_t = request.max_tokens or settings.DEFAULT_MAX_TOKENS
247
+
248
+ if request.stream:
249
+ return StreamingResponse(
250
+ run_stream_generator(prompt=prompt, temperature=temp, max_tokens=max_t),
251
+ media_type="text/event-stream"
252
+ )
253
+ else:
254
+ return await run_standard_generation(prompt=prompt, temperature=temp, max_tokens=max_t)
255
+ return endpoint
256
+
257
+ # Register code utility endpoints
258
+ app.post("/explain")(make_code_endpoint("explain"))
259
+ app.post("/debug")(make_code_endpoint("debug"))
260
+ app.post("/refactor")(make_code_endpoint("refactor"))
261
+ app.post("/generate-tests")(make_code_endpoint("generate-tests"))
262
+ app.post("/summarize")(make_code_endpoint("summarize"))
263
+
264
+ @app.get("/health")
265
+ async def health():
266
+ # Health check responds instantly for cloud pingers
267
+ status_info = await llm_manager.get_status_info()
268
+ return {
269
+ "status": "healthy",
270
+ "timestamp": time.time(),
271
+ "active_mode": status_info.get("active_mode"),
272
+ "local_model_loaded": status_info.get("initialized", False),
273
+ "is_downloading": status_info.get("is_downloading", False)
274
+ }
275
+
276
+ @app.get("/metrics")
277
+ async def get_metrics():
278
+ # Enriches metrics with current server state
279
+ report = metrics.get_metrics_report()
280
+ status_info = await llm_manager.get_status_info()
281
+ ram_stats = psutil.virtual_memory()
282
+ report.update({
283
+ "active_mode": "local",
284
+ "system_ram_gb": round(ram_stats.total / (1024 ** 3), 2),
285
+ "device": status_info.get("device", "CPU")
286
+ })
287
+ return report
288
+
289
+ @app.get("/model-info", response_model=ModelInfoResponse)
290
+ async def get_model_info():
291
+ status_info = await llm_manager.get_status_info()
292
+
293
+ # Calculate memory stats
294
+ ram_stats = psutil.virtual_memory()
295
+ total_gb = ram_stats.total / (1024 ** 3)
296
+ used_gb = ram_stats.used / (1024 ** 3)
297
+
298
+ local_exists = os.path.exists(settings.LOCAL_MODEL_PATH)
299
+
300
+ return ModelInfoResponse(
301
+ model_name=os.path.basename(settings.LOCAL_MODEL_PATH),
302
+ inference_mode="local",
303
+ status="loaded" if status_info.get("initialized") else "loading",
304
+ memory_usage_gb=round(used_gb, 2),
305
+ total_memory_gb=round(total_gb, 2),
306
+ local_model_exists=local_exists,
307
+ local_model_path=settings.LOCAL_MODEL_PATH,
308
+ device=status_info.get("device", "CPU")
309
+ )
310
+
311
+ # Serve Frontend static assets if available (production mode)
312
+ frontend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "frontend", "dist"))
313
+ if os.path.exists(frontend_dir):
314
+ app.mount("/", StaticFiles(directory=frontend_dir, html=True), name="frontend")
315
+ logger.info(f"Frontend dist found. Serving frontend from {frontend_dir}")
316
+ else:
317
+ logger.warning(f"Frontend dist not found at {frontend_dir}. Running API-only server mode.")
backend/app/middleware.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import logging
3
+ from collections import defaultdict
4
+ from fastapi import Request, Response, status
5
+ from fastapi.responses import JSONResponse
6
+ from starlette.middleware.base import BaseHTTPMiddleware
7
+ from backend.app.config import settings
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class RateLimiter:
12
+ def __init__(self, requests_per_minute: int):
13
+ self.limit = requests_per_minute
14
+ self.clients = defaultdict(list)
15
+
16
+ def is_allowed(self, ip: str) -> bool:
17
+ now = time.time()
18
+ # Clean up requests older than 60 seconds
19
+ self.clients[ip] = [req_time for req_time in self.clients[ip] if now - req_time < 60]
20
+
21
+ if len(self.clients[ip]) >= self.limit:
22
+ return False
23
+
24
+ self.clients[ip].append(now)
25
+ return True
26
+
27
+ class RateLimitMiddleware(BaseHTTPMiddleware):
28
+ def __init__(self, app, limit: int = None):
29
+ super().__init__(app)
30
+ self.limiter = RateLimiter(limit or settings.RATE_LIMIT_PER_MINUTE)
31
+
32
+ async def dispatch(self, request: Request, call_next) -> Response:
33
+ # Bypass rate limits for health and metric endpoints
34
+ if request.url.path in ["/health", "/metrics", "/model-info"]:
35
+ return await call_next(request)
36
+
37
+ # Get client IP
38
+ client_ip = request.client.host if request.client else "unknown"
39
+
40
+ if not self.limiter.is_allowed(client_ip):
41
+ logger.warning(f"Rate limit exceeded for client: {client_ip} on path {request.url.path}")
42
+ return JSONResponse(
43
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
44
+ content={"detail": "Too many requests. Please try again in a minute."}
45
+ )
46
+
47
+ return await call_next(request)
48
+
49
+ class LoggingMiddleware(BaseHTTPMiddleware):
50
+ async def dispatch(self, request: Request, call_next) -> Response:
51
+ start_time = time.time()
52
+ client_ip = request.client.host if request.client else "unknown"
53
+ logger.info(f"Incoming: {request.method} {request.url.path} from {client_ip}")
54
+
55
+ try:
56
+ response = await call_next(request)
57
+ duration = time.time() - start_time
58
+ logger.info(
59
+ f"Outgoing: {request.method} {request.url.path} - "
60
+ f"Status: {response.status_code} - Duration: {duration:.4f}s"
61
+ )
62
+ return response
63
+ except Exception as e:
64
+ duration = time.time() - start_time
65
+ logger.error(
66
+ f"Exception: {request.method} {request.url.path} failed - "
67
+ f"Error: {e} - Duration: {duration:.4f}s",
68
+ exc_info=True
69
+ )
70
+ return JSONResponse(
71
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
72
+ content={"detail": "Internal server error occurred."}
73
+ )
backend/app/models.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional, Dict, Any
2
+ from pydantic import BaseModel, Field
3
+
4
+ class ChatMessage(BaseModel):
5
+ role: str = Field(..., description="Role of the message author (system, user, assistant)")
6
+ content: str = Field(..., description="Content of the message")
7
+
8
+ class ChatRequest(BaseModel):
9
+ messages: List[ChatMessage] = Field(..., description="Conversation history including current prompt")
10
+ temperature: Optional[float] = Field(None, description="Controls randomness (0.0 to 1.0)")
11
+ max_tokens: Optional[int] = Field(None, description="Maximum number of tokens to generate")
12
+ top_p: Optional[float] = Field(None, description="Nucleus sampling threshold")
13
+ stream: Optional[bool] = Field(True, description="Whether to stream responses")
14
+
15
+ class CodeRequest(BaseModel):
16
+ code: str = Field(..., description="The code snippet to process")
17
+ language: Optional[str] = Field("python", description="Programming language of the code")
18
+ context: Optional[str] = Field(None, description="Additional developer instructions or query context")
19
+ temperature: Optional[float] = Field(None)
20
+ max_tokens: Optional[int] = Field(None)
21
+ stream: Optional[bool] = Field(True)
22
+
23
+ class CompletionRequest(BaseModel):
24
+ prefix: str = Field(..., description="Code before the cursor position")
25
+ suffix: Optional[str] = Field("", description="Code after the cursor position")
26
+ language: Optional[str] = Field("python")
27
+ max_tokens: Optional[int] = Field(128)
28
+ temperature: Optional[float] = Field(0.2)
29
+ stream: Optional[bool] = Field(False)
30
+
31
+ class ChatResponseChunk(BaseModel):
32
+ content: str
33
+ done: bool = False
34
+ usage: Optional[Dict[str, Any]] = None
35
+
36
+ class ChatResponse(BaseModel):
37
+ id: str
38
+ content: str
39
+ usage: Dict[str, Any]
40
+ model: str
41
+
42
+ class ModelInfoResponse(BaseModel):
43
+ model_name: str
44
+ inference_mode: str # "local" or "huggingface"
45
+ status: str # "loaded", "error", "fallback"
46
+ memory_usage_gb: float
47
+ total_memory_gb: float
48
+ local_model_exists: bool
49
+ local_model_path: str
50
+ device: str # "cpu", "cuda", etc.
backend/app/utils.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ from typing import Dict, Any, List, Optional
3
+
4
+ def estimate_tokens(text: str) -> int:
5
+ """
6
+ Estimates token count for Qwen/GPT-like models without heavy tokenizers.
7
+ Rule of thumb: 1 token ≈ 4 characters, or ~1.3 tokens per word.
8
+ """
9
+ if not text:
10
+ return 0
11
+ # Average of char-based and word-based estimations
12
+ char_est = len(text) / 4.0
13
+ word_est = len(text.split()) * 1.3
14
+ return int((char_est + word_est) / 2.0) + 1
15
+
16
+ def format_sse_chunk(content: str, done: bool = False, usage: Optional[Dict[str, Any]] = None) -> str:
17
+ """Format data for Server-Sent Events transmission."""
18
+ data = {
19
+ "content": content,
20
+ "done": done,
21
+ "usage": usage
22
+ }
23
+ return f"data: {json_dumps(data)}\n\n"
24
+
25
+ def json_dumps(obj: Any) -> str:
26
+ # A safe helper for JSON serialization
27
+ import json
28
+ return json.dumps(obj)
29
+
30
+ def get_code_prompt(action: str, code: str, language: str, context: Optional[str] = None) -> str:
31
+ """Returns specialized system/user prompts for code tasks."""
32
+ prompt_templates = {
33
+ "explain": (
34
+ "You are an expert software engineer. Explain this code step-by-step. "
35
+ "Highlight the flow of execution, core algorithms, and potential performance implications. "
36
+ "Write the explanation in clear, readable markdown.\n\n"
37
+ f"Language: {language}\n"
38
+ f"Code:\n```\n{code}\n```"
39
+ ),
40
+ "debug": (
41
+ "You are a senior debugger. Identify errors, logical bugs, edge cases, memory leaks, "
42
+ "or security vulnerabilities in the code below. Explain each issue found and "
43
+ "provide a corrected version of the code, indicating what changes were made.\n\n"
44
+ f"Language: {language}\n"
45
+ f"Code:\n```\n{code}\n```"
46
+ ),
47
+ "refactor": (
48
+ "You are a principal engineer. Refactor this code to improve its readability, "
49
+ "efficiency, and modularity. Adhere to SOLID principles and industry design patterns. "
50
+ "Provide the refactored code and list the improvements.\n\n"
51
+ f"Language: {language}\n"
52
+ f"Code:\n```\n{code}\n```"
53
+ ),
54
+ "generate-tests": (
55
+ "You are a QA automation lead. Write comprehensive unit tests for the following code snippet. "
56
+ "Cover typical inputs, edge cases, and error conditions. Use standard testing libraries.\n\n"
57
+ f"Language: {language}\n"
58
+ f"Code:\n```\n{code}\n```"
59
+ ),
60
+ "summarize": (
61
+ "Provide a brief, high-level summary of what this code does in 2-3 sentences. "
62
+ "Focus on the main inputs, transformations, and outputs.\n\n"
63
+ f"Language: {language}\n"
64
+ f"Code:\n```\n{code}\n```"
65
+ )
66
+ }
67
+
68
+ prompt = prompt_templates.get(action, f"Analyze the following code:\n\n```{language}\n{code}\n```")
69
+ if context:
70
+ prompt = f"Context/Instructions: {context}\n\n{prompt}"
71
+ return prompt
72
+
73
+ def get_completion_prompt(prefix: str, suffix: str, language: str) -> str:
74
+ """Constructs instructions for code completion."""
75
+ return (
76
+ f"You are a code auto-completion utility. Continue the code for the {language} language. "
77
+ "Your task is to fill in the missing code between the Prefix and Suffix. "
78
+ "Return ONLY the code that should be inserted directly at the transition point. "
79
+ "Do NOT write any explanations. Do NOT wrap your response in markdown code blocks.\n\n"
80
+ f"--- PREFIX ---\n{prefix}\n"
81
+ f"--- SUFFIX ---\n{suffix}\n"
82
+ "--- INSERT COMPLETED CODE BELOW ---"
83
+ )
backend/requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.100.0
2
+ uvicorn>=0.22.0
3
+ pydantic>=2.0
4
+ pydantic-settings>=2.0
5
+ python-dotenv>=1.0.0
6
+ requests>=2.31.0
7
+ httpx>=0.24.0
8
+ huggingface_hub>=0.16.0
9
+ psutil>=5.9.0
10
+ llama-cpp-python>=0.2.0
backend/run.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uvicorn
2
+ from backend.app.config import settings
3
+
4
+ if __name__ == "__main__":
5
+ print(f"Starting server on http://{settings.HOST}:{settings.PORT}")
6
+ uvicorn.run(
7
+ "backend.app.main:app",
8
+ host=settings.HOST,
9
+ port=settings.PORT,
10
+ reload=settings.DEBUG
11
+ )
docker/Dockerfile.backend ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage build to reduce final image size
2
+ FROM python:3.11-slim as builder
3
+
4
+ WORKDIR /app
5
+
6
+ # Install compilation tools needed for compiling llama-cpp-python
7
+ RUN apt-get update && apt-get install -y --no-install-recommends \
8
+ build-essential \
9
+ gcc \
10
+ g++ \
11
+ make \
12
+ python3-dev \
13
+ git \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # Copy backend requirements
17
+ COPY backend/requirements.txt .
18
+
19
+ # Install dependencies and build llama-cpp-python
20
+ RUN pip install --no-cache-dir --user -r requirements.txt
21
+
22
+ # Final production stage
23
+ FROM python:3.11-slim
24
+
25
+ RUN apt-get update && apt-get install -y --no-install-recommends \
26
+ libgomp1 \
27
+ && rm -rf /var/lib/apt/lists/*
28
+
29
+ WORKDIR /app
30
+
31
+ # Copy installed site-packages from builder stage
32
+ COPY --from=builder /root/.local /root/.local
33
+ ENV PATH=/root/.local/bin:$PATH
34
+
35
+ # Copy backend codebase
36
+ COPY backend/ /app/backend/
37
+
38
+ ENV PORT=8000
39
+ ENV HOST=0.0.0.0
40
+ ENV PYTHONPATH=/app
41
+
42
+ EXPOSE 8000
43
+
44
+ # Start command
45
+ CMD ["python", "backend/run.py"]
docker/Dockerfile.frontend ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY frontend/package*.json ./
6
+
7
+ RUN npm install
8
+
9
+ COPY frontend/ ./
10
+
11
+ EXPOSE 5173
12
+
13
+ CMD ["npm", "run", "dev", "--", "--host"]
docker/docker-compose.yml ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ backend:
5
+ build:
6
+ context: ../
7
+ dockerfile: docker/Dockerfile.backend
8
+ ports:
9
+ - "8000:8000"
10
+ volumes:
11
+ - ../backend:/app/backend
12
+ - ../models:/app/models
13
+ environment:
14
+ - PORT=8000
15
+ - HOST=0.0.0.0
16
+ - DEBUG=true
17
+ - INFERENCE_MODE=local
18
+ - LOCAL_MODEL_PATH=models/SmolLM2-360M-Instruct-Q4_K_M.gguf
19
+ - CORS_ORIGINS=http://localhost:5173,http://localhost:8000
20
+ restart: unless-stopped
21
+
22
+ frontend:
23
+ build:
24
+ context: ../
25
+ dockerfile: docker/Dockerfile.frontend
26
+ ports:
27
+ - "5173:5173"
28
+ volumes:
29
+ - ../frontend:/app
30
+ - /app/node_modules
31
+ environment:
32
+ - VITE_API_URL=http://backend:8000
33
+ command: npm run dev -- --host --force
34
+ depends_on:
35
+ - backend
36
+ restart: unless-stopped
docs/API.md ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # API Reference Documentation 📖
2
+
3
+ This document details the REST API specifications for the Antigravity Coding Assistant.
4
+
5
+ All APIs use JSON request bodies. Streaming endpoints support Server-Sent Events (`text/event-stream`).
6
+
7
+ ---
8
+
9
+ ## Endpoints Summary
10
+
11
+ | Method | Path | Description | Streaming Support |
12
+ | :--- | :--- | :--- | :--- |
13
+ | `POST` | `/chat` | Chat message handler | Yes |
14
+ | `POST` | `/complete` | Code auto-completion | Yes |
15
+ | `POST` | `/explain` | Generates explanation for a code block | Yes |
16
+ | `POST` | `/debug` | Locates bugs and provides fixes | Yes |
17
+ | `POST` | `/refactor` | Refactors code for quality & SOLID rules | Yes |
18
+ | `POST` | `/generate-tests`| Creates automated unit tests | Yes |
19
+ | `POST` | `/summarize` | Summarizes code in 2-3 sentences | Yes |
20
+ | `GET` | `/health` | Check backend service health | No |
21
+ | `GET` | `/metrics` | Get performance metrics telemetry | No |
22
+ | `GET` | `/model-info` | Check active LLM provider specifications| No |
23
+
24
+ ---
25
+
26
+ ## Detailed Specifications
27
+
28
+ ### 1. POST `/chat`
29
+ Generates conversational assistance based on prompt history.
30
+
31
+ **Request Payload:**
32
+ ```json
33
+ {
34
+ "messages": [
35
+ { "role": "system", "content": "You are a coding assistant." },
36
+ { "role": "user", "content": "Write a bubble sort in Python." }
37
+ ],
38
+ "temperature": 0.7,
39
+ "max_tokens": 1024,
40
+ "top_p": 0.9,
41
+ "stream": true
42
+ }
43
+ ```
44
+
45
+ ---
46
+
47
+ ### 2. POST `/complete`
48
+ Fills in missing code between a prefix and suffix (Fill-in-the-Middle).
49
+
50
+ **Request Payload:**
51
+ ```json
52
+ {
53
+ "prefix": "def add_numbers(a, b):\n ",
54
+ "suffix": "\n\nprint(add_numbers(5, 10))",
55
+ "language": "python",
56
+ "max_tokens": 64,
57
+ "temperature": 0.1,
58
+ "stream": false
59
+ }
60
+ ```
61
+
62
+ ---
63
+
64
+ ### 3. POST `/explain` | `/debug` | `/refactor` | `/generate-tests` | `/summarize`
65
+ Specialized endpoints for code-focused instructions.
66
+
67
+ **Request Payload:**
68
+ ```json
69
+ {
70
+ "code": "def double(x): return x * 2",
71
+ "language": "python",
72
+ "context": "Optimize this code",
73
+ "temperature": 0.7,
74
+ "max_tokens": 1024,
75
+ "stream": true
76
+ }
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Response Formats
82
+
83
+ ### Non-Streaming Response
84
+ Returned when `stream` parameter is `false`:
85
+ ```json
86
+ {
87
+ "id": "uuid-string-here",
88
+ "content": "Generated text or code review block here",
89
+ "usage": {
90
+ "prompt_tokens": 42,
91
+ "completion_tokens": 128,
92
+ "total_tokens": 170,
93
+ "duration_ms": 1240,
94
+ "tokens_per_second": 103.2
95
+ },
96
+ "model": "bartowski/Qwen2.5-Coder-0.5B-Instruct-GGUF"
97
+ }
98
+ ```
99
+
100
+ ### Streaming Response (SSE)
101
+ Returned when `stream` parameter is `true`. Standard SSE format:
102
+ ```
103
+ data: {"content": "Hello", "done": false}
104
+
105
+ data: {"content": " World", "done": false}
106
+
107
+ data: {"content": "", "done": true, "usage": {"prompt_tokens": 12, "completion_tokens": 2, "total_tokens": 14, "duration_ms": 230, "tokens_per_second": 8.7}}
108
+ ```
109
+ Each data line represents a single generated text token. The final SSE chunk contains `done: true` and the generation metrics.
docs/DEPLOYMENT.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Cloud Deployment Guide 🌐
2
+
3
+ This document covers instructions on deploying the unified **Antigravity AI Coder** container to Railway and Render.
4
+
5
+ ---
6
+
7
+ ## <a name="railway"></a> 🚄 Railway Deployment (Preferred)
8
+
9
+ Railway is the recommended host because of its native support for Dockerfile builds, fast container deployments, and reliable persistent volumes.
10
+
11
+ ### Step 1: Create a Railway Project
12
+ 1. Log into your [Railway Console](https://railway.app/).
13
+ 2. Click **New Project** -> **Deploy from GitHub repo**.
14
+ 3. Select your repository.
15
+
16
+ ### Step 2: Configure Environment Variables
17
+ Add the following variables in Railway's **Variables** tab:
18
+ * `PORT` = `8000`
19
+ * `HOST` = `0.0.0.0`
20
+ * `INFERENCE_MODE` = `auto` (Routes to Hugging Face Cloud if container RAM is limited)
21
+ * `HF_API_TOKEN` = `your_huggingface_api_token` (Strongly recommended to avoid rate limits)
22
+ * `HF_MODEL_ID` = `Qwen/Qwen2.5-Coder-0.5B-Instruct`
23
+ * `SECRET_KEY` = `generate-a-long-random-string`
24
+
25
+ ### Step 3: Mount a Persistent Volume (Optional but Recommended)
26
+ To prevent the container from re-downloading the 397MB local model file on every restart:
27
+ 1. In the service settings, click **Volume** -> **Add Volume**.
28
+ 2. Mount the volume to: `/app/models`.
29
+ 3. Save the changes. Railway will now persist the downloaded GGUF file inside this volume.
30
+
31
+ ---
32
+
33
+ ## <a name="render"></a> 💎 Render Deployment
34
+
35
+ Render supports Docker builds out of the box using our blueprint `render.yaml` configuration.
36
+
37
+ ### Step 1: Deploy using render.yaml Blueprint
38
+ 1. Log into your [Render Dashboard](https://dashboard.render.com/).
39
+ 2. Click **New** -> **Blueprint**.
40
+ 3. Link your repository. Render will automatically read the `render.yaml` file from your repo root.
41
+ 4. Render will prompt you for:
42
+ * **Service Name:** `antigravity-ai-coder`
43
+ * **HF_API_TOKEN:** Provide your Hugging Face API key.
44
+
45
+ ### Step 2: Custom Scaling & Memory Fallback
46
+ * **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.
47
+ * **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.
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/.oxlintrc.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "plugins": ["react", "typescript", "oxc"],
4
+ "rules": {
5
+ "react/rules-of-hooks": "error",
6
+ "react/only-export-components": ["warn", { "allowConstantExport": true }]
7
+ }
8
+ }
frontend/README.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ 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).
13
+
14
+ ## Expanding the Oxlint configuration
15
+
16
+ If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
17
+
18
+ ```json
19
+ {
20
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
21
+ "plugins": ["react", "typescript", "oxc"],
22
+ "options": {
23
+ "typeAware": true
24
+ },
25
+ "rules": {
26
+ "react/rules-of-hooks": "error",
27
+ "react/only-export-components": ["warn", { "allowConstantExport": true }]
28
+ }
29
+ }
30
+ ```
31
+
32
+ See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
frontend/index.html ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="Production-ready AI Coding Assistant running local Qwen2.5-Coder GGUF model with Hugging Face API fallback, Monaco Editor integration, and code playground." />
7
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚡</text></svg>" />
8
+ <title>Antigravity AI Coder | Qwen2.5-Coder Assistant</title>
9
+ </head>
10
+ <body class="bg-background text-foreground transition-colors duration-200">
11
+ <div id="root"></div>
12
+ <script type="module" src="/src/main.tsx"></script>
13
+ </body>
14
+ </html>
frontend/package-lock.json ADDED
@@ -0,0 +1,1909 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "frontend",
9
+ "version": "0.0.0",
10
+ "dependencies": {
11
+ "@monaco-editor/react": "^4.7.0",
12
+ "@tailwindcss/vite": "^4.3.2",
13
+ "@tanstack/react-query": "^5.101.2",
14
+ "clsx": "^2.1.1",
15
+ "framer-motion": "^12.42.2",
16
+ "lucide-react": "^1.23.0",
17
+ "react": "^19.2.7",
18
+ "react-dom": "^19.2.7",
19
+ "react-router-dom": "^7.18.1",
20
+ "tailwind-merge": "^3.6.0",
21
+ "tailwindcss": "^4.3.2"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^24.13.2",
25
+ "@types/react": "^19.2.17",
26
+ "@types/react-dom": "^19.2.3",
27
+ "@vitejs/plugin-react": "^6.0.3",
28
+ "oxlint": "^1.71.0",
29
+ "typescript": "~6.0.2",
30
+ "vite": "^8.1.1"
31
+ }
32
+ },
33
+ "node_modules/@emnapi/core": {
34
+ "version": "1.11.1",
35
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
36
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
37
+ "license": "MIT",
38
+ "optional": true,
39
+ "dependencies": {
40
+ "@emnapi/wasi-threads": "1.2.2",
41
+ "tslib": "^2.4.0"
42
+ }
43
+ },
44
+ "node_modules/@emnapi/runtime": {
45
+ "version": "1.11.1",
46
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
47
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
48
+ "license": "MIT",
49
+ "optional": true,
50
+ "dependencies": {
51
+ "tslib": "^2.4.0"
52
+ }
53
+ },
54
+ "node_modules/@emnapi/wasi-threads": {
55
+ "version": "1.2.2",
56
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
57
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
58
+ "license": "MIT",
59
+ "optional": true,
60
+ "dependencies": {
61
+ "tslib": "^2.4.0"
62
+ }
63
+ },
64
+ "node_modules/@jridgewell/gen-mapping": {
65
+ "version": "0.3.13",
66
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
67
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
68
+ "license": "MIT",
69
+ "dependencies": {
70
+ "@jridgewell/sourcemap-codec": "^1.5.0",
71
+ "@jridgewell/trace-mapping": "^0.3.24"
72
+ }
73
+ },
74
+ "node_modules/@jridgewell/remapping": {
75
+ "version": "2.3.5",
76
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
77
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
78
+ "license": "MIT",
79
+ "dependencies": {
80
+ "@jridgewell/gen-mapping": "^0.3.5",
81
+ "@jridgewell/trace-mapping": "^0.3.24"
82
+ }
83
+ },
84
+ "node_modules/@jridgewell/resolve-uri": {
85
+ "version": "3.1.2",
86
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
87
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
88
+ "license": "MIT",
89
+ "engines": {
90
+ "node": ">=6.0.0"
91
+ }
92
+ },
93
+ "node_modules/@jridgewell/sourcemap-codec": {
94
+ "version": "1.5.5",
95
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
96
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
97
+ "license": "MIT"
98
+ },
99
+ "node_modules/@jridgewell/trace-mapping": {
100
+ "version": "0.3.31",
101
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
102
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
103
+ "license": "MIT",
104
+ "dependencies": {
105
+ "@jridgewell/resolve-uri": "^3.1.0",
106
+ "@jridgewell/sourcemap-codec": "^1.4.14"
107
+ }
108
+ },
109
+ "node_modules/@monaco-editor/loader": {
110
+ "version": "1.7.0",
111
+ "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
112
+ "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
113
+ "license": "MIT",
114
+ "dependencies": {
115
+ "state-local": "^1.0.6"
116
+ }
117
+ },
118
+ "node_modules/@monaco-editor/react": {
119
+ "version": "4.7.0",
120
+ "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
121
+ "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
122
+ "license": "MIT",
123
+ "dependencies": {
124
+ "@monaco-editor/loader": "^1.5.0"
125
+ },
126
+ "peerDependencies": {
127
+ "monaco-editor": ">= 0.25.0 < 1",
128
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
129
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
130
+ }
131
+ },
132
+ "node_modules/@napi-rs/wasm-runtime": {
133
+ "version": "1.1.6",
134
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
135
+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
136
+ "license": "MIT",
137
+ "optional": true,
138
+ "dependencies": {
139
+ "@tybys/wasm-util": "^0.10.3"
140
+ },
141
+ "funding": {
142
+ "type": "github",
143
+ "url": "https://github.com/sponsors/Brooooooklyn"
144
+ },
145
+ "peerDependencies": {
146
+ "@emnapi/core": "^1.7.1",
147
+ "@emnapi/runtime": "^1.7.1"
148
+ }
149
+ },
150
+ "node_modules/@oxc-project/types": {
151
+ "version": "0.138.0",
152
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz",
153
+ "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==",
154
+ "license": "MIT",
155
+ "funding": {
156
+ "url": "https://github.com/sponsors/Boshen"
157
+ }
158
+ },
159
+ "node_modules/@oxlint/binding-android-arm-eabi": {
160
+ "version": "1.72.0",
161
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.72.0.tgz",
162
+ "integrity": "sha512-zhCmvn+1Mj3UchAc/90i99S0t7jJUsHmFVSPg4UWrjO8b8eaSGwscgO6QAUtvHBstkjQwBttQNswEnAF1mIQdA==",
163
+ "cpu": [
164
+ "arm"
165
+ ],
166
+ "dev": true,
167
+ "license": "MIT",
168
+ "optional": true,
169
+ "os": [
170
+ "android"
171
+ ],
172
+ "engines": {
173
+ "node": "^20.19.0 || >=22.12.0"
174
+ }
175
+ },
176
+ "node_modules/@oxlint/binding-android-arm64": {
177
+ "version": "1.72.0",
178
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.72.0.tgz",
179
+ "integrity": "sha512-mtH+aY/ozv1eZoCUC2owjFAtyNBKHpJHygKeEu9zXXnQGW1Q2/qOpvx+I+Lf23+TvTz66F4iiXUbl2cGvoLPCQ==",
180
+ "cpu": [
181
+ "arm64"
182
+ ],
183
+ "dev": true,
184
+ "license": "MIT",
185
+ "optional": true,
186
+ "os": [
187
+ "android"
188
+ ],
189
+ "engines": {
190
+ "node": "^20.19.0 || >=22.12.0"
191
+ }
192
+ },
193
+ "node_modules/@oxlint/binding-darwin-arm64": {
194
+ "version": "1.72.0",
195
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.72.0.tgz",
196
+ "integrity": "sha512-EvnajNPDtfknB3ZieeOOyDTwJn9QXDiwfnF4ZDQqART6RG6hjY4WigQcZdGoK2dkB3e1vrmEzN9aYbQCUkh/gQ==",
197
+ "cpu": [
198
+ "arm64"
199
+ ],
200
+ "dev": true,
201
+ "license": "MIT",
202
+ "optional": true,
203
+ "os": [
204
+ "darwin"
205
+ ],
206
+ "engines": {
207
+ "node": "^20.19.0 || >=22.12.0"
208
+ }
209
+ },
210
+ "node_modules/@oxlint/binding-darwin-x64": {
211
+ "version": "1.72.0",
212
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.72.0.tgz",
213
+ "integrity": "sha512-ZkCdEa/G80A7vEHfeCDz/+L3m33DE73v32mDKhgOIgz8Uwf0DFcK7+uu6qC+7LEhmz5fpOe1osWKyjSNMydFIQ==",
214
+ "cpu": [
215
+ "x64"
216
+ ],
217
+ "dev": true,
218
+ "license": "MIT",
219
+ "optional": true,
220
+ "os": [
221
+ "darwin"
222
+ ],
223
+ "engines": {
224
+ "node": "^20.19.0 || >=22.12.0"
225
+ }
226
+ },
227
+ "node_modules/@oxlint/binding-freebsd-x64": {
228
+ "version": "1.72.0",
229
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.72.0.tgz",
230
+ "integrity": "sha512-NroXv2vh+sxVY1uya/rM5pjhx1hm8BzlYpx9q67QP0Xhw5MH2bf5GJylpvLEC+781p1Xli/317EoV9AlGwViag==",
231
+ "cpu": [
232
+ "x64"
233
+ ],
234
+ "dev": true,
235
+ "license": "MIT",
236
+ "optional": true,
237
+ "os": [
238
+ "freebsd"
239
+ ],
240
+ "engines": {
241
+ "node": "^20.19.0 || >=22.12.0"
242
+ }
243
+ },
244
+ "node_modules/@oxlint/binding-linux-arm-gnueabihf": {
245
+ "version": "1.72.0",
246
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.72.0.tgz",
247
+ "integrity": "sha512-0NDywYgfj279Ou/BcQuCYSj7NJwBfmWn5qc5uGO/Ny7fUWmXyIpvawqX/8acQlWG6IXelJsJhj+JAy6sjsKj0A==",
248
+ "cpu": [
249
+ "arm"
250
+ ],
251
+ "dev": true,
252
+ "license": "MIT",
253
+ "optional": true,
254
+ "os": [
255
+ "linux"
256
+ ],
257
+ "engines": {
258
+ "node": "^20.19.0 || >=22.12.0"
259
+ }
260
+ },
261
+ "node_modules/@oxlint/binding-linux-arm-musleabihf": {
262
+ "version": "1.72.0",
263
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.72.0.tgz",
264
+ "integrity": "sha512-4vpXB06h65Ezsy4hRyrGjGrfa1SkVPii09yaajiYhmVpgsFiLD+KNxIx/BNAY+XiO+i1yqp9HHdwqM8VTqa5XQ==",
265
+ "cpu": [
266
+ "arm"
267
+ ],
268
+ "dev": true,
269
+ "license": "MIT",
270
+ "optional": true,
271
+ "os": [
272
+ "linux"
273
+ ],
274
+ "engines": {
275
+ "node": "^20.19.0 || >=22.12.0"
276
+ }
277
+ },
278
+ "node_modules/@oxlint/binding-linux-arm64-gnu": {
279
+ "version": "1.72.0",
280
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.72.0.tgz",
281
+ "integrity": "sha512-immaN4g2ZGFiOkKrvRX9LvzZdd2GkQM5wR+UyzYyUuyhUTXGQ4HKUJH18xp4G8OfhCVaVAJfKZxwE1r8+4hhaQ==",
282
+ "cpu": [
283
+ "arm64"
284
+ ],
285
+ "dev": true,
286
+ "license": "MIT",
287
+ "optional": true,
288
+ "os": [
289
+ "linux"
290
+ ],
291
+ "engines": {
292
+ "node": "^20.19.0 || >=22.12.0"
293
+ }
294
+ },
295
+ "node_modules/@oxlint/binding-linux-arm64-musl": {
296
+ "version": "1.72.0",
297
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.72.0.tgz",
298
+ "integrity": "sha512-JGHS9Mnr7iWyyLDxgCv1MhzVpAckgptg00F2gnxt/GD7lQ2SW1BRcxHqhSTaSdDpjWRrBkBxMMh4+Hn3aVtExg==",
299
+ "cpu": [
300
+ "arm64"
301
+ ],
302
+ "dev": true,
303
+ "license": "MIT",
304
+ "optional": true,
305
+ "os": [
306
+ "linux"
307
+ ],
308
+ "engines": {
309
+ "node": "^20.19.0 || >=22.12.0"
310
+ }
311
+ },
312
+ "node_modules/@oxlint/binding-linux-ppc64-gnu": {
313
+ "version": "1.72.0",
314
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.72.0.tgz",
315
+ "integrity": "sha512-AOYgBZqxNshrg83P9v0RYv+m8s10Cqkj4/PxXFDhcS3k7FqsIG5+CxErshZCIN7G8iy4Y+VGfAsuEdar8AcbBg==",
316
+ "cpu": [
317
+ "ppc64"
318
+ ],
319
+ "dev": true,
320
+ "license": "MIT",
321
+ "optional": true,
322
+ "os": [
323
+ "linux"
324
+ ],
325
+ "engines": {
326
+ "node": "^20.19.0 || >=22.12.0"
327
+ }
328
+ },
329
+ "node_modules/@oxlint/binding-linux-riscv64-gnu": {
330
+ "version": "1.72.0",
331
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.72.0.tgz",
332
+ "integrity": "sha512-QMybPS5ij3/vrKG67mqzHwW++91sYxK/PPUVi6SBtNCEzW4niS52fVBdXbQ6nou0wWbUPEpx8Sl/ZjtgE3clXA==",
333
+ "cpu": [
334
+ "riscv64"
335
+ ],
336
+ "dev": true,
337
+ "license": "MIT",
338
+ "optional": true,
339
+ "os": [
340
+ "linux"
341
+ ],
342
+ "engines": {
343
+ "node": "^20.19.0 || >=22.12.0"
344
+ }
345
+ },
346
+ "node_modules/@oxlint/binding-linux-riscv64-musl": {
347
+ "version": "1.72.0",
348
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.72.0.tgz",
349
+ "integrity": "sha512-gOc3W7JV0PXRpIL7stUlLe3Wa9Gp0Kdlup87IT3gHDvPKck2xNgMIl/Gs2lldYY2lyXZDC4rWi3hmoLUobkgbQ==",
350
+ "cpu": [
351
+ "riscv64"
352
+ ],
353
+ "dev": true,
354
+ "license": "MIT",
355
+ "optional": true,
356
+ "os": [
357
+ "linux"
358
+ ],
359
+ "engines": {
360
+ "node": "^20.19.0 || >=22.12.0"
361
+ }
362
+ },
363
+ "node_modules/@oxlint/binding-linux-s390x-gnu": {
364
+ "version": "1.72.0",
365
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.72.0.tgz",
366
+ "integrity": "sha512-rpGxph+FjjHcYI5q6uxB3Az+tnfmEnDbSA8+PK9ZE/VzyUAkvBOMeuY7ZQMhu5mpZH7YQDsTdW6Cx4kV/msc6w==",
367
+ "cpu": [
368
+ "s390x"
369
+ ],
370
+ "dev": true,
371
+ "license": "MIT",
372
+ "optional": true,
373
+ "os": [
374
+ "linux"
375
+ ],
376
+ "engines": {
377
+ "node": "^20.19.0 || >=22.12.0"
378
+ }
379
+ },
380
+ "node_modules/@oxlint/binding-linux-x64-gnu": {
381
+ "version": "1.72.0",
382
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.72.0.tgz",
383
+ "integrity": "sha512-WND+uhf/Ko13SLqQMWQUgsZuLvYYEvL0ZKgg0tgGYfLqxG7l8Ju123fHDMJyYSDl5E3bUbpFUuii/OvMreFQzw==",
384
+ "cpu": [
385
+ "x64"
386
+ ],
387
+ "dev": true,
388
+ "license": "MIT",
389
+ "optional": true,
390
+ "os": [
391
+ "linux"
392
+ ],
393
+ "engines": {
394
+ "node": "^20.19.0 || >=22.12.0"
395
+ }
396
+ },
397
+ "node_modules/@oxlint/binding-linux-x64-musl": {
398
+ "version": "1.72.0",
399
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.72.0.tgz",
400
+ "integrity": "sha512-SrpbrUL70nG9vh6zP4/oKHWgLuHquwsr7MW9XOn0olBVgh10Uqr8qscKhQoBGEn6olK/IUpn5GSKcdQ5AjUhGA==",
401
+ "cpu": [
402
+ "x64"
403
+ ],
404
+ "dev": true,
405
+ "license": "MIT",
406
+ "optional": true,
407
+ "os": [
408
+ "linux"
409
+ ],
410
+ "engines": {
411
+ "node": "^20.19.0 || >=22.12.0"
412
+ }
413
+ },
414
+ "node_modules/@oxlint/binding-openharmony-arm64": {
415
+ "version": "1.72.0",
416
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.72.0.tgz",
417
+ "integrity": "sha512-qkrsEn6NmgFKr7U/QnezQMb+q/vzAy0Dd9Y95gQGQTyjzDLN+HRZMuM5u70iyH4nBLCfKBzhjMsYCehKay2jyg==",
418
+ "cpu": [
419
+ "arm64"
420
+ ],
421
+ "dev": true,
422
+ "license": "MIT",
423
+ "optional": true,
424
+ "os": [
425
+ "openharmony"
426
+ ],
427
+ "engines": {
428
+ "node": "^20.19.0 || >=22.12.0"
429
+ }
430
+ },
431
+ "node_modules/@oxlint/binding-win32-arm64-msvc": {
432
+ "version": "1.72.0",
433
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.72.0.tgz",
434
+ "integrity": "sha512-LWR6ZlFZph+KPjXv8opgZsXRDCdrdQe8VL8Cg9zxCoBS73h6znzZpydVgmdnwj8mB9AuSM5jxEgDJDpQkjboeg==",
435
+ "cpu": [
436
+ "arm64"
437
+ ],
438
+ "dev": true,
439
+ "license": "MIT",
440
+ "optional": true,
441
+ "os": [
442
+ "win32"
443
+ ],
444
+ "engines": {
445
+ "node": "^20.19.0 || >=22.12.0"
446
+ }
447
+ },
448
+ "node_modules/@oxlint/binding-win32-ia32-msvc": {
449
+ "version": "1.72.0",
450
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.72.0.tgz",
451
+ "integrity": "sha512-yt6HEh7IsHvtjRWtmeZRX134eaXKHq5Gnqlf1xBJdJl1JtdoRUEJw3nAxpZoUDS860cX/foKbztO441anVBtVQ==",
452
+ "cpu": [
453
+ "ia32"
454
+ ],
455
+ "dev": true,
456
+ "license": "MIT",
457
+ "optional": true,
458
+ "os": [
459
+ "win32"
460
+ ],
461
+ "engines": {
462
+ "node": "^20.19.0 || >=22.12.0"
463
+ }
464
+ },
465
+ "node_modules/@oxlint/binding-win32-x64-msvc": {
466
+ "version": "1.72.0",
467
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.72.0.tgz",
468
+ "integrity": "sha512-b2eKFD2hX7tIwmo/cyH6TDq8vzWRZ2qNHrzoGntUTmq0h3zQh/uX3eTSHCwI8OB/ADQfJCRelLItK8BsxuucDA==",
469
+ "cpu": [
470
+ "x64"
471
+ ],
472
+ "dev": true,
473
+ "license": "MIT",
474
+ "optional": true,
475
+ "os": [
476
+ "win32"
477
+ ],
478
+ "engines": {
479
+ "node": "^20.19.0 || >=22.12.0"
480
+ }
481
+ },
482
+ "node_modules/@rolldown/binding-android-arm64": {
483
+ "version": "1.1.4",
484
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz",
485
+ "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==",
486
+ "cpu": [
487
+ "arm64"
488
+ ],
489
+ "license": "MIT",
490
+ "optional": true,
491
+ "os": [
492
+ "android"
493
+ ],
494
+ "engines": {
495
+ "node": "^20.19.0 || >=22.12.0"
496
+ }
497
+ },
498
+ "node_modules/@rolldown/binding-darwin-arm64": {
499
+ "version": "1.1.4",
500
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz",
501
+ "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==",
502
+ "cpu": [
503
+ "arm64"
504
+ ],
505
+ "license": "MIT",
506
+ "optional": true,
507
+ "os": [
508
+ "darwin"
509
+ ],
510
+ "engines": {
511
+ "node": "^20.19.0 || >=22.12.0"
512
+ }
513
+ },
514
+ "node_modules/@rolldown/binding-darwin-x64": {
515
+ "version": "1.1.4",
516
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz",
517
+ "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==",
518
+ "cpu": [
519
+ "x64"
520
+ ],
521
+ "license": "MIT",
522
+ "optional": true,
523
+ "os": [
524
+ "darwin"
525
+ ],
526
+ "engines": {
527
+ "node": "^20.19.0 || >=22.12.0"
528
+ }
529
+ },
530
+ "node_modules/@rolldown/binding-freebsd-x64": {
531
+ "version": "1.1.4",
532
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz",
533
+ "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==",
534
+ "cpu": [
535
+ "x64"
536
+ ],
537
+ "license": "MIT",
538
+ "optional": true,
539
+ "os": [
540
+ "freebsd"
541
+ ],
542
+ "engines": {
543
+ "node": "^20.19.0 || >=22.12.0"
544
+ }
545
+ },
546
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
547
+ "version": "1.1.4",
548
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz",
549
+ "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==",
550
+ "cpu": [
551
+ "arm"
552
+ ],
553
+ "license": "MIT",
554
+ "optional": true,
555
+ "os": [
556
+ "linux"
557
+ ],
558
+ "engines": {
559
+ "node": "^20.19.0 || >=22.12.0"
560
+ }
561
+ },
562
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
563
+ "version": "1.1.4",
564
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz",
565
+ "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==",
566
+ "cpu": [
567
+ "arm64"
568
+ ],
569
+ "license": "MIT",
570
+ "optional": true,
571
+ "os": [
572
+ "linux"
573
+ ],
574
+ "engines": {
575
+ "node": "^20.19.0 || >=22.12.0"
576
+ }
577
+ },
578
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
579
+ "version": "1.1.4",
580
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz",
581
+ "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==",
582
+ "cpu": [
583
+ "arm64"
584
+ ],
585
+ "license": "MIT",
586
+ "optional": true,
587
+ "os": [
588
+ "linux"
589
+ ],
590
+ "engines": {
591
+ "node": "^20.19.0 || >=22.12.0"
592
+ }
593
+ },
594
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
595
+ "version": "1.1.4",
596
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz",
597
+ "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==",
598
+ "cpu": [
599
+ "ppc64"
600
+ ],
601
+ "license": "MIT",
602
+ "optional": true,
603
+ "os": [
604
+ "linux"
605
+ ],
606
+ "engines": {
607
+ "node": "^20.19.0 || >=22.12.0"
608
+ }
609
+ },
610
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
611
+ "version": "1.1.4",
612
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz",
613
+ "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==",
614
+ "cpu": [
615
+ "s390x"
616
+ ],
617
+ "license": "MIT",
618
+ "optional": true,
619
+ "os": [
620
+ "linux"
621
+ ],
622
+ "engines": {
623
+ "node": "^20.19.0 || >=22.12.0"
624
+ }
625
+ },
626
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
627
+ "version": "1.1.4",
628
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz",
629
+ "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==",
630
+ "cpu": [
631
+ "x64"
632
+ ],
633
+ "license": "MIT",
634
+ "optional": true,
635
+ "os": [
636
+ "linux"
637
+ ],
638
+ "engines": {
639
+ "node": "^20.19.0 || >=22.12.0"
640
+ }
641
+ },
642
+ "node_modules/@rolldown/binding-linux-x64-musl": {
643
+ "version": "1.1.4",
644
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz",
645
+ "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==",
646
+ "cpu": [
647
+ "x64"
648
+ ],
649
+ "license": "MIT",
650
+ "optional": true,
651
+ "os": [
652
+ "linux"
653
+ ],
654
+ "engines": {
655
+ "node": "^20.19.0 || >=22.12.0"
656
+ }
657
+ },
658
+ "node_modules/@rolldown/binding-openharmony-arm64": {
659
+ "version": "1.1.4",
660
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz",
661
+ "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==",
662
+ "cpu": [
663
+ "arm64"
664
+ ],
665
+ "license": "MIT",
666
+ "optional": true,
667
+ "os": [
668
+ "openharmony"
669
+ ],
670
+ "engines": {
671
+ "node": "^20.19.0 || >=22.12.0"
672
+ }
673
+ },
674
+ "node_modules/@rolldown/binding-wasm32-wasi": {
675
+ "version": "1.1.4",
676
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz",
677
+ "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==",
678
+ "cpu": [
679
+ "wasm32"
680
+ ],
681
+ "license": "MIT",
682
+ "optional": true,
683
+ "dependencies": {
684
+ "@emnapi/core": "1.11.1",
685
+ "@emnapi/runtime": "1.11.1",
686
+ "@napi-rs/wasm-runtime": "^1.1.6"
687
+ },
688
+ "engines": {
689
+ "node": "^20.19.0 || >=22.12.0"
690
+ }
691
+ },
692
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
693
+ "version": "1.1.4",
694
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz",
695
+ "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==",
696
+ "cpu": [
697
+ "arm64"
698
+ ],
699
+ "license": "MIT",
700
+ "optional": true,
701
+ "os": [
702
+ "win32"
703
+ ],
704
+ "engines": {
705
+ "node": "^20.19.0 || >=22.12.0"
706
+ }
707
+ },
708
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
709
+ "version": "1.1.4",
710
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz",
711
+ "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==",
712
+ "cpu": [
713
+ "x64"
714
+ ],
715
+ "license": "MIT",
716
+ "optional": true,
717
+ "os": [
718
+ "win32"
719
+ ],
720
+ "engines": {
721
+ "node": "^20.19.0 || >=22.12.0"
722
+ }
723
+ },
724
+ "node_modules/@rolldown/pluginutils": {
725
+ "version": "1.0.1",
726
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
727
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
728
+ "license": "MIT"
729
+ },
730
+ "node_modules/@tailwindcss/node": {
731
+ "version": "4.3.2",
732
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz",
733
+ "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==",
734
+ "license": "MIT",
735
+ "dependencies": {
736
+ "@jridgewell/remapping": "^2.3.5",
737
+ "enhanced-resolve": "5.21.6",
738
+ "jiti": "^2.7.0",
739
+ "lightningcss": "1.32.0",
740
+ "magic-string": "^0.30.21",
741
+ "source-map-js": "^1.2.1",
742
+ "tailwindcss": "4.3.2"
743
+ }
744
+ },
745
+ "node_modules/@tailwindcss/oxide": {
746
+ "version": "4.3.2",
747
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz",
748
+ "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==",
749
+ "license": "MIT",
750
+ "engines": {
751
+ "node": ">= 20"
752
+ },
753
+ "optionalDependencies": {
754
+ "@tailwindcss/oxide-android-arm64": "4.3.2",
755
+ "@tailwindcss/oxide-darwin-arm64": "4.3.2",
756
+ "@tailwindcss/oxide-darwin-x64": "4.3.2",
757
+ "@tailwindcss/oxide-freebsd-x64": "4.3.2",
758
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2",
759
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2",
760
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.2",
761
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.2",
762
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.2",
763
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.2",
764
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2",
765
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.2"
766
+ }
767
+ },
768
+ "node_modules/@tailwindcss/oxide-android-arm64": {
769
+ "version": "4.3.2",
770
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz",
771
+ "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==",
772
+ "cpu": [
773
+ "arm64"
774
+ ],
775
+ "license": "MIT",
776
+ "optional": true,
777
+ "os": [
778
+ "android"
779
+ ],
780
+ "engines": {
781
+ "node": ">= 20"
782
+ }
783
+ },
784
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
785
+ "version": "4.3.2",
786
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz",
787
+ "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==",
788
+ "cpu": [
789
+ "arm64"
790
+ ],
791
+ "license": "MIT",
792
+ "optional": true,
793
+ "os": [
794
+ "darwin"
795
+ ],
796
+ "engines": {
797
+ "node": ">= 20"
798
+ }
799
+ },
800
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
801
+ "version": "4.3.2",
802
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz",
803
+ "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==",
804
+ "cpu": [
805
+ "x64"
806
+ ],
807
+ "license": "MIT",
808
+ "optional": true,
809
+ "os": [
810
+ "darwin"
811
+ ],
812
+ "engines": {
813
+ "node": ">= 20"
814
+ }
815
+ },
816
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
817
+ "version": "4.3.2",
818
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz",
819
+ "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==",
820
+ "cpu": [
821
+ "x64"
822
+ ],
823
+ "license": "MIT",
824
+ "optional": true,
825
+ "os": [
826
+ "freebsd"
827
+ ],
828
+ "engines": {
829
+ "node": ">= 20"
830
+ }
831
+ },
832
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
833
+ "version": "4.3.2",
834
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz",
835
+ "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==",
836
+ "cpu": [
837
+ "arm"
838
+ ],
839
+ "license": "MIT",
840
+ "optional": true,
841
+ "os": [
842
+ "linux"
843
+ ],
844
+ "engines": {
845
+ "node": ">= 20"
846
+ }
847
+ },
848
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
849
+ "version": "4.3.2",
850
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz",
851
+ "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==",
852
+ "cpu": [
853
+ "arm64"
854
+ ],
855
+ "license": "MIT",
856
+ "optional": true,
857
+ "os": [
858
+ "linux"
859
+ ],
860
+ "engines": {
861
+ "node": ">= 20"
862
+ }
863
+ },
864
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
865
+ "version": "4.3.2",
866
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz",
867
+ "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==",
868
+ "cpu": [
869
+ "arm64"
870
+ ],
871
+ "license": "MIT",
872
+ "optional": true,
873
+ "os": [
874
+ "linux"
875
+ ],
876
+ "engines": {
877
+ "node": ">= 20"
878
+ }
879
+ },
880
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
881
+ "version": "4.3.2",
882
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz",
883
+ "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==",
884
+ "cpu": [
885
+ "x64"
886
+ ],
887
+ "license": "MIT",
888
+ "optional": true,
889
+ "os": [
890
+ "linux"
891
+ ],
892
+ "engines": {
893
+ "node": ">= 20"
894
+ }
895
+ },
896
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
897
+ "version": "4.3.2",
898
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz",
899
+ "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==",
900
+ "cpu": [
901
+ "x64"
902
+ ],
903
+ "license": "MIT",
904
+ "optional": true,
905
+ "os": [
906
+ "linux"
907
+ ],
908
+ "engines": {
909
+ "node": ">= 20"
910
+ }
911
+ },
912
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
913
+ "version": "4.3.2",
914
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz",
915
+ "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==",
916
+ "bundleDependencies": [
917
+ "@napi-rs/wasm-runtime",
918
+ "@emnapi/core",
919
+ "@emnapi/runtime",
920
+ "@tybys/wasm-util",
921
+ "@emnapi/wasi-threads",
922
+ "tslib"
923
+ ],
924
+ "cpu": [
925
+ "wasm32"
926
+ ],
927
+ "license": "MIT",
928
+ "optional": true,
929
+ "dependencies": {
930
+ "@emnapi/core": "^1.11.1",
931
+ "@emnapi/runtime": "^1.11.1",
932
+ "@emnapi/wasi-threads": "^1.2.2",
933
+ "@napi-rs/wasm-runtime": "^1.1.4",
934
+ "@tybys/wasm-util": "^0.10.2",
935
+ "tslib": "^2.8.1"
936
+ },
937
+ "engines": {
938
+ "node": ">=14.0.0"
939
+ }
940
+ },
941
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
942
+ "version": "4.3.2",
943
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz",
944
+ "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==",
945
+ "cpu": [
946
+ "arm64"
947
+ ],
948
+ "license": "MIT",
949
+ "optional": true,
950
+ "os": [
951
+ "win32"
952
+ ],
953
+ "engines": {
954
+ "node": ">= 20"
955
+ }
956
+ },
957
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
958
+ "version": "4.3.2",
959
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz",
960
+ "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==",
961
+ "cpu": [
962
+ "x64"
963
+ ],
964
+ "license": "MIT",
965
+ "optional": true,
966
+ "os": [
967
+ "win32"
968
+ ],
969
+ "engines": {
970
+ "node": ">= 20"
971
+ }
972
+ },
973
+ "node_modules/@tailwindcss/vite": {
974
+ "version": "4.3.2",
975
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz",
976
+ "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==",
977
+ "license": "MIT",
978
+ "dependencies": {
979
+ "@tailwindcss/node": "4.3.2",
980
+ "@tailwindcss/oxide": "4.3.2",
981
+ "tailwindcss": "4.3.2"
982
+ },
983
+ "peerDependencies": {
984
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
985
+ }
986
+ },
987
+ "node_modules/@tanstack/query-core": {
988
+ "version": "5.101.2",
989
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz",
990
+ "integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==",
991
+ "license": "MIT",
992
+ "funding": {
993
+ "type": "github",
994
+ "url": "https://github.com/sponsors/tannerlinsley"
995
+ }
996
+ },
997
+ "node_modules/@tanstack/react-query": {
998
+ "version": "5.101.2",
999
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.2.tgz",
1000
+ "integrity": "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==",
1001
+ "license": "MIT",
1002
+ "dependencies": {
1003
+ "@tanstack/query-core": "5.101.2"
1004
+ },
1005
+ "funding": {
1006
+ "type": "github",
1007
+ "url": "https://github.com/sponsors/tannerlinsley"
1008
+ },
1009
+ "peerDependencies": {
1010
+ "react": "^18 || ^19"
1011
+ }
1012
+ },
1013
+ "node_modules/@tybys/wasm-util": {
1014
+ "version": "0.10.3",
1015
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
1016
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
1017
+ "license": "MIT",
1018
+ "optional": true,
1019
+ "dependencies": {
1020
+ "tslib": "^2.4.0"
1021
+ }
1022
+ },
1023
+ "node_modules/@types/node": {
1024
+ "version": "24.13.2",
1025
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
1026
+ "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
1027
+ "devOptional": true,
1028
+ "license": "MIT",
1029
+ "dependencies": {
1030
+ "undici-types": "~7.18.0"
1031
+ }
1032
+ },
1033
+ "node_modules/@types/react": {
1034
+ "version": "19.2.17",
1035
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
1036
+ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
1037
+ "dev": true,
1038
+ "license": "MIT",
1039
+ "dependencies": {
1040
+ "csstype": "^3.2.2"
1041
+ }
1042
+ },
1043
+ "node_modules/@types/react-dom": {
1044
+ "version": "19.2.3",
1045
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
1046
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
1047
+ "dev": true,
1048
+ "license": "MIT",
1049
+ "peerDependencies": {
1050
+ "@types/react": "^19.2.0"
1051
+ }
1052
+ },
1053
+ "node_modules/@types/trusted-types": {
1054
+ "version": "2.0.7",
1055
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
1056
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
1057
+ "license": "MIT",
1058
+ "optional": true,
1059
+ "peer": true
1060
+ },
1061
+ "node_modules/@vitejs/plugin-react": {
1062
+ "version": "6.0.3",
1063
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz",
1064
+ "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==",
1065
+ "dev": true,
1066
+ "license": "MIT",
1067
+ "dependencies": {
1068
+ "@rolldown/pluginutils": "^1.0.1"
1069
+ },
1070
+ "engines": {
1071
+ "node": "^20.19.0 || >=22.12.0"
1072
+ },
1073
+ "peerDependencies": {
1074
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
1075
+ "babel-plugin-react-compiler": "^1.0.0",
1076
+ "vite": "^8.0.0"
1077
+ },
1078
+ "peerDependenciesMeta": {
1079
+ "@rolldown/plugin-babel": {
1080
+ "optional": true
1081
+ },
1082
+ "babel-plugin-react-compiler": {
1083
+ "optional": true
1084
+ }
1085
+ }
1086
+ },
1087
+ "node_modules/clsx": {
1088
+ "version": "2.1.1",
1089
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
1090
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
1091
+ "license": "MIT",
1092
+ "engines": {
1093
+ "node": ">=6"
1094
+ }
1095
+ },
1096
+ "node_modules/cookie": {
1097
+ "version": "1.1.1",
1098
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
1099
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
1100
+ "license": "MIT",
1101
+ "engines": {
1102
+ "node": ">=18"
1103
+ },
1104
+ "funding": {
1105
+ "type": "opencollective",
1106
+ "url": "https://opencollective.com/express"
1107
+ }
1108
+ },
1109
+ "node_modules/csstype": {
1110
+ "version": "3.2.3",
1111
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1112
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1113
+ "dev": true,
1114
+ "license": "MIT"
1115
+ },
1116
+ "node_modules/detect-libc": {
1117
+ "version": "2.1.2",
1118
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1119
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1120
+ "license": "Apache-2.0",
1121
+ "engines": {
1122
+ "node": ">=8"
1123
+ }
1124
+ },
1125
+ "node_modules/dompurify": {
1126
+ "version": "3.2.7",
1127
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
1128
+ "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
1129
+ "license": "(MPL-2.0 OR Apache-2.0)",
1130
+ "peer": true,
1131
+ "optionalDependencies": {
1132
+ "@types/trusted-types": "^2.0.7"
1133
+ }
1134
+ },
1135
+ "node_modules/enhanced-resolve": {
1136
+ "version": "5.21.6",
1137
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
1138
+ "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
1139
+ "license": "MIT",
1140
+ "dependencies": {
1141
+ "graceful-fs": "^4.2.4",
1142
+ "tapable": "^2.3.3"
1143
+ },
1144
+ "engines": {
1145
+ "node": ">=10.13.0"
1146
+ }
1147
+ },
1148
+ "node_modules/fdir": {
1149
+ "version": "6.5.0",
1150
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1151
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1152
+ "license": "MIT",
1153
+ "engines": {
1154
+ "node": ">=12.0.0"
1155
+ },
1156
+ "peerDependencies": {
1157
+ "picomatch": "^3 || ^4"
1158
+ },
1159
+ "peerDependenciesMeta": {
1160
+ "picomatch": {
1161
+ "optional": true
1162
+ }
1163
+ }
1164
+ },
1165
+ "node_modules/framer-motion": {
1166
+ "version": "12.42.2",
1167
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz",
1168
+ "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==",
1169
+ "license": "MIT",
1170
+ "dependencies": {
1171
+ "motion-dom": "^12.42.2",
1172
+ "motion-utils": "^12.39.0",
1173
+ "tslib": "^2.4.0"
1174
+ },
1175
+ "peerDependencies": {
1176
+ "@emotion/is-prop-valid": "*",
1177
+ "react": "^18.0.0 || ^19.0.0",
1178
+ "react-dom": "^18.0.0 || ^19.0.0"
1179
+ },
1180
+ "peerDependenciesMeta": {
1181
+ "@emotion/is-prop-valid": {
1182
+ "optional": true
1183
+ },
1184
+ "react": {
1185
+ "optional": true
1186
+ },
1187
+ "react-dom": {
1188
+ "optional": true
1189
+ }
1190
+ }
1191
+ },
1192
+ "node_modules/fsevents": {
1193
+ "version": "2.3.3",
1194
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1195
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1196
+ "hasInstallScript": true,
1197
+ "license": "MIT",
1198
+ "optional": true,
1199
+ "os": [
1200
+ "darwin"
1201
+ ],
1202
+ "engines": {
1203
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1204
+ }
1205
+ },
1206
+ "node_modules/graceful-fs": {
1207
+ "version": "4.2.11",
1208
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
1209
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
1210
+ "license": "ISC"
1211
+ },
1212
+ "node_modules/jiti": {
1213
+ "version": "2.7.0",
1214
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
1215
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
1216
+ "license": "MIT",
1217
+ "bin": {
1218
+ "jiti": "lib/jiti-cli.mjs"
1219
+ }
1220
+ },
1221
+ "node_modules/lightningcss": {
1222
+ "version": "1.32.0",
1223
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
1224
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
1225
+ "license": "MPL-2.0",
1226
+ "dependencies": {
1227
+ "detect-libc": "^2.0.3"
1228
+ },
1229
+ "engines": {
1230
+ "node": ">= 12.0.0"
1231
+ },
1232
+ "funding": {
1233
+ "type": "opencollective",
1234
+ "url": "https://opencollective.com/parcel"
1235
+ },
1236
+ "optionalDependencies": {
1237
+ "lightningcss-android-arm64": "1.32.0",
1238
+ "lightningcss-darwin-arm64": "1.32.0",
1239
+ "lightningcss-darwin-x64": "1.32.0",
1240
+ "lightningcss-freebsd-x64": "1.32.0",
1241
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
1242
+ "lightningcss-linux-arm64-gnu": "1.32.0",
1243
+ "lightningcss-linux-arm64-musl": "1.32.0",
1244
+ "lightningcss-linux-x64-gnu": "1.32.0",
1245
+ "lightningcss-linux-x64-musl": "1.32.0",
1246
+ "lightningcss-win32-arm64-msvc": "1.32.0",
1247
+ "lightningcss-win32-x64-msvc": "1.32.0"
1248
+ }
1249
+ },
1250
+ "node_modules/lightningcss-android-arm64": {
1251
+ "version": "1.32.0",
1252
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
1253
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
1254
+ "cpu": [
1255
+ "arm64"
1256
+ ],
1257
+ "license": "MPL-2.0",
1258
+ "optional": true,
1259
+ "os": [
1260
+ "android"
1261
+ ],
1262
+ "engines": {
1263
+ "node": ">= 12.0.0"
1264
+ },
1265
+ "funding": {
1266
+ "type": "opencollective",
1267
+ "url": "https://opencollective.com/parcel"
1268
+ }
1269
+ },
1270
+ "node_modules/lightningcss-darwin-arm64": {
1271
+ "version": "1.32.0",
1272
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
1273
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
1274
+ "cpu": [
1275
+ "arm64"
1276
+ ],
1277
+ "license": "MPL-2.0",
1278
+ "optional": true,
1279
+ "os": [
1280
+ "darwin"
1281
+ ],
1282
+ "engines": {
1283
+ "node": ">= 12.0.0"
1284
+ },
1285
+ "funding": {
1286
+ "type": "opencollective",
1287
+ "url": "https://opencollective.com/parcel"
1288
+ }
1289
+ },
1290
+ "node_modules/lightningcss-darwin-x64": {
1291
+ "version": "1.32.0",
1292
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
1293
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
1294
+ "cpu": [
1295
+ "x64"
1296
+ ],
1297
+ "license": "MPL-2.0",
1298
+ "optional": true,
1299
+ "os": [
1300
+ "darwin"
1301
+ ],
1302
+ "engines": {
1303
+ "node": ">= 12.0.0"
1304
+ },
1305
+ "funding": {
1306
+ "type": "opencollective",
1307
+ "url": "https://opencollective.com/parcel"
1308
+ }
1309
+ },
1310
+ "node_modules/lightningcss-freebsd-x64": {
1311
+ "version": "1.32.0",
1312
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
1313
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
1314
+ "cpu": [
1315
+ "x64"
1316
+ ],
1317
+ "license": "MPL-2.0",
1318
+ "optional": true,
1319
+ "os": [
1320
+ "freebsd"
1321
+ ],
1322
+ "engines": {
1323
+ "node": ">= 12.0.0"
1324
+ },
1325
+ "funding": {
1326
+ "type": "opencollective",
1327
+ "url": "https://opencollective.com/parcel"
1328
+ }
1329
+ },
1330
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
1331
+ "version": "1.32.0",
1332
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
1333
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
1334
+ "cpu": [
1335
+ "arm"
1336
+ ],
1337
+ "license": "MPL-2.0",
1338
+ "optional": true,
1339
+ "os": [
1340
+ "linux"
1341
+ ],
1342
+ "engines": {
1343
+ "node": ">= 12.0.0"
1344
+ },
1345
+ "funding": {
1346
+ "type": "opencollective",
1347
+ "url": "https://opencollective.com/parcel"
1348
+ }
1349
+ },
1350
+ "node_modules/lightningcss-linux-arm64-gnu": {
1351
+ "version": "1.32.0",
1352
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
1353
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
1354
+ "cpu": [
1355
+ "arm64"
1356
+ ],
1357
+ "license": "MPL-2.0",
1358
+ "optional": true,
1359
+ "os": [
1360
+ "linux"
1361
+ ],
1362
+ "engines": {
1363
+ "node": ">= 12.0.0"
1364
+ },
1365
+ "funding": {
1366
+ "type": "opencollective",
1367
+ "url": "https://opencollective.com/parcel"
1368
+ }
1369
+ },
1370
+ "node_modules/lightningcss-linux-arm64-musl": {
1371
+ "version": "1.32.0",
1372
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
1373
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
1374
+ "cpu": [
1375
+ "arm64"
1376
+ ],
1377
+ "license": "MPL-2.0",
1378
+ "optional": true,
1379
+ "os": [
1380
+ "linux"
1381
+ ],
1382
+ "engines": {
1383
+ "node": ">= 12.0.0"
1384
+ },
1385
+ "funding": {
1386
+ "type": "opencollective",
1387
+ "url": "https://opencollective.com/parcel"
1388
+ }
1389
+ },
1390
+ "node_modules/lightningcss-linux-x64-gnu": {
1391
+ "version": "1.32.0",
1392
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
1393
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
1394
+ "cpu": [
1395
+ "x64"
1396
+ ],
1397
+ "license": "MPL-2.0",
1398
+ "optional": true,
1399
+ "os": [
1400
+ "linux"
1401
+ ],
1402
+ "engines": {
1403
+ "node": ">= 12.0.0"
1404
+ },
1405
+ "funding": {
1406
+ "type": "opencollective",
1407
+ "url": "https://opencollective.com/parcel"
1408
+ }
1409
+ },
1410
+ "node_modules/lightningcss-linux-x64-musl": {
1411
+ "version": "1.32.0",
1412
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
1413
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
1414
+ "cpu": [
1415
+ "x64"
1416
+ ],
1417
+ "license": "MPL-2.0",
1418
+ "optional": true,
1419
+ "os": [
1420
+ "linux"
1421
+ ],
1422
+ "engines": {
1423
+ "node": ">= 12.0.0"
1424
+ },
1425
+ "funding": {
1426
+ "type": "opencollective",
1427
+ "url": "https://opencollective.com/parcel"
1428
+ }
1429
+ },
1430
+ "node_modules/lightningcss-win32-arm64-msvc": {
1431
+ "version": "1.32.0",
1432
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
1433
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
1434
+ "cpu": [
1435
+ "arm64"
1436
+ ],
1437
+ "license": "MPL-2.0",
1438
+ "optional": true,
1439
+ "os": [
1440
+ "win32"
1441
+ ],
1442
+ "engines": {
1443
+ "node": ">= 12.0.0"
1444
+ },
1445
+ "funding": {
1446
+ "type": "opencollective",
1447
+ "url": "https://opencollective.com/parcel"
1448
+ }
1449
+ },
1450
+ "node_modules/lightningcss-win32-x64-msvc": {
1451
+ "version": "1.32.0",
1452
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
1453
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
1454
+ "cpu": [
1455
+ "x64"
1456
+ ],
1457
+ "license": "MPL-2.0",
1458
+ "optional": true,
1459
+ "os": [
1460
+ "win32"
1461
+ ],
1462
+ "engines": {
1463
+ "node": ">= 12.0.0"
1464
+ },
1465
+ "funding": {
1466
+ "type": "opencollective",
1467
+ "url": "https://opencollective.com/parcel"
1468
+ }
1469
+ },
1470
+ "node_modules/lucide-react": {
1471
+ "version": "1.23.0",
1472
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz",
1473
+ "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==",
1474
+ "license": "ISC",
1475
+ "peerDependencies": {
1476
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
1477
+ }
1478
+ },
1479
+ "node_modules/magic-string": {
1480
+ "version": "0.30.21",
1481
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
1482
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
1483
+ "license": "MIT",
1484
+ "dependencies": {
1485
+ "@jridgewell/sourcemap-codec": "^1.5.5"
1486
+ }
1487
+ },
1488
+ "node_modules/marked": {
1489
+ "version": "14.0.0",
1490
+ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
1491
+ "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
1492
+ "license": "MIT",
1493
+ "peer": true,
1494
+ "bin": {
1495
+ "marked": "bin/marked.js"
1496
+ },
1497
+ "engines": {
1498
+ "node": ">= 18"
1499
+ }
1500
+ },
1501
+ "node_modules/monaco-editor": {
1502
+ "version": "0.55.1",
1503
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
1504
+ "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
1505
+ "license": "MIT",
1506
+ "peer": true,
1507
+ "dependencies": {
1508
+ "dompurify": "3.2.7",
1509
+ "marked": "14.0.0"
1510
+ }
1511
+ },
1512
+ "node_modules/motion-dom": {
1513
+ "version": "12.42.2",
1514
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz",
1515
+ "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==",
1516
+ "license": "MIT",
1517
+ "dependencies": {
1518
+ "motion-utils": "^12.39.0"
1519
+ }
1520
+ },
1521
+ "node_modules/motion-utils": {
1522
+ "version": "12.39.0",
1523
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz",
1524
+ "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==",
1525
+ "license": "MIT"
1526
+ },
1527
+ "node_modules/nanoid": {
1528
+ "version": "3.3.15",
1529
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
1530
+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
1531
+ "funding": [
1532
+ {
1533
+ "type": "github",
1534
+ "url": "https://github.com/sponsors/ai"
1535
+ }
1536
+ ],
1537
+ "license": "MIT",
1538
+ "bin": {
1539
+ "nanoid": "bin/nanoid.cjs"
1540
+ },
1541
+ "engines": {
1542
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1543
+ }
1544
+ },
1545
+ "node_modules/oxlint": {
1546
+ "version": "1.72.0",
1547
+ "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.72.0.tgz",
1548
+ "integrity": "sha512-1rhdZIP/EvoI91ABIwNU5Q8+bWf8mjrS5UzIOZld4d4bXxJvtlUhlQvaoTogIGin/qdErMOrwaIJvCSIAKTLhA==",
1549
+ "dev": true,
1550
+ "license": "MIT",
1551
+ "bin": {
1552
+ "oxlint": "bin/oxlint"
1553
+ },
1554
+ "engines": {
1555
+ "node": "^20.19.0 || >=22.12.0"
1556
+ },
1557
+ "funding": {
1558
+ "url": "https://github.com/sponsors/Boshen"
1559
+ },
1560
+ "optionalDependencies": {
1561
+ "@oxlint/binding-android-arm-eabi": "1.72.0",
1562
+ "@oxlint/binding-android-arm64": "1.72.0",
1563
+ "@oxlint/binding-darwin-arm64": "1.72.0",
1564
+ "@oxlint/binding-darwin-x64": "1.72.0",
1565
+ "@oxlint/binding-freebsd-x64": "1.72.0",
1566
+ "@oxlint/binding-linux-arm-gnueabihf": "1.72.0",
1567
+ "@oxlint/binding-linux-arm-musleabihf": "1.72.0",
1568
+ "@oxlint/binding-linux-arm64-gnu": "1.72.0",
1569
+ "@oxlint/binding-linux-arm64-musl": "1.72.0",
1570
+ "@oxlint/binding-linux-ppc64-gnu": "1.72.0",
1571
+ "@oxlint/binding-linux-riscv64-gnu": "1.72.0",
1572
+ "@oxlint/binding-linux-riscv64-musl": "1.72.0",
1573
+ "@oxlint/binding-linux-s390x-gnu": "1.72.0",
1574
+ "@oxlint/binding-linux-x64-gnu": "1.72.0",
1575
+ "@oxlint/binding-linux-x64-musl": "1.72.0",
1576
+ "@oxlint/binding-openharmony-arm64": "1.72.0",
1577
+ "@oxlint/binding-win32-arm64-msvc": "1.72.0",
1578
+ "@oxlint/binding-win32-ia32-msvc": "1.72.0",
1579
+ "@oxlint/binding-win32-x64-msvc": "1.72.0"
1580
+ },
1581
+ "peerDependencies": {
1582
+ "oxlint-tsgolint": ">=0.22.1",
1583
+ "vite-plus": "*"
1584
+ },
1585
+ "peerDependenciesMeta": {
1586
+ "oxlint-tsgolint": {
1587
+ "optional": true
1588
+ },
1589
+ "vite-plus": {
1590
+ "optional": true
1591
+ }
1592
+ }
1593
+ },
1594
+ "node_modules/picocolors": {
1595
+ "version": "1.1.1",
1596
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1597
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1598
+ "license": "ISC"
1599
+ },
1600
+ "node_modules/picomatch": {
1601
+ "version": "4.0.4",
1602
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
1603
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
1604
+ "license": "MIT",
1605
+ "engines": {
1606
+ "node": ">=12"
1607
+ },
1608
+ "funding": {
1609
+ "url": "https://github.com/sponsors/jonschlinkert"
1610
+ }
1611
+ },
1612
+ "node_modules/postcss": {
1613
+ "version": "8.5.16",
1614
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
1615
+ "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
1616
+ "funding": [
1617
+ {
1618
+ "type": "opencollective",
1619
+ "url": "https://opencollective.com/postcss/"
1620
+ },
1621
+ {
1622
+ "type": "tidelift",
1623
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1624
+ },
1625
+ {
1626
+ "type": "github",
1627
+ "url": "https://github.com/sponsors/ai"
1628
+ }
1629
+ ],
1630
+ "license": "MIT",
1631
+ "dependencies": {
1632
+ "nanoid": "^3.3.12",
1633
+ "picocolors": "^1.1.1",
1634
+ "source-map-js": "^1.2.1"
1635
+ },
1636
+ "engines": {
1637
+ "node": "^10 || ^12 || >=14"
1638
+ }
1639
+ },
1640
+ "node_modules/react": {
1641
+ "version": "19.2.7",
1642
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
1643
+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
1644
+ "license": "MIT",
1645
+ "engines": {
1646
+ "node": ">=0.10.0"
1647
+ }
1648
+ },
1649
+ "node_modules/react-dom": {
1650
+ "version": "19.2.7",
1651
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
1652
+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
1653
+ "license": "MIT",
1654
+ "dependencies": {
1655
+ "scheduler": "^0.27.0"
1656
+ },
1657
+ "peerDependencies": {
1658
+ "react": "^19.2.7"
1659
+ }
1660
+ },
1661
+ "node_modules/react-router": {
1662
+ "version": "7.18.1",
1663
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz",
1664
+ "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==",
1665
+ "license": "MIT",
1666
+ "dependencies": {
1667
+ "cookie": "^1.0.1",
1668
+ "set-cookie-parser": "^2.6.0"
1669
+ },
1670
+ "engines": {
1671
+ "node": ">=20.0.0"
1672
+ },
1673
+ "peerDependencies": {
1674
+ "react": ">=18",
1675
+ "react-dom": ">=18"
1676
+ },
1677
+ "peerDependenciesMeta": {
1678
+ "react-dom": {
1679
+ "optional": true
1680
+ }
1681
+ }
1682
+ },
1683
+ "node_modules/react-router-dom": {
1684
+ "version": "7.18.1",
1685
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz",
1686
+ "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==",
1687
+ "license": "MIT",
1688
+ "dependencies": {
1689
+ "react-router": "7.18.1"
1690
+ },
1691
+ "engines": {
1692
+ "node": ">=20.0.0"
1693
+ },
1694
+ "peerDependencies": {
1695
+ "react": ">=18",
1696
+ "react-dom": ">=18"
1697
+ }
1698
+ },
1699
+ "node_modules/rolldown": {
1700
+ "version": "1.1.4",
1701
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
1702
+ "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==",
1703
+ "license": "MIT",
1704
+ "dependencies": {
1705
+ "@oxc-project/types": "=0.138.0",
1706
+ "@rolldown/pluginutils": "^1.0.0"
1707
+ },
1708
+ "bin": {
1709
+ "rolldown": "bin/cli.mjs"
1710
+ },
1711
+ "engines": {
1712
+ "node": "^20.19.0 || >=22.12.0"
1713
+ },
1714
+ "optionalDependencies": {
1715
+ "@rolldown/binding-android-arm64": "1.1.4",
1716
+ "@rolldown/binding-darwin-arm64": "1.1.4",
1717
+ "@rolldown/binding-darwin-x64": "1.1.4",
1718
+ "@rolldown/binding-freebsd-x64": "1.1.4",
1719
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.4",
1720
+ "@rolldown/binding-linux-arm64-gnu": "1.1.4",
1721
+ "@rolldown/binding-linux-arm64-musl": "1.1.4",
1722
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.4",
1723
+ "@rolldown/binding-linux-s390x-gnu": "1.1.4",
1724
+ "@rolldown/binding-linux-x64-gnu": "1.1.4",
1725
+ "@rolldown/binding-linux-x64-musl": "1.1.4",
1726
+ "@rolldown/binding-openharmony-arm64": "1.1.4",
1727
+ "@rolldown/binding-wasm32-wasi": "1.1.4",
1728
+ "@rolldown/binding-win32-arm64-msvc": "1.1.4",
1729
+ "@rolldown/binding-win32-x64-msvc": "1.1.4"
1730
+ }
1731
+ },
1732
+ "node_modules/scheduler": {
1733
+ "version": "0.27.0",
1734
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
1735
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
1736
+ "license": "MIT"
1737
+ },
1738
+ "node_modules/set-cookie-parser": {
1739
+ "version": "2.7.2",
1740
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
1741
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
1742
+ "license": "MIT"
1743
+ },
1744
+ "node_modules/source-map-js": {
1745
+ "version": "1.2.1",
1746
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1747
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1748
+ "license": "BSD-3-Clause",
1749
+ "engines": {
1750
+ "node": ">=0.10.0"
1751
+ }
1752
+ },
1753
+ "node_modules/state-local": {
1754
+ "version": "1.0.7",
1755
+ "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
1756
+ "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
1757
+ "license": "MIT"
1758
+ },
1759
+ "node_modules/tailwind-merge": {
1760
+ "version": "3.6.0",
1761
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
1762
+ "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
1763
+ "license": "MIT",
1764
+ "funding": {
1765
+ "type": "github",
1766
+ "url": "https://github.com/sponsors/dcastil"
1767
+ }
1768
+ },
1769
+ "node_modules/tailwindcss": {
1770
+ "version": "4.3.2",
1771
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz",
1772
+ "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==",
1773
+ "license": "MIT"
1774
+ },
1775
+ "node_modules/tapable": {
1776
+ "version": "2.3.3",
1777
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
1778
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
1779
+ "license": "MIT",
1780
+ "engines": {
1781
+ "node": ">=6"
1782
+ },
1783
+ "funding": {
1784
+ "type": "opencollective",
1785
+ "url": "https://opencollective.com/webpack"
1786
+ }
1787
+ },
1788
+ "node_modules/tinyglobby": {
1789
+ "version": "0.2.17",
1790
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
1791
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
1792
+ "license": "MIT",
1793
+ "dependencies": {
1794
+ "fdir": "^6.5.0",
1795
+ "picomatch": "^4.0.4"
1796
+ },
1797
+ "engines": {
1798
+ "node": ">=12.0.0"
1799
+ },
1800
+ "funding": {
1801
+ "url": "https://github.com/sponsors/SuperchupuDev"
1802
+ }
1803
+ },
1804
+ "node_modules/tslib": {
1805
+ "version": "2.8.1",
1806
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
1807
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
1808
+ "license": "0BSD"
1809
+ },
1810
+ "node_modules/typescript": {
1811
+ "version": "6.0.3",
1812
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
1813
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
1814
+ "dev": true,
1815
+ "license": "Apache-2.0",
1816
+ "bin": {
1817
+ "tsc": "bin/tsc",
1818
+ "tsserver": "bin/tsserver"
1819
+ },
1820
+ "engines": {
1821
+ "node": ">=14.17"
1822
+ }
1823
+ },
1824
+ "node_modules/undici-types": {
1825
+ "version": "7.18.2",
1826
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
1827
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
1828
+ "devOptional": true,
1829
+ "license": "MIT"
1830
+ },
1831
+ "node_modules/vite": {
1832
+ "version": "8.1.3",
1833
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
1834
+ "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==",
1835
+ "license": "MIT",
1836
+ "dependencies": {
1837
+ "lightningcss": "^1.32.0",
1838
+ "picomatch": "^4.0.4",
1839
+ "postcss": "^8.5.16",
1840
+ "rolldown": "~1.1.3",
1841
+ "tinyglobby": "^0.2.17"
1842
+ },
1843
+ "bin": {
1844
+ "vite": "bin/vite.js"
1845
+ },
1846
+ "engines": {
1847
+ "node": "^20.19.0 || >=22.12.0"
1848
+ },
1849
+ "funding": {
1850
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1851
+ },
1852
+ "optionalDependencies": {
1853
+ "fsevents": "~2.3.3"
1854
+ },
1855
+ "peerDependencies": {
1856
+ "@types/node": "^20.19.0 || >=22.12.0",
1857
+ "@vitejs/devtools": "^0.3.0",
1858
+ "esbuild": "^0.27.0 || ^0.28.0",
1859
+ "jiti": ">=1.21.0",
1860
+ "less": "^4.0.0",
1861
+ "sass": "^1.70.0",
1862
+ "sass-embedded": "^1.70.0",
1863
+ "stylus": ">=0.54.8",
1864
+ "sugarss": "^5.0.0",
1865
+ "terser": "^5.16.0",
1866
+ "tsx": "^4.8.1",
1867
+ "yaml": "^2.4.2"
1868
+ },
1869
+ "peerDependenciesMeta": {
1870
+ "@types/node": {
1871
+ "optional": true
1872
+ },
1873
+ "@vitejs/devtools": {
1874
+ "optional": true
1875
+ },
1876
+ "esbuild": {
1877
+ "optional": true
1878
+ },
1879
+ "jiti": {
1880
+ "optional": true
1881
+ },
1882
+ "less": {
1883
+ "optional": true
1884
+ },
1885
+ "sass": {
1886
+ "optional": true
1887
+ },
1888
+ "sass-embedded": {
1889
+ "optional": true
1890
+ },
1891
+ "stylus": {
1892
+ "optional": true
1893
+ },
1894
+ "sugarss": {
1895
+ "optional": true
1896
+ },
1897
+ "terser": {
1898
+ "optional": true
1899
+ },
1900
+ "tsx": {
1901
+ "optional": true
1902
+ },
1903
+ "yaml": {
1904
+ "optional": true
1905
+ }
1906
+ }
1907
+ }
1908
+ }
1909
+ }
frontend/package.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "oxlint",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@monaco-editor/react": "^4.7.0",
14
+ "@tailwindcss/vite": "^4.3.2",
15
+ "@tanstack/react-query": "^5.101.2",
16
+ "clsx": "^2.1.1",
17
+ "framer-motion": "^12.42.2",
18
+ "lucide-react": "^1.23.0",
19
+ "react": "^19.2.7",
20
+ "react-dom": "^19.2.7",
21
+ "react-router-dom": "^7.18.1",
22
+ "tailwind-merge": "^3.6.0",
23
+ "tailwindcss": "^4.3.2"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^24.13.2",
27
+ "@types/react": "^19.2.17",
28
+ "@types/react-dom": "^19.2.3",
29
+ "@vitejs/plugin-react": "^6.0.3",
30
+ "oxlint": "^1.71.0",
31
+ "typescript": "~6.0.2",
32
+ "vite": "^8.1.1"
33
+ }
34
+ }
frontend/public/favicon.svg ADDED
frontend/public/icons.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .counter {
2
+ font-size: 16px;
3
+ padding: 5px 10px;
4
+ border-radius: 5px;
5
+ color: var(--accent);
6
+ background: var(--accent-bg);
7
+ border: 2px solid transparent;
8
+ transition: border-color 0.3s;
9
+ margin-bottom: 24px;
10
+
11
+ &:hover {
12
+ border-color: var(--accent-border);
13
+ }
14
+ &:focus-visible {
15
+ outline: 2px solid var(--accent);
16
+ outline-offset: 2px;
17
+ }
18
+ }
19
+
20
+ .hero {
21
+ position: relative;
22
+
23
+ .base,
24
+ .framework,
25
+ .vite {
26
+ inset-inline: 0;
27
+ margin: 0 auto;
28
+ }
29
+
30
+ .base {
31
+ width: 170px;
32
+ position: relative;
33
+ z-index: 0;
34
+ }
35
+
36
+ .framework,
37
+ .vite {
38
+ position: absolute;
39
+ }
40
+
41
+ .framework {
42
+ z-index: 1;
43
+ top: 34px;
44
+ height: 28px;
45
+ transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
46
+ scale(1.4);
47
+ }
48
+
49
+ .vite {
50
+ z-index: 0;
51
+ top: 107px;
52
+ height: 26px;
53
+ width: auto;
54
+ transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
55
+ scale(0.8);
56
+ }
57
+ }
58
+
59
+ #center {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 25px;
63
+ place-content: center;
64
+ place-items: center;
65
+ flex-grow: 1;
66
+
67
+ @media (max-width: 1024px) {
68
+ padding: 32px 20px 24px;
69
+ gap: 18px;
70
+ }
71
+ }
72
+
73
+ #next-steps {
74
+ display: flex;
75
+ border-top: 1px solid var(--border);
76
+ text-align: left;
77
+
78
+ & > div {
79
+ flex: 1 1 0;
80
+ padding: 32px;
81
+ @media (max-width: 1024px) {
82
+ padding: 24px 20px;
83
+ }
84
+ }
85
+
86
+ .icon {
87
+ margin-bottom: 16px;
88
+ width: 22px;
89
+ height: 22px;
90
+ }
91
+
92
+ @media (max-width: 1024px) {
93
+ flex-direction: column;
94
+ text-align: center;
95
+ }
96
+ }
97
+
98
+ #docs {
99
+ border-right: 1px solid var(--border);
100
+
101
+ @media (max-width: 1024px) {
102
+ border-right: none;
103
+ border-bottom: 1px solid var(--border);
104
+ }
105
+ }
106
+
107
+ #next-steps ul {
108
+ list-style: none;
109
+ padding: 0;
110
+ display: flex;
111
+ gap: 8px;
112
+ margin: 32px 0 0;
113
+
114
+ .logo {
115
+ height: 18px;
116
+ }
117
+
118
+ a {
119
+ color: var(--text-h);
120
+ font-size: 16px;
121
+ border-radius: 6px;
122
+ background: var(--social-bg);
123
+ display: flex;
124
+ padding: 6px 12px;
125
+ align-items: center;
126
+ gap: 8px;
127
+ text-decoration: none;
128
+ transition: box-shadow 0.3s;
129
+
130
+ &:hover {
131
+ box-shadow: var(--shadow);
132
+ }
133
+ .button-icon {
134
+ height: 18px;
135
+ width: 18px;
136
+ }
137
+ }
138
+
139
+ @media (max-width: 1024px) {
140
+ margin-top: 20px;
141
+ flex-wrap: wrap;
142
+ justify-content: center;
143
+
144
+ li {
145
+ flex: 1 1 calc(50% - 8px);
146
+ }
147
+
148
+ a {
149
+ width: 100%;
150
+ justify-content: center;
151
+ box-sizing: border-box;
152
+ }
153
+ }
154
+ }
155
+
156
+ #spacer {
157
+ height: 88px;
158
+ border-top: 1px solid var(--border);
159
+ @media (max-width: 1024px) {
160
+ height: 48px;
161
+ }
162
+ }
163
+
164
+ .ticks {
165
+ position: relative;
166
+ width: 100%;
167
+
168
+ &::before,
169
+ &::after {
170
+ content: '';
171
+ position: absolute;
172
+ top: -4.5px;
173
+ border: 5px solid transparent;
174
+ }
175
+
176
+ &::before {
177
+ left: 0;
178
+ border-left-color: var(--border);
179
+ }
180
+ &::after {
181
+ right: 0;
182
+ border-right-color: var(--border);
183
+ }
184
+ }
frontend/src/App.tsx ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { AnimatePresence, motion } from 'framer-motion';
3
+ import { useChat } from './hooks/useChat';
4
+ import { Sidebar } from './components/Sidebar';
5
+ import { Navbar } from './components/Navbar';
6
+ import { Dashboard } from './components/Dashboard';
7
+ import { ChatInterface } from './components/ChatInterface';
8
+ import { CodePlayground } from './components/CodePlayground';
9
+ import { Settings } from './components/Settings';
10
+ import { AboutPage } from './components/AboutPage';
11
+
12
+ export const App: React.FC = () => {
13
+ const {
14
+ conversations,
15
+ activeConversationId,
16
+ setActiveConversationId,
17
+ activeConversation,
18
+ settings,
19
+ modelInfo,
20
+ metrics,
21
+ isLoading,
22
+ error,
23
+ updateSettings,
24
+ createNewConversation,
25
+ deleteConversation,
26
+ sendMessage,
27
+ executeCodeAction,
28
+ clearHistory,
29
+ } = useChat();
30
+
31
+ const [activeTab, setActiveTab] = useState<string>('dashboard');
32
+ const [sidebarCollapsed, setSidebarCollapsed] = useState<boolean>(false);
33
+ const [mobileSidebarOpen, setMobileSidebarOpen] = useState<boolean>(false);
34
+
35
+ const selectConversationFromSidebar = (id: string) => {
36
+ setActiveConversationId(id);
37
+ setActiveTab('chat');
38
+ setMobileSidebarOpen(false);
39
+ };
40
+
41
+ const selectTabFromNav = (tab: string) => {
42
+ setActiveTab(tab);
43
+ setMobileSidebarOpen(false);
44
+ };
45
+
46
+ return (
47
+ <div className="flex h-screen w-screen overflow-hidden bg-background text-foreground font-sans">
48
+
49
+ {/* 1. Sidebar Navigation (Desktop only, hidden on lg screens down) */}
50
+ <div className="hidden lg:block h-full">
51
+ <Sidebar
52
+ conversations={conversations}
53
+ activeConversationId={activeConversationId}
54
+ setActiveConversationId={selectConversationFromSidebar}
55
+ createNewConversation={createNewConversation}
56
+ deleteConversation={deleteConversation}
57
+ activeTab={activeTab}
58
+ setActiveTab={selectTabFromNav}
59
+ modelInfo={modelInfo}
60
+ collapsed={sidebarCollapsed}
61
+ setCollapsed={setSidebarCollapsed}
62
+ />
63
+ </div>
64
+
65
+ {/* 2. Responsive Overlay Drawer Sidebar (Mobile / Tablet) */}
66
+ <AnimatePresence>
67
+ {mobileSidebarOpen && (
68
+ <motion.div
69
+ initial={{ opacity: 0 }}
70
+ animate={{ opacity: 1 }}
71
+ exit={{ opacity: 0 }}
72
+ className="fixed inset-0 z-50 flex lg:hidden bg-background/80 backdrop-blur-sm"
73
+ >
74
+ <motion.div
75
+ initial={{ x: '-100%' }}
76
+ animate={{ x: 0 }}
77
+ exit={{ x: '-100%' }}
78
+ transition={{ type: 'spring', damping: 25, stiffness: 200 }}
79
+ className="relative"
80
+ >
81
+ <Sidebar
82
+ conversations={conversations}
83
+ activeConversationId={activeConversationId}
84
+ setActiveConversationId={selectConversationFromSidebar}
85
+ createNewConversation={createNewConversation}
86
+ deleteConversation={deleteConversation}
87
+ activeTab={activeTab}
88
+ setActiveTab={selectTabFromNav}
89
+ modelInfo={modelInfo}
90
+ collapsed={false}
91
+ setCollapsed={() => {}}
92
+ />
93
+ {/* Close trigger overlay inside the sidebar */}
94
+ <button
95
+ onClick={() => setMobileSidebarOpen(false)}
96
+ 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"
97
+ >
98
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" x2="6" y1="6" y2="18"></line><line x1="6" x2="18" y1="6" y2="18"></line></svg>
99
+ </button>
100
+ </motion.div>
101
+ {/* Backdrop shadow space clicking closer */}
102
+ <div className="flex-1 h-full" onClick={() => setMobileSidebarOpen(false)}></div>
103
+ </motion.div>
104
+ )}
105
+ </AnimatePresence>
106
+
107
+ {/* 3. Main content frame (Contains sticky header and tabs) */}
108
+ <main className="flex-1 flex flex-col min-w-0 overflow-hidden relative">
109
+ <Navbar
110
+ activeTab={activeTab}
111
+ modelInfo={modelInfo}
112
+ settings={settings}
113
+ updateSettings={updateSettings}
114
+ onOpenMobileSidebar={() => setMobileSidebarOpen(true)}
115
+ />
116
+
117
+ <div className="flex-1 flex flex-col min-h-0 overflow-hidden relative">
118
+ <AnimatePresence mode="wait">
119
+ <motion.div
120
+ key={activeTab}
121
+ initial={{ opacity: 0, y: 6 }}
122
+ animate={{ opacity: 1, y: 0 }}
123
+ exit={{ opacity: 0, y: -6 }}
124
+ transition={{ duration: 0.2 }}
125
+ className="flex-1 flex flex-col min-h-0 overflow-hidden"
126
+ >
127
+ {activeTab === 'dashboard' && (
128
+ <Dashboard
129
+ modelInfo={modelInfo}
130
+ metrics={metrics}
131
+ setActiveTab={selectTabFromNav}
132
+ createNewConversation={createNewConversation}
133
+ sendMessage={sendMessage}
134
+ />
135
+ )}
136
+
137
+ {activeTab === 'chat' && (
138
+ <ChatInterface
139
+ activeConversation={activeConversation}
140
+ sendMessage={sendMessage}
141
+ isLoading={isLoading}
142
+ error={error}
143
+ modelInfo={modelInfo}
144
+ />
145
+ )}
146
+
147
+ {activeTab === 'playground' && (
148
+ <CodePlayground
149
+ executeCodeAction={executeCodeAction}
150
+ isLoading={isLoading}
151
+ />
152
+ )}
153
+
154
+ {activeTab === 'settings' && (
155
+ <Settings
156
+ settings={settings}
157
+ updateSettings={updateSettings}
158
+ clearHistory={clearHistory}
159
+ modelInfo={modelInfo}
160
+ />
161
+ )}
162
+
163
+ {activeTab === 'about' && (
164
+ <AboutPage />
165
+ )}
166
+
167
+ {/* Placeholder tabs for nav items not yet implemented */}
168
+ {['history', 'documents', 'snippets', 'models'].includes(activeTab) && (
169
+ <div className="flex-1 flex flex-col items-center justify-center bg-[#090810] text-center p-8 space-y-4 select-none">
170
+ <div className="p-4 bg-primary/10 rounded-2xl text-primary">
171
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" x2="8" y1="13" y2="13"></line><line x1="16" x2="8" y1="17" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
172
+ </div>
173
+ <div className="space-y-1.5">
174
+ <h2 className="text-base font-bold text-white capitalize">{activeTab}</h2>
175
+ <p className="text-xs text-[#9d99b3] max-w-xs">This section is coming soon. Core chat and playground features are fully available.</p>
176
+ </div>
177
+ <button
178
+ onClick={() => selectTabFromNav('dashboard')}
179
+ 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"
180
+ >
181
+ Back to Dashboard
182
+ </button>
183
+ </div>
184
+ )}
185
+ </motion.div>
186
+ </AnimatePresence>
187
+ </div>
188
+ </main>
189
+ </div>
190
+ );
191
+ };
192
+
193
+ export default App;
frontend/src/assets/hero.png ADDED
frontend/src/assets/react.svg ADDED
frontend/src/assets/vite.svg ADDED
frontend/src/components/AboutPage.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Info, Cpu, Code, BookOpen, HardDrive, Share2 } from 'lucide-react';
3
+
4
+ export const AboutPage: React.FC = () => {
5
+ return (
6
+ <div className="flex-1 overflow-y-auto bg-background p-6 lg:p-8 space-y-8 select-none">
7
+ <div className="flex items-center gap-2 border-b border-border pb-4">
8
+ <span className="p-2 bg-primary/10 rounded-xl text-primary">
9
+ <Info size={20} />
10
+ </span>
11
+ <div>
12
+ <h1 className="text-2xl font-bold tracking-tight">About Antigravity AI Coder</h1>
13
+ <p className="text-xs text-muted-foreground">Learn about the architecture and technologies running under the hood.</p>
14
+ </div>
15
+ </div>
16
+
17
+ <div className="max-w-3xl space-y-6">
18
+ <div className="bg-card border border-border p-6 rounded-2xl space-y-4">
19
+ <h2 className="text-base font-bold flex items-center gap-2">
20
+ <Cpu size={18} className="text-primary" />
21
+ Hybrid Local/Cloud Architecture
22
+ </h2>
23
+ <p className="text-sm text-muted-foreground leading-relaxed">
24
+ Antigravity Coder is built on a cascading inference pipeline. It is configured to run the state-of-the-art <strong>Qwen2.5-Coder-0.5B-Instruct</strong> model locally in GGUF format using <code>llama-cpp-python</code>.
25
+ </p>
26
+ <p className="text-sm text-muted-foreground leading-relaxed">
27
+ 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 <strong>Hugging Face Serverless Inference API</strong>, guaranteeing high-performance execution without needing a local GPU or extensive memory budgets.
28
+ </p>
29
+ </div>
30
+
31
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
32
+ <div className="bg-card border border-border p-5 rounded-2xl space-y-2">
33
+ <h3 className="text-sm font-bold flex items-center gap-1.5">
34
+ <Code size={16} className="text-primary" />
35
+ Qwen2.5-Coder Engine
36
+ </h3>
37
+ <p className="text-xs text-muted-foreground leading-relaxed">
38
+ 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.
39
+ </p>
40
+ </div>
41
+
42
+ <div className="bg-card border border-border p-5 rounded-2xl space-y-2">
43
+ <h3 className="text-sm font-bold flex items-center gap-1.5">
44
+ <HardDrive size={16} className="text-primary" />
45
+ GGUF Quantization
46
+ </h3>
47
+ <p className="text-xs text-muted-foreground leading-relaxed">
48
+ 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.
49
+ </p>
50
+ </div>
51
+
52
+ <div className="bg-card border border-border p-5 rounded-2xl space-y-2">
53
+ <h3 className="text-sm font-bold flex items-center gap-1.5">
54
+ <BookOpen size={16} className="text-primary" />
55
+ Monaco Code Editor
56
+ </h3>
57
+ <p className="text-xs text-muted-foreground leading-relaxed">
58
+ Integrates the VS Code core editor (Monaco), delivering built-in code formatting, syntax highlighting, search/replace, and advanced editing features directly inside the Playground.
59
+ </p>
60
+ </div>
61
+
62
+ <div className="bg-card border border-border p-5 rounded-2xl space-y-2">
63
+ <h3 className="text-sm font-bold flex items-center gap-1.5">
64
+ <Share2 size={16} className="text-primary" />
65
+ Cloud Container Ready
66
+ </h3>
67
+ <p className="text-xs text-muted-foreground leading-relaxed">
68
+ Dockerized with independent backend and frontend packages, fully prepared for automated pipeline deployment on Railway, Render, and Fly.io.
69
+ </p>
70
+ </div>
71
+ </div>
72
+
73
+ <div className="text-center text-xs text-muted-foreground pt-4">
74
+ Antigravity AI Coding Assistant © 2026. Made with Google DeepMind Advanced Agentic Coding.
75
+ </div>
76
+ </div>
77
+ </div>
78
+ );
79
+ };
frontend/src/components/ChatInterface.tsx ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import {
4
+ Send, Sparkles, AlertCircle, Copy, Check,
5
+ Upload, Terminal, HelpCircle, FileText, Download,
6
+ CornerDownLeft, Paperclip, RotateCcw,
7
+ User, Shield, MessageSquare, Plus, Search, Code, Mic, Trash2
8
+ } from 'lucide-react';
9
+ import type { ChatMessage, Conversation, ModelInfo } from '../types';
10
+
11
+ interface ChatInterfaceProps {
12
+ activeConversation: Conversation | null;
13
+ sendMessage: (msg: string) => void;
14
+ isLoading: boolean;
15
+ error: string | null;
16
+ modelInfo: ModelInfo | null;
17
+ }
18
+
19
+ // Markdown & Code Renderer block
20
+ const MarkdownRenderer: React.FC<{ text: string }> = ({ text }) => {
21
+ const [copiedId, setCopiedId] = useState<string | null>(null);
22
+
23
+ const handleCopy = (code: string, blockId: string) => {
24
+ navigator.clipboard.writeText(code);
25
+ setCopiedId(blockId);
26
+ setTimeout(() => setCopiedId(null), 2000);
27
+ };
28
+
29
+ const handleDownload = (code: string, filename: string) => {
30
+ const blob = new Blob([code], { type: 'text/plain' });
31
+ const url = URL.createObjectURL(blob);
32
+ const a = document.createElement('a');
33
+ a.href = url;
34
+ a.download = filename;
35
+ a.click();
36
+ URL.revokeObjectURL(url);
37
+ };
38
+
39
+ if (!text) return null;
40
+
41
+ const parts = text.split(/(```[\s\S]*?```)/g);
42
+
43
+ return (
44
+ <div className="space-y-4 leading-relaxed text-sm select-text font-sans">
45
+ {parts.map((part, idx) => {
46
+ if (part.startsWith('```')) {
47
+ const lines = part.split('\n');
48
+ const firstLine = lines[0].replace('```', '').trim();
49
+ const language = firstLine || 'code';
50
+ const code = lines.slice(1, -1).join('\n');
51
+ const blockId = `${idx}-${language}`;
52
+ const filename = `code-snippet.${language === 'python' ? 'py' : language === 'javascript' ? 'js' : language === 'typescript' ? 'ts' : 'txt'}`;
53
+
54
+ const codeLines = code.split('\n');
55
+
56
+ return (
57
+ <div key={idx} className="my-4 border border-[#1d1b2e] rounded-xl overflow-hidden bg-[#0d1117] shadow-lg font-mono">
58
+ {/* Header Titlebar */}
59
+ <div className="flex items-center justify-between px-4 py-2 bg-[#161b22] border-b border-[#1d1b2e] text-xs text-[#9d99b3] select-none">
60
+ <span className="flex items-center gap-2 uppercase font-semibold text-foreground tracking-wide text-[11px]">
61
+ <Terminal size={12} className="text-primary" />
62
+ {language}
63
+ </span>
64
+ <div className="flex items-center gap-3">
65
+ <button
66
+ onClick={() => handleDownload(code, filename)}
67
+ className="flex items-center gap-1 hover:text-foreground transition-colors p-1"
68
+ title="Download File"
69
+ >
70
+ <Download size={13} />
71
+ </button>
72
+ <button
73
+ onClick={() => handleCopy(code, blockId)}
74
+ className="flex items-center gap-1.5 hover:text-foreground transition-colors p-1"
75
+ >
76
+ {copiedId === blockId ? (
77
+ <>
78
+ <Check size={13} className="text-emerald-500" />
79
+ <span className="text-emerald-500 font-bold text-[10px]">COPIED</span>
80
+ </>
81
+ ) : (
82
+ <>
83
+ <Copy size={13} />
84
+ <span className="text-[10px] font-bold">COPY</span>
85
+ </>
86
+ )}
87
+ </button>
88
+ </div>
89
+ </div>
90
+
91
+ {/* Editor area with line numbers */}
92
+ <div className="flex overflow-x-auto text-[12px] leading-6 py-3 bg-[#0d1117]">
93
+ <div className="text-right text-[#484f58] select-none pr-3 pl-4 border-r border-[#21262d] font-mono shrink-0">
94
+ {codeLines.map((_, i) => (
95
+ <div key={i}>{i + 1}</div>
96
+ ))}
97
+ </div>
98
+ <pre className="px-4 text-foreground font-mono w-full overflow-x-auto whitespace-pre">
99
+ <code>{code}</code>
100
+ </pre>
101
+ </div>
102
+ </div>
103
+ );
104
+ } else {
105
+ const lines = part.split('\n');
106
+ return (
107
+ <div key={idx} className="space-y-2">
108
+ {lines.map((line, lIdx) => {
109
+ const trimmed = line.trim();
110
+
111
+ if (trimmed.startsWith('* ') || trimmed.startsWith('- ')) {
112
+ return (
113
+ <ul key={lIdx} className="list-disc pl-6 my-1 space-y-1 text-foreground/90">
114
+ <li>{renderInlineStyles(trimmed.substring(2))}</li>
115
+ </ul>
116
+ );
117
+ }
118
+
119
+ if (trimmed.startsWith('#')) {
120
+ const level = (trimmed.match(/^#+/) || [''])[0].length;
121
+ const headerText = trimmed.replace(/^#+\s*/, '');
122
+ const headerClasses = level === 1
123
+ ? 'text-lg font-bold my-3 text-foreground tracking-tight border-b border-[#1d1b2e] pb-1.5'
124
+ : level === 2
125
+ ? 'text-base font-bold my-2 text-foreground tracking-tight'
126
+ : 'text-sm font-semibold my-2 text-foreground';
127
+ return <div key={lIdx} className={headerClasses}>{renderInlineStyles(headerText)}</div>;
128
+ }
129
+
130
+ return line ? <p key={lIdx} className="my-1.5 text-foreground/90 leading-relaxed">{renderInlineStyles(line)}</p> : <div key={lIdx} className="h-2" />;
131
+ })}
132
+ </div>
133
+ );
134
+ }
135
+ })}
136
+ </div>
137
+ );
138
+ };
139
+
140
+ const renderInlineStyles = (text: string) => {
141
+ const parts = text.split(/(\*\*.*?\*\*|`.*?`)/g);
142
+ return parts.map((part, idx) => {
143
+ if (part.startsWith('**') && part.endsWith('**')) {
144
+ return <strong key={idx} className="font-bold text-foreground">{part.slice(2, -2)}</strong>;
145
+ } else if (part.startsWith('`') && part.endsWith('`')) {
146
+ return <code key={idx} className="px-1.5 py-0.5 rounded bg-secondary font-mono text-xs text-primary font-semibold">{part.slice(1, -1)}</code>;
147
+ }
148
+ return part;
149
+ });
150
+ };
151
+
152
+ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
153
+ activeConversation,
154
+ sendMessage,
155
+ isLoading,
156
+ error,
157
+ modelInfo,
158
+ }) => {
159
+ const [input, setInput] = useState('');
160
+ const [searchQuery, setSearchQuery] = useState('');
161
+ const messagesEndRef = useRef<HTMLDivElement>(null);
162
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
163
+
164
+ // Hardcoded mock categories matching the reference screen design
165
+ const conversationsMock = {
166
+ today: [
167
+ { id: '1', title: 'Explain asyncio.gather', active: true },
168
+ { id: '2', title: 'React useMemo vs useCallback' },
169
+ ],
170
+ yesterday: [
171
+ { id: '3', title: 'SQL indexes best practices' },
172
+ { id: '4', title: 'Python list comprehension' },
173
+ ],
174
+ last7days: [
175
+ { id: '5', title: 'Fix Memory Leak' },
176
+ { id: '6', title: 'JWT authentication flow' },
177
+ { id: '7', title: 'Docker multi-stage build' },
178
+ ]
179
+ };
180
+
181
+ const scrollToBottom = () => {
182
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
183
+ };
184
+
185
+ useEffect(() => {
186
+ scrollToBottom();
187
+ }, [activeConversation?.messages, isLoading]);
188
+
189
+ useEffect(() => {
190
+ if (textareaRef.current) {
191
+ textareaRef.current.style.height = 'auto';
192
+ textareaRef.current.style.height = `${Math.min(120, textareaRef.current.scrollHeight)}px`;
193
+ }
194
+ }, [input]);
195
+
196
+ const handleSend = () => {
197
+ if (!input.trim() || isLoading) return;
198
+ sendMessage(input);
199
+ setInput('');
200
+ };
201
+
202
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
203
+ if (e.key === 'Enter' && !e.shiftKey) {
204
+ e.preventDefault();
205
+ handleSend();
206
+ }
207
+ };
208
+
209
+ const modelName = modelInfo?.model_name.split('/').pop() || 'SmolLM2-360M-Instruct';
210
+
211
+ return (
212
+ <div className="flex-1 flex h-full bg-[#090810] min-w-0 overflow-hidden font-sans">
213
+
214
+ {/* Split column 1: Conversation History List (320px width) */}
215
+ <div className="hidden md:flex w-[280px] flex-col border-r border-[#1d1b2e] bg-[#100d20] shrink-0 overflow-y-auto select-none">
216
+
217
+ {/* New Chat trigger button */}
218
+ <div className="p-4 border-b border-[#1d1b2e] space-y-3 shrink-0">
219
+ <button
220
+ onClick={() => sendMessage('Start a new clean chat session.')}
221
+ 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"
222
+ >
223
+ <Plus size={14} />
224
+ <span>New Chat</span>
225
+ </button>
226
+
227
+ {/* Search bar */}
228
+ <div className="relative">
229
+ <input
230
+ type="text"
231
+ value={searchQuery}
232
+ onChange={(e) => setSearchQuery(e.target.value)}
233
+ placeholder="Search conversations..."
234
+ 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"
235
+ />
236
+ <Search size={13} className="absolute left-3 top-3 text-[#58556f]" />
237
+ </div>
238
+ </div>
239
+
240
+ {/* Categories list */}
241
+ <div className="flex-1 p-3 space-y-4 overflow-y-auto text-xs">
242
+
243
+ {/* Today */}
244
+ <div className="space-y-1">
245
+ <h4 className="px-2 text-[10px] font-bold text-muted-foreground uppercase tracking-wider">Today</h4>
246
+ <div className="space-y-0.5">
247
+ {conversationsMock.today.map((item) => (
248
+ <button
249
+ key={item.id}
250
+ className={`w-full flex items-center justify-between p-2 rounded-xl text-left font-medium transition-all cursor-pointer ${
251
+ item.active
252
+ ? 'bg-[#2b1854] text-white border border-[#6344d5]/30'
253
+ : 'text-[#9d99b3] hover:text-foreground hover:bg-muted/15'
254
+ }`}
255
+ >
256
+ <span className="truncate">{item.title}</span>
257
+ </button>
258
+ ))}
259
+ </div>
260
+ </div>
261
+
262
+ {/* Yesterday */}
263
+ <div className="space-y-1">
264
+ <h4 className="px-2 text-[10px] font-bold text-muted-foreground uppercase tracking-wider">Yesterday</h4>
265
+ <div className="space-y-0.5">
266
+ {conversationsMock.yesterday.map((item) => (
267
+ <button
268
+ key={item.id}
269
+ className="w-full flex items-center justify-between p-2 rounded-xl text-left font-medium text-[#9d99b3] hover:text-foreground hover:bg-muted/15 transition-all cursor-pointer"
270
+ >
271
+ <span className="truncate">{item.title}</span>
272
+ </button>
273
+ ))}
274
+ </div>
275
+ </div>
276
+
277
+ {/* Last 7 days */}
278
+ <div className="space-y-1">
279
+ <h4 className="px-2 text-[10px] font-bold text-muted-foreground uppercase tracking-wider">Last 7 days</h4>
280
+ <div className="space-y-0.5">
281
+ {conversationsMock.last7days.map((item) => (
282
+ <button
283
+ key={item.id}
284
+ className="w-full flex items-center justify-between p-2 rounded-xl text-left font-medium text-[#9d99b3] hover:text-foreground hover:bg-muted/15 transition-all cursor-pointer"
285
+ >
286
+ <span className="truncate">{item.title}</span>
287
+ </button>
288
+ ))}
289
+ </div>
290
+ </div>
291
+
292
+ </div>
293
+
294
+ </div>
295
+
296
+ {/* Split column 2: Main chat scope */}
297
+ <div className="flex-1 flex flex-col h-full min-w-0 bg-[#090810]">
298
+
299
+ {/* Chat Header title */}
300
+ <div className="h-14 flex items-center justify-between px-6 border-b border-[#1d1b2e] select-none shrink-0 bg-[#100d20]">
301
+ <div>
302
+ <h2 className="text-xs font-bold text-white tracking-wide">
303
+ {activeConversation?.title && activeConversation.messages.length > 0
304
+ ? activeConversation.title
305
+ : 'Explain asyncio.gather'}
306
+ </h2>
307
+ </div>
308
+ <div className="text-[10px] text-[#9d99b3] font-mono">
309
+ {new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
310
+ </div>
311
+ </div>
312
+
313
+ {/* Messages Feed area */}
314
+ <div className="flex-1 overflow-y-auto p-6 space-y-6">
315
+ {!activeConversation || activeConversation.messages.length === 0 ? (
316
+ // Preload a clean reference conversation when empty to match screen 3
317
+ <div className="max-w-3xl mx-auto space-y-6">
318
+
319
+ {/* User Bubble (Right) */}
320
+ <div className="flex gap-4 flex-row-reverse">
321
+ <div className="w-8 h-8 rounded-full flex items-center justify-center text-[10px] font-bold select-none border shrink-0 bg-primary border-primary/20 text-primary-foreground">
322
+ <User size={13} />
323
+ </div>
324
+ <div className="space-y-1 max-w-[80%]">
325
+ <div className="px-4.5 py-3 rounded-2xl shadow-sm border border-[#6344d5]/30 text-sm leading-relaxed bg-[#2b1854] text-white rounded-tr-none">
326
+ Can you explain how asyncio.gather() works in Python?
327
+ </div>
328
+ <div className="text-[9px] text-[#58556f] text-right">10:30 AM</div>
329
+ </div>
330
+ </div>
331
+
332
+ {/* AI Bubble (Left) */}
333
+ <div className="flex gap-4">
334
+ <div className="w-8 h-8 rounded-full flex items-center justify-center text-[10px] font-bold select-none border border-border bg-[#131121] text-foreground shrink-0">
335
+ <Shield size={13} className="text-primary" />
336
+ </div>
337
+ <div className="space-y-1.5 max-w-[85%]">
338
+ <div className="p-4.5 rounded-2xl shadow-sm border border-[#1d1b2e] text-sm leading-relaxed bg-[#131121] text-foreground rounded-tl-none">
339
+ <p className="mb-3 leading-relaxed">
340
+ `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.
341
+ </p>
342
+ <p className="mb-3 font-semibold text-white">Here's how it works:</p>
343
+
344
+ <MarkdownRenderer text={`\`\`\`python
345
+ # Run multiple coroutines concurrently
346
+ results = await asyncio.gather(
347
+ fetch_data(url1),
348
+ fetch_data(url2),
349
+ fetch_data(url3)
350
+ )
351
+ \`\`\``} />
352
+
353
+ <p className="font-semibold text-white mt-3">Key points:</p>
354
+ <ul className="list-disc pl-5 space-y-1 mt-1 text-[#eae9fc]/90 text-xs">
355
+ <li>It runs all coroutines concurrently.</li>
356
+ <li>Returns results in the same order as the input list.</li>
357
+ <li>If `return_exceptions=True`, exceptions are returned instead of raised.</li>
358
+ </ul>
359
+ </div>
360
+ <div className="text-[9px] text-[#58556f]">10:30 AM</div>
361
+ </div>
362
+ </div>
363
+
364
+ </div>
365
+ ) : (
366
+ <div className="max-w-3xl mx-auto space-y-6">
367
+ {activeConversation.messages.map((msg, idx) => {
368
+ const isUser = msg.role === 'user';
369
+ return (
370
+ <div
371
+ key={idx}
372
+ className={`flex gap-4 group ${isUser ? 'flex-row-reverse' : ''}`}
373
+ >
374
+ <div className={`w-8 h-8 rounded-full flex items-center justify-center text-[10px] font-bold shadow-sm select-none border shrink-0 ${
375
+ isUser
376
+ ? 'bg-primary border-primary/20 text-primary-foreground'
377
+ : 'bg-card border-border text-foreground'
378
+ }`}>
379
+ {isUser ? <User size={13} /> : <Shield size={13} className="text-primary" />}
380
+ </div>
381
+
382
+ <div className="space-y-1 max-w-[85%]">
383
+ <div className={`p-4 rounded-2xl shadow-sm border text-sm leading-relaxed ${
384
+ isUser
385
+ ? 'bg-[#2b1854] border-[#6344d5]/30 text-white rounded-tr-none'
386
+ : 'bg-[#131121] border-[#1d1b2e] text-foreground rounded-tl-none'
387
+ }`}>
388
+ <MarkdownRenderer text={msg.content} />
389
+
390
+ {!msg.content && isLoading && !isUser && (
391
+ <div className="flex flex-col gap-2 py-2 w-64 select-none">
392
+ <div className="h-3 bg-muted rounded-full animate-shimmer w-full" />
393
+ <div className="h-3 bg-muted rounded-full animate-shimmer w-[85%]" />
394
+ <div className="h-3 bg-muted rounded-full animate-shimmer w-[60%]" />
395
+ </div>
396
+ )}
397
+ </div>
398
+ </div>
399
+ </div>
400
+ );
401
+ })}
402
+ </div>
403
+ )}
404
+ <div ref={messagesEndRef} />
405
+ </div>
406
+
407
+ {/* Error notification */}
408
+ {error && (
409
+ <div className="mx-6 mb-2 p-3 bg-destructive/10 border border-destructive/20 text-destructive text-xs rounded-xl flex items-center gap-2 shadow-sm shrink-0">
410
+ <AlertCircle size={15} />
411
+ <span className="font-semibold">Error: {error}</span>
412
+ </div>
413
+ )}
414
+
415
+ {/* Input panel bottom container */}
416
+ <div className="p-4 border-t border-[#1d1b2e] bg-[#100d20]">
417
+ <div className="max-w-3xl mx-auto relative flex items-end gap-3 border border-[#1d1b2e] focus-within:border-primary/50 bg-[#090810] rounded-xl p-3 shadow-md transition-all">
418
+ <button className="p-1.5 text-[#9d99b3] hover:text-white transition-colors cursor-pointer" title="Attach Code">
419
+ <Paperclip size={14} />
420
+ </button>
421
+ <button className="p-1.5 text-[#9d99b3] hover:text-white transition-colors cursor-pointer" title="Paste Block">
422
+ <Code size={14} />
423
+ </button>
424
+ <button className="p-1.5 text-[#9d99b3] hover:text-white transition-colors cursor-pointer" title="Record Prompt">
425
+ <Mic size={14} />
426
+ </button>
427
+
428
+ <textarea
429
+ ref={textareaRef}
430
+ value={input}
431
+ onChange={(e) => setInput(e.target.value)}
432
+ onKeyDown={handleKeyDown}
433
+ placeholder="Ask anything about your code..."
434
+ className="flex-1 max-h-[120px] min-h-[38px] resize-none outline-none border-none text-xs bg-transparent px-3.5 py-1.5 placeholder:text-[#58556f] text-white leading-relaxed font-sans"
435
+ rows={1}
436
+ />
437
+
438
+ <button
439
+ onClick={handleSend}
440
+ disabled={!input.trim() || isLoading}
441
+ className="p-2.5 bg-[#6344d5] hover:bg-[#6344d5]/90 text-white disabled:bg-[#131121] disabled:text-[#58556f] rounded-xl transition-all flex items-center justify-center cursor-pointer active:scale-95 glow-primary shrink-0"
442
+ >
443
+ <Send size={13} fill={input.trim() ? "white" : "none"} />
444
+ </button>
445
+ </div>
446
+
447
+ {/* Model Status info subtext */}
448
+ <div className="max-w-3xl mx-auto flex items-center gap-2 text-[10px] text-[#9d99b3] mt-2 px-1 select-none">
449
+ <span className="h-1.5 w-1.5 rounded-full bg-emerald-500"></span>
450
+ <span>{modelName}</span>
451
+ <span className="text-[#58556f]">•</span>
452
+ <span>Local Inference</span>
453
+ </div>
454
+ </div>
455
+
456
+ </div>
457
+
458
+ </div>
459
+ );
460
+ };
frontend/src/components/CodePlayground.tsx ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import {
4
+ Play, Trash2, Save, Terminal, Code2, Copy, Check, ChevronDown,
5
+ Maximize2, RefreshCw
6
+ } from 'lucide-react';
7
+ import { MonacoEditorWrapper } from './MonacoEditorWrapper';
8
+
9
+ interface CodePlaygroundProps {
10
+ executeCodeAction: (
11
+ action: 'explain' | 'debug' | 'refactor' | 'generate-tests' | 'summarize',
12
+ code: string,
13
+ language: string,
14
+ context?: string
15
+ ) => Promise<string>;
16
+ isLoading: boolean;
17
+ }
18
+
19
+ const LANGUAGES = [
20
+ { id: 'python', name: 'Python' },
21
+ { id: 'typescript', name: 'TypeScript' },
22
+ { id: 'javascript', name: 'JavaScript' },
23
+ { id: 'rust', name: 'Rust' },
24
+ { id: 'go', name: 'Go' },
25
+ { id: 'cpp', name: 'C++' },
26
+ { id: 'html', name: 'HTML' },
27
+ { id: 'css', name: 'CSS' },
28
+ ];
29
+
30
+ const THEMES = [
31
+ { id: 'dracula', name: 'Dracula' },
32
+ { id: 'monokai', name: 'Monokai' },
33
+ { id: 'vs-dark', name: 'VS Dark' },
34
+ { id: 'github-dark', name: 'GitHub Dark' },
35
+ ];
36
+
37
+ export const CodePlayground: React.FC<CodePlaygroundProps> = ({
38
+ executeCodeAction,
39
+ isLoading,
40
+ }) => {
41
+ const [code, setCode] = useState(`async def fetch_data(url: str):
42
+ import aiohttp
43
+ async with aiohttp.ClientSession() as session:
44
+ async with session.get(url) as response:
45
+ return await response.json()
46
+
47
+ async def main():
48
+ urls = [
49
+ "https://api.github.com/users/octocat",
50
+ "https://api.github.com/users/torvalds",
51
+ "https://api.github.com/users/gaearon"
52
+ ]
53
+
54
+ results = await asyncio.gather(*(fetch_data(url) for url in urls))
55
+
56
+ for user in results:
57
+ print(f"User: {user['login']} - {user['name']}")
58
+
59
+ if __name__ == "__main__":
60
+ import asyncio
61
+ asyncio.run(main())`);
62
+ const [language, setLanguage] = useState('python');
63
+ const [theme, setTheme] = useState('dracula');
64
+ const [output, setOutput] = useState(`octocat - The Octocat
65
+ torvalds - Linus Torvalds
66
+ gaearon - Dan Abramov
67
+
68
+ Process finished with exit code 0`);
69
+ const [activeConsoleTab, setActiveConsoleTab] = useState<'output' | 'console' | 'logs' | 'errors'>('output');
70
+ const [copied, setCopied] = useState(false);
71
+ const [statusText, setStatusText] = useState('Running main.py ...\nSuccess');
72
+
73
+ const handleRun = async () => {
74
+ if (!code.trim() || isLoading) return;
75
+ setStatusText('Running main.py ...\nExecuting local environment...');
76
+ try {
77
+ // Direct call to refactor/explain to act as Run execution
78
+ const result = await executeCodeAction('explain', code, language);
79
+ setOutput(result);
80
+ setStatusText('Running main.py ...\nSuccess');
81
+ } catch (e: any) {
82
+ setOutput(`Error during execution: ${e.message}`);
83
+ setStatusText('Running main.py ...\nFailed');
84
+ }
85
+ };
86
+
87
+ const handleClear = () => {
88
+ setCode('');
89
+ };
90
+
91
+ const handleCopy = () => {
92
+ navigator.clipboard.writeText(output);
93
+ setCopied(true);
94
+ setTimeout(() => setCopied(false), 2000);
95
+ };
96
+
97
+ return (
98
+ <div className="flex-1 flex flex-col h-full bg-[#090810] font-sans">
99
+
100
+ {/* Playground Top Toolbar */}
101
+ <div className="flex items-center justify-between px-6 py-3 border-b border-[#1d1b2e] bg-[#100d20] select-none shrink-0">
102
+
103
+ {/* Left: Dropdown selectors */}
104
+ <div className="flex items-center gap-4">
105
+ <div className="flex items-center gap-2">
106
+ <span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Language</span>
107
+ <div className="relative">
108
+ <select
109
+ value={language}
110
+ onChange={(e) => setLanguage(e.target.value)}
111
+ 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]"
112
+ >
113
+ {LANGUAGES.map((lang) => (
114
+ <option key={lang.id} value={lang.id}>{lang.name}</option>
115
+ ))}
116
+ </select>
117
+ <ChevronDown size={12} className="absolute right-2.5 top-2.5 text-muted-foreground pointer-events-none" />
118
+ </div>
119
+ </div>
120
+
121
+ <div className="flex items-center gap-2">
122
+ <span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Theme</span>
123
+ <div className="relative">
124
+ <select
125
+ value={theme}
126
+ onChange={(e) => setTheme(e.target.value)}
127
+ 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]"
128
+ >
129
+ {THEMES.map((t) => (
130
+ <option key={t.id} value={t.id}>{t.name}</option>
131
+ ))}
132
+ </select>
133
+ <ChevronDown size={12} className="absolute right-2.5 top-2.5 text-muted-foreground pointer-events-none" />
134
+ </div>
135
+ </div>
136
+ </div>
137
+
138
+ {/* Right: Actions */}
139
+ <div className="flex items-center gap-2.5">
140
+ <button
141
+ onClick={handleClear}
142
+ className="flex items-center gap-1.5 px-3 py-1.5 border border-[#1d1b2e] bg-[#131121] hover:bg-[#1c1a30] text-xs text-white font-bold rounded-lg cursor-pointer transition-all active:scale-95"
143
+ >
144
+ <Trash2 size={13} className="text-[#9d99b3]" />
145
+ <span>Clear</span>
146
+ </button>
147
+ <button
148
+ className="flex items-center gap-1.5 px-3 py-1.5 border border-[#1d1b2e] bg-[#131121] hover:bg-[#1c1a30] text-xs text-white font-bold rounded-lg cursor-pointer transition-all active:scale-95"
149
+ onClick={() => alert('Code saved successfully.')}
150
+ >
151
+ <Save size={13} className="text-[#9d99b3]" />
152
+ <span>Save</span>
153
+ </button>
154
+ <button
155
+ onClick={handleRun}
156
+ disabled={isLoading}
157
+ className="flex items-center gap-1.5 px-4.5 py-1.5 bg-[#6344d5] hover:bg-[#6344d5]/90 text-xs text-white font-bold rounded-lg cursor-pointer transition-all active:scale-95 shadow-sm glow-primary"
158
+ >
159
+ <Play size={13} fill="white" />
160
+ <span>Run</span>
161
+ </button>
162
+ </div>
163
+
164
+ </div>
165
+
166
+ {/* Main Split Layout: Editor & Outputs */}
167
+ <div className="flex-1 flex flex-col lg:flex-row min-h-0 overflow-hidden">
168
+
169
+ {/* Left: Code Editor Container */}
170
+ <div className="flex-1 flex flex-col border-r border-[#1d1b2e] min-h-0">
171
+ {/* Tab Header bar */}
172
+ <div className="flex h-10 items-center justify-between px-4 bg-[#100d20] border-b border-[#1d1b2e] select-none shrink-0">
173
+ <div className="flex items-center h-full">
174
+ <span className="flex items-center gap-2 px-4 h-full border-t-2 border-t-primary bg-[#090810] text-xs text-white font-semibold">
175
+ main.py
176
+ </span>
177
+ </div>
178
+ </div>
179
+
180
+ {/* Code Editor body */}
181
+ <div className="flex-1 min-h-0 relative">
182
+ <MonacoEditorWrapper
183
+ value={code}
184
+ onChange={setCode}
185
+ language={language}
186
+ theme="vs-dark"
187
+ />
188
+ </div>
189
+
190
+ {/* Editor Status Bar Footer */}
191
+ <div className="flex h-8 items-center justify-between px-4 bg-[#100d20] border-t border-[#1d1b2e] text-[10px] text-[#9d99b3] font-mono select-none shrink-0">
192
+ <div className="flex items-center gap-4">
193
+ <span>Ln 1, Col 1</span>
194
+ <span>Spaces: 4</span>
195
+ <span>UTF-8</span>
196
+ <span>LF</span>
197
+ <span className="uppercase">{language}</span>
198
+ </div>
199
+ <div className="flex items-center gap-3">
200
+ <span>Execution time: {isLoading ? 'Running...' : '1.24s'}</span>
201
+ <span>Memory: 45.6MB</span>
202
+ </div>
203
+ </div>
204
+ </div>
205
+
206
+ {/* Right: Output Review Panel */}
207
+ <div className="w-full lg:w-[40%] flex flex-col bg-[#090810] min-h-0">
208
+ {/* Output console tab headers */}
209
+ <div className="flex h-10 items-center justify-between px-4 bg-[#100d20] border-b border-[#1d1b2e] select-none shrink-0">
210
+ <div className="flex items-center h-full gap-1">
211
+ {(['output', 'console', 'logs', 'errors'] as const).map((tab) => (
212
+ <button
213
+ key={tab}
214
+ onClick={() => setActiveConsoleTab(tab)}
215
+ className={`px-3.5 h-full text-xs font-semibold uppercase tracking-wider border-b-2 transition-all cursor-pointer ${
216
+ activeConsoleTab === tab
217
+ ? 'border-primary text-white bg-[#090810]/40'
218
+ : 'border-transparent text-muted-foreground hover:text-foreground'
219
+ }`}
220
+ >
221
+ {tab}
222
+ </button>
223
+ ))}
224
+ </div>
225
+
226
+ <div className="flex items-center gap-3">
227
+ <button
228
+ onClick={handleCopy}
229
+ className="text-muted-foreground hover:text-foreground p-1 transition-colors"
230
+ title="Copy Logs"
231
+ >
232
+ {copied ? <Check size={13} className="text-emerald-500" /> : <Copy size={13} />}
233
+ </button>
234
+ </div>
235
+ </div>
236
+
237
+ {/* Console Text display */}
238
+ <div className="flex-1 p-5 overflow-y-auto font-mono text-[12px] leading-6 text-foreground select-text">
239
+ {activeConsoleTab === 'output' && (
240
+ <div className="space-y-4">
241
+ <pre className="text-white whitespace-pre-wrap">
242
+ {output}
243
+ </pre>
244
+
245
+ {/* Diagnostics Status line indicator */}
246
+ <div className="border-t border-[#1d1b2e] pt-4 text-[#58556f] space-y-1">
247
+ <div className="text-[#9d99b3] font-bold text-[10px] uppercase tracking-wider">Console output</div>
248
+ <pre className="text-emerald-500 text-[11px]">
249
+ {statusText}
250
+ </pre>
251
+ </div>
252
+ </div>
253
+ )}
254
+
255
+ {activeConsoleTab === 'console' && (
256
+ <pre className="text-blue-400"># Interactive local interpreter session is active</pre>
257
+ )}
258
+ {activeConsoleTab === 'logs' && (
259
+ <pre className="text-[#9d99b3]">[INFO] 172.18.0.3 - "GET /metrics HTTP/1.1" 200 OK</pre>
260
+ )}
261
+ {activeConsoleTab === 'errors' && (
262
+ <pre className="text-[#58556f]">No compile execution errors reported.</pre>
263
+ )}
264
+ </div>
265
+ </div>
266
+
267
+ </div>
268
+
269
+ </div>
270
+ );
271
+ };
frontend/src/components/Dashboard.tsx ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import {
4
+ MessageSquare, Code, FileText, ShieldAlert, List, RefreshCw,
5
+ Cpu, HardDrive, Zap, BarChart2, ChevronRight, HelpCircle, Database
6
+ } from 'lucide-react';
7
+ import type { ModelInfo, ModelMetrics } from '../types';
8
+
9
+ interface DashboardProps {
10
+ modelInfo: ModelInfo | null;
11
+ metrics: ModelMetrics | null;
12
+ setActiveTab: (tab: string) => void;
13
+ createNewConversation: (title?: string) => void;
14
+ sendMessage: (msg: string) => void;
15
+ }
16
+
17
+ export const Dashboard: React.FC<DashboardProps> = ({
18
+ modelInfo,
19
+ metrics,
20
+ setActiveTab,
21
+ createNewConversation,
22
+ sendMessage,
23
+ }) => {
24
+ const quickActions = [
25
+ { id: 'chat', title: 'AI Chat', desc: 'Chat with your AI assistant', icon: MessageSquare },
26
+ { id: 'playground', title: 'Playground', desc: 'Run code and experiments', icon: Code },
27
+ { id: 'explain', title: 'Explain Code', desc: 'Get detailed explanations', icon: FileText, prompt: 'Explain how this code block functions and list edge cases:\n\n' },
28
+ { id: 'debug', title: 'Debug Code', desc: 'Find and fix issues', icon: ShieldAlert, prompt: 'Find bugs and syntax errors in this code block:\n\n' },
29
+ { id: 'tests', title: 'Generate Tests', desc: 'Create unit tests', icon: List, prompt: 'Write comprehensive unit tests for this code block:\n\n' },
30
+ { id: 'refactor', title: 'Refactor Code', desc: 'Improve code quality', icon: RefreshCw, prompt: 'Refactor this code block to improve readability and complexity:\n\n' },
31
+ ];
32
+
33
+ const recentConversations = [
34
+ { title: 'Python async queue implementation', time: '2 mins ago' },
35
+ { title: 'React custom hook optimization', time: '28 mins ago' },
36
+ { title: 'SQL query performance tuning', time: '1 hour ago' },
37
+ { title: 'Debug memory leak in Node.js', time: '3 hours ago' },
38
+ { title: 'Implement auth with JWT', time: '5 hours ago' },
39
+ ];
40
+
41
+ const examplePrompts = [
42
+ { label: 'Explain this code', prompt: 'Explain the runtime complexity of this function:\n\n', icon: FileText },
43
+ { label: 'Write a Python function', prompt: 'Write a Python function to check if a string is a palindrome.', icon: Code },
44
+ { label: 'Debug this error', prompt: 'Explain how to fix this TypeError exception:\n\n', icon: ShieldAlert },
45
+ { label: 'Optimize this SQL query', prompt: 'Optimize this slow SQL query using proper indexes:\n\n', icon: Database },
46
+ { label: 'Generate unit tests', prompt: 'Generate unit tests for a standard user authentication class.', icon: List },
47
+ ];
48
+
49
+ const handleActionClick = (action: typeof quickActions[0]) => {
50
+ if (action.id === 'chat') {
51
+ createNewConversation();
52
+ setActiveTab('chat');
53
+ } else if (action.id === 'playground') {
54
+ setActiveTab('playground');
55
+ } else if (action.prompt) {
56
+ createNewConversation(action.title);
57
+ setActiveTab('chat');
58
+ setTimeout(() => {
59
+ sendMessage(action.prompt || '');
60
+ }, 100);
61
+ }
62
+ };
63
+
64
+ const handlePromptClick = (label: string, promptText: string) => {
65
+ createNewConversation(label);
66
+ setActiveTab('chat');
67
+ setTimeout(() => {
68
+ sendMessage(promptText);
69
+ }, 100);
70
+ };
71
+
72
+ // Safe formatting variables matching image values
73
+ const modelName = modelInfo?.model_name.split('/').pop() || 'SmolLM2-360M';
74
+ const memoryUsed = modelInfo?.memory_usage_gb || 1.53;
75
+ const memoryTotal = modelInfo?.total_memory_gb || 7.65;
76
+ const memoryPercent = Math.min(100, Math.round((memoryUsed / memoryTotal) * 100)) || 20;
77
+
78
+ const latency = metrics?.average_latency_seconds || 22.13;
79
+ const totalTokens = metrics?.total_tokens || 816;
80
+
81
+ return (
82
+ <div className="flex-1 overflow-y-auto bg-[#090810] p-6 space-y-6 font-sans max-w-[1400px] mx-auto w-full">
83
+
84
+ {/* Welcome Row with status badges */}
85
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-[#1d1b2e] pb-5 select-none">
86
+ <div className="space-y-1">
87
+ <h1 className="text-2xl font-bold tracking-tight text-white">Welcome back, Developer 👋</h1>
88
+ <p className="text-xs text-[#9d99b3]">Your AI Coding Assistant is ready to help you build, debug and ship faster.</p>
89
+ </div>
90
+ <div className="flex items-center gap-3">
91
+ <div className="flex items-center gap-2 px-3.5 py-1.5 bg-[#131121] border border-[#1d1b2e] rounded-xl text-xs">
92
+ <span className="text-[#9d99b3]">LOCAL MODEL:</span>
93
+ <span className="font-semibold font-mono text-primary">{modelName}</span>
94
+ </div>
95
+ <div className="flex items-center gap-2 px-3.5 py-1.5 bg-[#131121] border border-[#1d1b2e] rounded-xl text-xs">
96
+ <span className="text-[#9d99b3]">SYSTEM STATUS:</span>
97
+ <span className="flex items-center gap-1.5 font-semibold text-emerald-500">
98
+ <span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
99
+ Healthy
100
+ </span>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ {/* Grid: System Status & Metrics */}
106
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 select-none">
107
+
108
+ {/* Model Status Card */}
109
+ <div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl flex flex-col gap-4 min-h-[110px] hover:border-primary/20 transition-all duration-300">
110
+ <div className="flex items-center justify-between">
111
+ <span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Model Status</span>
112
+ <div className="p-1.5 bg-emerald-500/10 rounded-lg text-emerald-500">
113
+ <Cpu size={14} />
114
+ </div>
115
+ </div>
116
+ <div className="space-y-1 flex-1 flex flex-col justify-end">
117
+ <h2 className="text-sm font-bold truncate text-white">{modelName}</h2>
118
+ <p className="text-[10px] text-emerald-500 font-semibold">Local Inference</p>
119
+ </div>
120
+ </div>
121
+
122
+ {/* Memory Allocation Card with Progress Bar */}
123
+ <div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl flex flex-col gap-4 min-h-[110px] hover:border-primary/20 transition-all duration-300">
124
+ <div className="flex items-center justify-between">
125
+ <span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Memory Usage</span>
126
+ <div className="p-1.5 bg-blue-500/10 rounded-lg text-blue-500">
127
+ <HardDrive size={14} />
128
+ </div>
129
+ </div>
130
+ <div className="space-y-2 flex-1 flex flex-col justify-end">
131
+ <div className="flex justify-between items-baseline">
132
+ <h2 className="text-sm font-bold text-white">{memoryUsed.toFixed(2)} GB <span className="text-[10px] text-[#58556f] font-normal">/ {memoryTotal.toFixed(2)} GB</span></h2>
133
+ <span className="text-[10px] text-blue-400 font-semibold">{memoryPercent}%</span>
134
+ </div>
135
+ <div className="w-full bg-[#1c1a30] h-1.5 rounded-full overflow-hidden">
136
+ <div className="bg-blue-500 h-full rounded-full" style={{ width: `${memoryPercent}%` }}></div>
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ {/* Latency Card */}
142
+ <div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl flex flex-col gap-4 min-h-[110px] hover:border-primary/20 transition-all duration-300">
143
+ <div className="flex items-center justify-between">
144
+ <span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Avg. Latency</span>
145
+ <div className="p-1.5 bg-primary/10 rounded-lg text-primary">
146
+ <Zap size={14} />
147
+ </div>
148
+ </div>
149
+ <div className="space-y-1 flex-1 flex flex-col justify-end">
150
+ <h2 className="text-xl font-bold text-white">{latency.toFixed(2)} s</h2>
151
+ <p className="text-[10px] text-[#58556f]">Per request</p>
152
+ </div>
153
+ </div>
154
+
155
+ {/* Tokens Card */}
156
+ <div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl flex flex-col gap-4 min-h-[110px] hover:border-primary/20 transition-all duration-300">
157
+ <div className="flex items-center justify-between">
158
+ <span className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Tokens Used</span>
159
+ <div className="p-1.5 bg-red-500/10 rounded-lg text-red-500">
160
+ <BarChart2 size={14} />
161
+ </div>
162
+ </div>
163
+ <div className="space-y-1 flex-1 flex flex-col justify-end">
164
+ <h2 className="text-xl font-bold text-white">{totalTokens.toLocaleString()}</h2>
165
+ <p className="text-[10px] text-[#58556f]">Total (Input + Output)</p>
166
+ </div>
167
+ </div>
168
+
169
+ </div>
170
+
171
+ {/* Main Layout Split */}
172
+ <div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
173
+
174
+ {/* Left: Quick Actions Grid */}
175
+ <div className="lg:col-span-3 space-y-4">
176
+ <div className="select-none">
177
+ <h3 className="text-sm font-bold text-white">Quick Actions</h3>
178
+ <p className="text-[10px] text-[#9d99b3] mt-0.5">Choose a tool to get started</p>
179
+ </div>
180
+
181
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
182
+ {quickActions.map((action, idx) => {
183
+ const Icon = action.icon;
184
+ return (
185
+ <button
186
+ key={idx}
187
+ onClick={() => handleActionClick(action)}
188
+ 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"
189
+ >
190
+ <div className="p-2 bg-primary/10 rounded-lg text-primary glow-primary shrink-0 group-hover:scale-105 transition-transform">
191
+ <Icon size={15} />
192
+ </div>
193
+ <div className="space-y-0.5 min-w-0">
194
+ <h4 className="font-bold text-xs text-white group-hover:text-primary transition-colors">{action.title}</h4>
195
+ <p className="text-[10px] text-[#9d99b3] leading-relaxed truncate">{action.desc}</p>
196
+ </div>
197
+ </button>
198
+ );
199
+ })}
200
+ </div>
201
+ </div>
202
+
203
+ {/* Right: Recent Conversations */}
204
+ <div className="lg:col-span-2 space-y-4 flex flex-col">
205
+ <div className="flex items-center justify-between select-none">
206
+ <h3 className="text-sm font-bold text-white">Recent Conversations</h3>
207
+ <button
208
+ onClick={() => setActiveTab('chat')}
209
+ className="text-[10px] font-bold text-primary hover:underline cursor-pointer"
210
+ >
211
+ View all
212
+ </button>
213
+ </div>
214
+
215
+ <div className="flex-1 bg-[#131121] border border-[#1d1b2e] rounded-2xl overflow-hidden select-none">
216
+ {recentConversations.map((conv, idx) => (
217
+ <div
218
+ key={idx}
219
+ className="flex items-center justify-between px-4 py-3 border-b border-[#1d1b2e] last:border-0 hover:bg-[#1c1a30] cursor-pointer transition-colors gap-4"
220
+ onClick={() => { createNewConversation(conv.title); setActiveTab('chat'); }}
221
+ >
222
+ <span className="text-xs font-medium text-[#eae9fc] truncate min-w-0 flex-1">{conv.title}</span>
223
+ <span className="text-[10px] text-[#58556f] font-mono shrink-0 whitespace-nowrap">{conv.time}</span>
224
+ </div>
225
+ ))}
226
+ </div>
227
+ </div>
228
+
229
+ </div>
230
+
231
+ {/* Bottom: Example Prompts */}
232
+ <div className="space-y-4 pt-2">
233
+ <div className="select-none">
234
+ <h3 className="text-sm font-bold text-white">Example Prompts</h3>
235
+ <p className="text-[10px] text-[#9d99b3] mt-0.5">Try these examples to get started</p>
236
+ </div>
237
+
238
+ <div className="flex flex-wrap gap-3">
239
+ {examplePrompts.map((ep, idx) => {
240
+ const Icon = ep.icon;
241
+ return (
242
+ <button
243
+ key={idx}
244
+ onClick={() => handlePromptClick(ep.label, ep.prompt)}
245
+ 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"
246
+ >
247
+ <Icon size={12} className="text-[#9d99b3]" />
248
+ <span>{ep.label}</span>
249
+ </button>
250
+ );
251
+ })}
252
+ </div>
253
+ </div>
254
+
255
+ </div>
256
+ );
257
+ };
frontend/src/components/MonacoEditorWrapper.tsx ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef } from 'react';
2
+ import Editor from '@monaco-editor/react';
3
+ import type { Monaco } from '@monaco-editor/react';
4
+
5
+ interface MonacoEditorWrapperProps {
6
+ value: string;
7
+ onChange: (val: string) => void;
8
+ language: string;
9
+ theme?: 'vs-dark' | 'light';
10
+ height?: string;
11
+ readOnly?: boolean;
12
+ }
13
+
14
+ export const MonacoEditorWrapper: React.FC<MonacoEditorWrapperProps> = ({
15
+ value,
16
+ onChange,
17
+ language,
18
+ theme = 'vs-dark',
19
+ height = '100%',
20
+ readOnly = false,
21
+ }) => {
22
+ const editorRef = useRef<any>(null);
23
+
24
+ const handleEditorDidMount = (editor: any, monaco: Monaco) => {
25
+ editorRef.current = editor;
26
+
27
+ monaco.editor.defineTheme('antigravity-dark', {
28
+ base: 'vs-dark',
29
+ inherit: true,
30
+ rules: [
31
+ { token: 'comment', foreground: '6272a4', fontStyle: 'italic' },
32
+ { token: 'keyword', foreground: 'ff79c6' },
33
+ { token: 'string', foreground: 'f1fa8c' },
34
+ { token: 'number', foreground: 'bd93f9' },
35
+ { token: 'regexp', foreground: 'ffb86c' },
36
+ { token: 'type', foreground: '8be9fd' },
37
+ ],
38
+ colors: {
39
+ 'editor.background': '#0b0f19',
40
+ 'editor.foreground': '#f8f8f2',
41
+ 'editorLineNumber.foreground': '#4b5563',
42
+ 'editorLineNumber.activeForeground': '#3b82f6',
43
+ 'editor.lineHighlightBackground': '#1e293b50',
44
+ 'editorCursor.foreground': '#3b82f6',
45
+ },
46
+ });
47
+
48
+ monaco.editor.setTheme(theme === 'vs-dark' ? 'antigravity-dark' : 'light');
49
+ };
50
+
51
+ const handleEditorChange = (value: string | undefined) => {
52
+ if (value !== undefined) {
53
+ onChange(value);
54
+ }
55
+ };
56
+
57
+ return (
58
+ <div className="w-full h-full min-h-[300px] border border-border rounded-xl overflow-hidden shadow-sm">
59
+ <Editor
60
+ height={height}
61
+ language={language}
62
+ value={value}
63
+ onChange={handleEditorChange}
64
+ onMount={handleEditorDidMount}
65
+ loading={
66
+ <div className="flex items-center justify-center h-full bg-card text-muted-foreground text-xs gap-2">
67
+ <span className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin"></span>
68
+ Loading editor assets...
69
+ </div>
70
+ }
71
+ options={{
72
+ readOnly,
73
+ minimap: { enabled: true },
74
+ fontSize: 14,
75
+ fontFamily: "'Fira Code', 'Courier New', monospace",
76
+ fontLigatures: true,
77
+ lineNumbers: 'on',
78
+ roundedSelection: true,
79
+ scrollBeyondLastLine: false,
80
+ automaticLayout: true,
81
+ tabSize: 4,
82
+ padding: { top: 12, bottom: 12 },
83
+ wordWrap: 'on',
84
+ }}
85
+ />
86
+ </div>
87
+ );
88
+ };
frontend/src/components/Navbar.tsx ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import {
4
+ Bell, Search, Sun, Moon, Cpu, Globe,
5
+ CheckCircle2, AlertCircle, HelpCircle
6
+ } from 'lucide-react';
7
+ import type { ModelInfo, AppSettings } from '../types';
8
+
9
+ interface NavbarProps {
10
+ activeTab: string;
11
+ modelInfo: ModelInfo | null;
12
+ settings: AppSettings;
13
+ updateSettings: (newSettings: Partial<AppSettings>) => void;
14
+ onSearchClick?: () => void;
15
+ onOpenMobileSidebar?: () => void;
16
+ }
17
+
18
+ export const Navbar: React.FC<NavbarProps> = ({
19
+ activeTab,
20
+ modelInfo,
21
+ settings,
22
+ updateSettings,
23
+ onSearchClick,
24
+ onOpenMobileSidebar,
25
+ }) => {
26
+ const getTabTitle = () => {
27
+ switch (activeTab) {
28
+ case 'dashboard': return 'Dashboard';
29
+ case 'chat': return 'AI Chat Console';
30
+ case 'playground': return 'Code Playground';
31
+ case 'history': return 'History';
32
+ case 'documents': return 'Documents';
33
+ case 'snippets': return 'Snippets';
34
+ case 'models': return 'Models';
35
+ case 'settings': return 'System Settings';
36
+ case 'about': return 'About';
37
+ default: return 'Dashboard';
38
+ }
39
+ };
40
+
41
+ const toggleTheme = () => {
42
+ updateSettings({ theme: settings.theme === 'dark' ? 'light' : 'dark' });
43
+ };
44
+
45
+ return (
46
+ <motion.header
47
+ initial={{ opacity: 0, y: -8 }}
48
+ animate={{ opacity: 1, y: 0 }}
49
+ transition={{ duration: 0.3 }}
50
+ className="sticky top-0 z-40 w-full border-b border-border bg-card/60 backdrop-blur-md select-none shrink-0"
51
+ >
52
+ <div className="flex h-16 items-center justify-between px-6 gap-4">
53
+
54
+ {/* Left Section: Breadcrumb/Page Title */}
55
+ <div className="flex items-center gap-3">
56
+ {onOpenMobileSidebar && (
57
+ <button
58
+ onClick={onOpenMobileSidebar}
59
+ className="lg:hidden p-2 text-muted-foreground hover:text-foreground hover:bg-muted/50 rounded-lg transition-colors cursor-pointer"
60
+ >
61
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="4" x2="20" y1="12" y2="12"></line><line x1="4" x2="20" y1="6" y2="6"></line><line x1="4" x2="20" y1="18" y2="18"></line></svg>
62
+ </button>
63
+ )}
64
+ <span className="text-[#9d99b3] text-xs">‹</span>
65
+ <h1 className="text-sm font-bold tracking-tight text-foreground">{getTabTitle()}</h1>
66
+ </div>
67
+
68
+ {/* Middle Section: Mock Search Bar */}
69
+ <div className="hidden md:flex flex-1 max-w-md mx-auto">
70
+ <div
71
+ onClick={onSearchClick}
72
+ className="w-full flex items-center justify-between px-3 py-2 bg-background hover:bg-muted/30 border border-border focus-within:border-primary/50 rounded-xl text-xs text-muted-foreground transition-all cursor-pointer group"
73
+ >
74
+ <div className="flex items-center gap-2">
75
+ <Search size={14} className="text-muted-foreground group-hover:text-foreground transition-colors" />
76
+ <span>Quick search prompts...</span>
77
+ </div>
78
+ <kbd className="inline-flex h-5 select-none items-center gap-0.5 rounded border border-border bg-muted/50 px-1.5 font-mono text-[9px] font-medium text-muted-foreground opacity-100">
79
+ <span className="text-[10px]">⌘</span>K
80
+ </kbd>
81
+ </div>
82
+ </div>
83
+
84
+ {/* Right Section: Diagnostics Status Controls */}
85
+ <div className="flex items-center gap-3">
86
+
87
+ {/* Connection badge */}
88
+ <div className="hidden sm:flex items-center gap-2 px-3 py-1.5 bg-secondary border border-border rounded-full text-[10px] font-semibold text-muted-foreground">
89
+ {modelInfo?.status === 'loaded' ? (
90
+ <>
91
+ <CheckCircle2 size={12} className="text-emerald-500" />
92
+ <span className="text-emerald-500 font-bold uppercase tracking-wider">Online</span>
93
+ </>
94
+ ) : modelInfo?.status === 'loading' ? (
95
+ <>
96
+ <div className="h-1.5 w-1.5 rounded-full bg-amber-500 animate-pulse" />
97
+ <span className="text-amber-500 font-bold uppercase tracking-wider">Syncing</span>
98
+ </>
99
+ ) : (
100
+ <>
101
+ <AlertCircle size={12} className="text-muted-foreground" />
102
+ <span className="font-bold uppercase tracking-wider">Offline</span>
103
+ </>
104
+ )}
105
+ </div>
106
+
107
+ {/* Model info indicator */}
108
+ <div className="hidden lg:flex items-center gap-2 px-3 py-1.5 bg-secondary border border-border rounded-full text-xs font-semibold text-foreground">
109
+ <Cpu size={13} className="text-primary animate-pulse" />
110
+ <span className="truncate max-w-[120px] font-mono text-[11px]">
111
+ {modelInfo?.model_name.split('/').pop() || 'SmolLM2-360M'}
112
+ </span>
113
+ </div>
114
+
115
+ <div className="h-4 w-px bg-border hidden sm:block"></div>
116
+
117
+ {/* Notifications Trigger */}
118
+ <button className="p-2 text-muted-foreground hover:text-foreground hover:bg-muted/50 rounded-xl transition-colors cursor-pointer relative">
119
+ <Bell size={16} />
120
+ <span className="absolute top-2 right-2 h-1.5 w-1.5 rounded-full bg-primary animate-pulse-glow" />
121
+ </button>
122
+
123
+ {/* Theme Selector */}
124
+ <button
125
+ onClick={toggleTheme}
126
+ className="p-2 text-muted-foreground hover:text-foreground hover:bg-muted/50 rounded-xl transition-colors cursor-pointer"
127
+ title={settings.theme === 'dark' ? 'Light Theme' : 'Dark Theme'}
128
+ >
129
+ {settings.theme === 'dark' ? <Sun size={16} /> : <Moon size={16} />}
130
+ </button>
131
+
132
+ {/* User Profile avatar */}
133
+ <div className="h-8 w-8 rounded-full border border-border bg-gradient-to-tr from-primary to-accent text-primary-foreground flex items-center justify-center font-bold text-xs shadow-sm cursor-pointer hover:border-primary/50 transition-colors select-none">
134
+ AC
135
+ </div>
136
+ </div>
137
+
138
+ </div>
139
+ </motion.header>
140
+ );
141
+ };
frontend/src/components/Settings.tsx ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import {
3
+ Cpu, Sliders, AlertTriangle, ShieldCheck, Save, CheckCircle2,
4
+ HardDrive, Info, Activity, RefreshCw
5
+ } from 'lucide-react';
6
+ import type { AppSettings, ModelInfo } from '../types';
7
+
8
+ interface SettingsProps {
9
+ settings: AppSettings;
10
+ updateSettings: (newSettings: Partial<AppSettings>) => void;
11
+ clearHistory: () => void;
12
+ modelInfo: ModelInfo | null;
13
+ }
14
+
15
+ export const Settings: React.FC<SettingsProps> = ({
16
+ settings,
17
+ updateSettings,
18
+ clearHistory,
19
+ modelInfo,
20
+ }) => {
21
+ const [saveSuccess, setSaveSuccess] = useState(false);
22
+ const [resetting, setResetting] = useState(false);
23
+ const [confirmClear, setConfirmClear] = useState(false);
24
+
25
+ const handleSave = () => {
26
+ setSaveSuccess(true);
27
+ setTimeout(() => setSaveSuccess(false), 2000);
28
+ };
29
+
30
+ const handleClearHistory = () => {
31
+ if (confirmClear) {
32
+ setResetting(true);
33
+ setTimeout(() => {
34
+ clearHistory();
35
+ setConfirmClear(false);
36
+ setResetting(false);
37
+ alert('Local database log history has been successfully reset.');
38
+ }, 800);
39
+ } else {
40
+ setConfirmClear(true);
41
+ }
42
+ };
43
+
44
+ // Safe formatting variables matching image specifications
45
+ const memoryUsed = modelInfo?.memory_usage_gb || 1.53;
46
+ const memoryTotal = modelInfo?.total_memory_gb || 7.65;
47
+ const memoryPercent = Math.min(100, Math.round((memoryUsed / memoryTotal) * 100)) || 20;
48
+
49
+ return (
50
+ <div className="flex-1 overflow-y-auto bg-[#090810] p-6 lg:p-8 font-sans select-none">
51
+
52
+ {/* Settings Split Layout grid */}
53
+ <div className="max-w-6xl grid grid-cols-1 lg:grid-cols-5 gap-8 items-start">
54
+
55
+ {/* Left Column Form (3/5 width) */}
56
+ <div className="lg:col-span-3 space-y-5 bg-[#131121] border border-[#1d1b2e] p-6 rounded-2xl">
57
+
58
+ <div className="border-b border-[#1d1b2e] pb-4">
59
+ <h2 className="text-md font-bold text-white tracking-tight">Model & Inference</h2>
60
+ <p className="text-[10px] text-[#9d99b3] mt-0.5">Configure how the AI model runs and generates responses.</p>
61
+ </div>
62
+
63
+ <div className="space-y-5 pt-1">
64
+
65
+ {/* Inference Mode selector */}
66
+ <div className="space-y-1.5">
67
+ <label className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Inference Mode</label>
68
+ <select
69
+ className="w-full bg-[#090810] border border-[#1d1b2e] text-xs text-white rounded-xl px-3 py-2.5 outline-none font-semibold cursor-pointer"
70
+ disabled
71
+ >
72
+ <option>Local Inference</option>
73
+ </select>
74
+ </div>
75
+
76
+ {/* Target model selector */}
77
+ <div className="space-y-1.5">
78
+ <div className="flex justify-between items-center select-none">
79
+ <label className="text-[10px] font-bold text-[#9d99b3] uppercase tracking-wider">Model</label>
80
+ <span className="flex items-center gap-1 text-[10px] text-emerald-500 font-bold uppercase tracking-wider bg-emerald-500/10 px-2 py-0.5 rounded">
81
+ <CheckCircle2 size={10} />
82
+ Loaded
83
+ </span>
84
+ </div>
85
+ <select
86
+ className="w-full bg-[#090810] border border-[#1d1b2e] text-xs text-white rounded-xl px-3 py-2.5 outline-none font-mono cursor-default"
87
+ disabled
88
+ >
89
+ <option>{modelInfo?.local_model_path.split('/').pop() || 'SmolLM2-360M-Instruct-Q4_K_M.gguf'}</option>
90
+ </select>
91
+ </div>
92
+
93
+ <div className="h-px bg-[#1d1b2e] my-2"></div>
94
+
95
+ {/* Context length slider */}
96
+ <div className="space-y-2">
97
+ <div className="flex justify-between text-xs font-semibold">
98
+ <span className="text-white">Context Length</span>
99
+ <span className="text-primary font-mono text-[11px] bg-primary/10 px-2 py-0.5 rounded">{settings.contextLength}</span>
100
+ </div>
101
+ <input
102
+ type="range"
103
+ min="256"
104
+ max="4096"
105
+ step="256"
106
+ value={settings.contextLength}
107
+ onChange={(e) => updateSettings({ contextLength: parseInt(e.target.value) })}
108
+ className="w-full accent-primary h-1 bg-[#090810] rounded-lg appearance-none cursor-pointer focus:outline-none"
109
+ />
110
+ <p className="text-[9px] text-[#9d99b3]">Maximum context length constraints for the model.</p>
111
+ </div>
112
+
113
+ {/* Temperature Slider */}
114
+ <div className="space-y-2">
115
+ <div className="flex justify-between text-xs font-semibold">
116
+ <span className="text-white">Temperature</span>
117
+ <span className="text-primary font-mono text-[11px] bg-primary/10 px-2 py-0.5 rounded">{settings.temperature}</span>
118
+ </div>
119
+ <input
120
+ type="range"
121
+ min="0.1"
122
+ max="1.5"
123
+ step="0.1"
124
+ value={settings.temperature}
125
+ onChange={(e) => updateSettings({ temperature: parseFloat(e.target.value) })}
126
+ className="w-full accent-primary h-1 bg-[#090810] rounded-lg appearance-none cursor-pointer focus:outline-none"
127
+ />
128
+ <p className="text-[9px] text-[#9d99b3]">Controls randomness in responses.</p>
129
+ </div>
130
+
131
+ {/* Max Output Tokens Slider */}
132
+ <div className="space-y-2">
133
+ <div className="flex justify-between text-xs font-semibold">
134
+ <span className="text-white">Max Tokens</span>
135
+ <span className="text-primary font-mono text-[11px] bg-primary/10 px-2 py-0.5 rounded">{settings.maxTokens}</span>
136
+ </div>
137
+ <input
138
+ type="range"
139
+ min="64"
140
+ max="2048"
141
+ step="64"
142
+ value={settings.maxTokens}
143
+ onChange={(e) => updateSettings({ maxTokens: parseInt(e.target.value) })}
144
+ className="w-full accent-primary h-1 bg-[#090810] rounded-lg appearance-none cursor-pointer focus:outline-none"
145
+ />
146
+ <p className="text-[9px] text-[#9d99b3]">Maximum tokens in the response.</p>
147
+ </div>
148
+
149
+ {/* Top P Slider */}
150
+ <div className="space-y-2">
151
+ <div className="flex justify-between text-xs font-semibold">
152
+ <span className="text-white">Top P</span>
153
+ <span className="text-primary font-mono text-[11px] bg-primary/10 px-2 py-0.5 rounded">{settings.topP}</span>
154
+ </div>
155
+ <input
156
+ type="range"
157
+ min="0.1"
158
+ max="1.0"
159
+ step="0.05"
160
+ value={settings.topP}
161
+ onChange={(e) => updateSettings({ topP: parseFloat(e.target.value) })}
162
+ className="w-full accent-primary h-1 bg-[#090810] rounded-lg appearance-none cursor-pointer focus:outline-none"
163
+ />
164
+ <p className="text-[9px] text-[#9d99b3]">Nucleus sampling probability parameter.</p>
165
+ </div>
166
+
167
+ <div className="h-px bg-[#1d1b2e] my-2"></div>
168
+
169
+ {/* Stream response switch */}
170
+ <div className="flex items-center justify-between py-1.5">
171
+ <div className="space-y-0.5">
172
+ <label className="text-xs font-bold text-white cursor-pointer" htmlFor="stream-switch">
173
+ Stream Response
174
+ </label>
175
+ <p className="text-[10px] text-[#9d99b3]">Stream tokens as they are generated.</p>
176
+ </div>
177
+ <input
178
+ type="checkbox"
179
+ id="stream-switch"
180
+ checked={settings.streaming}
181
+ onChange={(e) => updateSettings({ streaming: e.target.checked })}
182
+ className="w-4.5 h-4.5 rounded text-primary focus:ring-primary accent-primary bg-background border-border cursor-pointer focus-ring"
183
+ />
184
+ </div>
185
+
186
+ {/* Keep history switch */}
187
+ <div className="flex items-center justify-between py-1.5">
188
+ <div className="space-y-0.5">
189
+ <label className="text-xs font-bold text-white cursor-pointer" htmlFor="history-switch">
190
+ Keep Conversation History
191
+ </label>
192
+ <p className="text-[10px] text-[#9d99b3]">Maintain conversation history log records in memory.</p>
193
+ </div>
194
+ <input
195
+ type="checkbox"
196
+ id="history-switch"
197
+ checked={true}
198
+ readOnly
199
+ className="w-4.5 h-4.5 rounded text-primary focus:ring-primary accent-primary bg-background border-border cursor-pointer focus-ring"
200
+ />
201
+ </div>
202
+
203
+ {/* Save Button */}
204
+ <div className="flex items-center justify-end pt-3">
205
+ <button
206
+ onClick={handleSave}
207
+ className="flex items-center justify-center gap-2 px-5 py-2.5 bg-[#6344d5] hover:bg-[#6344d5]/95 text-white font-bold text-xs rounded-xl shadow-md transition-all active:scale-[0.98] cursor-pointer glow-primary"
208
+ >
209
+ {saveSuccess ? (
210
+ <>
211
+ <CheckCircle2 size={13} />
212
+ <span>Settings Saved</span>
213
+ </>
214
+ ) : (
215
+ <>
216
+ <Save size={13} />
217
+ <span>Save Settings</span>
218
+ </>
219
+ )}
220
+ </button>
221
+ </div>
222
+
223
+ </div>
224
+ </div>
225
+
226
+ {/* Right Column Stats (2/5 width) */}
227
+ <div className="lg:col-span-2 space-y-6">
228
+
229
+ {/* Model Information metadata */}
230
+ <div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl space-y-4">
231
+ <h3 className="text-xs font-bold text-white uppercase tracking-wider flex items-center gap-2 select-none border-b border-[#1d1b2e] pb-3">
232
+ <Info size={13} className="text-primary" />
233
+ Model Information
234
+ </h3>
235
+ <div className="space-y-3.5 text-xs select-none">
236
+ <div className="flex justify-between">
237
+ <span className="text-[#9d99b3]">Model Name</span>
238
+ <span className="font-semibold text-white font-mono text-[11px] truncate max-w-[140px]">{modelInfo?.model_name.split('/').pop() || 'SmolLM2-360M-Instruct'}</span>
239
+ </div>
240
+ <div className="flex justify-between">
241
+ <span className="text-[#9d99b3]">Quantization</span>
242
+ <span className="font-semibold text-white font-mono text-[11px]">Q4_K_M</span>
243
+ </div>
244
+ <div className="flex justify-between">
245
+ <span className="text-[#9d99b3]">Model Size</span>
246
+ <span className="font-semibold text-white font-mono text-[11px]">1.6 GB</span>
247
+ </div>
248
+ <div className="flex justify-between">
249
+ <span className="text-[#9d99b3]">Context Length</span>
250
+ <span className="font-semibold text-white font-mono text-[11px]">4096</span>
251
+ </div>
252
+ <div className="flex justify-between">
253
+ <span className="text-[#9d99b3]">Vocabulary Size</span>
254
+ <span className="font-semibold text-white font-mono text-[11px]">151,936</span>
255
+ </div>
256
+ <div className="flex justify-between">
257
+ <span className="text-[#9d99b3]">Architecture</span>
258
+ <span className="font-semibold text-white font-mono text-[11px]">Transformers</span>
259
+ </div>
260
+ </div>
261
+ </div>
262
+
263
+ {/* System resource bars */}
264
+ <div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl space-y-5 select-none">
265
+ <h3 className="text-xs font-bold text-white uppercase tracking-wider flex items-center gap-2 border-b border-[#1d1b2e] pb-3">
266
+ <Activity size={13} className="text-primary" />
267
+ System Resources
268
+ </h3>
269
+
270
+ {/* RAM Usage progress bar */}
271
+ <div className="space-y-2">
272
+ <div className="flex justify-between text-xs font-semibold">
273
+ <span className="text-[#9d99b3]">RAM Usage</span>
274
+ <span className="text-white font-mono">{memoryPercent}%</span>
275
+ </div>
276
+ <div className="w-full bg-[#090810] h-1.5 rounded-full overflow-hidden">
277
+ <div className="bg-emerald-500 h-full" style={{ width: `${memoryPercent}%` }}></div>
278
+ </div>
279
+ <div className="text-[10px] text-[#58556f]">{memoryUsed.toFixed(2)} GB / {memoryTotal.toFixed(2)} GB</div>
280
+ </div>
281
+
282
+ {/* CPU Usage progress bar */}
283
+ <div className="space-y-2">
284
+ <div className="flex justify-between text-xs font-semibold">
285
+ <span className="text-[#9d99b3]">CPU Usage</span>
286
+ <span className="text-white font-mono">12%</span>
287
+ </div>
288
+ <div className="w-full bg-[#090810] h-1.5 rounded-full overflow-hidden">
289
+ <div className="bg-[#6344d5] h-full" style={{ width: '12%' }}></div>
290
+ </div>
291
+ </div>
292
+ </div>
293
+
294
+ {/* Danger zone log clear button */}
295
+ <div className="bg-[#131121] border border-[#1d1b2e] p-5 rounded-2xl space-y-3 select-none">
296
+ <h3 className="text-xs font-bold text-destructive uppercase tracking-wider flex items-center gap-1.5">
297
+ <AlertTriangle size={14} />
298
+ Reset Cache
299
+ </h3>
300
+
301
+ <button
302
+ onClick={handleClearHistory}
303
+ disabled={resetting}
304
+ className={`w-full flex items-center justify-center gap-2 px-3 py-2.5 border rounded-xl font-bold text-xs transition-all cursor-pointer ${
305
+ confirmClear
306
+ ? 'bg-destructive hover:bg-destructive/90 text-destructive-foreground border-transparent animate-pulse'
307
+ : 'bg-destructive/10 hover:bg-destructive/20 border-destructive/20 text-destructive'
308
+ }`}
309
+ >
310
+ {resetting ? (
311
+ <>
312
+ <RefreshCw size={13} className="animate-spin" />
313
+ <span>Reseting...</span>
314
+ </>
315
+ ) : (
316
+ <>
317
+ <span>{confirmClear ? 'Confirm Reset' : 'Reset Database Records'}</span>
318
+ </>
319
+ )}
320
+ </button>
321
+ {confirmClear && !resetting && (
322
+ <button
323
+ onClick={() => setConfirmClear(false)}
324
+ className="w-full text-center text-[10px] hover:underline text-muted-foreground block py-1 cursor-pointer"
325
+ >
326
+ Cancel
327
+ </button>
328
+ )}
329
+ </div>
330
+
331
+ </div>
332
+
333
+ </div>
334
+
335
+ </div>
336
+ );
337
+ };
frontend/src/components/Sidebar.tsx ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import {
4
+ MessageSquare, Code2, Settings2, Info, Plus,
5
+ Trash2, ChevronLeft, ChevronRight, LayoutDashboard, Cpu,
6
+ User, Database, Sparkles, HelpCircle, History, FileText,
7
+ Terminal, ShieldCheck, Settings
8
+ } from 'lucide-react';
9
+ import type { Conversation, ModelInfo } from '../types';
10
+
11
+ interface SidebarProps {
12
+ conversations: Conversation[];
13
+ activeConversationId: string | null;
14
+ setActiveConversationId: (id: string) => void;
15
+ createNewConversation: () => void;
16
+ deleteConversation: (id: string) => void;
17
+ activeTab: string;
18
+ setActiveTab: (tab: string) => void;
19
+ modelInfo: ModelInfo | null;
20
+ collapsed: boolean;
21
+ setCollapsed: (c: boolean) => void;
22
+ }
23
+
24
+ export const Sidebar: React.FC<SidebarProps> = ({
25
+ conversations,
26
+ activeConversationId,
27
+ setActiveConversationId,
28
+ createNewConversation,
29
+ deleteConversation,
30
+ activeTab,
31
+ setActiveTab,
32
+ modelInfo,
33
+ collapsed,
34
+ setCollapsed,
35
+ }) => {
36
+ const navItems = [
37
+ { id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
38
+ { id: 'chat', label: 'AI Chat', icon: MessageSquare },
39
+ { id: 'playground', label: 'Playground', icon: Code2 },
40
+ { id: 'history', label: 'History', icon: History },
41
+ { id: 'documents', label: 'Documents', icon: FileText },
42
+ { id: 'snippets', label: 'Snippets', icon: Terminal },
43
+ { id: 'models', label: 'Models', icon: Cpu },
44
+ { id: 'settings', label: 'Settings', icon: Settings2 },
45
+ ];
46
+
47
+ return (
48
+ <motion.aside
49
+ animate={{ width: collapsed ? 64 : 256 }}
50
+ transition={{ duration: 0.3, cubicBezier: [0.16, 1, 0.3, 1] }}
51
+ className="bg-[#100d20] border-r border-[#1d1b2e] flex flex-col h-screen relative select-none overflow-hidden shrink-0"
52
+ >
53
+ {/* Brand Header */}
54
+ <div className="p-4 border-b border-[#1d1b2e] flex items-center justify-between overflow-hidden shrink-0 h-16">
55
+ <div className="flex items-center gap-2.5 overflow-hidden">
56
+ <div className="p-1.5 bg-primary/10 rounded-lg text-primary glow-primary shrink-0">
57
+ <Cpu size={16} className="animate-pulse" />
58
+ </div>
59
+ <AnimatePresence>
60
+ {!collapsed && (
61
+ <motion.div
62
+ initial={{ opacity: 0, x: -8 }}
63
+ animate={{ opacity: 1, x: 0 }}
64
+ exit={{ opacity: 0, x: -8 }}
65
+ transition={{ duration: 0.2 }}
66
+ className="flex flex-col whitespace-nowrap"
67
+ >
68
+ <span className="font-bold text-sm tracking-tight text-foreground">
69
+ Antigravity
70
+ </span>
71
+ <span className="text-[10px] font-medium text-muted-foreground -mt-1">
72
+ Coder
73
+ </span>
74
+ </motion.div>
75
+ )}
76
+ </AnimatePresence>
77
+ </div>
78
+
79
+ {!collapsed && (
80
+ <button
81
+ onClick={() => setCollapsed(true)}
82
+ className="text-muted-foreground hover:text-foreground p-1 hover:bg-muted/50 rounded-lg transition-colors cursor-pointer shrink-0"
83
+ title="Collapse Sidebar"
84
+ >
85
+ <ChevronLeft size={15} />
86
+ </button>
87
+ )}
88
+ </div>
89
+
90
+ {/* Navigation Links */}
91
+ <nav className="p-4 space-y-1.5 shrink-0 flex-1 overflow-y-auto">
92
+ {navItems.map((item) => {
93
+ const Icon = item.icon;
94
+ const isActive = activeTab === item.id;
95
+ return (
96
+ <button
97
+ key={item.id}
98
+ onClick={() => setActiveTab(item.id)}
99
+ className={`w-full flex items-center gap-3 p-2.5 rounded-xl text-xs font-semibold transition-all group relative cursor-pointer ${
100
+ isActive
101
+ ? 'bg-[#2b1854] text-white border border-[#6344d5]/30'
102
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted/20'
103
+ }`}
104
+ >
105
+ <Icon size={16} className={isActive ? 'text-primary' : 'text-muted-foreground group-hover:text-foreground'} />
106
+
107
+ <AnimatePresence>
108
+ {!collapsed && (
109
+ <motion.span
110
+ initial={{ opacity: 0 }}
111
+ animate={{ opacity: 1 }}
112
+ exit={{ opacity: 0 }}
113
+ transition={{ duration: 0.15 }}
114
+ className="truncate whitespace-nowrap"
115
+ >
116
+ {item.label}
117
+ </motion.span>
118
+ )}
119
+ </AnimatePresence>
120
+
121
+ {collapsed && (
122
+ <div className="absolute left-18 bg-popover border border-border text-popover-foreground text-[10px] font-bold px-3 py-2 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 pointer-events-none shadow-md">
123
+ {item.label}
124
+ </div>
125
+ )}
126
+ </button>
127
+ );
128
+ })}
129
+ </nav>
130
+
131
+ {/* Expand trigger when collapsed */}
132
+ {collapsed && (
133
+ <div className="p-4 flex justify-center shrink-0 border-t border-[#1d1b2e] mt-2">
134
+ <button
135
+ onClick={() => setCollapsed(false)}
136
+ className="text-muted-foreground hover:text-foreground p-2 hover:bg-muted/50 rounded-xl transition-colors cursor-pointer"
137
+ title="Expand Sidebar"
138
+ >
139
+ <ChevronRight size={15} />
140
+ </button>
141
+ </div>
142
+ )}
143
+
144
+ {/* Mock User Profile Area */}
145
+ <div className="border-t border-[#1d1b2e] bg-[#0f0c1d] shrink-0">
146
+ <AnimatePresence>
147
+ {!collapsed && (
148
+ <motion.div
149
+ initial={{ opacity: 0, height: 0 }}
150
+ animate={{ opacity: 1, height: 'auto' }}
151
+ exit={{ opacity: 0, height: 0 }}
152
+ className="p-4"
153
+ >
154
+ <div className="flex items-center justify-between p-2 bg-[#131121] border border-[#1d1b2e] rounded-xl shadow-sm hover:border-primary/20 transition-all group">
155
+ <div className="flex items-center gap-2.5 min-w-0">
156
+ <div className="h-8 w-8 rounded-lg bg-gradient-to-tr from-primary to-[#2b1854] text-white flex items-center justify-center font-bold text-xs select-none shrink-0">
157
+ D
158
+ </div>
159
+ <div className="flex flex-col text-[11px] truncate min-w-0">
160
+ <span className="font-semibold text-foreground truncate">Developer</span>
161
+ <span className="text-[9px] text-[#9d99b3]">Pro Plan</span>
162
+ </div>
163
+ </div>
164
+ <button
165
+ onClick={() => setActiveTab('settings')}
166
+ className="text-muted-foreground hover:text-foreground cursor-pointer p-1 rounded-md hover:bg-muted/40 shrink-0"
167
+ title="Settings"
168
+ >
169
+ <Settings2 size={14} />
170
+ </button>
171
+ </div>
172
+ </motion.div>
173
+ )}
174
+ </AnimatePresence>
175
+ </div>
176
+ </motion.aside>
177
+ );
178
+ };