Skip to content

Prompt-to-app (AI authoring)

There are two ways an LLM meets a Mateu app, and they are different things:

Who defines the UIWhat the LLM does
Operate (MCP)a developer (the Mateu definition)reads/writes data + runs actions + navigates, over MCP
Author (this page)the LLMwrites the UI definition (UIDL) as data

This page is the second one: the LLM writes the UIDL.

Why this is different from “AI generates React”

Section titled “Why this is different from “AI generates React””

On Mateu the UI is data, and Mateu publishes JSON Schemas for that data (backend/shared/uidl/*-schema.json, the same files editors point IntelliSense at). So an LLM’s output is a small definition that is verifiable against a contract — a wrong shape is caught and repaired, not shipped as thousands of lines of unreviewable imperative code. That is the whole thesis: the AI produces reviewable, schema-valid data, and the framework (tested once) supplies the behaviour, a11y, i18n and security. The volume that makes AI-generated frontends a liability never exists.

prompt ──▶ LLM ──▶ extract JSON ──▶ validate against the PUBLISHED schema (ajv)
│ valid ──▶ done ✅
│ invalid ─▶ send the errors back ──▶ (repeat, bounded)

The harness lives in frontend/prompt-to-app/:

ModuleRole
schemas.mjsLoads a published schema by name (specs | uidl | routes | sources | mount) straight from backend/shared/uidl — so it validates against exactly what Mateu ships.
validate.mjsajv over that draft-07 schema → { valid, errors[] }.
generate.mjsgenerateDefinition(prompt, { llm, schemaName, maxAttempts }) — the repair loop. The LLM is injected.
llm.mjsanthropicLlm() (the real, pluggable edge) + scriptedLlm() (tests).

The LLM is a parameter, not baked in: the loop takes an llm(messages, { system }) => Promise<string> function. That is what makes the mechanism testable without a key — and what lets you point it at any model, including an existing Mateu AI agent (see below).

Terminal window
cd frontend/prompt-to-app
npm install
# generate against a real model (Anthropic):
ANTHROPIC_API_KEY=… node index.mjs --schema routes \
"an app with a bookings list and a customers list"
# verify the harness with no key — scripted LLM + the REAL published schemas:
npm test

Example — the model authors a routes.yaml (as JSON) and it passes the published routes-schema.json:

{
"type": "Routes",
"routes": [
{ "route": "bookings", "layout": "bookings" },
{ "route": "customers", "layout": "customers" }
]
}

The harness is not tied to a specific provider — its llm is injected. Pointed at a real Mateu AI agent (an SSE endpoint) as the backend, the prompt “create a routes file with two screens” returned exactly the definition above, valid against the published routes-schema.json on the first attempt — the LLM authoring UIDL, not operating an app. live-ecdemo-probe.mjs in the same folder is the probe used (it folds the system prompt + schema into the agent’s single message and keeps the plain-text reply).

  • The LLM call is the only unverified part in the tests — the loop is exercised with a scripted LLM and the real published schemas; npm test never calls an API.
  • Schema-shape only. Mateu’s schemas are an OPEN lower bound (no additionalProperties: false), so this catches wrong types / missing required fields, not “unknown component” or semantic errors. A stronger check would round-trip the definition through the real loader (renders → valid); that is the natural next step, not done here.
  • The full schema is inlined in the prompt — fine for routes/sources; the uidl/specs schemas are ~130 KB, so a real tool would trim or retrieve.
  • Emits JSON; a routes.yaml/type: UI author would convert to YAML (1:1).

See frontend/prompt-to-app/README.md for the full module reference.