Skip to content

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.

  1. The agent’s code sends a displayComponent command with a name and props.
  2. Urai relays the command to the widget, and stores it on the assistant message if it is valid.
  3. The widget finds the component that you registered under that name and renders it below the reply text.
  4. 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.

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.

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 an
order that you did not find.

A library function is more reliable, because the agent calls it the same way each time.

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.

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.

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.

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.

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.

Urai and the widget apply the same rules:

  • component starts with a letter. It contains only letters, digits, and _ . : -. The maximum length is 100 characters.
  • props is optional. If you send it, it must be a JSON object. An array, a string, or null makes 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.

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.

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 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.

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.

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.

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.