demo D289
Ask Your Data: Natural-Language Filtering for a JavaScript Data Grid in React
Describe the view you want in plain English and the grid reshapes itself, filtering, sorting and grouping to match: the model returns a query, the grid validates it against its own columns and runs it, and one request moves the grid, a chart and the KPI tiles together
createAI · ai.query · ai.applyQuery · ai.askBar
This reshapes the grid from a plain-English request: say 'show overdue invoices by region' and the model returns a query the grid validates against its own columns and runs, filtering, sorting and grouping to match. One request moves the grid, a chart and the KPI tiles together, so there is nothing to learn but the question.
This is the React version. The grid mounts through the createLatticeGrid adapter, which takes its configuration as ordinary props and hands back the live grid through a ref. The grid below is the same one every other tab runs.
The configuration
<link rel="stylesheet" href="https://cdn.latticegrid.dev/lattice-grid@1.107.0/lattice-grid.min.css">
<div id="app"></div>
<script type="module">
import React from 'https://esm.sh/react@18';
import { createRoot } from 'https://esm.sh/react-dom@18/client';
import { createGrid } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/lattice-grid.esm.min.js';
import { createLatticeReact } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/modules/react.esm.min.js';
import { createAI } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/modules/ai.esm.min.js';
import { createChart } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/modules/charts.esm.min.js';
import { createKPI } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/modules/kpi.esm.min.js';
import { createDataRouter } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/modules/data-router.esm.min.js';
const { LatticeGrid, LatticeChart, LatticeKPI, useLatticeRouter, LatticeRouterProvider } =
createLatticeReact({ React, createGrid, createChart, createKPI, createDataRouter });
const MONEY = { style: 'currency', currency: 'USD', decimals: 0 };
const columns = [
{ field: 'id', title: 'Ref', layout: { width: 104, pin: 'start' } },
{ field: 'region', title: 'Region', filter: { type: 'set' } },
{ field: 'customer', title: 'Customer', filter: { type: 'text' } },
{ field: 'status', title: 'Status', filter: { type: 'set' } },
{ field: 'amount', title: 'Amount', type: 'number', format: MONEY, total: 'sum' },
{ field: 'due', title: 'Due', type: 'dateString' },
];
const rows = [/* invoices: id, kind: 'invoice', region, customer, status, amount, due */];
const tiles = [
{ id: 'total', label: 'Total shown', aggregation: 'sum', field: 'amount', format: 'currency' },
{ id: 'count', label: 'Invoices shown', aggregation: 'count' },
{ id: 'avg', label: 'Average', aggregation: 'avg', field: 'amount', format: { type: 'currency', decimals: 0 } },
{ id: 'biggest', label: 'Biggest', aggregation: 'max', field: 'amount', format: 'currency' },
];
// Your model, your key, your network call. ask() is handed the question and
// the grid's own column schema and returns a query SPEC (filters, sort,
// grouping), never rows: the grid validates the spec and runs it itself.
const ask = async ({ prompt }) => {
const spec = await myProvider.toQuery(prompt);
return spec;
};
function App() {
const [grid, setGrid] = React.useState(null);
const askRef = React.useRef(null);
const router = useLatticeRouter({ key: 'kind', rowKey: 'id', overlap: true });
React.useEffect(() => {
if (!grid || !router) return undefined;
router.attach(grid, 'invoice');
// tools: false hands ask() the request and takes a spec back directly.
// autoApply lands a resolved query once the grid has validated it.
// Passing the router re-feeds the tiles with the rows the grid returned.
const ai = createAI(grid, { ask, tools: false, router, autoApply: true });
// The in-grid ask bar: an input and the resolved-query preview.
ai.askBar(askRef.current, { placeholder: 'Describe the view: "overdue invoices by region, biggest first"' });
return () => { router.detach(grid); ai.destroy?.(); };
}, [grid, router]);
return (
<LatticeRouterProvider router={router}>
<div ref={askRef} />
<LatticeGrid rowKey="id" columns={columns} rows={rows} onGridReady={setGrid} style={{ height: '320px', marginTop: '10px' }} />
{grid && <LatticeChart grid={grid} type="bar" x="region" y="amount" title="Amount by region" style={{ height: '260px', marginTop: '14px' }} />}
{grid && <LatticeKPI grid={grid} columns={4} tiles={tiles} style={{ marginTop: '14px' }} />}
</LatticeRouterProvider>
);
}
createRoot(document.getElementById('app')).render(<App />);
</script>
Shape your data with a plain-English request
Describe the view you want the way you would say it out loud, “show overdue invoices by region” or “sort by amount, biggest first”, and the grid reshapes itself to match, filtering, sorting and grouping its own rows. There is nothing to learn: no operators, no query language, no menu to hunt through. The people who read your grid all day can now shape it in a sentence, and the shaping is the same shaping they would have done by hand, so it lands on the same undo step and reads the same in every view.
One request can move several views at once. Ask for overdue invoices by region and the grid narrows, the chart redraws for the narrower set, and the stat tiles retotal, all from the one request, because they are all reading the same rows.
This demo reshapes the view. For a plain-language reading of what the numbers say, see the narrative demo, the one that answers questions about your data.
How you can trust the result
The model never touches your data. It returns a query, a short description of which columns and operators to use, and never a single row. The grid takes that query, checks it against its own columns and their types, and runs it in its own engine, so every row you see is a real row the grid returned, not something a model wrote.
The resolved query is shown back to you in plain English, “Filter Status is Overdue, sort Amount, largest first”, so you always see what it did, and then it applies on its own. A request that would do anything more than reshape what you are looking at is refused.
It only reshapes the view. It filters, sorts and groups the rows in front of you. It does not edit, add or remove your data. A request that cannot be turned into one of those changes is declined with the reason, rather than guessed at.
Your model, your key, and only your schema leaves
Asking your 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.
What leaves the browser is only what your ask() sends, and that is the schema, the names and types of your columns, so the model knows what it is allowed to ask for. Your row values never leave to get an answer, because the model only ever proposes a query and the grid runs it here against the real rows.
How do I let people filter a data grid in plain English?
Load the AI module and call createAI(grid, { ask }), passing the live grid and your own model callback. Then call ai.askBar(element) to drop an ask bar above the grid: a person types a question, sees the resolved query in plain English, and presses Apply. For your own interface, ai.query(question) returns the validated query and ai.applyQuery(result) runs it, so you can render the preview and the Apply button however you like.
How does asking my data stay safe?
The model returns a query, never rows, and the grid validates that query against its own columns and operators before it runs. A query naming a column that does not exist, or an operator that does not fit a column’s type, is refused rather than half applied. The resolved query is shown in plain English and waits for you to press Apply, and the whole feature only reshapes your view, so it can filter, sort and group but never change your data.
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 a few questions and turns each into a real query the grid then validates and runs. 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 validates the query and runs it here against its own rows.