Live demo · no LLM required
Wire your own data with setTools()
Query() reads data and Mutation() writes it. Both
delegate to plain async functions you register on the element via
el.setTools({ name: handler }). The three demos below render the
very same components a real LLM would produce — but the tools are mocked in
this page so you can study the wiring end-to-end.
1. Read with Query
A live filter on top of an in-memory contacts list. The
$search state variable two-way-binds to the input. Whenever it
changes, the list_contacts tool runs again — same flow you'd
get talking to a real backend.
$search = ""
data = Query("list_contacts", {q: $search}, {rows: []})
searchBox = FormControl("Search", Input("search", "Filter by name, email, or role…", "text", null, $search))
empty = TextContent("No contacts match your filter.", "small", "muted")
tbl = Table([
Col("Name", data.rows.name),
Col("Email", data.rows.email),
Col("Role", data.rows.role),
Col("Team", @Each(data.rows, "row", Tag(row.team, null, "sm", row.teamColor)))
])
view = @Count(data.rows) > 0 ? tbl : empty
root = Stack([Card([CardHeader("Team contacts", "Type to filter" ), searchBox, view])])
2. Read + write with Query and Mutation
The list of todos is read by list_todos, while add_todo and
toggle_todo mutate it. Buttons run @Run(...) followed by
@Run(list) to refresh the view. Adding an item also resets the
input via @Reset($newTitle).
$newTitle = ""
list = Query("list_todos", {}, {rows: []})
add = Mutation("add_todo", {title: $newTitle})
toggle = Mutation("toggle_todo", {id: $toggleId})
$toggleId = ""
addBtn = Button("Add", Action([@Run(add), @Run(list), @Reset($newTitle)]), "primary")
input = FormControl("New todo", Input("newTitle", "Buy milk…", "text", null, $newTitle))
form = Stack([input, addBtn], "row", "m", "end")
rowToggle = @Each(list.rows, "t", Button(t.done ? "Mark open" : "Mark done", Action([@Set($toggleId, t.id), @Run(toggle), @Run(list)]), t.done ? "secondary" : "primary", "normal", "small"))
status = @Each(list.rows, "t", Tag(t.done ? "Done" : "Open", null, "sm", t.done ? "success" : "info"))
tbl = Table([
Col("Title", list.rows.title),
Col("Status", status),
Col("Actions", rowToggle)
])
empty = TextContent("Nothing on the list yet — add your first todo above.", "small", "muted")
view = @Count(list.rows) > 0 ? tbl : empty
root = Stack([Card([CardHeader("Todos", "Stored in memory in this page"), form, view])])
3. Live data with polling
Pass a refreshSeconds argument to Query() to auto-poll the
tool. Here a fake metrics endpoint returns a fresh data point every two seconds —
the chart redraws without any extra wiring.
data = Query("metrics", {}, {labels: [], series: []}, 2)
chart = LineChart(data.labels, [Series("Requests/min", data.series)])
status = TextContent("Auto-refreshing every 2s", "small", "muted")
root = Stack([Card([CardHeader("Live metrics", "Polled tool"), chart, status])])
How setTools() works
Tools are plain functions. They receive the args object the language called them with,
and may return a value or a promise. The returned value becomes the result of the
corresponding Query / Mutation.
const el = document.querySelector("streaming-ui-script");
el.setTools({
list_contacts: ({ q }) => ({ rows: filterContacts(q) }),
list_todos: () => ({ rows: store.todos }),
add_todo: ({ title }) => { store.add(title); return { ok: true }; },
toggle_todo: ({ id }) => { store.toggle(id); return { ok: true }; },
metrics: async () => ({ labels, series }),
});