Lattice Grid Buy a licence

demo D136

The AI skill layer

A model proposes a change; the grid validates it before applying

grid.ai.schema · prompt · plan · apply

Building…
Loading a live grid…

The configuration

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.28.0/lattice-grid.min.css">

<div id="app"></div>

<script type="module">
  import React from 'https://esm.sh/react@18';
  import { createRoot } from 'https://esm.sh/react-dom@18/client';
  import { createGrid } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.28.0/lattice-grid.esm.min.js';
  import createLatticeGrid from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.28.0/modules/react.esm.min.js';

  const LatticeGrid = createLatticeGrid({ React, createGrid });

  // ask is the host's, and the only thing that touches a model. The grid opens
  // no connection of its own: it describes itself, hands the schema to this
  // callback and waits. Column ids, titles, types and lookup options leave the
  // grid; values never do unless the host puts them in context itself. Here the
  // answer is looked up from a small script, so nothing is sent anywhere.
  const scenes = [
    {
      match: ['prod', 'production', 'live'],
      reply: JSON.stringify({
        actions: [
          { action: 'setFilters', filters: { op: 'and', conditions: [
            { col: 'environment', op: 'in', value: ['prod', 'prod-2', 'definitely-prod'] },
          ] } },
          { action: 'setSort', sort: [{ col: 'cost', dir: 'desc' }] },
        ],
        explain: 'Filter to the production environments and sort by cost, largest first.',
      }),
    },
    {
      match: ['over', 'expensive', 'costly', '1000', 'budget'],
      reply: JSON.stringify({
        actions: [
          { action: 'setFilters', filters: { col: 'cost', op: 'gt', value: 1000 } },
          { action: 'setSort', sort: [{ col: 'cost', dir: 'desc' }] },
        ],
        explain: 'Show only resources costing more than $1,000 a month.',
      }),
    },
    {
      match: ['group', 'service', 'breakdown', 'by team'],
      reply: JSON.stringify({
        actions: [
          { action: 'groupBy', columns: ['service'] },
          { action: 'setSort', sort: [{ col: 'cost', dir: 'desc' }] },
          { action: 'hideColumns', columns: ['resource'] },
        ],
        explain: 'Group by service, sort by cost and hide the resource names.',
      }),
    },
  ];

  // The stand-in for a real ask. Its signature matches: it is handed the prompt,
  // the generated schema and the assembled message, and returns a string.
  const ask = (req) => {
    const text = String(req.prompt || '').toLowerCase();
    const scene = scenes.find((s) => s.match.some((w) => text.includes(w)));
    const noAnswer = JSON.stringify({ actions: [], explain: 'No answer for that request.' });
    return new Promise((resolve) => {
      setTimeout(() => resolve(scene ? scene.reply : noAnswer), 500);
    });
  };

  const columns = [
    { field: 'account', title: 'Account', filter: { type: 'set' } },
    { field: 'service', title: 'Service', filter: { type: 'set' } },
    { field: 'resource', title: 'Resource', layout: { flex: 1, min: 200, max: 320 } },
    { field: 'region', title: 'Region', filter: { type: 'set' } },
    { field: 'environment', title: 'Env', filter: { type: 'set' },
      cell: { decoration: 'pill', variant: { map: {
        prod: 'danger', 'prod-2': 'danger', 'definitely-prod': 'danger',
        staging: 'warning', untagged: 'warning', test: 'info',
        dev: 'success', 'not-prod': 'neutral',
      } } } },
    { field: 'change', title: 'Change', type: 'number',
      layout: { width: 140, min: 140 }, format: { style: 'percent', decimals: 1 } },
    { field: 'cost', title: 'Monthly cost', type: 'number', layout: { width: 170 },
      format: { style: 'currency', currency: 'USD', decimals: 2 }, total: 'sum' },
  ];

  const rows = [/* cloud-cost records: account, service, resource, region, environment, change, cost */];

  function App() {
    const gridRef = React.useRef(null);

    // The schema is what leaves the grid on an ask, and the tool is how it is
    // offered to a model with tool calling. Both are read through the ref,
    // nothing is sent. A reply is validated against the schema before you see it,
    // and nothing reaches the grid until you press Apply.
    React.useEffect(() => {
      const grid = gridRef.current.grid;
      const schema = grid.ai.schema({ maxOptions: 12 });
      const tool = grid.ai.tool();
      grid.promptBar.ask('production only, most expensive first');
    }, []);

    return (
      <LatticeGrid
        ref={gridRef}
        rowKey="id"
        selection="multiple"
        toolPanel={{ side: 'left',
          panels: ['columns', 'filters', 'views', 'quick'],
          actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
          exportName: 'lattice-demo' }}
        columns={columns}
        rows={rows}
        ai={{
          ask,
          // No element given, so the prompt bar mounts at the top of the grid's
          // own viewport.
          placeholder: 'Ask for a view: "production only, most expensive first"',
          schemaOptions: { maxOptions: 12 },
        }}
        style={{ height: '540px' }}
      />
    );
  }

  createRoot(document.getElementById('app')).render(<App />);
</script>

Letting a model propose an edit without letting it touch the grid directly

The AI skill layer gives a language model a narrow, typed surface for changing grid state instead of a raw handle to the DOM or the row store. A developer reaches for it when a chat interface or an agent needs to sort a column, apply a filter, or edit a batch of cells on a user’s behalf, but the change still has to pass the same rules a human edit would. Lattice Grid exposes this through grid.ai.schema, describing the operations a model may request in terms the grid already understands, plus a prompt, plan, apply sequence: the prompt produces a plan as structured data, not executed code, and the grid checks that plan against column types, edit.validate rules, and permissions before it touches a row. A plan referencing a missing column, or proposing a value a validator would reject, is refused before it reaches the grid rather than partially applied and rolled back. This keeps a JavaScript data grid’s AI layer bounded to operations the grid can already perform, and the audit trail that records a manual edit records a model-proposed one too, with no separate code path to review. The plan step also gives a host a place to show the user what will change before it happens, rather than surfacing an edit only after it has landed.

How do you let an AI model edit a data grid safely?

Route the model’s intent through a schema that only describes operations the grid supports, then split execution into a plan step and an apply step. Lattice Grid’s grid.ai.schema constrains what a prompt can request, plan turns that into structured, inspectable changes, and apply runs them through the grid’s existing validation and permission checks, so a proposed edit is refused under the same rules a manual one would be.