CopilotKit

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 (Wave 4a). * * Dedicated single-mode demo that ports the starter's hashbrown renderer * onto a langgraph-python agent. Streaming structured output from the agent * (`byoc_hashbrown_agent`) is parsed progressively by `@hashbrownai/react`'s * `useJsonParser` + `useUiKit` and rendered with MetricCard + PieChart + * BarChart from `./charts/`. * * Layout: * - Header with title + short description. * - Chat composer with pre-seeded suggestion pills (via *   `useConfigureSuggestions`). Clicking a pill sends the canned prompt. * - Assistant messages are routed through `HashBrownAssistantMessage` via *   `<CopilotChat messageView={{ assistantMessage: ... }} />`. * * Runtime: dedicated endpoint `/api/copilotkit-byoc-hashbrown` with its own * agent — no bleed into the default runtime. */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 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 agent emits a catalog- constrained UI 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() {  // Pre-seed the composer with canonical prompts that steer the agent toward  // hashbrown-shaped output. `useConfigureSuggestions` renders pills inside  // the CopilotChat composer; clicking a pill sends its `message` directly.  useConfigureSuggestions({    suggestions: BYOC_HASHBROWN_SUGGESTIONS.map((s) => ({      title: s.label,      message: s.prompt,      // E2E testid-friendly class — Playwright targets visible text, but we      // keep a class hook in case we need finer-grained selectors later.      className: `byoc-hashbrown-suggestion-${s.label        .toLowerCase()        .replace(/\s+/g, "-")}`,    })),    available: "always",  });  // Resolve the memoized HashBrownRenderMessage component from the kit  // provider. It consumes the shared kit via context (see  // hashbrown-renderer.tsx) and renders assistant messages as a progressively  // assembled UI catalog.  const HashBrownMessage = useHashBrownMessageRenderer();  return (    <CopilotChat      className="h-full"      messageView={{        // `HashBrownMessage` matches the RenderMessage slot shape ({ message })        // but the v2 assistantMessage slot expects CopilotChatAssistantMessage's        // wider props. The cast is intentional — the renderer reads only        // `message`, exactly like the starter's page does with `RenderMessage`        // on CopilotSidebar.        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:

frontend/src/app/page.tsx
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:

frontend/src/app/hashbrown-renderer.tsx
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.

example agent output (streaming)
{
  "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.