9. Show components in replies
Some answers are better as UI than as text. When a visitor asks “where is my order?”, your product already has an order card with the right layout, the right links, and a Track button. Display components let the agent show that card inside the reply.
The agent does not send markup or code. It sends the name of a component and a JSON object of props. Your page looks up the name in a list of components that you register, and renders your component with those props. The agent cannot show UI that you did not write.
How it works
Section titled “How it works”- The agent’s code sends a
displayComponentcommand with a name and props. - Urai relays the command to the widget, and stores it on the assistant message if it is valid.
- The widget finds the component that you registered under that name and renders it below the reply text.
- When the conversation loads again, the widget renders the stored component again.
Display components are commands with a fixed shape. Read handle commands first if you have not sent a command before.
Send a component from the agent
Section titled “Send a component from the agent”Inside a script or a library function, send the command to the current thread:
await meta.urai.sendCommand(meta.vars.thread_id, { command: "displayComponent", component: "OrderCard", props: { orderId: "o-1042", status: "shipped", total: "$42.00" },});The first argument is the thread id from meta.vars. Urai uses it to attach the
component to the correct assistant message.
As with other commands, put this in a library function. The function fixes the name and the shape of the props, so the agent cannot get them wrong:
// index.ts in your library/** Show an order card in the reply. */export async function showOrder(order: { orderId: string; status: "pending" | "shipped" | "delivered"; total: string;}) { await meta.urai.sendCommand(meta.vars.thread_id, { command: "displayComponent", component: "OrderCard", props: order, });}Then tell the agent in AGENT.md when to use it and what to write in the reply:
- `showOrder(order)`: show the order card below your reply. Use it every time you talk about one specific order. The visitor can see the order number, status, and total on the card, so do not repeat them in your reply. Say what the card does not show, such as the expected delivery date.The agent does not see the component. sendCommand sends a signal to the page
and does not add anything to the transcript that the model reads. If the agent
needs to know that the card was shown, return a short receipt from the script:
await showOrder(order);await urai.complete({ shown: "OrderCard", orderId: order.orderId });Do not return the full order again. If the agent reads the same data twice, it tends to repeat the card in its reply.
Without a library
Section titled “Without a library”An advanced agent can also write the sendCommand call itself. In this case,
the list of components goes in the agent’s role instructions. Give the name,
each prop and its type, and when to use the component:
## Components this page can show
| Component | Props | Use it for || --- | --- | --- || `OrderCard` | `orderId` (string), `status` (string), `total` (string) | Any time you name a specific order. `status` is `pending`, `shipped`, or `delivered`. |
These names are the full list. Do not use other names. Never show a card for anorder that you did not find.A library function is more reliable, because the agent calls it the same way each time.
Register the component in React
Section titled “Register the component in React”With the React UI package, pass React components in displayComponents. The key
of each entry is the name that the agent sends:
import { UraiChat, type DisplayComponentProps, type UraiChatDisplayComponents,} from "@uraiai/chat-widget-react/ui";
function OrderCard({ props, sendMessage }: DisplayComponentProps) { // props is model output. Check each value before you use it. const orderId = typeof props.orderId === "string" ? props.orderId : null; const status = typeof props.status === "string" ? props.status : "pending"; const total = typeof props.total === "string" ? props.total : null; if (!orderId) return null;
return ( <div className="order-card"> <strong>Order {orderId}</strong> <span className={`status is-${status}`}>{status}</span> {total && <div>{total}</div>} <button onClick={() => sendMessage(`Where is order ${orderId}?`)}> Track this order </button> </div> );}
const DISPLAY_COMPONENTS: UraiChatDisplayComponents = { OrderCard };
export function Assistant({ user }: { user: { widgetUserId: string } }) { return ( <UraiChat widgetToken="wgt_..." userId={user.widgetUserId} displayComponents={DISPLAY_COMPONENTS} /> );}Each component gets these props:
| Prop | Value |
|---|---|
props |
The props object from the command, not changed |
component |
The name that the agent sent |
sendMessage(text) |
Sends a message as the visitor |
The props from the command come in one props object. The widget does not
spread them onto your component, so a command cannot set key, ref, or
children.
sendMessage continues the conversation. A button in your card sends a message
as if the visitor typed it, and the agent answers it.
Define the components and the displayComponents object outside your
component function. A component that you define inside a render function is a
new component each render, and React mounts it again each time.
Type the props
Section titled “Type the props”DisplayComponentProps takes a type parameter:
function OrderCard({ props }: DisplayComponentProps<{ orderId: string }>) { if (typeof props.orderId !== "string") return null; return <div>Order {props.orderId}</div>;}The type does not check anything at runtime. The values come from a model, so keep the checks in the component.
Where it renders
Section titled “Where it renders”Components render in the assistant bubble, below the reply text and above any files. They render in the order that the agent sent them. They show while the reply streams, and they show again when the conversation loads from history.
Each component renders inside an error boundary. If your component throws, the widget removes it, logs the error with the component name, and the rest of the conversation stays on the screen.
Change the layout
Section titled “Change the layout”Components render through the ComponentList slot. To change how the list
looks, replace that slot. You can wrap DefaultComponentList, or render
DisplayComponent for each item.
If you replace the AssistantMessage or StreamingMessage slot, render its
displayComponents prop. If you do not, no components show.
To style the default list, use the componentList class name key or the
[data-urai-part="component-list"] selector.
Register the component in the floating widget
Section titled “Register the component in the floating widget”UraiChatWidget from @uraiai/chat-widget-react renders the widget in a shadow
root, so it cannot render React components directly. It takes renderer
functions instead. A renderer gets an empty element, the props, and a context
with sendMessage. It can return a cleanup function:
import { createRoot } from "react-dom/client";import { UraiChatWidget, type ComponentRenderers,} from "@uraiai/chat-widget-react";import { OrderCard } from "./OrderCard";
const DISPLAY_COMPONENTS: ComponentRenderers = { OrderCard(element, props, { sendMessage }) { const root = createRoot(element); root.render(<OrderCard props={props} sendMessage={sendMessage} />); return () => root.unmount(); },};
<UraiChatWidget widgetToken="wgt_..." userId={user.widgetUserId} displayComponents={DISPLAY_COMPONENTS}/>;The element is in your page’s DOM. The widget projects it into the reply bubble
through a slot, so your stylesheets apply to it. You can mount a React app in
it, as above, or build the content with DOM calls. If you use DOM calls, set
text with textContent. Never put a value from props into innerHTML.
The cleanup function runs when the message leaves the transcript: a new
conversation, a thread switch, a change of visitor, or destroy(). Unmount your
app and stop any timers there.
The widget reads the renderers one time, when it creates the widget. A change to
displayComponents after that has no effect.
If a renderer throws, the widget logs the error and removes the element. The rest of the reply stays.
Vue, Svelte, and the core package
Section titled “Vue, Svelte, and the core package”The Vue and Svelte packages and @uraiai/chat-widget-core take the same
renderer functions in displayComponents. Mount your component into the
element and unmount it in the cleanup.
Vue:
import { createApp, h } from "vue";import type { ComponentRenderers } from "@uraiai/chat-widget-vue";import OrderCard from "./OrderCard.vue";
const displayComponents: ComponentRenderers = { OrderCard(element, props, { sendMessage }) { const app = createApp({ render: () => h(OrderCard, { ...props, sendMessage }) }); app.mount(element); return () => app.unmount(); },};<UraiChatWidget widget-token="wgt_..." user-id="user_42" :display-components="displayComponents"/>Svelte:
<script lang="ts"> import { mount, unmount } from "svelte"; import { UraiChatWidget, type ComponentRenderers } from "@uraiai/chat-widget-svelte"; import OrderCard from "./OrderCard.svelte";
const displayComponents: ComponentRenderers = { OrderCard: (element, props, { sendMessage }) => { const app = mount(OrderCard, { target: element, props: { ...props, sendMessage } }); return () => unmount(app); }, };</script>
<UraiChatWidget widgetToken="wgt_..." userId="user_42" {displayComponents} />These two examples spread props into the component. The command can then set
any prop of your component, so check each value inside the component.
Rules for the command
Section titled “Rules for the command”Urai and the widget apply the same rules:
componentstarts with a letter. It contains only letters, digits, and_ . : -. The maximum length is 100 characters.propsis optional. If you send it, it must be a JSON object. An array, a string, ornullmakes the full command not valid.- The full command is 64 KB or less, the same limit as other commands. Send an id and let your component fetch the details, not a large payload.
A command that breaks a rule does not render and Urai does not store it. It
still goes to your onCommand handler, so you can see it when you debug.
A valid command also goes to onCommand. If your onCommand handler matches on
an allowlist, it ignores displayComponent and you do not need to change it.
Names that do not match
Section titled “Names that do not match”Urai does not keep a list of component names. The names in your library or
instructions and the keys in displayComponents must agree, and nothing checks
this for you.
If the agent sends a name that you did not register, the widget renders nothing and writes one warning to the console for that name. This is useful when you release a new component: if the library change goes live before the page change, visitors see no card and no error. But it also means that a typo does not show an error in the UI. Look at the console first.
After a reload
Section titled “After a reload”Most commands are signals. Urai relays them and then forgets them. A
displayComponent command is different, because the component is part of the
answer. Urai stores each valid component on the assistant message and returns it
with the conversation history. The card is still there when the visitor comes
back.
Delivery follows the same rule as other commands: the turn’s stream must be
open. Send the command before the agent calls final_answer. A command that
arrives after the turn ends can be lost, and then Urai does not store it.
Where components show
Section titled “Where components show”| Where | What shows |
|---|---|
Your page, with a @uraiai/chat-widget-* package |
Your component |
| The Urai console and shared transcripts | A card with the component name and props |
| The script tag widget | Nothing in the transcript. Your command handler still gets the command. |
The console does not have your code, so it shows what the agent asked for. Use this view when you read conversations back. It also shows when the agent sends a name that the page does not register.
Treat props as untrusted
Section titled “Treat props as untrusted”A model wrote the props. Check them in your component as you check any input from outside:
- Check the type of each value before you use it.
- Never pass a value into
innerHTML,eval, or a URL that can go to another origin. - Never let a button in the card do an action that the visitor cannot do
themselves. For a sensitive action, let the button call
sendMessage, or ask the visitor to confirm in your own UI.
Troubleshooting
Section titled “Troubleshooting”Nothing renders and there is no warning. The command did not arrive. Log
onCommand. If it does not fire, the agent did not send the command. The usual
cause is a first argument other than meta.vars.thread_id.
The console shows no component registered for "X". The command arrived,
but X is not a key in displayComponents. Check the spelling. In the React UI
package, make sure that the prop is on the same UraiChat that you are looking
at.
The component renders live but not after a reload. The command was not
valid, for example props was not an object. Or the command arrived after the
turn ended.
The component renders and then goes away. Your component threw an error. The console shows the error with the component name.
The reply repeats what the card shows. Tell the agent in AGENT.md or in
its instructions what the card shows, and to write only what the card does not
show. Return a short receipt from the script, not the full data.
Check before moving on
Section titled “Check before moving on”Ask the agent about a specific record, such as “where is order o-1042?”. The card should render below the reply, with your styles. Select a button in the card and confirm that the conversation continues. Then reload the page and confirm that the card is still there.
Next: go live.