---
title: ChatWindow
description: A fully controlled chatbot panel with a header, scrollable message list, typing indicator, and input bar.
---

```tsx
'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

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/chat-window.json
```

Or manually, copy the following code into `/ui/ChatWindow.tsx`:

```tsx
'use client';

import { Icon } from '@clubmed/trident-icons';
import { forwardRef, type ReactNode } from 'react';
import { twMerge } from '../helpers/twMerge';
import type { ChatMessageSender } from './ChatMessage';
import type { ChatWindowCsatPromptLabels } from './ChatWindowCsatPrompt';
import type { ChatWindowCsatThankYouLabels } from './ChatWindowCsatThankYou';
import type { ChatWindowQuoteItem } from './ChatWindowQuote';
import { ChatHeader, type ChatHeaderProps } from './ChatHeader';
import { ChatBody, type ChatBodyProps } from './ChatBody';
import { ChatInput, type ChatInputProps } from './ChatInput';

export type { ChatWindowCsatPromptLabels, ChatWindowCsatThankYouLabels, ChatWindowQuoteItem };

export interface ChatWindowMessage {
  id: string;
  sender: ChatMessageSender;
  content: ReactNode;
}

interface ChatWindowInputProps {
  inputValue: ChatInputProps['value'];
  inputPlaceholder?: ChatInputProps['placeholder'];
  onInputChange: ChatInputProps['onValueChange'];
  onSubmit: ChatInputProps['onSubmit'];
  onAudioClick?: ChatInputProps['onAudioClick'];
  submitKey?: ChatInputProps['submitKey'];
  isStreaming?: ChatInputProps['isStreaming'];
  onStop?: ChatInputProps['onStop'];
}

interface ChatWindowBaseProps extends ChatHeaderProps {
  onClose?: () => void;
}

// Either compose the panel from `ChatHeader`/`ChatBody`/`ChatInput` children,
// or fall back to the opaque props-only form — in which case the message-log
// and input props become required.
export type ChatWindowProps = ChatWindowBaseProps &
  (
    | ({ children: ReactNode } & Partial<ChatBodyProps> & Partial<ChatWindowInputProps>)
    | (ChatBodyProps & ChatWindowInputProps & { children?: undefined })
  );

export interface ChatWindowHandle {
  askForFeedback: () => void;
  showQuotes: (quotes: ChatWindowQuoteItem[]) => void;
}

export const ChatWindow = forwardRef<ChatWindowHandle, ChatWindowProps>(function ChatWindow(
  {
    title,
    onClose,
    logo,
    className,
    children,
    inputValue,
    inputPlaceholder,
    onInputChange,
    onSubmit,
    onAudioClick,
    submitKey,
    isStreaming,
    onStop,
    onMessageReplay,
    onMessageEdit,
    onMessageCopy,
    messageActionLabels,
    greeting,
    messages,
    isTyping,
    onFeedbackRate,
    csatLabels,
    csatRenderRatingLabel,
    csatThankYouLabels,
    onQuoteCtaClick,
    ...props
  },
  ref,
) {
  return (
    <div {...props} className={twMerge('flex flex-col gap-20 items-center', className)}>
      <div className="chat-border bg-white flex flex-col h-full w-full rounded-32 overflow-hidden">
        {children ?? (
          <>
            <ChatHeader title={title} logo={logo} />
            <ChatBody
              greeting={greeting}
              messages={messages!}
              isTyping={isTyping}
              onMessageReplay={onMessageReplay}
              onMessageEdit={onMessageEdit}
              onMessageCopy={onMessageCopy}
              messageActionLabels={messageActionLabels}
              onFeedbackRate={onFeedbackRate}
              csatLabels={csatLabels}
              csatRenderRatingLabel={csatRenderRatingLabel}
              csatThankYouLabels={csatThankYouLabels}
              onQuoteCtaClick={onQuoteCtaClick}
              ref={ref}
            />
            <div className="shrink-0 px-8 pb-8 pt-12">
              <ChatInput
                value={inputValue!}
                placeholder={inputPlaceholder}
                onValueChange={onInputChange!}
                onSubmit={onSubmit!}
                onAudioClick={onAudioClick}
                submitKey={submitKey}
                isStreaming={isStreaming}
                onStop={onStop}
              />
            </div>
          </>
        )}
      </div>

      {onClose && (
        <button
          type="button"
          aria-label="Close chat"
          onClick={onClose}
          className="flex items-center justify-center size-48 rounded-full bg-black border border-black text-white shrink-0 hover:opacity-80 transition-opacity outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2"
        >
          <Icon name="CrossDefault" width="24px" />
        </button>
      )}
    </div>
  );
});
```

## 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

```tsx showLineNumbers
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.

```tsx showLineNumbers
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.

```tsx showLineNumbers
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)

```tsx
'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:

1. A **user-side message** appears in the log with the rating label (e.g. `"4 stars — Satisfied"`).
2. The star buttons lock — the prompt stays visible but cannot be re-submitted.
3. A **thank-you note** from the assistant is appended below.
4. `onFeedbackRate` is called with the chosen value (`1`–`5`).

```tsx showLineNumbers
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

```tsx
'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.

```tsx showLineNumbers
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](/docs/components/chat-input#submit-with-enter) |
| `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

```tsx
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.
- `ChatWindow` is composable: pass `ChatHeader`/`ChatBody`/`ChatInput` as `children` to control the layout yourself, or skip `ChatWindow` entirely and compose them inside your own wrapper (see [Advanced: compose without ChatWindow](#advanced-compose-without-chatwindow)).
- The message list (`ChatBody`) uses `role="log"` with `aria-live="polite"` so screen readers announce new messages automatically.
- `isTyping` should be set to `true` while waiting for the AI response, then `false` when the response arrives.
- Built from sub-components: `ChatHeader`, `ChatBody`, `ChatInput`, `ChatMessage`, `ChatTypingIndicator`, `ChatWindowCsatPrompt`, and `ChatWindowCsatThankYou`.
- `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 `ChatBody` and does not touch the parent-controlled `messages` array.
