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:
Markdown
is the React component that we want to expose.description
is a human-readable description of the component that will be used by the model to understand what the component does.name
is the stable component reference for the model.props
is 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
data
prop is expected to be astring
type. - 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
components
option 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
lastAssistantMessage
value. - The
ui
property 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
switch
statement is used to determine the role of the message (eitheruser
orassistant
). - For user messages, we display the text content.
- For assistant messages, we render the UI elements using the
ui
property. - The
ui
property 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.