Skip to main content

UI Components

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

Installation

npm install @erdoai/ui
Peer Dependencies:
npm install react react-dom

ErdoProvider

Wrap your app with ErdoProvider to configure the SDK:
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>
  );
}
Never expose API keys to the browser. For streaming invocations, use ephemeral tokens or proxy streaming. See Integration Patterns for secure patterns.
Config Options:
OptionTypeDescription
baseUrlstringAPI base URL (or your proxy URL)
tokenstringEphemeral token from createToken() for authenticated data fetching
dataFetcherDataFetcherOptional custom data fetcher (see Custom Data Fetching)
ContentComponentReact.ComponentTypeOptional component for rendering nested content
threadIdstringThread 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):
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
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:
MethodRequiredDescription
fetchDatasetContents(slug, invocationId)YesFetch dataset rows for charts/tables
getDatasetDetails(slug, threadId)NoGet dataset ID and name for download buttons
downloadDataset(datasetId)NoDownload 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:
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:
OptionTypeDescription
botKeystringBot key for the thread (e.g., 'data-analyst')
threadIdstringOptional existing thread ID to resume
onFinish() => voidCalled when message streaming completes
onError(error: Error) => voidCalled on error
Returns:
PropertyTypeDescription
streamingContentsContentItem[]Visible content items ready for rendering
isStreamingbooleanWhether currently streaming
errorError | nullMost recent error
sendMessage(content: string) => Promise<void>Send a message
threadThread | nullCurrent thread object
activeMessagesMessageWithContents[]Raw messages (for advanced use)
setThread(thread: Thread) => voidSet thread manually

useDatasetContents

Fetch dataset contents using plain React state (no React Query required):
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:
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:
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:
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

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

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

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

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

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

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:
PropTypeDescription
titlestringChart title
subtitlestringOptional subtitle
dataany[]Data array
displayConfigChartConfigDisplay configuration
dataConfigDataConfigData mapping configuration
stackedbooleanStack series (bar charts)
disableAnimationbooleanDisable animations
enableDownloadbooleanShow download button (default: true)
onDownloadSuccess(fileName) => voidDownload success callback
onDownloadError(error) => voidDownload error callback
onZoomChange(domain) => voidZoom 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:
// 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, these are already defined. Otherwise, add them to your global CSS:
/* 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
The --chart-1 through --chart-5 variables are used for chart colors. Customize these to match your brand.

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:
import {
  InvocationEvents,  // Renders nested steps/outputs
  StepInvocation,    // Renders a single step
  Output,            // Renders output contents
} from '@erdoai/ui';

Example: Custom Bot Invocation Renderer

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,
  }}
/>
Most applications don’t need custom renderers. The default Content component handles all standard content types including nested bot invocations, charts, tables, and markdown.