Skip to content
Mizen UI
Docs

IntroductionInstallationCLIStylingFoundationsThemingCompositionIntegrationsAccessibilityFor agents

useCopyToClipboard

AccordionAspect RatioCardCollapsibleFrameGridGroupInlineScroll AreaSectionSeparatorSpacerStack

BlockquoteCitationCodeHeadingKbdLabelLinkTextVisually Hidden

ButtonButton GroupToggleToggle groupToolbar

CalendarCheckboxCheckbox groupComboboxDate pickerDate range pickerDate-Time InputFieldFieldsetFile UploadFormInputInput groupNumber fieldOTP FieldRadio groupSelectSliderSwitchTextareaTime Input

BreadcrumbMenubarNavigation MenuPaginationSegmented ControlStepperTabs

Alert dialogCommandContext MenuDialogDrawerMenuPopoverPreview CardSheetTooltip

AlertBadgeEmptyError StateMeterProgressSkeletonSpinnerStatus dotToast

Activity FeedAvatarAvatar GroupData TableListMetricOverflow ListProperty ListTableTokenTree

Area ChartBar ListCalendar HeatmapCategory ComparisonChart FrameChart LegendChart TooltipDistributionFunnelHeatmapMetric TrendPart-to-WholeProgress RingsRadar ChartRadial BarsSankey ChartScatter ChartScore GaugeTime Series

Diagram

AttachmentCarouselCode BlockLightboxMarkdownThumbnailTimestamp

Agent ThinkingApproval CardComposerConversationInline CitationMessageMessage ScrollerQuestion CardSourcesStreaming TextSuggestionTask ListTerminalTool Call

On this page

  • Preview
  • Usage
  • With the AI SDK
  • With TanStack AI
  • Examples
  • Group chat
  • Attachments
  • Not sent
  • Long text
  • Recommendations
  • Usage boundaries
  • API reference

Agentic

Message

One turn in a conversation. The person's message is a bubble with Copy, Edit, and the time; the agent's turn holds its thinking, tool calls, and reply.

idle

Usage

import { Message } from "@mizen-labs/ui";

With the AI SDK

Branch on the message role. Editing calls sendMessage with the message's id, which replaces it and runs the turn again. An error before any reply marks the last message as not sent, and Retry calls regenerate.

import { useChat } from "@ai-sdk/react";
import { Message, StreamingText } from "@mizen-labs/ui";

function textOf(message: { parts: { text?: string; type: string }[] }) {
	return message.parts
		.flatMap((part) => (part.type === "text" ? [part.text] : []))
		.join("");
}

export function Thread() {
	const { error, messages, regenerate, sendMessage, status } = useChat();
	return messages.map((message, index) => {
		const last = index === messages.length - 1;
		if (message.role === "user") {
			return (
				<Message
					from="user"
					key={message.id}
					// An error before any reply leaves the person's message last.
					onRetry={() => regenerate()}
					status={error && last ? "failed" : "sent"}
					// A messageId replaces that message and runs the turn again.
					onEdit={(text) => sendMessage({ messageId: message.id, text })}
				>
					{textOf(message)}
				</Message>
			);
		}
		return (
			<Message from="agent" key={message.id}>
				{/* AgentThinking and ToolCallGroup go here; see their pages. */}
				<StreamingText
					content={textOf(message)}
					status={last && status === "streaming" ? "streaming" : "complete"}
				/>
			</Message>
		);
	});
}

With TanStack AI

Text parts carry content instead of text. The docs do not show how to replace a sent message, so pass Edit to your own handler.

import { fetchServerSentEvents, useChat } from "@tanstack/ai-react";
import { Message, StreamingText } from "@mizen-labs/ui";

function textOf(message: { parts: { content?: string; type: string }[] }) {
	return message.parts
		.flatMap((part) => (part.type === "text" ? [part.content] : []))
		.join("");
}

export function Thread({ onEdit }: { onEdit: (id: string, text: string) => void }) {
	const { error, isLoading, messages, reload } = useChat({
		connection: fetchServerSentEvents("/api/chat"),
	});
	return messages.map((message, index) =>
		message.role === "user" ? (
			<Message
				from="user"
				key={message.id}
				onRetry={reload}
				status={error && index === messages.length - 1 ? "failed" : "sent"}
				// Your handler drops the later turns and sends the new text.
				onEdit={(text) => onEdit(message.id, text)}
			>
				{textOf(message)}
			</Message>
		) : (
			<Message from="agent" key={message.id}>
				<StreamingText
					content={textOf(message)}
					status={isLoading && index === messages.length - 1 ? "streaming" : "complete"}
				/>
			</Message>
		)
	);
}

Examples

Group chat

Others sit on the left with their name and picture; the reader's own message sits on the right.

Attachments

Files the person sent sit above their words. Images open in a lightbox; other files open in a new tab.

Not sent

A message that did not go out stays in place with Retry.

Long text

Pasted text folds after eight lines, with Show more.

Recommendations

GuidancePractice
Do

Use from="user" for people and from="agent" for the agent's turn, and put AgentThinking, ToolCallGroup, and StreamingText inside the agent's Message in the order they arrive. Add author and avatar only in group chats.

Usage boundaries

GuidanceBoundary
Scope

The application stores messages and runs turns. Message shows one turn; Edit only reports the new text.

Scope

Show time in the reader's time zone. Format it on the client or from their settings, and pass the instant as dateTime.

API reference

Public props from @mizen-labs/ui. Use exported prop types for the complete TypeScript contract.

PropTypeDefaultDescription
from"user" | "agent"—user is a person's message in a bubble; agent holds the agent's parts in a column without a bubble.
childrenstring / ReactNode—What the person wrote, for from="user". For from="agent", the parts in order, such as AgentThinking, ToolCallGroup, and StreamingText.
align"end" | "start""end"end for the person reading, start for others in a group chat.
author / avatarstring / ReactNode—The sender's name above the message, and their picture beside it. Use them in group chats.
attachmentsreadonly { name: string; preview?: string; href?: string }[]—Files the person sent, shown above the bubble as the same tiles Composer showed. Images open together in a lightbox, showing href when set; other files with an href open in a new tab.
time / dateTimestring—The send time formatted for the reader, such as 4:20 PM, and its machine-readable instant.
onEdit(text: string) => void—Shows Edit, which opens the text in place. Enter sends, Shift+Enter adds a line, and Escape cancels. Called only when the text changed.
status / onRetry"sent" | "failed" / () => void"sent"failed keeps the message with Not sent in view, and onRetry adds Retry beside it.
PreviousInline CitationNextMessage Scroller

How do I make Card respond to its container instead of the viewport?

4:20 PM
Thought for 4s

Give the card's wrapper container-type: inline-size, then replace its media queries with @container rules. The card now responds to the space it gets, not the viewport. developer.mozilla.org, CSS container queries, opens in a new tab Container queries work in every current browser.web.dev, Container queries land in stable browsers, opens in a new tab

MC
Maya Chen

Card breaks in the sidebar again. Can someone look before the release?

9:41 AM
AR
Alex Rivera

It uses media queries, so it only knows the viewport width.

9:43 AM

I'll move it to container queries today.

9:44 AM
  • CSVmonthly-report.csv, opens in a new tab

Summarize the trends in this report, and check that the charts use our palette.

2:05 PM

Can you also check the Card in the settings sidebar?

Not sent

Card overflows in the settings sidebar. Here is the build output: > mizen-docs@0.0.0 build > next build ▲ Next.js 16.0.0 Creating an optimized production build ... Compiled with warnings ./app/settings/sidebar.tsx Card: min-width 320px exceeds container (288px) at Card (packages/ui/src/components/card/card.tsx:42:7) at SettingsSidebar (app/settings/sidebar.tsx:18:5) Generating static pages (24/24) Finalizing page optimization ...

4:31 PM