🥞PancakeJS

Tools vs Widgets

PancakeJS supports two types of capabilities: Tools and Widgets. Understanding when to use each is key to building great AI experiences.

Overview

AspectToolsWidgets
PurposePerform actions or fetch dataInteractive UI experiences
OutputText, structured data, or bothReact components
InteractionSingle request/responseStateful, multi-step
Best forData fetching, calculations, API callsForms, visualizations, workflows

Tools

Tools are functions that an AI assistant can call to perform actions or fetch data. They return text, structured data, or both.

Defining a Tool

import { z } from 'zod';
import { textResult, withData } from '@pancakeapps/server';

app.tool(
  {
    name: 'getWeather',
    description: 'Get current weather for a location',
    inputSchema: z.object({
      city: z.string().describe('City name'),
      units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
    }),
  },
  async (input) => {
    const weather = await fetchWeather(input.city, input.units);

    return withData(
      textResult(`It's ${weather.temp}° and ${weather.conditions} in ${input.city}`),
      weather, // Structured data for further processing
    );
  },
);

Tool Response Types

// Simple text response
return textResult('Operation complete');
// Rich HTML content
return htmlResult('<strong>Success!</strong> Your file was uploaded.');
// Text + structured data
return withData(
  textResult('Found 10 items'),
  { items: [...], total: 10 }
);
// Combine multiple response types
return result()
  .text('Primary message')
  .html('<em>Rich content</em>')
  .data({ key: 'value' })
  .build();

Widgets

Widgets are tools that render a custom UI. They're perfect for interactive experiences, data visualization, or complex user flows.

Defining a Widget

import { renderWidget } from '@pancakeapps/server';

app.widget(
  {
    name: 'flightSearch',
    description: 'Search and book flights',
    inputSchema: z.object({
      origin: z.string(),
      destination: z.string(),
      date: z.string(),
    }),
    ui: {
      entry: 'src/widgets/flight-search.tsx',
      csp: {
        connectDomains: ['api.flights.com'],  // Allow API calls
        imgDomains: ['images.airline.com'],   // Allow images
      },
    },
  },
  async (input) => {
    // Fetch initial data
    const flights = await searchFlights(input);

    return renderWidget('flightSearch', {
      content: [{ type: 'text', text: `Found ${flights.length} flights` }],
      data: { flights, searchParams: input },
    });
  },
);

Widget UI

Widgets are React components that use special hooks to interact with the host:

import {
  UniversalAppProvider,
  useToolInvocation,
  useCallTool,
  useWidgetState,
  useHost,
} from '@pancakeapps/react';

function FlightSearchWidget() {
  // Get initial data from tool invocation
  const { data } = useToolInvocation();

  // Persist state across renders
  const [selectedFlight, setSelectedFlight] = useWidgetState('selected', null);

  // Call other tools from the widget
  const bookFlight = useCallTool('bookFlight');

  // Access host capabilities
  const host = useHost();

  const handleBook = async (flight) => {
    const result = await bookFlight({ flightId: flight.id });
    // Handle booking result
  };

  return (
    <div className={host.context.theme === 'dark' ? 'dark' : 'light'}>
      {data.flights.map((flight) => (
        <FlightCard
          key={flight.id}
          flight={flight}
          onBook={() => handleBook(flight)}
        />
      ))}
    </div>
  );
}

When to Use Each

Use CaseToolWidget
Simple data fetch
Text-based response
Interactive UI
Data visualization
Multi-step workflow
Form input
Real-time updates

Hybrid Approach

Widgets can call tools, enabling powerful patterns where the widget handles UI while delegating actions to tools.

function DataExplorer() {
  const { data: initialData } = useToolInvocation();
  const [data, setData] = useWidgetState('data', initialData);

  const fetchMore = useCallTool('fetchMoreData');

  const handleLoadMore = async () => {
    const more = await fetchMore({ page: data.page + 1 });
    setData({
      ...data,
      items: [...data.items, ...more.items],
      page: more.page,
    });
  };

  return (
    <div>
      <ItemList items={data.items} />
      <button onClick={handleLoadMore}>Load More</button>
    </div>
  );
}

This pattern:

  1. Initial load → Widget tool fetches data and renders UI
  2. User interaction → Widget calls other tools for actions
  3. Update UI → Widget state updates with new data

Best Practices

Tools

  • Keep tools focused on a single task
  • Always provide clear descriptions for AI understanding
  • Use Zod schemas with .describe() for better AI comprehension
  • Return structured data when it might be useful for follow-up actions

Widgets

  • Wrap with <UniversalAppProvider> at the root
  • Use useWidgetState for any state that should persist
  • Check host capabilities before using advanced features
  • Keep initial data fetching in the tool handler, not the widget

Next Steps

On this page