> ## Documentation Index
> Fetch the complete documentation index at: https://docs.erdo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# UI Components

> React components for rendering Erdo agent results

# UI Components

The `@erdoai/ui` package provides React components for rendering agent results, including charts, tables, and formatted content.

## Installation

```bash theme={null}
npm install @erdoai/ui
```

**Peer Dependencies:**

```bash theme={null}
npm install react react-dom
```

## ErdoProvider

Wrap your app with `ErdoProvider` to configure the SDK:

```tsx theme={null}
import { ErdoProvider } from '@erdoai/ui';

function App() {
  return (
    <ErdoProvider
      config={{
        baseUrl: 'https://api.erdo.ai',
        // Use an ephemeral token for authenticated data fetching
        // Token should be obtained from your backend via createToken()
        token: ephemeralToken,
      }}
    >
      {children}
    </ErdoProvider>
  );
}
```

<Warning>
  Never expose API keys to the browser. For streaming invocations, use ephemeral tokens or proxy streaming. See [Integration Patterns](/ts-sdk/integration#client-component-with-streaming) for secure patterns.
</Warning>

**Config Options:**

| Option             | Type                  | Description                                                                      |
| ------------------ | --------------------- | -------------------------------------------------------------------------------- |
| `baseUrl`          | `string`              | API base URL (or your proxy URL)                                                 |
| `token`            | `string`              | Ephemeral token from `createToken()` for authenticated data fetching             |
| `dataFetcher`      | `DataFetcher`         | Optional custom data fetcher (see [Custom Data Fetching](#custom-data-fetching)) |
| `ContentComponent` | `React.ComponentType` | Optional component for rendering nested content                                  |
| `threadId`         | `string`              | Thread ID for dataset operations                                                 |

### ContentComponent

The `ContentComponent` config option allows you to provide a default component for rendering nested content in invocation components (like `InvocationEvents`, `Output`, `StepInvocation`):

```tsx theme={null}
import { ErdoProvider, Content } from '@erdoai/ui';

<ErdoProvider
  config={{
    baseUrl: 'https://api.erdo.ai',
    ContentComponent: Content, // Use SDK's default Content
  }}
>
  {children}
</ErdoProvider>
```

This is useful when:

* You want to customize how content is rendered across all invocation components
* You have your own Content component with custom renderers
* You want to avoid passing `ContentComponent` as a prop to every component

Components will automatically use the provider's `ContentComponent` as a default, but you can still override it via props if needed.

### Custom Data Fetching

The SDK uses plain React state for data fetching (no React Query required). You can provide a custom `DataFetcher` to:

* **Proxy requests** through your own backend (keeping API keys server-side)
* **Add custom authentication** headers or logic
* **Transform data** before it reaches components
* **Use your own API** instead of Erdo's REST endpoints

```tsx theme={null}
import { ErdoProvider, type DataFetcher } from '@erdoai/ui';

const customFetcher: DataFetcher = {
  // Required: fetch dataset contents for charts/tables
  fetchDatasetContents: async (slug, invocationId) => {
    const response = await fetch(`/api/datasets/${slug}?invocationId=${invocationId}`);
    return response.json();
  },

  // Optional: get dataset details for download buttons
  getDatasetDetails: async (slug, threadId) => {
    const response = await fetch(`/api/datasets/${slug}/details?threadId=${threadId}`);
    if (!response.ok) return null;
    return response.json(); // { id: string, name: string }
  },

  // Optional: download dataset as file
  downloadDataset: async (datasetId) => {
    const response = await fetch(`/api/datasets/${datasetId}/download`);
    return response.blob();
  },
};

function App() {
  return (
    <ErdoProvider
      config={{
        baseUrl: 'https://api.erdo.ai',
        dataFetcher: customFetcher,
      }}
    >
      {children}
    </ErdoProvider>
  );
}
```

**DataFetcher Interface:**

| Method                                     | Required | Description                                  |
| ------------------------------------------ | -------- | -------------------------------------------- |
| `fetchDatasetContents(slug, invocationId)` | Yes      | Fetch dataset rows for charts/tables         |
| `getDatasetDetails(slug, threadId)`        | No       | Get dataset ID and name for download buttons |
| `downloadDataset(datasetId)`               | No       | Download dataset file as Blob                |

If no `dataFetcher` is provided, the SDK falls back to the REST API using `baseUrl` and `token`/`authToken` for authentication.

## Hooks

### useThread

Send messages to threads with streaming support:

```tsx theme={null}
import { useThread, Content } from '@erdoai/ui';

function ChatInterface() {
  const { isStreaming, streamingContents, error, sendMessage } = useThread({
    botKey: 'data-analyst',
    onFinish: () => console.log('Done'),
    onError: (error) => console.error('Error:', error),
  });

  return (
    <div>
      <button onClick={() => sendMessage('Analyze data')}>
        {isStreaming ? 'Analyzing...' : 'Analyze'}
      </button>

      {error && <div className="error">{error.message}</div>}

      {streamingContents.map((content) => (
        <Content key={content.id} content={content} />
      ))}
    </div>
  );
}
```

**Options:**

| Option     | Type                     | Description                                     |
| ---------- | ------------------------ | ----------------------------------------------- |
| `botKey`   | `string`                 | Bot key for the thread (e.g., `'data-analyst'`) |
| `threadId` | `string`                 | Optional existing thread ID to resume           |
| `onFinish` | `() => void`             | Called when message streaming completes         |
| `onError`  | `(error: Error) => void` | Called on error                                 |

**Returns:**

| Property            | Type                                 | Description                               |
| ------------------- | ------------------------------------ | ----------------------------------------- |
| `streamingContents` | `ContentItem[]`                      | Visible content items ready for rendering |
| `isStreaming`       | `boolean`                            | Whether currently streaming               |
| `error`             | `Error \| null`                      | Most recent error                         |
| `sendMessage`       | `(content: string) => Promise<void>` | Send a message                            |
| `thread`            | `Thread \| null`                     | Current thread object                     |
| `activeMessages`    | `MessageWithContents[]`              | Raw messages (for advanced use)           |
| `setThread`         | `(thread: Thread) => void`           | Set thread manually                       |

### useDatasetContents

Fetch dataset contents using plain React state (no React Query required):

```tsx theme={null}
import { useDatasetContents } from '@erdoai/ui';

function DataView({ datasetSlug, invocationId }) {
  const { data, isLoading, error, refetch } = useDatasetContents(datasetSlug, invocationId);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <button onClick={refetch}>Refresh</button>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}
```

The hook uses the `DataFetcher` from the provider if available, otherwise falls back to the REST API.

### useMultipleDatasetContents

Fetch multiple datasets in parallel:

```tsx theme={null}
import { useMultipleDatasetContents } from '@erdoai/ui';

function MultiDataView({ invocationId }) {
  const results = useMultipleDatasetContents(
    ['dataset-1', 'dataset-2'],
    invocationId
  );

  const isLoading = results.some(r => r.isLoading);
  const allData = results.map(r => r.data || []);

  if (isLoading) return <div>Loading...</div>;

  return <pre>{JSON.stringify(allData, null, 2)}</pre>;
}
```

## Content Renderers

### Content

Auto-routes to the appropriate renderer based on content type:

```tsx theme={null}
import { Content } from '@erdoai/ui';

function ResultView({ content }) {
  return <Content content={content} />;
}
```

#### Custom Renderers

Override how specific content types are rendered using the `components` prop:

```tsx theme={null}
import { Content } from '@erdoai/ui';

// Custom component receives { content, className } props
function MyBotInvocationRenderer({ content, className }) {
  return (
    <div className={className}>
      <h3>Bot: {content.content.bot_name}</h3>
      {/* Custom rendering logic */}
    </div>
  );
}

function ResultView({ content }) {
  return (
    <Content
      content={content}
      components={{
        'bot_invocation': MyBotInvocationRenderer,
        'my_custom_type': MyCustomRenderer,
      }}
    />
  );
}
```

The `components` prop is a `Record<string, ComponentType>` where:

* **Key**: The `content_type` or `ui_content_type` string (e.g., `'bot_invocation'`, `'text'`, `'chart'`)
* **Value**: A React component that receives `{ content, className }` props

When Content encounters a content type, it first checks if there's a custom renderer in `components`, then falls back to built-in renderers.

### Individual Renderers

```tsx theme={null}
import {
  TextContent,
  JsonContent,
  MarkdownContent,
  TableContent,
  ChartContent,
} from '@erdoai/ui';

// Text
<TextContent content="Hello world" />

// JSON
<JsonContent content={{ key: 'value' }} />

// Markdown
<MarkdownContent text="# Heading\n\nParagraph text" />

// Table
<TableContent
  columns={[{ key: 'name', column_name: 'Name' }]}
  data={[{ name: 'Alice' }, { name: 'Bob' }]}
/>
```

## Chart Components

All charts support download functionality via the download button.

### BarChart

```tsx theme={null}
import { BarChart } from '@erdoai/ui';

<BarChart
  title="Monthly Sales"
  data={[
    { month: 'Jan', revenue: 1000 },
    { month: 'Feb', revenue: 1200 },
  ]}
  displayConfig={{
    month: { label: 'Month' },
    revenue: { label: 'Revenue', color: '#3b82f6' },
  }}
  dataConfig={{
    chartType: 'bar',
    xAxis: { key: 'month', label: 'Month', type: 'category' },
    yAxes: [{ key: 'revenue', label: 'Revenue ($)', type: 'number' }],
    series: [{ key: 'revenue', name: 'Revenue', color: '#3b82f6' }],
  }}
/>
```

### LineChart

```tsx theme={null}
import { LineChart } from '@erdoai/ui';

<LineChart
  title="Revenue Trend"
  data={data}
  displayConfig={config}
  dataConfig={{
    chartType: 'line',
    xAxis: { key: 'date', label: 'Date', type: 'date', format: 'MMM yyyy' },
    yAxes: [{ key: 'revenue', label: 'Revenue', type: 'number' }],
    series: [{ key: 'revenue', name: 'Revenue', color: '#10b981' }],
  }}
/>
```

### PieChart

```tsx theme={null}
import { PieChart } from '@erdoai/ui';

<PieChart
  title="Sales by Category"
  data={[
    { category: 'Electronics', sales: 45 },
    { category: 'Clothing', sales: 30 },
    { category: 'Food', sales: 25 },
  ]}
  displayConfig={config}
  dataConfig={{
    chartType: 'pie',
    xAxis: { key: 'category', label: 'Category' },
    yAxes: [{ key: 'sales', label: 'Sales %' }],
    series: [{ key: 'sales', name: 'Sales', color: '#6366f1' }],
  }}
/>
```

### ScatterChart

```tsx theme={null}
import { ScatterChart } from '@erdoai/ui';

<ScatterChart
  title="Price vs Quantity"
  data={data}
  displayConfig={config}
  dataConfig={{
    chartType: 'scatter',
    xAxis: { key: 'price', label: 'Price', type: 'number' },
    yAxes: [{ key: 'quantity', label: 'Quantity', type: 'number' }],
    series: [{ key: 'quantity', name: 'Quantity', color: '#f59e0b' }],
  }}
/>
```

### HeatmapChart

```tsx theme={null}
import { HeatmapChart } from '@erdoai/ui';

<HeatmapChart
  title="Activity Heatmap"
  data={data}
  displayConfig={config}
  dataConfig={{
    chartType: 'heatmap',
    xAxis: { key: 'hour', label: 'Hour', type: 'number' },
    yAxes: [{ key: 'day', label: 'Day', type: 'category' }],
    series: [{ key: 'value', name: 'Activity', color: '#ef4444' }],
  }}
/>
```

## Chart Props

All chart components accept these common props:

| Prop                | Type                 | Description                          |
| ------------------- | -------------------- | ------------------------------------ |
| `title`             | `string`             | Chart title                          |
| `subtitle`          | `string`             | Optional subtitle                    |
| `data`              | `any[]`              | Data array                           |
| `displayConfig`     | `ChartConfig`        | Display configuration                |
| `dataConfig`        | `DataConfig`         | Data mapping configuration           |
| `stacked`           | `boolean`            | Stack series (bar charts)            |
| `disableAnimation`  | `boolean`            | Disable animations                   |
| `enableDownload`    | `boolean`            | Show download button (default: true) |
| `onDownloadSuccess` | `(fileName) => void` | Download success callback            |
| `onDownloadError`   | `(error) => void`    | Download error callback              |
| `onZoomChange`      | `(domain) => void`   | Zoom change callback                 |

## Styling

`@erdoai/ui` components use Tailwind CSS classes and CSS custom properties (variables). You need to configure both for the components to render correctly.

### 1. Tailwind Configuration

Add `@erdoai/ui` to your Tailwind content paths:

```js theme={null}
// tailwind.config.js
module.exports = {
  content: [
    './node_modules/@erdoai/ui/**/*.js',
    // ... your content paths
  ],
};
```

### 2. CSS Variables

Components rely on CSS variables for theming. If you're using [shadcn/ui](https://ui.shadcn.com/), these are already defined. Otherwise, add them to your global CSS:

```css theme={null}
/* globals.css or app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;
    --popover: 0 0% 100%;
    --popover-foreground: 222.2 84% 4.9%;
    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;
    --secondary: 210 40% 96.1%;
    --secondary-foreground: 222.2 47.4% 11.2%;
    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;
    --accent: 210 40% 96.1%;
    --accent-foreground: 222.2 47.4% 11.2%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 210 40% 98%;
    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;
    --ring: 222.2 84% 4.9%;
    --radius: 0.5rem;
    --chart-1: 12 76% 61%;
    --chart-2: 173 58% 39%;
    --chart-3: 197 37% 24%;
    --chart-4: 43 74% 66%;
    --chart-5: 27 87% 67%;
  }

  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --card: 222.2 84% 4.9%;
    --card-foreground: 210 40% 98%;
    --popover: 222.2 84% 4.9%;
    --popover-foreground: 210 40% 98%;
    --primary: 210 40% 98%;
    --primary-foreground: 222.2 47.4% 11.2%;
    --secondary: 217.2 32.6% 17.5%;
    --secondary-foreground: 210 40% 98%;
    --muted: 217.2 32.6% 17.5%;
    --muted-foreground: 215 20.2% 65.1%;
    --accent: 217.2 32.6% 17.5%;
    --accent-foreground: 210 40% 98%;
    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 210 40% 98%;
    --border: 217.2 32.6% 17.5%;
    --input: 217.2 32.6% 17.5%;
    --ring: 212.7 26.8% 83.9%;
    --chart-1: 220 70% 50%;
    --chart-2: 160 60% 45%;
    --chart-3: 30 80% 55%;
    --chart-4: 280 65% 60%;
    --chart-5: 340 75% 55%;
  }
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }
}
```

### Using with shadcn/ui

If you're already using shadcn/ui, the CSS variables are configured automatically. Just ensure `@erdoai/ui` is in your Tailwind content paths.

### Minimal Setup (No shadcn/ui)

If you're not using shadcn/ui but want Erdo charts to work:

1. Add the CSS variables above to your global styles
2. Configure Tailwind content paths
3. Components will use your theme colors automatically

<Note>
  The `--chart-1` through `--chart-5` variables are used for chart colors. Customize these to match your brand.
</Note>

## Advanced: Custom Content Renderers

The `Content` component handles all content types automatically. For advanced use cases, you can override specific content type renderers using the `components` prop.

### Building Block Components

When building custom renderers that need to display nested content (like bot invocations with steps), these building block components are available:

```tsx theme={null}
import {
  InvocationEvents,  // Renders nested steps/outputs
  StepInvocation,    // Renders a single step
  Output,            // Renders output contents
} from '@erdoai/ui';
```

### Example: Custom Bot Invocation Renderer

```tsx theme={null}
import { Content, InvocationEvents } from '@erdoai/ui';

function MyBotInvocationRenderer({ content, className }) {
  const data = content.content;
  const botInvocation = content.botInvocation;

  return (
    <div className={className}>
      <h3>Custom Bot: {data.bot_name}</h3>
      {botInvocation && (
        <InvocationEvents eventsByID={botInvocation.eventsByID} />
      )}
    </div>
  );
}

// Use in Content
<Content
  content={content}
  components={{
    'bot_invocation': MyBotInvocationRenderer,
  }}
/>
```

<Note>
  Most applications don't need custom renderers. The default `Content` component handles all standard content types including nested bot invocations, charts, tables, and markdown.
</Note>
