Skip to content
GitHub

Web SDK response components

How the Runbear chat widget renders the confirm, select, card, list, table and chart components an agent attaches to its reply, what a click sends, and how components reach you on the API.


On this page

Alongside its prose, an agent can attach a response component: a small block the widget renders directly under the reply. The built-in components are confirm, a short yes/no style question; select, a set of choices; card, a formatted block of details about one record, with an optional image and links; list, a set of search results or records with links, thumbnails and short facts; table, rows of records the visitor can sort by column and page through; and chart, a line, bar or area chart of numbers.

confirm and select are offers the visitor answers. The display components — card, list, table and chart — have nothing to answer: the agent shows them without pausing for a reply, so they can arrive while the rest of the answer is still being written. The visitor can page through a list or a table, sort a table by a column, hover over or focus a chart point for its values, narrow a date chart's range, or view a chart as a table, right in the widget, without sending anything or spending a turn.

This page is the rendering side. Whether and how often an agent produces components is per-agent: the Component frequency select and the per-component switches under Agent settings → Response components, or the same fields on the Response Components API. New components start off. Turning the agent's Choice buttons (all channels) switch off withholds every component, and scheduled or automated runs never offer one.

What you need#

  • Response components enabled for your organization (see the callout above).
  • An agent whose Component frequency is set above off, with the component's switch on.
  • An SDK release that renders the component; see which SDK release renders what.

Which SDK release renders what#

ComponentRenders fromOn an older release the visitor sees
confirm0.3.00.2.x and earlier: the reply's text with the choices appended as a plain list
select0.3.0Same as confirm
card0.4.00.3.x: the card's plain-text summary (fallbackText). 0.2.x and earlier: that summary appended to the reply's text
card with image or links0.6.00.4.x and 0.5.x: the card without its image and links. Anything the links carry must also be in the card's text
list0.6.00.3.x to 0.5.x: its fallbackText. 0.2.x and earlier: that summary appended to the reply's text
table0.6.0Same as list
chart0.6.0Same as list. The summary states the key numbers

This is the intended forward-compatibility behavior, not a defect: an older widget never breaks on a newer component, it shows the summary instead. Upgrade the widget before switching a component on in Web SDK components.

From 0.6.0 a widget can also opt out of rendering a component: config.components.disabled shows each component it names as its fallbackText instead (see Configuration). It takes component names such as "chart", not the agent's switch names such as choiceButtons, and it only changes what this widget renders — the agent still produces the component. To stop the agent producing it, turn its switch off.

A click is a message#

This section is about confirm and select; a display component (card, list, table, chart) carries no options and never sends anything. Opening one of their links is ordinary navigation, not a message.

Activating an option sends that option's value into the conversation exactly as if the visitor had typed it. There is no separate callback and no component-level handler, and no answer state for you to read or reconcile: the transcript is the record. Three things worth planning for:

  • A click costs a turn. In session mode a pass carries a lifetime turn cap, 100 by default, which never resets for the life of that session. A click spends one exactly as a typed message would: the click is the visitor's next message, not an extra one on top of it, so buttons don't consume the cap any faster than typing does.
  • The conversation never pauses. A component is an offer, not a modal: nothing waits on a click, and answering in your own words instead is always valid. The message box behaves exactly as it does on any other turn.
  • One activation per open question. The widget drops a second click on a component that has already been used, so a double-click can't bill two turns. Each activation that does go through carries its own id, so two genuine answers aren't collapsed into one.

The visitor can see what a click will send. When an option's value differs from its label, the widget lists each such option under the buttons as a "Label → value" line, so a button reading "Approve" that sends "Approve the request" says so before it is clicked.

From 0.6.0, a turn that renders a confirm or select, or shows any component as its plain-text fallbackText, shows no suggestion chips, even with config.suggestions enabled: the chips would repeat the component's own options, and clicking one would send a message that makes the real component inert. A turn whose only components are display components (card, list, table, chart) keeps its chips. On 0.3.x to 0.5.x every rendered component suppresses them.

A component goes inert once it is answered#

Interactive components only; a display component (card, list, table, chart) has no answered state. Paging, sorting and chart controls stay usable.

A component stays interactive while no visitor message appears after the message that carried it and the widget is idle. Once a later visitor message exists, whether it came from a click, from typing, or from your own addUserMessage call, the controls stop sending.

Inert controls stay on screen, readable and focusable. They are the record of what was offered, so a scrolled-back conversation still reads correctly. A group the visitor clicked names the option they picked; one that went inert some other way says why (answered, or a reply still arriving). All three strings are overridable. To style the two states, see Theming.

An unrecognized component renders prose#

Every component carries a fallbackText string. A client that reads components but can't render this one shows that text instead of the controls: a name its renderer table predates, a known name whose props fail that renderer's shape check, or a renderer that threw, which also logs console.warn("Runbear: a message component failed to render"). On a reloaded transcript, a known name with unreadable props is dropped instead, leaving the message's own text to carry it. This is the forward-compatibility contract: a name added after your last upgrade reads as prose rather than breaking the widget.

One shape check is worth knowing in advance: a chart shows its fallbackText when any of its values is neither 0 nor between 1e-12 and 1e15 in magnitude, so a chart of tiny or astronomical numbers reads as its summary.

A client that doesn't read components at all, such as an SDK from before this feature or your own code against the REST API, is covered by a different mechanism: a prose rendering of the component (a display component's fallbackText, or a prompt's choices) is appended to the end of the assistant message's own content. It isn't byte-identical to what the component itself carries: markdown metacharacters are escaped on the way in, and a prompt's block is built from its captions. Each component appends its own paragraph, so a message carrying four cards ends with four of them. A client that does render the components should suppress those trailing blocks by position, one per component it rendered, rather than by matching their text.

Surviving a tab close#

Components are part of the conversation, so they come back with it. Whether the conversation itself comes back is decided by auth.storage, which by default discards it when the tab closes. See Where the session is stored. Components vanishing when the tab closes is that default working, not a defect.

Customizing the rendered strings#

Every user-visible string the widget adds to a component can be replaced: the confirm and select captions, and from 0.6.0 the pager, table and chart strings. The rest of what a component shows is the agent's own text. Each field is optional and is defaulted where it is shown, so a partial override replaces only what it names.

const client = new Runbear({
  assistantId: "…",
  config: {
    components: {
      labels: {
        untitledGroup: "Choose an option",
        optionCount: "{count} options available",
        answered: "Answered.",
        chosen: "You chose: {label}",
        streaming: "Waiting for the assistant…",
      },
    },
  },
})
LabelDefaultShown whenSince
untitledGroup"Choose an option"The accessible name for a group whose reply carried no text to borrow0.3.0
optionCount"{count} options available"Announced once per group by a screen reader0.3.0
answered"Answered."The group is inert because a later message answered it and the visitor didn't click it on this page: a reloaded transcript, or a second group in the same turn0.3.0
chosen"You chose: {label}"The visitor clicked one of this group's options on this page. Shown from the click onward and outranks answered, so write it as a record ("You chose: X"), not as a transient "Sending…"0.3.0
streaming"Waiting for the assistant…"The group is inert because the assistant is still replying0.3.0
pagerPrevious"Previous"The button that shows the previous page of a list or table0.6.0
pagerNext"Next"The button that shows the next page of a list or table0.6.0
pagerStatus"Page {page} of {count}"The page status a screen reader announces0.6.0
tableUntitled"Table with {count} rows"The accessible caption of a table with no title0.6.0
tableSortedAscending"Sorted by {label}, ascending"Announced after a table is sorted ascending by a column0.6.0
tableSortedDescending"Sorted by {label}, descending"Announced after a table is sorted descending by a column0.6.0
tableUnsorted"Original order"Announced when a table returns to its original row order0.6.0
chartTypeLine"Line chart"The start of a line chart's accessible name0.6.0
chartTypeBar"Bar chart"The start of a bar chart's accessible name0.6.0
chartTypeArea"Area chart"The start of an area chart's accessible name0.6.0
chartSummary"{count} series, {from} to {to}, values from {min} to {max}."The chart's screen-reader summary after its type and title; {count} is the series, {from} and {to} the first and last x, {min} and {max} the smallest and largest value0.6.0
chartHiddenSeries"…and {count} more series"Under a chart that carried more series than it draws0.6.0

The placeholders in braces are replaced where they appear. An older SDK ignores a key it does not know.

Components on the API#

If you read the API directly rather than through the SDK, components reach you on two carriers, both carrying the same object.

On the thread listing. GET /v1/threads/{threadId}/messages adds an optional components array to each message, in the order the components were produced, omitted entirely when the message carried none. At most 4 per message, counting every kind together.

On the stream. Both streaming routes, POST /v1/threads/{threadId}/runs/stream and the streaming form of POST /v1/chat/completions, publish a thread.message.component event (see Streaming). It carries no message id, and its position relative to thread.message.completed is not guaranteed: a confirm or select follows the completed reply it decorates, while a display component (card, list, table or chart) can be published mid-turn, before it. Attach the component to the assistant turn you are currently rendering rather than to the last message you saw complete.

{"event":"thread.message.component","data":{"componentId":"9f2b…","name":"confirm","props":{"options":[{"label":"Approve","value":"Approve the request","style":"primary"},{"label":"Reject","value":"Reject the request","style":"danger"}]},"fallbackText":"You can reply with:\n• Approve\n• Reject"}}
FieldTypeNotes
componentIdstringOpaque key for this instance, echoed back for analytics only. It is not an authorization input, and the server neither resolves it nor trusts it.
namestringThe render key. An open vocabulary, see below.
propsobjectPayload whose shape depends on name, passed through verbatim. Built-in shapes: confirm and select carry an options array of { label, value, style? }, with the question itself in the message's content; an option's style is default (outline), primary or danger (destructive), and any other token renders as default; card carries a title, an optional subtitle, an optional fields array of { label, value }, an optional image of { url, alt }, and an optional links array of up to 3 { label, url }. The SDK shows at most 10 fields and cuts a title past 200 characters, a subtitle past 500, a field label past 75 and a field value past 300 with "…"; fields beyond the tenth collapse into one "…and N more" line. A card without a title shows its fallbackText. list carries an optional title and an items array of 1 to 10 { title, subtitle?, url?, image?, meta? }, where image is { url, alt } and meta is up to 3 { label, value }; the SDK shows 5 items per page with Previous and Next controls. Every URL in a card or a list is https. table carries an optional title of up to 200 characters, a columns array of 1 to 8 { label, align? } with labels of up to 60 characters, and a rows array of 1 to 30 { cells }, where cells holds exactly one string of up to 80 characters per column; align is left or right, and any other token renders as left. The SDK drops a row whose cell count does not match the columns, sorts by any column and shows 10 rows per page. chart carries a type (line, bar or area), an optional title, an optional xType (category, the default, or date, where every x value is a YYYY-MM-DD date), an x array of 2 to 100 labels, an optional xLabel, yLabel and yUnit, and a series array of 1 to 4 { name, values }, where values holds one finite number per entry of x; series colours come from the widget's theme, never from props. A client that does not recognise name must not read props.
fallbackTextstringProse to show whenever no renderer matches name. Never empty.

The SDK exports ChatComponent for this object, and ConfirmProps and SelectProps for narrowing props once you have checked name. CardProps is exported from 0.5.0, and gains image and links in 0.6.0. See Types.

Server limits#

Runbear holds every component to these bounds before it is published:

WhatLimit
Components per messageAt most 4, counting every kind together
confirm and select options1 to 10 per component
Option labelAt most 75 characters
Option valueAt most 200 characters
fallbackTextAt most 2,000 characters
nameLowercase letters, digits and hyphens, starting with a letter (^[a-z][a-z0-9-]*$), at most 64 characters. constructor is reserved and rejected
componentIdAt most 64 characters
propsAt most 65,536 characters of JSON

Posting a click yourself#

To post a click from your own client, send the option's value as an ordinary message. Give the message a distinct id if you send two identical values in one conversation, or the second collapses into the first and the agent answers the first choice. A message id is 1–200 characters. Don't start one with rbc:: that prefix is reserved for the SDK's own click ids, whose shape is rbc:<uuid>:<uuid>, and anything else behind it rejects the whole run with a 400.