Lattice Grid Buy a licence

demo D290

Governed AI Edits: Change Grid Data in Plain Language, With Approval in Angular

Ask in plain language to change data and approve it first: the model proposes a change, the grid finds the row and previews a before/after diff, nothing is written until you approve, and every change goes through the same gate a person’s edit does, so it can be blocked and it is reversible

createAI · ai.propose · ai.applyProposal · ai.actorBar

This lets you ask in plain language to change data, then approve it first: the model proposes a change, the grid finds the row and previews a before and after diff, and nothing is written until you say so. Every change goes through the same gate a person's edit does, so it can be blocked and it is reversible.

Building…
Loading a live grid…

This is the Angular version. A standalone component takes the whole configuration through one config input, surfaces grid events as outputs, and exposes the live grid on a getter for anything the inputs do not cover.

The configuration

import { Component, ViewChild, ElementRef } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createAI } from '@toclocoinc/lattice-grid/modules/ai';
import { LatticeGridComponent, provideLattice } from '@toclocoinc/lattice-grid/angular';

const STATUS_OPTIONS = [
  { id: 'planned', label: 'Planned' },
  { id: 'in-progress', label: 'In Progress' },
  { id: 'blocked', label: 'Blocked' },
  { id: 'done', label: 'Done' },
];

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [LatticeGridComponent],
  template:
    '<div #act></div>' +
    '<lattice-grid [config]="grid" (grid-ready)="onGridReady()" style="display:block;height:300px;margin-top:10px"></lattice-grid>',
})
export class AppComponent {
  @ViewChild(LatticeGridComponent) gridRef!: LatticeGridComponent;
  @ViewChild('act') actRef!: ElementRef<HTMLElement>;

  blockAiWrites = false;

  grid = {
    rowKey: 'id',
    columns: [
      { field: 'id', title: 'Ref', layout: { width: 96, pin: 'start' } },
      { field: 'name', title: 'Project', layout: { flex: 1, min: 180 } },
      { field: 'owner', title: 'Owner', filter: { type: 'set' } },
      { field: 'status', title: 'Status',
        lookup: { options: STATUS_OPTIONS },
        edit: { enabled: true, editor: 'select' },
        cell: { decoration: 'pill', variant: { map: { planned: 'info', 'in-progress': 'warning', blocked: 'danger', done: 'success' } } } },
    ],
    rows: [/* projects: id, name, owner, status */],
  };

  // Your model, your key. ask() returns a PROPOSAL naming the row and the new
  // value, never a write and never rows.
  private ask = async ({ message, prompt }: any) => {
    return (window as any).myProvider.toProposal(message || prompt);
  };

  // The grid is created once the view has rendered, so the live instance
  // arrives on grid-ready rather than in ngAfterViewInit.
  onGridReady() {
    const grid = this.gridRef.grid;
    // The one non-negotiable: an AI write goes through the same gate a
    // person's edit does.
    grid.on('beforeEdit', (e: any) => {
      if (this.blockAiWrites && e.origin === 'ai') e.preventDefault('Only people can change data here');
    });
    // The actor bar: an input, the diff preview, and Approve / Discard.
    const ai = createAI(grid, { ask: this.ask, tools: false });
    ai.actorBar(this.actRef.nativeElement, { placeholder: 'Ask the grid to change data: "set the Network Upgrade project to In Progress"' });
  }
}

bootstrapApplication(AppComponent, {
  providers: [provideLattice({ createGrid })],
});

Ask the grid to change data, and approve it before anything happens

Type the change the way you would say it out loud, “set the Network Upgrade project to In Progress” or, on a board, “move Network Upgrade to In Progress”, and the grid shows you exactly what it would do before it does anything. It finds the row you named, works out that “In Progress” is the value your data stores for it, and lays out a before and after so you can see which rows would change and from what to what. Nothing is written until you press Approve, and Discard throws the whole thing away untouched.

This works on what you are looking at. If your view is filtered, the change is bounded to the rows in front of you, and the preview says so. If an instruction names more than one row, the grid shows you the choices instead of guessing which one you meant.

Nothing changes until you approve

The model never writes your data. It proposes a change, and the proposal is just a description of a row and a new value. The grid takes that description, checks it against its own columns, resolves the label you used to the value it actually stores, and turns it into a before and after diff. You read the diff, and only then do you approve it. Until you do, your data is exactly as it was.

A label that does not match one of a column’s real values is refused rather than forced in, and a value that a column’s own rules reject is refused too, so the diff you approve is always a change the grid would have accepted from a person.

The same gate as a person’s edit

Every approved change goes through the same approval step one of your own people’s edits goes through. That means the rules you already have keep working: a rule that says a status cannot move straight to Done, or that a locked row cannot be touched, applies to a change made this way just as it applies to one typed by hand.

It also means you can treat a change made this way differently from a person’s. In this demo, turn on “Only people can change data” and approving a proposal writes nothing, while you can still edit a status by hand, because the block is set to refuse the AI’s change and allow a person’s. Every change is reversible, so Undo takes the last one back out the way it took any edit back out.

Your model, your key, and it is part of the grid

Changing data in plain language is part of the grid you already have, not a separate product and not an added cost. The grid ships no model and holds no key, and it makes no AI call of its own. You supply one small function, ask(), that wraps the model and key you already use, and everything the grid does with the reply, finding the row, previewing the diff, gating the write, happens here in the browser against your real data.

How do I let people change grid data in plain language?

Load the AI module and call createAI(grid, { ask }), passing the live grid and your own model callback. Then call ai.actorBar(element) to drop an instruction bar above the grid: a person types a change, sees the before and after diff, and presses Approve. For your own interface, ai.propose(instruction) returns the reviewable proposal and ai.applyProposal(result) applies it through the gate, so you can render the preview and the Approve button however you like. For a board, pass the board to ai.actorBar(element, { board }) and a change to the board’s column becomes a card move.

How does changing data this way stay safe?

The model proposes a change, never a write, and the grid validates it before showing you anything: an unknown column, an unknown label, or a value a column’s rules reject is refused. What survives is shown as a before and after diff, bounded to the rows you are looking at, and nothing is written until you approve it. Every approved change then goes through the same gate a person’s edit does, so your own rules can allow a person’s change and block or review the one made in plain language, and any change can be undone.

Wire your own model in production

The demo on this page uses a stand-in ask() that runs in your browser with no key, so the page works offline: it recognises the shape of a change instruction and turns it into a proposal the grid then validates, previews and gates. In a product you replace that one function with a call to the model you choose. Everything else stays the same, and the guarantees are the same whichever model you wire in, because the grid finds the row, previews the diff and gates the write here against your own data.