Upload 38 files
Browse files- Dockerfile +41 -0
- app/agents/__init__.py +6 -0
- app/agents/backend_gen.py +67 -0
- app/agents/base.py +91 -0
- app/agents/frontend_gen.py +60 -0
- app/agents/orchestrator.py +100 -0
- app/agents/research.py +31 -0
- app/config.py +29 -0
- app/main.py +249 -0
- app/models/__init__.py +1 -0
- app/models/schemas.py +43 -0
- app/pipeline/__init__.py +1 -0
- app/pipeline/engine.py +555 -0
- app/preview/__init__.py +1 -0
- app/preview/builder.py +45 -0
- fronend/index.html +14 -0
- fronend/package.json +25 -0
- fronend/postcss.config.js +6 -0
- fronend/src/App.jsx +221 -0
- fronend/src/components/AgentFeed.jsx +92 -0
- fronend/src/components/ChatInterface.jsx +195 -0
- fronend/src/components/CodeView.jsx +128 -0
- fronend/src/components/ControlPanel.jsx +84 -0
- fronend/src/components/FileTree.jsx +141 -0
- fronend/src/components/Header.jsx +80 -0
- fronend/src/components/LivePreview.jsx +159 -0
- fronend/src/components/StatusBar.jsx +104 -0
- fronend/src/components/SystemTabs.jsx +37 -0
- fronend/src/components/ThemeProvider.jsx +31 -0
- fronend/src/hooks/useSSE.js +61 -0
- fronend/src/hooks/useTheme.js +2 -0
- fronend/src/index.css +126 -0
- fronend/src/main.jsx +13 -0
- fronend/src/utils/api.js +80 -0
- fronend/tailwind.config.js +48 -0
- fronend/vite.config.js +15 -0
- requirements.txt +8 -0
- start.sh +2 -0
Dockerfile
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# System dependencies
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
curl \
|
| 6 |
+
gnupg \
|
| 7 |
+
build-essential \
|
| 8 |
+
zip \
|
| 9 |
+
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
| 10 |
+
&& apt-get install -y nodejs \
|
| 11 |
+
&& apt-get clean \
|
| 12 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
WORKDIR /app
|
| 15 |
+
|
| 16 |
+
# Python dependencies
|
| 17 |
+
COPY requirements.txt .
|
| 18 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 19 |
+
|
| 20 |
+
# Frontend build
|
| 21 |
+
COPY frontend/ /app/frontend/
|
| 22 |
+
WORKDIR /app/frontend
|
| 23 |
+
RUN npm ci && npm run build
|
| 24 |
+
RUN mkdir -p /app/static && cp -r dist/* /app/static/
|
| 25 |
+
|
| 26 |
+
# Backend
|
| 27 |
+
WORKDIR /app
|
| 28 |
+
COPY app/ /app/app/
|
| 29 |
+
COPY start.sh /app/start.sh
|
| 30 |
+
RUN chmod +x /app/start.sh
|
| 31 |
+
|
| 32 |
+
# Create temp directories
|
| 33 |
+
RUN mkdir -p /tmp/nexus_projects /tmp/nexus_previews
|
| 34 |
+
|
| 35 |
+
EXPOSE 7860
|
| 36 |
+
|
| 37 |
+
ENV PYTHONUNBUFFERED=1
|
| 38 |
+
ENV HOST=0.0.0.0
|
| 39 |
+
ENV PORT=7860
|
| 40 |
+
|
| 41 |
+
CMD ["./start.sh"]
|
app/agents/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.agents.research import ResearchAgent
|
| 2 |
+
from app.agents.orchestrator import OrchestratorAgent
|
| 3 |
+
from app.agents.frontend_gen import FrontendAgent
|
| 4 |
+
from app.agents.backend_gen import BackendAgent
|
| 5 |
+
|
| 6 |
+
__all__ = ["ResearchAgent", "OrchestratorAgent", "FrontendAgent", "BackendAgent"]
|
app/agents/backend_gen.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.agents.base import BaseAgent
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class BackendAgent(BaseAgent):
|
| 5 |
+
role = "backend"
|
| 6 |
+
model_key = "backend"
|
| 7 |
+
temperature = 0.4
|
| 8 |
+
max_tokens = 24000
|
| 9 |
+
system_prompt = """You are the BACKEND, SECURITY, DATABASE & DEVOPS AGENT for Nexus Builder.
|
| 10 |
+
|
| 11 |
+
YOUR ROLE: Generate complete backend code, database schemas, security configurations, and deployment files.
|
| 12 |
+
|
| 13 |
+
TECH STACK:
|
| 14 |
+
- Python FastAPI for backend API
|
| 15 |
+
- Supabase for database (PostgreSQL) + Auth + Realtime + Edge Functions
|
| 16 |
+
- PayPal REST API v2 for payments
|
| 17 |
+
- Docker for containerization
|
| 18 |
+
- Firebase Hosting for frontend deployment (config only)
|
| 19 |
+
|
| 20 |
+
YOUR OUTPUTS INCLUDE:
|
| 21 |
+
|
| 22 |
+
1. **Supabase SQL Schema** — Complete CREATE TABLE statements with:
|
| 23 |
+
- All columns, types, defaults, constraints
|
| 24 |
+
- Foreign key relationships
|
| 25 |
+
- Indexes for performance
|
| 26 |
+
- Row Level Security (RLS) policies
|
| 27 |
+
- Triggers (e.g., auto-create profile on signup)
|
| 28 |
+
- Realtime publication setup
|
| 29 |
+
|
| 30 |
+
2. **FastAPI Backend** — Complete Python files:
|
| 31 |
+
- main.py with all routes
|
| 32 |
+
- Auth middleware (JWT verification via Supabase)
|
| 33 |
+
- PayPal integration (create order, capture, webhooks)
|
| 34 |
+
- Rate limiting middleware
|
| 35 |
+
- CORS configuration
|
| 36 |
+
- Input validation with Pydantic
|
| 37 |
+
- Error handling
|
| 38 |
+
|
| 39 |
+
3. **Supabase Edge Functions** — TypeScript/Deno functions for:
|
| 40 |
+
- PayPal webhook verification
|
| 41 |
+
- Welcome email on signup
|
| 42 |
+
- Scheduled cleanup tasks
|
| 43 |
+
|
| 44 |
+
4. **Security Layer**:
|
| 45 |
+
- SQL injection prevention (parameterized queries)
|
| 46 |
+
- XSS prevention headers
|
| 47 |
+
- CORS whitelist configuration
|
| 48 |
+
- Rate limiting per endpoint
|
| 49 |
+
- Input sanitization
|
| 50 |
+
- JWT token refresh logic
|
| 51 |
+
|
| 52 |
+
5. **DevOps Files**:
|
| 53 |
+
- Dockerfile for backend
|
| 54 |
+
- docker-compose.yml for local development
|
| 55 |
+
- .env.example with all required variables documented
|
| 56 |
+
- firebase.json for hosting config
|
| 57 |
+
- .firebaserc for project linking
|
| 58 |
+
- Step-by-step deployment guide as DEPLOY.md
|
| 59 |
+
|
| 60 |
+
RULES:
|
| 61 |
+
1. Generate COMPLETE files — no placeholders or truncation
|
| 62 |
+
2. All SQL must be valid PostgreSQL
|
| 63 |
+
3. All Python must be valid, typed, and async where appropriate
|
| 64 |
+
4. Security is paramount — never expose secrets, always validate input
|
| 65 |
+
5. Each file must be marked with: # FILE: path/to/file.py or -- FILE: schema.sql
|
| 66 |
+
|
| 67 |
+
OUTPUT FORMAT:
|
app/agents/base.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import httpx
|
| 3 |
+
from typing import AsyncGenerator
|
| 4 |
+
from app.config import settings
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class BaseAgent:
|
| 8 |
+
"""Base class for all AI agents using OpenRouter API."""
|
| 9 |
+
|
| 10 |
+
role: str = ""
|
| 11 |
+
model_key: str = ""
|
| 12 |
+
system_prompt: str = ""
|
| 13 |
+
temperature: float = 0.7
|
| 14 |
+
max_tokens: int = 16000
|
| 15 |
+
|
| 16 |
+
@property
|
| 17 |
+
def model_id(self) -> str:
|
| 18 |
+
return settings.model_ids[self.model_key]
|
| 19 |
+
|
| 20 |
+
@property
|
| 21 |
+
def model_name(self) -> str:
|
| 22 |
+
return settings.model_names[self.model_key]
|
| 23 |
+
|
| 24 |
+
async def call(self, messages: list[dict], stream: bool = True) -> AsyncGenerator[str, None]:
|
| 25 |
+
"""Call the model via OpenRouter, yielding streamed tokens."""
|
| 26 |
+
full_messages = [
|
| 27 |
+
{"role": "system", "content": self.system_prompt},
|
| 28 |
+
*messages,
|
| 29 |
+
]
|
| 30 |
+
payload = {
|
| 31 |
+
"model": self.model_id,
|
| 32 |
+
"messages": full_messages,
|
| 33 |
+
"temperature": self.temperature,
|
| 34 |
+
"max_tokens": self.max_tokens,
|
| 35 |
+
"stream": stream,
|
| 36 |
+
}
|
| 37 |
+
headers = {
|
| 38 |
+
"Authorization": f"Bearer {settings.OPENROUTER_API_KEY}",
|
| 39 |
+
"Content-Type": "application/json",
|
| 40 |
+
"HTTP-Referer": "https://huggingface.co/spaces/nexus-builder",
|
| 41 |
+
"X-Title": "Nexus Builder",
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
if stream:
|
| 45 |
+
async with httpx.AsyncClient(timeout=settings.STREAM_TIMEOUT) as client:
|
| 46 |
+
async with client.stream(
|
| 47 |
+
"POST",
|
| 48 |
+
settings.OPENROUTER_BASE_URL,
|
| 49 |
+
json=payload,
|
| 50 |
+
headers=headers,
|
| 51 |
+
) as response:
|
| 52 |
+
if response.status_code != 200:
|
| 53 |
+
body = await response.aread()
|
| 54 |
+
raise Exception(
|
| 55 |
+
f"OpenRouter error {response.status_code}: {body.decode()}"
|
| 56 |
+
)
|
| 57 |
+
async for line in response.aiter_lines():
|
| 58 |
+
if not line.startswith("data: "):
|
| 59 |
+
continue
|
| 60 |
+
data_str = line[6:]
|
| 61 |
+
if data_str.strip() == "[DONE]":
|
| 62 |
+
break
|
| 63 |
+
try:
|
| 64 |
+
chunk = json.loads(data_str)
|
| 65 |
+
delta = chunk["choices"][0].get("delta", {})
|
| 66 |
+
content = delta.get("content", "")
|
| 67 |
+
if content:
|
| 68 |
+
yield content
|
| 69 |
+
except (json.JSONDecodeError, KeyError, IndexError):
|
| 70 |
+
continue
|
| 71 |
+
else:
|
| 72 |
+
async with httpx.AsyncClient(timeout=settings.STREAM_TIMEOUT) as client:
|
| 73 |
+
response = await client.post(
|
| 74 |
+
settings.OPENROUTER_BASE_URL,
|
| 75 |
+
json=payload,
|
| 76 |
+
headers=headers,
|
| 77 |
+
)
|
| 78 |
+
if response.status_code != 200:
|
| 79 |
+
raise Exception(
|
| 80 |
+
f"OpenRouter error {response.status_code}: {response.text}"
|
| 81 |
+
)
|
| 82 |
+
data = response.json()
|
| 83 |
+
content = data["choices"][0]["message"]["content"]
|
| 84 |
+
yield content
|
| 85 |
+
|
| 86 |
+
async def call_full(self, messages: list[dict]) -> str:
|
| 87 |
+
"""Non-streaming call that returns the full response."""
|
| 88 |
+
result = []
|
| 89 |
+
async for token in self.call(messages, stream=False):
|
| 90 |
+
result.append(token)
|
| 91 |
+
return "".join(result)
|
app/agents/frontend_gen.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
---
|
| 3 |
+
|
| 4 |
+
## FILE: `app/agents/frontend_gen.py`
|
| 5 |
+
|
| 6 |
+
```python
|
| 7 |
+
from app.agents.base import BaseAgent
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class FrontendAgent(BaseAgent):
|
| 11 |
+
role = "frontend"
|
| 12 |
+
model_key = "frontend"
|
| 13 |
+
temperature = 0.6
|
| 14 |
+
max_tokens = 32000
|
| 15 |
+
system_prompt = """You are the FRONTEND CODE GENERATION AGENT for Nexus Builder.
|
| 16 |
+
|
| 17 |
+
YOUR ROLE: Generate complete, production-ready frontend code for web applications based on a blueprint.
|
| 18 |
+
|
| 19 |
+
TECH STACK (always use):
|
| 20 |
+
- React 18+ with functional components and hooks
|
| 21 |
+
- Tailwind CSS for styling
|
| 22 |
+
- React Router v6 for routing
|
| 23 |
+
- Supabase JS client (@supabase/supabase-js) for auth & database
|
| 24 |
+
- @paypal/react-paypal-js for payment integration
|
| 25 |
+
- Recharts for analytics charts
|
| 26 |
+
- Lucide React for icons
|
| 27 |
+
|
| 28 |
+
DESIGN SYSTEM (always follow):
|
| 29 |
+
Dark Mode Colors:
|
| 30 |
+
- Background: #0A0A0F
|
| 31 |
+
- Surface/Cards: #111118
|
| 32 |
+
- Borders: #1E1E2E
|
| 33 |
+
- Primary accent: #6C63FF (electric purple)
|
| 34 |
+
- Secondary accent: #00D9FF (cyan)
|
| 35 |
+
- Text primary: #F0F0FF
|
| 36 |
+
- Text secondary: #8888AA
|
| 37 |
+
- Success: #22D3A8
|
| 38 |
+
- Error: #FF4D6D
|
| 39 |
+
- Warning: #FFB547
|
| 40 |
+
|
| 41 |
+
Light Mode Colors:
|
| 42 |
+
- Background: #F8F8FC
|
| 43 |
+
- Surface: #FFFFFF
|
| 44 |
+
- Borders: #E0E0EF
|
| 45 |
+
- Primary accent: #5B53E8
|
| 46 |
+
- Text primary: #0A0A1A
|
| 47 |
+
- Text secondary: #666688
|
| 48 |
+
|
| 49 |
+
RULES:
|
| 50 |
+
1. Generate COMPLETE files — no placeholders, no "// ... rest of code", no truncation
|
| 51 |
+
2. Every component must be fully functional with proper state management
|
| 52 |
+
3. Use CSS variables for theming (dark/light mode toggle)
|
| 53 |
+
4. Mobile-first responsive design
|
| 54 |
+
5. Smooth animations and transitions (CSS transitions, not heavy libraries)
|
| 55 |
+
6. Accessible markup (semantic HTML, ARIA labels, keyboard navigation)
|
| 56 |
+
7. Error boundaries and loading states for all async operations
|
| 57 |
+
8. Each file must be clearly marked with its path using: // FILE: path/to/file.jsx
|
| 58 |
+
|
| 59 |
+
OUTPUT FORMAT:
|
| 60 |
+
For each file, output:
|
app/agents/orchestrator.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.agents.base import BaseAgent
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class OrchestratorAgent(BaseAgent):
|
| 5 |
+
role = "orchestrator"
|
| 6 |
+
model_key = "orchestrator"
|
| 7 |
+
temperature = 0.5
|
| 8 |
+
max_tokens = 24000
|
| 9 |
+
system_prompt = """You are the MASTER ORCHESTRATOR for Nexus Builder. You are the brain of the system.
|
| 10 |
+
|
| 11 |
+
YOUR ROLE: Take the user's app description + research data and produce a comprehensive app blueprint that frontend and backend agents will use to generate code.
|
| 12 |
+
|
| 13 |
+
You must produce a MASTER BLUEPRINT in JSON format containing:
|
| 14 |
+
|
| 15 |
+
## Blueprint Structure:
|
| 16 |
+
|
| 17 |
+
```json
|
| 18 |
+
{
|
| 19 |
+
"project_name": "string",
|
| 20 |
+
"description": "string",
|
| 21 |
+
"systems": {
|
| 22 |
+
"client_portal": {
|
| 23 |
+
"pages": [
|
| 24 |
+
{
|
| 25 |
+
"path": "/dashboard",
|
| 26 |
+
"title": "Dashboard",
|
| 27 |
+
"components": ["Sidebar", "StatsCards", "RecentActivity", "QuickActions"],
|
| 28 |
+
"description": "Main user dashboard showing project overview",
|
| 29 |
+
"auth_required": true,
|
| 30 |
+
"role_required": "user"
|
| 31 |
+
}
|
| 32 |
+
],
|
| 33 |
+
"features": ["auth", "project_management", "billing", "settings"]
|
| 34 |
+
},
|
| 35 |
+
"public_landing": {
|
| 36 |
+
"pages": [...],
|
| 37 |
+
"features": ["hero", "pricing", "features", "signup", "seo"]
|
| 38 |
+
},
|
| 39 |
+
"marketing_cms": {
|
| 40 |
+
"pages": [...],
|
| 41 |
+
"features": ["blog_editor", "email_capture", "campaigns"]
|
| 42 |
+
},
|
| 43 |
+
"analytics_dashboard": {
|
| 44 |
+
"pages": [...],
|
| 45 |
+
"features": ["realtime_metrics", "charts", "user_tracking", "revenue"]
|
| 46 |
+
},
|
| 47 |
+
"admin_panel": {
|
| 48 |
+
"pages": [...],
|
| 49 |
+
"features": ["user_management", "moderation", "system_health", "logs"]
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
"database_schema": {
|
| 53 |
+
"tables": [
|
| 54 |
+
{
|
| 55 |
+
"name": "profiles",
|
| 56 |
+
"columns": [
|
| 57 |
+
{"name": "id", "type": "uuid", "primary": true, "references": "auth.users.id"},
|
| 58 |
+
{"name": "full_name", "type": "text"},
|
| 59 |
+
{"name": "role", "type": "text", "default": "user"},
|
| 60 |
+
{"name": "created_at", "type": "timestamptz", "default": "now()"}
|
| 61 |
+
],
|
| 62 |
+
"rls_policies": [
|
| 63 |
+
{"name": "Users read own profile", "operation": "SELECT", "check": "auth.uid() = id"}
|
| 64 |
+
]
|
| 65 |
+
}
|
| 66 |
+
]
|
| 67 |
+
},
|
| 68 |
+
"api_endpoints": [
|
| 69 |
+
{"method": "POST", "path": "/api/auth/signup", "description": "Register new user"},
|
| 70 |
+
{"method": "POST", "path": "/api/payments/create-order", "description": "Create PayPal order"}
|
| 71 |
+
],
|
| 72 |
+
"auth_config": {
|
| 73 |
+
"providers": ["email", "google", "github"],
|
| 74 |
+
"jwt_expiry": 3600,
|
| 75 |
+
"refresh_enabled": true
|
| 76 |
+
},
|
| 77 |
+
"payment_config": {
|
| 78 |
+
"provider": "paypal",
|
| 79 |
+
"plans": [
|
| 80 |
+
{"name": "Free", "price": 0, "features": ["1 project", "Basic analytics"]},
|
| 81 |
+
{"name": "Pro", "price": 29, "features": ["Unlimited projects", "Advanced analytics", "Priority support"]},
|
| 82 |
+
{"name": "Enterprise", "price": 99, "features": ["Everything in Pro", "Custom integrations", "SLA"]}
|
| 83 |
+
]
|
| 84 |
+
},
|
| 85 |
+
"design_tokens": {
|
| 86 |
+
"colors": {
|
| 87 |
+
"primary": "#6C63FF",
|
| 88 |
+
"secondary": "#00D9FF",
|
| 89 |
+
"background": "#0A0A0F",
|
| 90 |
+
"surface": "#111118",
|
| 91 |
+
"text": "#F0F0FF"
|
| 92 |
+
},
|
| 93 |
+
"fonts": {
|
| 94 |
+
"heading": "Inter",
|
| 95 |
+
"body": "Inter",
|
| 96 |
+
"mono": "JetBrains Mono"
|
| 97 |
+
},
|
| 98 |
+
"border_radius": "12px"
|
| 99 |
+
}
|
| 100 |
+
}
|
app/agents/research.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.agents.base import BaseAgent
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class ResearchAgent(BaseAgent):
|
| 5 |
+
role = "research"
|
| 6 |
+
model_key = "research"
|
| 7 |
+
temperature = 0.4
|
| 8 |
+
max_tokens = 8000
|
| 9 |
+
system_prompt = """You are the RESEARCH AGENT for Nexus Builder, an AI-powered web application generator.
|
| 10 |
+
|
| 11 |
+
YOUR ROLE: Gather and synthesize technical knowledge to inform the app-building pipeline.
|
| 12 |
+
|
| 13 |
+
When given a user's app idea, you must produce a structured JSON research report covering:
|
| 14 |
+
|
| 15 |
+
1. **stack_recommendation**: Recommend the optimal frontend framework (React/Next.js), CSS approach (Tailwind), backend (FastAPI/Express), database (Supabase), and deployment target (Firebase Hosting).
|
| 16 |
+
|
| 17 |
+
2. **schema_hints**: Suggest database tables, columns, and relationships relevant to this app type. Include Supabase-specific features like RLS policies, realtime subscriptions, and Edge Functions.
|
| 18 |
+
|
| 19 |
+
3. **api_docs_summary**: Summarize the key API endpoints needed. Include PayPal integration endpoints (Orders API v2, Subscriptions API), Supabase Auth endpoints, and any domain-specific APIs.
|
| 20 |
+
|
| 21 |
+
4. **security_notes**: List security best practices for this app type — authentication flows, input validation, CORS configuration, rate limiting, SQL injection prevention, XSS prevention.
|
| 22 |
+
|
| 23 |
+
5. **hosting_notes**: Firebase Hosting configuration tips, environment variable management, Docker deployment considerations.
|
| 24 |
+
|
| 25 |
+
6. **ui_patterns**: Recommend UI patterns, component structures, and UX flows that work well for this type of application.
|
| 26 |
+
|
| 27 |
+
7. **competitor_analysis**: Brief analysis of similar apps and what features users expect.
|
| 28 |
+
|
| 29 |
+
ALWAYS respond with valid JSON wrapped in ```json code blocks. Be thorough but concise.
|
| 30 |
+
Keep recommendations modern (2024-2025 best practices).
|
| 31 |
+
Prioritize: Supabase for DB/Auth, PayPal for payments, React+Tailwind for frontend, FastAPI for backend."""
|
app/config.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
|
| 4 |
+
load_dotenv()
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Settings:
|
| 8 |
+
OPENROUTER_API_KEY: str = os.getenv("OPENROUTER_API_KEY", "")
|
| 9 |
+
OPENROUTER_BASE_URL: str = "https://openrouter.ai/api/v1/chat/completions"
|
| 10 |
+
|
| 11 |
+
model_ids = {
|
| 12 |
+
"research": "z-ai/glm-4.5-air:free",
|
| 13 |
+
"orchestrator": "arcee-ai/trinity-large-preview:free",
|
| 14 |
+
"frontend": "qwen/qwen3-coder:free",
|
| 15 |
+
"backend": "minimax/minimax-m2.5:free",
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
model_names = {
|
| 19 |
+
"research": "GLM 4.5 Air",
|
| 20 |
+
"orchestrator": "Trinity Large Preview",
|
| 21 |
+
"frontend": "Qwen3 Coder 480B",
|
| 22 |
+
"backend": "MiniMax M2.5",
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
MAX_RETRIES: int = 3
|
| 26 |
+
STREAM_TIMEOUT: int = 120
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
settings = Settings()
|
app/main.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import uuid
|
| 4 |
+
import asyncio
|
| 5 |
+
import shutil
|
| 6 |
+
import zipfile
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from contextlib import asynccontextmanager
|
| 9 |
+
|
| 10 |
+
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
|
| 11 |
+
from fastapi.staticfiles import StaticFiles
|
| 12 |
+
from fastapi.responses import (
|
| 13 |
+
FileResponse, JSONResponse, StreamingResponse, HTMLResponse
|
| 14 |
+
)
|
| 15 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 16 |
+
from sse_starlette.sse import EventSourceResponse
|
| 17 |
+
|
| 18 |
+
from app.config import settings
|
| 19 |
+
from app.models.schemas import (
|
| 20 |
+
GenerateRequest, FixRequest, ProjectState, AgentMessage
|
| 21 |
+
)
|
| 22 |
+
from app.pipeline.engine import PipelineEngine
|
| 23 |
+
|
| 24 |
+
# ── Global State ──────────────────────────────────────────────
|
| 25 |
+
sessions: dict[str, ProjectState] = {}
|
| 26 |
+
pipeline_engine = PipelineEngine()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@asynccontextmanager
|
| 30 |
+
async def lifespan(app: FastAPI):
|
| 31 |
+
Path("/tmp/nexus_projects").mkdir(parents=True, exist_ok=True)
|
| 32 |
+
Path("/tmp/nexus_previews").mkdir(parents=True, exist_ok=True)
|
| 33 |
+
yield
|
| 34 |
+
# cleanup on shutdown if desired
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
app = FastAPI(
|
| 38 |
+
title="Nexus Builder API",
|
| 39 |
+
version="1.0.0",
|
| 40 |
+
lifespan=lifespan,
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
app.add_middleware(
|
| 44 |
+
CORSMiddleware,
|
| 45 |
+
allow_origins=["*"],
|
| 46 |
+
allow_credentials=True,
|
| 47 |
+
allow_methods=["*"],
|
| 48 |
+
allow_headers=["*"],
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# ── API Routes ────────────────────────────────────────────────
|
| 53 |
+
|
| 54 |
+
@app.post("/api/generate")
|
| 55 |
+
async def generate_project(req: GenerateRequest):
|
| 56 |
+
"""Start a new project generation pipeline."""
|
| 57 |
+
session_id = str(uuid.uuid4())[:12]
|
| 58 |
+
project_dir = Path(f"/tmp/nexus_projects/{session_id}")
|
| 59 |
+
project_dir.mkdir(parents=True, exist_ok=True)
|
| 60 |
+
(project_dir / "frontend").mkdir(exist_ok=True)
|
| 61 |
+
(project_dir / "backend").mkdir(exist_ok=True)
|
| 62 |
+
|
| 63 |
+
state = ProjectState(
|
| 64 |
+
session_id=session_id,
|
| 65 |
+
user_prompt=req.prompt,
|
| 66 |
+
app_type=req.app_type or "saas",
|
| 67 |
+
status="queued",
|
| 68 |
+
project_dir=str(project_dir),
|
| 69 |
+
systems=req.systems or [
|
| 70 |
+
"client_portal", "public_landing",
|
| 71 |
+
"marketing_cms", "analytics_dashboard", "admin_panel"
|
| 72 |
+
],
|
| 73 |
+
)
|
| 74 |
+
sessions[session_id] = state
|
| 75 |
+
# Pipeline runs in background; client listens via SSE
|
| 76 |
+
asyncio.create_task(pipeline_engine.run(state, sessions))
|
| 77 |
+
return {"session_id": session_id, "status": "started"}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
@app.get("/api/stream/{session_id}")
|
| 81 |
+
async def stream_events(request: Request, session_id: str):
|
| 82 |
+
"""SSE endpoint for real-time agent updates."""
|
| 83 |
+
if session_id not in sessions:
|
| 84 |
+
raise HTTPException(404, "Session not found")
|
| 85 |
+
|
| 86 |
+
async def event_generator():
|
| 87 |
+
state = sessions[session_id]
|
| 88 |
+
last_idx = 0
|
| 89 |
+
while True:
|
| 90 |
+
if await request.is_disconnected():
|
| 91 |
+
break
|
| 92 |
+
messages = state.messages[last_idx:]
|
| 93 |
+
for msg in messages:
|
| 94 |
+
yield {
|
| 95 |
+
"event": msg.event_type,
|
| 96 |
+
"data": json.dumps(msg.model_dump(), default=str),
|
| 97 |
+
}
|
| 98 |
+
last_idx = len(state.messages)
|
| 99 |
+
if state.status in ("completed", "error"):
|
| 100 |
+
yield {
|
| 101 |
+
"event": "done",
|
| 102 |
+
"data": json.dumps({
|
| 103 |
+
"status": state.status,
|
| 104 |
+
"session_id": session_id,
|
| 105 |
+
}),
|
| 106 |
+
}
|
| 107 |
+
break
|
| 108 |
+
await asyncio.sleep(0.3)
|
| 109 |
+
|
| 110 |
+
return EventSourceResponse(event_generator())
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
@app.get("/api/status/{session_id}")
|
| 114 |
+
async def get_status(session_id: str):
|
| 115 |
+
if session_id not in sessions:
|
| 116 |
+
raise HTTPException(404, "Session not found")
|
| 117 |
+
state = sessions[session_id]
|
| 118 |
+
return {
|
| 119 |
+
"session_id": session_id,
|
| 120 |
+
"status": state.status,
|
| 121 |
+
"current_agent": state.current_agent,
|
| 122 |
+
"file_tree": state.file_tree,
|
| 123 |
+
"errors": state.errors,
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
@app.get("/api/files/{session_id}")
|
| 128 |
+
async def get_files(session_id: str):
|
| 129 |
+
"""Return the generated file tree with contents."""
|
| 130 |
+
if session_id not in sessions:
|
| 131 |
+
raise HTTPException(404, "Session not found")
|
| 132 |
+
state = sessions[session_id]
|
| 133 |
+
return {"files": state.generated_files}
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@app.get("/api/file/{session_id}/{path:path}")
|
| 137 |
+
async def get_file_content(session_id: str, path: str):
|
| 138 |
+
if session_id not in sessions:
|
| 139 |
+
raise HTTPException(404, "Session not found")
|
| 140 |
+
state = sessions[session_id]
|
| 141 |
+
content = state.generated_files.get(path)
|
| 142 |
+
if content is None:
|
| 143 |
+
raise HTTPException(404, "File not found")
|
| 144 |
+
return {"path": path, "content": content}
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
@app.post("/api/fix/{session_id}")
|
| 148 |
+
async def fix_bug(session_id: str, req: FixRequest):
|
| 149 |
+
"""Send a bug report through the pipeline for targeted fixing."""
|
| 150 |
+
if session_id not in sessions:
|
| 151 |
+
raise HTTPException(404, "Session not found")
|
| 152 |
+
state = sessions[session_id]
|
| 153 |
+
state.status = "fixing"
|
| 154 |
+
asyncio.create_task(pipeline_engine.fix(state, req.error_message, req.file_path))
|
| 155 |
+
return {"status": "fix_started"}
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
@app.get("/api/export/{session_id}")
|
| 159 |
+
async def export_project(session_id: str):
|
| 160 |
+
"""Export the generated project as a ZIP file."""
|
| 161 |
+
if session_id not in sessions:
|
| 162 |
+
raise HTTPException(404, "Session not found")
|
| 163 |
+
state = sessions[session_id]
|
| 164 |
+
zip_path = Path(f"/tmp/nexus_projects/{session_id}.zip")
|
| 165 |
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
| 166 |
+
for file_path, content in state.generated_files.items():
|
| 167 |
+
zf.writestr(file_path, content)
|
| 168 |
+
return FileResponse(
|
| 169 |
+
zip_path,
|
| 170 |
+
filename=f"nexus-project-{session_id}.zip",
|
| 171 |
+
media_type="application/zip",
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
@app.get("/api/preview/{session_id}")
|
| 176 |
+
async def preview_landing(session_id: str):
|
| 177 |
+
"""Serve a combined preview HTML of the generated app."""
|
| 178 |
+
if session_id not in sessions:
|
| 179 |
+
raise HTTPException(404, "Session not found")
|
| 180 |
+
state = sessions[session_id]
|
| 181 |
+
# Look for index.html in generated files
|
| 182 |
+
for key in [
|
| 183 |
+
"frontend/index.html",
|
| 184 |
+
"public_landing/index.html",
|
| 185 |
+
"client_portal/index.html",
|
| 186 |
+
"index.html",
|
| 187 |
+
]:
|
| 188 |
+
if key in state.generated_files:
|
| 189 |
+
return HTMLResponse(state.generated_files[key])
|
| 190 |
+
# Build a combined preview
|
| 191 |
+
html = _build_combined_preview(state)
|
| 192 |
+
return HTMLResponse(html)
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
@app.get("/api/preview/{session_id}/{system}")
|
| 196 |
+
async def preview_system(session_id: str, system: str):
|
| 197 |
+
if session_id not in sessions:
|
| 198 |
+
raise HTTPException(404, "Session not found")
|
| 199 |
+
state = sessions[session_id]
|
| 200 |
+
key = f"{system}/index.html"
|
| 201 |
+
if key in state.generated_files:
|
| 202 |
+
return HTMLResponse(state.generated_files[key])
|
| 203 |
+
html = _build_system_preview(state, system)
|
| 204 |
+
return HTMLResponse(html)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def _build_combined_preview(state: ProjectState) -> str:
|
| 208 |
+
pages = []
|
| 209 |
+
for path, content in state.generated_files.items():
|
| 210 |
+
if path.endswith(".html"):
|
| 211 |
+
pages.append(f"<!-- {path} -->\n{content}")
|
| 212 |
+
if not pages:
|
| 213 |
+
return "<html><body style='background:#0A0A0F;color:#F0F0FF;font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh'><h1>⏳ Preview will appear here once generation completes</h1></body></html>"
|
| 214 |
+
return pages[0]
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def _build_system_preview(state: ProjectState, system: str) -> str:
|
| 218 |
+
system_files = {
|
| 219 |
+
k: v for k, v in state.generated_files.items()
|
| 220 |
+
if k.startswith(f"{system}/")
|
| 221 |
+
}
|
| 222 |
+
for path, content in system_files.items():
|
| 223 |
+
if path.endswith(".html"):
|
| 224 |
+
return content
|
| 225 |
+
return f"<html><body style='background:#0A0A0F;color:#F0F0FF;font-family:sans-serif;padding:40px'><h1>System: {system}</h1><p>No preview available yet.</p></body></html>"
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
@app.get("/api/health")
|
| 229 |
+
async def health():
|
| 230 |
+
return {"status": "ok", "models": settings.model_ids}
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
# ── Serve Frontend ────────────────────────────────────────────
|
| 234 |
+
static_dir = Path("/app/static")
|
| 235 |
+
if static_dir.exists():
|
| 236 |
+
app.mount("/assets", StaticFiles(directory=static_dir / "assets"), name="assets")
|
| 237 |
+
|
| 238 |
+
@app.get("/{path:path}")
|
| 239 |
+
async def serve_frontend(path: str):
|
| 240 |
+
file_path = static_dir / path
|
| 241 |
+
if file_path.is_file():
|
| 242 |
+
return FileResponse(file_path)
|
| 243 |
+
return FileResponse(static_dir / "index.html")
|
| 244 |
+
else:
|
| 245 |
+
@app.get("/")
|
| 246 |
+
async def root():
|
| 247 |
+
return HTMLResponse(
|
| 248 |
+
"<h1>Nexus Builder</h1><p>Frontend not built. Run npm build in /frontend</p>"
|
| 249 |
+
)
|
app/models/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
undefined
|
app/models/schemas.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
from typing import Optional
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class GenerateRequest(BaseModel):
|
| 8 |
+
prompt: str
|
| 9 |
+
app_type: Optional[str] = "saas"
|
| 10 |
+
systems: Optional[list[str]] = None
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class FixRequest(BaseModel):
|
| 14 |
+
error_message: str
|
| 15 |
+
file_path: Optional[str] = None
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class AgentMessage(BaseModel):
|
| 19 |
+
event_type: str # agent_start, token, code_block, agent_done, error, file_created
|
| 20 |
+
agent: str # research, orchestrator, frontend, backend, system
|
| 21 |
+
content: str = ""
|
| 22 |
+
file_path: Optional[str] = None
|
| 23 |
+
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
| 24 |
+
metadata: dict = Field(default_factory=dict)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class ProjectState(BaseModel):
|
| 28 |
+
session_id: str
|
| 29 |
+
user_prompt: str
|
| 30 |
+
app_type: str = "saas"
|
| 31 |
+
status: str = "queued" # queued, researching, orchestrating, building_frontend, building_backend, merging, completed, error, fixing
|
| 32 |
+
current_agent: str = ""
|
| 33 |
+
project_dir: str = ""
|
| 34 |
+
systems: list[str] = Field(default_factory=list)
|
| 35 |
+
messages: list[AgentMessage] = Field(default_factory=list)
|
| 36 |
+
generated_files: dict[str, str] = Field(default_factory=dict)
|
| 37 |
+
file_tree: list[str] = Field(default_factory=list)
|
| 38 |
+
errors: list[str] = Field(default_factory=list)
|
| 39 |
+
research_output: dict = Field(default_factory=dict)
|
| 40 |
+
blueprint: dict = Field(default_factory=dict)
|
| 41 |
+
|
| 42 |
+
class Config:
|
| 43 |
+
arbitrary_types_allowed = True
|
app/pipeline/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
undefined
|
app/pipeline/engine.py
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import re
|
| 3 |
+
import asyncio
|
| 4 |
+
import traceback
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
from app.agents import (
|
| 8 |
+
ResearchAgent, OrchestratorAgent, FrontendAgent, BackendAgent
|
| 9 |
+
)
|
| 10 |
+
from app.models.schemas import ProjectState, AgentMessage
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class PipelineEngine:
|
| 14 |
+
"""Orchestrates the 4-agent pipeline for project generation."""
|
| 15 |
+
|
| 16 |
+
def __init__(self):
|
| 17 |
+
self.research_agent = ResearchAgent()
|
| 18 |
+
self.orchestrator_agent = OrchestratorAgent()
|
| 19 |
+
self.frontend_agent = FrontendAgent()
|
| 20 |
+
self.backend_agent = BackendAgent()
|
| 21 |
+
|
| 22 |
+
def _emit(self, state: ProjectState, event_type: str, agent: str,
|
| 23 |
+
content: str = "", file_path: str | None = None, metadata: dict | None = None):
|
| 24 |
+
msg = AgentMessage(
|
| 25 |
+
event_type=event_type,
|
| 26 |
+
agent=agent,
|
| 27 |
+
content=content,
|
| 28 |
+
file_path=file_path,
|
| 29 |
+
metadata=metadata or {},
|
| 30 |
+
)
|
| 31 |
+
state.messages.append(msg)
|
| 32 |
+
|
| 33 |
+
async def run(self, state: ProjectState, sessions: dict):
|
| 34 |
+
"""Execute the full 4-agent pipeline."""
|
| 35 |
+
try:
|
| 36 |
+
# ── Phase 1: Research ──────────────────────────────
|
| 37 |
+
state.status = "researching"
|
| 38 |
+
state.current_agent = "research"
|
| 39 |
+
self._emit(state, "agent_start", "research",
|
| 40 |
+
"🔍 Starting research phase...")
|
| 41 |
+
|
| 42 |
+
research_output = await self._run_research(state)
|
| 43 |
+
state.research_output = research_output
|
| 44 |
+
|
| 45 |
+
self._emit(state, "agent_done", "research",
|
| 46 |
+
"✅ Research complete",
|
| 47 |
+
metadata={"summary": str(research_output)[:500]})
|
| 48 |
+
|
| 49 |
+
# ── Phase 2: Orchestration ─────────────────────────
|
| 50 |
+
state.status = "orchestrating"
|
| 51 |
+
state.current_agent = "orchestrator"
|
| 52 |
+
self._emit(state, "agent_start", "orchestrator",
|
| 53 |
+
"🧠 Creating master blueprint...")
|
| 54 |
+
|
| 55 |
+
blueprint = await self._run_orchestrator(state)
|
| 56 |
+
state.blueprint = blueprint
|
| 57 |
+
|
| 58 |
+
self._emit(state, "agent_done", "orchestrator",
|
| 59 |
+
"✅ Blueprint created",
|
| 60 |
+
metadata={"systems": list(blueprint.get("systems", {}).keys())})
|
| 61 |
+
|
| 62 |
+
# ── Phase 3 & 4: Frontend + Backend (parallel) ─────
|
| 63 |
+
state.status = "building"
|
| 64 |
+
self._emit(state, "agent_start", "system",
|
| 65 |
+
"🚀 Starting parallel code generation...")
|
| 66 |
+
|
| 67 |
+
state.current_agent = "frontend+backend"
|
| 68 |
+
await asyncio.gather(
|
| 69 |
+
self._run_frontend(state),
|
| 70 |
+
self._run_backend(state),
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
# ── Phase 5: Merge & Finalize ──────────────────────
|
| 74 |
+
state.status = "merging"
|
| 75 |
+
state.current_agent = "system"
|
| 76 |
+
self._emit(state, "agent_start", "system",
|
| 77 |
+
"📦 Merging outputs and building preview...")
|
| 78 |
+
|
| 79 |
+
self._build_file_tree(state)
|
| 80 |
+
self._generate_combined_preview(state)
|
| 81 |
+
|
| 82 |
+
state.status = "completed"
|
| 83 |
+
state.current_agent = ""
|
| 84 |
+
self._emit(state, "agent_done", "system",
|
| 85 |
+
f"🎉 Project complete! {len(state.generated_files)} files generated.")
|
| 86 |
+
|
| 87 |
+
except Exception as e:
|
| 88 |
+
state.status = "error"
|
| 89 |
+
state.errors.append(str(e))
|
| 90 |
+
self._emit(state, "error", state.current_agent or "system",
|
| 91 |
+
f"❌ Error: {str(e)}",
|
| 92 |
+
metadata={"traceback": traceback.format_exc()})
|
| 93 |
+
|
| 94 |
+
async def _run_research(self, state: ProjectState) -> dict:
|
| 95 |
+
prompt = f"""Research the following app idea and produce a comprehensive technical report:
|
| 96 |
+
|
| 97 |
+
APP IDEA: {state.user_prompt}
|
| 98 |
+
APP TYPE: {state.app_type}
|
| 99 |
+
REQUIRED SYSTEMS: {', '.join(state.systems)}
|
| 100 |
+
|
| 101 |
+
Produce your report as a JSON object with keys: stack_recommendation, schema_hints, api_docs_summary, security_notes, hosting_notes, ui_patterns, competitor_analysis"""
|
| 102 |
+
|
| 103 |
+
full_response = []
|
| 104 |
+
async for token in self.research_agent.call(
|
| 105 |
+
[{"role": "user", "content": prompt}], stream=True
|
| 106 |
+
):
|
| 107 |
+
full_response.append(token)
|
| 108 |
+
if len(full_response) % 20 == 0:
|
| 109 |
+
self._emit(state, "token", "research", token)
|
| 110 |
+
|
| 111 |
+
text = "".join(full_response)
|
| 112 |
+
self._emit(state, "token", "research", "\n[Research output received]")
|
| 113 |
+
|
| 114 |
+
# Parse JSON from response
|
| 115 |
+
return self._extract_json(text)
|
| 116 |
+
|
| 117 |
+
async def _run_orchestrator(self, state: ProjectState) -> dict:
|
| 118 |
+
prompt = f"""Create a master blueprint for this application:
|
| 119 |
+
|
| 120 |
+
USER REQUEST: {state.user_prompt}
|
| 121 |
+
APP TYPE: {state.app_type}
|
| 122 |
+
SYSTEMS TO BUILD: {', '.join(state.systems)}
|
| 123 |
+
|
| 124 |
+
RESEARCH DATA:
|
| 125 |
+
{json.dumps(state.research_output, indent=2, default=str)[:8000]}
|
| 126 |
+
|
| 127 |
+
Generate the complete master blueprint as a JSON object following the exact structure from your instructions. Customize everything for this specific app idea."""
|
| 128 |
+
|
| 129 |
+
full_response = []
|
| 130 |
+
async for token in self.orchestrator_agent.call(
|
| 131 |
+
[{"role": "user", "content": prompt}], stream=True
|
| 132 |
+
):
|
| 133 |
+
full_response.append(token)
|
| 134 |
+
if len(full_response) % 15 == 0:
|
| 135 |
+
self._emit(state, "token", "orchestrator", token)
|
| 136 |
+
|
| 137 |
+
text = "".join(full_response)
|
| 138 |
+
return self._extract_json(text)
|
| 139 |
+
|
| 140 |
+
async def _run_frontend(self, state: ProjectState):
|
| 141 |
+
self._emit(state, "agent_start", "frontend",
|
| 142 |
+
"🎨 Generating frontend code...")
|
| 143 |
+
|
| 144 |
+
for system in state.systems:
|
| 145 |
+
system_blueprint = state.blueprint.get("systems", {}).get(system, {})
|
| 146 |
+
design_tokens = state.blueprint.get("design_tokens", {})
|
| 147 |
+
payment_config = state.blueprint.get("payment_config", {})
|
| 148 |
+
|
| 149 |
+
prompt = f"""Generate the complete frontend code for the "{system}" system.
|
| 150 |
+
|
| 151 |
+
SYSTEM BLUEPRINT:
|
| 152 |
+
{json.dumps(system_blueprint, indent=2, default=str)}
|
| 153 |
+
|
| 154 |
+
DESIGN TOKENS:
|
| 155 |
+
{json.dumps(design_tokens, indent=2, default=str)}
|
| 156 |
+
|
| 157 |
+
PAYMENT CONFIG:
|
| 158 |
+
{json.dumps(payment_config, indent=2, default=str)}
|
| 159 |
+
|
| 160 |
+
DATABASE SCHEMA (for reference):
|
| 161 |
+
{json.dumps(state.blueprint.get('database_schema', {}), indent=2, default=str)[:4000]}
|
| 162 |
+
|
| 163 |
+
Generate ALL files needed for this system. Use React + Tailwind CSS.
|
| 164 |
+
Include: all page components, shared components, routing, Supabase client setup, theme system.
|
| 165 |
+
Mark each file with: // FILE: {system}/src/ComponentName.jsx
|
| 166 |
+
|
| 167 |
+
The app should look premium with the specified dark mode colors. Include animations and responsive design."""
|
| 168 |
+
|
| 169 |
+
full_response = []
|
| 170 |
+
async for token in self.frontend_agent.call(
|
| 171 |
+
[{"role": "user", "content": prompt}], stream=True
|
| 172 |
+
):
|
| 173 |
+
full_response.append(token)
|
| 174 |
+
if len(full_response) % 10 == 0:
|
| 175 |
+
self._emit(state, "token", "frontend", token)
|
| 176 |
+
|
| 177 |
+
text = "".join(full_response)
|
| 178 |
+
files = self._extract_files(text)
|
| 179 |
+
|
| 180 |
+
for path, content in files.items():
|
| 181 |
+
state.generated_files[path] = content
|
| 182 |
+
self._emit(state, "file_created", "frontend",
|
| 183 |
+
f"Created {path}", file_path=path)
|
| 184 |
+
|
| 185 |
+
self._emit(state, "agent_done", "frontend",
|
| 186 |
+
f"✅ Frontend generation complete")
|
| 187 |
+
|
| 188 |
+
async def _run_backend(self, state: ProjectState):
|
| 189 |
+
self._emit(state, "agent_start", "backend",
|
| 190 |
+
"🔐 Generating backend code...")
|
| 191 |
+
|
| 192 |
+
db_schema = state.blueprint.get("database_schema", {})
|
| 193 |
+
api_endpoints = state.blueprint.get("api_endpoints", [])
|
| 194 |
+
auth_config = state.blueprint.get("auth_config", {})
|
| 195 |
+
payment_config = state.blueprint.get("payment_config", {})
|
| 196 |
+
|
| 197 |
+
prompt = f"""Generate the complete backend code for this application.
|
| 198 |
+
|
| 199 |
+
DATABASE SCHEMA:
|
| 200 |
+
{json.dumps(db_schema, indent=2, default=str)}
|
| 201 |
+
|
| 202 |
+
API ENDPOINTS:
|
| 203 |
+
{json.dumps(api_endpoints, indent=2, default=str)}
|
| 204 |
+
|
| 205 |
+
AUTH CONFIG:
|
| 206 |
+
{json.dumps(auth_config, indent=2, default=str)}
|
| 207 |
+
|
| 208 |
+
PAYMENT CONFIG:
|
| 209 |
+
{json.dumps(payment_config, indent=2, default=str)}
|
| 210 |
+
|
| 211 |
+
APP DESCRIPTION: {state.user_prompt}
|
| 212 |
+
|
| 213 |
+
Generate ALL files:
|
| 214 |
+
1. Complete Supabase SQL schema (CREATE TABLE, RLS, triggers, indexes)
|
| 215 |
+
2. FastAPI backend with all routes, auth middleware, PayPal integration
|
| 216 |
+
3. Supabase Edge Functions (TypeScript)
|
| 217 |
+
4. Docker configuration (Dockerfile, docker-compose.yml)
|
| 218 |
+
5. Firebase hosting config
|
| 219 |
+
6. .env.example
|
| 220 |
+
7. DEPLOY.md with step-by-step deployment guide
|
| 221 |
+
|
| 222 |
+
Mark each file clearly with: # FILE: backend/filename.py or -- FILE: database/schema.sql"""
|
| 223 |
+
|
| 224 |
+
full_response = []
|
| 225 |
+
async for token in self.backend_agent.call(
|
| 226 |
+
[{"role": "user", "content": prompt}], stream=True
|
| 227 |
+
):
|
| 228 |
+
full_response.append(token)
|
| 229 |
+
if len(full_response) % 10 == 0:
|
| 230 |
+
self._emit(state, "token", "backend", token)
|
| 231 |
+
|
| 232 |
+
text = "".join(full_response)
|
| 233 |
+
files = self._extract_files(text)
|
| 234 |
+
|
| 235 |
+
for path, content in files.items():
|
| 236 |
+
state.generated_files[path] = content
|
| 237 |
+
self._emit(state, "file_created", "backend",
|
| 238 |
+
f"Created {path}", file_path=path)
|
| 239 |
+
|
| 240 |
+
self._emit(state, "agent_done", "backend",
|
| 241 |
+
"✅ Backend generation complete")
|
| 242 |
+
|
| 243 |
+
async def fix(self, state: ProjectState, error_message: str,
|
| 244 |
+
file_path: str | None = None):
|
| 245 |
+
"""Run a targeted bug fix through the appropriate agent."""
|
| 246 |
+
self._emit(state, "agent_start", "backend",
|
| 247 |
+
f"🔧 Fixing bug: {error_message[:100]}...")
|
| 248 |
+
|
| 249 |
+
relevant_code = ""
|
| 250 |
+
if file_path and file_path in state.generated_files:
|
| 251 |
+
relevant_code = state.generated_files[file_path]
|
| 252 |
+
|
| 253 |
+
prompt = f"""FIX THIS BUG:
|
| 254 |
+
|
| 255 |
+
ERROR: {error_message}
|
| 256 |
+
|
| 257 |
+
{"FILE: " + file_path if file_path else ""}
|
| 258 |
+
{"CODE:" + chr(10) + relevant_code[:8000] if relevant_code else ""}
|
| 259 |
+
|
| 260 |
+
Provide the COMPLETE fixed file content. Mark it with the original file path."""
|
| 261 |
+
|
| 262 |
+
# Determine which agent to use based on file type
|
| 263 |
+
if file_path and any(file_path.endswith(ext) for ext in [".jsx", ".tsx", ".css", ".html"]):
|
| 264 |
+
agent = self.frontend_agent
|
| 265 |
+
agent_name = "frontend"
|
| 266 |
+
else:
|
| 267 |
+
agent = self.backend_agent
|
| 268 |
+
agent_name = "backend"
|
| 269 |
+
|
| 270 |
+
full_response = []
|
| 271 |
+
async for token in agent.call(
|
| 272 |
+
[{"role": "user", "content": prompt}], stream=True
|
| 273 |
+
):
|
| 274 |
+
full_response.append(token)
|
| 275 |
+
|
| 276 |
+
text = "".join(full_response)
|
| 277 |
+
files = self._extract_files(text)
|
| 278 |
+
|
| 279 |
+
for path, content in files.items():
|
| 280 |
+
state.generated_files[path] = content
|
| 281 |
+
self._emit(state, "file_created", agent_name,
|
| 282 |
+
f"Fixed {path}", file_path=path)
|
| 283 |
+
|
| 284 |
+
state.status = "completed"
|
| 285 |
+
self._emit(state, "agent_done", agent_name, "✅ Bug fix applied")
|
| 286 |
+
|
| 287 |
+
def _extract_json(self, text: str) -> dict:
|
| 288 |
+
"""Extract JSON from a model response that may contain markdown."""
|
| 289 |
+
# Try to find JSON in code blocks
|
| 290 |
+
patterns = [
|
| 291 |
+
r'```json\s*([\s\S]*?)\s*```',
|
| 292 |
+
r'```\s*([\s\S]*?)\s*```',
|
| 293 |
+
r'\{[\s\S]*\}',
|
| 294 |
+
]
|
| 295 |
+
for pattern in patterns:
|
| 296 |
+
matches = re.findall(pattern, text)
|
| 297 |
+
for match in matches:
|
| 298 |
+
try:
|
| 299 |
+
return json.loads(match)
|
| 300 |
+
except json.JSONDecodeError:
|
| 301 |
+
continue
|
| 302 |
+
|
| 303 |
+
# If no valid JSON found, create a minimal structure
|
| 304 |
+
return {
|
| 305 |
+
"project_name": "generated-app",
|
| 306 |
+
"description": text[:200],
|
| 307 |
+
"systems": {
|
| 308 |
+
"client_portal": {"pages": [{"path": "/dashboard", "title": "Dashboard", "components": ["Layout", "Stats"], "auth_required": True}], "features": ["auth", "dashboard"]},
|
| 309 |
+
"public_landing": {"pages": [{"path": "/", "title": "Home", "components": ["Hero", "Features", "Pricing"], "auth_required": False}], "features": ["hero", "pricing"]},
|
| 310 |
+
"marketing_cms": {"pages": [{"path": "/blog", "title": "Blog", "components": ["BlogList", "Editor"], "auth_required": True}], "features": ["blog"]},
|
| 311 |
+
"analytics_dashboard": {"pages": [{"path": "/analytics", "title": "Analytics", "components": ["Charts", "Metrics"], "auth_required": True}], "features": ["charts"]},
|
| 312 |
+
"admin_panel": {"pages": [{"path": "/admin", "title": "Admin", "components": ["UserTable", "Settings"], "auth_required": True}], "features": ["user_management"]},
|
| 313 |
+
},
|
| 314 |
+
"database_schema": {"tables": []},
|
| 315 |
+
"api_endpoints": [],
|
| 316 |
+
"auth_config": {"providers": ["email"]},
|
| 317 |
+
"payment_config": {"provider": "paypal", "plans": []},
|
| 318 |
+
"design_tokens": {
|
| 319 |
+
"colors": {"primary": "#6C63FF", "secondary": "#00D9FF", "background": "#0A0A0F", "surface": "#111118", "text": "#F0F0FF"},
|
| 320 |
+
"fonts": {"heading": "Inter", "body": "Inter", "mono": "JetBrains Mono"},
|
| 321 |
+
},
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
def _extract_files(self, text: str) -> dict[str, str]:
|
| 325 |
+
"""Extract file contents from model output marked with FILE: comments."""
|
| 326 |
+
files = {}
|
| 327 |
+
# Match patterns like: // FILE: path/to/file.jsx or # FILE: path/to/file.py or -- FILE: path/to/file.sql
|
| 328 |
+
pattern = r'(?://|#|--)\s*FILE:\s*(.+?)(?:\n)([\s\S]*?)(?=(?://|#|--)\s*FILE:|$)'
|
| 329 |
+
matches = re.findall(pattern, text)
|
| 330 |
+
|
| 331 |
+
if matches:
|
| 332 |
+
for file_path, content in matches:
|
| 333 |
+
path = file_path.strip()
|
| 334 |
+
# Clean content — remove trailing code block markers
|
| 335 |
+
content = re.sub(r'\s*```\s*$', '', content.strip())
|
| 336 |
+
content = re.sub(r'^```\w*\s*', '', content.strip())
|
| 337 |
+
files[path] = content.strip()
|
| 338 |
+
else:
|
| 339 |
+
# Fallback: try to extract from code blocks with filenames
|
| 340 |
+
code_blocks = re.findall(
|
| 341 |
+
r'(?:#+\s*)?(?:`([^`]+)`|(\S+\.\w+))\s*\n```\w*\n([\s\S]*?)```',
|
| 342 |
+
text
|
| 343 |
+
)
|
| 344 |
+
for name1, name2, content in code_blocks:
|
| 345 |
+
name = name1 or name2
|
| 346 |
+
if name and content.strip():
|
| 347 |
+
files[name.strip()] = content.strip()
|
| 348 |
+
|
| 349 |
+
# If still no files extracted, save as raw output
|
| 350 |
+
if not files:
|
| 351 |
+
if "def " in text or "import " in text:
|
| 352 |
+
files["backend/generated_output.py"] = text
|
| 353 |
+
elif "function " in text or "const " in text or "import " in text:
|
| 354 |
+
files["frontend/generated_output.jsx"] = text
|
| 355 |
+
elif "CREATE TABLE" in text.upper():
|
| 356 |
+
files["database/schema.sql"] = text
|
| 357 |
+
else:
|
| 358 |
+
files["output/raw_output.txt"] = text
|
| 359 |
+
|
| 360 |
+
return files
|
| 361 |
+
|
| 362 |
+
def _build_file_tree(self, state: ProjectState):
|
| 363 |
+
"""Build a sorted file tree from generated files."""
|
| 364 |
+
state.file_tree = sorted(state.generated_files.keys())
|
| 365 |
+
|
| 366 |
+
def _generate_combined_preview(self, state: ProjectState):
|
| 367 |
+
"""Generate a combined HTML preview page for the app."""
|
| 368 |
+
design = state.blueprint.get("design_tokens", {})
|
| 369 |
+
colors = design.get("colors", {})
|
| 370 |
+
project_name = state.blueprint.get("project_name", "Generated App")
|
| 371 |
+
systems = state.blueprint.get("systems", {})
|
| 372 |
+
|
| 373 |
+
system_cards = ""
|
| 374 |
+
for sys_name, sys_data in systems.items():
|
| 375 |
+
pages = sys_data.get("pages", [])
|
| 376 |
+
features = sys_data.get("features", [])
|
| 377 |
+
page_list = "".join(
|
| 378 |
+
f'<li>{p.get("title", p.get("path", "Page"))}</li>'
|
| 379 |
+
for p in pages[:5]
|
| 380 |
+
)
|
| 381 |
+
feature_list = "".join(f'<span class="tag">{f}</span>' for f in features[:6])
|
| 382 |
+
nice_name = sys_name.replace("_", " ").title()
|
| 383 |
+
system_cards += f"""
|
| 384 |
+
<div class="system-card">
|
| 385 |
+
<h3>{nice_name}</h3>
|
| 386 |
+
<div class="tags">{feature_list}</div>
|
| 387 |
+
<ul>{page_list}</ul>
|
| 388 |
+
<div class="page-count">{len(pages)} pages</div>
|
| 389 |
+
</div>"""
|
| 390 |
+
|
| 391 |
+
file_count = len(state.generated_files)
|
| 392 |
+
file_types = {}
|
| 393 |
+
for f in state.generated_files:
|
| 394 |
+
ext = f.rsplit(".", 1)[-1] if "." in f else "other"
|
| 395 |
+
file_types[ext] = file_types.get(ext, 0) + 1
|
| 396 |
+
file_stats = " • ".join(f"{count} .{ext}" for ext, count in sorted(file_types.items()))
|
| 397 |
+
|
| 398 |
+
preview_html = f"""<!DOCTYPE html>
|
| 399 |
+
<html lang="en">
|
| 400 |
+
<head>
|
| 401 |
+
<meta charset="UTF-8">
|
| 402 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 403 |
+
<title>{project_name} — Preview</title>
|
| 404 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 405 |
+
<style>
|
| 406 |
+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
| 407 |
+
body {{
|
| 408 |
+
font-family: 'Inter', sans-serif;
|
| 409 |
+
background: {colors.get('background', '#0A0A0F')};
|
| 410 |
+
color: {colors.get('text', '#F0F0FF')};
|
| 411 |
+
min-height: 100vh;
|
| 412 |
+
padding: 2rem;
|
| 413 |
+
}}
|
| 414 |
+
.container {{ max-width: 1200px; margin: 0 auto; }}
|
| 415 |
+
.hero {{
|
| 416 |
+
text-align: center;
|
| 417 |
+
padding: 4rem 2rem;
|
| 418 |
+
background: linear-gradient(135deg, {colors.get('surface', '#111118')} 0%, {colors.get('background', '#0A0A0F')} 100%);
|
| 419 |
+
border-radius: 24px;
|
| 420 |
+
border: 1px solid #1E1E2E;
|
| 421 |
+
margin-bottom: 2rem;
|
| 422 |
+
}}
|
| 423 |
+
.hero h1 {{
|
| 424 |
+
font-size: 3rem;
|
| 425 |
+
font-weight: 700;
|
| 426 |
+
background: linear-gradient(135deg, {colors.get('primary', '#6C63FF')}, {colors.get('secondary', '#00D9FF')});
|
| 427 |
+
-webkit-background-clip: text;
|
| 428 |
+
-webkit-text-fill-color: transparent;
|
| 429 |
+
margin-bottom: 1rem;
|
| 430 |
+
}}
|
| 431 |
+
.hero p {{ color: #8888AA; font-size: 1.2rem; max-width: 600px; margin: 0 auto; }}
|
| 432 |
+
.stats {{
|
| 433 |
+
display: flex;
|
| 434 |
+
gap: 1rem;
|
| 435 |
+
justify-content: center;
|
| 436 |
+
margin-top: 2rem;
|
| 437 |
+
flex-wrap: wrap;
|
| 438 |
+
}}
|
| 439 |
+
.stat {{
|
| 440 |
+
background: #111118;
|
| 441 |
+
border: 1px solid #1E1E2E;
|
| 442 |
+
border-radius: 12px;
|
| 443 |
+
padding: 1rem 1.5rem;
|
| 444 |
+
text-align: center;
|
| 445 |
+
}}
|
| 446 |
+
.stat-value {{
|
| 447 |
+
font-size: 2rem;
|
| 448 |
+
font-weight: 700;
|
| 449 |
+
color: {colors.get('primary', '#6C63FF')};
|
| 450 |
+
}}
|
| 451 |
+
.stat-label {{ color: #8888AA; font-size: 0.85rem; margin-top: 0.25rem; }}
|
| 452 |
+
.systems-grid {{
|
| 453 |
+
display: grid;
|
| 454 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 455 |
+
gap: 1.5rem;
|
| 456 |
+
margin-top: 2rem;
|
| 457 |
+
}}
|
| 458 |
+
.system-card {{
|
| 459 |
+
background: #111118;
|
| 460 |
+
border: 1px solid #1E1E2E;
|
| 461 |
+
border-radius: 16px;
|
| 462 |
+
padding: 1.5rem;
|
| 463 |
+
transition: all 0.3s ease;
|
| 464 |
+
}}
|
| 465 |
+
.system-card:hover {{
|
| 466 |
+
border-color: {colors.get('primary', '#6C63FF')}44;
|
| 467 |
+
transform: translateY(-2px);
|
| 468 |
+
box-shadow: 0 8px 32px {colors.get('primary', '#6C63FF')}11;
|
| 469 |
+
}}
|
| 470 |
+
.system-card h3 {{
|
| 471 |
+
font-size: 1.2rem;
|
| 472 |
+
font-weight: 600;
|
| 473 |
+
margin-bottom: 0.75rem;
|
| 474 |
+
color: {colors.get('text', '#F0F0FF')};
|
| 475 |
+
}}
|
| 476 |
+
.tags {{ display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 1rem; }}
|
| 477 |
+
.tag {{
|
| 478 |
+
background: {colors.get('primary', '#6C63FF')}22;
|
| 479 |
+
color: {colors.get('primary', '#6C63FF')};
|
| 480 |
+
padding: 0.2rem 0.6rem;
|
| 481 |
+
border-radius: 6px;
|
| 482 |
+
font-size: 0.75rem;
|
| 483 |
+
font-weight: 500;
|
| 484 |
+
}}
|
| 485 |
+
ul {{ list-style: none; margin-bottom: 0.75rem; }}
|
| 486 |
+
li {{
|
| 487 |
+
padding: 0.3rem 0;
|
| 488 |
+
color: #8888AA;
|
| 489 |
+
font-size: 0.9rem;
|
| 490 |
+
border-bottom: 1px solid #1E1E2E;
|
| 491 |
+
}}
|
| 492 |
+
li:last-child {{ border-bottom: none; }}
|
| 493 |
+
.page-count {{
|
| 494 |
+
color: {colors.get('secondary', '#00D9FF')};
|
| 495 |
+
font-size: 0.8rem;
|
| 496 |
+
font-weight: 500;
|
| 497 |
+
}}
|
| 498 |
+
.file-info {{
|
| 499 |
+
text-align: center;
|
| 500 |
+
margin-top: 2rem;
|
| 501 |
+
padding: 1rem;
|
| 502 |
+
color: #8888AA;
|
| 503 |
+
font-family: 'JetBrains Mono', monospace;
|
| 504 |
+
font-size: 0.85rem;
|
| 505 |
+
}}
|
| 506 |
+
.section-title {{
|
| 507 |
+
font-size: 1.5rem;
|
| 508 |
+
font-weight: 600;
|
| 509 |
+
margin-bottom: 0.5rem;
|
| 510 |
+
}}
|
| 511 |
+
@keyframes fadeIn {{
|
| 512 |
+
from {{ opacity: 0; transform: translateY(10px); }}
|
| 513 |
+
to {{ opacity: 1; transform: translateY(0); }}
|
| 514 |
+
}}
|
| 515 |
+
.system-card {{ animation: fadeIn 0.5s ease forwards; }}
|
| 516 |
+
.system-card:nth-child(2) {{ animation-delay: 0.1s; }}
|
| 517 |
+
.system-card:nth-child(3) {{ animation-delay: 0.2s; }}
|
| 518 |
+
.system-card:nth-child(4) {{ animation-delay: 0.3s; }}
|
| 519 |
+
.system-card:nth-child(5) {{ animation-delay: 0.4s; }}
|
| 520 |
+
</style>
|
| 521 |
+
</head>
|
| 522 |
+
<body>
|
| 523 |
+
<div class="container">
|
| 524 |
+
<div class="hero">
|
| 525 |
+
<h1>{project_name}</h1>
|
| 526 |
+
<p>{state.user_prompt[:200]}</p>
|
| 527 |
+
<div class="stats">
|
| 528 |
+
<div class="stat">
|
| 529 |
+
<div class="stat-value">{len(systems)}</div>
|
| 530 |
+
<div class="stat-label">Systems</div>
|
| 531 |
+
</div>
|
| 532 |
+
<div class="stat">
|
| 533 |
+
<div class="stat-value">{file_count}</div>
|
| 534 |
+
<div class="stat-label">Files Generated</div>
|
| 535 |
+
</div>
|
| 536 |
+
<div class="stat">
|
| 537 |
+
<div class="stat-value">{sum(len(s.get('pages',[])) for s in systems.values())}</div>
|
| 538 |
+
<div class="stat-label">Total Pages</div>
|
| 539 |
+
</div>
|
| 540 |
+
</div>
|
| 541 |
+
</div>
|
| 542 |
+
|
| 543 |
+
<h2 class="section-title">Generated Systems</h2>
|
| 544 |
+
<div class="systems-grid">
|
| 545 |
+
{system_cards}
|
| 546 |
+
</div>
|
| 547 |
+
|
| 548 |
+
<div class="file-info">
|
| 549 |
+
{file_stats}
|
| 550 |
+
</div>
|
| 551 |
+
</div>
|
| 552 |
+
</body>
|
| 553 |
+
</html>"""
|
| 554 |
+
|
| 555 |
+
state.generated_files["preview/index.html"] = preview_html
|
app/preview/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
undefined
|
app/preview/builder.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Preview builder utilities — generates live previews of generated apps."""
|
| 2 |
+
|
| 3 |
+
import subprocess
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def build_frontend_preview(project_dir: str, session_id: str) -> str | None:
|
| 8 |
+
"""Attempt to build a frontend project for live preview.
|
| 9 |
+
|
| 10 |
+
Returns the path to built files, or None if build is not possible
|
| 11 |
+
(e.g., no package.json, or npm not available on CPU tier).
|
| 12 |
+
"""
|
| 13 |
+
frontend_dir = Path(project_dir) / "frontend"
|
| 14 |
+
package_json = frontend_dir / "package.json"
|
| 15 |
+
|
| 16 |
+
if not package_json.exists():
|
| 17 |
+
return None
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
# Install deps
|
| 21 |
+
subprocess.run(
|
| 22 |
+
["npm", "install"],
|
| 23 |
+
cwd=str(frontend_dir),
|
| 24 |
+
timeout=120,
|
| 25 |
+
capture_output=True,
|
| 26 |
+
)
|
| 27 |
+
# Build
|
| 28 |
+
result = subprocess.run(
|
| 29 |
+
["npm", "run", "build"],
|
| 30 |
+
cwd=str(frontend_dir),
|
| 31 |
+
timeout=120,
|
| 32 |
+
capture_output=True,
|
| 33 |
+
)
|
| 34 |
+
if result.returncode == 0:
|
| 35 |
+
dist_dir = frontend_dir / "dist"
|
| 36 |
+
if dist_dir.exists():
|
| 37 |
+
preview_dir = f"/tmp/nexus_previews/{session_id}"
|
| 38 |
+
Path(preview_dir).mkdir(parents=True, exist_ok=True)
|
| 39 |
+
import shutil
|
| 40 |
+
shutil.copytree(str(dist_dir), preview_dir, dirs_exist_ok=True)
|
| 41 |
+
return preview_dir
|
| 42 |
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
| 43 |
+
pass
|
| 44 |
+
|
| 45 |
+
return None
|
fronend/index.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚀</text></svg>" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>NEXUS Builder — AI Full-Stack App Maker</title>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div id="root"></div>
|
| 12 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 13 |
+
</body>
|
| 14 |
+
</html>
|
fronend/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "nexus-builder-ui",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"react": "^18.3.1",
|
| 13 |
+
"react-dom": "^18.3.1",
|
| 14 |
+
"lucide-react": "^0.469.0"
|
| 15 |
+
},
|
| 16 |
+
"devDependencies": {
|
| 17 |
+
"@types/react": "^18.3.18",
|
| 18 |
+
"@types/react-dom": "^18.3.5",
|
| 19 |
+
"@vitejs/plugin-react": "^4.3.4",
|
| 20 |
+
"autoprefixer": "^10.4.20",
|
| 21 |
+
"postcss": "^8.4.49",
|
| 22 |
+
"tailwindcss": "^3.4.17",
|
| 23 |
+
"vite": "^6.0.5"
|
| 24 |
+
}
|
| 25 |
+
}
|
fronend/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
fronend/src/App.jsx
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useCallback, useRef, useEffect } from 'react'
|
| 2 |
+
import Header from './components/Header'
|
| 3 |
+
import ControlPanel from './components/ControlPanel'
|
| 4 |
+
import LivePreview from './components/LivePreview'
|
| 5 |
+
import StatusBar from './components/StatusBar'
|
| 6 |
+
import { useSSE } from './hooks/useSSE'
|
| 7 |
+
import { generateProject, getProjectStatus, getProjectFiles, exportProject, fixBug } from './utils/api'
|
| 8 |
+
|
| 9 |
+
const INITIAL_AGENTS = {
|
| 10 |
+
research: { name: 'GLM 4.5 Air', status: 'idle', icon: '🌐' },
|
| 11 |
+
orchestrator: { name: 'Trinity Large', status: 'idle', icon: '🧠' },
|
| 12 |
+
frontend: { name: 'Qwen3 Coder', status: 'idle', icon: '🎨' },
|
| 13 |
+
backend: { name: 'MiniMax M2.5', status: 'idle', icon: '🔐' },
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export default function App() {
|
| 17 |
+
const [sessionId, setSessionId] = useState(null)
|
| 18 |
+
const [status, setStatus] = useState('idle')
|
| 19 |
+
const [agents, setAgents] = useState(INITIAL_AGENTS)
|
| 20 |
+
const [messages, setMessages] = useState([])
|
| 21 |
+
const [agentFeed, setAgentFeed] = useState([])
|
| 22 |
+
const [files, setFiles] = useState({})
|
| 23 |
+
const [fileTree, setFileTree] = useState([])
|
| 24 |
+
const [selectedFile, setSelectedFile] = useState(null)
|
| 25 |
+
const [previewSystem, setPreviewSystem] = useState('preview')
|
| 26 |
+
const [viewMode, setViewMode] = useState('preview') // preview | code
|
| 27 |
+
const [errors, setErrors] = useState([])
|
| 28 |
+
|
| 29 |
+
// Handle SSE events from the pipeline
|
| 30 |
+
const handleSSEEvent = useCallback((event) => {
|
| 31 |
+
const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data
|
| 32 |
+
|
| 33 |
+
switch (event.type || data.event_type) {
|
| 34 |
+
case 'agent_start':
|
| 35 |
+
setAgents(prev => ({
|
| 36 |
+
...prev,
|
| 37 |
+
[data.agent]: { ...prev[data.agent], status: 'active' }
|
| 38 |
+
}))
|
| 39 |
+
setAgentFeed(prev => [...prev, {
|
| 40 |
+
agent: data.agent,
|
| 41 |
+
content: data.content,
|
| 42 |
+
time: new Date().toLocaleTimeString(),
|
| 43 |
+
type: 'start'
|
| 44 |
+
}])
|
| 45 |
+
setStatus(data.content || 'Working...')
|
| 46 |
+
break
|
| 47 |
+
|
| 48 |
+
case 'token':
|
| 49 |
+
setAgentFeed(prev => {
|
| 50 |
+
const last = prev[prev.length - 1]
|
| 51 |
+
if (last && last.agent === data.agent && last.type === 'stream') {
|
| 52 |
+
return [...prev.slice(0, -1), { ...last, content: last.content + data.content }]
|
| 53 |
+
}
|
| 54 |
+
return [...prev, {
|
| 55 |
+
agent: data.agent,
|
| 56 |
+
content: data.content,
|
| 57 |
+
time: new Date().toLocaleTimeString(),
|
| 58 |
+
type: 'stream'
|
| 59 |
+
}]
|
| 60 |
+
})
|
| 61 |
+
break
|
| 62 |
+
|
| 63 |
+
case 'file_created':
|
| 64 |
+
setAgentFeed(prev => [...prev, {
|
| 65 |
+
agent: data.agent,
|
| 66 |
+
content: data.content,
|
| 67 |
+
time: new Date().toLocaleTimeString(),
|
| 68 |
+
type: 'file',
|
| 69 |
+
filePath: data.file_path
|
| 70 |
+
}])
|
| 71 |
+
if (data.file_path) {
|
| 72 |
+
setFileTree(prev => {
|
| 73 |
+
const newTree = [...new Set([...prev, data.file_path])]
|
| 74 |
+
return newTree.sort()
|
| 75 |
+
})
|
| 76 |
+
}
|
| 77 |
+
break
|
| 78 |
+
|
| 79 |
+
case 'agent_done':
|
| 80 |
+
setAgents(prev => ({
|
| 81 |
+
...prev,
|
| 82 |
+
[data.agent]: { ...prev[data.agent], status: 'done' }
|
| 83 |
+
}))
|
| 84 |
+
setAgentFeed(prev => [...prev, {
|
| 85 |
+
agent: data.agent,
|
| 86 |
+
content: data.content,
|
| 87 |
+
time: new Date().toLocaleTimeString(),
|
| 88 |
+
type: 'done'
|
| 89 |
+
}])
|
| 90 |
+
break
|
| 91 |
+
|
| 92 |
+
case 'error':
|
| 93 |
+
setErrors(prev => [...prev, data.content])
|
| 94 |
+
setAgents(prev => ({
|
| 95 |
+
...prev,
|
| 96 |
+
[data.agent]: { ...prev[data.agent], status: 'error' }
|
| 97 |
+
}))
|
| 98 |
+
break
|
| 99 |
+
|
| 100 |
+
case 'done':
|
| 101 |
+
setStatus(data.status === 'completed' ? 'completed' : 'error')
|
| 102 |
+
// Fetch final files
|
| 103 |
+
if (data.session_id) {
|
| 104 |
+
getProjectFiles(data.session_id).then(res => {
|
| 105 |
+
if (res.files) {
|
| 106 |
+
setFiles(res.files)
|
| 107 |
+
setFileTree(Object.keys(res.files).sort())
|
| 108 |
+
}
|
| 109 |
+
})
|
| 110 |
+
}
|
| 111 |
+
break
|
| 112 |
+
|
| 113 |
+
default:
|
| 114 |
+
break
|
| 115 |
+
}
|
| 116 |
+
}, [])
|
| 117 |
+
|
| 118 |
+
const { connect, disconnect } = useSSE(handleSSEEvent)
|
| 119 |
+
|
| 120 |
+
// Start generation
|
| 121 |
+
const handleGenerate = useCallback(async (prompt, appType) => {
|
| 122 |
+
// Reset state
|
| 123 |
+
setAgents(INITIAL_AGENTS)
|
| 124 |
+
setAgentFeed([])
|
| 125 |
+
setFiles({})
|
| 126 |
+
setFileTree([])
|
| 127 |
+
setErrors([])
|
| 128 |
+
setSelectedFile(null)
|
| 129 |
+
setStatus('starting')
|
| 130 |
+
|
| 131 |
+
setMessages(prev => [...prev, { role: 'user', content: prompt }])
|
| 132 |
+
|
| 133 |
+
try {
|
| 134 |
+
const res = await generateProject(prompt, appType)
|
| 135 |
+
setSessionId(res.session_id)
|
| 136 |
+
setStatus('connected')
|
| 137 |
+
|
| 138 |
+
// Connect to SSE stream
|
| 139 |
+
connect(res.session_id)
|
| 140 |
+
|
| 141 |
+
setMessages(prev => [...prev, {
|
| 142 |
+
role: 'assistant',
|
| 143 |
+
content: `🚀 Project generation started! Session: ${res.session_id}\n\nI'm coordinating 4 AI agents to build your application...`
|
| 144 |
+
}])
|
| 145 |
+
} catch (err) {
|
| 146 |
+
setStatus('error')
|
| 147 |
+
setErrors(prev => [...prev, err.message])
|
| 148 |
+
setMessages(prev => [...prev, {
|
| 149 |
+
role: 'assistant',
|
| 150 |
+
content: `❌ Error starting generation: ${err.message}`
|
| 151 |
+
}])
|
| 152 |
+
}
|
| 153 |
+
}, [connect])
|
| 154 |
+
|
| 155 |
+
// Export
|
| 156 |
+
const handleExport = useCallback(async () => {
|
| 157 |
+
if (!sessionId) return
|
| 158 |
+
try {
|
| 159 |
+
await exportProject(sessionId)
|
| 160 |
+
} catch (err) {
|
| 161 |
+
setErrors(prev => [...prev, `Export failed: ${err.message}`])
|
| 162 |
+
}
|
| 163 |
+
}, [sessionId])
|
| 164 |
+
|
| 165 |
+
// Fix bug
|
| 166 |
+
const handleFix = useCallback(async (errorMessage, filePath) => {
|
| 167 |
+
if (!sessionId) return
|
| 168 |
+
try {
|
| 169 |
+
await fixBug(sessionId, errorMessage, filePath)
|
| 170 |
+
connect(sessionId) // Reconnect SSE for fix updates
|
| 171 |
+
} catch (err) {
|
| 172 |
+
setErrors(prev => [...prev, `Fix failed: ${err.message}`])
|
| 173 |
+
}
|
| 174 |
+
}, [sessionId, connect])
|
| 175 |
+
|
| 176 |
+
return (
|
| 177 |
+
<div className="h-screen w-screen flex flex-col overflow-hidden" style={{ background: 'var(--bg)' }}>
|
| 178 |
+
<Header
|
| 179 |
+
agents={agents}
|
| 180 |
+
sessionId={sessionId}
|
| 181 |
+
status={status}
|
| 182 |
+
/>
|
| 183 |
+
|
| 184 |
+
<div className="flex flex-1 overflow-hidden">
|
| 185 |
+
{/* Left Panel — Control Center */}
|
| 186 |
+
<ControlPanel
|
| 187 |
+
messages={messages}
|
| 188 |
+
agentFeed={agentFeed}
|
| 189 |
+
fileTree={fileTree}
|
| 190 |
+
files={files}
|
| 191 |
+
selectedFile={selectedFile}
|
| 192 |
+
onSelectFile={setSelectedFile}
|
| 193 |
+
onGenerate={handleGenerate}
|
| 194 |
+
onFix={handleFix}
|
| 195 |
+
status={status}
|
| 196 |
+
/>
|
| 197 |
+
|
| 198 |
+
{/* Right Panel — Preview */}
|
| 199 |
+
<LivePreview
|
| 200 |
+
sessionId={sessionId}
|
| 201 |
+
files={files}
|
| 202 |
+
selectedFile={selectedFile}
|
| 203 |
+
previewSystem={previewSystem}
|
| 204 |
+
onChangeSystem={setPreviewSystem}
|
| 205 |
+
viewMode={viewMode}
|
| 206 |
+
onChangeViewMode={setViewMode}
|
| 207 |
+
onExport={handleExport}
|
| 208 |
+
status={status}
|
| 209 |
+
/>
|
| 210 |
+
</div>
|
| 211 |
+
|
| 212 |
+
<StatusBar
|
| 213 |
+
status={status}
|
| 214 |
+
sessionId={sessionId}
|
| 215 |
+
agents={agents}
|
| 216 |
+
errorCount={errors.length}
|
| 217 |
+
fileCount={fileTree.length}
|
| 218 |
+
/>
|
| 219 |
+
</div>
|
| 220 |
+
)
|
| 221 |
+
}
|
fronend/src/components/AgentFeed.jsx
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useRef, useEffect } from 'react'
|
| 2 |
+
import { Bot, FileCode2, CheckCircle2, AlertCircle, Zap } from 'lucide-react'
|
| 3 |
+
|
| 4 |
+
const AGENT_COLORS = {
|
| 5 |
+
research: '#00D9FF',
|
| 6 |
+
orchestrator: '#6C63FF',
|
| 7 |
+
frontend: '#22D3A8',
|
| 8 |
+
backend: '#FFB547',
|
| 9 |
+
system: '#8888AA',
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const AGENT_ICONS = {
|
| 13 |
+
research: '🌐',
|
| 14 |
+
orchestrator: '🧠',
|
| 15 |
+
frontend: '🎨',
|
| 16 |
+
backend: '🔐',
|
| 17 |
+
system: '⚙️',
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export default function AgentFeed({ feed }) {
|
| 21 |
+
const feedEndRef = useRef(null)
|
| 22 |
+
|
| 23 |
+
useEffect(() => {
|
| 24 |
+
feedEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
| 25 |
+
}, [feed])
|
| 26 |
+
|
| 27 |
+
if (feed.length === 0) {
|
| 28 |
+
return (
|
| 29 |
+
<div className="flex flex-col items-center justify-center h-full text-center px-6">
|
| 30 |
+
<Bot className="w-12 h-12 mb-3" style={{ color: 'var(--text-secondary)', opacity: 0.4 }} />
|
| 31 |
+
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
| 32 |
+
Agent activity will appear here once you start generating.
|
| 33 |
+
</p>
|
| 34 |
+
</div>
|
| 35 |
+
)
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
return (
|
| 39 |
+
<div className="h-full overflow-y-auto p-3 space-y-2">
|
| 40 |
+
{feed.map((item, i) => (
|
| 41 |
+
<div
|
| 42 |
+
key={i}
|
| 43 |
+
className="flex items-start gap-2.5 p-2.5 rounded-lg animate-fade-in text-xs"
|
| 44 |
+
style={{
|
| 45 |
+
background: item.type === 'done' ? `${AGENT_COLORS[item.agent]}11` : 'var(--surface)',
|
| 46 |
+
border: `1px solid ${item.type === 'done' ? AGENT_COLORS[item.agent] + '33' : 'var(--border)'}`,
|
| 47 |
+
}}
|
| 48 |
+
>
|
| 49 |
+
{/* Agent icon */}
|
| 50 |
+
<span className="text-base flex-shrink-0 mt-0.5">
|
| 51 |
+
{AGENT_ICONS[item.agent] || '⚙️'}
|
| 52 |
+
</span>
|
| 53 |
+
|
| 54 |
+
<div className="flex-1 min-w-0">
|
| 55 |
+
{/* Agent name + time */}
|
| 56 |
+
<div className="flex items-center justify-between mb-1">
|
| 57 |
+
<span className="font-semibold capitalize" style={{ color: AGENT_COLORS[item.agent] }}>
|
| 58 |
+
{item.agent}
|
| 59 |
+
</span>
|
| 60 |
+
<span className="text-[10px]" style={{ color: 'var(--text-secondary)' }}>
|
| 61 |
+
{item.time}
|
| 62 |
+
</span>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
{/* Content */}
|
| 66 |
+
<div style={{ color: 'var(--text-secondary)' }}>
|
| 67 |
+
{item.type === 'file' ? (
|
| 68 |
+
<div className="flex items-center gap-1.5">
|
| 69 |
+
<FileCode2 className="w-3 h-3" style={{ color: 'var(--success)' }} />
|
| 70 |
+
<span className="font-mono text-[11px]">{item.content}</span>
|
| 71 |
+
</div>
|
| 72 |
+
) : item.type === 'done' ? (
|
| 73 |
+
<div className="flex items-center gap-1.5">
|
| 74 |
+
<CheckCircle2 className="w-3 h-3" style={{ color: AGENT_COLORS[item.agent] }} />
|
| 75 |
+
<span>{item.content}</span>
|
| 76 |
+
</div>
|
| 77 |
+
) : item.type === 'stream' ? (
|
| 78 |
+
<pre className="whitespace-pre-wrap font-mono text-[11px] max-h-24 overflow-y-auto leading-relaxed">
|
| 79 |
+
{item.content.slice(-300)}
|
| 80 |
+
<span className="typing-cursor" />
|
| 81 |
+
</pre>
|
| 82 |
+
) : (
|
| 83 |
+
<span>{item.content}</span>
|
| 84 |
+
)}
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
))}
|
| 89 |
+
<div ref={feedEndRef} />
|
| 90 |
+
</div>
|
| 91 |
+
)
|
| 92 |
+
}
|
fronend/src/components/ChatInterface.jsx
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useRef, useEffect } from 'react'
|
| 2 |
+
import { Send, Sparkles, Loader2 } from 'lucide-react'
|
| 3 |
+
|
| 4 |
+
const APP_TYPES = [
|
| 5 |
+
{ id: 'saas', label: 'SaaS Platform', emoji: '💼' },
|
| 6 |
+
{ id: 'ecommerce', label: 'E-Commerce', emoji: '🛒' },
|
| 7 |
+
{ id: 'marketplace', label: 'Marketplace', emoji: '🏪' },
|
| 8 |
+
{ id: 'social', label: 'Social Network', emoji: '👥' },
|
| 9 |
+
{ id: 'education', label: 'EdTech / LMS', emoji: '📚' },
|
| 10 |
+
{ id: 'health', label: 'HealthTech', emoji: '🏥' },
|
| 11 |
+
{ id: 'finance', label: 'FinTech', emoji: '💰' },
|
| 12 |
+
{ id: 'custom', label: 'Custom', emoji: '⚡' },
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
const EXAMPLE_PROMPTS = [
|
| 16 |
+
"Build a project management SaaS like Linear with team workspaces, sprint boards, and issue tracking",
|
| 17 |
+
"Create an online course marketplace where instructors can sell video courses with progress tracking",
|
| 18 |
+
"Build a subscription-based fitness app with workout plans, progress photos, and meal tracking",
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
export default function ChatInterface({ messages, onGenerate, onFix, status }) {
|
| 22 |
+
const [input, setInput] = useState('')
|
| 23 |
+
const [appType, setAppType] = useState('saas')
|
| 24 |
+
const [showTypes, setShowTypes] = useState(false)
|
| 25 |
+
const chatEndRef = useRef(null)
|
| 26 |
+
const inputRef = useRef(null)
|
| 27 |
+
|
| 28 |
+
const isGenerating = !['idle', 'completed', 'error'].includes(status)
|
| 29 |
+
|
| 30 |
+
useEffect(() => {
|
| 31 |
+
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
| 32 |
+
}, [messages])
|
| 33 |
+
|
| 34 |
+
const handleSubmit = (e) => {
|
| 35 |
+
e.preventDefault()
|
| 36 |
+
if (!input.trim() || isGenerating) return
|
| 37 |
+
onGenerate(input.trim(), appType)
|
| 38 |
+
setInput('')
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
const handleExampleClick = (prompt) => {
|
| 42 |
+
setInput(prompt)
|
| 43 |
+
inputRef.current?.focus()
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
return (
|
| 47 |
+
<div className="flex flex-col h-full">
|
| 48 |
+
{/* Messages area */}
|
| 49 |
+
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
| 50 |
+
{messages.length === 0 ? (
|
| 51 |
+
/* Welcome screen */
|
| 52 |
+
<div className="flex flex-col items-center justify-center h-full text-center px-4">
|
| 53 |
+
<div className="text-5xl mb-4">🚀</div>
|
| 54 |
+
<h2 className="text-xl font-bold gradient-text mb-2">
|
| 55 |
+
Welcome to Nexus Builder
|
| 56 |
+
</h2>
|
| 57 |
+
<p className="text-sm mb-6" style={{ color: 'var(--text-secondary)' }}>
|
| 58 |
+
Describe your app idea and 4 AI agents will build it for you —
|
| 59 |
+
complete with auth, payments, analytics, and admin panel.
|
| 60 |
+
</p>
|
| 61 |
+
|
| 62 |
+
{/* App type selector */}
|
| 63 |
+
<div className="w-full mb-4">
|
| 64 |
+
<button
|
| 65 |
+
onClick={() => setShowTypes(!showTypes)}
|
| 66 |
+
className="w-full text-left text-xs font-medium px-3 py-2 rounded-lg border transition-all"
|
| 67 |
+
style={{
|
| 68 |
+
borderColor: 'var(--border)',
|
| 69 |
+
background: 'var(--surface)',
|
| 70 |
+
color: 'var(--text-secondary)',
|
| 71 |
+
}}
|
| 72 |
+
>
|
| 73 |
+
App Type: {APP_TYPES.find(t => t.id === appType)?.emoji}{' '}
|
| 74 |
+
{APP_TYPES.find(t => t.id === appType)?.label}
|
| 75 |
+
</button>
|
| 76 |
+
{showTypes && (
|
| 77 |
+
<div className="mt-1 grid grid-cols-2 gap-1 p-2 rounded-lg border animate-fade-in"
|
| 78 |
+
style={{ background: 'var(--surface)', borderColor: 'var(--border)' }}>
|
| 79 |
+
{APP_TYPES.map(type => (
|
| 80 |
+
<button
|
| 81 |
+
key={type.id}
|
| 82 |
+
onClick={() => { setAppType(type.id); setShowTypes(false) }}
|
| 83 |
+
className="text-left text-xs px-2.5 py-2 rounded-md transition-all hover:scale-[1.02]"
|
| 84 |
+
style={{
|
| 85 |
+
background: appType === type.id ? 'var(--accent)22' : 'transparent',
|
| 86 |
+
color: appType === type.id ? 'var(--accent)' : 'var(--text-secondary)',
|
| 87 |
+
}}
|
| 88 |
+
>
|
| 89 |
+
{type.emoji} {type.label}
|
| 90 |
+
</button>
|
| 91 |
+
))}
|
| 92 |
+
</div>
|
| 93 |
+
)}
|
| 94 |
+
</div>
|
| 95 |
+
|
| 96 |
+
{/* Example prompts */}
|
| 97 |
+
<div className="w-full space-y-2">
|
| 98 |
+
<p className="text-xs font-medium" style={{ color: 'var(--text-secondary)' }}>
|
| 99 |
+
Try an example:
|
| 100 |
+
</p>
|
| 101 |
+
{EXAMPLE_PROMPTS.map((prompt, i) => (
|
| 102 |
+
<button
|
| 103 |
+
key={i}
|
| 104 |
+
onClick={() => handleExampleClick(prompt)}
|
| 105 |
+
className="w-full text-left text-xs p-3 rounded-lg border transition-all hover:scale-[1.01]"
|
| 106 |
+
style={{
|
| 107 |
+
borderColor: 'var(--border)',
|
| 108 |
+
background: 'var(--surface)',
|
| 109 |
+
color: 'var(--text-secondary)',
|
| 110 |
+
}}
|
| 111 |
+
>
|
| 112 |
+
<Sparkles className="w-3 h-3 inline mr-1.5" style={{ color: 'var(--accent)' }} />
|
| 113 |
+
{prompt}
|
| 114 |
+
</button>
|
| 115 |
+
))}
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
) : (
|
| 119 |
+
/* Chat messages */
|
| 120 |
+
messages.map((msg, i) => (
|
| 121 |
+
<div
|
| 122 |
+
key={i}
|
| 123 |
+
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'} animate-slide-up`}
|
| 124 |
+
>
|
| 125 |
+
<div
|
| 126 |
+
className="max-w-[85%] rounded-2xl px-4 py-3 text-sm leading-relaxed"
|
| 127 |
+
style={{
|
| 128 |
+
background: msg.role === 'user' ? 'var(--accent)' : 'var(--surface)',
|
| 129 |
+
color: msg.role === 'user' ? 'white' : 'var(--text-primary)',
|
| 130 |
+
border: msg.role === 'user' ? 'none' : '1px solid var(--border)',
|
| 131 |
+
}}
|
| 132 |
+
>
|
| 133 |
+
<pre className="whitespace-pre-wrap font-sans">{msg.content}</pre>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
))
|
| 137 |
+
)}
|
| 138 |
+
|
| 139 |
+
{isGenerating && (
|
| 140 |
+
<div className="flex justify-start animate-slide-up">
|
| 141 |
+
<div className="rounded-2xl px-4 py-3 text-sm flex items-center gap-2"
|
| 142 |
+
style={{ background: 'var(--surface)', border: '1px solid var(--border)' }}>
|
| 143 |
+
<Loader2 className="w-4 h-4 animate-spin" style={{ color: 'var(--accent)' }} />
|
| 144 |
+
<span style={{ color: 'var(--text-secondary)' }}>Agents are working...</span>
|
| 145 |
+
<span className="typing-cursor" />
|
| 146 |
+
</div>
|
| 147 |
+
</div>
|
| 148 |
+
)}
|
| 149 |
+
|
| 150 |
+
<div ref={chatEndRef} />
|
| 151 |
+
</div>
|
| 152 |
+
|
| 153 |
+
{/* Input area */}
|
| 154 |
+
<form onSubmit={handleSubmit} className="p-3 border-t" style={{ borderColor: 'var(--border)' }}>
|
| 155 |
+
<div className="flex items-end gap-2">
|
| 156 |
+
<div className="flex-1 relative">
|
| 157 |
+
<textarea
|
| 158 |
+
ref={inputRef}
|
| 159 |
+
value={input}
|
| 160 |
+
onChange={(e) => setInput(e.target.value)}
|
| 161 |
+
onKeyDown={(e) => {
|
| 162 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 163 |
+
e.preventDefault()
|
| 164 |
+
handleSubmit(e)
|
| 165 |
+
}
|
| 166 |
+
}}
|
| 167 |
+
placeholder="Describe the app you want to build..."
|
| 168 |
+
rows={2}
|
| 169 |
+
className="w-full resize-none rounded-xl px-4 py-3 text-sm outline-none transition-all"
|
| 170 |
+
style={{
|
| 171 |
+
background: 'var(--surface)',
|
| 172 |
+
color: 'var(--text-primary)',
|
| 173 |
+
border: '1px solid var(--border)',
|
| 174 |
+
}}
|
| 175 |
+
disabled={isGenerating}
|
| 176 |
+
/>
|
| 177 |
+
</div>
|
| 178 |
+
<button
|
| 179 |
+
type="submit"
|
| 180 |
+
disabled={!input.trim() || isGenerating}
|
| 181 |
+
className="p-3 rounded-xl transition-all hover:scale-105 disabled:opacity-40 disabled:hover:scale-100"
|
| 182 |
+
style={{
|
| 183 |
+
background: 'var(--accent)',
|
| 184 |
+
color: 'white',
|
| 185 |
+
}}
|
| 186 |
+
>
|
| 187 |
+
{isGenerating
|
| 188 |
+
? <Loader2 className="w-5 h-5 animate-spin" />
|
| 189 |
+
: <Send className="w-5 h-5" />}
|
| 190 |
+
</button>
|
| 191 |
+
</div>
|
| 192 |
+
</form>
|
| 193 |
+
</div>
|
| 194 |
+
)
|
| 195 |
+
}
|
fronend/src/components/CodeView.jsx
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useMemo } from 'react'
|
| 2 |
+
import { Copy, Check } from 'lucide-react'
|
| 3 |
+
|
| 4 |
+
// Basic syntax highlighting without external deps
|
| 5 |
+
function highlightCode(code, ext) {
|
| 6 |
+
if (!code) return ''
|
| 7 |
+
|
| 8 |
+
// Simple keyword-based highlighting
|
| 9 |
+
const keywords = {
|
| 10 |
+
js: /\b(const|let|var|function|return|if|else|for|while|class|import|export|from|default|async|await|try|catch|throw|new|this|typeof|instanceof|switch|case|break|continue)\b/g,
|
| 11 |
+
py: /\b(def|class|import|from|return|if|elif|else|for|while|try|except|raise|with|as|yield|async|await|lambda|pass|break|continue|and|or|not|in|is|True|False|None)\b/g,
|
| 12 |
+
sql: /\b(SELECT|FROM|WHERE|INSERT|INTO|VALUES|UPDATE|SET|DELETE|CREATE|TABLE|ALTER|DROP|INDEX|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AND|OR|NOT|NULL|PRIMARY|KEY|FOREIGN|REFERENCES|CASCADE|UNIQUE|DEFAULT|CHECK|CONSTRAINT|GRANT|REVOKE|BEGIN|COMMIT|ROLLBACK|TRIGGER|FUNCTION|PROCEDURE|VIEW|ENABLE|ROW|LEVEL|SECURITY|POLICY|USING|WITH)\b/gi,
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const lang = ['jsx', 'tsx', 'js', 'ts'].includes(ext) ? 'js'
|
| 16 |
+
: ['py'].includes(ext) ? 'py'
|
| 17 |
+
: ['sql'].includes(ext) ? 'sql'
|
| 18 |
+
: 'js'
|
| 19 |
+
|
| 20 |
+
let highlighted = code
|
| 21 |
+
.replace(/&/g, '&')
|
| 22 |
+
.replace(/</g, '<')
|
| 23 |
+
.replace(/>/g, '>')
|
| 24 |
+
|
| 25 |
+
// Strings
|
| 26 |
+
highlighted = highlighted.replace(
|
| 27 |
+
/(["'`])(?:(?=(\\?))\2[\s\S])*?\1/g,
|
| 28 |
+
'<span style="color:#22D3A8">$&</span>'
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
// Comments
|
| 32 |
+
highlighted = highlighted.replace(
|
| 33 |
+
/(\/\/.*$|#.*$)/gm,
|
| 34 |
+
'<span style="color:#555577">$&</span>'
|
| 35 |
+
)
|
| 36 |
+
highlighted = highlighted.replace(
|
| 37 |
+
/(\/\*[\s\S]*?\*\/|--.*$)/gm,
|
| 38 |
+
'<span style="color:#555577">$&</span>'
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
// Keywords
|
| 42 |
+
if (keywords[lang]) {
|
| 43 |
+
highlighted = highlighted.replace(
|
| 44 |
+
keywords[lang],
|
| 45 |
+
'<span style="color:#6C63FF;font-weight:600">$&</span>'
|
| 46 |
+
)
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Numbers
|
| 50 |
+
highlighted = highlighted.replace(
|
| 51 |
+
/\b(\d+\.?\d*)\b/g,
|
| 52 |
+
'<span style="color:#FFB547">$&</span>'
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
return highlighted
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
export default function CodeView({ content, fileName }) {
|
| 59 |
+
const [copied, setCopied] = React.useState(false)
|
| 60 |
+
|
| 61 |
+
const ext = fileName?.split('.').pop()?.toLowerCase() || ''
|
| 62 |
+
const highlighted = useMemo(
|
| 63 |
+
() => highlightCode(content || '', ext),
|
| 64 |
+
[content, ext]
|
| 65 |
+
)
|
| 66 |
+
const lineCount = (content || '').split('\n').length
|
| 67 |
+
|
| 68 |
+
const handleCopy = async () => {
|
| 69 |
+
if (!content) return
|
| 70 |
+
await navigator.clipboard.writeText(content)
|
| 71 |
+
setCopied(true)
|
| 72 |
+
setTimeout(() => setCopied(false), 2000)
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
if (!content) {
|
| 76 |
+
return (
|
| 77 |
+
<div className="w-full h-full flex items-center justify-center">
|
| 78 |
+
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
| 79 |
+
Select a file from the tree to view its code
|
| 80 |
+
</p>
|
| 81 |
+
</div>
|
| 82 |
+
)
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
return (
|
| 86 |
+
<div className="w-full h-full flex flex-col rounded-lg overflow-hidden border"
|
| 87 |
+
style={{ borderColor: 'var(--border)' }}>
|
| 88 |
+
{/* File header */}
|
| 89 |
+
<div className="flex items-center justify-between px-4 py-2 border-b"
|
| 90 |
+
style={{ background: 'var(--bg)', borderColor: 'var(--border)' }}>
|
| 91 |
+
<span className="text-xs font-mono" style={{ color: 'var(--text-secondary)' }}>
|
| 92 |
+
{fileName}
|
| 93 |
+
</span>
|
| 94 |
+
<div className="flex items-center gap-2">
|
| 95 |
+
<span className="text-[10px]" style={{ color: 'var(--text-secondary)' }}>
|
| 96 |
+
{lineCount} lines
|
| 97 |
+
</span>
|
| 98 |
+
<button
|
| 99 |
+
onClick={handleCopy}
|
| 100 |
+
className="p-1 rounded transition-all hover:scale-110"
|
| 101 |
+
style={{ color: copied ? 'var(--success)' : 'var(--text-secondary)' }}
|
| 102 |
+
title="Copy code"
|
| 103 |
+
>
|
| 104 |
+
{copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
|
| 105 |
+
</button>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
{/* Code content */}
|
| 110 |
+
<div className="flex-1 overflow-auto" style={{ background: '#0D0D14' }}>
|
| 111 |
+
<div className="flex">
|
| 112 |
+
{/* Line numbers */}
|
| 113 |
+
<div className="flex-shrink-0 text-right pr-4 pl-4 py-3 select-none"
|
| 114 |
+
style={{ color: '#333355', fontSize: '12px', lineHeight: '1.6' }}>
|
| 115 |
+
{Array.from({ length: lineCount }, (_, i) => (
|
| 116 |
+
<div key={i}>{i + 1}</div>
|
| 117 |
+
))}
|
| 118 |
+
</div>
|
| 119 |
+
{/* Code */}
|
| 120 |
+
<pre className="flex-1 py-3 pr-4 overflow-x-auto"
|
| 121 |
+
style={{ fontSize: '12px', lineHeight: '1.6' }}>
|
| 122 |
+
<code dangerouslySetInnerHTML={{ __html: highlighted }} />
|
| 123 |
+
</pre>
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
)
|
| 128 |
+
}
|
fronend/src/components/ControlPanel.jsx
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react'
|
| 2 |
+
import ChatInterface from './ChatInterface'
|
| 3 |
+
import AgentFeed from './AgentFeed'
|
| 4 |
+
import FileTree from './FileTree'
|
| 5 |
+
import { MessageSquare, Activity, FolderTree } from 'lucide-react'
|
| 6 |
+
|
| 7 |
+
const TABS = [
|
| 8 |
+
{ id: 'chat', label: 'Chat', icon: MessageSquare },
|
| 9 |
+
{ id: 'agents', label: 'Agents', icon: Activity },
|
| 10 |
+
{ id: 'files', label: 'Files', icon: FolderTree },
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
export default function ControlPanel({
|
| 14 |
+
messages, agentFeed, fileTree, files, selectedFile,
|
| 15 |
+
onSelectFile, onGenerate, onFix, status
|
| 16 |
+
}) {
|
| 17 |
+
const [activeTab, setActiveTab] = useState('chat')
|
| 18 |
+
|
| 19 |
+
return (
|
| 20 |
+
<div className="w-full md:w-[420px] lg:w-[480px] flex flex-col border-r"
|
| 21 |
+
style={{ borderColor: 'var(--border)', background: 'var(--bg)' }}>
|
| 22 |
+
{/* Tab bar */}
|
| 23 |
+
<div className="flex border-b" style={{ borderColor: 'var(--border)' }}>
|
| 24 |
+
{TABS.map(tab => {
|
| 25 |
+
const Icon = tab.icon
|
| 26 |
+
const isActive = activeTab === tab.id
|
| 27 |
+
return (
|
| 28 |
+
<button
|
| 29 |
+
key={tab.id}
|
| 30 |
+
onClick={() => setActiveTab(tab.id)}
|
| 31 |
+
className="flex-1 flex items-center justify-center gap-2 py-2.5 text-xs font-medium transition-all relative"
|
| 32 |
+
style={{
|
| 33 |
+
color: isActive ? 'var(--accent)' : 'var(--text-secondary)',
|
| 34 |
+
background: isActive ? 'var(--surface)' : 'transparent',
|
| 35 |
+
}}
|
| 36 |
+
>
|
| 37 |
+
<Icon className="w-3.5 h-3.5" />
|
| 38 |
+
{tab.label}
|
| 39 |
+
{tab.id === 'agents' && agentFeed.length > 0 && (
|
| 40 |
+
<span className="ml-1 w-4 h-4 text-[10px] flex items-center justify-center rounded-full"
|
| 41 |
+
style={{ background: 'var(--accent)33', color: 'var(--accent)' }}>
|
| 42 |
+
{agentFeed.length}
|
| 43 |
+
</span>
|
| 44 |
+
)}
|
| 45 |
+
{tab.id === 'files' && fileTree.length > 0 && (
|
| 46 |
+
<span className="ml-1 w-4 h-4 text-[10px] flex items-center justify-center rounded-full"
|
| 47 |
+
style={{ background: 'var(--success)33', color: 'var(--success)' }}>
|
| 48 |
+
{fileTree.length}
|
| 49 |
+
</span>
|
| 50 |
+
)}
|
| 51 |
+
{isActive && (
|
| 52 |
+
<div className="absolute bottom-0 left-0 right-0 h-0.5"
|
| 53 |
+
style={{ background: 'var(--accent)' }} />
|
| 54 |
+
)}
|
| 55 |
+
</button>
|
| 56 |
+
)
|
| 57 |
+
})}
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
{/* Tab content */}
|
| 61 |
+
<div className="flex-1 overflow-hidden">
|
| 62 |
+
{activeTab === 'chat' && (
|
| 63 |
+
<ChatInterface
|
| 64 |
+
messages={messages}
|
| 65 |
+
onGenerate={onGenerate}
|
| 66 |
+
onFix={onFix}
|
| 67 |
+
status={status}
|
| 68 |
+
/>
|
| 69 |
+
)}
|
| 70 |
+
{activeTab === 'agents' && (
|
| 71 |
+
<AgentFeed feed={agentFeed} />
|
| 72 |
+
)}
|
| 73 |
+
{activeTab === 'files' && (
|
| 74 |
+
<FileTree
|
| 75 |
+
tree={fileTree}
|
| 76 |
+
files={files}
|
| 77 |
+
selectedFile={selectedFile}
|
| 78 |
+
onSelect={onSelectFile}
|
| 79 |
+
/>
|
| 80 |
+
)}
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
)
|
| 84 |
+
}
|
fronend/src/components/FileTree.jsx
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useMemo } from 'react'
|
| 2 |
+
import {
|
| 3 |
+
Folder, FolderOpen, FileCode2, FileJson, FileType2,
|
| 4 |
+
Database, FileText, Settings
|
| 5 |
+
} from 'lucide-react'
|
| 6 |
+
|
| 7 |
+
const FILE_ICONS = {
|
| 8 |
+
jsx: { icon: FileCode2, color: '#61DAFB' },
|
| 9 |
+
tsx: { icon: FileCode2, color: '#3178C6' },
|
| 10 |
+
js: { icon: FileCode2, color: '#F7DF1E' },
|
| 11 |
+
ts: { icon: FileCode2, color: '#3178C6' },
|
| 12 |
+
py: { icon: FileCode2, color: '#3776AB' },
|
| 13 |
+
css: { icon: FileType2, color: '#1572B6' },
|
| 14 |
+
html: { icon: FileCode2, color: '#E34F26' },
|
| 15 |
+
json: { icon: FileJson, color: '#A8B9CC' },
|
| 16 |
+
sql: { icon: Database, color: '#336791' },
|
| 17 |
+
md: { icon: FileText, color: '#083FA1' },
|
| 18 |
+
yml: { icon: Settings, color: '#CB171E' },
|
| 19 |
+
yaml: { icon: Settings, color: '#CB171E' },
|
| 20 |
+
env: { icon: Settings, color: '#ECD53F' },
|
| 21 |
+
txt: { icon: FileText, color: '#8888AA' },
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
function buildTree(paths) {
|
| 25 |
+
const root = {}
|
| 26 |
+
for (const path of paths) {
|
| 27 |
+
const parts = path.split('/')
|
| 28 |
+
let current = root
|
| 29 |
+
for (let i = 0; i < parts.length; i++) {
|
| 30 |
+
const part = parts[i]
|
| 31 |
+
if (i === parts.length - 1) {
|
| 32 |
+
current[part] = path // leaf = full path
|
| 33 |
+
} else {
|
| 34 |
+
if (!current[part] || typeof current[part] === 'string') {
|
| 35 |
+
current[part] = {}
|
| 36 |
+
}
|
| 37 |
+
current = current[part]
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
return root
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
function TreeNode({ name, node, selectedFile, onSelect, depth = 0 }) {
|
| 45 |
+
const [open, setOpen] = React.useState(depth < 2)
|
| 46 |
+
const isFile = typeof node === 'string'
|
| 47 |
+
const isSelected = isFile && node === selectedFile
|
| 48 |
+
|
| 49 |
+
const ext = isFile ? name.split('.').pop()?.toLowerCase() : ''
|
| 50 |
+
const fileConfig = FILE_ICONS[ext] || { icon: FileText, color: 'var(--text-secondary)' }
|
| 51 |
+
const Icon = isFile ? fileConfig.icon : (open ? FolderOpen : Folder)
|
| 52 |
+
const iconColor = isFile ? fileConfig.color : 'var(--accent)'
|
| 53 |
+
|
| 54 |
+
if (isFile) {
|
| 55 |
+
return (
|
| 56 |
+
<button
|
| 57 |
+
onClick={() => onSelect(node)}
|
| 58 |
+
className="w-full flex items-center gap-2 py-1 px-2 rounded text-xs transition-all hover:scale-[1.01] text-left"
|
| 59 |
+
style={{
|
| 60 |
+
paddingLeft: `${depth * 16 + 8}px`,
|
| 61 |
+
background: isSelected ? 'var(--accent)15' : 'transparent',
|
| 62 |
+
color: isSelected ? 'var(--accent)' : 'var(--text-secondary)',
|
| 63 |
+
}}
|
| 64 |
+
>
|
| 65 |
+
<Icon className="w-3.5 h-3.5 flex-shrink-0" style={{ color: iconColor }} />
|
| 66 |
+
<span className="truncate font-mono">{name}</span>
|
| 67 |
+
</button>
|
| 68 |
+
)
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
const entries = Object.entries(node).sort(([a, av], [b, bv]) => {
|
| 72 |
+
const aIsFile = typeof av === 'string'
|
| 73 |
+
const bIsFile = typeof bv === 'string'
|
| 74 |
+
if (aIsFile !== bIsFile) return aIsFile ? 1 : -1
|
| 75 |
+
return a.localeCompare(b)
|
| 76 |
+
})
|
| 77 |
+
|
| 78 |
+
return (
|
| 79 |
+
<div>
|
| 80 |
+
<button
|
| 81 |
+
onClick={() => setOpen(!open)}
|
| 82 |
+
className="w-full flex items-center gap-2 py-1 px-2 rounded text-xs font-medium transition-all text-left"
|
| 83 |
+
style={{
|
| 84 |
+
paddingLeft: `${depth * 16 + 8}px`,
|
| 85 |
+
color: 'var(--text-primary)',
|
| 86 |
+
}}
|
| 87 |
+
>
|
| 88 |
+
<Icon className="w-3.5 h-3.5 flex-shrink-0" style={{ color: iconColor }} />
|
| 89 |
+
<span className="truncate">{name}</span>
|
| 90 |
+
</button>
|
| 91 |
+
{open && entries.map(([childName, childNode]) => (
|
| 92 |
+
<TreeNode
|
| 93 |
+
key={childName}
|
| 94 |
+
name={childName}
|
| 95 |
+
node={childNode}
|
| 96 |
+
selectedFile={selectedFile}
|
| 97 |
+
onSelect={onSelect}
|
| 98 |
+
depth={depth + 1}
|
| 99 |
+
/>
|
| 100 |
+
))}
|
| 101 |
+
</div>
|
| 102 |
+
)
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
export default function FileTree({ tree, files, selectedFile, onSelect }) {
|
| 106 |
+
const treeStructure = useMemo(() => buildTree(tree), [tree])
|
| 107 |
+
|
| 108 |
+
if (tree.length === 0) {
|
| 109 |
+
return (
|
| 110 |
+
<div className="flex flex-col items-center justify-center h-full text-center px-6">
|
| 111 |
+
<Folder className="w-12 h-12 mb-3" style={{ color: 'var(--text-secondary)', opacity: 0.4 }} />
|
| 112 |
+
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
| 113 |
+
Generated files will appear here.
|
| 114 |
+
</p>
|
| 115 |
+
</div>
|
| 116 |
+
)
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
return (
|
| 120 |
+
<div className="h-full overflow-y-auto py-2">
|
| 121 |
+
<div className="px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider"
|
| 122 |
+
style={{ color: 'var(--text-secondary)' }}>
|
| 123 |
+
Project Files ({tree.length})
|
| 124 |
+
</div>
|
| 125 |
+
{Object.entries(treeStructure).sort(([a, av], [b, bv]) => {
|
| 126 |
+
const aIsFile = typeof av === 'string'
|
| 127 |
+
const bIsFile = typeof bv === 'string'
|
| 128 |
+
if (aIsFile !== bIsFile) return aIsFile ? 1 : -1
|
| 129 |
+
return a.localeCompare(b)
|
| 130 |
+
}).map(([name, node]) => (
|
| 131 |
+
<TreeNode
|
| 132 |
+
key={name}
|
| 133 |
+
name={name}
|
| 134 |
+
node={node}
|
| 135 |
+
selectedFile={selectedFile}
|
| 136 |
+
onSelect={onSelect}
|
| 137 |
+
/>
|
| 138 |
+
))}
|
| 139 |
+
</div>
|
| 140 |
+
)
|
| 141 |
+
}
|
fronend/src/components/Header.jsx
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import { useTheme } from './ThemeProvider'
|
| 3 |
+
import { Sun, Moon, Settings, Zap } from 'lucide-react'
|
| 4 |
+
|
| 5 |
+
const AGENT_LABELS = {
|
| 6 |
+
research: { icon: '🌐', label: 'Research' },
|
| 7 |
+
orchestrator: { icon: '🧠', label: 'Orchestrator' },
|
| 8 |
+
frontend: { icon: '🎨', label: 'Frontend' },
|
| 9 |
+
backend: { icon: '🔐', label: 'Backend' },
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const STATUS_COLORS = {
|
| 13 |
+
idle: 'bg-gray-500',
|
| 14 |
+
active: 'bg-cyan-400 agent-active',
|
| 15 |
+
done: 'bg-emerald-400',
|
| 16 |
+
error: 'bg-red-400',
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export default function Header({ agents, sessionId, status }) {
|
| 20 |
+
const { theme, toggleTheme } = useTheme()
|
| 21 |
+
|
| 22 |
+
return (
|
| 23 |
+
<header className="glass flex items-center justify-between px-4 py-2.5 border-b z-50"
|
| 24 |
+
style={{ borderColor: 'var(--border)' }}>
|
| 25 |
+
{/* Logo */}
|
| 26 |
+
<div className="flex items-center gap-3">
|
| 27 |
+
<div className="flex items-center gap-2">
|
| 28 |
+
<div className="relative">
|
| 29 |
+
<Zap className="w-6 h-6" style={{ color: 'var(--accent)' }} />
|
| 30 |
+
<div className="absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full bg-emerald-400 agent-active" />
|
| 31 |
+
</div>
|
| 32 |
+
<span className="text-xl font-bold tracking-tight font-mono gradient-text">
|
| 33 |
+
NEXUS
|
| 34 |
+
</span>
|
| 35 |
+
</div>
|
| 36 |
+
<span className="text-xs px-2 py-0.5 rounded-full font-medium"
|
| 37 |
+
style={{ background: 'var(--accent)22', color: 'var(--accent)' }}>
|
| 38 |
+
BUILDER
|
| 39 |
+
</span>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
{/* Agent Status Indicators */}
|
| 43 |
+
<div className="hidden md:flex items-center gap-4">
|
| 44 |
+
{Object.entries(agents).map(([key, agent]) => (
|
| 45 |
+
<div key={key} className="flex items-center gap-2 text-xs" title={`${agent.name}: ${agent.status}`}>
|
| 46 |
+
<span>{AGENT_LABELS[key]?.icon}</span>
|
| 47 |
+
<div className={`w-2 h-2 rounded-full ${STATUS_COLORS[agent.status] || STATUS_COLORS.idle}`} />
|
| 48 |
+
<span style={{ color: 'var(--text-secondary)' }}>
|
| 49 |
+
{AGENT_LABELS[key]?.label}
|
| 50 |
+
</span>
|
| 51 |
+
</div>
|
| 52 |
+
))}
|
| 53 |
+
</div>
|
| 54 |
+
|
| 55 |
+
{/* Right controls */}
|
| 56 |
+
<div className="flex items-center gap-3">
|
| 57 |
+
{sessionId && (
|
| 58 |
+
<span className="text-xs font-mono px-2 py-1 rounded"
|
| 59 |
+
style={{ background: 'var(--surface)', color: 'var(--text-secondary)', border: '1px solid var(--border)' }}>
|
| 60 |
+
{sessionId}
|
| 61 |
+
</span>
|
| 62 |
+
)}
|
| 63 |
+
|
| 64 |
+
<button
|
| 65 |
+
onClick={toggleTheme}
|
| 66 |
+
className="p-2 rounded-lg transition-all hover:scale-110"
|
| 67 |
+
style={{ color: 'var(--text-secondary)' }}
|
| 68 |
+
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
| 69 |
+
>
|
| 70 |
+
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
| 71 |
+
</button>
|
| 72 |
+
|
| 73 |
+
<button className="p-2 rounded-lg transition-all"
|
| 74 |
+
style={{ color: 'var(--text-secondary)' }}>
|
| 75 |
+
<Settings className="w-4 h-4" />
|
| 76 |
+
</button>
|
| 77 |
+
</div>
|
| 78 |
+
</header>
|
| 79 |
+
)
|
| 80 |
+
}
|
fronend/src/components/LivePreview.jsx
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react'
|
| 2 |
+
import SystemTabs from './SystemTabs'
|
| 3 |
+
import CodeView from './CodeView'
|
| 4 |
+
import {
|
| 5 |
+
Eye, Code2, Download, Monitor, Tablet, Smartphone,
|
| 6 |
+
ExternalLink, RefreshCw, Maximize2
|
| 7 |
+
} from 'lucide-react'
|
| 8 |
+
|
| 9 |
+
const VIEWPORT_SIZES = {
|
| 10 |
+
desktop: { width: '100%', label: 'Desktop', icon: Monitor },
|
| 11 |
+
tablet: { width: '768px', label: 'Tablet', icon: Tablet },
|
| 12 |
+
mobile: { width: '375px', label: 'Mobile', icon: Smartphone },
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default function LivePreview({
|
| 16 |
+
sessionId, files, selectedFile, previewSystem, onChangeSystem,
|
| 17 |
+
viewMode, onChangeViewMode, onExport, status
|
| 18 |
+
}) {
|
| 19 |
+
const [viewport, setViewport] = useState('desktop')
|
| 20 |
+
const [iframeKey, setIframeKey] = useState(0)
|
| 21 |
+
|
| 22 |
+
const previewUrl = sessionId
|
| 23 |
+
? (previewSystem === 'preview'
|
| 24 |
+
? `/api/preview/${sessionId}`
|
| 25 |
+
: `/api/preview/${sessionId}/${previewSystem}`)
|
| 26 |
+
: null
|
| 27 |
+
|
| 28 |
+
const selectedFileContent = selectedFile && files[selectedFile]
|
| 29 |
+
? files[selectedFile]
|
| 30 |
+
: null
|
| 31 |
+
|
| 32 |
+
const showCode = viewMode === 'code' || selectedFileContent
|
| 33 |
+
|
| 34 |
+
return (
|
| 35 |
+
<div className="flex-1 flex flex-col overflow-hidden" style={{ background: 'var(--bg)' }}>
|
| 36 |
+
{/* System tabs */}
|
| 37 |
+
<SystemTabs active={previewSystem} onChange={onChangeSystem} />
|
| 38 |
+
|
| 39 |
+
{/* Toolbar */}
|
| 40 |
+
<div className="flex items-center justify-between px-3 py-2 border-b"
|
| 41 |
+
style={{ borderColor: 'var(--border)' }}>
|
| 42 |
+
<div className="flex items-center gap-1">
|
| 43 |
+
{/* View mode toggle */}
|
| 44 |
+
<button
|
| 45 |
+
onClick={() => onChangeViewMode('preview')}
|
| 46 |
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all"
|
| 47 |
+
style={{
|
| 48 |
+
background: viewMode === 'preview' ? 'var(--accent)15' : 'transparent',
|
| 49 |
+
color: viewMode === 'preview' ? 'var(--accent)' : 'var(--text-secondary)',
|
| 50 |
+
}}
|
| 51 |
+
>
|
| 52 |
+
<Eye className="w-3.5 h-3.5" />
|
| 53 |
+
Preview
|
| 54 |
+
</button>
|
| 55 |
+
<button
|
| 56 |
+
onClick={() => onChangeViewMode('code')}
|
| 57 |
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all"
|
| 58 |
+
style={{
|
| 59 |
+
background: viewMode === 'code' ? 'var(--accent)15' : 'transparent',
|
| 60 |
+
color: viewMode === 'code' ? 'var(--accent)' : 'var(--text-secondary)',
|
| 61 |
+
}}
|
| 62 |
+
>
|
| 63 |
+
<Code2 className="w-3.5 h-3.5" />
|
| 64 |
+
Code
|
| 65 |
+
</button>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<div className="flex items-center gap-1">
|
| 69 |
+
{/* Viewport toggle */}
|
| 70 |
+
{viewMode === 'preview' && Object.entries(VIEWPORT_SIZES).map(([key, val]) => {
|
| 71 |
+
const Icon = val.icon
|
| 72 |
+
return (
|
| 73 |
+
<button
|
| 74 |
+
key={key}
|
| 75 |
+
onClick={() => setViewport(key)}
|
| 76 |
+
className="p-1.5 rounded-md transition-all"
|
| 77 |
+
style={{
|
| 78 |
+
color: viewport === key ? 'var(--accent)' : 'var(--text-secondary)',
|
| 79 |
+
background: viewport === key ? 'var(--accent)11' : 'transparent',
|
| 80 |
+
}}
|
| 81 |
+
title={val.label}
|
| 82 |
+
>
|
| 83 |
+
<Icon className="w-3.5 h-3.5" />
|
| 84 |
+
</button>
|
| 85 |
+
)
|
| 86 |
+
})}
|
| 87 |
+
|
| 88 |
+
{/* Refresh */}
|
| 89 |
+
<button
|
| 90 |
+
onClick={() => setIframeKey(k => k + 1)}
|
| 91 |
+
className="p-1.5 rounded-md transition-all"
|
| 92 |
+
style={{ color: 'var(--text-secondary)' }}
|
| 93 |
+
title="Refresh preview"
|
| 94 |
+
>
|
| 95 |
+
<RefreshCw className="w-3.5 h-3.5" />
|
| 96 |
+
</button>
|
| 97 |
+
|
| 98 |
+
{/* Export */}
|
| 99 |
+
{sessionId && (
|
| 100 |
+
<button
|
| 101 |
+
onClick={onExport}
|
| 102 |
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all hover:scale-105"
|
| 103 |
+
style={{ background: 'var(--accent)', color: 'white' }}
|
| 104 |
+
>
|
| 105 |
+
<Download className="w-3.5 h-3.5" />
|
| 106 |
+
Export ZIP
|
| 107 |
+
</button>
|
| 108 |
+
)}
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
{/* Preview area */}
|
| 113 |
+
<div className="flex-1 overflow-hidden flex items-center justify-center p-4"
|
| 114 |
+
style={{ background: viewMode === 'code' ? 'var(--surface)' : '#0D0D12' }}>
|
| 115 |
+
{showCode ? (
|
| 116 |
+
<CodeView
|
| 117 |
+
content={selectedFileContent || _getFirstFile(files)}
|
| 118 |
+
fileName={selectedFile || Object.keys(files)[0] || ''}
|
| 119 |
+
/>
|
| 120 |
+
) : previewUrl ? (
|
| 121 |
+
<div
|
| 122 |
+
className="h-full rounded-lg overflow-hidden border transition-all duration-300"
|
| 123 |
+
style={{
|
| 124 |
+
width: VIEWPORT_SIZES[viewport].width,
|
| 125 |
+
maxWidth: '100%',
|
| 126 |
+
borderColor: 'var(--border)',
|
| 127 |
+
}}
|
| 128 |
+
>
|
| 129 |
+
<iframe
|
| 130 |
+
key={iframeKey}
|
| 131 |
+
src={previewUrl}
|
| 132 |
+
className="w-full h-full border-0"
|
| 133 |
+
style={{ background: 'white', borderRadius: '8px' }}
|
| 134 |
+
title="App Preview"
|
| 135 |
+
sandbox="allow-scripts allow-same-origin"
|
| 136 |
+
/>
|
| 137 |
+
</div>
|
| 138 |
+
) : (
|
| 139 |
+
/* Empty state */
|
| 140 |
+
<div className="text-center">
|
| 141 |
+
<div className="text-6xl mb-4 opacity-20">🖥️</div>
|
| 142 |
+
<h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
|
| 143 |
+
Live Preview
|
| 144 |
+
</h3>
|
| 145 |
+
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
| 146 |
+
Your generated app will be previewed here in real-time.
|
| 147 |
+
<br />Start by describing your app idea in the chat.
|
| 148 |
+
</p>
|
| 149 |
+
</div>
|
| 150 |
+
)}
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
)
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
function _getFirstFile(files) {
|
| 157 |
+
const keys = Object.keys(files)
|
| 158 |
+
return keys.length > 0 ? files[keys[0]] : ''
|
| 159 |
+
}
|
fronend/src/components/StatusBar.jsx
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import { Wifi, WifiOff, AlertTriangle, Files, Cpu } from 'lucide-react'
|
| 3 |
+
|
| 4 |
+
const STATUS_LABELS = {
|
| 5 |
+
idle: { label: 'Ready', color: 'var(--text-secondary)' },
|
| 6 |
+
starting: { label: 'Starting...', color: 'var(--warning)' },
|
| 7 |
+
connected: { label: 'Connected', color: 'var(--success)' },
|
| 8 |
+
researching: { label: '🔍 Researching...', color: 'var(--accent-2)' },
|
| 9 |
+
orchestrating: { label: '🧠 Creating Blueprint...', color: 'var(--accent)' },
|
| 10 |
+
building: { label: '🚀 Building...', color: 'var(--accent)' },
|
| 11 |
+
building_frontend: { label: '🎨 Building Frontend...', color: 'var(--success)' },
|
| 12 |
+
building_backend: { label: '🔐 Building Backend...', color: 'var(--warning)' },
|
| 13 |
+
merging: { label: '📦 Merging...', color: 'var(--accent-2)' },
|
| 14 |
+
fixing: { label: '🔧 Fixing...', color: 'var(--warning)' },
|
| 15 |
+
completed: { label: '✅ Complete', color: 'var(--success)' },
|
| 16 |
+
error: { label: '❌ Error', color: 'var(--error)' },
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export default function StatusBar({ status, sessionId, agents, errorCount, fileCount }) {
|
| 20 |
+
const statusConfig = STATUS_LABELS[status] || STATUS_LABELS.idle
|
| 21 |
+
const isActive = !['idle', 'completed', 'error'].includes(status)
|
| 22 |
+
|
| 23 |
+
return (
|
| 24 |
+
<footer
|
| 25 |
+
className="flex items-center justify-between px-4 py-1.5 text-[11px] border-t"
|
| 26 |
+
style={{
|
| 27 |
+
borderColor: 'var(--border)',
|
| 28 |
+
background: 'var(--surface)',
|
| 29 |
+
color: 'var(--text-secondary)',
|
| 30 |
+
}}
|
| 31 |
+
>
|
| 32 |
+
{/* Left: status */}
|
| 33 |
+
<div className="flex items-center gap-3">
|
| 34 |
+
<div className="flex items-center gap-1.5">
|
| 35 |
+
{isActive ? (
|
| 36 |
+
<Wifi className="w-3 h-3" style={{ color: 'var(--success)' }} />
|
| 37 |
+
) : (
|
| 38 |
+
<WifiOff className="w-3 h-3" />
|
| 39 |
+
)}
|
| 40 |
+
<span style={{ color: statusConfig.color, fontWeight: 500 }}>
|
| 41 |
+
{statusConfig.label}
|
| 42 |
+
</span>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
{/* Build progress */}
|
| 46 |
+
{isActive && (
|
| 47 |
+
<div className="hidden sm:flex items-center gap-1">
|
| 48 |
+
{['researching', 'orchestrating', 'building', 'merging'].map((step, i) => {
|
| 49 |
+
const stepStatuses = {
|
| 50 |
+
researching: ['researching'],
|
| 51 |
+
orchestrating: ['orchestrating'],
|
| 52 |
+
building: ['building', 'building_frontend', 'building_backend'],
|
| 53 |
+
merging: ['merging', 'completed'],
|
| 54 |
+
}
|
| 55 |
+
const isCurrentOrPast = stepStatuses[step]?.includes(status) ||
|
| 56 |
+
['researching', 'orchestrating', 'building', 'merging'].indexOf(step) <
|
| 57 |
+
['researching', 'orchestrating', 'building', 'merging'].findIndex(s =>
|
| 58 |
+
stepStatuses[s]?.includes(status)
|
| 59 |
+
)
|
| 60 |
+
return (
|
| 61 |
+
<React.Fragment key={step}>
|
| 62 |
+
<div
|
| 63 |
+
className="w-1.5 h-1.5 rounded-full"
|
| 64 |
+
style={{
|
| 65 |
+
background: stepStatuses[step]?.includes(status)
|
| 66 |
+
? 'var(--accent)'
|
| 67 |
+
: isCurrentOrPast ? 'var(--success)' : 'var(--border)',
|
| 68 |
+
}}
|
| 69 |
+
/>
|
| 70 |
+
{i < 3 && (
|
| 71 |
+
<div className="w-4 h-px" style={{ background: 'var(--border)' }} />
|
| 72 |
+
)}
|
| 73 |
+
</React.Fragment>
|
| 74 |
+
)
|
| 75 |
+
})}
|
| 76 |
+
</div>
|
| 77 |
+
)}
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
{/* Right: metrics */}
|
| 81 |
+
<div className="flex items-center gap-4">
|
| 82 |
+
{fileCount > 0 && (
|
| 83 |
+
<div className="flex items-center gap-1">
|
| 84 |
+
<Files className="w-3 h-3" />
|
| 85 |
+
<span>{fileCount} files</span>
|
| 86 |
+
</div>
|
| 87 |
+
)}
|
| 88 |
+
{errorCount > 0 && (
|
| 89 |
+
<div className="flex items-center gap-1" style={{ color: 'var(--error)' }}>
|
| 90 |
+
<AlertTriangle className="w-3 h-3" />
|
| 91 |
+
<span>{errorCount} errors</span>
|
| 92 |
+
</div>
|
| 93 |
+
)}
|
| 94 |
+
<div className="flex items-center gap-1">
|
| 95 |
+
<Cpu className="w-3 h-3" />
|
| 96 |
+
<span>HuggingFace CPU</span>
|
| 97 |
+
</div>
|
| 98 |
+
{sessionId && (
|
| 99 |
+
<span className="font-mono">{sessionId}</span>
|
| 100 |
+
)}
|
| 101 |
+
</div>
|
| 102 |
+
</footer>
|
| 103 |
+
)
|
| 104 |
+
}
|
fronend/src/components/SystemTabs.jsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import { Layout, Globe, Megaphone, BarChart3, Shield } from 'lucide-react'
|
| 3 |
+
|
| 4 |
+
const SYSTEMS = [
|
| 5 |
+
{ id: 'preview', label: 'Overview', icon: Layout },
|
| 6 |
+
{ id: 'client_portal', label: 'Portal', icon: Layout },
|
| 7 |
+
{ id: 'public_landing', label: 'Landing', icon: Globe },
|
| 8 |
+
{ id: 'marketing_cms', label: 'Marketing', icon: Megaphone },
|
| 9 |
+
{ id: 'analytics_dashboard', label: 'Analytics', icon: BarChart3 },
|
| 10 |
+
{ id: 'admin_panel', label: 'Admin', icon: Shield },
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
export default function SystemTabs({ active, onChange }) {
|
| 14 |
+
return (
|
| 15 |
+
<div className="flex items-center gap-0.5 overflow-x-auto px-2 py-1.5 border-b"
|
| 16 |
+
style={{ borderColor: 'var(--border)' }}>
|
| 17 |
+
{SYSTEMS.map(sys => {
|
| 18 |
+
const Icon = sys.icon
|
| 19 |
+
const isActive = active === sys.id
|
| 20 |
+
return (
|
| 21 |
+
<button
|
| 22 |
+
key={sys.id}
|
| 23 |
+
onClick={() => onChange(sys.id)}
|
| 24 |
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium whitespace-nowrap transition-all"
|
| 25 |
+
style={{
|
| 26 |
+
background: isActive ? 'var(--accent)15' : 'transparent',
|
| 27 |
+
color: isActive ? 'var(--accent)' : 'var(--text-secondary)',
|
| 28 |
+
}}
|
| 29 |
+
>
|
| 30 |
+
<Icon className="w-3.5 h-3.5" />
|
| 31 |
+
{sys.label}
|
| 32 |
+
</button>
|
| 33 |
+
)
|
| 34 |
+
})}
|
| 35 |
+
</div>
|
| 36 |
+
)
|
| 37 |
+
}
|
fronend/src/components/ThemeProvider.jsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { createContext, useContext, useState, useEffect } from 'react'
|
| 2 |
+
|
| 3 |
+
const ThemeContext = createContext()
|
| 4 |
+
|
| 5 |
+
export function ThemeProvider({ children }) {
|
| 6 |
+
const [theme, setTheme] = useState(() => {
|
| 7 |
+
if (typeof window !== 'undefined') {
|
| 8 |
+
return localStorage.getItem('nexus-theme') || 'dark'
|
| 9 |
+
}
|
| 10 |
+
return 'dark'
|
| 11 |
+
})
|
| 12 |
+
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
document.documentElement.setAttribute('data-theme', theme)
|
| 15 |
+
localStorage.setItem('nexus-theme', theme)
|
| 16 |
+
}, [theme])
|
| 17 |
+
|
| 18 |
+
const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark')
|
| 19 |
+
|
| 20 |
+
return (
|
| 21 |
+
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
| 22 |
+
{children}
|
| 23 |
+
</ThemeContext.Provider>
|
| 24 |
+
)
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export function useTheme() {
|
| 28 |
+
const ctx = useContext(ThemeContext)
|
| 29 |
+
if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
|
| 30 |
+
return ctx
|
| 31 |
+
}
|
fronend/src/hooks/useSSE.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useRef, useCallback } from 'react'
|
| 2 |
+
|
| 3 |
+
export function useSSE(onEvent) {
|
| 4 |
+
const eventSourceRef = useRef(null)
|
| 5 |
+
|
| 6 |
+
const connect = useCallback((sessionId) => {
|
| 7 |
+
// Close existing connection
|
| 8 |
+
if (eventSourceRef.current) {
|
| 9 |
+
eventSourceRef.current.close()
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const url = `/api/stream/${sessionId}`
|
| 13 |
+
const eventSource = new EventSource(url)
|
| 14 |
+
eventSourceRef.current = eventSource
|
| 15 |
+
|
| 16 |
+
const eventTypes = [
|
| 17 |
+
'agent_start', 'token', 'code_block', 'agent_done',
|
| 18 |
+
'error', 'file_created', 'done'
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
eventTypes.forEach(type => {
|
| 22 |
+
eventSource.addEventListener(type, (event) => {
|
| 23 |
+
onEvent({ type, data: event.data })
|
| 24 |
+
})
|
| 25 |
+
})
|
| 26 |
+
|
| 27 |
+
// Also handle generic messages
|
| 28 |
+
eventSource.onmessage = (event) => {
|
| 29 |
+
try {
|
| 30 |
+
const data = JSON.parse(event.data)
|
| 31 |
+
onEvent({ type: data.event_type || 'message', data: event.data })
|
| 32 |
+
} catch {
|
| 33 |
+
// ignore parse errors
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
eventSource.onerror = (err) => {
|
| 38 |
+
console.error('SSE error:', err)
|
| 39 |
+
// Don't auto-reconnect on error to avoid infinite loops
|
| 40 |
+
if (eventSource.readyState === EventSource.CLOSED) {
|
| 41 |
+
onEvent({
|
| 42 |
+
type: 'error',
|
| 43 |
+
data: JSON.stringify({
|
| 44 |
+
event_type: 'error',
|
| 45 |
+
agent: 'system',
|
| 46 |
+
content: 'Connection lost. Check the status and try again.'
|
| 47 |
+
})
|
| 48 |
+
})
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
}, [onEvent])
|
| 52 |
+
|
| 53 |
+
const disconnect = useCallback(() => {
|
| 54 |
+
if (eventSourceRef.current) {
|
| 55 |
+
eventSourceRef.current.close()
|
| 56 |
+
eventSourceRef.current = null
|
| 57 |
+
}
|
| 58 |
+
}, [])
|
| 59 |
+
|
| 60 |
+
return { connect, disconnect }
|
| 61 |
+
}
|
fronend/src/hooks/useTheme.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Re-exported from ThemeProvider for convenience
|
| 2 |
+
export { useTheme } from '../components/ThemeProvider'
|
fronend/src/index.css
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
:root {
|
| 6 |
+
--bg: #0A0A0F;
|
| 7 |
+
--surface: #111118;
|
| 8 |
+
--border: #1E1E2E;
|
| 9 |
+
--accent: #6C63FF;
|
| 10 |
+
--accent-2: #00D9FF;
|
| 11 |
+
--text-primary: #F0F0FF;
|
| 12 |
+
--text-secondary: #8888AA;
|
| 13 |
+
--success: #22D3A8;
|
| 14 |
+
--error: #FF4D6D;
|
| 15 |
+
--warning: #FFB547;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
[data-theme="light"] {
|
| 19 |
+
--bg: #F8F8FC;
|
| 20 |
+
--surface: #FFFFFF;
|
| 21 |
+
--border: #E0E0EF;
|
| 22 |
+
--accent: #5B53E8;
|
| 23 |
+
--accent-2: #0099CC;
|
| 24 |
+
--text-primary: #0A0A1A;
|
| 25 |
+
--text-secondary: #666688;
|
| 26 |
+
--success: #16A085;
|
| 27 |
+
--error: #E74C6F;
|
| 28 |
+
--warning: #E6A030;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
* {
|
| 32 |
+
margin: 0;
|
| 33 |
+
padding: 0;
|
| 34 |
+
box-sizing: border-box;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
html, body, #root {
|
| 38 |
+
height: 100%;
|
| 39 |
+
width: 100%;
|
| 40 |
+
overflow: hidden;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
body {
|
| 44 |
+
font-family: 'Inter', system-ui, sans-serif;
|
| 45 |
+
background-color: var(--bg);
|
| 46 |
+
color: var(--text-primary);
|
| 47 |
+
-webkit-font-smoothing: antialiased;
|
| 48 |
+
-moz-osx-font-smoothing: grayscale;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* Custom scrollbar */
|
| 52 |
+
::-webkit-scrollbar {
|
| 53 |
+
width: 6px;
|
| 54 |
+
height: 6px;
|
| 55 |
+
}
|
| 56 |
+
::-webkit-scrollbar-track {
|
| 57 |
+
background: transparent;
|
| 58 |
+
}
|
| 59 |
+
::-webkit-scrollbar-thumb {
|
| 60 |
+
background: var(--border);
|
| 61 |
+
border-radius: 3px;
|
| 62 |
+
}
|
| 63 |
+
::-webkit-scrollbar-thumb:hover {
|
| 64 |
+
background: var(--text-secondary);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/* Code blocks */
|
| 68 |
+
pre, code {
|
| 69 |
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
/* Selection */
|
| 73 |
+
::selection {
|
| 74 |
+
background: var(--accent);
|
| 75 |
+
color: white;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/* Focus styles */
|
| 79 |
+
*:focus-visible {
|
| 80 |
+
outline: 2px solid var(--accent);
|
| 81 |
+
outline-offset: 2px;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/* Glassmorphism utility */
|
| 85 |
+
.glass {
|
| 86 |
+
background: rgba(17, 17, 24, 0.7);
|
| 87 |
+
backdrop-filter: blur(12px);
|
| 88 |
+
-webkit-backdrop-filter: blur(12px);
|
| 89 |
+
border: 1px solid var(--border);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
[data-theme="light"] .glass {
|
| 93 |
+
background: rgba(255, 255, 255, 0.75);
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/* Agent status dots */
|
| 97 |
+
@keyframes agentPulse {
|
| 98 |
+
0%, 100% { opacity: 1; transform: scale(1); }
|
| 99 |
+
50% { opacity: 0.5; transform: scale(1.3); }
|
| 100 |
+
}
|
| 101 |
+
.agent-active {
|
| 102 |
+
animation: agentPulse 1.5s ease-in-out infinite;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/* Gradient text */
|
| 106 |
+
.gradient-text {
|
| 107 |
+
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
|
| 108 |
+
-webkit-background-clip: text;
|
| 109 |
+
-webkit-text-fill-color: transparent;
|
| 110 |
+
background-clip: text;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/* Typing indicator */
|
| 114 |
+
@keyframes blink {
|
| 115 |
+
0%, 50% { opacity: 1; }
|
| 116 |
+
51%, 100% { opacity: 0; }
|
| 117 |
+
}
|
| 118 |
+
.typing-cursor {
|
| 119 |
+
display: inline-block;
|
| 120 |
+
width: 2px;
|
| 121 |
+
height: 1.1em;
|
| 122 |
+
background: var(--accent);
|
| 123 |
+
margin-left: 2px;
|
| 124 |
+
animation: blink 1s infinite;
|
| 125 |
+
vertical-align: text-bottom;
|
| 126 |
+
}
|
fronend/src/main.jsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import ReactDOM from 'react-dom/client'
|
| 3 |
+
import App from './App'
|
| 4 |
+
import { ThemeProvider } from './components/ThemeProvider'
|
| 5 |
+
import './index.css'
|
| 6 |
+
|
| 7 |
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
| 8 |
+
<React.StrictMode>
|
| 9 |
+
<ThemeProvider>
|
| 10 |
+
<App />
|
| 11 |
+
</ThemeProvider>
|
| 12 |
+
</React.StrictMode>,
|
| 13 |
+
)
|
fronend/src/utils/api.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const BASE = '' // Same origin
|
| 2 |
+
|
| 3 |
+
export async function generateProject(prompt, appType = 'saas', systems = null) {
|
| 4 |
+
const res = await fetch(`${BASE}/api/generate`, {
|
| 5 |
+
method: 'POST',
|
| 6 |
+
headers: { 'Content-Type': 'application/json' },
|
| 7 |
+
body: JSON.stringify({ prompt, app_type: appType, systems }),
|
| 8 |
+
})
|
| 9 |
+
if (!res.ok) {
|
| 10 |
+
const err = await res.text()
|
| 11 |
+
throw new Error(`Generation failed: ${err}`)
|
| 12 |
+
}
|
| 13 |
+
return res.json()
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export async function getProjectStatus(sessionId) {
|
| 17 |
+
const res = await fetch(`${BASE}/api/status/${sessionId}`)
|
| 18 |
+
if (!res.ok) throw new Error('Failed to get status')
|
| 19 |
+
return res.json()
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export async function getProjectFiles(sessionId) {
|
| 23 |
+
const res = await fetch(`${BASE}/api/files/${sessionId}`)
|
| 24 |
+
if (!res.ok) throw new Error('Failed to get files')
|
| 25 |
+
return res.json()
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export async function getFileContent(sessionId, path) {
|
| 29 |
+
const res = await fetch(`${BASE}/api/file/${sessionId}/${path}`)
|
| 30 |
+
if (!res.ok) throw new Error('Failed to get file')
|
| 31 |
+
return res.json()
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export async function exportProject(sessionId) {
|
| 35 |
+
const res = await fetch(`${BASE}/api/export/${sessionId}`)
|
| 36 |
+
if (!res.ok) throw new Error('Export failed')
|
| 37 |
+
const blob = await res.blob()
|
| 38 |
+
const url = URL.createObjectURL(blob)
|
| 39 |
+
const a = document.createElement('a')
|
| 40 |
+
a.href = url
|
| 41 |
+
a.download = `nexus-project-${sessionId}.zip`
|
| 42 |
+
document.body.appendChild(a)
|
| 43 |
+
a.click()
|
| 44 |
+
document.body.removeChild(a)
|
| 45 |
+
URL.revokeObjectURL(url)
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
export async function fixBug(sessionId, errorMessage, filePath = null) {
|
| 49 |
+
const res = await fetch(`${BASE}/api/fix/${sessionId}`, {
|
| 50 |
+
method: 'POST',
|
| 51 |
+
headers: { 'Content-Type': 'application/json' },
|
| 52 |
+
body: JSON.stringify({ error_message: errorMessage, file_path: filePath }),
|
| 53 |
+
})
|
| 54 |
+
if (!res.ok) throw new Error('Fix request failed')
|
| 55 |
+
return res.json()
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// Keepalive ping to prevent HuggingFace sleep
|
| 59 |
+
let keepaliveInterval = null
|
| 60 |
+
|
| 61 |
+
export function startKeepalive() {
|
| 62 |
+
if (keepaliveInterval) return
|
| 63 |
+
keepaliveInterval = setInterval(async () => {
|
| 64 |
+
try {
|
| 65 |
+
await fetch(`${BASE}/api/health`)
|
| 66 |
+
} catch {
|
| 67 |
+
// ignore
|
| 68 |
+
}
|
| 69 |
+
}, 30000) // every 30 seconds
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
export function stopKeepalive() {
|
| 73 |
+
if (keepaliveInterval) {
|
| 74 |
+
clearInterval(keepaliveInterval)
|
| 75 |
+
keepaliveInterval = null
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// Start keepalive on load
|
| 80 |
+
startKeepalive()
|
fronend/tailwind.config.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
| 4 |
+
darkMode: 'class',
|
| 5 |
+
theme: {
|
| 6 |
+
extend: {
|
| 7 |
+
colors: {
|
| 8 |
+
nexus: {
|
| 9 |
+
bg: 'var(--bg)',
|
| 10 |
+
surface: 'var(--surface)',
|
| 11 |
+
border: 'var(--border)',
|
| 12 |
+
accent: 'var(--accent)',
|
| 13 |
+
'accent-2': 'var(--accent-2)',
|
| 14 |
+
'text-primary': 'var(--text-primary)',
|
| 15 |
+
'text-secondary': 'var(--text-secondary)',
|
| 16 |
+
success: 'var(--success)',
|
| 17 |
+
error: 'var(--error)',
|
| 18 |
+
warning: 'var(--warning)',
|
| 19 |
+
},
|
| 20 |
+
},
|
| 21 |
+
fontFamily: {
|
| 22 |
+
sans: ['Inter', 'system-ui', 'sans-serif'],
|
| 23 |
+
mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
|
| 24 |
+
},
|
| 25 |
+
animation: {
|
| 26 |
+
'pulse-slow': 'pulse 3s ease-in-out infinite',
|
| 27 |
+
'fade-in': 'fadeIn 0.5s ease forwards',
|
| 28 |
+
'slide-up': 'slideUp 0.4s ease forwards',
|
| 29 |
+
'glow': 'glow 2s ease-in-out infinite alternate',
|
| 30 |
+
},
|
| 31 |
+
keyframes: {
|
| 32 |
+
fadeIn: {
|
| 33 |
+
'0%': { opacity: '0', transform: 'translateY(8px)' },
|
| 34 |
+
'100%': { opacity: '1', transform: 'translateY(0)' },
|
| 35 |
+
},
|
| 36 |
+
slideUp: {
|
| 37 |
+
'0%': { opacity: '0', transform: 'translateY(20px)' },
|
| 38 |
+
'100%': { opacity: '1', transform: 'translateY(0)' },
|
| 39 |
+
},
|
| 40 |
+
glow: {
|
| 41 |
+
'0%': { boxShadow: '0 0 5px var(--accent)33' },
|
| 42 |
+
'100%': { boxShadow: '0 0 20px var(--accent)55' },
|
| 43 |
+
},
|
| 44 |
+
},
|
| 45 |
+
},
|
| 46 |
+
},
|
| 47 |
+
plugins: [],
|
| 48 |
+
}
|
fronend/vite.config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
plugins: [react()],
|
| 6 |
+
build: {
|
| 7 |
+
outDir: 'dist',
|
| 8 |
+
sourcemap: false,
|
| 9 |
+
},
|
| 10 |
+
server: {
|
| 11 |
+
proxy: {
|
| 12 |
+
'/api': 'http://localhost:7860',
|
| 13 |
+
},
|
| 14 |
+
},
|
| 15 |
+
})
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.6
|
| 2 |
+
uvicorn[standard]==0.34.0
|
| 3 |
+
httpx==0.28.1
|
| 4 |
+
python-multipart==0.0.20
|
| 5 |
+
sse-starlette==2.2.1
|
| 6 |
+
pydantic==2.11.1
|
| 7 |
+
aiofiles==24.1.0
|
| 8 |
+
python-dotenv==1.1.0
|
start.sh
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
exec uvicorn app.main:app --host 0.0.0.0 --port 7860 --workers 1 --timeout-keep-alive 120
|