SDK: React Hooks
Use FlapjackProvider and useChat to build streaming chat UIs with Flapjack agents in React.
The @flapjack/sdk/react subpath export provides React components and hooks for building chat UIs.
npm install @flapjack/sdk
FlapjackProvider
Wrap your app (or the chat section) with FlapjackProvider to initialize the SDK context.
Security warning: The example below uses NEXT_PUBLIC_ environment variables for prototyping. This exposes your API key in the client bundle. For production, use the server proxy pattern instead.
import { FlapjackProvider } from '@flapjack/sdk/react';
function App() {
return (
<FlapjackProvider config={{
apiKey: process.env.NEXT_PUBLIC_FLAPJACK_API_KEY!, // ⚠️ Dev only — use server proxy in production
baseUrl: process.env.NEXT_PUBLIC_FLAPJACK_BASE_URL,
}}>
{/* Chat components go here */}
</FlapjackProvider>
);
}
Props
| Prop | Type | Required | Description |
|---|---|---|---|
config | FlapjackConfig | Yes | SDK configuration with apiKey and optional baseUrl |
children | React.ReactNode | Yes | Child components |
📋 Copy as prompt
Wrap my React app with
FlapjackProviderfrom@flapjack/sdk/react. Use my API key from theNEXT_PUBLIC_FLAPJACK_API_KEYenvironment variable.
useFlapjack
Access the FlapjackClient instance from context. Useful when you need to call SDK methods outside of useChat (e.g., listing agents, managing knowledge, or calling runner APIs).
import { useFlapjack } from '@flapjack/sdk/react';
function AgentList() {
const { client } = useFlapjack();
const [agents, setAgents] = useState<Agent[]>([]);
useEffect(() => {
client.listAgents().then(setAgents);
}, [client]);
return <ul>{agents.map(a => <li key={a.id}>{a.name}</li>)}</ul>;
}
Return Value
| Field | Type | Description |
|---|---|---|
client | FlapjackClient | The SDK client instance from the nearest FlapjackProvider |
Throws if called outside of a FlapjackProvider.
useChat
The useChat hook manages conversation state, message sending, and streaming for a specific agent.
import { useChat } from '@flapjack/sdk/react';
function ChatWidget({ agentId }: { agentId: string }) {
const { messages, sendMessage, isStreaming, error, reset, stop, threadId } = useChat(agentId);
return (
<div>
{messages.map((msg, i) => (
<div key={i} className={msg.role === 'user' ? 'user' : 'assistant'}>
{msg.content}
</div>
))}
{isStreaming && <div className="typing">Thinking...</div>}
{error && <div className="error">{error}</div>}
<input
onKeyDown={(e) => {
if (e.key === 'Enter' && !isStreaming) {
sendMessage(e.currentTarget.value);
e.currentTarget.value = '';
}
}}
disabled={isStreaming}
placeholder="Type a message..."
/>
{isStreaming && <button onClick={stop}>Stop</button>}
<button onClick={reset}>New conversation</button>
</div>
);
}
Parameters
useChat(agentId: string, options?: UseChatOptions)
| Name | Type | Description |
|---|---|---|
agentId | string | The agent's UUID |
options.threadId | string? | Resume an existing thread — loads message history on mount |
options.tools | ToolDef[]? | Custom tool definitions for client-side execution |
options.onToolCall | Function? | Handler for client-side tool calls (auto-submits results) |
options.onCustomEvent | (kind: string, payload: unknown) => void? | Called when a custom SSE event is received. See Streaming: Custom Events. |
options.onPlanActivity | (toolName: string) => void? | Called when plan/todo tools fire (e.g. plan_create, todo_update) |
options.resourceBindings | ResourceBinding[]? | Attach resource context to threads |
options.modelOverride | string? | Override the agent's default model |
options.webOverrides | object? | Override web tool settings per message |
options.memoryOverrides | object? | Override memory settings per message |
options.planOverrides | object? | Override plan settings per message |
options.computerOverrides | object? | Override computer settings per message |
options.onProfileSwitch | (proposal: ProfileSwitchProposal) => void? | Called when the agent proposes switching to a different tool profile |
options.toolProfile | string? | Use a specific tool profile for all messages |
options.orgId | string? | Target org (multi-org only) |
Return Value
| Field | Type | Description |
|---|---|---|
messages | Message[] | All messages in the conversation |
sendMessage | (content: string, options?: SendMessageOptions) => Promise<void> | Send a message and trigger streaming |
addSystemMessage | (meta, options?) => boolean | Inject a system activity indicator (see below). Returns true if agent turn was sent. |
isStreaming | boolean | true while the agent is responding |
error | string | null | Error message if something went wrong |
reset | () => void | Clear messages and start a new thread |
stop | () => void | Stop the current streaming response |
threadId | string | null | Current thread ID (null before first message) |
loadThread | (threadId: string) => Promise<void> | Switch to a different thread — aborts any active stream and loads message history |
activeProfile | string | null | Currently active tool profile |
switchProfile | (profileKey: string) => Promise<void> | Switch the active tool profile, persists to thread and notifies agent |
Message Type
type Message = {
role: 'user' | 'assistant';
content: string;
/** When set, renders as a compact system activity pill instead of a chat bubble. */
systemMessage?: SystemMessageMeta;
};
type SystemMessageMeta = {
icon: string; // 'check' | 'tool' | 'info' | 'error' or any emoji/character
label: string; // Short human-readable label
};
addSystemMessage
Injects a system activity message into the chat. The message appears as a compact, centered pill (icon + label) instead of a regular user bubble. Used for plan approvals, tool confirmations, MCP events, etc.
const { addSystemMessage } = useChat(agentId);
// Trigger an agent response (default)
addSystemMessage({ icon: 'check', label: 'Plan approved' }, {
content: 'Approved. Please proceed with the plan.',
});
// Visual-only — no agent turn
addSystemMessage({ icon: 'info', label: 'Memory saved' }, {
sendToAgent: false,
});
// With extra send options
addSystemMessage({ icon: 'tool', label: 'Tool confirmed' }, {
content: 'Execute the search tool.',
messageOptions: { planOverrides: { enabled: true } },
});
Options
| Field | Type | Default | Description |
|---|---|---|---|
content | string | meta.label | Text sent to the agent |
sendToAgent | boolean | true | Whether to trigger an agent response |
messageOptions | SendMessageOptions | — | Extra options forwarded to the agent turn |
Return value: Returns true if the agent turn was triggered, false if the message was added as visual-only (either because sendToAgent was false or because a stream was in progress).
If called while streaming is active, the indicator appears in the UI but the agent turn is not sent (visual-only fallback). Check the return value if you need to know whether the agent received the message.
Resuming a Thread
Pass a threadId in the options to resume an existing conversation. Message history is loaded automatically on mount.
function ResumedChat({ agentId, threadId }: { agentId: string; threadId: string }) {
const { messages, sendMessage, isStreaming } = useChat(agentId, { threadId });
// messages are populated with the thread's history on mount
return <div>{/* render messages */}</div>;
}
Switching Threads
Use loadThread to switch to a different thread at runtime. This aborts any active stream and loads the new thread's message history.
function ChatWithHistory({ agentId }: { agentId: string }) {
const { messages, sendMessage, loadThread, threadId } = useChat(agentId);
async function handleSelectThread(id: string) {
await loadThread(id);
// messages now contain the selected thread's history
}
return (
<div>
<ThreadList onSelect={handleSelectThread} />
<Messages messages={messages} />
</div>
);
}
Copy as prompt
Use the
useChathook with{ threadId }option to resume an existing Flapjack conversation. UseloadThread(id)to switch between threads dynamically.
Built-in Icon Keys
| Key | Glyph | Use case |
|---|---|---|
check | ✓ | Plan approved, action confirmed |
tool | 🔧 | Tool / MCP activity |
info | ℹ | General information |
error | ✕ | Error / rejection |
Any other string is rendered as-is — pass an emoji (e.g. "🚀") or character directly.
usePlan
The usePlan hook manages plan state for a thread. Use it alongside useChat to build plan-aware UIs.
import { usePlan } from '@flapjack/sdk/react';
function PlanView({ threadId }: { threadId: string | null }) {
const { plan, planOpen, setPlanOpen, fetchPlan } = usePlan(threadId);
if (!plan) return null;
return (
<div>
<h3>{plan.title}</h3>
{plan.todos?.map((todo) => (
<div key={todo.id}>{todo.status === 'done' ? '✓' : '○'} {todo.content}</div>
))}
</div>
);
}
Parameters
| Name | Type | Description |
|---|---|---|
threadId | string | null | The thread's UUID. Plan auto-fetches on change. |
Return Value
| Field | Type | Description |
|---|---|---|
plan | Plan | null | Current plan with todos |
setPlan | (plan: Plan | null) => void | Manually set plan state |
planOpen | boolean | Whether plan panel is visible |
setPlanOpen | (open: boolean) => void | Toggle plan visibility |
planMode | boolean | Whether plan mode is active |
setPlanMode | (mode: boolean) => void | Toggle plan mode |
fetchPlan | () => Promise<void> | Manually fetch the latest plan |
debouncedFetchPlan | () => void | Debounced fetch (500ms) — use for plan_result events |
Complete Example
import { useState } from 'react';
import { FlapjackProvider, useChat } from '@flapjack/sdk/react';
export default function App() {
return (
<FlapjackProvider config={{
apiKey: process.env.NEXT_PUBLIC_FLAPJACK_API_KEY!, // ⚠️ Dev only
}}>
<Chat agentId="your-agent-id" />
</FlapjackProvider>
);
}
function Chat({ agentId }: { agentId: string }) {
const { messages, sendMessage, isStreaming, error, reset } = useChat(agentId);
const [input, setInput] = useState('');
const handleSend = () => {
if (!input.trim() || isStreaming) return;
sendMessage(input);
setInput('');
};
return (
<div style={{ maxWidth: 600, margin: '0 auto', padding: 20 }}>
<div style={{ minHeight: 400, overflowY: 'auto' }}>
{messages.map((msg, i) => (
<div key={i} style={{
padding: 8,
margin: '4px 0',
background: msg.role === 'user' ? '#e3f2fd' : '#f5f5f5',
borderRadius: 8,
}}>
<strong>{msg.role}:</strong> {msg.content}
</div>
))}
{isStreaming && <div style={{ color: '#999' }}>Thinking...</div>}
{error && <div style={{ color: 'red' }}>{error}</div>}
</div>
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
disabled={isStreaming}
placeholder="Type a message..."
style={{ flex: 1, padding: 8 }}
/>
<button onClick={handleSend} disabled={isStreaming}>Send</button>
<button onClick={reset}>Reset</button>
</div>
</div>
);
}
📋 Copy as prompt
Build a complete Flapjack chat page using React with
FlapjackProvideranduseChat. Include a message list, streaming indicator, error display, controlled text input, send button, and reset button. Style it simply with inline styles.
Pre-Built Components
The @flapjack/sdk/components export provides ready-made chat UI components:
import { ChatPanel, FloatingChat, PlanPanel } from '@flapjack/sdk/components';
| Component | Description |
|---|---|
ChatPanel | Full chat UI (messages + input) |
ChatMessages | Message display area |
ChatMessage | Individual message bubble |
ChatInput | Input field with plan integration |
ChatEmpty | Empty state placeholder |
ChatLoading | Loading indicator |
ChatToolCall | Tool execution display |
FloatingChat | Floating chat bubble widget |
PlanPanel | Plan display and approval UI |
SystemUserMessage | System activity indicator (compact pill with icon + label) |
These components use inline styles and work without any CSS framework. Pass useChat and usePlan return values as props.
SystemUserMessage
Renders a system activity indicator as a compact, centered pill instead of a regular chat bubble. Used for plan approvals, tool confirmations, MCP events, etc.
import { SystemUserMessage } from '@flapjack/sdk/components';
<SystemUserMessage meta={{ icon: 'check', label: 'Plan approved' }} />
<SystemUserMessage meta={{ icon: 'tool', label: 'Search executed' }} />
<SystemUserMessage meta={{ icon: '🚀', label: 'Deployed' }} />
| Prop | Type | Description |
|---|---|---|
meta | SystemMessageMeta | Object with icon (key or emoji) and label (short text) |
className | string? | Optional CSS class name |
Custom Events in Components
ChatPanel supports custom event handling via the onCustomEvent prop:
| Prop | Component | Type | Description |
|---|---|---|---|
onCustomEvent | ChatPanel | (kind: string, payload: unknown) => void | Callback fired for each custom SSE event |
<ChatPanel
agentId={agentId}
onCustomEvent={(kind, payload) => {
if (kind === 'suggestion') showSuggestionToast(payload);
if (kind === 'navigation_proposal') showNavigationCard(payload);
}}
/>
Security Note
Using FlapjackProvider with an API key directly in client-side code exposes the key in the browser. For production applications, use the server proxy pattern instead.
Next Steps
- Server Proxy — secure API key handling for production
- Examples — more complete examples
- Concepts: Streaming — SSE event types