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
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.13.0/lattice-grid.min.css">
<script src="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.13.0/lattice-grid.min.js"></script>
<div id="bar" class="lattice"></div>
<div id="grid" style="height: 540px"></div>
<script>
// ai.ask is the host's, and the only key that touches a model. The grid opens
// no connection of its own: it describes itself, hands the description to this
// callback, and waits. What the callback receives is the schema, not the rows:
// column ids, titles, types and lookup options leave the grid; values never do
// unless the host puts them in context itself. Here the callback answers 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 ai.ask. Its signature is exactly a real one's: it is handed
// the prompt, the generated schema and the assembled message, and returns a
// string. This one looks the answer up rather than sending anything anywhere.
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 grid = LatticeGrid.createGrid(document.getElementById('grid'), {
rowKey: 'id',
selection: 'multiple',
toolPanel: {
side: 'left',
panels: ['columns', 'filters', 'views', 'quick'],
actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
exportName: 'lattice-demo',
},
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' },
],
ai: {
ask,
// The prompt bar mounts here. Give it no element and it goes to the top of
// the grid's own viewport instead. Mounted outside the grid, as here, its
// container needs the .lattice class its stylesheet is written under.
element: document.getElementById('bar'),
placeholder: 'Ask for a view: "production only, most expensive first"',
schemaOptions: { maxOptions: 12 },
},
rows, // cloud-cost records: account, service, resource, region, environment, change, cost
});
// 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 here, nothing is sent.
const schema = grid.ai.schema({ maxOptions: 12 });
const tool = grid.ai.tool();
// The same call the bar's own button makes. A reply is validated against the
// schema before you see it, and nothing reaches the grid until you press Apply.
const plan = await grid.promptBar.ask('production only, most expensive first');
</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.