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

import * as ng from '@angular/core';
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import createLatticeGrid from '@toclocoinc/lattice-grid/modules/angular';

const { LatticeGridComponent } = createLatticeGrid({ ng, 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: any) => {
  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' },
];

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [LatticeGridComponent],
  template: '<lattice-grid [config]="grid" style="display:block;height:540px"></lattice-grid>',
})
export class AppComponent implements AfterViewInit {
  @ViewChild(LatticeGridComponent) gridRef!: LatticeGridComponent;

  grid = {
    rowKey: 'id',
    selection: 'multiple',
    toolPanel: { side: 'left',
      panels: ['columns', 'filters', 'views', 'quick'],
      actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
      exportName: 'lattice-demo' },
    columns,
    rows: [/* cloud-cost records: account, service, resource, region, environment, change, cost */],
    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 },
    },
  };

  // 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 from the live grid,
  // nothing is sent. A reply is validated against the schema before you see it,
  // and nothing reaches the grid until you press Apply.
  ngAfterViewInit() {
    const grid = this.gridRef.grid;
    const schema = grid.ai.schema({ maxOptions: 12 });
    const tool = grid.ai.tool();
    grid.promptBar.ask('production only, most expensive first');
  }
}

bootstrapApplication(AppComponent);

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.