# AI fields

A field a model fills in for each record from a prompt that names other fields. The value is stored in the record, generated one cell at a time or for all records three at a time, written again when a named field changes, and editable by hand.

Requires `@svgrid/enterprise` (and `@svgrid/grid`). Category: Base. Live demo: https://svgrid.com/demos/716-base-ai-field/

Install: `npm i @svgrid/grid @svgrid/enterprise` (Svelte 5).

## About this example

AI fields in an SvGrid Base, in Svelte 5. A prompt names the record's fields ({Name}, {Audience}), a model writes the value for each record, and the value is stored in the record, so it sorts, filters and exports like typed text. A spark in each cell generates one record, the column menu generates all of them a few at a time, and a field with auto on is written again when a field its prompt names changes.

An AI field (`fieldType: 'ai'`) holds a value a model writes for each record from a prompt that names the record's fields: "Write a one-line pitch for {Name}, aimed at {Audience}, that mentions {Key feature}." The answer is stored in the record, so it sorts, filters and exports like typed text, and anyone can edit it by hand.

- The spark in a cell generates that record; the column menu has "Generate for all". Requests run three at a time.
- Pitch has `auto` on: change a product's Audience or Key feature and its pitch is written again. Price band has it off, so it changes only when asked, and it picks one of its options (`output: 'singleSelect'`).
- A failed request shows its message in the cell (the product with no Key feature fails here on purpose). Values go through the grid's edit path: Ctrl+Z undoes a generated value like a typed one.

The model here is a stand-in that writes from templates, so the demo runs without a key. A real one is a function from a request to the model's text, registered once with `setAIProvider` from `@svgrid/grid`. Keep the key on your server:

```text
setAIProvider(async ({ prompt, maxOutputTokens, signal }) => {
  const res = await fetch('/api/ai', { method: 'POST', body: JSON.stringify({ prompt, maxOutputTokens }), signal })
  return (await res.json()).text
})
```

## Imports, features and API used

- Imports: `@svgrid/grid`, `@svgrid/enterprise/base`
- Columns: `name` (Name), `audience` (Audience), `feature` (Key feature), `pitch` (Pitch), `price` (Price), `band` (Price band)

## Frequently asked questions

### Which model does it call?

The one your app registers with setAIProvider from @svgrid/grid: a function that takes the request (the prompt, a signal to cancel, a token hint) and returns the model's text. The demo registers a stand-in that writes from templates, so it runs without a key. Keep a real key on your server and call it from the provider with fetch.

### Is the value computed on every render?

No. The answer is written into the record through the grid's edit path, the same way a typed edit is. It stays until someone generates it again or edits it by hand, and Ctrl+Z undoes it.

### When does a value change on its own?

Only with auto on, and only when a field the prompt names changes in that record. With auto off the value changes from the cell's button, the column menu or generateAIField in code.

### Can it pick from a list instead of writing text?

Yes. Set output to singleSelect and give the field options: the model is asked to pick one, and an answer that is not one of them shows as an error in the cell instead of being stored. output: 'number' stores a number.

### Which license does it need?

SvGrid Base is part of the Suite edition of @svgrid/enterprise. Without a key it runs with a watermark, so it can be evaluated as is. The AI provider functions are free and come from @svgrid/grid.

## Related documentation

- [Let an AI agent work in your base](https://svgrid.com/docs/help/base/ai-agents.md)

## Related articles

- [A Svelte Data Grid with SvelteKit and Supabase](https://svgrid.com/blog/svelte-data-grid-sveltekit-supabase/) - Wire SvGrid to a Supabase Postgres backend with server-side pagination, sorting, and filtering - keeping credentials on the server and queries fast with proper indexing.
- [Column Definitions in SvGrid - Fields, Accessors, and Formatting](https://svgrid.com/blog/column-definitions-deep-dive/) - A practical look at how ColumnDef controls field binding, value resolution, sort behavior, and custom cell rendering in SvGrid.
- [A Live Svelte Data Grid with Firebase Firestore](https://svgrid.com/blog/svelte-data-grid-firebase-firestore/) - Wire Firestore onSnapshot to SvGrid in Svelte 5 - reactive listeners, query-driven sort and filter, cursor pagination, and the subtle cleanup detail most implementations get wrong.

## Source code (716-base-ai-field.svelte)

```svelte
<script lang="ts">
  /**
   * 716. SvGrid Base: AI fields
   * ---------------------------
   * An AI field (`fieldType: 'ai'`) holds a value a model writes for each
   * record from a prompt that names the record's fields:
   * "Write a one-line pitch for {Name}, aimed at {Audience}, that mentions
   * {Key feature}." The answer is stored in the record, so it sorts, filters
   * and exports like typed text, and anyone can edit it by hand.
   *
   * - The spark in a cell generates that record; the column menu has
   *   "Generate for all". Requests run three at a time.
   * - Pitch has `auto` on: change a product's Audience or Key feature and its
   *   pitch is written again. Price band has it off, so it changes only when
   *   asked, and it picks one of its options (`output: 'singleSelect'`).
   * - A failed request shows its message in the cell (the product with no Key
   *   feature fails here on purpose). Values go through the grid's edit path:
   *   Ctrl+Z undoes a generated value like a typed one.
   *
   * The model here is a stand-in that writes from templates, so the demo runs
   * without a key. A real one is a function from a request to the model's text,
   * registered once with `setAIProvider` from `@svgrid/grid`. Keep the key on
   * your server:
   *
   *   setAIProvider(async ({ prompt, maxOutputTokens, signal }) => {
   *     const res = await fetch('/api/ai', { method: 'POST', body: JSON.stringify({ prompt, maxOutputTokens }), signal })
   *     return (await res.json()).text
   *   })
   */
  import { getAIProvider, setAIProvider, type AIProvider } from '@svgrid/grid'
  import { SvBase, enableBase, generateAIField, type BaseTable } from '@svgrid/enterprise/base'

  enableBase()

  // ---- The stand-in model ---------------------------------------------------------

  const OPENERS = ['Built for', 'Made for', 'Ready for']
  const pick = (list: string[], seed: string) => list[[...seed].reduce((n, c) => n + c.charCodeAt(0), 0) % list.length]!

  const demoProvider: AIProvider = async (req) => {
    await new Promise((r) => setTimeout(r, 400 + Math.random() * 500))
    if (req.signal?.aborted) throw new DOMException('Cancelled', 'AbortError')
    const ask = /Instruction:\n([\s\S]*?)\n\n/.exec(req.prompt)?.[1] ?? ''
    const pitch = /pitch for (.*), aimed at (.*), that mentions (.*)\.$/.exec(ask)
    if (pitch) {
      const [, name, audience, feature] = pitch
      if (!feature!.trim()) throw new Error('Key feature is empty: nothing to build the pitch on')
      return JSON.stringify({ value: `${pick(OPENERS, name!)} ${audience!.toLowerCase()}: the ${name} brings ${feature!.toLowerCase()}.` })
    }
    // "at $189.00", or however the locale writes it: the whole units are enough here.
    const price = /price band fits .* at \D*(\d+)/.exec(ask)
    if (price) {
      const n = Number(price[1])
      return JSON.stringify({ value: n < 80 ? 'Budget' : n < 200 ? 'Mid-range' : 'Premium' })
    }
    return JSON.stringify({ value: ask.slice(0, 80) })
  }

  // Registered before the base draws, so its cells see a provider; the
  // gallery's previous one comes back when the demo closes.
  const previous = getAIProvider()
  setAIProvider(demoProvider)
  $effect(() => () => setAIProvider(previous))

  // ---- The base -------------------------------------------------------------------

  const AUDIENCE = [
    { value: 'Hikers', color: '#16a34a' },
    { value: 'Trail runners', color: '#ea580c' },
    { value: 'Commuters', color: '#2563eb' },
    { value: 'Climbers', color: '#7c3aed' },
  ]
  const BANDS = [
    { value: 'Budget', color: '#94a3b8' },
    { value: 'Mid-range', color: '#0ea5e9' },
    { value: 'Premium', color: '#a855f7' },
  ]
  const PRODUCTS: Array<[string, string, string, number]> = [
    ['Ridge 40 pack', 'Hikers', 'A frame that carries 15 kg without shoulder strain', 189],
    ['Fellrunner vest', 'Trail runners', 'Two soft flasks at the chest', 95],
    ['Metro shell', 'Commuters', 'Taped seams and a hood that fits over a helmet', 229],
    ['Crag chalk bag', 'Climbers', 'A brush loop and a drawstring that closes one-handed', 29],
    ['Summit trekking poles', 'Hikers', 'Carbon shafts at 230 g a pair', 149],
    ['Night loop headlamp', 'Trail runners', '', 59],
    ['Commute pannier', 'Commuters', 'Clips on and off the rack in one move', 119],
    ['Belay gloves', 'Climbers', 'Leather palms that last through rope burn', 45],
  ]

  let tables = $state.raw<BaseTable[]>([
    {
      id: 'ai-products',
      name: 'Products',
      primaryField: 'name',
      data: PRODUCTS.map(([name, audience, feature, price], i) => ({ id: `p${i + 1}`, name, audience, feature, price })),
      columns: [
        { field: 'name', header: 'Name', width: 180, fieldType: 'text' },
        { field: 'audience', header: 'Audience', width: 130, fieldType: 'singleSelect', fieldProps: { options: AUDIENCE } },
        { field: 'feature', header: 'Key feature', width: 210, fieldType: 'text' },
        {
          field: 'pitch', header: 'Pitch', width: 300, fieldType: 'ai',
          fieldProps: { prompt: 'Write a one-line pitch for {Name}, aimed at {Audience}, that mentions {Key feature}.', output: 'text', auto: true },
          description: 'Written by a model from Name, Audience and Key feature.',
        },
        { field: 'price', header: 'Price', width: 100, fieldType: 'number', fieldProps: { format: 'currency', precision: 2 } },
        {
          field: 'band', header: 'Price band', width: 130, fieldType: 'ai',
          fieldProps: { prompt: 'Which price band fits {Name} at {Price}?', output: 'singleSelect', options: BANDS, auto: false },
        },
      ],
      views: [{ id: 'all', name: 'All products', type: 'grid' }],
    },
  ])

  let api: unknown = null
  const onApiReady = (a: unknown) => (api = a)
  let busy = $state(false)
  let result = $state('')

  async function generateAll() {
    if (!api || busy) return
    busy = true
    result = ''
    try {
      const run = await generateAIField(api, 'pitch', 'all')
      result = `${run.written} written${run.failed ? `, ${run.failed} failed` : ''}`
    } finally {
      busy = false
    }
  }
</script>

<section class="aif-demo">
  <div class="aif-bar">
    <button type="button" class="aif-run" onclick={() => void generateAll()} disabled={busy}>{busy ? 'Generating...' : 'Generate all pitches'}</button>
    <span class="aif-result" aria-live="polite">{result}</span>
    <p class="aif-tip">Then change a product's Audience: its pitch is written again. Price band changes only from its spark or column menu.</p>
  </div>
  <SvBase
    bind:tables
    {onApiReady}
    editable
    sortable
    showRowNumbers
    expandRows
    showRowSelection={false}
    rowHeight={40}
    containerHeight="100%"
    columnAdder
    fieldMenu
  />
</section>

<style>
  .aif-demo { display: flex; flex-direction: column; flex: 1; min-height: 480px; height: 100%; gap: 10px; }
  .aif-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; font-size: 13px; }
  .aif-run {
    height: 32px; padding: 0 14px; border: 0; border-radius: var(--sg-radius, 6px);
    background: var(--sg-accent, #2563eb); color: var(--sg-accent-fg, #fff); font: inherit; font-weight: 600; cursor: pointer;
  }
  .aif-run:disabled { opacity: 0.6; cursor: default; }
  .aif-result { color: var(--sg-fg, #0f172a); }
  .aif-tip { flex: 1 1 260px; margin: 0; line-height: 1.5; color: var(--sg-muted, #64748b); }
</style>
```

[View this example on GitHub](https://github.com/sv-grid/sv-grid/blob/main/examples/src/demos/716-base-ai-field.svelte)
