Chat

Chat Response

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.

Free, MIT licenseOpen in Browse
"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 }

Installation

pnpm dlx shadcn add @01orbital/chat-response
npx shadcn add @01orbital/chat-response
yarn shadcn add @01orbital/chat-response
bunx --bun shadcn add @01orbital/chat-response

Free 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.

Usage

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>
  )
}

API reference

Prop

Type

Dependencies

npm: cn. The CLI installs them for you.

Registry pieces it installs with it: