Delivery Tracker

Live tracking of one delivery, with stages, the arrival time, minutes left, the courier and quick actions.

Experience · Shares patterns with Timeline, Flight Status

≈ 2,087 tokens per use (tool 1,497 · guidance 315 · typical payload 275)How token costs are estimated

Example: Delivery Tracker

Where's my food?

On the way…

Green Bowl Kitchen

3 minutes away

Arriving 10:30 AM

  1. Order received, done, 10:02 AM
  2. Preparing, done, 10:04 AM
  3. Ready for pickup, done, 10:17 AM
  4. On the way, in progress, 10:20 AM
  5. Delivered, upcoming, expected 10:30 AM
Richard, your courier

Richard O., rated 4.6 out of 5

Scooter · 12-AB-34

Delivering to 27 Elm Road

When to use

  • The user asks where their order, groceries, parcel or ride is: "Where's my food?", "When will the pizza get here?", "Track my parcel".
  • The agent has the stages and ETA from a tracking tool result.

When not to use

  • The agent doesn't have live tracking data. Never guess stages, times or couriers.
  • The user wants to place, change or cancel an order. Use the ordering components, such as the Cart.
  • It's a flight. Use Flight Status.

How the agent should use it

  • Pass every stage in order, in the source's words, with one current stage and the finished ones done. A pizza tracker is just its stages: Order received, Preparing, Baking, Quality check, Out for delivery.
  • Pass times with the delivery address's UTC offset, as the source gives them. The component shows them on that clock and works out the minutes left.
  • Only add courier once someone is assigned, and phoneAvailable only when the service can connect a call.
  • Only list actions the service supports. On a result, carry it out through the service and confirm in text. The component never shows phone numbers.
  • For live tracking, re-render the same component with new props instead of calling the tool again.
  • Lifecycle: this is live data, so set fetchedAt to the time of the latest update and staleAfterMs (about 2 minutes), so a feed that stops reporting shows "may have changed" instead of a frozen ETA. Drop both once it's delivered: a finished delivery is a record. No expiresAt. When a new tracking call replaces the card (the user asks again), supersede the old one.

What it shows

  • Header: a heading for the current stage ("On the way…", "Baking…", or "Delivered at 10:41 AM" once every stage is done) and the merchant's name as one quiet line.
  • Arrival: the minutes left in large type ("3 min"; "Now" under a minute and up to 5 minutes past; "Late" after that; hours when it's 90 minutes or more away), with "Arriving 10:30 AM" under it. When it's a day or more away, the window itself is the large line: "Arriving tomorrow, 9:00 AM–1:00 PM". Screen readers hear "3 minutes away". Hidden once delivered.
  • Segmented progress: one equal segment per stage. Done stages are filled, the current one is filled and pulses gently (no pulse with reduced motion), upcoming ones are muted. Stage names and times are read to screen readers ("Step 4 of 5, On the way"); the heading already names the current stage.
  • Issue: a warning icon and the source's message, such as "Nobody was home at 2:10 PM."
  • Courier: photo (or initials), name, the vehicle, and a round call button labelled "Call Richard O.". The rating is read to screen readers only.
  • Items: "3 items · Order #4821", which opens to list the items. The address is read to screen readers only.
  • Actions: "Contact restaurant" (store, sender or operator for other kinds), "Delivery instructions" and "Get help". With three footer actions, "Get help" becomes an icon button with the same label.

No illustrations. A merchant's own logo (as a small app-icon tile beside the title) and brand colour (which tints the progress bar) appear only when its data provides them.

Times

Every time shows on the clock of the ISO string it came from: 10:30+01:00 is 10:30 for every viewer, because that's the time at the delivery address. Formatting uses Intl only; the helpers are in delivery-tracker-utils.ts (formatEtaWindow, remaining, headingText, resolveActions, …).

Minutes left are computed after mount and refreshed every 30 seconds, so server and client render the same markup. Pass now to measure from a fixed moment instead.

Actions

ActionShown whenSends
Call buttoncourier.phoneAvailable, not delivered{ action: "contact-courier" }
Contact restaurantFood and grocery by default{ action: "contact-merchant" }
Delivery instructionsEverything but rides by default. Opens a text box inline{ action: "change-instructions", text }
Get helpAlways by default; the only one left once delivered{ action: "get-help" }

When actions is set, it's the full list (the call button still needs phoneAvailable). The card never locks: each action shows a quiet confirmation in the footer, such as "Instructions sent", and the user can act again.

Collapsed by exception. The item list starts collapsed behind its summary, because it's secondary to where the delivery is.

Live updates

Re-render with new props as the tracking source changes. Segments fill with a short animation (a transform, so it's skipped under reduced motion), and stage changes and new issues are announced in a polite live region, such as "Step 4 of 5, Out for delivery" or "Delivered at 10:41 AM". Nothing is announced on the first render.

Installation

npx shadcn@latest add https://your-site.com/r/delivery-tracker.json

Usage

app/api/chat/route.ts
import { tool } from "ai"
import { deliveryTrackerTool } from "@/components/genui/delivery-tracker.schema"

const tools = {
  [deliveryTrackerTool.name]: tool({
    description: deliveryTrackerTool.description,
    inputSchema: deliveryTrackerTool.inputSchema,
  }),
}

Render it, refreshing the props from your tracking feed:

components/message.tsx
import { DeliveryTracker } from "@/components/genui/delivery-tracker"

function ToolPart({ part }) {
  if (part.type !== "tool-track_delivery") return null
  const live = useLiveTracking(part.input) // your polling or push feed

  return (
    <DeliveryTracker
      {...live}
      status={part.state === "input-streaming" ? "streaming" : undefined}
      result={savedAction(part.toolCallId)}
      onResult={(result) => {
        // Hand it to the service (call bridge, merchant chat, support), then tell the agent.
        handleDeliveryAction(part.input.orderNumber, result)
        sendMessage({ text: `Delivery action: ${JSON.stringify(result)}` })
      }}
    />
  )
}

The agent receives one of:

{ action: "contact-courier" }
{ action: "contact-merchant" }
{ action: "change-instructions", text: "Leave it with the concierge" }
{ action: "get-help" }

With LangGraph

Push it from a graph node with ui.push({ name: "track_delivery", props: { toolCallId, ...args } }, { message }), and push it again with the same UI message id as the tracking changes. Render the thread's UI messages with the Renderer.

Restoring from history

Pass the last stored action back as result. Its confirmation shows in the footer and onResult isn't called for it. The card stays usable, since a delivery can need more than one action.

<DeliveryTracker {...props} result={{ action: "change-instructions", text: "Ring twice" }} />

Schema

PropTypeNotes
kind"food" | "grocery" | "parcel" | "ride"Picks icons, labels and default actions
merchant{ name, logo?: { src, alt }, color? }?Restaurant, store, sender or service. Shown as text, with its logo (app-icon tile) and #rrggbb brand colour only when the data provides them. Data-bound
orderNumberstring?Data-bound
stages{ id, label, state, at? }[]3–7, in order. state is "done" | "current" | "upcoming". Data-bound
eta{ at, windowEnd? }?ISO 8601 with the local offset. Data-bound
courier{ name, initials, rating?, vehicle?, phoneAvailable?, photo? }?rating out of 5, read to screen readers; photo is { src, alt }, http(s) or data:image only. Data-bound
items{ name, quantity }[]?Up to 8 lines. Data-bound
addressstring?Only the street (before the first comma) is read, to screen readers. Data-bound
issue{ kind, message }?kind is "delayed" | "address-problem" | "failed-attempt". Data-bound
asOfstringWhen the tracking data was read. Data-bound
actions("contact-courier" | "contact-merchant" | "change-instructions" | "get-help")[]?Defaults by kind and stage

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

Source

On this page