streaming-ui-script · tools example ← Back to docs
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 }),
});