assistant-ui
Render OpenUI Lang in an existing assistant-ui chat.
This guide assumes the application already has a working assistant-ui chat, and that its backend responds with OpenUI Lang. If it doesn't yet, follow Set up UI generation first.
The change is in one place: the assistant message's text part. The thread, composer, runtime, and message layout stay the same.
Install
Skip this step if the chat UI and the backend share a package and you already installed these in Set up UI generation.
pnpm add @openuidev/react-lang @openuidev/react-ui@openuidev/react-lang renders OpenUI Lang. @openuidev/react-ui provides the built-in component library and its styles; if the agent uses only your own components, skip it and the styles import below.
When using the built-in library, import its styles once, from the application's global stylesheet:
@import "@openuidev/react-ui/styles/index.css";The examples below import library from src/lib/openui.tsx, the file created in Set up UI generation. If the chat UI lives in a separate package, define the same library there so it renders every component the agent can write.
Render OpenUI Lang
In the existing AssistantMessage component, replace the Markdown text renderer passed to MessagePrimitive.Parts with the OpenUI renderer:
"use client";
import { MessagePrimitive, type TextMessagePartProps } from "@assistant-ui/react";
import { Renderer } from "@openuidev/react-lang";
import { library } from "@/lib/openui";
function OpenUIText({ text, status }: TextMessagePartProps) {
return <Renderer response={text} library={library} isStreaming={status.type === "running"} />;
}
export function AssistantMessage() {
return (
<MessagePrimitive.Root>
<MessagePrimitive.Parts components={{ Text: OpenUIText }} />
</MessagePrimitive.Root>
);
}text is the accumulated response supplied by assistant-ui, and status.type stays "running" until the stream closes, so the renderer draws each component as it arrives.
See The Renderer for all of its props, including error handling.
Handle actions
Buttons, follow-ups, and forms in a generated interface report clicks through the renderer's onAction callback. Without it, they do nothing. Handle the two built-in action types:
continue_conversation: send a new user turn. Include the form values so the model knows what the user entered, as described in Conversation history.open_url: openevent.params.urlin a new tab.
Append the user turn with assistant-ui's useAui() API:
"use client";
import { useAui, type TextMessagePartProps } from "@assistant-ui/react";
import { BuiltinActionType, Renderer, type ActionEvent } from "@openuidev/react-lang";
import { library } from "@/lib/openui";
function toModelMessage(event: ActionEvent) {
const lines = [`User clicked: ${event.humanFriendlyMessage}`];
if (event.formState) lines.push(`Form values: ${JSON.stringify(event.formState)}`);
return lines.join("\n");
}
function OpenUIText({ text, status }: TextMessagePartProps) {
const aui = useAui();
const onAction = (event: ActionEvent) => {
if (event.type === BuiltinActionType.ContinueConversation) {
aui.thread.append({
role: "user",
content: [{ type: "text", text: toModelMessage(event) }],
});
} else if (event.type === BuiltinActionType.OpenUrl) {
window.open(String(event.params?.url), "_blank", "noopener,noreferrer");
}
};
return (
<Renderer
response={text}
library={library}
isStreaming={status.type === "running"}
onAction={onAction}
/>
);
}The appended message starts a new run, just like a message typed in the composer.
Integration overview: assistant-ui + OpenUI: generative UI inside assistant-ui chats.