Quantity Scaler

Quantities that scale live with a number of people, guests or servings, with a copyable shopping list.

Pattern

≈ 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

Your shopping quantities

  • Bone-in leg of lamb2.0 kg
  • Potatoes1.5 kg
  • Carrots8
  • Parsnips5
  • Tenderstem broccoli500 g
  • Lamb stock625 ml
  • Rosemary1 bunch

Lamb at 400 g per person, bone-in.

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 base to the count the quantities are written for, and value to what the user asked for. "Lamb roast for 5" from a recipe for 4 is base: 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 scale per item. linear (the default) grows with the count. fixed never changes ("1 oven tray", "1 bunch of rosemary"). step rounds up to whole units (eggs, packs).
  • Give each item its natural unit. Leave unit empty for a count.
  • Keep note to one line (120 characters), and only when it changes what to buy.
  • Lifecycle: quantities from a recipe are a snapshot, so none of expiresAt or fetchedAt is 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:

UnitRounding
g, mlTo 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, lbTo 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 min and max (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

app/api/chat/route.ts
import { tool } from "ai"
import { quantityScalerTool } from "@/components/genui/quantity-scaler.schema"

const tools = {
  [quantityScalerTool.name]: tool({
    description: quantityScalerTool.description,
    inputSchema: quantityScalerTool.inputSchema,
  }),
}
components/message.tsx
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

PropTypeNotes
titlestring?Heading over the list, e.g. "Your shopping quantities"
unitLabel{ singular, plural }e.g. person / people
basenumberThe count the quantities are written for
valuenumber?The starting count. Default base
min, maxnumberDefault 1 and 50
itemsItem[]1–20. Data-bound
notestring?One line under the list, up to 120 characters

Item

FieldTypeNotes
idstringReturned in the result
namestringe.g. "Bone-in leg of lamb"
quantitynumberThe amount for base
unitstring?g, kg, ml, l, any other unit, or empty for a count
scalelinear | fixed | stepDefault linear
roundnumber?Decimal places (0–3), overriding the unit's rounding

Host props: status (streaming, ready), onResult, result and className.

Source

On this page