Bluestrikeai commited on
Commit
23678fc
·
verified ·
1 Parent(s): 24dac61

Upload 38 files

Browse files
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, '&amp;')
22
+ .replace(/</g, '&lt;')
23
+ .replace(/>/g, '&gt;')
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