Three Paradigms of Generative UI: From Component Mapping to Declarative Interfaces
There are three ways to let an agent decide what the interface looks like, handing control progressively from the frontend to the model. Implement all three, focusing on their security boundaries and why the most flexible one is usually the most dangerous in production.
Today's Goals
- Distinguish the three generative UI paradigms and rank them by control versus freedom
- Implement a component registry with a fallback so an unexpected type does not break the interface
- Explain the risks of open-ended generation and give a clear production recommendation
Yesterday's shared state was really the precursor to this: once the data is decided by the model, the natural next question is whether the components rendering it can be too. When you finish, scroll back up and tick the three goals off.
Plain-Language Walkthrough
From choosing to generating: how much of the interface does the model decide?
Interpreters normally just speak. But imagine a stronger version: they can press a button and put a table, a poll, or a chart on the conference's main screen.
That raises a permissions question: what exactly are they allowed to put up there?
- Version one: the screen has ten preset templates, and the interpreter picks one and fills it in
- Version two: the interpreter describes "title on the left, three badges on the right" and a technician renders it to the venue's standards
- Version three: the interpreter can project anything at all, including programs they wrote themselves
Freedom rises, control falls. The three generative UI paradigms map onto this exactly.
The three paradigms and the criterion that separates them
| Paradigm | The model decides | The frontend keeps | Typical payload |
|---|---|---|---|
| Static generative | Which component, and its data | Implementation, styling, interaction | { component: 'metric', props: {...} } |
| Declarative | A UI description tree | Node whitelist, render rules, styling | { type: 'stack', children: [...] } |
| Open-ended | UI code directly | Almost nothing | A JSX or HTML string |
What matters is not the table but the criterion that separates them:
Does the model produce data, or code?
The first two produce data. Data can be validated, whitelisted, and degraded when unrecognized. The third produces code, and executing code has no middle ground — you cannot partially run a script.
This criterion lets you classify any new approach, including ones marketed under new names.
Paradigm one: static generative, where safety comes entirely from the registry
The frontend defines a component registry and the model may only pick from it:
export const REGISTRY = {
metric: {
description: 'show a key metric. props: label (string), value (number), unit (optional string)',
validate: (p) => {
const label = str(p.label)
const value = num(p.value)
if (label === null || value === null) return null // invalid data
return { label, value, unit: str(p.unit) ?? '' }
},
},
// choices, table, ...
}Two details worth noting.
description goes into the system prompt. When the model picks the wrong component, it is usually not being dim — that description was vague. This is an easily overlooked debugging entry point: when generative UI misbehaves, check the registry copy first.
Each component validates its own props. Model-supplied data is untrustworthy — not from malice but from ordinary error: a missing field, a number sent as a string, an array sent as comma-separated text. On validation failure, degrade rather than passing bad data into a component and letting it crash.
Report the two failures separately
Resolving a component spec can fail two ways, and they point in completely different directions:
// The component is not in the registry
{ ok: false, reason: 'unknown-component', component: 'timeline_3d' }
// The component exists, but the data is invalid
{ ok: false, reason: 'invalid-props', component: 'metric' }Collapsing both into "render failed" leaves you nowhere to start. The first means the model picked something that does not exist — either the prompt documentation is unclear or upstream added a component the frontend has not caught up with. The second means the name is right and the fields are wrong.
The user-facing copy should differ too. "This version does not support X yet" and "the data is incomplete" tell users different things; the first hints that upgrading may help, the second does not.
Paradigm two: declarative, freer but still data
The limitation of the static paradigm is that layout is baked into each component. If the model wants "a title on the left with two badges side by side on the right", it cannot say so.
The declarative paradigm lets the model return a node tree:
{
type: 'stack',
children: [
{ type: 'heading', text: 'Q3 2026 summary' },
{ type: 'row', children: [
{ type: 'badge', text: 'reviewed' },
{ type: 'text', text: 'exported from the finance system' },
]},
],
}The frontend holds a whitelist of node types and renders them with its own styling. The model gains layout freedom, but what it emits is still data — nothing in the tree can be executed.
This is the direction of projects like Google's A2UI. Its appeal is that the same description can render across different frameworks, because it describes intent rather than implementation.
On encountering the unknown: degrade, do not reject the tree
This is the most important implementation detail of the declarative paradigm.
What do you do when the model's tree contains a node type outside your whitelist?
Do not refuse to render the whole tree. Blanking an entire reply because one node is unrecognized is the least defensible option — the rest is usually still useful.
The right move is to degrade that one node: keep its text, drop its structure, render it as plain text with a visual marker, and render its siblings normally.
if (!ALLOWED.has(node.type)) {
return {
type: 'text',
text: node.text ?? `(unsupported component: ${node.type})`,
children: [],
issue: 'unknown-type', // the UI uses this for a degraded style
}
}One more easily missed guard: a nesting depth limit. The model can emit a tree deep enough to blow the stack, and that needs no malice — a recursive generation that wanders is enough. Set a cap (six here), truncate beyond it, and leave a visible note.
Paradigm three: open-ended, and why this course does not implement it
The open-ended paradigm has the model emit UI code that runs in the user's browser. It is the most flexible and the only genuinely dangerous one of the three.
The core risk is not "the model might write buggy code" but:
The generated code runs with your page's privileges.
It can read cookies and localStorage, issue arbitrary requests, and modify any DOM. And the model is influenced by whatever it reads — an email, a web page, a user-uploaded document may all carry instructions to emit malicious code.
The point worth emphasizing about this attack path: it does not require the model to turn malicious, only to be obedient. The model faithfully followed instructions it found in a document, and an attacker wrote that document.
A sandbox (an iframe with a strict CSP) reduces the risk but carries two costs: it does not eliminate it (sandbox escapes have a long history), and it strips the generated interface of its ability to interact with the host application — which was the reason to want open-ended in the first place.
So the production recommendation is clear: start with the static paradigm, escalate to declarative when you need layout freedom, and avoid open-ended unless you have a very strong reason and a complete sandbox plan.
Writing registry descriptions the model can act on
Earlier we noted that a model picking the wrong component usually means a vague description. This section expands on that, because it is the highest-frequency debugging entry point in generative UI.
A poor description reads: shows data. The model sees three components that all "show data" and has to guess.
A good one covers three things: what situation it suits, the type of each field, and when not to use it.
metric: {
description:
'Show a single key metric, for emphasizing one number. props: label (string), value (number), unit (optional string). ' +
'For comparing several numbers use table; do not stack multiple metrics.',
}That last clause matters most — telling the model when not to pick something reduces misselection more than describing when to pick it. The same lesson applies to prompt writing generally: boundaries carry more information than definitions.
One practical recommendation: keep the description in the same file as the component implementation. Separated, someone changes the component and forgets the description, the model keeps selecting against a stale spec, and no mechanism catches the drift.
When not to use generative UI at all
The last section inverts the question: when is this whole approach wrong?
When the interface shape is stable. If your scenario is "weather query returns a weather card", just hardcode that mapping. Generative UI's costs — registry upkeep, validation, fallbacks, tests — only pay off when the shape genuinely varies.
When you need precise brand alignment. The model's compositions will surprise your design system, especially in the declarative paradigm.
Be especially careful with forms. A model-composed form leaves its validation rules, submission logic, and error handling for you to catch, and those are exactly where gaps appear.
An actionable test: if you can enumerate every possible interface shape and the list is short, do not use generative UI.
Versioning the registry
One consequence of the static paradigm deserves planning for: the registry changes over time, and clients update on their own schedule.
Add a component today and older clients will receive payloads referencing something they do not know. That is exactly the unknown-component case, which is why the fallback is not a nicety — it is the mechanism that makes registry evolution survivable.
The reverse also happens: remove a component and stored conversation history may still contain it. If you render history, the fallback carries that too.
The practical takeaway is to treat the registry as a versioned interface between two independently deployed parties, the same discipline you would apply to an API, rather than as a local constant you can freely edit.
Concretely, that means additive changes are cheap and removals are not. Adding a component only requires that older clients degrade gracefully, which they already do. Removing or renaming one breaks any stored history that references it, so prefer leaving a deprecated entry in place, rendering it, and simply not describing it to the model any more.
Source Reading
Hands-On Lab
One thing to watch while building: fallback copy is for users, not a place to dump technical errors. "This version does not support X yet" is far more useful than "Unknown component type".
- Define the component registry and implement the three static components
- Implement a declarative renderer that walks the nested description tree
- Use the unregistered-component script to verify the fallback neither crashes nor confuses
- Handle incomplete structured output arriving mid-stream
- Write up a comparison of the three paradigms and when each applies
Interview Questions
Four questions below, focused on choosing among the generative paradigms, designing fallbacks for unexpected input, and the security boundary around model-generated code. Open each one and read the analysis before the answer points — practicing the derivation beats memorizing bullets. The "common in China / common globally" tags let you filter by target market.
Checklist and Tomorrow
- Distinguish the three generative UI paradigms and rank them by control versus freedom
- Implement a component registry with a fallback so an unexpected type does not break the interface
- Explain the risks of open-ended generation and give a clear production recommendation
- Say when generative UI is the wrong tool entirely
- All 6 lab acceptance criteria pass
- Answer at least 3 of the 4 interview questions without looking at the answer points
Tomorrow (D6) we assemble the parts from the first five days into a workbench people can actually use: stopping mid-run, retrying after failure, editing and resending the last turn, branching from any point, plus progress for long tasks and subagent visualization. Everything so far has lived within one run; tomorrow we handle the relationships between runs, which is what separates a session from a one-shot question.
Interview questions
How much control do the three generative UI paradigms hand to the model, and which would you choose in production?生成式界面的三种范式分别把多少控制权交给模型?你在生产里会选哪种?
Common in ChinaCommon overseasIntermediate#generative-ui#architecture#decision-makingHow to reason about it · think before answering
- You need to describe all three, but the real signal is whether you have a criterion for classifying new approaches rather than three memorized names.
- The taxonomy: static generative, where the frontend owns components and the model only picks one and fills data; declarative, where the model returns a UI description tree that the frontend renders against a whitelist and its own styling; and open-ended, where the model emits UI code directly. Freedom increases, control decreases.
- Then the criterion, which is the heart of the answer: **does the model produce data or code?** The first two produce data, which can be validated, whitelisted, and degraded when unrecognized. The third produces code, and executing code has no middle ground — you cannot partially run a script. This criterion classifies any new approach, whatever it is branded.
- For production, give a clear escalation path: **start static, move to declarative when you need layout freedom, and avoid open-ended unless you have a strong reason and a complete sandbox.** Committing to a recommendation shows more judgment than listing pros and cons three times.
- Add the cost view: generative UI is not free. Registry upkeep, prop validation, fallback copy, and tests are ongoing costs, justified only when the range of interfaces genuinely varies.
- Expect the follow-up on when not to use it at all: if you can enumerate every possible interface and the list is short, hardcode the mapping. Also be careful with forms, since a model-assembled form leaves validation, submission, and error handling for you to catch.
分析过程 · 先想清楚再作答
- 这题要能把三种范式说清楚,但真正的区分度在于你有没有一条能判断新方案的判据,而不是背下三个名字。
- 先给分类:静态生成式,前端定组件、模型只选哪个并填数据;声明式,模型返回一棵界面描述树、前端按白名单和自己的样式渲染;开放式,模型直接产出界面代码。自由度递增,可控性递减。
- 然后给判据,这是答案的核心:**模型产出的是数据还是代码**。前两种是数据,可以校验、可以过白名单、可以在不认识时降级;第三种是代码,一旦执行就没有中间地带,你没法部分执行一段脚本。这条判据能用来归类任何新出现的方案,包括那些起了新名字的。
- 生产选型给一条明确的递进路径:**从静态开始,需要布局自由度时升到声明式,除非有非常强的理由并准备好完整沙箱,否则不用开放式。** 敢给出明确建议比罗列三种各有优劣更能体现判断力。
- 补一句成本视角:生成式界面不是免费的,注册表维护、props 校验、兜底文案、测试都是持续成本。只有界面形态真的多变时才划算。
- 可预期的追问是「什么时候根本不该用」——如果你能列出全部可能的界面形态而且这个列表不长,那就写死映射,不要引入生成式。另外表单类交互要特别小心,模型临时组合出来的表单,它的校验、提交、错误处理都要你自己接住。
Key points
- The three are static generative, declarative, and open-ended, with rising freedom and falling control.
- The criterion is whether the model emits data or code: data can be validated and degraded, code cannot be partially executed.
- Production path: start static, escalate to declarative for layout freedom, avoid open-ended without a full sandbox.
- Generative UI carries ongoing costs and only pays off when interface shapes genuinely vary.
- If you can enumerate every interface and the list is short, hardcode the mapping instead.
答题要点
- 三种范式是静态生成式、声明式、开放式,自由度递增而可控性递减。
- 判据是模型产出的是数据还是代码:数据能校验能降级,代码一旦执行没有中间地带。
- 生产路径:从静态开始,需要布局自由度升到声明式,没有完整沙箱不用开放式。
- 生成式有持续成本(注册表、校验、兜底、测试),只有形态真多变时才划算。
- 能列全所有界面形态且列表不长时,直接写死映射,不要用生成式。
The model returns a component type you never registered. How should the interface handle it?模型返回了一个你没注册过的组件类型,界面应该怎么处理?
Common in ChinaCommon overseasIntermediate#generative-ui#error-handling#resilienceHow to reason about it · think before answering
- It looks like a detail but tests your habits around partial failure. 'Throw' or 'render nothing' are too coarse and do not survive follow-ups.
- Principle one: **degrade that block, do not fail the whole response**. An unrecognized component should not blank out the entire reply, since the rest is usually still useful. The same holds in the declarative paradigm: degrade the unknown node to plain text and render its siblings normally.
- Principle two: **distinguish the two failure modes**. A component missing from the registry and a registered component with invalid data point in completely different directions — the first suggests unclear prompt documentation or an upstream addition the frontend has not caught up with, the second means the fields are wrong. Collapsing both into 'render failed' leaves you nowhere to start.
- Principle three is copy: the fallback is for **users**, not a place to dump technical errors. 'This version does not support X yet; the rest is unaffected' beats 'Unknown component type', and it hints that upgrading may help.
- One more guard worth raising unprompted: **a nesting depth limit**. Declarative trees can be deep enough to blow the stack, and that needs no malice, only a recursive generation that wanders. Cap it, truncate, and leave a visible note.
- Expect the follow-up on reducing occurrences: put the registry descriptions into the system prompt and write them well. Models usually pick the wrong component because that documentation was vague — an easily overlooked debugging entry point.
分析过程 · 先想清楚再作答
- 这题看着是个小细节,实际考的是你对「部分失败」的处理习惯。答「报错」或者「不渲染」都太粗糙,接不住追问。
- 第一条原则:**降级这一块,而不是让整个回答失败**。模型给了一个不认识的组件,不该让整条回复变成空白——其余内容通常仍然有用。这一条在声明式范式里同样成立:树里一个节点不认识,就把那个节点降级成纯文本,其余节点照常渲染。
- 第二条:**两种失败要分开**。组件名不在注册表里,和组件名认识但数据不合法,对排查的指向完全不同。前者说明 prompt 里的说明不清楚或者上游加了新组件而前端没跟上,后者说明字段错了。合并成一个「渲染失败」会让排查无从下手。
- 第三条是文案:兜底界面是给**用户**看的,不是把技术错误抛给用户。「这个版本还不支持某某,其余内容不受影响」比「Unknown component type」有用得多,而且前者暗示升级可能解决,后者什么都没说。
- 还有一个容易漏的防护值得主动提:**嵌套深度上限**。声明式的树可能深到爆栈,这不需要恶意,递归生成跑偏就够了。设个上限,超了截断并留一句可见说明。
- 可预期的追问是「怎么减少这种情况」——把注册表说明写进 system prompt 并写清楚,模型选错组件多半是那段说明写得不好。这是个容易被忽略的调试入口。
Key points
- Degrade that block rather than failing the whole response; the rest is usually still useful.
- Report unknown-component and invalid-props separately, since they point to different causes.
- Write fallback copy for users, describing the effect and its scope, not a raw technical error.
- Declarative trees also need a depth cap, since runaway recursive generation can blow the stack.
- Reduce occurrences by writing clear registry descriptions into the system prompt.
答题要点
- 降级这一块而不是让整个回答失败,其余内容通常仍然有用。
- 未注册组件与数据不合法要分开报,两者对排查的指向完全不同。
- 兜底文案写给用户看,说明现象与影响范围,不要抛技术错误。
- 声明式树里还要设嵌套深度上限,递归生成跑偏就能产出爆栈的树。
- 减少这类情况的入口是把注册表说明写清楚并放进 system prompt。
What are the risks of having a model generate frontend code that runs in the user's browser, and how would you bound them?让模型直接生成前端代码并在用户浏览器里执行,有哪些风险?你会怎么设边界?
Common in ChinaCommon overseasDeep dive#security#generative-ui#prompt-injectionHow to reason about it · think before answering
- A security question where the signal is naming the attack path, not saying 'it is unsafe'.
- Start with the core: **the generated code runs with your page's privileges**. It can read cookies and localStorage, issue arbitrary requests, and modify any DOM. That is not theoretical; any script on the page has those powers.
- Then the step most candidates miss: **the model is influenced by whatever it reads**. An email, a web page, or a user-uploaded document can carry instructions to emit malicious code. So the attack path **does not require the model to turn malicious, only to be obedient** — it faithfully follows instructions it found in a document an attacker wrote. Naming this shows you understand prompt injection compounded by generative UI.
- On bounding it: the preferred answer is not to use the paradigm at all and use declarative instead, where the model emits data rather than code and you can validate, whitelist, and degrade. That is the only approach that truly removes the risk.
- If you must, a sandbox is the floor: a separate-origin iframe with a strict CSP, no same-origin access, and no credentials passed in. Be honest about the two costs: sandbox escapes have a long history so the risk is reduced rather than eliminated, and the sandbox strips the generated UI of the ability to interact with the host app — **which was the reason to want open-ended in the first place**.
- Expect the follow-up on why anyone ships it: mostly internal tools and demos, where inputs are controlled and the audience is trusted. The deciding question is whether the model's inputs can come from untrusted sources.
分析过程 · 先想清楚再作答
- 这是道安全题,区分度在于你能不能说出攻击路径,而不是泛泛地说「不安全」。
- 先说清楚风险的核心:**模型产出的代码会以你的页面权限运行**。它能读 cookie、读 localStorage、发任意请求、改任意 DOM。这不是理论风险,页面里的脚本本来就有这些能力。
- 然后是关键的一步,也是多数人答不出来的:**模型是可以被它读到的内容影响的**。一封邮件、一个网页、一份用户上传的文档,里面都可能藏着让它产出恶意代码的指令。所以这条攻击路径**不需要模型变坏,只需要它听话**——它忠实执行了它在文档里读到的指令,而那份文档是攻击者写的。能说出这一层,就说明你理解了提示注入与生成式界面叠加起来的后果。
- 边界怎么设:首选是根本不用这种范式,改用声明式——模型给的是数据不是代码,能校验能白名单能降级。这是唯一能真正消除风险的做法。
- 如果非用不可,沙箱是最低要求:独立 origin 的 iframe 加严格 CSP、禁用同源访问、不传任何凭据进去。但要诚实说出它的两个代价:沙箱逃逸的历史很长,不能算完全消除;而且沙箱会让生成的界面失去与主应用交互的能力,**而那恰恰是当初想用开放式的理由**。
- 可预期的追问是「那业界为什么还有人做」——做的多是内部工具或者演示场景,那里输入可控、受众可信。判断依据是模型读到的内容是不是可能来自不可信来源。
Key points
- The core risk is that generated code runs with page privileges: cookies, storage, arbitrary requests, full DOM access.
- The attack needs no malicious model, only an obedient one, since instructions can hide in emails, pages, or uploaded documents.
- The preferred boundary is switching to declarative so the model emits data that can be validated, whitelisted, and degraded.
- If unavoidable, the floor is a separate-origin iframe with a strict CSP and no credentials.
- Be honest that sandboxing reduces rather than removes risk, and costs the interaction that motivated open-ended in the first place.
答题要点
- 核心风险是生成的代码以页面权限运行,能读 cookie 与本地存储、发任意请求、改任意 DOM。
- 攻击路径不需要模型变坏只需要它听话:它读到的邮件、网页、文档里可能藏着指令。
- 首选边界是改用声明式范式,让模型产出数据而非代码,可校验可白名单可降级。
- 必须用时的最低要求是独立 origin 的 iframe 加严格 CSP 且不传凭据。
- 要诚实承认沙箱的代价:不能完全消除风险,且会让生成界面失去与主应用交互的能力。
Structured output also streams. How should the interface render a JSON payload that is only half complete?结构化输出也是流式生成的,界面在 JSON 只到一半时应该怎么渲染?
Common in ChinaCommon overseasIntermediate#streaming#generative-ui#ui-stateHow to reason about it · think before answering
- Not a hard question, but a good check on whether you see incomplete data as the norm in streaming systems rather than an exception.
- The direct answer: render if it parses, show a placeholder if it does not, and **never display the partial JSON**. Half-formed JSON on screen reads as a crash.
- Worth abstracting one level: this pattern recurs. In this course it appears three times — partial message deltas, partial tool arguments, partial generative payloads. **Every place that receives streaming data must answer the same question: what do you show when it is half there?**
- Placeholder choice matters: if you already know which component is coming (because the component field arrived first), show that component's skeleton so the user sees 'a table is being generated' rather than a generic spinner. Field order in the payload is therefore designable — put the shape-determining fields first.
- An implementation detail: do not attempt a parse on every character, which is pure waste. Throttle per frame or wait for a clear boundary such as the end event. Same thinking as the per-frame batching from day two.
- Expect the follow-up on making partial JSON usable: streaming JSON parsers can emit partial objects so completed fields render early. The costs are complexity and having to handle fields that later change. For most cases, placeholder then full render is enough.
分析过程 · 先想清楚再作答
- 这题本身不难,但它是个很好的检验:你有没有意识到「不完整的数据」在流式系统里是常态而不是异常。
- 直接答案:能 parse 就渲染,不能就显示占位,**绝不把半截 JSON 原文显示给用户**。半截 JSON 出现在界面上,用户会以为程序崩了。
- 值得往上抽一层的是这个模式的重复性。本课里它出现了三次:消息增量拼到一半、工具参数拼到一半、生成式载荷拼到一半。**每一处接收流式数据的地方都要回答同一个问题:只到一半时显示什么。**
- 占位内容的选择有讲究:如果知道要渲染的是什么组件(比如 component 字段已经先到了),可以显示那个组件的骨架屏,用户看到的是「表格正在生成」而不是一个通用转圈。载荷字段的顺序因此是可以设计的——把决定形态的字段放前面。
- 另一个实现细节:不要每来一个字符就试着 parse 一次,那是纯粹的浪费。可以按帧节流,或者等到明显的边界(比如收到结束事件)再解析。这一条和 D2 的按帧合并是同一个思路。
- 可预期的追问是「有没有办法让部分 JSON 也能用」——有,流式 JSON 解析器可以产出部分对象,让已经到齐的字段先渲染。代价是实现复杂度,而且要处理「字段后来又变了」的情况。多数场景下先占位再整体渲染就够了。
Key points
- Render if it parses, otherwise show a placeholder, and never display the raw partial JSON.
- This is the general streaming pattern: message deltas, tool arguments, and generative payloads all pose the same question.
- Placeholders can be component-specific skeletons once the component field arrives, so payload field order is worth designing.
- Do not parse on every character; throttle per frame or wait for the end event, mirroring per-frame batching.
- Streaming JSON parsers can render completed fields early, at the cost of complexity and fields that may later change.
答题要点
- 能 parse 就渲染,不能就显示占位,绝不显示半截 JSON 原文。
- 这是流式系统的通用模式:消息增量、工具参数、生成式载荷都要回答同一个问题。
- 占位可以按已到达的组件字段显示对应骨架屏,所以载荷字段顺序是可设计的。
- 不要每个字符都试着 parse,按帧节流或等结束事件,思路同按帧合并。
- 流式 JSON 解析器能让已到齐的字段先渲染,代价是复杂度与字段可能回改。