Calculator
A single-use calculator the agent builds from inputs and formulas. It never writes code.
≈ 2,018 tokens per use (tool 1,404 · guidance 353 · typical payload 261)How token costs are estimated
Example: Calculator
How much could my Roth IRA grow?
Illustrative Roth IRA growth
When to use
- The answer depends on numbers the user may want to adjust: headcount, budget, rate, duration.
- The user is exploring a trade-off, such as a budget split, a recipe scale or a loan term.
When not to use
- All the inputs are known and fixed. Calculate the answer and say it in a sentence.
- You're unsure of the right formula. Explain the method in text instead.
- The numbers have to come from live data that the formulas can't see. Fetch the data first.
How it works
The agent sends inputs, each with a control (stepper, slider, number, choice or toggle), and outputs, each with a formula. Every output updates as the user changes an input. Later outputs can use earlier ones:
{
"inputs": [{ "id": "bill", "label": "Bill", "default": 186.4 }],
"outputs": [
{ "id": "tipAmount", "label": "Tip", "formula": "bill * tip / 100" },
{ "id": "total", "label": "Total", "formula": "bill + tipAmount" }
]
}Formulas run in a small, safe language, not JavaScript. There's no eval, no property access and no strings, so a model can't run code through a formula.
| Supported | |
|---|---|
| Operators | + - * / % ^, plus < > <= >= == != (comparisons return 1 or 0) |
| Functions | min, max, round(x, digits), floor, ceil, abs, sqrt, pow, clamp(x, lo, hi), if(condition, then, else) |
| Names | Input IDs and earlier output IDs |
If a formula refers to something unknown, that one output shows a formula error instead of a wrong number. Results that aren't defined, like division by zero, show "—".
Layout
The title sits small and quiet. Outputs are tiles on a grid that never leaves one tile alone on a row: up to four share a row, more split evenly (5 → 3+2, 6 → 3+3), and on a narrow card they go two to a row with the primary output on its own row when the count is odd. Each tile is a large number with its label as a short caption above it. An output label can include an input ID in braces, which shows that input's current value, so "Projected balance after {years} years" reads "Projected balance after 30 years" and follows the slider.
A breakdown splits the primary output into two or three parts, drawn as a stacked bar with each part's label and value under it (first on the left, last on the right). The labels carry the meaning, so the bar never relies on color alone:
{
"id": "balance",
"label": "Projected balance after {years} years",
"formula": "if(rate == 0, monthly * 12 * years, monthly * ((1 + rate / 1200) ^ (12 * years) - 1) / (rate / 1200))",
"primary": true,
"breakdown": {
"parts": [
{ "label": "Your contributions", "formula": "monthly * 12 * years" },
{ "label": "Investment growth", "formula": "balance - monthly * 12 * years" }
]
}
}Bands name the ranges an output falls into. The tile shows the current band as a small tag, and a segmented scale under the tiles fills the current band and marks the value. A band's optional tone (good, caution, bad) colours it; the band names stay visible, so colour is never the only signal. Use bands instead of an output that encodes categories as numbers ("1 under, 2 healthy…"):
{
"id": "bmi",
"label": "Your BMI",
"formula": "weight / (height / 100) ^ 2",
"primary": true,
"bands": [
{ "label": "Under", "below": 18.5, "tone": "caution" },
{ "label": "Healthy", "below": 25, "tone": "good" },
{ "label": "Over", "below": 30, "tone": "caution" },
{ "label": "Obese", "tone": "bad" }
]
}Part formulas use the same language and can read any output. Negative parts get no width in the bar but still show their value. Below a divider come the inputs. A slider shows its label on the left, its value in bold on the right, a thick track, and ticks under it: min, a round middle and max by default (0–625 shows "$0 $300 $625"), or the values in ticks.
How the agent should use it
- Start from what the user said. Default every input to their numbers, and only expose inputs they might change.
- Mark one output as
primary. It's shown large at the top of the card, above the inputs, and it should be the answer to their question. Its label is the caption, so write it as one short line. - Use a
breakdownwhen the answer is a sum of parts (contributions and growth, principal and interest) instead of adding more outputs. - Use
bandswhen the value falls into named ranges (a BMI category, a risk level), not a separate coded output. - Keep output labels to 2–4 words (40 characters at most). Never put a legend or a list of codes in a label.
- Prefer sliders. Any value with a sensible range (weight, height, age, amounts, rates) gets a slider with
min,maxandsteparound the user's value; use a number field only when no range makes sense. - Give every output a format or a unit, and round sensibly. Inputs take
decimalstoo, e.g.0for whole dollars. - Keep
assumptionsto one short line. They're joined into one quiet line at the bottom of the card, so state only what changes the decision (e.g. "Assumes ROAS stays flat as spend grows."). Present results as estimates, not financial advice. descriptionis not shown. It's kept for screen readers; put anything the user must see in the title or the inputs.- Add
submitLabelwhen the numbers should feed what you do next, like "Use this split" before a Change Preview. - Lifecycle: usually nothing to set. If defaults come from live data (rates, balances), set
fetchedAtandstaleAfterMs.
Installation
npx shadcn@latest add https://your-site.com/r/calculator.json
This adds the component, its schema and calculator-formula.ts, the formula engine.
Usage
import { tool } from "ai"
import { calculatorTool } from "@/components/genui/calculator.schema"
const tools = {
[calculatorTool.name]: tool({
description: calculatorTool.description,
inputSchema: calculatorTool.inputSchema,
}),
}import { Calculator } from "@/components/genui/calculator"
function ToolPart({ part, addToolResult }) {
if (part.type !== "tool-build_calculator") return null
return (
<Calculator
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) =>
addToolResult({
tool: "build_calculator",
toolCallId: part.toolCallId,
output: result,
})
}
/>
)
}Without submitLabel, the calculator is the whole answer, so give the tool an execute that returns immediately. With it, the agent receives:
{
action: "submit",
inputs: { budget: 10000, metaShare: 70, metaRoas: 2.4, googleRoas: 3.1 },
outputs: { metaSpend: 7000, googleSpend: 3000, revenue: 26100, blended: 2.61 },
}With LangGraph
Push it from a graph node with ui.push({ name: "build_calculator", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.
Restoring from history
When a conversation is reloaded, pass the stored result back as result. The component renders its finished state (the numbers that were sent) and never calls onResult again, so a decision can't be made twice. With the AI SDK, that's the tool call's output:
<Calculator
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
title | string | What it calculates |
description | string? | Screen readers only, e.g. where the defaults come from |
inputs | Input[] | 1–6 inputs |
outputs | Output[] | 1–8 outputs |
assumptions | string[]? | Up to 4, joined into one line at the bottom. Keep it to one short line |
submitLabel | string? | Adds a button that sends the numbers back |
Input
| Field | Type | Notes |
|---|---|---|
id | string | Name used in formulas |
label | string | |
control | stepper | slider | number | choice | toggle | Default slider. toggle is 1 (on) or 0 (off) |
default | number | |
min, max, step | number? | A slider without bounds brackets its default, e.g. 175 → 0–400 |
ticks | number[]? | Sliders only. 2–5 values marked under the track. Default min, a round middle and max |
format | number | currency | percent | percent shows 12.5 as 12.5% |
currency | string? | ISO 4217 code |
decimals | number? | 0–4 digits shown |
unit | string? | e.g. "people" |
options | { label, value }[] | 2–6, required for choice |
Output
| Field | Type | Notes |
|---|---|---|
id | string | Later formulas can use it |
label | string | 2–4 words, 40 characters at most. May include an input ID in braces, e.g. {years}, filled with its current value |
formula | string | See the table above |
format, currency, decimals, unit | Display | |
primary | boolean? | Mark exactly one |
breakdown | { parts: { label, formula }[] }? | 2–3 parts of the primary output, shown as a stacked bar. Only drawn on the primary output |
bands | { label, below?, tone? }[]? | 2–6 named ranges, lowest first; below is where each ends (omit on the last); tone is good, caution or bad. Shown as a tag and a scale; one scale per calculator |
Host props: status (streaming, ready, submitted), onResult, result and className.