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
Ask me a question
My son is 14 years old, and I would like to find activities for him during our stay.
'use client';
import { useRef, useState } from 'react';
import { ChatWindow } from '@/ui/ChatWindow';
import type {
ChatWindowCsatPromptLabels,
ChatWindowCsatThankYouLabels,
ChatWindowHandle,
ChatWindowMessage,
} from '@/ui/ChatWindow';
const csatLabels: ChatWindowCsatPromptLabels = {
intro: 'Your opinion matters! Tell us what you think. Your feedback helps us improve.',
question:
'Following our conversation, from 1 to 5, how satisfied are you with the level of 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. Thanks to your responses, we keep improving our services to offer you an even more exceptional Club Med experience.',
};
function csatRenderRatingLabel(rating: number) {
return `${rating} star${rating > 1 ? 's' : ''} — ${csatLabels.starLabels[rating - 1]}`;
}
export function ChatWindowDemo() {
const chatRef = useRef<ChatWindowHandle>(null);
const [messages, setMessages] = useState<ChatWindowMessage[]>([
{
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: ChatWindowMessage = {
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 (
<ChatWindow
aria-label="Assistant chat"
ref={chatRef}
messages={messages}
inputValue={inputValue}
isTyping={isTyping}
onInputChange={setInputValue}
onSubmit={handleSubmit}
onClose={() => {}}
onAudioClick={() => {}}
onFeedbackRate={(rating) => console.log('Feedback rating:', rating)}
csatLabels={csatLabels}
csatRenderRatingLabel={csatRenderRatingLabel}
csatThankYouLabels={csatThankYouLabels}
className="w-380 h-660"
/>
);
}
Installation
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/chat-window.json
Usage
ChatWindow supports two forms: an opaque, props-only form for simple cases, and a
composable form where you assemble the panel yourself from ChatHeader, ChatBody,
and ChatInput. The composable form is the recommended way to build a chat panel — it
gives you full control over the layout while keeping ChatWindow's outer chrome
(border, rounded corners, close button).
Props-only
import { ChatWindow } from '@/ui/ChatWindow';
import type { ChatWindowMessage } from '@/ui/ChatWindow';
import { useState } from 'react';
export function Component() {
const [messages, setMessages] = useState<ChatWindowMessage[]>([]);
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 (
<ChatWindow
messages={messages}
inputValue={inputValue}
isTyping={isTyping}
onInputChange={setInputValue}
onSubmit={handleSubmit}
onClose={() => console.log('close')}
onAudioClick={() => console.log('audio')}
/>
);
}Composable (recommended)
Pass ChatHeader, ChatBody, and ChatInput as children instead of the message-list
props. When ChatWindow receives children, it renders them as-is inside its bordered
wrapper and ignores title/logo/messages/inputValue/etc.
import { ChatWindow } from '@/ui/ChatWindow';
import { ChatHeader } from '@/ui/ChatHeader';
import { ChatBody } from '@/ui/ChatBody';
import { ChatInput } from '@/ui/ChatInput';
import type { ChatMessage } from '@/ui/ChatBody';
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 (
<ChatWindow onClose={() => console.log('close')}>
<ChatHeader title="Ask me a question" />
<ChatBody messages={messages} isTyping={isTyping} />
<div className="shrink-0 px-8 pb-8 pt-12">
<ChatInput value={inputValue} onValueChange={setInputValue} onSubmit={handleSubmit} />
</div>
</ChatWindow>
);
}Advanced: compose without ChatWindow
If you don't need ChatWindow's bordered wrapper or close button — for example to embed
the chat inside an existing layout, or to fully customize the outer markup — skip
ChatWindow entirely and compose ChatHeader, ChatBody, and ChatInput directly.
This reproduces exactly what ChatWindow renders internally when it has no children.
import { ChatHeader } from '@/ui/ChatHeader';
import { ChatBody } from '@/ui/ChatBody';
import { ChatInput } from '@/ui/ChatInput';
import type { ChatMessage } from '@/ui/ChatBody';
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">
<ChatHeader title="Ask me a question" />
<ChatBody messages={messages} isTyping={isTyping} />
<div className="shrink-0 px-8 pb-8 pt-12">
<ChatInput value={inputValue} onValueChange={setInputValue} onSubmit={handleSubmit} />
</div>
</div>
);
}Ask for Feedback (CSAT)
Ask me a question
Hi. Ask me anything about Club Med. I'm here to help you find the right information quickly.
My son is 14 years old, what activities are available for him during our stay?
Great question! For teenagers aged 13–17, Club Med offers the Passworld programme — supervised sports, creative workshops, and evening events designed specifically for that age group.
'use client';
import { useRef } from 'react';
import { ChatWindow } from '@/ui/ChatWindow';
import type {
ChatWindowCsatPromptLabels,
ChatWindowCsatThankYouLabels,
ChatWindowHandle,
ChatWindowMessage,
} from '@/ui/ChatWindow';
const messages: ChatWindowMessage[] = [
{
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, what activities are available for him during our stay?',
},
{
id: '3',
sender: 'assistant',
content:
'Great question! For teenagers aged 13–17, Club Med offers the Passworld programme — supervised sports, creative workshops, and evening events designed specifically for that age group.',
},
];
const csatLabels: ChatWindowCsatPromptLabels = {
intro: 'Your opinion matters! Tell us what you think. Your feedback helps us improve.',
question:
'Following our conversation, from 1 to 5, how satisfied are you with the level of 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. Thanks to your responses, we keep improving our services to offer you an even more exceptional Club Med experience.',
};
function csatRenderRatingLabel(rating: number) {
return `${rating} star${rating > 1 ? 's' : ''} — ${csatLabels.starLabels[rating - 1]}`;
}
export function ChatWindowCsatDemo() {
const chatRef = useRef<ChatWindowHandle>(null);
return (
<div className="flex flex-col items-center gap-12">
<ChatWindow
aria-label="Assistant chat"
ref={chatRef}
messages={messages}
inputValue=""
onInputChange={() => {}}
onSubmit={() => {}}
csatLabels={csatLabels}
csatRenderRatingLabel={csatRenderRatingLabel}
csatThankYouLabels={csatThankYouLabels}
onFeedbackRate={(rating) => console.log('Feedback rating:', rating)}
onClose={() => {}}
className="w-380 h-660"
/>
<button
type="button"
onClick={() => chatRef.current?.askForFeedback()}
className="px-16 py-8 bg-saffron rounded-pill text-black text-b4 font-semibold hover:opacity-80 transition-opacity"
>
Trigger feedback prompt
</button>
</div>
);
}
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 { ChatWindow } from '@/ui/ChatWindow';
import type {
ChatWindowCsatPromptLabels,
ChatWindowCsatThankYouLabels,
ChatWindowHandle,
} 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<ChatWindowHandle>(null);
return (
<ChatWindow
ref={chatRef}
messages={messages}
inputValue={inputValue}
onInputChange={setInputValue}
onSubmit={handleSubmit}
csatLabels={csatLabels}
csatRenderRatingLabel={csatRenderRatingLabel}
csatThankYouLabels={csatThankYouLabels}
onFeedbackRate={(rating) => console.log('Rated:', rating)}
/>
);
}Show Price Quotes
Ask me a question
Hi. Ask me anything about Club Med. I'm here to help you find the right information quickly.
Can you show me some quotes for a family trip to the Caribbean?
Of course! Here are some resorts in the Caribbean that could be perfect for your family vacation.
'use client';
import { useRef } from 'react';
import { ChatWindow } from '@/ui/ChatWindow';
import type { ChatWindowHandle, ChatWindowMessage, ChatWindowQuoteItem } from '@/ui/ChatWindow';
const messages: ChatWindowMessage[] = [
{
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: 'Can you show me some quotes for a family trip to the Caribbean?',
},
{
id: '3',
sender: 'assistant',
content:
'Of course! Here are some resorts in the Caribbean that could be perfect for your family vacation.',
},
];
const quotes: ChatWindowQuoteItem[] = [
{
id: '1',
resortName: 'Cancún',
country: 'Mexico',
imageUrl: 'https://picsum.photos/seed/cancun/400/300',
dateRange: '15 June 2026 to 24 June 2026',
pax: 'for 2 adults 2 children',
packageIncludes: [
'Meals and snacks at any time',
'Drinks, alcoholic and non-alcoholic',
'Sports and activities',
],
additionalInfo: 'You can customise your transport and room category directly on the website.',
},
{
id: '2',
resortName: 'Punta Cana',
country: 'Dominican Republic',
imageUrl: 'https://picsum.photos/seed/puntacana/400/300',
dateRange: '15 June 2026 to 24 June 2026',
pax: 'for 2 adults 2 children',
},
];
export function ChatWindowQuotesDemo() {
const chatRef = useRef<ChatWindowHandle>(null);
return (
<div className="flex flex-col items-center gap-12">
<ChatWindow
aria-label="Assistant chat"
ref={chatRef}
messages={messages}
inputValue=""
onInputChange={() => {}}
onSubmit={() => {}}
onQuoteCtaClick={(quote) => console.log('Selected quote:', quote.resortName)}
onClose={() => {}}
className="w-380 h-660"
/>
<button
type="button"
onClick={() => chatRef.current?.showQuotes(quotes)}
className="px-16 py-8 bg-saffron rounded-pill text-black text-b4 font-semibold hover:opacity-80 transition-opacity"
>
Trigger quotes
</button>
</div>
);
}
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.
- Single quote — renders as a full-width card with resort image, dates, a saffron CTA, and optional package details.
- Multiple quotes — renders as compact square cards in a horizontal scroll zone.
import { useRef } from 'react';
import { ChatWindow } from '@/ui/ChatWindow';
import type { ChatWindowHandle, ChatWindowQuoteItem } from '@/ui/ChatWindow';
const quotes: ChatWindowQuoteItem[] = [
{
id: '1',
resortName: 'Cancún',
country: 'Mexico',
imageUrl: 'https://example.com/cancun.jpg',
dateRange: '15 June 2026 to 24 June 2026',
pax: 'for 2 adults 2 children',
packageIncludes: ['Meals and snacks at any time', 'Drinks included', 'Sports and activities'],
additionalInfo: 'You can customise your transport directly on the website.',
},
{
id: '2',
resortName: 'Punta Cana',
country: 'Dominican Republic',
imageUrl: 'https://example.com/puntacana.jpg',
dateRange: '15 June 2026 to 24 June 2026',
pax: 'for 2 adults 2 children',
},
];
export function Component() {
const chatRef = useRef<ChatWindowHandle>(null);
return (
<ChatWindow
ref={chatRef}
messages={messages}
inputValue={inputValue}
onInputChange={setInputValue}
onSubmit={handleSubmit}
onQuoteCtaClick={(quote) => console.log('Selected:', quote.resortName)}
/>
);
}API Reference
ChatWindow
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | undefined | Composable form: pass ChatHeader/ChatBody/ChatInput (or any custom markup) to fully control the layout. When set, all props below except onClose/title/logo are ignored |
messages | ChatWindowMessage[] | — | Required unless children is set. Ordered list of messages to display |
inputValue | string | — | Required unless children is set. Controlled value of the text input |
onInputChange | (value: string) => void | — | Required unless children is set. Called on every keystroke in the input |
onSubmit | () => void | — | Required unless children is set. Called when the user submits a message |
title | string | "Ask me a question" | Heading displayed in the intro header |
greeting | string | undefined | Optional static greeting text shown above the message list |
inputPlaceholder | string | "Ask a question?" | Placeholder text for the input field |
isTyping | boolean | false | When true, shows the animated typing indicator |
onClose | () => void | undefined | When provided, renders a close button below the panel |
onAudioClick | () => void | undefined | Enables the microphone button in the input |
submitKey | "mod+enter" | "enter" | "mod+enter" | Keyboard shortcut that submits the message, see ChatInput |
isStreaming | boolean | false | While true, the send button becomes a stop button and submitting is disabled |
onStop | () => void | undefined | Called when the stop button, shown while isStreaming, is clicked |
onMessageReplay | (message: ChatWindowMessage) => void | undefined | Renders the replay action on the user messages |
onMessageEdit | (message: ChatWindowMessage, content: string) => void | undefined | Enables the in-place edition of the user text messages, called with the new text |
onMessageCopy | (message: ChatWindowMessage) => void | Promise<void> | undefined | Renders the copy action on the user messages |
messageActionLabels | Partial<ChatMessageLabels> | English | Labels of the message actions and of the editor |
logo | ReactNode | undefined | Custom logo node; defaults to a Sparkles icon button |
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 outer wrapper |
ChatHeader
Renders the intro header (title + logo/AI-assistant button). Used standalone when composing.
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | "Ask me a question" | Heading text |
logo | ReactNode | undefined | Custom logo node; defaults to a Sparkles icon button |
ChatBody
Renders the scrollable message log — greeting, messages, typing indicator, CSAT flow, and quotes. Does not render the input bar; pair it with ChatInput.
| 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 |
onMessageCopy | (message: ChatMessage) => void | Promise<void> | undefined | Renders the copy action on the user messages |
messageActionLabels | Partial<ChatMessageLabels> | English | Labels of the message 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 |
ChatBody exposes the same imperative handle as ChatWindow (askForFeedback, showQuotes) via ref.
ChatInput
Renders the message text field, send button (a stop button while isStreaming), and optional microphone button. It also takes submitKey, isStreaming and onStop. See its own docs page for the full prop reference.
ChatWindowHandle
Imperative handle exposed via ref. Use forwardRef or useRef<ChatWindowHandle>.
| 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. |
ChatWindowMessage
interface ChatWindowMessage {
id: string;
sender: 'assistant' | 'user';
content: ReactNode;
}Notes
- The component is entirely controlled from the outside — all state (messages, input value, typing indicator) is managed by the parent.
ChatWindowis composable: passChatHeader/ChatBody/ChatInputaschildrento control the layout yourself, or skipChatWindowentirely and compose them inside your own wrapper (see Advanced: compose without ChatWindow).- The message list (
ChatBody) usesrole="log"witharia-live="polite"so screen readers announce new messages automatically. isTypingshould be set totruewhile waiting for the AI response, thenfalsewhen the response arrives.- Built from sub-components:
ChatHeader,ChatBody,ChatInput,ChatMessage,ChatTypingIndicator,ChatWindowCsatPrompt, andChatWindowCsatThankYou. 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.