@assistant-ui/react-generative-ui
Generative UI tools for assistant-ui.
Installation
npm install @assistant-ui/react-generative-ui
Usage
Declare the components the model is allowed to render in a library, then expose
them through the present tool. The model emits a { $type, ...props } tree
($type names the component, so a real type prop never collides); the tool
renders it against the library.
import { JSONGenerativeUI } from "@assistant-ui/react-generative-ui";
import { Thread, Tools } from "@assistant-ui/react";
import { z } from "zod";
const generativeUI = new JSONGenerativeUI({
library: {
Card: {
description: "A card container with a title.",
properties: z.object({ title: z.string() }),
render: ({ title, children }) => (
<section className="card">
<h3>{title}</h3>
{children}
</section>
),
},
Text: {
description: "A run of text.",
properties: z.object({ tone: z.enum(["muted", "normal"]).optional() }),
render: ({ tone, children }) => <p data-tone={tone}>{children}</p>,
},
},
});
const toolkit = {
present: generativeUI.present(),
};
function App() {
return (
<Thread>
<Tools toolkit={toolkit} />
{/* MessageList, Composer, ... */}
</Thread>
);
}
A node's children prop is rendered recursively, so components can be nested:
{
"$type": "Card",
"title": "Hello",
"children": [{ "$type": "Text", "tone": "muted", "children": "Nested text" }]
}
Streaming props
By default a component renders only once its props have fully arrived, so
render always sees complete props. Opt into rendering from partial props with
streamProperties. When you do, render receives an injected $status prop
that discriminates the props: while "streaming" they are Partial, and once
"done" they are complete.
const generativeUI = new JSONGenerativeUI({
library: {
Weather: {
description: "A live weather card.",
properties: z.object({ city: z.string(), temp: z.number() }),
streamProperties: true,
render: (props) => {
if (props.$status === "done") {
return <Card>{`${props.city}: ${props.temp}°`}</Card>; // complete props
}
return <Card>{props.city ?? "Loading…"}</Card>; // partial props
},
},
},
});
render is a function of props, not a React component — but it runs on its
own fiber, so you can call hooks inside it as usual.
"use generative" authoring
The examples above wire the library up by hand on the client. With the
"use generative" compiler (@assistant-ui/next or @assistant-ui/vite) you
can instead colocate a component's properties schema with its render and
expose the library as tools, and the build splits each half to the right target:
the schema goes to the server (so the model sees the tool), the render stays on
the client.
"use generative";
import { z } from "zod";
import { Weather } from "@/components/weather";
import { defineToolkit } from "@assistant-ui/react";
import {
JSONGenerativeUI,
defineGenerativeComponents,
} from "@assistant-ui/react-generative-ui";
const generative = new JSONGenerativeUI({
library: defineGenerativeComponents({
Weather: {
description: "Show a weather card.",
properties: z.object({ city: z.string() }),
render: (props) => <Weather {...props} />,
},
}),
});
export default defineToolkit({
// `present` displays a component; `prompt_user` is a human-in-the-loop prompt.
present: generative.present(),
prompt_user: generative.promptUser(),
});
The model calls present with a node like { "$type": "Weather", "city": "SF" }.
Pass present({ display: "standalone" }) to render the component on its own
surface instead of inline. See the
"use generative" docs for the build setup.
Slack, Teams, and A2UI
The same tree is plain JSON, so it is not tied to the browser. Three React-free subpaths convert it for Slack Block Kit, Microsoft Teams Adaptive Cards, and inbound A2UI surfaces. They run in server actions, queue workers, and webhook handlers. Conversion is over the built-in vocabulary; a $type outside it is dropped with a warning. What each target does with a component, and how unsupported content degrades, is documented on Generative UI on Slack, Generative UI on Microsoft Teams, and A2UI over AG-UI.
@assistant-ui/react-generative-ui/slack
Converts a tree to Slack Block Kit JSON (toSlackBlocks), decodes block_actions webhooks back into $action payloads (decodeBlockAction), and maps Block Kit back into vocabulary nodes (fromSlackBlocks).
import { toSlackBlocks } from "@assistant-ui/react-generative-ui/slack";
const { blocks, warnings } = toSlackBlocks({
$type: "Card",
title: "Order #48213",
children: [{ $type: "Text", value: "Shipped, arriving Thursday." }],
});
@assistant-ui/react-generative-ui/teams
Converts a tree to a Microsoft Teams Adaptive Card (toAdaptiveCard) or bot-framework attachments with root-carousel support (toTeamsAttachments), and decodes an incoming activity.value (decodeSubmitData).
import { toAdaptiveCard } from "@assistant-ui/react-generative-ui/teams";
const { card, warnings } = toAdaptiveCard({
$type: "Card",
title: "Order #48213",
children: [{ $type: "Text", value: "Shipped, arriving Thursday." }],
});
@assistant-ui/react-generative-ui/a2ui
The inbound direction: applies A2UI surface operations (applyA2uiOperations) and converts a surface into a vocabulary tree (convertSurfaceToUISpec) that renders through the same present path.
import {
applyA2uiOperations,
convertSurfaceToUISpec,
} from "@assistant-ui/react-generative-ui/a2ui";
const { state } = applyA2uiOperations(new Map(), operations);
for (const surface of state.values()) {
const { spec, warnings } = convertSurfaceToUISpec(surface);
}
License
MIT