Hi. Ask me anything about Club Med. I'm here to help you find the right information quickly.
- Accordion
- Advanced Toast
- AlertMessage
- ArrowButton
- Arrows
- Avatar
- Backdrop
- Basic Toast
- Breadcrumb
- Button
- CardBackground
- Card
- Carousel
- Chapters
- ChatBody
- ChatButton
- ChatHeader
- ChatInput
- ChatMessageActions
- ChatMessageEditor
- ChatMessage
- ChatTypingIndicator
- ChatWindowCsatPrompt
- ChatWindowQuote
- ChatWindow
- CheckboxSelect
- Checkbox
- Checkboxes
- Chip
- ChoiceExpander
- Clickable
- ContentReview
- Countdown
- CriteriaDates
- CriteriaParticipants
- CriteriaTransports
- Date Field
- Datepicker
- Dropdown
- ElasticHeight
- ExpandableCard
- ExpandableText
- Filter
- FloatingLabelTextField
- Form Control
- Form Label
- Frame
- HamburgerIcon
- Heading
- Image
- JourneyCard
- LayerHeader
- Layer
- Link
- Loader
- Number Field
- PageNavigation
- Pagination
- Paragraph
- Password
- Phone Field
- Popin
- Portal
- ProductThumbnail
- Promises
- Prose
- Radio Group
- RadioSelect
- Radio
- Range
- Scrollbar
- Select
- SidebarLayout
- SliderFrame
- SparkleWrapper
- Spinner
- Switch
- Table
- Tabs
- Tag
- TextField
- Toggle
- Tooltip
- ValidationMessage
My son is 14 years old, and I would like to find activities for him during our stay.
'use client';
import { useState } from 'react';
import { ChatBody } from '@/ui/ChatBody';
import type { ChatMessage } from '@/ui/ChatBody';
import { ChatInput } from '@/ui/ChatInput';
export function ChatBodyDemo() {
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: '1',
sender: 'assistant',
content:
"Hi. Ask me anything about Club Med. I'm here to help you find the right information quickly.",
},
{
id: '2',
sender: 'user',
content:
'My son is 14 years old, and I would like to find activities for him during our stay.',
},
]);
const [inputValue, setInputValue] = useState('');
const [isTyping, setIsTyping] = useState(false);
function handleSubmit() {
if (!inputValue.trim()) return;
const userMsg: ChatMessage = {
id: Date.now().toString(),
sender: 'user',
content: inputValue,
};
setMessages((prev) => [...prev, userMsg]);
setInputValue('');
setIsTyping(true);
setTimeout(() => {
setIsTyping(false);
setMessages((prev) => [
...prev,
{
id: (Date.now() + 1).toString(),
sender: 'assistant',
content: 'Let me look into that for you…',
},
]);
}, 2000);
}
return (
<div className="chat-border bg-white flex flex-col w-380 h-660 rounded-32 overflow-hidden">
<ChatBody messages={messages} isTyping={isTyping} />
<div className="shrink-0 px-8 pb-8 pt-12">
<ChatInput value={inputValue} onValueChange={setInputValue} onSubmit={handleSubmit} />
</div>
</div>
);
}
Installation
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/chat-body.json
Usage
ChatBody renders the message log only — pair it with ChatInput for a working chat panel.
import { ChatBody } from '@/ui/ChatBody';
import type { ChatMessage } from '@/ui/ChatBody';
import { ChatInput } from '@/ui/ChatInput';
import { useState } from 'react';
export function Component() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [inputValue, setInputValue] = useState('');
const [isTyping, setIsTyping] = useState(false);
function handleSubmit() {
if (!inputValue.trim()) return;
setMessages((prev) => [
...prev,
{ id: Date.now().toString(), sender: 'user', content: inputValue },
]);
setInputValue('');
// Trigger your AI call here, then set isTyping accordingly
}
return (
<div className="chat-border bg-white flex flex-col w-380 h-660 rounded-32 overflow-hidden">
<ChatBody messages={messages} isTyping={isTyping} />
<div className="shrink-0 px-8 pb-8 pt-12">
<ChatInput value={inputValue} onValueChange={setInputValue} onSubmit={handleSubmit} />
</div>
</div>
);
}ChatBody is typically composed alongside ChatHeader and ChatInput — see the
ChatWindow docs for the full
composable pattern.
Message actions
Give ChatBody onMessageReplay, onMessageEdit and/or onMessageCopy to show the
actions of its user messages. Each callback receives the
message concerned, and onMessageEdit also receives the new text once the user sent the in-place
edition (only text messages can be edited).
<ChatBody
messages={messages}
onMessageReplay={(message) => resend(message)}
onMessageEdit={(message, content) => resendEdited(message, content)}
onMessageCopy={(message) => navigator.clipboard.writeText(String(message.content))}
messageActionLabels={{ copy: 'Copier', copied: 'Copié' }}
/>It is up to you to update the conversation: a typical edition replaces the message and removes the ones after it before generating a new answer.
Ask for Feedback (CSAT)
Call chatRef.current.askForFeedback() at any point to inject the CSAT flow into the message log.
When the user picks a star rating, three things happen in sequence:
- A user-side message appears in the log with the rating label (e.g.
"4 stars — Satisfied"). - The star buttons lock — the prompt stays visible but cannot be re-submitted.
- A thank-you note from the assistant is appended below.
onFeedbackRateis called with the chosen value (1–5).
import { useRef } from 'react';
import { ChatBody } from '@/ui/ChatBody';
import type { ChatHandle } from '@/ui/ChatBody';
import type { ChatWindowCsatPromptLabels, ChatWindowCsatThankYouLabels } from '@/ui/ChatWindow';
const csatLabels: ChatWindowCsatPromptLabels = {
intro: 'Your opinion matters! Tell us what you think.',
question: 'From 1 to 5, how satisfied are you with the service you received,',
questionNote: '1 star being "Very dissatisfied" and 5 stars "Very satisfied".',
groupAriaLabel: 'Rate your experience from 1 to 5 stars',
starLabels: ['Very dissatisfied', 'Dissatisfied', 'Neutral', 'Satisfied', 'Very satisfied'],
};
const csatThankYouLabels: ChatWindowCsatThankYouLabels = {
title: 'Thank you for your feedback!',
body: 'Your opinion means a lot to us.',
};
function csatRenderRatingLabel(rating: number) {
return `${rating} star${rating > 1 ? 's' : ''} — ${csatLabels.starLabels[rating - 1]}`;
}
export function Component() {
const chatRef = useRef<ChatHandle>(null);
return (
<ChatBody
ref={chatRef}
messages={messages}
csatLabels={csatLabels}
csatRenderRatingLabel={csatRenderRatingLabel}
csatThankYouLabels={csatThankYouLabels}
onFeedbackRate={(rating) => console.log('Rated:', rating)}
/>
);
}Show Price Quotes
Call chatRef.current.showQuotes(quotes) to inject a quote block into the message log.
Each call appends a new block below any previous ones, so multiple rounds of quotes stack naturally in the conversation.
import { useRef } from 'react';
import { ChatBody } from '@/ui/ChatBody';
import type { ChatHandle } from '@/ui/ChatBody';
import type { ChatWindowQuoteItem } from '@/ui/ChatWindowQuote';
export function Component() {
const chatRef = useRef<ChatHandle>(null);
return (
<ChatBody
ref={chatRef}
messages={messages}
onQuoteCtaClick={(quote: ChatWindowQuoteItem) => console.log('Selected:', quote.resortName)}
/>
);
}API Reference
ChatBody
| Prop | Type | Default | Description |
|---|---|---|---|
messages | ChatMessage[] | — | Required. Ordered list of messages to display |
greeting | string | undefined | Optional static greeting text shown above the message list |
isTyping | boolean | false | When true, shows the animated typing indicator |
onMessageReplay | (message: ChatMessage) => void | undefined | Renders the replay action on the user messages |
onMessageEdit | (message: ChatMessage, content: string) => void | undefined | Enables the in-place edition of the user text messages, called with the new text |
onMessageCopy | (message: ChatMessage) => void | Promise<void> | undefined | Renders the copy action on the user messages. A check confirms it once it succeeded |
messageActionLabels | Partial<ChatMessageLabels> | English | Labels of the actions and of the editor |
onFeedbackRate | (rating: number) => void | undefined | Called with the star rating (1–5) when the user submits feedback |
csatLabels | ChatWindowCsatPromptLabels | undefined | Labels for the rating prompt. Required to enable the CSAT flow |
csatRenderRatingLabel | (rating: number) => ReactNode | undefined | Renders the caption shown under the active star. Required with CSAT |
csatThankYouLabels | ChatWindowCsatThankYouLabels | undefined | Labels for the thank-you note shown after submission |
onQuoteCtaClick | (quote: ChatWindowQuoteItem) => void | undefined | Called with the selected quote when a "See prices" CTA is tapped |
className | string | undefined | Additional CSS classes on the message-log element |
Inherits all remaining HTMLDivElement attributes.
ChatHandle
Imperative handle exposed via ref. Use forwardRef or useRef<ChatHandle>.
| Method | Signature | Description |
|---|---|---|
askForFeedback | () => void | Displays the CSAT star-rating prompt in the message log |
showQuotes | (quotes: ChatWindowQuoteItem[]) => void | Appends a quote block to the message log. Each call stacks a new block below the previous. |
ChatMessage
interface ChatMessage {
id: string;
sender: 'assistant' | 'user';
content: ReactNode;
}Notes
- The component is entirely controlled from the outside —
messagesandisTypingare managed by the parent. - Renders with
role="log"andaria-live="polite"so screen readers announce new messages automatically. - Does not render an input bar — pair it with
ChatInput(see the Usage example above). askForFeedback()is idempotent per mount — calling it more than once only shows the prompt once.- The CSAT flow only activates when all three
csat*props are provided. Omitting any of them silently disables the flow. - The rating message appended to the log is purely internal to
ChatBodyand does not touch the parent-controlledmessagesarray.