| 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 |
|
|
| |
| 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", |
| ] |
|
|
|
|
| |
| |
| |
|
|
| @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( |
| "<html><body style='background:#0A0A0F;color:#F0F0FF;font-family:sans-serif;" |
| "display:flex;align-items:center;justify-content:center;height:100vh'>" |
| "<h2>β³ Preview buildingβ¦</h2></body></html>" |
| ) |
|
|
|
|
| @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"<html><body style='background:#0A0A0F;color:#F0F0FF;font-family:sans-serif;padding:40px'>" |
| f"<h2>{system.replace('_',' ').title()}</h2><p>No preview yet.</p></body></html>" |
| ) |
|
|
|
|
| @app.get("/api/health") |
| async def health(): |
| return { |
| "status": "ok", |
| "key_set": bool(settings.OPENROUTER_API_KEY), |
| "models": settings.MODEL_IDS, |
| } |
|
|
|
|
| |
| |
| |
|
|
| @app.get("/") |
| async def root(): |
| return FileResponse("index.html", media_type="text/html") |