Skip to content
lab components / AI

Chat

A flexible conversational UI with pluggable adapters and built-in feedback support.

This is a Lab component!

That means it doesn't satisfy our definition of done and may be changed or even deleted. For an exact status, please reach out to the Fancy team through the dev_fancy or ux_fancy channels.

import { Chat } from "@siteimprove/fancylab";

The Chat component renders a message thread with a composer, optional welcome content, and optional error presentation. It is transport-agnostic: the component does not talk to a backend by itself. Instead, it uses a ChatAdapter to send user input and (optionally) to handle actions such as message feedback.

To integrate Chat into your product, you typically:

  • Provide and manage messages and loading state.
  • Implement a ChatAdapter (or use the built-in HTTP adapter) that updates the message state.
  • Optionally provide welcome content, error handling, and feedback support.

#Basic usage

This example uses a small in-memory adapter and keeps all state in the parent. It demonstrates the core API: adapter, messages, and loading.

AI Assistant

Let’s dive in to your Analytics data

If you're looking to explore visitor behavior, page performance, or traffic trends, you're in the right place.

Theme: agentic-ai-2025
const { adapter, messages, loading, error, resetChat } = useMockedAdapter(); const welcomeChoices: Choice[] = [ { label: "Where are users dropping off or struggling?", icon: ( <Icon> <IconAreaChart /> </Icon> ), }, { label: "Compare traffic and conversions to last month.", icon: ( <Icon> <IconSite /> </Icon> ), }, { label: "Show me unusual patterns in user behavior.", icon: ( <Icon> <IconCampaign /> </Icon> ), }, ]; return ( <div style={{ height: "800px" }}> <Chat adapter={adapter} messages={messages} loading={loading} onClear={resetChat} welcomeHeading="Let’s dive in to your Analytics data" welcomeText="If you're looking to explore visitor behavior, page performance, or traffic trends, you're in the right place." welcomeChoices={welcomeChoices} error={error} data-observe-key="ConversationalAnalytics" /> </div> );

#API

#Chat props

The Chat component is controlled: you own the state. The adapter is responsible for updating messages, loading, and any error state.

PropTypeDescription
adapterChatAdapterRequired. The transport + orchestration layer. Called when the user sends a prompt and when the UI triggers actions such as feedback.
messagesChatMessage[]The message thread to render. When empty, the welcome state can be shown.
loadingbooleanWhether the assistant is currently working. When true, the list shows a reasoning indicator.
loadingMessagestringOptional label next to the reasoning indicator.
onClose() => voidCalled when the user closes the chat (for example when the chat is rendered inside a floating container).
disableTypingEffectbooleanDisable the typing effect. This is automatically disabled for users who has enabled prefers-reduced-motion on their device.
composerPlaceholderstringPlaceholder for the text input in the composer.
welcomeHeadingstringHeading shown when there are no messages yet.
welcomeTextstringSupporting text shown in the welcome state.
welcomeChoicesChoice[]Optional prompt suggestions shown in the welcome state. When clicked, the choice label is sent as a prompt.
errorChatError | nullOptional error state. Use displayMode: "interrupt" to replace the chat with a retry screen, or displayMode: "notify" to show a non-blocking message.
onMessageInterrupted(payload) => void | Promise<void>Optional callback fired when a response is stopped during typing. The payload contains messageId, stoppedMarkdown, andcancellationMarkdown. Use this to persist interrupted state so reloads can restore the same truncated message and cancellation notice.

#Message types

Messages are rendered based on their payload type. This enables structured UI responses while keeping the message model explicit and type-safe.

#Markdown messages

Use markdown messages for regular free-form content. Markdown is rendered using the platform styling.

Payload shape:

type MarkdownChatMessagePayload = { type: "markdown"; markdown: string; };

AI Assistant

Sample Markdown Message

This is an example of a markdown message rendered in the Chat component.

  • It supports bold and italic text.
  • You can create lists:
    1. First item
    2. Second item
    3. Third item
Theme: agentic-ai-2025
const [messages] = useState<ChatMessage[]>([ { id: "1234", role: "assistant", payload: { type: "markdown", markdown: ` ## Sample Markdown Message This is an example of a markdown message rendered in the Chat component. - It supports **bold** and *italic* text. - You can create lists: 1. First item 2. Second item 3. Third item `, }, createdAt: new Date(), }, ]); const adapter: ChatAdapter = { send: async () => {}, }; return ( <div style={{ height: "420px" }}> <Chat adapter={adapter} messages={messages} loading={false} {...commonProps} /> </div> );

#Table messages

Use table messages for displaying tabular data. Tables are rendered as Fancy Table components.

Payload shape:

type TableChatMessagePayload = { type: "table"; table: TableProps<TDto>; };

AI Assistant

Basic usage table
Dish
Cook Time
Servings
Lasagna
45
2
Pancakes
20
4
Sushi
90
6
Cake
30
8
Theme: agentic-ai-2025
type DishItem = { id: number; title: string; cookTime: number; servings: number }; const items: DishItem[] = [ { id: 1, title: "Lasagna", cookTime: 45, servings: 2 }, { id: 2, title: "Pancakes", cookTime: 20, servings: 4 }, { id: 3, title: "Sushi", cookTime: 90, servings: 6 }, { id: 4, title: "Cake", cookTime: 30, servings: 8 }, ]; const [messages] = useState<ChatMessage[]>([ { id: "1234", role: "assistant", payload: { type: "table", table: { columns: [ { header: { content: "Dish", "data-observe-key": "table-header-dish", }, render: (dto: DishItem) => dto.title, options: { isKeyColumn: true, }, }, { header: { content: "Cook Time", tooltip: "in minutes", "data-observe-key": "table-header-cook-time", }, render: (dto: DishItem) => dto.cookTime, }, { header: { content: "Servings", tooltip: "in persons", "data-observe-key": "table-header-servings", }, render: (dto: DishItem) => dto.servings, }, ], items, sort: null, setSort: () => {}, loading: false, caption: "Basic usage table", }, }, createdAt: new Date(), } as ChatMessage, ]); const adapter: ChatAdapter = { send: async () => {}, }; return ( <div style={{ height: "420px" }}> <Chat adapter={adapter} messages={messages} loading={false} {...commonProps} /> </div> );

#Choices messages

Use choices messages when the assistant should guide the user with suggested actions. Choices are similar to Fancy interactive Pill components in style, but has been changed slightly to support multi-line content.

Payload shape:

type ChoicesChatMessagePayload = { type: "choices"; choices: Array<{ label: string; icon?: ReactElement }>; };

The welcome view also uses the same Choice type. This makes it easy to reuse the same prompt suggestions both before and during the conversation.

AI Assistant

Theme: agentic-ai-2025
const [messages] = useState<ChatMessage[]>([ { id: "1234", role: "assistant", payload: { type: "choices", choices: [ { label: "Option 1", icon: ( <Icon> <IconOpenNew /> </Icon> ), }, { label: "Option 2", }, { label: "Option 3", }, ], }, createdAt: new Date(), }, ]); const adapter: ChatAdapter = { send: async () => {}, }; return ( <div style={{ height: "420px" }}> <Chat adapter={adapter} messages={messages} loading={false} {...commonProps} /> </div> );

#Reasoning messages

Use reasoning messages to show the assistant's internal thought process or decision-making steps. Reasoning messages are collapsible and render after content messages (markdown, chart, table) but before choices when part of a message group. When part of a message group with reasoning, message actions are attached to the reasoning message (comes right after it, but doesn't copy its content).

Payload shape:

type ReasoningChatMessagePayload = { type: "reasoning"; markdown: string; };

AI Assistant

Let’s dive in to your Analytics data

If you're looking to explore visitor behavior, page performance, or traffic trends, you're in the right place.

Theme: agentic-ai-2025
const [messages, setMessages] = useState<ChatMessage[]>([]); const [loading, setLoading] = useState(false); const adapter: ChatAdapter = { send: async (draft: ChatMessageDraft) => { setMessages((prev) => [ ...prev, { id: crypto.randomUUID(), role: "user", payload: { type: "markdown", markdown: draft.content }, createdAt: new Date(), }, ]); setLoading(true); await new Promise((r) => setTimeout(r, 800)); // Messages are deliberately in the wrong order to demonstrate automatic sorting. // The UI will render them as: chart → markdown → reasoning + actions → choices. const groupId = crypto.randomUUID(); setMessages((prev) => [ ...prev, { id: crypto.randomUUID(), groupId, role: "assistant", payload: { type: "choices", choices: [ { label: "Drill into mobile traffic" }, { label: "Compare with last quarter" }, ], }, createdAt: new Date(), }, { id: crypto.randomUUID(), groupId, role: "assistant", payload: { type: "reasoning", markdown: `- Aggregated 12K sessions over 30 days\n- Segmented by device type and referrer\n- Detected a 15% week-over-week increase on mobile`, }, createdAt: new Date(), }, { id: crypto.randomUUID(), groupId, role: "assistant", payload: { type: "markdown", markdown: "Traffic is trending upward, especially on mobile. The chart above shows daily sessions over the past week.", }, createdAt: new Date(), feedback: MessageFeedbackType.NEUTRAL, }, { id: crypto.randomUUID(), groupId, role: "assistant", payload: { type: "chart", chart: { options: { series: [ { type: "line", name: "Sessions", data: [320, 410, 390, 480, 520, 610, 590].map((val, i) => ({ x: i, y: val, name: `Day ${i + 1}`, })), }, ], }, }, }, createdAt: new Date(), }, ]); setLoading(false); }, }; return ( <div style={{ height: "800px" }}> <Chat adapter={adapter} messages={messages} loading={loading} {...commonProps} /> </div> );

#Chart messages

Use chart messages for chart visualization. Charts are rendered as Fancy Chart components.

Payload shape:

type ChartChatMessagePayload = { type: "chart"; chart: ChartWithoutAnnotationProps | ChartWithAnnotationProps; };

AI Assistant

Theme: agentic-ai-2025
const [messages] = useState<ChatMessage[]>([ { id: "1234", role: "assistant", payload: { type: "chart", chart: { options: { series: [ { type: "bar", name: "Series 1", data: [25, 23, 48, 52, 52, 57, 56, 61, 66].map((val, i) => { return { x: i, y: val, name: i.toString(), }; }), }, { type: "bar", name: "Series 2", data: [18, 26, 36, 44, 49, 59, 65, 71, 82].map((val, i) => { return { x: i, y: val, name: i.toString(), }; }), }, ], }, }, }, createdAt: new Date(), }, ]); const adapter: ChatAdapter = { send: async () => {}, }; return ( <div style={{ height: "500px" }}> <Chat adapter={adapter} messages={messages} loading={false} {...commonProps} /> </div> );

#Message grouping

Messages that belong to the same assistant response can share a single set of action buttons by providing the same groupId. Only the last message in a group will render action buttons, reducing visual clutter when multiple content types (markdown, table, chart) are returned together.

The backend assigns the same groupId to related messages. The UI automatically calculates which message is last in each group and only renders actions for that message.

AI Assistant

Here's an analysis of your data. First, let me provide some context about what we're looking at:

Page views
Page
Views
/home
1250
/about
890
/contact
456

As you can see, traffic has been steadily increasing. Notice how all three content types above share a single set of action buttons at the bottom.

  • Scanned all page elements. Found 156 elements including images, links, and forms.
  • Checked accessibility standards and compared against WCAG 2.1 AA guidelines.
  • Identified critical issues and prioritized by impact on user experience
Theme: agentic-ai-2025
const groupId = "response-123"; const [messages] = useState<ChatMessage[]>([ { id: "msg-1", groupId, role: "assistant", payload: { type: "markdown", markdown: "Here's an analysis of your data. First, let me provide some context about what we're looking at:", }, createdAt: new Date(), }, { id: "msg-2", groupId, role: "assistant", payload: { type: "table", table: { columns: [ { header: { content: "Page", "data-observe-key": "table-header-page", }, render: (dto: unknown) => (dto as { page: string }).page, options: { isKeyColumn: true, }, }, { header: { content: "Views", "data-observe-key": "table-header-views", }, render: (dto: unknown) => (dto as { views: number }).views, }, ], items: [ { page: "/home", views: 1250 }, { page: "/about", views: 890 }, { page: "/contact", views: 456 }, ], caption: "Page views", sort: null, setSort: () => {}, loading: false, }, }, createdAt: new Date(), }, { id: "msg-3", groupId, role: "assistant", payload: { type: "chart", chart: { options: { series: [ { type: "line", name: "Trend", data: [850, 920, 1050, 1180, 1250].map((val, i) => ({ x: i, y: val, name: `Week ${i + 1}`, })), }, ], }, }, }, createdAt: new Date(), }, { id: "msg-4", groupId, role: "assistant", payload: { type: "markdown", markdown: "As you can see, traffic has been steadily increasing. Notice how all three content types above share a single set of action buttons at the bottom.", }, createdAt: new Date(), feedback: MessageFeedbackType.NEUTRAL, }, { id: "msg-5", groupId, role: "assistant", payload: { type: "choices", choices: [ { label: "Dive deeper into the traffic trends" }, { label: "Compare with previous period" }, { label: "Show me the top referral sources" }, ], }, createdAt: new Date(), }, { id: "msg-6", groupId, role: "assistant", payload: { type: "reasoning", markdown: `- Scanned all page elements. Found 156 elements including images, links, and forms. - Checked accessibility standards and compared against WCAG 2.1 AA guidelines. - Identified critical issues and prioritized by impact on user experience`, }, createdAt: new Date(), }, ]); const adapter: ChatAdapter = { send: async () => {}, }; return ( <div style={{ height: "700px" }}> <Chat adapter={adapter} messages={messages} loading={false} {...commonProps} /> </div> );

#Message sorting

Within a message group, the Chat component automatically sorts messages to ensure the correct rendering order, regardless of how they arrive from the backend:

  • Content messages (markdown, chart, table) render first in their original order.
  • Reasoning messages render next.
  • Choices messages always render last.

Message actions (copy, feedback) are attached to the last reasoning message when reasoning exists in the group. If there is no reasoning, actions appear on the last content message. The UI order is: content → reasoning + actions → choices.

In the example below, messages are provided in random order:

AI Assistant

Performance Summary

Here's an overview of your system performance. The data below shows key metrics across different dimensions.

Key metrics
Metric
Value
Uptime
99.95%
Latency
45ms
Throughput
15K req/s

Note: All messages above were provided in random order, but rendered in the correct sequence. The message actions (copy and feedback) are attached to the reasoning section.

  • Analyzed 1,250 data points across 4 time periods
  • Compared current metrics against historical averages
  • Identified 3 anomalies worth investigating
Theme: agentic-ai-2025
const groupId = "sorted-response-456"; // Messages provided in random order to demonstrate automatic sorting const [messages] = useState<ChatMessage[]>([ { id: "sort-msg-choices", groupId, role: "assistant", payload: { type: "choices", choices: [ { label: "View detailed analytics" }, { label: "Generate a report" }, { label: "Compare with competitors" }, ], }, createdAt: new Date(), }, { id: "sort-msg-markdown-1", groupId, role: "assistant", payload: { type: "markdown", markdown: "## Performance Summary\n\nHere's an overview of your system performance. The data below shows key metrics across different dimensions.", }, createdAt: new Date(), }, { id: "sort-msg-reasoning", groupId, role: "assistant", payload: { type: "reasoning", markdown: `- Analyzed 1,250 data points across 4 time periods - Compared current metrics against historical averages - Identified 3 anomalies worth investigating`, }, createdAt: new Date(), }, { id: "sort-msg-chart", groupId, role: "assistant", payload: { type: "chart", chart: { options: { series: [ { type: "bar", name: "Performance", data: [45, 52, 48, 61, 55].map((val, i) => ({ x: i, y: val, name: `Day ${i + 1}`, })), }, ], }, }, }, createdAt: new Date(), }, { id: "sort-msg-table", groupId, role: "assistant", payload: { type: "table", table: { columns: [ { header: { content: "Metric", "data-observe-key": "table-header-metric", }, render: (dto: unknown) => (dto as { metric: string }).metric, options: { isKeyColumn: true, }, }, { header: { content: "Value", "data-observe-key": "table-header-value", }, render: (dto: unknown) => (dto as { value: string }).value, }, ], items: [ { metric: "Uptime", value: "99.95%" }, { metric: "Latency", value: "45ms" }, { metric: "Throughput", value: "15K req/s" }, ], caption: "Key metrics", sort: null, setSort: () => {}, loading: false, }, }, createdAt: new Date(), }, { id: "sort-msg-markdown-2", groupId, role: "assistant", payload: { type: "markdown", markdown: "**Note:** All messages above were provided in random order, but rendered in the correct sequence. The message actions (copy and feedback) are attached to the reasoning section.", }, createdAt: new Date(), }, ]); const adapter: ChatAdapter = { send: async () => {}, }; return ( <div style={{ height: "750px" }}> <Chat adapter={adapter} messages={messages} loading={false} {...commonProps} /> </div> );

#Extra message actions

Use the Chat component's extraMessageActions prop to add custom action buttons to chart and table messages. The actions are defined once at the Chat level and receive the current message in their isVisible and onClick callbacks. This lets the consuming application decide which messages should show an action without storing React elements or callback functions in the serialized message payload.

For use cases such as "Add to dashboard", keep any dashboard-specific metadata outside Fancy in the consuming application. For example, PFG can store a serializable metadata lookup keyed by ChatMessage.id. When metadata events arrive from the stream, PFG updates that lookup. The action's isVisible callback can then check whether the current chart/table message has matching metadata, and onClick can use that metadata to open the Add to dashboard modal.

AI Assistant

Theme: agentic-ai-2025
const [showModal, setShowModal] = useState(false); const [selectedMetadata, setSelectedMetadata] = useState<{ requestType: string; request: unknown; service: string; } | null>(null); // In a real application, this would be synced with the backend as metadata arrives // during the chat session. For now, we use a static mock. const dashboardMetadataByMessageId: Record< string, { requestType: string; request: unknown; service: string } > = { "extra-actions-assistant-1": { requestType: "PfgFramework.Common.Analytics.Models.Something.SomeRequest", request: { account_id: 123, site_id: 456, period: { start: "2026-01-01", end: "2026-01-31", }, }, service: "analytics", }, }; const extraMessageActions: ChatMessageAction[] = [ { id: "add-to-dashboard", tooltip: "Add to dashboard", icon: ( <Icon> <IconAdd /> </Icon> ), isVisible: (message) => { // Only show for chart/table messages that have metadata const isChartOrTable = message.payload.type === "chart" || message.payload.type === "table"; const hasMetadata = dashboardMetadataByMessageId[message.id] !== undefined; return isChartOrTable && hasMetadata; }, onClick: (message) => { const metadata = dashboardMetadataByMessageId[message.id]; if (metadata) { setSelectedMetadata(metadata); setShowModal(true); } }, }, ]; const [messages] = useState<ChatMessage[]>([ { id: "extra-actions-assistant-1", role: "assistant", payload: { type: "chart", chart: { options: { series: [ { type: "line", name: "Trend", data: [850, 920, 1050, 1180, 1250].map((val, i) => ({ x: i, y: val, name: `Week ${i + 1}`, })), }, ], }, }, }, createdAt: new Date(), feedback: MessageFeedbackType.NEUTRAL, }, ]); const adapter: ChatAdapter = { send: async () => {}, }; const [value, setValue] = useState("Top Regional Performance"); const items = [ { value: 0, title: "Dashboard 1" }, { value: 1, title: "Dashboard 2" }, { value: 2, title: "Dashboard 3" }, { value: 3, title: "Dashboard 4" }, { value: 4, title: "Dashboard 5" }, ]; const [selectedItems, setSelectedItems] = useState<number[]>([]); return ( <> <div style={{ height: "520px" }}> <Chat adapter={adapter} messages={messages} loading={false} extraMessageActions={extraMessageActions} {...commonProps} /> </div> <Modal shown={showModal} headerTitle="Add to dashboard" onClose={() => setShowModal(false)}> <Modal.Content> <FormElementWrapper label="Widget title" name="widget-title"> <InputField value={value} onChange={setValue} fullWidth /> </FormElementWrapper> <FormElementWrapper label="Add to dashboard(s)" name="add-to-dashboard"> <Select items={items} value={selectedItems} onChange={setSelectedItems} noDefaultOption bulkActions placeholder="Select one or more" fullWidth /> </FormElementWrapper> </Modal.Content> <Modal.Footer> <ActionBar primary={{ children: "Confirm", onClick: () => { console.log("Adding to dashboard with metadata:", { widgetTitle: value, selectedDashboards: selectedItems, metadata: selectedMetadata, }); setShowModal(false); }, }} cancel={{ children: "Cancel", onClick: () => setShowModal(false) }} /> </Modal.Footer> </Modal> </> );

#Adapters

The adapter is the only piece that knows how to communicate with your backend. It receives a ChatMessageDraft when the user sends something.

type ChatMessageDraft = { content: string; type: "prompt" | "choice"; }; type ChatAdapter = { send: (draft: ChatMessageDraft) => Promise<void>; act?: (action: ChatAction) => Promise<void>; };

The Chat UI is controlled; therefore the adapter is expected to update your message state. A common pattern is:

  • Append the user message to messages as soon as the user sends.
  • Set loading while your backend request is in flight.
  • Append the assistant response as one (or more) ChatMessage items.

AI Assistant

Let’s dive in to your Analytics data

If you're looking to explore visitor behavior, page performance, or traffic trends, you're in the right place.

Theme: agentic-ai-2025
const [messages, setMessages] = useState<ChatMessage[]>([]); const [loading, setLoading] = useState<boolean>(false); const mockAdapter: ChatAdapter = { send: async (draft: ChatMessageDraft) => { // depending on the use case, one might want to implement different logic for prompts vs choices. For this demo, we treat them the same, but you could easily branch based on draft.type. setMessages((prev) => [ ...prev, { id: crypto.randomUUID(), role: "user", payload: { type: "markdown", markdown: draft.content }, createdAt: new Date(), }, ]); setLoading(true); await sleep(800); setMessages((prev) => [ ...prev, { id: crypto.randomUUID(), role: "assistant", payload: { type: "markdown", markdown: `You said: ${draft.content}`, }, createdAt: new Date(), }, ]); setLoading(false); }, act: async (action: ChatAction) => { if (action.type === "feedback") { const { feedbackValue, feedbackContext } = action.payload; setMessages((prev) => prev.map((msg) => msg.id === feedbackContext.id ? { ...msg, feedback: feedbackValue } : msg ) ); } }, }; return ( <div style={{ height: "800px" }}> <Chat adapter={mockAdapter} messages={messages} loading={loading} {...commonProps} /> </div> );

#Built-in HTTP adapter

If your backend is reachable via HTTP, you can use the built-in hook useHttpChatAdapter. The hook owns the messages and loading state, and it returns an adapter compatible with Chat.

AI Assistant

Let’s dive in to your Analytics data

If you're looking to explore visitor behavior, page performance, or traffic trends, you're in the right place.

Theme: agentic-ai-2025
const { adapter, messagesState, loadingState } = useHttpChatAdapter({ baseUrl: "https://api.example.com", endpoints: { chat: "/chat", feedback: "/feedback", }, // optional customizations mapInbound: (data): ChatMessage => ({ id: data.id, role: "assistant", payload: data.payload ?? { type: "markdown", markdown: data.content ?? "" }, createdAt: data.createdAt ? new Date(data.createdAt) : new Date(), feedback: data.feedback ?? undefined, }), // mock fetch implementation for demonstration purposes fetchImpl: async (input: RequestInfo | URL, init?: RequestInit) => { console.log("HTTP Adapter fetch called with:", input, init); await sleep(800); // Feedback endpoint demo if (String(input).endsWith("/feedback")) { return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { "Content-Type": "application/json" }, }); } const body = JSON.parse(String(init?.body ?? "{}")); return new Response( JSON.stringify({ id: crypto.randomUUID(), content: "You sent: " + (body?.messages?.[0]?.content ?? ""), }), { status: 200, headers: { "Content-Type": "application/json" } } ); }, }); const [messages] = messagesState; const [loading] = loadingState; return ( <div style={{ height: "800px" }}> <Chat adapter={adapter} messages={messages} loading={loading} {...commonProps} /> </div> );

By default, the HTTP adapter:

  • Serializes outbound drafts as { messages: [{ role: 'user', content: string }] }.
  • Maps inbound JSON into an assistant markdown message.
  • Optionally posts feedback to a dedicated feedback endpoint when configured.

You can customize the adapter using:

  • composeMessage to control how user messages are appended locally.
  • mapOutbound to match your backend request schema.
  • mapInbound to map the backend response into a ChatMessage.
  • headers and fetchImpl for auth and testing.

#Feedback

Assistant markdown messages can store feedback via the optional feedback field. The UI uses the adapter action act with type: "feedback".

Feedback values use MessageFeedbackType:

enum MessageFeedbackType { POSITIVE = 1, NEGATIVE = -1, NEUTRAL = 0 }

The built-in HTTP adapter will automatically:

  • POST feedback to endpoints.feedback when provided.
  • Update the feedback field in local message state after a successful call.

AI Assistant

This message supports feedback. Use the message actions to rate it and optionally leave a comment.

Theme: agentic-ai-2025
const initialAssistant: ChatMessage = { id: "1234", role: "assistant", payload: { type: "markdown", markdown: "This message supports feedback. Use the message actions to rate it and optionally leave a comment.", }, createdAt: new Date(), feedback: MessageFeedbackType.NEUTRAL, }; const [messages, setMessages] = useState<ChatMessage[]>([initialAssistant]); const [loading] = useState<boolean>(false); const adapter: ChatAdapter = { send: async () => { // not needed for this demo }, act: async (action: ChatAction) => { if (action.type === "feedback") { const { feedbackValue, feedbackContext } = action.payload; // In a real adapter, you would also persist feedback to your backend. setMessages((prev) => prev.map((m) => (m.id === feedbackContext.id ? { ...m, feedback: feedbackValue } : m)) ); } }, }; return ( <div style={{ height: "500px" }}> <Chat adapter={adapter} messages={messages} loading={loading} {...commonProps} /> </div> );

#Errors

Provide error to render either a blocking interrupt state or a non-blocking notification.

In the interactive example below, type notify or interrupt to simulate chat errors, or use the buttons above to trigger them directly.

AI Assistant

Show me my analytics summary.

Sure! Use the buttons below to simulate different error states.

Theme: agentic-ai-2025
const [messages, setMessages] = useState<ChatMessage[]>([ { id: "error-example-user-1", role: "user", payload: { type: "markdown", markdown: "Show me my analytics summary." }, createdAt: new Date(), }, { id: "error-example-assistant-1", role: "assistant", payload: { type: "markdown", markdown: "Sure! Use the buttons below to simulate different error states.", }, createdAt: new Date(), }, ]); const [loading] = useState(false); const [mode, setMode] = useState<"none" | "notifyWarning" | "notifyError" | "interrupt">("none"); const adapter: ChatAdapter = { send: async (draft: ChatMessageDraft) => { setMessages((prev) => [ ...prev, { id: crypto.randomUUID(), role: "user", payload: { type: "markdown", markdown: draft.content }, createdAt: new Date(), }, ]); }, }; const error: ChatError | null = (() => { switch (mode) { case "notifyWarning": return { displayMode: "notify", errorType: "warning", dismissible: true, message: "This is a non-blocking notification warning. The chat remains usable.", }; case "notifyError": return { displayMode: "notify", message: "This is a non-blocking notification error. The chat remains usable.", }; case "interrupt": return { displayMode: "interrupt", heading: "We couldn't connect to the assistant.", message: "This is a blocking interrupt error. Provide an action to retry.", action: { label: "Try again", callback: () => setMode("none"), }, }; default: return null; } })(); return ( <div style={{ height: "650px" }}> <Content gap="small" padding="none"> <Button.Group aria-label="Error mode"> <Button onClick={() => setMode("none")} variant={mode === "none" ? "primary" : "default"}> No error </Button> <Button onClick={() => setMode("notifyWarning")} variant={mode === "notifyWarning" ? "primary" : "default"} > Notify warning </Button> <Button onClick={() => setMode("notifyError")} variant={mode === "notifyError" ? "primary" : "default"} > Notify error </Button> <Button onClick={() => setMode("interrupt")} variant={mode === "interrupt" ? "primary" : "default"} > Interrupt error </Button> </Button.Group> </Content> <div style={{ height: "560px", marginTop: 16 }}> <Chat adapter={adapter} messages={messages} loading={loading} error={error} {...commonProps} /> </div> </div> );

#Layout

#Floating container

Use ChatFloatingContainer when you need a floating chat that overlays page content. The container handles positioning and the close affordance.

#Full-screen mode

The ChatFloatingContainer supports full-screen mode using the following three props: isFullScreen, onFullScreen and overlayContainer. When onFullScreen is provided, a full-screen toggle button is automatically rendered in the ChatHeader.

You must manage the full-screen state yourself and toggle it in the onFullScreen callback:

const [isFullScreen, setIsFullScreen] = useState(false); <ChatFloatingContainer data-observe-key="chat-floating" isFullScreen={isFullScreen} onFullScreen={() => setIsFullScreen(!isFullScreen)} // ...other props />

When isFullScreen is true, the container expands to fill the specified overlay container (see below). The full-screen button icon automatically updates to show either an expand or collapse icon based on the current state.

The floating container automatically adjusts its position and size based on any changes to the overlay container or the viewport.

Theme: agentic-ai-2025
const { adapter, messages, loading, error, resetChat } = useMockedAdapter(); const [shown, setShown] = useState<boolean>(false); const [isFullScreen, setIsFullScreen] = useState<boolean>(false); return ( <> <Button onClick={() => setShown(!shown)} variant="primary"> Toggle chat </Button> <ChatFloatingContainer data-observe-key="chat-floating-container" shown={shown} onClose={() => setShown(false)} isFullScreen={isFullScreen} onFullScreen={() => setIsFullScreen(!isFullScreen)} overlayContainer="#main-content" > <Chat adapter={adapter} messages={messages} loading={loading} onClose={() => setShown(false)} onClear={resetChat} error={error} data-observe-key="chat" {...commonProps} /> </ChatFloatingContainer> </> );

#Typing effect

Assistant markdown messages are rendered with a typing effect that progressively reveals the content. Inline formatting (bold, italic, code), code blocks, links, images, blockquotes, and nested lists are all handled gracefully during the animation — formatting is applied as it appears rather than showing raw syntax.

The example below demonstrates the typing effect with a large markdown response that exercises all supported syntax.

AI Assistant

Let’s dive in to your Analytics data

If you're looking to explore visitor behavior, page performance, or traffic trends, you're in the right place.

Theme: agentic-ai-2025
const typingEffectMarkdownContent = ` # Heading 1 — Typing effect ## Heading 2 — Overview This response demonstrates the full range of supported markdown syntax. ## Text formatting Plain paragraph with **bold text**, *italic text*, and \`inline code\` all in one sentence. You can also combine them: ***bold and italic*** at the same time. ## Lists Unordered list: - First item - Second item with **bold** inside - Third item with *italic* inside Ordered list: 1. Step one — install dependencies 2. Step two — run the build 3. Step three — verify the output ## Link and image Here is a [link to the Markdown specification](https://spec.commonmark.org/). And here is an image: ![Bar chart](/static/pattern-bar-chart-e711e5231e17994d58ed7defeab3776a.png) ## Code Inline code: \`const x = 42;\` Double-backtick inline code: \`\`const y = \`hello\`;\`\` Code block: \`\`\`typescript function greet(name: string): string { return \`Hello, \${name}!\`; } console.log(greet("world")); \`\`\` ## Blockquote > Simplicity is a prerequisite for reliability. > — Edsger W. Dijkstra ## Nested list - Animals - Mammals - Dog - Cat - Birds - Eagle - Parrot - Plants - Trees - Flowers `.trim(); const [messages, setMessages] = useState<ChatMessage[]>([]); const [loading, setLoading] = useState(false); const [streaming, setStreaming] = useState(false); const adapter: ChatAdapter = { send: async (draft: ChatMessageDraft) => { setMessages((prev) => [ ...prev, { id: crypto.randomUUID(), role: "user", payload: { type: "markdown", markdown: draft.content }, createdAt: new Date(), }, ]); setLoading(true); setStreaming(true); await new Promise((r) => setTimeout(r, 800)); setMessages((prev) => [ ...prev, { id: crypto.randomUUID(), role: "assistant", payload: { type: "markdown", markdown: typingEffectMarkdownContent }, createdAt: new Date(), }, ]); setLoading(false); setStreaming(false); }, }; return ( <div style={{ height: "800px" }}> <Chat adapter={adapter} messages={messages} loading={loading} streaming={streaming} {...commonProps} /> </div> );

#Accessibility

  • ...
  • ...

#Writing

  • ...
  • ...