Quantity Scaler
Quantities that scale live with a number of people, guests or servings, with a copyable shopping list.
≈ 1,078 tokens per use (tool 680 · guidance 227 · typical payload 171)How token costs are estimated
Example: Quantity Scaler
Sunday lamb roast for 5
When to use
- Quantities depend on a headcount the user may change: a recipe, party supplies, catering, packing for a group.
- The user asked how much to buy or bring for a number of people.
When not to use
- The count is fixed and final. Give the list in text.
- The quantities depend on more than one number, like a budget or a duration. Use a Calculator.
- You don't know the per-person amounts. Find a source first; the quantities are data, not guesses.
How the agent should use it
- Set
baseto the count the quantities are written for, andvalueto what the user asked for. "Lamb roast for 5" from a recipe for 4 isbase: 4, value: 5. - Name what the count counts with
unitLabel:{ singular: "guest", plural: "guests" }. The card shows "Number of guests" over the big number and "Guests" under it. - Choose a
scaleper item.linear(the default) grows with the count.fixednever changes ("1 oven tray", "1 bunch of rosemary").steprounds up to whole units (eggs, packs). - Give each item its natural unit. Leave
unitempty for a count. - Keep
noteto one line (120 characters), and only when it changes what to buy. - Lifecycle: quantities from a recipe are a snapshot, so none of
expiresAtorfetchedAtis needed. When the agent sends a revised list (another recipe, more items), mark the old call superseded by the new one.
Rounding
Amounts are rounded so they read like a shopping list, never down to zero:
| Unit | Rounding |
|---|---|
g, ml | To the nearest 25 (5 under 100, 1 under 20). At 1000 they switch to kg or l with one decimal: 1500 g shows as 1.5 kg |
kg, l, lb | To the nearest 0.1, shown with one decimal |
| Count (no unit) | Up to a whole number |
Other units (tbsp, rolls) | Up to the next half |
Set round (decimal places) on an item to override this. With round, the unit is kept as given, counts still round up and everything else rounds to the nearest value.
Interaction
-
The count changes with the round − and + buttons, by typing into the big number, or with the arrow keys (Page Up/Down by 5, Home/End to the limits). It stays within
minandmax(default 1–50). -
The number is a spinbutton labelled "Number of people"; button presses are announced ("6 people"), and each quantity has its exact value for screen readers while the digits roll.
-
"Copy shopping list" copies a plain list:
For 5 people • Bone-in leg of lamb — 2.0 kg • Potatoes — 1.5 kg • Carrots — 8 -
The scaler never locks. While streaming, items appear one by one and the count can't be changed until the call finishes.
Installation
npx shadcn@latest add https://your-site.com/r/quantity-scaler.json
This adds the component, its schema and quantity-scaler-utils.ts (scaling, rounding and the copied text).
Usage
import { tool } from "ai"
import { quantityScalerTool } from "@/components/genui/quantity-scaler.schema"
const tools = {
[quantityScalerTool.name]: tool({
description: quantityScalerTool.description,
inputSchema: quantityScalerTool.inputSchema,
}),
}import { QuantityScaler } from "@/components/genui/quantity-scaler"
function ToolPart({ part, addToolResult }) {
if (part.type !== "tool-scale_quantities") return null
return (
<QuantityScaler
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) =>
addToolResult({
tool: "scale_quantities",
toolCallId: part.toolCallId,
output: result,
})
}
/>
)
}The scaler is useful on its own, so the tool can also have an execute that returns immediately; then send onResult calls to the agent as messages instead. It sends set about 0.7 seconds after the user settles on a count, with the amounts as shown, so the agent knows the final headcount:
{
action: "set",
value: 6,
items: [
{ id: "lamb", quantity: 2.4, unit: "kg" },
{ id: "potatoes", quantity: 1.8, unit: "kg" },
{ id: "carrots", quantity: 9, unit: "" },
],
}
// After "Copy shopping list"
{ action: "copy" }With LangGraph
Push it from a graph node with ui.push({ name: "scale_quantities", 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. A set result restores the count the user chose. The scaler stays usable, but it never calls onResult again, so nothing is sent twice:
<QuantityScaler
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
title | string? | Heading over the list, e.g. "Your shopping quantities" |
unitLabel | { singular, plural } | e.g. person / people |
base | number | The count the quantities are written for |
value | number? | The starting count. Default base |
min, max | number | Default 1 and 50 |
items | Item[] | 1–20. Data-bound |
note | string? | One line under the list, up to 120 characters |
Item
| Field | Type | Notes |
|---|---|---|
id | string | Returned in the result |
name | string | e.g. "Bone-in leg of lamb" |
quantity | number | The amount for base |
unit | string? | g, kg, ml, l, any other unit, or empty for a count |
scale | linear | fixed | step | Default linear |
round | number? | Decimal places (0–3), overriding the unit's rounding |
Host props: status (streaming, ready), onResult, result and className.