Search documentation

Search documentation

Customize your docs experience — choose your preferred framework for code examples:

Inline UI tools

Inline UI tools render your components directly in the chat. When the AI calls an inline_ui tool, it extracts data from the conversation via the inputSchema and passes it to your render component. There is no execute function.

For a conceptual overview, see Tools — Inline UI.

Quick start

Register a tool with type: 'inline_ui' and a render prop:

Quick Start
import { usePillarTool, type ToolRenderProps } from '@pillar-ai/react';
interface InviteData {
emails: string[];
role: string;
}
function InviteCard({ data, sendResult, context }: ToolRenderProps<InviteData>) {
const handleSend = async () => {
await inviteApi.send(data.emails, data.role);
sendResult({ sent: data.emails.length, role: data.role });
};
return (
<div className="p-4 border rounded-lg">
<h3 className="font-semibold mb-3">Invite Team Members</h3>
<p className="text-sm text-gray-600 mb-4">
{data.emails.length} member{data.emails.length > 1 ? 's' : ''} as {data.role}
</p>
<ul className="mb-4 space-y-1">
{data.emails.map(email => (
<li key={email} className="text-sm">{email}</li>
))}
</ul>
<button
onClick={handleSend}
disabled={!context.isReady}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
Send Invites
</button>
</div>
);
}
export function useInviteTools() {
usePillarTool({
name: 'invite_members',
type: 'inline_ui',
description: 'Invite team members to the workspace',
inputSchema: {
type: 'object',
properties: {
emails: { type: 'array', items: { type: 'string' }, description: 'Email addresses' },
role: { type: 'string', enum: ['admin', 'member'], description: 'Role for new members' },
},
required: ['emails'],
},
render: InviteCard,
});
}

The render prop varies by framework:

Frameworkrender value
ReactA React component (ComponentType<ToolRenderProps>)
VueA Vue component created with defineComponent
AngularAn Angular component class with @Input() bindings
Vanilla JSA function (container, data, callbacks, context) => cleanup

Render component props

Your render component receives the following props. The shape is the same across React, Vue, and Angular (Angular uses @Input() decorators instead of function props).

typescript
interface ToolRenderProps<T = Record<string, unknown>> {
/** Data the AI extracted from the conversation via inputSchema */
data: T;
/** Send a result back to the AI agent, continuing the conversation.
* The agent sees this as the tool's response and can act on it. */
sendResult: (result: Record<string, unknown>) => Promise<void>;
/** Position and state info for this card in the chat */
context: ToolCardContext;
}

data

The structured data the AI extracted from the user's message. The shape matches whatever the AI can fill from your inputSchema. Always validate or provide defaults for fields since the AI may omit optional ones.

sendResult

Sends a result back to the AI agent as a new message. The agent sees it and can reason about what to do next. Call this when the user completes an action in your card and you want to continue the conversation.

tsx
const handleCheckout = () => {
cartStore.add(selectedItems);
sendResult({ action: "checkout_requested", items: selectedItems });
};

After calling sendResult, the agent might call another tool (like a checkout flow). You can call sendResult multiple times from the same card.

context

Metadata about where this card sits in the chat:

typescript
interface ToolCardContext {
/** True when this card is the most recent one across all messages */
isLatest: boolean;
/** True when the AI is not streaming — safe to call sendResult */
isReady: boolean;
/** Zero-based message index */
messageIndex: number;
/** Zero-based segment index within the message */
segmentIndex: number;
/** The tool name */
toolName: string;
}

Use context.isLatest to collapse older cards or disable interactions. Use context.isReady to prevent calling sendResult while the AI is still streaming a response.

Vanilla JS render function

In vanilla JS, the render prop is a function instead of a component. It receives the container element, the data, a callbacks object, and a context object:

javascript
render: (container, data, callbacks, context) => {
// container — HTMLElement to render into
// data — same as ToolRenderProps.data
// callbacks.sendResult — same as ToolRenderProps.sendResult
// context — same as ToolCardContext
container.innerHTML = `<div>...</div>`;
// Return a cleanup function (called on unmount)
return () => {
container.innerHTML = "";
};
};

Example: confirm delete

A full example with loading states, error handling, and sendResult:

Confirm Delete
import { useState } from 'react';
import { usePillarTool, type ToolRenderProps } from '@pillar-ai/react';
interface DeleteData {
itemId: string;
itemName: string;
}
function ConfirmDeleteCard({ data, sendResult, context }: ToolRenderProps<DeleteData>) {
const [loading, setLoading] = useState(false);
const handleDelete = async () => {
setLoading(true);
try {
await deleteItem(data.itemId);
sendResult({ deleted: true, itemId: data.itemId });
} catch {
setLoading(false);
}
};
return (
<div className="p-4 border border-red-200 rounded-lg bg-red-50">
<h3 className="font-semibold text-red-900">Delete {data.itemName}?</h3>
<p className="text-red-700 text-sm mt-1">This action cannot be undone.</p>
<div className="flex gap-2 mt-4">
<button
onClick={() => sendResult({ deleted: false })}
disabled={loading || !context.isReady}
>
Cancel
</button>
<button
onClick={handleDelete}
disabled={loading || !context.isReady}
className="bg-red-600 text-white"
>
{loading ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
);
}
export function useDeleteTools() {
usePillarTool({
name: 'delete_item',
type: 'inline_ui',
description: 'Delete an item permanently',
inputSchema: {
type: 'object',
properties: {
itemId: { type: 'string', description: 'ID of the item to delete' },
itemName: { type: 'string', description: 'Display name of the item' },
},
required: ['itemId', 'itemName'],
},
render: ConfirmDeleteCard,
});
}

Best practices

Never pass secrets through sendResult

Data passed to sendResult flows through the SDK pipeline and can appear in telemetry, agent context, and server-side logs. Keep API keys, tokens, passwords, and PII out of the callback. Do your sensitive work inside the card component and only return non-sensitive metadata.

typescript
// Bad — the secret enters the SDK pipeline
sendResult({ apiKey: "plr_abc123...", name: "production" });
// Good — secret stays in component state, only metadata is returned
sendResult({ created: true, keyName: "production" });

Use context.isLatest to manage card state

When the AI calls the same tool multiple times (e.g., multiple search queries), older cards stay in the chat. Disable interactions on stale cards so users act on the most recent one:

tsx
function ResultsCard({ data, sendResult, context }: ToolRenderProps<Results>) {
return (
<div className={context.isLatest ? "" : "opacity-50 pointer-events-none"}>
{/* card content */}
</div>
);
}

Wait for isReady before sending results

If the AI is still streaming a response, calling sendResult can cause issues. Check context.isReady first:

tsx
const handleAction = () => {
if (!context.isReady) return;
sendResult({ action: "confirmed" });
};

Next steps