Can you make a markdown of this note pleas
Template
Chat
Controlled chat shell that composes rendered turns and a provider-owned composer.
Composition
How this template nests its exported parts and installed Control UI sources.
Controlled shell
The block owns layout only. Usage examples render native provider messages at the application boundary.
ChatBlock├── ChatLayout│ └── ChatThread│ └── children (rendered turns)└── composerFirst install and activate one skin. Core deliberately contains no visual token defaults.
npx shadcn@latest add https://control-ui.dev/r/chat-block.jsonUsage
"use client";import { MastraReactProvider, MessageFactory, type MessageRoleRendererProps, type MessageRoleRenderers, type ToolInvocationPart, useChat,} from "@mastra/react";import type { ReactNode } from "react";import { Activity, ActivityContent, ActivityDetail, ActivityDetailContent, ActivityDetailLabel, ActivityIcon, ActivityStatus, ActivityTitle, ActivityTrigger,} from "@/components/control-ui/activity";import { ChatBlock } from "@/components/control-ui/blocks/chat";import { ChatComposer, ChatComposerShell, ChatComposerSubmit, ChatComposerTextarea, ChatComposerToolbar, ChatComposerTools,} from "@/components/control-ui/chat-composer";import { ChatThought, ChatTurn } from "@/components/control-ui/chat-layout";import { ChatMessage, ChatMessageBody, ChatMessageContent, ChatMessageRow } from "@/components/control-ui/chat-message";import type { ActivityState } from "@/components/control-ui/contracts";import { SourceBadge } from "@/components/control-ui/source-badge";import { Button } from "@/components/control-ui/ui/button";function activityState(state: string): ActivityState { if (state === "result" || state === "output-available") return "success"; if (state === "output-error" || state === "output-denied") return "error"; if (state === "partial-call" || state === "input-streaming") return "running"; return "pending";}function renderTurn(from: "user" | "assistant", children: ReactNode) { return ( <ChatTurn from={from}> <ChatMessage from={from}> <ChatMessageRow> <ChatMessageBody> <ChatMessageContent>{children}</ChatMessageContent> </ChatMessageBody> </ChatMessageRow> </ChatMessage> </ChatTurn> );}function renderToolInvocation(part: ToolInvocationPart) { const invocation = part.toolInvocation; const state = activityState(invocation.state); return ( <Activity kind="tool" name={invocation.toolName} state={state}> <ActivityTrigger> <ActivityIcon /> <ActivityTitle /> <ActivityStatus className="sr-only" /> </ActivityTrigger> <ActivityContent> <ActivityDetail> <ActivityDetailLabel>Input</ActivityDetailLabel> <ActivityDetailContent className="font-mono text-caption"> {JSON.stringify(invocation.rawInput ?? invocation.args, null, 2)} </ActivityDetailContent> </ActivityDetail> {invocation.result !== undefined || invocation.errorText ? ( <ActivityDetail> <ActivityDetailLabel>Output</ActivityDetailLabel> <ActivityDetailContent className={state === "error" ? "text-destructive-text" : undefined}> {invocation.errorText ?? JSON.stringify(invocation.result, null, 2)} </ActivityDetailContent> </ActivityDetail> ) : null} </ActivityContent> </Activity> );}const messageRoles = { User: ({ children }: MessageRoleRendererProps) => renderTurn("user", children), Assistant: ({ children }: MessageRoleRendererProps) => renderTurn("assistant", children), System: () => null, Signal: ({ children }: MessageRoleRendererProps) => renderTurn("assistant", children),} satisfies MessageRoleRenderers;function MastraChat({ agentId }: { agentId: string }) { const { cancelRun, isRunning, messages, sendMessage } = useChat({ agentId }); const composer = ( <ChatComposer state={isRunning ? "submitting" : "idle"} onSubmit={async ({ value, clear }) => { await sendMessage({ message: value }); clear(); }} > <ChatComposerShell> <ChatComposerTextarea placeholder="Ask the agent..." /> <ChatComposerToolbar> <ChatComposerTools>Mastra agent</ChatComposerTools> {isRunning ? ( <Button type="button" size="xs" variant="quiet" onClick={cancelRun}> Stop </Button> ) : ( <ChatComposerSubmit>Send</ChatComposerSubmit> )} </ChatComposerToolbar> </ChatComposerShell> </ChatComposer> ); return ( <ChatBlock composer={composer}> {messages.map((message) => ( <MessageFactory key={message.id} message={message} roles={messageRoles} Text={({ text }) => <span>{text}</span>} Reasoning={({ reasoning }) => <ChatThought details={reasoning}>Reasoning</ChatThought>} ToolInvocation={renderToolInvocation} SourceUrl={({ title, url }) => <SourceBadge href={url}>{title}</SourceBadge>} SourceDocument={({ title }) => <span>{title}</span>} fallback={(part) => <span>Unsupported message part: {part.type}</span>} /> ))} </ChatBlock> );}export function ChatSurface({ agentId, baseUrl }: { agentId: string; baseUrl: string }) { return ( <MastraReactProvider baseUrl={baseUrl}> <MastraChat agentId={agentId} /> </MastraReactProvider> );}Library dependencies
Public registry items keep their source on their own documentation page instead of duplicating it here.
Owned source
This block's recipe and private support files. Public dependencies stay linked above.
Block recipe
src/registry/blocks/chat.tsximport type { ComponentProps, ReactNode } from "react";import { ChatLayout, ChatThread } from "@/components/control-ui/chat-layout";export type ChatBlockProps = Omit<ComponentProps<typeof ChatLayout>, "children"> & { children: ReactNode; composer: ReactNode;};export function ChatBlock({ children, composer, className, ...props }: ChatBlockProps) { return ( <ChatLayout className={className} {...props}> <ChatThread>{children}</ChatThread> {composer} </ChatLayout> );}