Skip to main content

Message Composition Primitives

The custom renderer lets you take over how a message is drawn entirely. But most of the time what you want to change is the content, not the layout — full-width bot text, a right-aligned user bubble, the placement of quick replies and the message-action row should all stay as they are.

So the SDK exports the primitives its default rows are built from, ready to compose.

Message Composition Primitives

To customize a message row's content without re-implementing the SDK's layout (full-width bot text, right-aligned user bubble, timestamp format and theme colors), compose the exported primitives: TemplateBox, TemplateBoxContent, BotMessageText, UserMessageText and Time. Use them from renderMessageContent and fall back with renderDefaultContent() for rows you don't handle.

import {
  TemplateBox, TemplateBoxContent,
  BotMessageText, UserMessageText, Time,
} from '@asgard-js/react';

<Chatbot
  renderMessageContent={({ message, renderDefaultContent }) => {
    if (message.type === 'user') {
      return (
        <TemplateBox type="user" direction="horizontal">
          <UserMessageText>{/* your content */}</UserMessageText>
          <Time time={message.time} />
        </TemplateBox>
      );
    }
    return renderDefaultContent();
  }}
/>

Both modes should look identical in layout — the only difference is that the composed mode turns the leading mention into a chip. That is the point of these primitives: change the content, keep the layout.

Loading chatbot...

The Primitives

PrimitiveRole
TemplateBoxThe row frame — alignment and direction (type: 'user' | 'bot')
TemplateBoxContentThe bot row's content container; handles quick replies and message actions
BotMessageTextThe bot text body, with Markdown rendering and theme colors
UserMessageTextThe user text bubble
TimeDeprecated — renders nothing (see below)

Usage

Use them from renderMessageContent, and fall back with renderDefaultContent() for rows you don't handle:

import {
TemplateBox,
TemplateBoxContent,
BotMessageText,
UserMessageText,
} from "@asgard-js/react";
import { MessageTemplateType } from "@asgard-js/core";

<Chatbot
renderMessageContent={({ message, renderDefaultContent }) => {
// User row: turn the leading mention into a chip, keep everything else
if (message.type === "user" && message.text.startsWith(MENTION)) {
return (
<TemplateBox type="user" direction="horizontal">
<UserMessageText>
<span className="mention-chip">{MENTION}</span>
{message.text.slice(MENTION.length)}
</UserMessageText>
</TemplateBox>
);
}

// Bot text row
if (message.type === "bot") {
const template = message.message?.template;
const isText = template?.type === MessageTemplateType.TEXT || !template?.type;
const text = message.isTyping ? message.typingText : message.message?.text;

if (isText && text) {
return (
<TemplateBox type="bot" direction="horizontal">
<TemplateBoxContent
message={message}
quickReplies={template?.quickReplies}
references={template?.references}
>
<BotMessageText>{text}</BotMessageText>
</TemplateBoxContent>
</TemplateBox>
);
}
}

return renderDefaultContent();
}}
/>

Text While Streaming

When message.isTyping is true, read message.typingText rather than message.message.text — the latter is only populated once the message completes. The example above already handles this.

Why Not Just Hand-roll It

Hand-rolled rows drift from these, and keep drifting every time the SDK changes:

  • Bot text is full-width with no bubble; only user rows are bubbles — different layout rules
  • TemplateBoxContent also positions quick replies and the message-action row
  • Alignment, direction and theme colors all hang off TemplateBox rather than being scattered

In short: change the content, keep the layout.

Time No Longer Renders Anything

The chat surface no longer shows message timestamps anywhere. <Time /> is kept as a no-op and every TimeProps field is ignored, purely so existing callers keep compiling; delete your <Time /> when convenient.

The value it used to render was never the message's own time: the SDK stamped new Date() as each frame arrived, and a GET rejoin replays the whole history at once, so every replayed message was re-stamped with the moment the page opened. The backend carries no per-message timestamp to put in its place.

The time field on ConversationMessage is deprecated for the same reason — nothing in the SDK renders it.

ChatHeader

The same idea applies to the heading bar. ChatHeader is exported for use with renderHeader; to swap only the title area while keeping the rest of the structure, use renderTitle. See Custom Header.

See Also