Notes
How forms work
A form is the same idea: register a DynamicForm component, then have the agent return a field list in props.
componentRegistry.register("DynamicForm", DynamicForm);
System prompt:
When you need to collect details, respond with this JSON:
{
"component": "DynamicForm",
"props": {
"title": "Schedule a demo",
"fields": [
{ "label": "Email", "type": "email", "required": true },
{ "label": "Notes", "type": "textarea" }
],
"submit_text": "Request meeting"
}
}
The form renderer turns each field object into inputs, handles validation/submission, and can use formStyles or props.styles for the visual variants above.
How components work
1. Create a normal component.
function ProductCard({ title }) {
const button = document.createElement("button");
button.textContent = "Add to Cart";
button.addEventListener("click", () => {
button.textContent = "Added";
});
return button;
}
2. Register it, then initialize Persona. The widget reads from the shared registry automatically.
import { componentRegistry, initAgentWidget } from "@runtypelabs/persona";
componentRegistry.register("ProductCard", ProductCard);
initAgentWidget({
target: "#app",
config: {
apiUrl: "/api/chat/dispatch-component",
parserType: "json" // needed for streamed JSON directives
}
});
3. Tell the agent to return this JSON when it should show the component. This usually goes in your system prompt or flow instructions.
System prompt:
When showing a product, respond with this JSON:
{
"component": "ProductCard",
"props": {
"title": "{{name of the product}}",
"price": {{price of the product}}
}
}
4. Persona renders it in the chat.
// The asks for a product.
// LLM returns the JSON, Persona renders registered component.
// The user clicks "Add to Cart".
// Action performed, button changes to "Added".
The component is normal DOM, so buttons, forms, saved state, and page events all work.