You said, 4:32 PM
Which invoices from September are overdue? Put them in a table and suggest what to do next.
AI Kit
Chat Message
v1.0.0Added in 1.0.0
A React AI chat message: the question in a bubble, the answer with its thinking, tool calls, markdown, sources and actions.
Your question sits in a bubble on the right, the answer reads on the left with a small mark. While it streams, three dots wait for the first word, the thinking row shimmers, tools spin inline and a caret follows the text; the actions appear when it finishes. Under the answer: Copy (a check confirms it), thumbs up or down (the pressed one fills and pops, the other steps aside and "Thanks" shows for a moment), Regenerate, the "‹ 2 / 3 ›" version switcher and the "⋯" menu. On the latest answer the actions always show; elsewhere they fade in on hover or focus, and on a phone a tap on the message shows them and its time (one message at a time) while a long press opens an action sheet with the same actions. Edit turns your bubble into a text box: Cmd or Ctrl + Enter sends, Escape cancels and puts focus back on Edit. Images in attachments open a full-screen viewer with swipe, files and sources open in a new tab, long questions fold behind "Show more", and a failed answer shows its error with Retry. Every action is a button in the Tab order with its own name; thumbs are toggles.
Chat Message is a Pro React component in the React AI components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add chat-message, together with Markdown, Reasoning Indicator, Tool Call Timeline, Action Sheet, Avatar and 17 more and the npm packages motion and lucide-react.
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add chat-messageChat Message is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Import ChatMessage from @/components/ui/chat-message.
import { ChatMessage, type ChatMessageData, type ChatMessageLabels } from "@/components/ui/chat-message"
const labels: Partial<ChatMessageLabels> = {
assistant: "Summit Assistant",
assistantWrote: (name) => `${name} replied`,
regenerate: "Try again",
good: "Helpful",
bad: "Not helpful",
feedbackThanks: "Thanks for the feedback",
}
export function Answer({ message, onRegenerate }: { message: ChatMessageData; onRegenerate: () => Promise<void> }) {
return (
<ChatMessage
{...message}
isLatest
labels={labels}
onRegenerate={onRegenerate}
onFeedbackChange={(feedback) => console.log(message.id, feedback)}
/>
)
}roledefault "assistant""user" | "assistant" | "system"Who wrote it. user: the person's bubble with Copy and Edit. assistant: the answer with its thinking, tools, sources and response actions. system: a quiet centered line between hairlines, without avatar or actions.
contentdefault ""stringThe message as markdown (tables, lists, code). Empty on a finished assistant message renders only the footer; empty while pending or streaming shows the typing dots. The playground falls back to the canonical invoice answer.
statusdefault "complete""pending" | "streaming" | "complete" | "stopped" | "error"pending: sent, nothing back yet (typing dots). streaming: text arriving (caret, aria-busy, actions hidden). stopped: a Stopped note in the meta line, actions still work. error: the error row with Retry.
modeldefault ""stringThe model that wrote the answer, shown in the meta line (a mono chip in the flat header).
errordefault ""ReactNodeThe message of the error row when status is "error"; empty shows labels.error.
editeddefault falsebooleanAdds "edited" to the meta line.
variantdefault "default""default" | "bubbles" | "flat"default: the person in a bubble on the right, the answer as text on the left (ChatGPT). bubbles: both sides in bubbles (a messenger, a support widget). flat: both on the left with avatars and name headers, no bubbles (team threads, docs).
sizedefault "md""sm" | "md"Text 14 / 15px, bubble padding and avatars 24 / 28px. sm fits copilot side panels.
bubbledefault "soft""soft" | "solid"The person's bubble: a light tint of the tone, or a solid fill (black / white when neutral).
tonedefault "neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The color of the person's bubble and the edit Send button. Neutral is gray (soft) or black / white (solid).
colordefault ""stringAny CSS color for the bubble; replaces the tone. Text on a solid fill turns black or white by lightness.
showAvatardefault "assistant""assistant" | "all" | "none"Which side shows an avatar. The flat variant always shows both unless "none".
assistantAvatarReactNodeReplaces the assistant's round Sparkles mark: a MatrixOrb, a logo, an image.
assistantNamedefault ""stringThe assistant's name in the flat header and the screen reader heading; falls back to author.name, then labels.assistant.
timestampdefault "hover""hover" | "always" | "never"The time (and model) in the meta line: shown on hover, focus or a tap, always, or never. The flat header always shows the time.
timeFormatdefault "time""relative" | "time" | "date" | "datetime""16:35", or relative ("5 min. ago", ticking once a minute), a date, or both.
actionsdefault "auto""auto" | "always" | "hover" | "never"auto: always visible on the latest assistant message, on hover, focus or a tap elsewhere. always / hover force one rule; never hides the action bar (the long-press sheet still works).
isLatestdefault falsebooleanThe last assistant message of a thread (chat-thread sets it). Drives actions="auto".
groupeddefault falsebooleanA follow-up from the same sender: the avatar and header hide (the text stays aligned) and the bubble tightens its corner on the sender's side.
copyFormatdefault "markdown""markdown" | "text"What Copy puts on the clipboard: the markdown source, or plain text through markdownToText.
caretdefault "dot""dot" | "bar" | "block" | "none"The caret at the end of streaming text.
codeAccentdefault ""stringThe accent of code blocks in the answer and the tool inputs and outputs; empty keeps the code block's own.
maxUserLinesdefault 8numberLong questions fold past this many lines behind "Show more"; 0 never folds.
longPressdefault truebooleanOn touch screens a long press on the message opens an action sheet with every action (and a haptic tick). Links, code and the buttons keep their own touch behavior.
localedefault ""stringTimes, durations and file sizes. Empty uses UIProvider's locale, then the browser's.
createdAtDate | string | numberWhen it was sent; the meta line, the flat header and the screen reader heading show it.
authorChatPerson{ id, name, avatar? }: the person (their avatar and name) or the assistant (its name and picture).
attachmentsChatAttachment[]Images in a grid that opens the full-screen viewer (one large, two to four as tiles, more as +N), files as tiles with their type color and size.
reasoningChatReasoning{ content, status?, startedAt?, endedAt?, durationMs? }: the thinking row above the answer (ReasoningIndicator).
toolCallsToolCall[]The tools it used, as an inline summary that expands into the timeline (ToolCallTimeline variant inline).
sourcesChatSource[]Chips under the answer with favicon, domain and number; they open their url in a new tab.
idstringThe message id, set on the root; keeps the tap-to-show state per message.
branch{ index: number; count: number }Which version this is (1-based) of how many: shows the "‹ 2 / 3 ›" switcher when count is above 1, and the text cross-fades on every switch.
onBranchChange(index: number) => voidThe switcher's previous / next, with the new 1-based index. Without it the arrows are disabled.
onCopy(text: string) => voidAfter copying (Copy is built in).
onRegenerate() => (void | Promise<unknown>)Shows Regenerate on assistant messages; a returned promise keeps it spinning.
onEdit(content: string) => (void | Promise<unknown>)Shows Edit on user messages; Send calls it with the new text (unchanged text just closes the editor). A promise keeps Send loading and the editor open until it resolves.
feedback"up" | "down" | nullControlled thumbs; without onFeedbackChange they are read-only (in the bar and the long-press sheet). The thumbs show when any of feedback, defaultFeedback or onFeedbackChange is set.
defaultFeedbackdefault """up" | "down" | nullThe starting thumbs while uncontrolled; unset (null) shows neither pressed.
onFeedbackChange(feedback: "up" | "down" | null) => (void | Promise<unknown>)Pressing the pressed thumb clears it. Optimistic: a rejected promise rolls the choice back.
onRetry() => (void | Promise<unknown>)Shows Retry in the error row; a promise keeps it loading.
menuMenuEntry[]The "⋯" menu (Share, Read aloud, Report, Delete): a dropdown on desktop, a bottom sheet on phones; its items also join the long-press sheet.
onSourceClick(source: ChatSource) => voidReplaces opening the source url.
onAttachmentClick(attachment: ChatAttachment) => voidReplaces the default: images open the viewer, files open their url.
nowDate | numberPins the clock for relative times and live durations (screenshots, tests). Default: a shared ticker.
markdownPropsPartial<MarkdownProps>Passed to the Markdown of the content (labels, linkTarget, components). Keep it stable.
reasoningPropsPartial<ReasoningIndicatorProps>Passed to the thinking row (labels, variant, peek). Keep it stable.
toolCallPropsPartial<ToolCallTimelineProps>Passed to the inline tool timeline (labels, onApprove, onReject). Keep it stable.
labelsPartial<ChatMessageLabels>Every visible and spoken string: you, assistant, youWrote, personWrote(name), assistantWrote(name), system, copy, copied, regenerate, edit, save, cancel, good, bad, feedbackThanks, more, retry, error, stopped, edited, sources, attachments, previous, next, branch(index, count), branchLabel(index, count), showMore, showLess, thinking, attachment(name, size), openImage(name), moreImages(count). English by default (see Parts).
classNamesPartial<Record<"avatar" | "header" | "attachments" | "bubble" | "reasoning" | "tools" | "content" | "typing" | "error" | "sources" | "footer" | "actions" | "action" | "meta" | "editor", string>>Extra classes for the inner parts.
The action bar alone (copy, edit, thumbs, regenerate, versions, menu), for custom message layouts. Props: ChatMessageActionsProps.
The image grid with its viewer and the file tiles. Props: ChatMessageAttachmentsProps.
The rail of source chips. Props: ChatMessageSourcesProps.
Every English string of the message, its action bar, sources, attachments and versions, to spread under a translation.
The three typing dots of a pending reply, with sr-only text (chat-thread's thinking row uses it). Props: ChatMessageTypingProps.
A reply from the first token: typing dots, the thinking row, two tools, then text with a caret.
A support widget: variant="bubbles", size sm and a solid brand bubble for the customer.
Team thread with avatars, name headers, a system line and an orb as the assistant's avatar.
An inline error with Retry that succeeds on the second try, and an answer stopped halfway.
Four photos in a grid that opens the viewer, a PDF tile, and three source chips.
Edit a question in place; each version keeps its own answer behind the ‹ › switcher.
At 390px: tap to show actions, long press for the action sheet, the menu as a bottom sheet.
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
Run npx wingo-ui@latest add chat-message. It also adds the Wingo UI items it builds on (Markdown, Reasoning Indicator, Tool Call Timeline, Action Sheet, Avatar and 17 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/chat-message.
On the latest answer the actions always show; elsewhere they fade in on hover or focus, and on a phone a tap on the message shows them and its time (one message at a time) while a long press opens an action sheet with the same actions.
Building UI with AI agents10 min read
Building UI with AI agents10 min read
Hover the answer (or tap it on a phone) for its actions; isLatest keeps them on.