import json import uuid import asyncio import zipfile from pathlib import Path from contextlib import asynccontextmanager from fastapi import FastAPI, Request, HTTPException from fastapi.responses import ( HTMLResponse, JSONResponse, FileResponse, StreamingResponse, ) from fastapi.middleware.cors import CORSMiddleware from sse_starlette.sse import EventSourceResponse from config import settings from schemas import GenerateRequest, FixRequest, ProjectState from pipeline import PipelineEngine # ─── state ──────────────────────────────────────────────── sessions: dict[str, ProjectState] = {} engine = PipelineEngine() @asynccontextmanager async def lifespan(app: FastAPI): Path("/tmp/nexus_projects").mkdir(parents=True, exist_ok=True) yield app = FastAPI(title="Nexus Builder", version="1.0.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) DEFAULT_SYSTEMS = [ "client_portal", "public_landing", "marketing_cms", "analytics_dashboard", "admin_panel", ] # ═══════════════════════════════════════════════════════════ # API ROUTES # ═══════════════════════════════════════════════════════════ @app.post("/api/generate") async def generate(req: GenerateRequest): sid = str(uuid.uuid4())[:12] state = ProjectState( session_id=sid, user_prompt=req.prompt, app_type=req.app_type or "saas", status="queued", systems=req.systems or DEFAULT_SYSTEMS, ) sessions[sid] = state asyncio.create_task(engine.run(state)) return {"session_id": sid, "status": "started"} @app.get("/api/stream/{sid}") async def stream(request: Request, sid: str): if sid not in sessions: raise HTTPException(404, "Session not found") async def gen(): state = sessions[sid] cursor = 0 while True: if await request.is_disconnected(): break msgs = state.messages[cursor:] for m in msgs: yield { "event": m.event_type, "data": json.dumps(m.model_dump(), default=str), } cursor = len(state.messages) if state.status in ("completed", "error"): yield { "event": "done", "data": json.dumps( {"status": state.status, "session_id": sid} ), } break await asyncio.sleep(0.25) return EventSourceResponse(gen()) @app.get("/api/status/{sid}") async def status(sid: str): if sid not in sessions: raise HTTPException(404) s = sessions[sid] return { "session_id": sid, "status": s.status, "current_agent": s.current_agent, "file_tree": s.file_tree, "errors": s.errors, } @app.get("/api/files/{sid}") async def files(sid: str): if sid not in sessions: raise HTTPException(404) return {"files": sessions[sid].generated_files} @app.get("/api/file/{sid}/{path:path}") async def file_content(sid: str, path: str): if sid not in sessions: raise HTTPException(404) c = sessions[sid].generated_files.get(path) if c is None: raise HTTPException(404, "File not found") return {"path": path, "content": c} @app.post("/api/fix/{sid}") async def fix(sid: str, req: FixRequest): if sid not in sessions: raise HTTPException(404) state = sessions[sid] state.status = "fixing" asyncio.create_task(engine.fix(state, req.error_message, req.file_path)) return {"status": "fix_started"} @app.get("/api/export/{sid}") async def export(sid: str): if sid not in sessions: raise HTTPException(404) zp = Path(f"/tmp/nexus_projects/{sid}.zip") with zipfile.ZipFile(zp, "w", zipfile.ZIP_DEFLATED) as zf: for fp, content in sessions[sid].generated_files.items(): zf.writestr(fp, content) return FileResponse(zp, filename=f"nexus-{sid}.zip", media_type="application/zip") @app.get("/api/preview/{sid}") async def preview(sid: str): if sid not in sessions: raise HTTPException(404) s = sessions[sid] for k in ("preview/index.html", "frontend/index.html", "index.html"): if k in s.generated_files: return HTMLResponse(s.generated_files[k]) return HTMLResponse( "" "

⏳ Preview building…

" ) @app.get("/api/preview/{sid}/{system}") async def preview_system(sid: str, system: str): if sid not in sessions: raise HTTPException(404) s = sessions[sid] key = f"{system}/index.html" if key in s.generated_files: return HTMLResponse(s.generated_files[key]) return HTMLResponse( f"" f"

{system.replace('_',' ').title()}

No preview yet.

" ) @app.get("/api/health") async def health(): return { "status": "ok", "key_set": bool(settings.OPENROUTER_API_KEY), "models": settings.MODEL_IDS, } # ═══════════════════════════════════════════════════════════ # SERVE FRONTEND (index.html at root) # ═══════════════════════════════════════════════════════════ @app.get("/") async def root(): return FileResponse("index.html", media_type="text/html")