Option Builder

Pick a combination of options where each choice carries metrics, and see the totals, budgets and score update before continuing.

Pattern

≈ 2,664 tokens per use (tool 1,624 · guidance 307 · typical payload 733)How token costs are estimated

Example: Option Builder

Which APIs should we combine for the widget?

Combine APIs for the widget

Load time
368 msof 500 ms
Size
329 KBof 512 KB

Score 89 out of 100, grade B

Step 1 of 3
External

3 selected · 368 ms · 329 KB

When to use

  • The user is assembling a combination where every choice has a cost or impact: APIs and their load time, plan add-ons and their price, PC parts within a budget, integrations and their monthly cost.
  • Seeing the combined total, and whether it fits a budget, should shape the choice.

When not to use

  • The options have no numbers to combine. Use the Entity Picker or the Question Stepper.
  • The agent doesn't have real metrics for every option. Fetch them first; never estimate prices, sizes or timings.
  • It's one product with variants (size, color). Use Product Options.

How the agent should use it

  • Every metric value and budget comes from a tool result or the user, never from the model.
  • Order metrics by importance. The first two are the big header numbers; on each row they sit on the right, the first in body text and the second quieter.
  • Use requires for real dependencies and excludes for real conflicts, and set a group's max: 1 for one-of choices like a CPU.
  • Only add score when budgets exist, and say what the budget is in the reply.
  • Over budget is allowed. On submit, read totals and point out any overrun before moving on.
  • On dismiss, drop the flow and ask what the user would like instead.

How it works

BehaviorDetails
TotalsEach metric combines the selected options with its aggregate: sum, max, min or avg. Options without a value for a metric don't take part. An empty selection sums to 0; max, min and avg show "—"
BudgetsWith better: "lower" the budget is a ceiling ("of 500 ms", then "Over budget by 81 ms"); with "higher" it's a floor ("Target 99.9", then "Below target by …"). The warning is text plus an icon, never color alone
ScoreEvery budgeted metric scores 100 at or under half its budget, falls linearly to 70 at the budget and to 0 at twice the budget (inverted for "higher"). The score is the rounded average; grades are A ≥ 90, B ≥ 80, C ≥ 70, D ≥ 60, F below. Exactly on budget is a C
RequiresSelecting an option selects everything it requires, transitively ("Adds Auth service"); the dependency shows "Included with Session store". Removing a dependency removes what needed it. Cycles are safe
ExcludesWorks both ways. A conflicting option is faded and disabled, with the reason in a tooltip ("Can't combine with Algolia Search"), including conflicts its own dependencies would bring in
Limitsselection.max caps the whole selection, dependencies included; a group's max caps that group. max: 1 turns options into radios, and picking another replaces the previous one
ContinuingThe primary button is disabled until selection.min and every group's min are met; the footer says what's missing ("Choose CPU"), and once that's met it sums up the choice ("3 selected · 368 ms · 329 KB"). It reads "Next" while steps.current < steps.total, otherwise "Done", unless submitLabel is set. Cancel dismisses

Selections persist across tabs, and each change is announced in a polite live region, e.g. "Added Mapbox GL. Load time 582 ms, over budget by 82 ms. Size 611 KB, over budget by 99 KB. Score 57, grade F." The pure helpers (aggregation, budgets, score, dependency resolution, formatting) live in option-builder-utils.ts so the host can reuse them, e.g. to recheck totals on the server.

Installation

npx shadcn@latest add https://your-site.com/r/option-builder.json

Usage

app/api/chat/route.ts
import { tool } from "ai"
import { optionBuilderTool } from "@/components/genui/option-builder.schema"

const tools = {
  // No execute(): the call is completed by the user's choice in the UI.
  [optionBuilderTool.name]: tool({
    description: optionBuilderTool.description,
    inputSchema: optionBuilderTool.inputSchema,
  }),
}
components/message.tsx
import { OptionBuilder } from "@/components/genui/option-builder"

function ToolPart({ part, addToolResult }) {
  if (part.type !== "tool-build_selection") return null

  return (
    <OptionBuilder
      {...part.input}
      status={part.state === "input-streaming" ? "streaming" : undefined}
      result={part.state === "output-available" ? part.output : undefined}
      onResult={(result) =>
        addToolResult({
          tool: "build_selection",
          toolCallId: part.toolCallId,
          output: result,
        })
      }
    />
  )
}

The agent receives one of:

{
  action: "submit",
  selected: ["northwind-vision", "contoso-speech", "fabrikam-search"],
  totals: { load: 368, size: 336896 },
  score: { value: 89, grade: "B" },
}
{ action: "dismiss" }

selected includes dependencies that were added automatically. totals leaves out metrics with no value (e.g. max of nothing).

With LangGraph

Push it from a graph node with ui.push({ name: "build_selection", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.

Restoring from history

Pass the stored result back as result. The builder shows the submitted totals, score and selected options (or the closed state) read-only, and never calls onResult again.

<OptionBuilder
  {...part.input}
  result={part.state === "output-available" ? part.output : undefined}
/>

Schema

PropTypeNotes
kind"apis" | "add-ons" | "hardware" | "other"?What is being built. Shown as a coloured icon beside the title
titlestringWhat the user is building
descriptionstring?One short line under the title. Budgets already show on the totals, so don't repeat them here
metrics{ id, label, unit?, format?, currency?, aggregate, budget?, better }[]1–4. format: number, currency, bytes (values in bytes, 1 KB = 1,024), duration-ms. budget is data-bound
score{ label?, formula: "budget" }?0–100 score and grade from the budgets
groups{ id, label, min?, max?, options }[]1–6; tabs when more than one
groups[].options{ id, name, logo?, description?, metrics, badges?, disabled?, requires?, excludes? }[]metrics (value per metric id) is data-bound. logo is { url, darkUrl?, alt? } from the catalog, shown in a tile beside the name (white in light mode; dark, with darkUrl, in dark mode); badges ≤ 2; disabled is the reason
selection{ min?, max? }min defaults to 1
steps{ current, total }?Shows "Step 1 of 3" and picks Next vs Done
defaultSelectedstring[]?Option ids to start with
submitLabelstring?Primary button label

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

Source

On this page