Lattice Grid Buy a licence

demo D290

Ask the grid to change data, safely

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

Building…
Loading a live grid…

The configuration

'governed-actor': () => ({
  rows: [],
  config: {},
  foot: [
    'the model callback runs in this tab: no key, no network request',
    'the model proposes a change, never a write; the grid finds the row, resolves the label to the value it stores, and shows a before/after diff',
    'nothing changes until you press Approve, and Discard throws the proposal away untouched',
    'every approved change goes through the same gate a person’s edit does: turn on the block and the AI is refused while you can still edit by hand',
    'a change works on what you are looking at, and an instruction that names more than one row shows you the choices rather than guessing',
    'in a product you pass your own model through ask(); the grid ships no model, holds no key, and makes no AI call of its own',
  ],
  mount: (el: HTMLElement, LG: any) => {
    let disposed = false;
    let gridAi: any;
    let boardAi: any;
    let board: any;
    let boardGrid: any;
    let watching: MutationObserver | undefined;
    const gridProjects: Project[] = [
      { id: 'P-101', name: 'Network Upgrade', owner: 'Dana', status: 'planned' },
      { id: 'P-102', name: 'Data Migration', owner: 'Wei', status: 'in-progress' },
      { id: 'P-103', name: 'Cloud Migration', owner: 'Priya', status: 'planned' },
      { id: 'P-104', name: 'Payroll Refresh', owner: 'Sam', status: 'blocked' },
      { id: 'P-105', name: 'Website Relaunch', owner: 'Alex', status: 'planned' },
      { id: 'P-106', name: 'Security Audit', owner: 'Jordan', status: 'in-progress' },
    ];
    const statusCol = {
      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' } },
      },
    };
    const askCol = document.createElement('div');
    askCol.style.minWidth = '0';
    const tools = document.createElement('div');
    tools.style.cssText = 'display:flex;flex-wrap:wrap;gap:14px;align-items:center;margin-bottom:12px';
    const blockWrap = document.createElement('label');
    blockWrap.style.cssText = 'display:inline-flex;gap:7px;align-items:center;font-size:.875rem;color:var(--ink-2);cursor:pointer';
    const blockBox = document.createElement('input');
    blockBox.type = 'checkbox';
    const blockText = document.createElement('span');
    blockText.textContent = 'Only people can change data (block AI writes)';
    blockWrap.append(blockBox, blockText);
    const undoBtn = document.createElement('button');
    undoBtn.type = 'button';
    undoBtn.className = 'lattice';
    undoBtn.textContent = 'Undo last change';
    undoBtn.disabled = true;
    undoBtn.style.cssText = 'font-size:.8125rem;padding:6px 13px;border:1px solid var(--rule);border-radius:7px;'
      + 'background:var(--paper);color:var(--ink);cursor:pointer';
    tools.append(blockWrap, undoBtn);
    const chipRow = document.createElement('div');
    chipRow.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px';
    const chipLabel = document.createElement('span');
    chipLabel.style.cssText = 'font-size:.875rem;color:var(--ink-2);align-self:center;margin-right:2px';
    chipLabel.textContent = 'Try an instruction:';
    chipRow.append(chipLabel);
    const barEl = document.createElement('div');
    barEl.className = 'lattice';
    barEl.style.cssText = 'min-width:0;' + BAR_VARS;
    askCol.append(
      gridTitle('Ask the grid to make a change'),
      tools, chipRow, barEl,
      note('The model proposes a change. The grid finds the row you named, resolves the label to the value it stores, and shows a before/after diff. Nothing is written until you press Approve.'),
    );
    const gridEl = document.createElement('div');
    gridEl.style.cssText = 'height:300px;border:1px solid var(--rule);border-radius:9px;background:var(--paper);min-width:0';
    const gridColWrap = document.createElement('div');
    gridColWrap.style.cssText = 'min-width:0;margin-top:12px';
    gridColWrap.append(gridTitle('Projects'), gridEl);
    const boardIntro = document.createElement('div');
    boardIntro.style.cssText = 'margin-top:36px';
    boardIntro.append(
      gridTitle('Or move a card by instruction'),
      note('The same proposal-then-approve step, surfaced as a board move: "move Network Upgrade to In Progress" previews the move and applies it only when you approve, through the board’s own move gate.'),
    );
    const boardChipRow = document.createElement('div');
    boardChipRow.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;margin:12px 0';
    const boardChipLabel = document.createElement('span');
    boardChipLabel.style.cssText = 'font-size:.875rem;color:var(--ink-2);align-self:center;margin-right:2px';
    boardChipLabel.textContent = 'Try an instruction:';
    boardChipRow.append(boardChipLabel);
    const boardBarEl = document.createElement('div');
    boardBarEl.className = 'lattice';
    boardBarEl.style.cssText = 'min-width:0;margin-bottom:12px;' + BAR_VARS;
    const boardEl = document.createElement('div');
    boardEl.style.cssText = 'height:420px;border:1px solid var(--rule);border-radius:9px;background:var(--paper);overflow:hidden';
    const live = liveRequested();
    const gridLiveNote = note('Live model unavailable, showing a sample answer.');
    const boardLiveNote = note('Live model unavailable, showing a sample answer.');
    gridLiveNote.style.display = 'none';
    boardLiveNote.style.display = 'none';
    if (live) { askCol.append(gridLiveNote); boardIntro.append(boardLiveNote); }
    const setGridFallback = (v: boolean) => { gridLiveNote.style.display = v ? '' : 'none'; };
    const setBoardFallback = (v: boolean) => { boardLiveNote.style.display = v ? '' : 'none'; };
    const gridAskFn = live ? makeLiveAsk(setGridFallback) : mockAsk;
    const boardAskFn = live ? makeLiveAsk(setBoardFallback) : mockAsk;
    el.append(askCol, gridColWrap, boardIntro, boardChipRow, boardBarEl, boardEl);
    const grid = LG.createGrid(gridEl, {
      rowKey: 'id',
      theme: 'light',
      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' } },
        statusCol,
      ],
      rows: gridProjects,
    });
    grid.on('beforeEdit', (e: any) => {
      if (blockBox.checked && e && e.origin === 'ai') {
        e.preventDefault('Only people can change data here');
      }
    });
    const refreshUndo = () => { undoBtn.disabled = !grid.history?.canUndo?.(); };
    grid.on('history:changed', refreshUndo);
    undoBtn.addEventListener('click', () => { grid.history?.undo?.(); });
    const themed = [barEl, boardBarEl];
    if (grid.element) {
      const copy = () => {
        const t = grid.element.getAttribute('data-theme') || 'light';
        for (const node of themed) node.setAttribute('data-theme', t);
      };
      copy();
      watching = new MutationObserver(copy);
      watching.observe(grid.element, { attributes: true, attributeFilter: ['data-theme'] });
    }
    loadAI()
      .then((AI: any) => {
        if (disposed) return;
        gridAi = AI.createAI(grid, { ask: gridAskFn, tools: false, locale: 'en-US' });
        gridAi.actorBar(barEl, {
          placeholder: 'Ask the grid to change data: “set the Network Upgrade project to In Progress”',
        });
        const input = barEl.querySelector('.lat-ai-ask__input') as HTMLInputElement | null;
        const proposeBtn = barEl.querySelector('.lat-ai-act__propose') as HTMLButtonElement | null;
        const instructions = [
          'set the Network Upgrade project to In Progress',
          'mark Payroll Refresh as Done',
          'move the migration project to Blocked',
        ];
        for (const text of instructions) {
          const c = chip(text);
          c.addEventListener('click', () => {
            if (!input || !proposeBtn) return;
            input.value = text;
            proposeBtn.click();
          });
          chipRow.append(c);
        }
      })
      .catch((err) => console.error('[governed-actor:grid]', err));
    const boardProjects: Project[] = [
      { id: 'B-201', name: 'Network Upgrade', owner: 'Dana', status: 'planned' },
      { id: 'B-202', name: 'Data Warehouse', owner: 'Wei', status: 'planned' },
      { id: 'B-203', name: 'Payroll Refresh', owner: 'Sam', status: 'blocked' },
      { id: 'B-204', name: 'Website Relaunch', owner: 'Alex', status: 'in-progress' },
      { id: 'B-205', name: 'Security Audit', owner: 'Jordan', status: 'in-progress' },
      { id: 'B-206', name: 'Mobile App', owner: 'Priya', status: 'done' },
    ];
    boardGrid = LG.createHeadlessGrid({
      rowKey: 'id',
      columns: [
        { field: 'id' },
        { field: 'name' },
        { field: 'owner' },
        { field: 'status', lookup: { options: STATUS_OPTIONS }, edit: { enabled: true } },
      ],
      rows: boardProjects,
    });
    Promise.all([loadAI(), loadKanban()])
      .then(([AI, KB]: any[]) => {
        if (disposed) return;
        board = KB.createKanban(boardEl, {
          grid: boardGrid,
          columnProperty: 'status',
          columns: [
            { id: 'planned', title: 'Planned' },
            { id: 'in-progress', title: 'In Progress' },
            { id: 'blocked', title: 'Blocked' },
            { id: 'done', title: 'Done', color: '#2e7d32' },
          ],
          card: { title: { field: 'name' }, subtitle: 'owner' },
          doneColumns: ['done'],
          ariaLabel: 'Projects board',
          emptyText: 'No projects',
        });
        boardAi = AI.createAI(boardGrid, { ask: boardAskFn, tools: false, board, locale: 'en-US' });
        boardAi.actorBar(boardBarEl, {
          board,
          placeholder: 'Ask the board to move a card: “move Network Upgrade to In Progress”',
        });
        const input = boardBarEl.querySelector('.lat-ai-ask__input') as HTMLInputElement | null;
        const proposeBtn = boardBarEl.querySelector('.lat-ai-act__propose') as HTMLButtonElement | null;
        const instructions = [
          'move Network Upgrade to In Progress',
          'move Security Audit to Done',
        ];
        for (const text of instructions) {
          const c = chip(text);
          c.addEventListener('click', () => {
            if (!input || !proposeBtn) return;
            input.value = text;
            proposeBtn.click();
          });
          boardChipRow.append(c);
        }
      })
      .catch((err) => console.error('[governed-actor:board]', err));
    return () => {
      disposed = true;
      watching?.disconnect();
      gridAi?.destroy?.();
      boardAi?.destroy?.();
      board?.destroy?.();
      boardGrid?.destroy?.();
      grid?.destroy?.();
    };
  },
})

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.