Screens and components
Tell the assistant where the user is, and what it can point at.
Screens
Call useAssistantScreen once on each page or view:
useAssistantScreen({
screenId: "inventory.products",
route: "/inventory/products",
title: "Products",
description: "Manage the catalog, pricing and stock levels.",
visibleData: { productsCount: 128, lowStockCount: 7 },
});visibleData is what makes answers concrete: "you have 7 low-stock products"
rather than "check your stock levels". Keep it to the few summary values
already on screen, such as counts, totals and statuses. It is sent on every
screen change, and it goes to the model, so never put anything there the user
cannot already see.
Say when the data has not arrived. A page usually declares itself before
its queries resolve, and the counts it computes from empty lists are zeros.
Pass loading: true until the data is in:
useAssistantScreen({ /* … */ visibleData: { lowStockCount }, loading: isLoading });While loading, visibleData is not sent. When the assistant navigates to a
screen, it waits for the screen to finish loading before reading it.
Otherwise it would tell the user there is nothing on a page that is about to
show sixteen rows.
Publish your screens too
The assistant can only navigate to a screen in your
published registry. That is deliberate: the model
picks a screenId, and the route comes from your registry, so no text the
model writes ever becomes a URL.
{
"screens": [
{
"screenId": "orders",
"route": "/orders",
"title": "Orders",
"description": "Every order, with its payment and delivery status.",
"keywords": ["sales", "purchases"],
"filters": [
{ "key": "status", "values": ["unpaid", "paid", "refunded"] },
{ "key": "from", "kind": "date" }
]
}
]
}filters lets the assistant open a screen already filtered, as query
parameters. A filter is either a list of values, or a date (YYYY-MM-DD)
checked by shape. An unknown key or an unlisted value is refused. Declare
filters only if your screen reads them from the URL. One that keeps its filters
in component state cannot be filtered from outside.
Components
Declare the elements the assistant may talk about or point at, and spread the props it returns onto a real DOM element:
const addProps = useAssistantComponent("products.addButton", {
type: "button",
label: "Add product",
description: "Opens the form to create a new product",
});
return <button {...addProps}>Add product</button>;type is one of button, input, select, table, card, link,
modal or other. Without the spread props the component is described to the
assistant but cannot be found, and it will confidently point at nothing.
Components are dropped when the screen changes, so the assistant is never told about a button on the page the user just left.
Tabs
Declare a set of tabs, and the assistant can see which one is open and switch to another. "Show me the stock report" then opens the Stock tab rather than telling the user where to click.
const [tab, setTab] = useState("sales");
useAssistantTabs({
id: "reports.tabs",
label: "Report",
tabs: [
{ value: "sales", label: "Sales" },
{ value: "stock", label: "Stock" },
],
value: tab,
onChange: setTab,
});onChange is your own code, so it works however your tabs are built: a
component library's tabs, a route segment, a query parameter, or plain state.
The assistant only ever asks for one of the values you listed.
Pass the tab that is really showing as value. A switch is reported done only
once value matches what was asked, so if your onChange did not take
effect, the assistant says so instead of claiming it worked. If onChange
throws, your error message is what the assistant hears.
Tabs change what is shown and nothing else, so switching one needs no permission or confirmation. Anything that changes data stays an action.
Tables
Declare a table, and the assistant can answer questions about what is in it: "which of these are unpaid?", "what is the oldest order here?".
useAssistantTable({
id: "orders.table",
label: "Orders on this page",
columns: [
{ key: "orderNumber", label: "Order" },
{ key: "customerName", label: "Customer" },
{ key: "total", label: "Total" },
],
rows: orders,
rowId: (order) => order.orderNumber,
rowIdName: "orderNumber",
total, // when `rows` is one page of a longer list
});The rows are not sent with the screen. The assistant is told the table and its columns, and reads rows when a question needs them, up to 50 at a time. A table therefore costs a line per answer, not its contents. Only the columns you declare ever leave the browser. Long cells are cut to 200 characters, and what is read is treated as data, never as instructions.
rowIdName says which action input a row's id fills. When the assistant
reads rows, it is told which actions this user may run here that take that
input. That comes from your registry and the user's permissions, not from the
table. Pick a distinctive name, such as orderNumber or ingredientName,
rather than name or id, which would match unrelated actions. Leave it out
for rows nothing acts on.
opens says which screen a row opens. Records are often acted on by one
id and linked by another: an order may be changed by its number and opened by
its database id. Declare the screen and the route params, and each row the
assistant reads carries the exact params to open it:
opens: { screenId: "orders.detail", params: (order) => ({ id: order.id }) },For a paged table, the assistant can read the page that is loaded and knows how many rows there are in all.
Charts
Declare a chart, and the assistant can explain it. Pass it the same rows your chart library draws from:
useAssistantChart({
id: "stock.trend",
title: "Stock trend",
data: series,
x: { key: "month", label: "Month", format: (v) => monthLabel(v) },
series: [
{ key: "inflow", name: "Stock in", unit: "NGN" },
{ key: "outflow", name: "Stock out", unit: "NGN" },
],
});The SDK summarises each series in the browser: where it starts and ends, its high and low, its total, and its largest rise and fall. That summary is what the assistant gets, a few dozen tokens a series, and it answers most questions about a trend. Models are unreliable at arithmetic over a list of numbers, so the summary is computed for them rather than left to the model. For an exact value ("what was it in March?") the assistant reads the points, thinned to 60 a series. A picture of the chart is never sent.
Set x.kind: "category" for a chart of things rather than a sequence (top
products, expenses by category), so it is not described as rising and falling.
Figures shown beside a chart, like stat cards, often live in a component below
the page that declares the screen. useAssistantValues("stockSummary", { … })
adds them to the screen's values from wherever they are rendered. Searching the rest is not built yet.
Showing instead of doing
Eight built-in commands, free, and none of them change data:
| Command | What it does |
|---|---|
navigate | Goes to a published screen, through your navigate prop. |
highlight | Draws attention to a declared component, with an optional caption. |
focus | Puts the cursor in a declared input. |
scroll_to | Scrolls a declared component into view. |
open_form | Opens a form or dialog, through your openModal prop, so the user can see what it asks. |
switch_view | Switches a set of tabs you declared, through your onChange. |
read_table | Reads rows of a table you declared, from the browser. |
read_chart | Reads the points of a chart you declared. |
Showing stops at opening the form. Filling it in and submitting it is an action, with a permission, a risk tier and a confirmation.
Wire up the two props these use:
<AssistantProvider
navigate={(route) => router.push(route)}
openModal={(key) => openDialog(key)} // return false if it is not one you can open
// …
/>Without navigate, the SDK does a full page load, which works and throws away
your app's state.