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.

Pattern

≈ 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
Row 21
Row 22
Row 23
Row 24
Row 25
Row 26
Row 27
Row 28
Row 29
Row 30
Row 35
Row 36
Row 37
Row 38
Row 39
Row 40
Row 41
Row 42
Row 43
Row 44
Row 45
Row 46
Row 47
Row 48
Row 49
Row 50
Current seat41F

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 passengers when you know who's travelling, so each person gets one seat. Otherwise set count.
  • Pass the user's existing seats as current when they're changing seats. They show with a dashed outline ("Current seat" in the legend) and start selected.
  • Set price only for seats that cost extra, with currency. For shows priced by area, send zones and set each seat's zone instead.
  • For a theatre, send each level as a section: Stalls, Dress circle, Upper circle, Boxes, lowest first. Set stage: "below" on circles and balconies and layout: "boxes" on boxes.
  • Mark seats with a restricted view with the restricted-view feature, and say why in note ("Pillar on the left"). Use note for other short facts too ("Extra leg room").
  • This only picks seats. Book or change them after a select result, and confirm any charge before taking payment.
  • Lifecycle: availability changes while the user looks, so set fetchedAt and staleAfterMs. If the booking system holds the seats for a time, set expiresAt too.

Interaction

InputAction
Click, Space or Enter on a seatPicks 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 EndFirst or last available seat in the row
Ctrl+Home / EndFirst or last available seat on the map
Passenger buttons in the footerChoose 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

app/api/chat/route.ts
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,
  }),
}
components/message.tsx
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

PropTypeNotes
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
titlestring?Data-bound. What's being booked: "NW 482 · LIS → JFK"
sectionsSection[]Data-bound. Cabin classes, seating areas or theatre levels, front or lowest first. Up to 6
currencystring?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
countnumberWithout passengers: how many seats to pick. Default 1
current{ passengerId?, seatId }[]?Data-bound. Seats the user already holds
legendbooleanShow the key above the map. Default true
submitLabelstring?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.

Source

On this page