Search documentation

Search for docs or ask AI

Building Custom Cards

This guide walks through implementing custom cards in your application. For a conceptual overview of what cards are and when to use them, see Custom Cards.

Build with AI

Tip: Use.to enable Plan mode in Cursor for best results
# Create Pillar Custom Cards for My Actions

Look at my existing Pillar actions and create custom confirmation card components.

## What Custom Cards Do

Instead of a generic confirmation dialog, cards let you:
- Show rich, branded UI for confirmations
- Let users edit AI-extracted data before confirming
- Display previews of what will happen
- Use your existing design system components

## Your Task

### 1. Find My Existing Actions
Look for my Pillar actions definition (usually in lib/pillar/actions.ts or similar).

### 2. Identify Actions That Need Cards
Focus on actions that:
- Modify data (create, update, delete)
- Have multiple parameters the user might want to edit
- Would benefit from a preview (e.g., "Invite 3 members as Admin")

### 3. For Each Card, Generate:

```tsx
// components/cards/[ActionName]Card.tsx
import type { CardComponentProps } from '@pillar-ai/react';
import { useState } from 'react';
// Import my design system components
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';

interface ActionData {
  // Match the dataSchema from my action
}

export function ActionNameCard({
  data,
  onConfirm,
  onCancel,
}: CardComponentProps<ActionData>) {
  const [formData, setFormData] = useState(data);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleConfirm = async () => {
    setLoading(true);
    setError(null);
    try {
      // Call my API if needed
      onConfirm(formData);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="...">
      {/* Editable form fields */}
      {/* Preview of action */}
      {error && <p className="text-red-500">{error}</p>}
      <div className="flex gap-2">
        <Button variant="outline" onClick={onCancel} disabled={loading}>
          Cancel
        </Button>
        <Button onClick={handleConfirm} disabled={loading}>
          {loading ? 'Processing...' : 'Confirm'}
        </Button>
      </div>
    </div>
  );
}
```

### 4. Register Cards in Provider

```tsx
import { InviteMembersCard } from '@/components/cards/InviteMembersCard';
import { ConfirmDeleteCard } from '@/components/cards/ConfirmDeleteCard';

<PillarProvider
  productKey="..."
  publicKey="..."
  cards={{
    invite_members: InviteMembersCard,
    delete_item: ConfirmDeleteCard,
    // ... other cards
  }}
>
```

## Start Here

1. Find my Pillar actions definition
2. Look at my design system (components/ui/ or similar)
3. Identify which actions would benefit from custom cards
4. Generate card components using my design patterns

How It Works

  1. Define an action with type: 'inline_ui' and a card_type
  2. Register a React component for that card_type
  3. When AI suggests the action, your component renders inline
  4. User interacts and confirms, triggering your callback
User: "Invite [email protected] as an admin"
                    ↓
AI extracts email and role
                    ↓
Your InviteMembersCard renders with the data
                    ↓
User confirms → Your API is called

Quick Start

1. Create the Card Component

examples/guides/custom-cards/invite-members-card.tsx
// components/cards/InviteMembersCard.tsx
import type { CardComponentProps } from '@pillar-ai/react';
import { useState } from 'react';
interface InviteData {
emails: string[];
role: 'admin' | 'member';
}
export function InviteMembersCard({
data,
onConfirm,
onCancel,
}: CardComponentProps<InviteData>) {
const [emails, setEmails] = useState(data.emails);
const [loading, setLoading] = useState(false);
const handleConfirm = async () => {
setLoading(true);
try {
await fetch('/api/invitations', {
method: 'POST',
body: JSON.stringify({ emails, role: data.role }),
});
onConfirm({ emails, role: data.role });
} finally {
setLoading(false);
}
};
return (
<div className="p-4 border rounded-lg">
<h3 className="font-semibold mb-3">Invite Team Members</h3>
<div className="flex flex-wrap gap-2 mb-4">
{emails.map(email => (
<span key={email} className="px-2 py-1 bg-gray-100 rounded text-sm">
{email}
<button
onClick={() => setEmails(e => e.filter(x => x !== email))}
className="ml-2"
>
×
</button>
</span>
))}
</div>
<div className="flex gap-2">
<button onClick={onCancel} disabled={loading}>
Cancel
</button>
<button onClick={handleConfirm} disabled={loading}>
{loading ? 'Sending...' : 'Send Invites'}
</button>
</div>
</div>
);
}

2. Register the Card

examples/guides/custom-cards/register-card.tsx
import { PillarProvider } from '@pillar-ai/react';
import { InviteMembersCard } from '@/components/cards/InviteMembersCard';
<PillarProvider
productKey="..."
publicKey="..."
cards={{
invite_members: InviteMembersCard,
}}
>

Component Props

examples/guides/custom-cards/card-component-props.ts
interface CardComponentProps<T> {
/** Data extracted by the AI */
data: T;
/** Call when user confirms */
onConfirm: (modifiedData?: Record<string, unknown>) => void;
/** Call when user cancels */
onCancel: () => void;
/** Report state changes */
onStateChange?: (state: 'loading' | 'success' | 'error', message?: string) => void;
}

Default Fallback

If no card is registered for a card_type, Pillar shows a default confirmation UI with:

  • Action name as title
  • Data displayed as key-value pairs
  • Confirm and Cancel buttons

Best Practices

Let Users Modify Data

The AI extracts data from messages but might make mistakes. Let users edit:

examples/guides/custom-cards/modify-data.tsx
const [emails, setEmails] = useState(data.emails);
// User can add/remove emails before confirming

Show Loading States

examples/guides/custom-cards/loading-states.tsx
const [loading, setLoading] = useState(false);
<button disabled={loading}>
{loading ? 'Processing...' : 'Confirm'}
</button>

Handle Errors

examples/guides/custom-cards/handle-errors.tsx
const [error, setError] = useState<string | null>(null);
{error && (
<div className="text-red-500 text-sm mt-2">{error}</div>
)}

Use Your Design System

Cards render inside your app, so use your existing components:

examples/guides/custom-cards/design-system.tsx
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';

Example: Confirm Delete

examples/guides/custom-cards/confirm-delete-card.tsx
export function ConfirmDeleteCard({ data, onConfirm, onCancel }) {
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-sm text-red-700 mt-1">
This action cannot be undone.
</p>
<div className="flex gap-2 mt-4">
<button onClick={onCancel}>Cancel</button>
<button
onClick={() => onConfirm()}
className="bg-red-600 text-white"
>
Delete
</button>
</div>
</div>
);
}

TypeScript

examples/guides/custom-cards/typescript-example.ts
import type { CardComponentProps, CardComponent } from '@pillar-ai/react';
interface MyData {
items: string[];
count: number;
}
const MyCard: CardComponent<MyData> = ({ data, onConfirm, onCancel }) => {
// data is typed as MyData
return <div>...</div>;
};