BYOC — Hashbrown
Bring your own component library. Have the agent stream structured output and let Hashbrown's progressive JSON parser render it as React components in real time.
"use client";/** * byoc-hashbrown demo page. * * Ports the hashbrown renderer onto a CrewAI crew whose system prompt is * installed via `install_custom_system_message` to emit the hashbrown JSON * schema shape directly (NOT the XML `<ui>...</ui>` DSL — that DSL is used * only when hashbrown itself drives the LLM; here CrewAI drives, so the * agent must emit the raw schema shape). * * Streaming structured output from the agent is parsed progressively by * `@hashbrownai/react`'s `useJsonParser` + `useUiKit` and rendered with * MetricCard + PieChart + BarChart from `./charts/`. */import React from "react";import { CopilotKit, CopilotChat, CopilotChatAssistantMessage, useConfigureSuggestions,} from "@copilotkit/react-core/v2";import { HashBrownDashboard, useHashBrownMessageRenderer,} from "./hashbrown-renderer";import { BYOC_HASHBROWN_SUGGESTIONS } from "./suggestions";export default function ByocHashbrownDemoPage() { return ( <CopilotKit runtimeUrl="/api/copilotkit-byoc-hashbrown" agent="byoc-hashbrown-demo" > <HashBrownDashboard> <div data-testid="byoc-hashbrown-root" className="flex h-screen flex-col gap-3 p-6" > <header> <h1 className="text-lg font-semibold">BYOC: Hashbrown</h1> <p className="text-sm text-[var(--muted-foreground)]"> Streaming structured output via <code>@hashbrownai/react</code>. The crew emits a catalog-constrained JSON envelope that renders progressively as data streams. </p> </header> <div className="flex-1 overflow-hidden rounded-md border border-[var(--border)]"> <ChatBody /> </div> </div> </HashBrownDashboard> </CopilotKit> );}function ChatBody() { useConfigureSuggestions({ suggestions: BYOC_HASHBROWN_SUGGESTIONS.map((s) => ({ title: s.label, message: s.prompt, className: `byoc-hashbrown-suggestion-${s.label .toLowerCase() .replace(/\s+/g, "-")}`, })), available: "always", }); const HashBrownMessage = useHashBrownMessageRenderer(); return ( <CopilotChat className="h-full" messageView={{ assistantMessage: HashBrownMessage as unknown as typeof CopilotChatAssistantMessage, }} /> );}You have a chat surface and you want the agent to draw a dashboard, not just describe one in prose. By the end of this guide, the agent will stream a structured output object, @hashbrownai/react's progressive JSON parser will hand each finished slice to your renderer as it arrives, and the user sees the dashboard fill in live.
When to use this#
- Streaming dashboards where partial state should render before the full payload arrives.
- Agents authoring structured UI where the output is JSON-shaped, not free text.
- Cases where you already use Hashbrown for UI generation elsewhere in your stack.
If you'd rather work with an explicit catalog of registered components rather than streaming a JSON tree, see the sibling page BYOC — JSON Render for the same scenario implemented with @json-render/react.
Frontend#
The integration point is <CopilotChat>'s messageView.assistantMessage slot. Replace the default assistant-message renderer with a Hashbrown-backed one, and the chat takes care of everything else:
import {
CopilotKit,
CopilotChat,
useConfigureSuggestions,
} from "@copilotkit/react-core/v2";
import { HashBrownAssistantMessage } from "./hashbrown-renderer";
export default function ByocHashbrownDemo() {
useConfigureSuggestions({
suggestions: [
{ title: "Sales overview", message: "Show me a sales dashboard." },
{ title: "Region split", message: "Break down sales by region." },
],
available: "always",
});
return (
<CopilotKit runtimeUrl="/api/copilotkit-byoc-hashbrown" agent="byoc_hashbrown">
<CopilotChat
messageView={{ assistantMessage: HashBrownAssistantMessage }}
/>
</CopilotKit>
);
}The custom renderer is where Hashbrown earns its keep. useJsonParser consumes the streaming text content of an assistant message and emits typed JSON values as they parse; useUiKit resolves component names against your catalog and renders them with their props:
import { useJsonParser, useUiKit } from "@hashbrownai/react";
import { MetricCard } from "./metric-card";
import { PieChart, BarChart } from "./charts";
const catalog = {
MetricCard,
PieChart,
BarChart,
};
export function HashBrownAssistantMessage({ message }: { message: AssistantMessage }) {
const parsed = useJsonParser(message.content ?? "");
const ui = useUiKit({ catalog, value: parsed });
return <div className="space-y-3">{ui}</div>;
}Each component in the catalog is just a regular React component. The catalog acts as a typed allowlist: anything the agent emits that isn't in the catalog won't render, so the agent can't draw arbitrary HTML into the page.
Backend#
The agent's job is to stream structured output, not text. How you do that depends on your framework, but the shape is always the same: emit a JSON object at the top level of the assistant message, where each child references a component name and props matching the catalog.
{
"type": "MetricCard",
"title": "Total revenue",
"value": 184302,
"delta": 0.07
}Or a tree:
{
"type": "Stack",
"children": [
{ "type": "MetricCard", "title": "Total revenue", "value": 184302 },
{ "type": "BarChart", "data": [...] }
]
}Hashbrown's parser tolerates partial JSON, so the user sees MetricCard resolve before BarChart even starts streaming.
Comparing the two BYOC patterns#
Both byoc-hashbrown and byoc-json-render solve the same problem (agent-authored structured UI, rendered through a typed catalog), with two different rendering libraries. Pick whichever you already use elsewhere — the agent contract is the same shape; the React glue is what changes.
