Skip to Content
AI Chat

AI Chat Module Recipe

The AI Chat module puts an assistant in your app. It talks to an agent you create in Chocolate Factory (its model, system prompt, knowledge base and tools are configured there) and streams the agent’s answers into a chat.

The module gives you two layouts:

  • AiChatPanel — a panel docked to the right edge at full height. On a wide screen the page makes room for it, so an answer sits beside the screen it explains; on a narrower one it overlays the page; on a phone it fills the screen. Open it from an Ask AI button in your header, or with ⌘ I on macOS and Ctrl I elsewhere.
  • AiChatWidget — a launcher bubble in a bottom corner that opens a chat window above it, for customer-facing apps.

Both show the same chat: streaming Markdown answers (code blocks with a copy button, tables, links), one working word while the agent searches or thinks, Stop and Try again, Copy answer, starter questions, a recent-chats list, and images: attach them with the paper clip, paste them, drop them, or take a screenshot of the page (drag over the part to show, then draw on it).

AiChatPanel docked beside the page — a streamed answer with numbered steps, a code block, a table and a source (from the AI Chat Storybook)

The pieces

PiecePackageRole
AiChatPanel@buildpad/ui-chatThe chat docked to the right edge; sets --bp-ai-chat-offset while docked
AiChatWidget@buildpad/ui-chatThe chat as a floating launcher and window
AiChatButton@buildpad/ui-chatHeader button that opens and closes the chat, with the shortcut in its tooltip
AiChatProvider / useAiChat@buildpad/ui-chatOpen/closed state shared by the button and the panel, and the keyboard shortcut
AiChat@buildpad/ui-chatThe chat itself (header, thread, question box), for a shell of your own
AiChatContextChip@buildpad/ui-chatA chip for the context slot, saying what the agent is told about
chocolateFactoryBackend / useChocolateFactoryConfig@buildpad/ui-chatBrowser-direct backend, and the hook that reads its settings from your config route
routeBackend@buildpad/ui-chatBackend for your own proxy route
Strings@buildpad/utilsThe chat dictionary namespace (English and Bahasa Indonesia)

This recipe is opt-in — it is not part of bootstrap. Generate the module with buildpad add ai-chat-routes; it pulls in the ai-chat component and the services / utils lib modules, and installs ai, @ai-sdk/react, react-markdown, remark-gfm, use-stick-to-bottom and @zumer/snapdom. In a generated app the imports point at your local copy (@/components/ui/ai-chat).

Choose a backend

The chat reaches the agent one of two ways. The choice is about where the Chocolate Factory key lives and whether answers can stream.

Browser-direct (default)Server proxy
CLI targetai-chat-routesai-chat-proxy-routes
RouteGET /api/ai-chat/configPOST /api/ai-chat, GET /api/ai-chat/history
BackendchocolateFactoryBackend(config)routeBackend()
The keyVisible to the browserStays on the server
Streams on Amplify HostingYesNo — the route is a buffered Lambda (~30 s): answers arrive at once, long ones time out
Streams on a Node serverYesYes
Conversations keptIn the browser (local storage, without image data)In Chocolate Factory, linked to the browser by a signed token

Amplify Hosting runs a Next.js API route as a Lambda that buffers its whole response, so a streamed answer can’t pass through your own route there. The browser-direct backend calls Chocolate Factory itself, which is why it streams everywhere — and why the browser needs the key.

Browser-direct shows the key to anyone who loads the page. Chocolate Factory’s agent keys (agk_…) reach only one agent, but its CORS headers don’t allow their x-agent-key header from browsers yet, so a browser call needs a project key (cf_…), which reaches every agent in the project. Protect the config route with your app’s auth, keep the project’s other agents harmless, or use the server proxy on a host where it can stream.

Install

Add the module

buildpad add ai-chat-routes

or, for the server proxy:

buildpad add ai-chat-proxy-routes

This copies the route files into app/api/ai-chat/ (and, for the proxy, lib/ai-chat/server.ts), and the components into components/ui/ai-chat/ (20 files). To install only the components:

buildpad add ai-chat

Connect Chocolate Factory and set the agent

Connect Chocolate Factory on the project’s Connectors page in the Buildpad Platform; that gives the project CHOCOLATE_FACTORY_BASE_URL and CHOCOLATE_FACTORY_API_KEY. Then copy your agent’s ID from its page in the Chocolate Factory Portal.

.env.local
CHOCOLATE_FACTORY_BASE_URL=https://app.chocolatefactory.ai CHOCOLATE_FACTORY_API_KEY=cf_… CHOCOLATE_FACTORY_AGENT_ID=… # Server proxy only: 32+ random characters that sign conversation tokens AI_CHAT_SECRET=…

CHOCOLATE_FACTORY_AGENT_KEY (an agk_… key from the agent’s Integrations section) takes precedence over CHOCOLATE_FACTORY_API_KEY when set. Use it with the server proxy; with browser-direct, leave it unset for now — the config route would hand it to the browser, and Chocolate Factory’s CORS headers refuse the header it travels in, so every answer would fail. The routes read these at request time; on Amplify, add them to the app’s environment variables and redeploy.

Configure the agent

In the Chocolate Factory Portal, turn the agent’s Memory off: every user of your app reaches it with the same key, so Chocolate Factory sees them as one user, and something one person tells the agent could come up in another’s answers. Give it a knowledge base or an MCP server with your docs, so its answers are grounded in them.

Mount it

Docked panel

Wrap the layout in AiChatProvider, put the button in the header and the panel at the layout’s edge, and let the main area make room:

components/layout/assistant.tsx
'use client'; import { AiChatPanel, chocolateFactoryBackend, useChocolateFactoryConfig } from '@/components/ui/ai-chat'; export function Assistant({ userId }: { userId: string }) { const config = useChocolateFactoryConfig(); // GET /api/ai-chat/config if (!config) return null; // not configured: no chat return ( <AiChatPanel backend={chocolateFactoryBackend(config)} title="Ask Acme" storageKey={userId} suggestions={['How do I invite a teammate?', 'Where do I change my plan?']} /> ); }
app/[lang]/(authenticated)/layout.tsx
<AiChatProvider> <header>… <AiChatButton /> …</header> <main style={{ marginInlineEnd: 'var(--bp-ai-chat-offset, 0)', transition: 'margin 200ms ease' }}> {children} </main> <Assistant userId={user.id} /> </AiChatProvider>

The panel sets --bp-ai-chat-offset on <html> only while it is open and docked (from 75em, the dockFrom prop). Set --bp-ai-chat-header-height if your header isn’t 60 px tall, so the panel’s header lines up with it.

Floating widget

<AiChatWidget backend={chocolateFactoryBackend(config)} title="Acme Support" greeting="Hi! How can we help today?" />

It keeps its own open state, or shares AiChatProvider’s with an AiChatButton.

Server proxy

<AiChatPanel backend={routeBackend()} title="Ask Acme" storageKey={userId} />

lib/ai-chat/server.ts binds conversations to an HttpOnly cookie, so it works without sign-in. Behind your app’s auth, make chatOwner return the signed-in user’s id and reject anonymous requests in the two routes.

Tell the agent where the user is

vars are sent with every message as Chocolate Factory template variables, so the agent’s system prompt can use them ({{page}}, {{user_name}}, …). Pair them with an AiChatContextChip in the context slot, so the user sees what is shared and can stop sharing:

const [share, setShare] = useState(true); <AiChatPanel backend={backend} vars={() => (share ? { page: 'Settings › Members', page_path: pathname } : undefined)} context={ share ? ( <AiChatContextChip label="Settings › Members" title={pathname} onRemove={() => setShare(false)} removeLabel="Don't share this page" /> ) : null } />

Props

Shared by AiChat, AiChatPanel and AiChatWidget:

PropTypeDefaultNotes
backendAiChatBackendrequiredchocolateFactoryBackend(config) or routeBackend()
titlestring”Assistant”Header title
badgestring—e.g. “Beta”
greetingstring”How can I help?”Empty-state heading
descriptionstring—Empty-state text
suggestionsstring[][]Starter questions
placeholderstring”Ask a question…”
disclaimerstring”AI answers can be wrong. Check anything important.”Under the question box
varsobject | () => object—Template variables sent with every message
contextReactNode—Above the question box
features{ attachments?, screenshot?, recentChats? }all on
storageKeystring"default"Scopes recent chats in local storage — pass the user’s id
linkTarget(href) => '_self' | '_blank'app pages in place, others in a new tab
onNavigate(href) => void—Client-side navigation for in-place links, e.g. router.push
imageHostsstring[][]Hosts an answer may show images from; others show their alt text
translationsDeepPartial<ChatTranslations>—Per-instance strings
onError(error) => voidconsole.error

AiChatPanel adds opened, onClose, width (420) and dockFrom (75em); without opened/onClose it follows AiChatProvider. AiChatWidget adds opened, onOpenChange, defaultOpened, position (bottom-right or bottom-left), launcherLabel and width (400). AiChatProvider takes hotkey (mod+I; null turns it off) and defaultOpened.

Strings and theme

Every string comes from the chat namespace of the shared dictionary, in English and Bahasa Indonesia; override them app-wide with BuildpadI18nProvider, or per instance with translations. The styles are plain CSS (bp-ai-chat-* classes) drawn with Mantine variables and the --ds-* design tokens, so the chat follows your theme and colour scheme. --bp-ai-chat-width and --bp-ai-chat-z-index (180, below Mantine modals) can be overridden.

Last updated on