ChatMessage Component
Renders a single message in the chat: markdown text, images, file attachments, tool calls, sources, and the approval / question cards of a paused turn.
New in 0.4.0
Stop notices and warnings under an answer, cards for paused turns, image thumbnails, the built-in button row (toolbar), an editor for your own messages, and an AI label.
Import
import { ChatMessage } from "@cognipeer/chat-ui";Usage
<ChatMessage
message={{
id: "msg-1",
conversationId: "c-1",
role: "assistant",
content: "Hello! How can I help you?",
createdAt: new Date(),
updatedAt: new Date(),
}}
/>2
3
4
5
6
7
8
9
10
Props
| Prop | Type | Default | Description |
|---|---|---|---|
message | Message | required | The message to render |
isStreaming | boolean | false | Whether this message is streaming |
streamingText | string | - | Current streaming text (used while isStreaming) |
showAvatar | boolean | true | Show the avatar |
showTimestamp | boolean | false | Show the time |
enableCitations | boolean | true | Sources and inline markers under assistant messages |
actions | ChatActionsConfig | - | How approval / question cards behave under this message. Without onResolve they are read-only; see Approvals and Questions |
renderActions | (props: MessageActionProps) => ReactNode | - | Custom action buttons (shown on hover) |
renderAvatar | (role) => ReactNode | - | Custom avatar |
renderImage | (props: ImageRenderProps) => ReactNode | - | Draw an image yourself, e.g. fetch it with credentials and show a blob URL |
renderStopNotice | (props: StopNoticeRenderProps) => ReactNode | - | Replace the notice under an answer that stopped early |
resolveFileUrl | (file, { disposition? }) => Promise<string | undefined> | - | How to get a URL for a stored file that has none of its own (useChat returns one, ChatTransport.getFileUrl) |
toolbar | MessageToolbarConfig | - | The built-in row under the message: copy, onRegenerate, onEdit, feedback, persistent. ChatMessageList builds it from Chat's messageActions and feedback props |
onSaveEdit | (content: string) => Promise<boolean> | boolean | void | - | Receives the new text of one of the person's messages once toolbar.onEdit opened the editor. Resolve false to keep the editor open |
aiLabel | string | - | Labels an assistant message with this text ("AI-generated") |
className | string | - | Additional class names |
Message content
content is a string, or an array of parts:
content: [
{ type: "text", text: "Here is the chart" },
{ type: "image_url", image_url: { url: "https://cdn.example.com/chart.png" } },
]2
3
4
- Text parts are joined, one per line, and rendered as markdown.
- Image parts (
image_url,input_image,image) are drawn as pictures below the text, lazily loaded, each opening full size in a new tab. Only a safe URL is drawn:https:,blob:, relative or same-origin URLs,http:when the page is itself served over http, and base64 PNG / JPEG / GIF / WebP.javascript:URLs, SVG data URLs and other schemes are dropped. - Other parts are ignored.
Attachments
message.files shows under the text:
- an image with a usable
url(or a localpreviewUrl, for a file just sent from this page) as a thumbnail with its name and size; it opens full size in a new tab; - an image without a URL of its own (a conversation loaded from the server) is fetched through
resolveFileUrland then shown as a thumbnail; - any other file, or an image without a usable URL, as a chip with its name, extension and size, and a Download link when
urlis a safe link (http(s),blob:or relative) or, withresolveFileUrl, a Download button that fetches it; - a file whose
expiresAthas passed, or that is markedexpired, is a muted, struck-through chip marked Expired, without a download; a file whoseconversionStatusisfailed/skippedis tinted "No readable text"; a file with a deadline in the future says "Kept until ..." on hover.
Why an answer ended
An assistant message that was stopped (stopReason cancelled, limit, max_turns, budget_reached or error, or status cancelled / incomplete) shows a notice under its text (MessageStopNotice, a role="status" block) saying so, and whether the text above is a partial answer. A plain answer, and a pause, show none. metadata.warnings are listed under the answer (MessageWarnings). See Running turns, Stop and reattach.
A turn that only paused for a tool of the embedding page (client_tool) and has no text, files, tool rows or stop reason renders nothing: the host page answers it unseen.
Status and actions
A paused assistant message (status: "requires_action", pendingActions) shows an approval or question card per action; how a settled card looks comes from message.actionStates, which the app fills as cards settle and which a server may send for a reloaded history. See Approvals and Questions.
Examples
With Tool Calls
Tool calls of a finished message are read from metadata.toolCallDetails:
<ChatMessage
message={{
...message,
metadata: {
toolCallDetails: [{ id: "call-1", name: "get_weather", args: { location: "New York" }, result: { temp: 22 } }],
toolCallDurationSeconds: 2,
},
}}
/>2
3
4
5
6
7
8
9
Streaming
<ChatMessage message={message} isStreaming streamingText="I'm currently writing..." />With an Image
<ChatMessage
message={{
...message,
role: "user",
content: [
{ type: "text", text: "Analyze this image" },
{ type: "image_url", image_url: { url: "data:image/png;base64,..." } },
],
}}
/>2
3
4
5
6
7
8
9
10
Images behind authentication
An <img> cannot send an Authorization header, so draw the image yourself:
<ChatMessage
message={message}
renderImage={({ src, alt }) => <AuthenticatedImage src={src} alt={alt} />}
/>2
3
4
With Actions
<ChatMessage
message={message}
renderActions={() => (
<div className="flex gap-2">
<button>Like</button>
<button>Copy</button>
</div>
)}
/>2
3
4
5
6
7
8
9
Styling
Colours come from the theme variables; markdown is styled by the .chat-markdown rules in styles.css. See Theming.
Markdown Support
The component renders markdown content (GitHub-flavoured): bold and italic text, inline code, code blocks, lists, tables and links (links open in a new tab).

