Component Library

Render product cards, carts, maps, timelines, booking slots, and other typed UI in chat from plain JSON. Schema-validated, versioned, no custom HTML.

The component library lets your AI actions respond with real UI instead of text. Your action handler returns a component type and a JSON payload; the widget validates the payload against a schema and renders a native component in the chat. Buttons on the component dispatch events back to your page code.

You never write HTML or JavaScript for the components themselves. That constraint is what makes the payloads safe to store and replay in conversation history.

Requires Agent Mode

Components are rendered through AI actions, so your bot needs Agent Mode enabled.

Two ways to use components

From the dashboard — no code. Components have built-in display actions (show_product_list, show_info_panel, …). Enable the ones your bot should use on AI Copilot Actions → Components. (cart_summary is the exception — it needs your real cart data, so it's SDK-only.) The AI fills them with data from your knowledge base and function results, and button clicks come back to the AI as chat messages to act on. Enabling is the entire setup. If your data is missing something — product links, images, prices — say so in your bot's persona, or the AI will guess a value rather than leave the field out.

Enabling built-in AI chatbot components from AI Copilot Actions in the dashboard, showing toggles for product list, carousel, map, timeline and location picker

From your page — custom data and handling. When the data must come from your API, or your page code should handle the clicks, register your own action instead: your own name, description and schema, plus a handler in your site's code. You don't enable the built-in toggles for this — they're a separate, independent path. Both render the same components, and you can mix them.

Creating a custom widget AI action in the dashboard with a name, description and generated parameter schema

Add a parameter for anything the AI should pass you — a search term, an order id. Leave the list empty if your handler doesn't need one.

Quick start (custom data via SDK)

Two things to get right before the code:

  1. Create the action in the dashboard first. Under AI Copilot Actions, add a function named show_top_products with a description of when the AI should call it and a parameters schema ({ "type": "object", "properties": {} } if it takes none). Your handler only fires for actions that exist there.
  2. Run your code after the widget script loads. $yourgptChatbot doesn't exist until then — this is the most common reason a handler never fires.
(function () {
  var script = document.createElement("script");
  script.src = "https://widget.yourgpt.ai/script.js";
  script.id = "yourgpt-chatbot";
  script.setAttribute("data-widget", "<YOUR_WIDGET_ID>");
  script.onload = setupChatbot; // everything below goes in here
  document.body.appendChild(script);
})();

Now register the action and handle the clicks:

function setupChatbot() {
  // 1. The AI calls your action; you respond with a component
  $yourgptChatbot.on("ai:action:show_top_products", async (data, helpers) => {
    const products = await fetch("/api/products/top").then((r) => r.json());

    helpers.respondComponent({
      type: "product_list",
      data: {
        items: products.map((p) => ({
          id: p.id,
          name: p.name,
          price: { amount: p.price, currency: "USD" },
          image_url: p.image,
          url: p.link,
          actions: [
            { kind: "emit", id: "add_to_cart", label: "Add", payload: { product_id: p.id } },
          ],
        })),
      },
      summary: `Showing ${products.length} products`,
    });
  });

  // 2. Component buttons report intent to your page — you own what happens.
  // An async handler drives the button's feedback: spinner while pending,
  // ✓ on success, a failure message on throw (see Acknowledgments).
  $yourgptChatbot.on("component:action", async (e) => {
    if (e.action === "add_to_cart") {
      await myCart.add(e.payload.product_id);
    }
  });
}

summary is what the AI remembers it showed. It's stored with the message and read back on the next turn, so make it descriptive — "Showing 6 products" lets the AI answer "is the second one in stock?". It isn't displayed to the visitor (the component is the response); pass showSummary: true to also render it as a chat bubble above the component. It's the fallback text anywhere the component itself can't render.

Reading the AI's arguments

If your action takes parameters — a search query, an order id — the AI's arguments arrive on the data argument as a JSON string, not an object:

$yourgptChatbot.on("ai:action:search_products", async (data, helpers) => {
  const { query } = JSON.parse(data.action?.tool?.function?.arguments || "{}");

  const products = await fetch(`/api/search?q=${encodeURIComponent(query)}`).then((r) => r.json());
  helpers.respondComponent({ type: "product_list", data: { items: products }, summary: `${products.length} results for "${query}"` });
});

The parameter names come from the schema you set on the action in the dashboard, so query here means the dashboard schema declared a query property. data also carries session_uid and message_id if you need them.

Components

TypeUse it for
product_listA list of things to choose from, with prices and buttons
item_detailOne thing in depth — gallery, specs, description
recommendation_stripA compact horizontal row of suggestions
cart_summaryLine items, quantities and a total
info_panelLabel/value rows — order details, confirmations, specs
timelineOrdered steps with states — order or request progress
booking_slotsPick a time from available slots
location_pickerAsk the visitor where they are
mapShow places on a map with an address list
carouselImages one at a time with captions
buttonsA row of choices, no card around them

Every component takes the same envelope — type, data, summary — so once one works, the rest are just a different payload shape.

product_list

A vertical list of product cards. Only id and name are required per item; every other field renders when present and disappears when omitted.

AI chatbot product list component on an online store, showing product cards with price range, sale badge, variant options and out-of-stock state
{
  "title": "Top picks",
  "items": [
    {
      "id": 42,
      "name": "Classic Leather Jacket",
      "price": { "amount": 199.0, "currency": "USD" },
      "compare_at_price": { "amount": 249.0, "currency": "USD" },
      "description": "Full-grain leather, quilted lining.",
      "image_url": "/img/jacket.jpg",
      "url": "/products/classic-leather-jacket",
      "badge": "Sale",
      "variants": [
        { "id": "v1", "label": "Black / M" },
        { "id": "v2", "label": "Black / L", "price": { "amount": 209.0, "currency": "USD" } },
        { "id": "v3", "label": "Brown / M", "available": false }
      ],
      "actions": [
        { "kind": "emit", "id": "add_to_cart", "label": "Add", "payload": { "product_id": 42 } }
      ]
    }
  ],
  "actions": [{ "kind": "link", "label": "View all", "url": "/shop" }]
}
FieldTypeRequiredNotes
itemsarrayyes1–20 items
items[].idstring | numberyesReturned in action context
items[].namestringyes
items[].priceMoneyno
items[].price_maxMoneynoRenders "price – price_max" when higher
items[].compare_at_priceMoneynoStrikethrough, only when higher than price
items[].descriptionstringnoClamped to 2 lines
items[].image_urlstringnoRelative paths allowed
items[].urlstringnoMakes image and title links
items[].badgestringnoShort chip, e.g. "Sale"
items[].availablebooleannofalse replaces actions with an "Out of stock" label
items[].variantsarrayno1–50; see below
items[].actionsAction[]noMax 3
titlestringnoList header
actionsAction[]noFooter, max 3

info_panel

A structured card for order details, booking confirmations, contact info, spec sheets.

AI chatbot info panel component on an online store, showing order status, courier, tracking number and delivery details as label and value rows
{
  "title": "Order #1042",
  "subtitle": "Placed on Jul 28",
  "fields": [
    { "label": "Status", "value": "Shipped" },
    { "label": "Tracking", "value": "1Z999AA10123456784", "url": "https://track.example.com/1Z999" }
  ],
  "actions": [{ "kind": "link", "label": "View order", "url": "/orders/1042" }]
}
FieldTypeRequiredNotes
titlestringyes
subtitlestringno
image_urlstringno44px thumbnail beside the title
fieldsarraynoMax 20 label/value rows
fields[].urlstringnoRenders the value as a link
actionsAction[]noMax 3

cart_summary

A read-only cart: items with quantities and totals, plus up to two actions (typically a checkout link). SDK-only — it needs your real cart data, so there's no dashboard preset.

AI chatbot cart summary component on an online store, showing cart line items with options, quantities, subtotal, total and a checkout action
{
  "items": [
    { "id": 7, "name": "Hoodie", "quantity": 2, "options": "Blue / L",
      "line_total": { "amount": 90.0, "currency": "USD" }, "image_url": "/img/hoodie.jpg" }
  ],
  "total": { "amount": 90.0, "currency": "USD" },
  "note": "Taxes calculated at checkout",
  "actions": [{ "kind": "link", "label": "Checkout", "url": "/checkout" }]
}

Required: items (0–30, each with id, name, quantity). An empty items array renders a localized "Your cart is empty" state — actions still show, so an empty cart can offer "Browse products". Optional per item: options, unit_price, line_total, image_url, url. Optional at the top level: title, subtotal, total, note, actions (max 2).

item_detail

A single item in depth: image gallery with prev/next, price, description, spec fields, actions.

AI chatbot item detail component showing an image gallery, price, description and specification fields
{
  "name": "Classic Leather Jacket",
  "brand": "Acme",
  "images": ["/img/jacket-1.jpg", "/img/jacket-2.jpg"],
  "price": { "amount": 199.0, "currency": "USD" },
  "description": "Full-grain leather, quilted lining.",
  "fields": [{ "label": "Material", "value": "Leather" }],
  "url": "/products/classic-leather-jacket",
  "actions": [{ "kind": "emit", "id": "add_to_cart", "label": "Add to cart", "payload": { "product_id": 42 } }]
}

Required: name. Optional: brand, badge, description, images (max 10), price, compare_at_price, fields (max 12), url, actions (max 3).

recommendation_strip

A compact horizontal row of mini product cards, one optional action each. Use product_list when items need descriptions or multiple actions.

AI chatbot recommendation strip component showing a horizontal row of mini product cards
{
  "items": [
    { "id": 9, "name": "Wool Beanie", "price": { "amount": 24.0, "currency": "USD" },
      "image_url": "/img/beanie.jpg", "url": "/products/wool-beanie",
      "action": { "kind": "emit", "id": "add_to_cart", "label": "Add", "payload": { "product_id": 9 } } }
  ]
}

Required: items (1–10, each with id, name). title defaults to a localized "You might also like".

buttons

A row of 1–6 action buttons with an optional lead-in line. No card chrome.

AI chatbot buttons component showing a row of quick reply choice chips with a link action
{
  "title": "What would you like to do?",
  "actions": [
    { "kind": "emit", "id": "book_demo", "label": "Book a demo" },
    { "kind": "link", "label": "See pricing", "url": "/pricing" }
  ]
}

A horizontal slider of image cards with prev/next navigation. Each card shows its caption under the image; tapping a card opens link (or the image) in a new tab.

AI chatbot carousel component showing a horizontal slider of image cards with captions and next navigation
{
  "title": "Room photos",
  "items": [
    { "url": "/img/room-1.jpg", "caption": "Deluxe suite" },
    { "url": "/img/room-2.jpg", "caption": "Sea view", "link": "/rooms/sea-view" }
  ]
}

Required: items (1–12, each with url). Optional per item: alt, caption, link.

map

An embedded map plus a marker list with open-in-maps links. The embed uses a keyless Google Maps iframe centered on center or the first marker; if the host page's CSP blocks the iframe, the marker list still renders.

AI chatbot map component showing store locations on an embedded map with a list of branches
{
  "title": "Our stores",
  "zoom": 12,
  "markers": [
    { "lat": 28.6139, "lng": 77.209, "label": "Connaught Place", "description": "Open 10am–9pm" },
    { "lat": 28.5355, "lng": 77.391, "label": "Noida" }
  ]
}

Required: markers (1–20, each with lat, lng). Optional: title, center, zoom (1–21, default 14), per-marker label and description.

timeline

Ordered steps with states, for order status and progress flows.

AI chatbot timeline component on an online store, showing delivery progress steps as complete, current and upcoming
{
  "title": "Order #1042",
  "steps": [
    { "label": "Ordered", "timestamp": "Jul 28", "state": "complete" },
    { "label": "Shipped", "timestamp": "Jul 29", "state": "complete" },
    { "label": "Out for delivery", "state": "current" },
    { "label": "Delivered", "state": "upcoming" }
  ]
}

Required: steps (1–15, each with label). state is one of complete, current, upcoming (default), failed. timestamp is displayed as-is; format dates yourself.

booking_slots

A grid of selectable time slots. Tapping an available slot highlights it and dispatches a component:action event; your page performs the booking.

AI chatbot booking slots component showing selectable time slots grouped by day
{
  "title": "Pick a time",
  "subtitle": "Times in IST",
  "slots": [
    { "id": "s1", "start": "2026-08-02T10:00:00+05:30" },
    { "id": "s2", "start": "2026-08-02T11:00:00+05:30", "available": false },
    { "id": "s3", "start": "2026-08-02T14:30:00+05:30" },
    { "id": "s4", "start": "2026-08-03T10:00:00+05:30" },
    { "id": "s5", "start": "2026-08-03T15:00:00+05:30" }
  ],
  "action_id": "slot_selected"
}

Required: slots (1–40, each with id and an ISO 8601 start). Slots group by day automatically — each day gets a header ("Sun, Aug 2") and its chips show the time only, so a week of availability reads as days, not one wall of chips. Grouping and formatting use the visitor's locale and timezone; a label overrides a chip's text. available: false renders disabled. With no listener, a selection goes to the AI as a chat message ("slot selected: Sun, Aug 2, 10:00 AM"); to handle it yourself, the event carries { slot_id, start } under your action_id (default slot_selected):

$yourgptChatbot.on("component:action", (e) => {
  if (e.component === "booking_slots" && e.action === "slot_selected") {
    bookAppointment(e.payload.slot_id);
  }
});

Acknowledgments apply here too: return a promise from the handler and a rejection un-selects the slot and shows your error message under the grid. Selection state is visual only and resets on reload — your booking system owns the truth.

location_picker

Collects a location from the visitor: a "use my current location" button (browser geolocation, no key needed) and a place search box.

AI chatbot location picker component asking the visitor for a delivery address with a use my current location button

Above is the picker without a Google key — geolocation only. Add a key and a place search box appears under the button; see the setup steps below.

{
  "title": "Where should we deliver?",
  "action_id": "delivery_location",
  "countries": ["in"],
  "bias": { "lat": 28.6139, "lng": 77.209 }
}

All fields are optional: title, subtitle, action_id (default location_selected), allow_current_location, allow_search, placeholder, bias (search near a point), countries (ISO alpha-2, max 5).

Search needs a Google key, set on your page — never in the payload, which is stored in conversation history:

  1. Already loading Google Maps JS? The picker uses it automatically, and "current location" also returns a street address.

  2. Otherwise set a key once on your page. Enable Places API (New) for it and restrict it to your domain:

    <script>window.YGPT_GOOGLE_MAPS_KEY = "AIza...";</script>
  3. With neither, the search box hides and geolocation still works, emitting coordinates without an address.

Picking dispatches through the acknowledgment pipeline:

$yourgptChatbot.on("component:action", async (e) => {
  if (e.action === "delivery_location") {
    // e.payload = { lat, lng, address, place_id?, source: "geolocation" | "search" }
    const ok = await checkDeliveryArea(e.payload.lat, e.payload.lng);
    if (!ok) throw "Sorry, we don't deliver there yet"; // shown on the picker
  }
});

On success the picker shows the address, a map preview, and a Change button. Selection is visual state — your handler owns what the location means.

No-code alternative: ask_user_location. When the AI needs the location itself and you don't want host-page code, this built-in tool renders the same picker in the composer tray, waits for the visitor, and hands the result straight back to the AI:

AI calls ask_user_location({ question: "Where should we deliver?" })
→ visitor picks (current location or search)
→ AI receives { "Where should we deliver?": "{\"lat\":28.61,\"lng\":77.2,\"address\":\"...\",\"source\":\"search\"}" }
→ { "_dismissed": true } if the visitor declines

The request survives a page refresh, and history replays the answer as a pinned address. Search needs the same Google key as above.

Variants

Use variants to offer one product in several sizes, colours or configurations, instead of listing each one as its own row.

AI chatbot product variant picker showing size and colour options with their own prices and a sold out variant

When an item has variants, its emit actions open a picker; choosing one dispatches the action with variant_id and variant_label added to the payload and context. A single variant skips the picker. The card shows an "N options" hint next to the price.

If variants differ in price, set the item's price to the lowest and price_max to the highest — the card shows the range and the picker shows each variant's own price.

Resolve the price on your server

The pick event carries variant_id, not a price. Always look the real price up from that id — never trust a price sent by the browser.

Acknowledgments apply here too: an async handler keeps the chosen row spinning until it settles, success closes the picker, and a rejection shows your message inside it.

Variant fieldTypeRequiredNotes
idstring | numberyesMerged into the action payload as variant_id
labelstringyese.g. "Black / M"
priceMoneynoFalls back to the item price
compare_at_priceMoneynoStrikethrough when higher
image_urlstringnoFalls back to the item image
availablebooleannofalse renders a disabled "Sold out" row

Shared types

Money

{ "amount": 19.99, "currency": "USD" }

The widget formats amount + currency (ISO 4217) with the visitor's locale. If your backend only has display strings, pass formatted and it wins:

{ "amount": 19.99, "currency": "USD", "formatted": "$19.99" }

Actions

Two kinds. link opens a URL. emit reports an intent — with two handling modes:

  • Default (no code): if your page has no component:action listener, the click is sent into the conversation as a visitor message (e.g. "Add: Classic Tee — Black / L (product_id: 42)") and the AI acts on it — calling your project's functions, confirming, continuing the flow.
  • Override (your code): register a component:action listener and it receives the event instead — instant, silent, no AI turn. This is the right mode when your page should call your API directly.
{ "kind": "link", "label": "View order", "url": "/orders/1042", "new_tab": false }
{ "kind": "emit", "id": "add_to_cart", "label": "Add", "payload": { "product_id": 42 } }

new_tab defaults to true. payload is arbitrary JSON, delivered verbatim. Both kinds accept disabled: true to render the button disabled.

Acknowledgments

Return a promise from your component:action handler and the widget drives the button's feedback for you: a spinner while it's pending, a check on success, and a failure message inline on rejection.

$yourgptChatbot.on("component:action", async (e) => {
  if (e.action === "add_to_cart") {
    await myCart.add(e.payload.product_id); // button spins until this settles
    // resolve → ✓ on the button
    // throw "Only 2 left in stock" → that text shown on the component
    // throw new Error("anything else") → generic localized failure message
  }
});

Visitors only ever see text you chose to show them:

  • Shown verbatim — a string rejection, or an Error carrying a userMessage property: throw Object.assign(new Error("SKU-42 depleted"), { userMessage: "Only 2 left in stock" }).
  • Shown as a generic "Something went wrong" — everything else, so network errors and internal API text never leak. The real error goes to the console.

A handler that returns nothing gets a brief "sent" flash, and one that hangs fails after 10 seconds — a timeout is never shown as success.

The component:action event:

$yourgptChatbot.on("component:action", (e) => {
  // e.component  "product_list"
  // e.action     "add_to_cart"        (your id)
  // e.payload    { product_id: 42 }   (your payload)
  // e.context    { item_id: 42, item_name: "Classic Leather Jacket" }
});

context tells you where in the component the click happened: product actions carry item_id/item_name (plus variant_id/variant_label when a variant was picked), item_detail actions carry item_name. List-footer and buttons actions have no narrower scope, so they omit it.

Validation

Payloads are validated when you call respondComponent, and again when conversation history replays.

  • A missing or invalid required field rejects the whole component. It is not rendered, the AI receives a plain-text failure so the conversation continues, and the browser console lists every violated path:

    [YourGPT] Component "product_list" rejected — fix the data passed to respondComponent:
      - items[0].name: Invalid input: expected string, received undefined
  • Optional fields degrade one by one. { "items": [{ "id": 1, "name": "Widget" }] } is a complete, valid product list.

  • Unknown keys are stripped silently. If an optional field never appears, check its spelling first — a typo is dropped without a warning.

  • URL fields (url, image_url, link) accept http, https, mailto, tel, or a relative path. Anything else is rejected, which is what keeps a stored payload safe to replay later.

Styling

Components inherit the widget theme (colors, fonts) automatically. For deeper changes, every component carries stable class names you can target from your site's CSS or the dashboard's custom CSS field — these names are part of the public API and won't change, unlike the generated ones next to them.

ClassWhere
ygpt-cEvery component root
ygpt-c-<type>Component root, e.g. ygpt-c-product-list, ygpt-c-info-panel
ygpt-c-headerHeader row
ygpt-c-itemRepeated rows/cards: products, fields, cart lines, markers, steps
ygpt-c-pricePrice blocks
ygpt-c-actionAction buttons (ygpt-c-action--primary on the first)
ygpt-c-slotBooking slot chips
/* Example: brand the primary action and square off the cards */
.ygpt-c-action--primary { background: #1a1a2e; border-radius: 4px; }
.ygpt-c-product-list .ygpt-c-item { padding: 14px; }

Language and RTL

Text in a component comes from two places, handled differently:

  • Your data (names, labels, captions, titles): the widget renders it verbatim, so supply it in the visitor's language. AI-generated arguments already follow the conversation language. If your handler builds strings itself, add a user_locale string property to your action's schema — the AI fills it with the visitor's chat language ("en", "fr", …) and you can localize from there.
  • Built-in chrome ("Total", "You might also like"): translated by the widget's own language setting.

Numbers, currencies, and slot times are formatted with the widget's active locale, not the browser locale — a visitor chatting in French sees 199,00 € even on an English browser. timeline timestamps are the exception: they render as-is, so format them yourself.

Right-to-left languages work without configuration. When the widget language is Arabic or Hebrew, components mirror automatically — layout and price alignment follow the text direction.

Versioning

Each payload is stamped with the schema version it was written with, so old conversations keep rendering after a schema changes. You don't manage any of this — emit the current shape and the widget handles the rest.

On this page