--- title: Generative UI sidebarTitle: Generative UI description: Let an LLM write Prefab code that executes safely and renders in real time. icon: wand-magic-sparkles --- Generative UI flips the usual pattern: instead of you writing the UI code, an LLM writes it. The LLM receives data and a prompt, generates Prefab Python code, and a sandbox executes it safely. The user sees a bespoke interface — built for their specific data and question — rendered as a live Prefab app. ## How it works ``` User prompt → LLM generates Prefab code → Sandbox executes → PrefabApp renders ``` The LLM writes standard Prefab Python — context managers, `.rx`, charts, everything. The code runs in a persistent Pyodide interpreter, with Deno filesystem and network permissions revoked after startup. The sandbox returns wire protocol JSON, which renders like any other Prefab app. ```python from prefab_ui.sandbox import Sandbox from prefab_ui.app import PrefabApp sandbox = Sandbox() code = ''' from prefab_ui.components import Heading, Slider, Text from prefab_ui.app import PrefabApp with PrefabApp(state={"confidence": 75}, css_class="p-6") as app: Heading("Revenue Dashboard") slider = Slider(value=75, min=0, max=100, name="confidence") Text(f"Confidence: {slider.rx}%") ''' wire = await sandbox.run(code, data={"revenue": 1_200_000}) app = PrefabApp.from_json(wire) ``` The `data` parameter injects values as global variables in the sandbox. The LLM's code can use them freely — loops, f-strings, list comprehensions, computation. ## The Sandbox `prefab_ui.sandbox.Sandbox` manages a persistent Deno subprocess running Pyodide (CPython compiled to WebAssembly). Deno must be [installed](https://deno.land) on the host. The first call pays a ~2 second cold start while Pyodide boots and Prefab loads. Every subsequent call executes in about 1 millisecond — the process stays warm between calls. ```python sandbox = Sandbox() # First call: ~2s (cold start) result = await sandbox.run(code) # All subsequent calls: ~1ms result = await sandbox.run(more_code) ``` The sandbox also works as an async context manager for explicit lifecycle control: ```python async with Sandbox() as sandbox: result = await sandbox.run(code) # Deno process is killed on exit ``` If the Deno process crashes, the sandbox automatically restarts on the next call. `run()` has no built-in timeout. Use `asyncio.wait_for()` to apply a deadline to a call. Cancellation during startup or execution closes that worker, and the next call starts a fresh process. When `run()` starts the sandbox lazily, its deadline also covers startup. ```python import asyncio from prefab_ui.sandbox import Sandbox async with Sandbox() as sandbox: result = await asyncio.wait_for( sandbox.run('from prefab_ui.components import Text\nview = Text("Hello")'), timeout=10, ) ``` ### Security The packaged `deno.lock` pins npm and URL module dependencies with integrity hashes. The runner uses that lockfile in frozen mode, independently of the current working directory. During startup, Deno reads the Prefab source and Pyodide runtime, downloads packages from `cdn.jsdelivr.net`, and writes downloaded wheels to its cache. Before accepting code, the runner revokes its filesystem read, filesystem write, and network permissions. Permission prompts are disabled. Generated code uses the mounted virtual filesystem and preloaded Prefab and Pydantic packages; installing additional packages during a call is unsupported. Each `run()` call receives a fresh namespace for its variables. The interpreter and imported modules remain shared between calls, so this is not per-request Python isolation. WASM execution alone does not establish a host security boundary. ## MCP server pattern The generative UI sandbox pairs naturally with FastMCP. The example at `examples/generative-ui/server.py` shows two tools working together: **`execute_ui`** takes Prefab Python code and optional data, runs it in the sandbox, and returns the rendered UI. Its docstring teaches the LLM the key patterns — context managers, `.rx`, chart imports: ```python @mcp.tool(app=True) async def execute_ui(code: str, data: dict | None = None) -> PrefabApp: wire = await sandbox.run(code, data=data) return PrefabApp.from_json(wire) ``` **`components`** is a searchable reference of all Prefab components with their import paths and field signatures. The LLM calls this to look up what's available before generating code. Together, these two tools are sufficient for an LLM to generate complex, interactive dashboards. In testing, an LLM produced a 200-line multi-section dashboard with charts, sparklines, data tables, and reactive controls — using only the `execute_ui` docstring and the component search tool. ## `PrefabApp.from_json` The sandbox returns wire protocol JSON (a dict), not a live `PrefabApp`. `PrefabApp.from_json()` bridges the gap — it wraps the wire data so FastMCP's app pipeline handles it correctly: ```python wire = await sandbox.run(code) app = PrefabApp.from_json(wire) ``` You can override fields from the wire data: ```python app = PrefabApp.from_json(wire, state={"extra": "value"}, theme=my_theme) ``` ## What the LLM needs to know Surprisingly little. In testing, an LLM successfully generated complex Prefab UIs from just: 1. A small code example in the tool docstring (context managers, `PrefabApp`, `.rx`) 2. A component search tool that returns field signatures and import paths Prefab's component names follow [shadcn/ui](https://ui.shadcn.com) conventions (`Card`, `CardHeader`, `Badge`, `Alert`) and the chart API follows [Recharts](https://recharts.org) patterns (`BarChart`, `ChartSeries`, `data_key`). LLMs already know these conventions — Prefab's API is immediately familiar. The most common mistake is importing chart components from `prefab_ui.components` instead of `prefab_ui.components.charts`. The component search tool prevents this by including the correct import path in every result.