Renders an assistant's streamed text, splitting fenced code into Chat Code Blocks and setting inline backtick code in mono, with a slot for a cursor at the end.
"use client"
import * as React from "react"
import { cn } from "cn"
import { ChatCodeBlock } from "@/components/chat/chat-code-block"
// Code is the one surface in a response: a fenced block gets a real darker
// surface, pinned in both modes. The prose around it stays bare.
type Block =
| { kind: "text"; body: string }
| { kind: "code"; body: string; lang?: string; open: boolean }
// Fences are whole lines, as in CommonMark: backticks inside the code never
// close it. A fence still open at the end is a block mid-stream.
const OPENING = /^ {0,3}(`{3,})([^`]*)$/
const CLOSING = /^ {0,3}(`{3,})[ \t\r]*$/
function split(source: string): Block[] {
const out: Block[] = []
let text: string[] = []
let code: { fence: number; lang?: string; body: string[] } | null = null
const flushText = () => {
if (text.length) out.push({ kind: "text", body: text.join("\n") })
text = []
}
for (const line of source.split("\n")) {
if (code) {
const close = CLOSING.exec(line)
if (close && close[1].length >= code.fence) {
out.push({
kind: "code",
body: code.body.join("\n"),
lang: code.lang,
open: false,
})
code = null
} else {
code.body.push(line)
}
continue
}
const open = OPENING.exec(line)
if (open) {
flushText()
const lang = open[2].trim().split(/\s+/)[0]
code = { fence: open[1].length, lang: lang || undefined, body: [] }
} else {
text.push(line)
}
}
if (code)
out.push({
kind: "code",
body: code.body.join("\n"),
lang: code.lang,
open: true,
})
else flushText()
return out.filter((b) => b.body.trim().length > 0)
}
function TextBlock({
body,
className,
index,
children,
}: {
body: string
className?: string
index: number
children?: React.ReactNode
}) {
return (
<p className={cn("whitespace-pre-wrap", className)}>
{inline(body, index)}
{children}
</p>
)
}
// Inline `code` gets mono without a pill — a fill would be a container.
function inline(body: string, key: number) {
const parts = body.split(/(`[^`\n]+`)/g)
return (
<React.Fragment key={key}>
{parts.map((part, i) =>
part.startsWith("`") && part.endsWith("`") && part.length > 2 ? (
<code key={i} className="font-mono text-[0.9em] text-foreground">
{part.slice(1, -1)}
</code>
) : (
<React.Fragment key={i}>{part}</React.Fragment>
)
)}
</React.Fragment>
)
}
/**
* An assistant reply rendered from its text as it streams. Fenced code
* blocks become highlighted code panels; the prose around them stays plain,
* with inline `code` set in mono.
*/
function ChatResponse({
text,
className,
children,
}: {
/** The reply so far. A code fence still open at the end shows as a block mid-stream. */
text: string
/**
* Extra classes for each paragraph of prose. Code panels and the wrapping
* `div` do not receive them.
*/
className?: string
/** Placed at the end of the last block, such as a streaming cursor. */
children?: React.ReactNode
}) {
const blocks = split(text)
if (blocks.length === 0) {
return (
<p
data-slot="chat-response"
className={cn("whitespace-pre-wrap", className)}
>
{children}
</p>
)
}
return (
<div data-slot="chat-response" className="flex flex-col gap-4">
{blocks.map((block, i) => {
const last = i === blocks.length - 1
return block.kind === "code" ? (
<React.Fragment key={i}>
<ChatCodeBlock
code={block.body}
lang={block.lang}
streaming={block.open}
/>
{last ? children : null}
</React.Fragment>
) : (
<TextBlock
key={i}
index={i}
className={className}
body={block.body.replace(/^\n+|\n+$/g, "")}
>
{last ? children : null}
</TextBlock>
)
})}
</div>
)
}
export { ChatResponse }
pnpm dlx shadcn add @01orbital/chat-responsenpx shadcn add @01orbital/chat-responseyarn shadcn add @01orbital/chat-responsebunx --bun shadcn add @01orbital/chat-responseFree under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.
It lands at components/chat/chat-response.tsx, as source you own and edit.
import { ChatResponse } from "@/components/chat/chat-response"
const TEXT = `Install the block with the CLI:
\`\`\`bash
npx shadcn add @01orbital/three-plan-pricing-01
\`\`\`
It lands with its \`Button\` and \`Card\` dependencies resolved. Fenced code gets the pinned-dark surface; inline code stays mono with no pill.`
export default function ChatResponseDemo() {
return (
<div className="max-w-xl">
<ChatResponse className="text-sm/7 text-foreground" text={TEXT} />
</div>
)
}Prop
Type
npm: cn. The CLI installs them for you.
Registry pieces it installs with it:
font-geist-monoChat Message
A single chat turn: the user's message in a muted bubble aligned to the end, the assistant's as bare text at the start. Built on Message and Bubble.
Suggest Field
Textarea with inline completion: a ghost suggestion follows the text, Tab accepts and Esc dismisses, and a Suggest button turns into Accept beside Dismiss. The caller supplies the suggestion.