Generative UI with React Components
Expose trusted, tested, and compliant components to the model.
The exposeComponent() Function
The
import { exposeComponent } from '@hashbrownai/react';
import { s } from '@hashbrownai/core';
import { Markdown } from './Markdown';
exposeComponent(Markdown, {
description: 'Show markdown to the user',
name: 'Markdown',
props: {
data: s.string('The markdown content'),
},
});
Let's break down the example above:
Markdownis the React component that we want to expose.descriptionis a human-readable description of the component that will be used by the model to understand what the component does.nameis the stable component reference for the model.propsis an object that defines the props that the component accepts. In this case, it accepts a single prop calleddata, which is a string representing the markdown content to be displayed.- The
s.string()function is used to define the type of the prop.
We should mention here that Skillet, our LLM-optimized schema language, is type safe.
- The
dataprop is expected to be astringtype. - The schema specified is a
string(). - If the schema does not match the React component's prop type, you'll see an error in both your editor and when you attempt to build the application.
Streaming with Skillet
Streaming generative user interfaces is baked into the core of Hashbrown. Hashbrown ships with an LLM-optimized schema language called Skillet.
Skillet supports streaming for:
- arrays
- objects
- strings
Let's update the previous example to support streaming of the markdown string into the Markdown component.
exposeComponent(Markdown, {
description: 'Show markdown to the user',
props: {
data: s.streaming.string('The markdown content'),
},
});
The s.streaming.string() function is used to define the type of the prop, indicating that it can be a string that will be streamed in chunks.
Streaming Docs
Learn more about streaming with Skillet
Children
When exposing components, you can also define the children that the component can accept.
exposeComponent(LightList, {
description: 'Show a list of lights to the user',
props: {
title: s.string('The name of the list'),
},
children: 'any',
});
In the example above, we're allowing any children to be rendered within the LightList component using the children prop.
However, if we wanted to explicitly limit the children that the model can generate, we can provide an array of exposed components.
exposeComponent(LightList, {
description: 'Show a list of lights to the user',
props: {
title: s.string('The name of the list'),
},
children: [
exposeComponent(Light, {
description: 'Show a light to the user',
props: {
lightId: s.string('The id of the light'),
},
}),
],
}),
In the example above, the LightList children is limited to the Light component.
The useUiChat() Hook
import { useUiChat, exposeComponent } from '@hashbrownai/react';
import { s } from '@hashbrownai/core';
import { Markdown } from './Markdown';
// 1. Create the UI chat hook
const chat = useUiChat({
// 2. Specify the collection of exposed components
components: [
// 3. Expose the Markdown component to the model
exposeComponent(Markdown, {
description: 'Show markdown to the user',
props: {
data: s.streaming.string('The markdown content'),
},
}),
],
});
- The
hook is used to create a UI chat instance. - The
componentsoption defines the collection of exposed components that the model can choose to render in the application. - The
function creates an exposed component.
UiChatOptions
| Option | Type | Required | Description |
|---|---|---|---|
components |
ExposedComponent |
Yes | The components to use for the UI chat hook |
model |
KnownModelIds |
Yes | The model to use for the UI chat hook |
system |
string |
Yes | The system prompt to use for the UI chat hook |
messages |
Chat.Message |
No | The initial messages for the UI chat hook |
tools |
Tools[] |
No | The tools to use for the UI chat hook |
debugName |
string |
No | The debug name for the UI chat hook |
debounceTime |
number |
No | The debounce time for the UI chat hook |
API Reference
useUiChat() API
See the full hook
UiChatOptions API
See the options
Render User Interface
Assistant messages produced by useUiChat() include a ui property containing rendered React elements.
<div className="assistant">{message.ui}</div>
Render Last Assistant Message
If you only want to render the last assistant message, useUiChat() provides a lastAssistantMessage value.
function UI() {
const chat = useUiChat({
components: [
exposeComponent(Markdown, { props: { data: s.string('md') } }),
],
});
const message = chat.lastAssistantMessage;
return message ? <div className="assistant">{message.ui}</div> : null;
}
- We render the last assistant message using the
lastAssistantMessagevalue. - The
uiproperty contains the rendered React elements generated by the model. - The
hook creates a new chat instance with the exposed components.
Render All Messages with Components
If you are building a chat-like experience, you likely want to iterate over all messages and render the generated text and components.
function Messages({ chat }: { chat: ReturnType<typeof useUiChat> }) {
return (
<>
{chat.messages.map((message, idx) => {
switch (message.role) {
case 'user':
return (
<div className="chat-message user" key={idx}>
<p>{message.content}</p>
</div>
);
case 'assistant':
return (
<div className="chat-message assistant" key={idx}>
{message.ui}
</div>
);
default:
return null;
}
})}
</>
);
}
- We iterate over the messages in the chat using
Array.prototype.map. - The
switchstatement is used to determine the role of the message (eitheruserorassistant). - For user messages, we display the text content.
- For assistant messages, we render the UI elements using the
uiproperty. - The
uiproperty contains the React elements that match the components defined viaexposeComponent(). - These elements are derived from the model's response using the schema built from your exposed components.
The prompt Tagged Template Literal
Providing examples in the system instructions enables few-shot prompting.
Hashbrown provides the prompt tagged template literal for generative UI for better instruction following.
useUiChat({
// 1. Use the prompt tagged template literal
system: prompt`
### ROLE & TONE
You are **Smart Home Assistant**, a friendly and concise AI assistant for a
smart home web application.
- Voice: clear, helpful, and respectful.
- Audience: users controlling lights and scenes via the web interface.
### RULES
1. **Never** expose raw data or internal code details.
2. For commands you cannot perform, **admit it** and suggest an alternative.
3. For actionable requests (e.g., changing light settings), **precede** any
explanation with the appropriate tool call.
### EXAMPLES
<user>Hello</user>
<assistant>
<ui>
<app-markdown data="How may I assist you?" />
</ui>
</assistant>
`,
components: [
exposeComponent(MarkdownComponent, { ... })
]
});
The prompt tagged template literal will parse the content inside of the brackets and do the following for you:
- Validate that the examples match the list of components provided to the model.
- Validate that the component props have been set correctly based on their schema definitions.
- Convert the example into Hashbrown's underlying JSON representation.
Next Steps
Get structured data from models
Use Skillet schema to describe model responses.
Execute LLM-generated JS in the browser (safely)
Use Hashbrown's JavaScript runtime for complex and mathematical operations.