Documentation

AI Fill

Add AI-filled columns to a grid with one prop. Answers come from Jev's Choice, Score and Noul primitives, and nothing is written until someone accepts it.

AI Fill lets you add AI-filled columns to a DataEditor in configuration alone. For each AI column you describe a question for Jev (TypeSafe's Choice, Score and Noul primitives), the row data it is asked about, and how answers become cell values. AI Fill asks the question for the cells a user picks, shows the answers as suggestions, and writes a value only when someone accepts it, unless you explicitly configure automatic application for a "Fill and apply" run.

It is part of @specstory/ai-data-grid: there is no second package to install, no new runtime dependency, and a grid without the aiFill prop doesn't change and doesn't load any AI code.

What Jev answers

Jev returns structured answers, not prose. Each primitive answers a different kind of question:

PrimitiveYou defineJev answers with
ChoiceA question and 2 to 255 options, each with a descriptionThe selected option, the probability of every option (they sum to 1), and a separate model confidence
ScoreA question and an ordered rubric of 2 to 10 levelsA score from 0 to levels − 1 (a probability-weighted position on the rubric, not a percentage), each level's probability, and a confidence
NoulA yes/no question, optionally with what yes and no meanThe probability that the answer is yes, from 0 to 1. There is no confidence.

Primitives explains what each number means, and why probability and confidence are different things.

Quick start

AI Fill needs a way to reach Jev. In production that's a route on your own server that holds the TypeSafe API key; the browser never sees it. The server helper implements the route:

// app/api/jev/route.ts (Next.js App Router)
import { createJevHandler } from "@specstory/ai-data-grid/server";

export const POST = createJevHandler({
    apiKey: process.env.TYPESAFE_API_KEY ?? "",
    authorize: request => isSignedIn(request), // your own check: who may spend your key
});

Then turn AI Fill on for an existing grid with the aiFill prop:

import * as React from "react";
import { DataEditor, type AIFillConfig } from "@specstory/ai-data-grid";
import "@specstory/ai-data-grid/dist/index.css"; // the same CSS import as before

function Contacts({ contacts, columns, getCellContent, onCellEdited }) {
    const aiFill = React.useMemo<AIFillConfig>(
        () => ({
            connection: { mode: "endpoint", url: "/api/jev" },
            model: "jev-latest",
            rows: { getRowId: row => contacts[row].id },
            rowScope: () => ({ rows: "displayed", label: "contacts" }),
            columns: {
                // keyed by GridColumn.id
                persona: {
                    primitive: "choice",
                    instructions: "Which buyer persona best describes this contact?",
                    sources: ["title", "notes"],
                    options: {
                        champion: { description: "Drives the purchase internally", label: "Champion" },
                        economic: { description: "Controls the budget and signs off on spend", label: "Economic buyer" },
                        user: { description: "Uses the product day to day", label: "End user" },
                    },
                    policy: { show: { minProbability: 0.5 }, ready: { minProbability: 0.8 } },
                },
            },
        }),
        [contacts]
    );

    return (
        <DataEditor
            aiFill={aiFill}
            columns={columns}
            rows={contacts.length}
            getCellContent={getCellContent}
            onCellEdited={onCellEdited}
        />
    );
}

That's the whole integration. The grid now has:

  • a ✦ badge and a ▾ menu on the Persona header, with "Fill empty cells in Persona", "Accept N eligible", "Review N" and the rest;
  • suggestions drawn in the cells, with review, withheld and error markers;
  • a status bar with progress, a summary, "Review next" and "Accept N eligible";
  • an inspector that shows each answer's probabilities, the decision and its reason, and the model that answered;
  • keyboard shortcuts, and a confirmation before large or overwriting fills.

Every accepted value is written through your onCellEdited (or onCellsEdited), exactly like a paste. See Fill scopes and review for what users see and do.

What the prop changes

  • Unset, nothing changes. Without aiFill, DataEditor passes the same props to the grid, the ref is the grid's own handle, and no AI module loads.
  • It loads lazily. AI Fill's controller loads in its own chunk the first time aiFill is set. Until it has loaded (normally a few milliseconds) the grid renders as if the prop were unset. Then ref.current.aiFill is set, and aiFill.onReady(api) is called once with the same API.
  • Keep the object stable, for example with useMemo. A new object is validated and applied again: a changed policy re-decides stored answers without a request, a changed question marks results stale, and a removed column drops its results.
  • Clearing it aborts every request in flight and drops every result. Nothing is written, and the grid isn't remounted.
  • Your handlers are wrapped, never replaced. AI Fill composes columns, drawCell, drawHeader, the edit handlers, onKeyDown, the menu handlers, onCellClicked, className and the selection props with yours, and passes everything else through. The API reference lists what it does with each prop, in the "Quick start (aiFill prop)" section of API.md, which ships in the package.

The API

App code can drive everything the built-in UI does through ref.current.aiFill (or the onReady argument), an AIFillApi:

const api = ref.current?.aiFill;
const run = api?.fill("column-empty", { columns: ["persona"] }); // { runId, done, cells, requests, skipped }
api?.accept({ column: "persona", filter: "eligible" }); // "Accept all eligible"
api?.getCellState("contact-42", "persona"); // status, decision, output, error, metadata

Fill scopes and review lists every method.

Try it

The Storybook has 13 AI Fill stories, under AI-Data-Grid / AI Fill. They answer from a deterministic mock with seeded answers, so they make no network calls, and each shows one behavior: the full buyer-persona workflow, Score level mapping, Noul bands, the 0.79 / 0.80 boundary, probability versus confidence, review and auto-apply, errors, staleness, sorting while pending, undo, app menus and the disabled grid. Most have an "endpoint URL" control that points them at the local dev proxy instead, to see real answers.

In this section

The complete reference, including every type, is the "AI Fill" chapter of API.md, which ships in the @specstory/ai-data-grid package.

AI Data Grid is forked from Glide Data Grid by Glide (typeguard, Inc.), MIT licensed. AI Fill is new in AI Data Grid.