# Records Table

> Show records in a table where some columns are filled in by an agent, one cell at a time.

Source: https://ai.honestui.com/docs/components/records-table

```tsx
"use client"

import * as React from "react"

import {
  RecordsTable,
  type GeneratedCell,
  type RecordsTableRow,
} from "@/components/ai/records-table"
import { Button } from "@/components/ui/button"

// Sample records and the summary each row resolves to.
const sampleRecords = [
  { id: "a", name: "Northwind Dairy", region: "EMEA", summary: "Pays on time; two open invoices." },
  { id: "b", name: "Harbor Paper Co.", region: "APAC", summary: "One invoice 14 days late." },
  { id: "c", name: "Ridge Tooling", region: "AMER", summary: "No open invoices." },
]

const CELL_MILLISECONDS = 1100

/**
 * No model fills these cells. A timer completes one cell at a time, and the
 * second one fails on its first pass to show the error state and Retry.
 */
export function RecordsTableDemo() {
  const [completed, setCompleted] = React.useState(0)
  const [isRetried, setIsRetried] = React.useState(false)
  const isRunning = completed < sampleRecords.length

  React.useEffect(() => {
    if (!isRunning) return
    const timer = window.setTimeout(() => setCompleted((count) => count + 1), CELL_MILLISECONDS)
    return () => window.clearTimeout(timer)
  }, [completed, isRunning])

  const rows = sampleRecords.map((record, index): RecordsTableRow => {
    let cell: GeneratedCell = { status: "queued" }
    if (index < completed) {
      cell =
        record.id === "b" && !isRetried
          ? { status: "error", message: "Timed out" }
          : { status: "done", value: record.summary }
    } else if (index === completed) {
      cell = { status: "running" }
    }
    return {
      id: record.id,
      name: record.name,
      cells: { name: record.name, region: record.region },
      generated: { summary: cell },
    }
  })

  return (
    <div className="flex w-full max-w-xl min-w-0 flex-col items-start gap-[var(--hui-space-4)]">
      <RecordsTable
        label="Suppliers"
        columns={[
          { id: "name", header: "Supplier" },
          { id: "region", header: "Region" },
          { id: "summary", header: "Payment summary", isGenerated: true },
        ]}
        rows={rows}
        onRetry={() => setIsRetried(true)}
      />
      <Button
        variant="secondary"
        size="sm"
        disabled={isRunning}
        onClick={() => {
          setIsRetried(false)
          setCompleted(0)
        }}
      >
        Run the sample again
      </Button>
    </div>
  )
}

```

## Overview [#overview]

Records Table is a table in which some columns are generated: an agent reads each record and fills in a summary, a category, a score. Each generated cell shows where it stands, and a line under the table counts how many are done.

It shows the state you pass for every cell. Running the agent, and deciding the order, are your application's work.

Use [Data Grid](https://www.honestui.com/docs/product/data-grid) when people need to sort, resize, or edit columns. The Beautiful UI original also had column settings and resizing, which Data Grid already covers.

## Anatomy [#anatomy]

<Anatomy title="RecordsTable">
  <AnatomyItem name="Columns" description="Ordinary columns, and generated columns marked with an icon" />

  <AnatomyItem name="Generated cell" description="Queued, working, a value, or an error with Retry" />

  <AnatomyItem name="Progress" description="A count of generated cells that are done" />
</Anatomy>

Installing `records-table` brings [Button](https://www.honestui.com/docs/components/button) and [Table](https://www.honestui.com/docs/components/table) along as registry dependencies.

## Behavior [#behavior]

### Generated cells [#generated-cells]

A generated cell is `queued`, `running`, `done` with a value, or `error` with an optional message. A cell with no state reads as queued.

### Retry [#retry]

Pass `onRetry` to show Retry in every failed cell. It calls your handler with the row and column ids.

### Progress [#progress]

The line under the table counts done cells across every generated column. It is left out when the table has no generated columns.

## Accessibility [#accessibility]

The table uses native markup with column headers and is named by `label`. A generated column's header is read with "Generated: " before it. Queued, working, and failed are words in the cell, so no state depends on an icon or a color.

Retry is named with its column and row, such as "Retry Payment summary for Harbor Paper Co.". The progress line is a polite status, so each completed cell is announced as a count, not as a re-reading of the table. With reduced motion, the working icon does not rotate.

| Key                                  | Action                        |
| ------------------------------------ | ----------------------------- |
| <kbd>Tab</kbd>                       | Move across the Retry buttons |
| <kbd>Enter</kbd> or <kbd>Space</kbd> | Retry the focused cell        |

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;@honestai/records-table&#x22;]" />
  

  
    
      
        Add the components it is built from.
      

      <CliBlock commands="[&#x22;button&#x22;, &#x22;table&#x22;]" />

      
        Copy and paste this file into your project.
      

      ### components/ai/records-table.tsx

```tsx
"use client"

import * as React from "react"
import {
  CircleAlert as CircleAlertIcon,
  LoaderCircle as LoaderCircleIcon,
  Sparkles as SparklesIcon,
} from "honestui/icons"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

export type RecordsTableColumn = {
  id: string
  header: string
  /** Marks a column whose cells an agent fills in. */
  isGenerated?: boolean
}

export type GeneratedCell =
  | { status: "queued" }
  | { status: "running" }
  | { status: "done"; value: React.ReactNode }
  | { status: "error"; message?: string }

export type RecordsTableRow = {
  id: string
  /** Names the row in button labels, such as the record's title. */
  name: string
  /** The cell for each ordinary column `id`. */
  cells: Record<string, React.ReactNode>
  /** The state of each generated column `id`. A missing entry reads as queued. */
  generated?: Record<string, GeneratedCell>
}

export type RecordsTableLabels = {
  generated: string
  queued: string
  running: string
  failed: string
  retry: string
  retryCell: (column: string, row: string) => string
  progress: (done: number, total: number) => string
}

export const defaultRecordsTableLabels: RecordsTableLabels = {
  generated: "Generated",
  queued: "Queued",
  running: "Working…",
  failed: "Failed",
  retry: "Retry",
  retryCell: (column, row) => `Retry ${column} for ${row}`,
  progress: (done, total) => `${done} of ${total} generated cells done`,
}

// Table's own header color is too light for small text, so these headers use the secondary foreground.
const headText = "text-[var(--hui-color-foreground-base-secondary)]!"

export type RecordsTableProps = {
  /** Names the table for screen readers. */
  label: string
  columns: RecordsTableColumn[]
  rows: RecordsTableRow[]
  /** Passing it shows Retry in every failed cell. */
  onRetry?: (rowId: string, columnId: string) => void
  labels?: Partial<RecordsTableLabels>
  className?: string
}

/**
 * A table of records where some columns are filled in by an agent, cell by
 * cell. Each generated cell shows the state you pass for it: queued, running,
 * done with a value, or failed.
 */
function RecordsTable({
  label,
  columns,
  rows,
  onRetry,
  labels: labelsProp,
  className,
}: RecordsTableProps) {
  const labels = { ...defaultRecordsTableLabels, ...labelsProp }
  const generatedColumns = columns.filter((column) => column.isGenerated)
  const total = generatedColumns.length * rows.length
  const done = rows.reduce(
    (count, row) =>
      count +
      generatedColumns.filter((column) => row.generated?.[column.id]?.status === "done").length,
    0
  )

  return (
    <div data-slot="records-table" className={cn("flex w-full min-w-0 flex-col gap-[var(--hui-space-3)]", className)}>
      <Table aria-label={label}>
        <TableHeader>
          <TableRow>
            {columns.map((column) => (
              <TableHead key={column.id} scope="col" className={headText}>
                <span className="inline-flex items-center gap-[var(--hui-space-2)]">
                  {column.isGenerated && (
                    <>
                      <SparklesIcon aria-hidden="true" className="size-3.5 shrink-0" />
                      <span className="sr-only">{labels.generated}: </span>
                    </>
                  )}
                  {column.header}
                </span>
              </TableHead>
            ))}
          </TableRow>
        </TableHeader>
        <TableBody>
          {rows.map((row) => (
            <TableRow key={row.id}>
              {columns.map((column) => (
                <TableCell key={column.id} data-generated={column.isGenerated ? "" : undefined}>
                  {column.isGenerated ? (
                    <GeneratedCellContent
                      cell={row.generated?.[column.id] ?? { status: "queued" }}
                      labels={labels}
                      retryLabel={labels.retryCell(column.header, row.name)}
                      onRetry={onRetry ? () => onRetry(row.id, column.id) : undefined}
                    />
                  ) : (
                    row.cells[column.id]
                  )}
                </TableCell>
              ))}
            </TableRow>
          ))}
        </TableBody>
      </Table>
      {total > 0 && (
        // Announced as the count changes, so progress is heard without reading every cell.
        <p
          role="status"
          className="m-0 text-[var(--hui-color-foreground-base-secondary)] [font-size:var(--hui-font-size-small)] [line-height:var(--hui-line-height-small)]"
        >
          {labels.progress(done, total)}
        </p>
      )}
    </div>
  )
}

function GeneratedCellContent({
  cell,
  labels,
  retryLabel,
  onRetry,
}: {
  cell: GeneratedCell
  labels: RecordsTableLabels
  retryLabel: string
  onRetry?: () => void
}) {
  if (cell.status === "done") return <>{cell.value}</>

  if (cell.status === "error") {
    return (
      <span className="flex min-w-0 flex-wrap items-center gap-[var(--hui-space-2)]">
        <CircleAlertIcon strokeWidth={1.5} strokeLinecap="round"
          aria-hidden="true"
          className="size-3.5 shrink-0 text-[var(--hui-color-foreground-danger-primary)]"
        />
        <span className="min-w-0 break-words">{cell.message ?? labels.failed}</span>
        {onRetry && (
          <Button variant="secondary" size="xs" aria-label={retryLabel} onClick={onRetry}>
            {labels.retry}
          </Button>
        )}
      </span>
    )
  }

  return (
    <span className="flex items-center gap-[var(--hui-space-2)] text-[var(--hui-color-foreground-base-secondary)]">
      {cell.status === "running" && (
        <LoaderCircleIcon aria-hidden="true" className="size-3.5 shrink-0 motion-safe:animate-spin" />
      )}
      {cell.status === "running" ? labels.running : labels.queued}
    </span>
  )
}

export { RecordsTable }

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import { RecordsTable } from "@/components/ai/records-table"

<RecordsTable
  label="Suppliers"
  columns={[
    { id: "name", header: "Supplier" },
    { id: "summary", header: "Payment summary", isGenerated: true },
  ]}
  rows={suppliers.map((supplier) => ({
    id: supplier.id,
    name: supplier.name,
    cells: { name: supplier.name },
    generated: { summary: enrichment.cellFor(supplier.id) },
  }))}
  onRetry={(rowId, columnId) => enrichment.retry(rowId, columnId)}
/>
```

## Examples [#examples]

### All cells done [#all-cells-done]

A settled table. The progress line reads 2 of 2.

```tsx
import { RecordsTable } from "@/components/ai/records-table"

export function RecordsTableStaticExample() {
  return (
    <div className="w-full max-w-xl min-w-0">
      <RecordsTable
        label="Tickets"
        columns={[
          { id: "title", header: "Ticket" },
          { id: "category", header: "Category", isGenerated: true },
        ]}
        rows={[
          {
            id: "1",
            name: "Export totals are off",
            cells: { title: "Export totals are off" },
            generated: { category: { status: "done", value: "Billing" } },
          },
          {
            id: "2",
            name: "Cannot reset password",
            cells: { title: "Cannot reset password" },
            generated: { category: { status: "done", value: "Account" } },
          },
        ]}
      />
    </div>
  )
}

```

## API reference [#api-reference]

### RecordsTable [#recordstable]

| Prop        | Type                                        | Default  | Description                                             |
| ----------- | ------------------------------------------- | -------- | ------------------------------------------------------- |
| `label`     | string                                      | Required | Names the table.                                        |
| `columns`   | `RecordsTableColumn[]`                      | Required | Each has `id`, `header`, and an optional `isGenerated`. |
| `rows`      | `RecordsTableRow[]`                         | Required | The records.                                            |
| `onRetry`   | `(rowId: string, columnId: string) => void` | None     | Passing it shows Retry in failed cells.                 |
| `labels`    | `Partial<RecordsTableLabels>`               | English  | State words, button names, and the progress text.       |
| `className` | string                                      | None     | Added to the root.                                      |

### Types [#types]

| Export            | Shape                                                                                                |
| ----------------- | ---------------------------------------------------------------------------------------------------- |
| `RecordsTableRow` | `id`, `name`, `cells` keyed by column `id`, and optional `generated` keyed by column `id`            |
| `GeneratedCell`   | `status` of `"queued"`, `"running"`, `"done"` (with `value`), or `"error"` (with optional `message`) |
