Can you make a markdown of this note pleas
Template
Chat
Controlled chat shell that composes rendered turns and a provider-owned composer.
Composition
Rendered chat shell
- <ChatBlock>
- <ChatLayout>
- <ChatThread>
- children prop
- rendered turns
- composer prop
- <ChatComposer />
- children prop
- <ChatThread>
- <ChatLayout>
Installation
First 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 type { ActivityState } from "@/components/control-ui/activity";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 { ChatTurn } from "@/components/control-ui/chat-layout";import { ChatMessage, ChatMessageBody, ChatMessageContent, ChatMessageRow } from "@/components/control-ui/chat-message";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 format="code">{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, state }) => ( <Activity kind="reasoning" state={state === "streaming" ? "running" : "success"}> <ActivityTrigger> <ActivityIcon /> <ActivityTitle>Reasoning</ActivityTitle> </ActivityTrigger> <ActivityContent>{reasoning}</ActivityContent> </Activity> )} 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
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 composer={composer}>{children}</ChatThread> </ChatLayout> );}