"""Trikona custom-frontend server (gradio.Server) — parallel to app.py. Serves a fully custom UI at / that calls the /generate and /regenerate API endpoints via the Gradio JS client. The frontend renders shapes IN PLACE (no iframe reload, no postMessage->hidden-button bridge), so the camera/orbit state persists and there is no white flash between shapes. Reuses app.py's generation core (codegen pipeline / mesh / raw) and its flags + caches, so behavior matches the Blocks app. The Modal endpoint URLs stay server-side (the browser only ever calls /generate and /regenerate). Run: TRIKONA_CODEGEN=1 MODAL_ENDPOINT_URL=... python3 server.py The original app.py (Blocks + iframe) is untouched and still works. """ import json import os from pathlib import Path import gradio as gr from fastapi.responses import HTMLResponse import app as core # generation logic, flags (CODEGEN/MESH_ON), endpoint URLs, caches HERE = Path(__file__).parent CANVAS_HTML = (HERE / "canvas.html").read_text() INDEX_HTML = (HERE / "web" / "index.html").read_text() server = gr.Server(title="Trikona") def _generate(prompt: str, params=None) -> dict: """Return {"contract": , "status": } reusing app.py's core.""" if not core.CODEGEN: return {"contract": core.generate_shape(prompt, params=params), "status": "raw"} from geometry import pipeline mesh_fn = core._mesh_emit if (core.MESH_ON and core.MESH_URL) else None result, meta = pipeline.build(core._model_emit, prompt, params=params, mesh_fn=mesh_fn) if meta.get("spec") is not None: core._SPEC_CACHE[prompt] = meta["spec"] return {"contract": json.dumps(result), "status": meta.get("status", "ok")} @server.api(name="generate") def generate(prompt: str) -> str: prompt = (prompt or "").strip() if not prompt: return json.dumps({"contract": json.dumps({"error": "empty_prompt"}), "status": "error"}) if len(prompt) > core.MAX_PROMPT_CHARS: return json.dumps({"contract": json.dumps({"error": "prompt_too_long"}), "status": "error"}) return json.dumps(_generate(prompt)) @server.api(name="regenerate") def regenerate(prompt: str, params: str) -> str: """Topological regen. params is a JSON object of param values (or empty).""" prompt = (prompt or "").strip() try: p = json.loads(params) if params else None except (json.JSONDecodeError, TypeError): p = None if core.CODEGEN: spec = core._SPEC_CACHE.get(prompt) if spec is not None: try: out = core._regen_modelfree(spec, p) if out is not None: return json.dumps({"contract": out, "status": "regen"}) except Exception: pass return json.dumps(_generate(prompt, params=p)) @server.api(name="examples") def examples() -> str: return json.dumps(core.EXAMPLE_PROMPTS) @server.get("/", response_class=HTMLResponse) def home(): return INDEX_HTML @server.get("/canvas", response_class=HTMLResponse) def canvas(): return CANVAS_HTML if __name__ == "__main__": server.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", "7860")))