demo D136
The AI skill layer
A model proposes a change; the grid validates it before applying
grid.ai.schema · prompt · plan · apply
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.