Seat Map
Let the user pick seats on a real layout (a plane, a cinema, a theatre with curved rows and several levels, a train or bus coach, an event floor), one per passenger or a set count.
≈ 6,499 tokens per use (tool 1,233 · guidance 355 · typical payload 4,911)How token costs are estimated
Example: Seat Map
Pick a window seat for my flight. I'm stuck in a middle seat.
NW 482 · TLV to EWR · 787-9
Seat for Alex Rivera. Your current seat is 41F.
The front of the plane is at the top. Arrow keys move between available seats. Space or Enter picks one.
- Taken
- Current seat
- €25–€75 extra
When to use
- The user picks or changes seats on a real layout: a flight, a cinema or theater show, a train or bus, an event floor.
- The agent fetched the seat map and availability from the booking system.
When not to use
- The user asked for any seat, or a type of seat the agent can just request ("any aisle seat"). Book it and say which one.
- The agent doesn't have the real map. Fetch it first; never invent rows or availability.
- The choice is between ticket types or zones, not seats. Use Product Options or the Entity Picker.
How the agent should use it
- Every row, seat and state comes from the booking system, with the system's real seat ids.
- Send rows front to back and seats left to right, with
{ gap: true }for aisles so the columns line up. - Use
passengerswhen you know who's travelling, so each person gets one seat. Otherwise setcount. - Pass the user's existing seats as
currentwhen they're changing seats. They show with a dashed outline ("Current seat" in the legend) and start selected. - Set
priceonly for seats that cost extra, withcurrency. For shows priced by area, sendzonesand set each seat'szoneinstead. - For a theatre, send each level as a section: Stalls, Dress circle, Upper circle, Boxes, lowest first. Set
stage: "below"on circles and balconies andlayout: "boxes"on boxes. - Mark seats with a restricted view with the
restricted-viewfeature, and say why innote("Pillar on the left"). Usenotefor other short facts too ("Extra leg room"). - This only picks seats. Book or change them after a
selectresult, and confirm any charge before taking payment. - Lifecycle: availability changes while the user looks, so set
fetchedAtandstaleAfterMs. If the booking system holds the seats for a time, setexpiresAttoo.
Interaction
| Input | Action |
|---|---|
| Click, Space or Enter on a seat | Picks it. Picking your own seat again removes it |
| ← → ↑ ↓ | Move between available seats, skipping aisles and taken seats. Up and down land on the nearest seat by column, or by angle on curved rows |
| Home End | First or last available seat in the row |
| Ctrl+Home / End | First or last available seat on the map |
| Passenger buttons in the footer | Choose which passenger you're picking for; each shows that passenger's seat |
| Class, section or level menu (beside the title) | Switches cabin, seating area or theatre level; picks are kept |
With passengers, a pick goes to the active passenger and then moves on to the next one without a seat. Picking another passenger's seat swaps the two. With count, picking more than the count drops the oldest pick. Each pick is announced in a live region ("14C selected for Dana. 1 of 2 assigned."). The footer shows the chosen seats large, with the extra charge (or the total, with zones) beside them, and one quiet line for progress and whether the seats are together in one row.
The map is an ARIA grid of seat buttons with a roving tabindex, so Tab enters and leaves it in one stop. Taken seats are disabled, empty faded squares (blocked ones carry a line); their state is also in each seat's name. The legend is one compact row above the map with only the entries the map needs (taken, unavailable, current seat, the green extra-charge tint with its price range, zones, restricted view, accessible); available and selected seats explain themselves. Window, aisle and other features are in each seat's name, and on hover or focus a detail line for that seat takes the legend's place. Tall or wide layouts scroll inside the card and open on the seats: the current seat when there is one, else the first row. The plane's nose scales with the cabin. When changing seats, the footer reads "New seat 23A from 41F".
Layouts
Each section draws with one of three layouts. A section's layout wins, then the map's layout, then the kind: theater is curved, everything else is a grid.
- Grid (default): rows of seats in columns, with
{ gap: true }cells as aisles. Markers (exits, wings, galleys) work only here. - Curved: rows bend into concentric arcs around the stage. Every row spreads over the same angle (70° by default, or the section's
curve), so the front rows are shorter arcs and aisles at the same place in each row line up as angular gaps. The first radius is the smallest at which no row's seats touch, and a row with far fewer seats keeps them together in the middle instead of stretching. Seats turn to face the stage with their numbers kept level, and the row label sits at both ends of the arc. The map shrinks seats to fit the card down to a 24px tap target, then scrolls sideways, starting at the middle. - Boxes: each row is a box, drawn as a small cluster beside the stage with its name ("Box A"). The first half of the boxes go on the left and the rest on the right, unless a row sets
side.
Keyboard order always follows the rows: left and right move along a row or box, and up and down go to the nearest seat in the next row that has one (by angle on curved rows).
Levels
Theatre levels are sections, and the section menu is named "Level" for screen readers when kind is theater. Send them lowest first: Stalls, Dress circle, Upper circle, Boxes. A circle or balcony with stage: "below" draws the stage as a dashed outline reading "Stage below", and tells screen readers the stage is ahead of and below the level.
sections: [
{ id: "stalls", label: "Stalls", rows: [/* 14 rows */] },
{ id: "dress-circle", label: "Dress circle", stage: "below", curve: 80, rows: [/* 6 rows */] },
{ id: "boxes", label: "Boxes", layout: "boxes", rows: [{ label: "A", seats: [/* 4 seats */] }] },
]Price zones
When zones is set ({ id, label, price }, e.g. "Band A"), seats that name a zone take its price unless they have their own. Available seats are shaded by zone, strongest for the dearest, and the legend lists each zone with its price. The shade is never the only cue: the zone's label is in each seat's name and in the detail line ("Stalls F7 · Band A · $95 · Available"). With zones, the footer shows the total rather than an extra charge. Zones without seats on the map are left out of the legend.
Seats with the restricted-view feature show an eye-off icon that the legend explains, and their note follows in the name and detail line.
Installation
npx shadcn@latest add https://your-site.com/r/seat-map.json
Usage
import { tool } from "ai"
import { seatMapTool } from "@/components/genui/seat-map.schema"
const tools = {
// No execute(): the call is completed by the user's choice in the UI.
[seatMapTool.name]: tool({
description: seatMapTool.description,
inputSchema: seatMapTool.inputSchema,
}),
}import { SeatMap } from "@/components/genui/seat-map"
function ToolPart({ part, addToolResult }) {
if (part.type !== "tool-pick_seats") return null
return (
<SeatMap
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) =>
addToolResult({
tool: "pick_seats",
toolCallId: part.toolCallId,
output: result,
})
}
/>
)
}The agent receives one of:
{ action: "select", seats: [{ seatId: "14C", passengerId: "dana", price: 25 }, { seatId: "14D", passengerId: "sam" }] }
{ action: "dismiss" }Seats come in passenger order, or in map order with count. price is included only for paid seats the user doesn't already hold; for a zoned seat it's the zone's price unless the seat has its own.
With LangGraph
Push it from a graph node with ui.push({ name: "pick_seats", 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 map shows the chosen seats read-only (or the cancelled state) and never calls onResult again.
<SeatMap
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
kind | "plane" | "cinema" | "theater" | "train" | "bus" | "venue" | Draws the framing: a nose, a screen, a stage or a front |
layout | "grid" | "curved"? | How rows are drawn. Default curved for theater, else grid |
title | string? | Data-bound. What's being booked: "NW 482 · LIS → JFK" |
sections | Section[] | Data-bound. Cabin classes, seating areas or theatre levels, front or lowest first. Up to 6 |
currency | string? | ISO 4217 code for seat prices |
zones | { id, label, price }[]? | Data-bound. Named price zones seats refer to with zone. Up to 8 |
passengers | { id, name }[]? | Data-bound. One seat each, in order. Up to 9 |
count | number | Without passengers: how many seats to pick. Default 1 |
current | { passengerId?, seatId }[]? | Data-bound. Seats the user already holds |
legend | boolean | Show the key above the map. Default true |
submitLabel | string? | Verb for the confirm button. Default "Confirm seat(s)" |
Section is { id, label, rows: { label, seats, side? }[], markers?, layout?, curve?, stage? }. layout is grid, curved or boxes and overrides the map's; curve is the arc angle in degrees (10 to 150, default 70); stage is front (default) or below; a row's side (left or right) places a box. Each entry in seats is a seat { id, label?, state, features?, price?, note?, zone? } or a gap { gap: true }. state is available, taken or blocked; features are window, aisle, middle, extra-legroom, exit-row, accessible, near-screen, power and restricted-view; note is a short fact of up to 40 characters; zone is the id of one of zones.
markers are { afterRow, kind }: exit labels an exit on both sides, rows between wing-start and wing-end get wing ticks, and galley, lavatory, screen and table draw a labelled divider. A marker whose afterRow matches no row goes before the first row.
Host props: status (streaming, ready, submitting, selected, dismissed), onResult, result and className.