Generative UI with Angular Components
Expose trusted, tested, and compliant components to the model.
The exposeComponent()
Function
The
import { exposeComponent } from '@hashbrownai/angular';
exposeComponent(MarkdownComponent, {
description: 'Show markdown to the user',
input: {
data: s.string('The markdown content'),
},
});
Let's break down the example above:
MarkdownComponent
is the Angular component that we want to expose.description
is a human-readable description of the component that will be used by the LLM to understand what the component does.input
is an object that defines the inputs that the component accepts. In this case, it accepts a single input calleddata
, which is a string representing the markdown content to be displayed.- The
s.string()
function is used to define the type of the input.
We should mention here that Skillet, our LLM-optimized schema language, is type safe.
- The
data
input is expected to be astring
type. - The schema specified is a
string()
. - If the schema does not match the Angular component's input 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(MarkdownComponent, {
description: 'Show markdown to the user',
input: {
data: s.streaming.string('The markdown content'),
},
});
The s.streaming.string()
function is used to define the type of the input, indicating that it can be a string that will be streamed in chunks.
A note on the streaming
keyword: this is a Skillet-specific keyword that indicates that the input can be streamed in chunks, which is useful for large content like markdown.
Streaming Docs
Learn more about streaming with Skillet
Children
When exposing components, you can also define the children
that the component can accept.
exposeComponent(LightListComponent, {
description: 'Show a list of lights to the user',
input: {
title: s.string('The name of the list'),
},
children: 'any',
});
In the example above, we're allowing any
children to be rendered within the LightListComponent
using the <ng-content>
element.
However, if we wanted to explicitly limit the children that the model can generate, we can provide an array of exposed components.
exposeComponent(LightListComponent, {
description: 'Show a list of lights to the user',
input: {
title: s.string('The name of the list'),
icon: LightListIconSchema,
},
children: [
exposeComponent(LightComponent, {
description: 'Show a light to the user',
input: {
lightId: s.string('The id of the light'),
icon: LightIconSchema,
},
}),
],
}),
In the example above, the LightListComponent
children is limited to the LightComponent
.
The uiChatResource()
Function
// 1. Create the UI chat resource
chat = uiChatResource({
// 2. Specify the collection of exposed components
components: [
// 3. Expose the MarkdownComponent to th emodel
exposeComponent(MarkdownComponent, {
description: 'Show markdown to the user',
input: {
data: s.streaming.string('The markdown content'),
},
}),
],
});
- The
function is used to create a UI chat resource. - The
components
option defines the collection of exposed components that the model can choose to render in the application. - The
function creates and exposed component.
UiChatResourceOptions
Option | Type | Required | Description |
---|---|---|---|
components |
ExposedComponent |
Yes | The components to use for the UI chat resource |
model |
KnownModelIds |
Yes | The model to use for the UI chat resource |
system |
string | Signal |
Yes | The system prompt to use for the UI chat resource |
messages |
Chat.Message |
No | The initial messages for the UI chat resource |
tools |
Tools[] |
No | The tools to use for the UI chat resource |
debugName |
string |
No | The debug name for the UI chat resource |
debounce |
number |
No | The debounce time for the UI chat resource |
apiUrl |
string |
No | The API URL to use for the UI chat resource |
API Reference
uiChatResource() API
See the full resource
UiChatResourceOptions API
See the options
Render User Interface
The <hb-render-message>
Angular component renders an assistant message that contains the user interface schema.
<hb-render-message [message]="message" />
Render Last Assistant Message
If you only want to render the last assistant message, Hashbrown provides the lastAssistantMessage
Signal.
@Component({
// 1. Render the last assistant message
template: `
@let message = chat.lastAssistantMessage();
@if (message) {
<hb-render-message [message]="message" />
}
`,
})
export class UI {
// 2. Create the UI chat resource specifying the exposed components
chat = uiChatResource({
components: [
exposeComponent(ChartComponent, { ... })
]
})
}
- We render the last assistant message using the
lastAssistantMessage
signal. - The
<hb-render-message>
requires themessage
to render. - The
function creates a new resource with the exposed components.
Render All Message with Components
If you building a chat-like experience, you likely want to iterate over all of the messages
and render the generated text and components.
@Component({
template: `
@for (message of chat.value(); track $index) {
@switch (message.role) {
@case ('user') {
<div class="chat-message user">
<p>{{ message.content }}</p>
</div>
}
@case ('assistant') {
<div class="chat-message assistant">
@if (message.content) {
<hb-render-message [message]="message" />
}
</div>
}
}
}
`,
})
export class Chat {}
- We use Angular's
@for
control-flow syntax to iterate over the messages in the chat resourcevalue()
signal. - The
@switch
control-flow is used to determine the role of the message (either 'user' or 'assistant'). - For user messages, it simply displays the content in a paragraph tag.
- For assistant messages, it uses the
component to render the message content. - The
component is responsible for rendering the exposed components based on the message content. - The
message
input is passed to thecomponent, which will handle rendering the components defined in the chat resource.
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.