ChatBody

PreviousNext

The scrollable message log of a chat panel — messages, typing indicator, CSAT prompt, and price quotes.

Hi. Ask me anything about Club Med. I'm here to help you find the right information quickly.

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:

  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).
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

PropTypeDefaultDescription
messagesChatMessage[]—Required. Ordered list of messages to display
greetingstringundefinedOptional static greeting text shown above the message list
isTypingbooleanfalseWhen true, shows the animated typing indicator
onMessageReplay(message: ChatMessage) => voidundefinedRenders the replay action on the user messages
onMessageEdit(message: ChatMessage, content: string) => voidundefinedEnables the in-place edition of the user text messages, called with the new text
onMessageCopy(message: ChatMessage) => void | Promise<void>undefinedRenders the copy action on the user messages. A check confirms it once it succeeded
messageActionLabelsPartial<ChatMessageLabels>EnglishLabels of the actions and of the editor
onFeedbackRate(rating: number) => voidundefinedCalled with the star rating (1–5) when the user submits feedback
csatLabelsChatWindowCsatPromptLabelsundefinedLabels for the rating prompt. Required to enable the CSAT flow
csatRenderRatingLabel(rating: number) => ReactNodeundefinedRenders the caption shown under the active star. Required with CSAT
csatThankYouLabelsChatWindowCsatThankYouLabelsundefinedLabels for the thank-you note shown after submission
onQuoteCtaClick(quote: ChatWindowQuoteItem) => voidundefinedCalled with the selected quote when a "See prices" CTA is tapped
classNamestringundefinedAdditional CSS classes on the message-log element

Inherits all remaining HTMLDivElement attributes.

ChatHandle

Imperative handle exposed via ref. Use forwardRef or useRef<ChatHandle>.

MethodSignatureDescription
askForFeedback() => voidDisplays the CSAT star-rating prompt in the message log
showQuotes(quotes: ChatWindowQuoteItem[]) => voidAppends 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 — messages and isTyping are managed by the parent.
  • Renders with role="log" and aria-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 ChatBody and does not touch the parent-controlled messages array.