62 / Agent UI
Response
An assistant's answer as markdown, including while it is still half-written and briefly invalid.
The renewal is automatic unless notice is given.
- Notice window: 60 days
- Governing law:
England and Wales
const renews = !noticeGiven(contract, { days: 60 })
Ask again if you want the clause quoted in full.
"use client"
import * as React from "react"
import { Response } from "mischief-ui/response"
const answer = `The renewal is **automatic** unless notice is given.
- Notice window: 60 days
- Governing law: \`England and Wales\`
\`\`\`ts
const renews = !noticeGiven(contract, { days: 60 })
\`\`\`
Ask again if you want the clause quoted in full.`
export function ResponseDemo() {
const [shown, setShown] = React.useState(answer.length)
const [streaming, setStreaming] = React.useState(false)
React.useEffect(() => {
if (!streaming) return
const timer = setInterval(() => {
setShown((n) => {
if (n >= answer.length) {
setStreaming(false)
return n
}
return n + 3
})
}, 24)
return () => clearInterval(timer)
}, [streaming])
return (
<div className="w-full max-w-md space-y-3">
<Response streaming={streaming}>{answer.slice(0, shown)}</Response>
<button
type="button"
className="border-border hover:bg-muted focus-visible:ring-ring inline-flex min-h-9 items-center rounded-full border px-3 text-xs font-semibold focus-visible:ring-2 focus-visible:outline-none"
onClick={() => {
setShown(0)
setStreaming(true)
}}
>
Stream it again
</button>
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/responseimport { Response } from "mischief-ui/response"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import Markdown from "react-markdown"import remarkGfm from "remark-gfm" import { cn } from "@/lib/utils" export type ResponseProps = Omit< React.HTMLAttributes<HTMLDivElement>, "children"> & { children: string /** Still arriving. Half-written markdown is closed off rather than shown raw. */Usage
export function Answer({ text, streaming }) {
return <Response streaming={streaming}>{text}</Response>
}Markdown that is not finished yet
Text arriving a token at a time is markdown that is invalid for most of its life: a fence opened three lines ago and not yet closed, a bold marker with nothing after it. Rendered as it stands, the reply fills with stray asterisks and a code block that swallows everything after it.
While streaming is set, the unterminated markers are closed for the length of that render and reopened by the next token. The reader sees a code block that grows rather than a page that breaks and repairs itself.
Code inside an answer
A fenced block becomes a panel and inline code stays inline, because a panel around three words in the middle of a sentence is a worse answer than the sentence. The panel is deliberately plain: a component should be one thing you install, so this one does not drag a code viewer in behind it. Pass renderCode and it will use whatever you already have.
API
childrenstringThe markdown, whole or partial.streamingbooleanCloses unterminated markers for this render. Off by default.renderCode(code: string, language?: string) => ReactNodeRenders a fenced block. Pass Code Block here for the copy control; the default is a plain panel so this installs alone.Accessibility
The answer is ordinary prose in the accessibility tree: headings are headings, lists are lists, and code blocks are code. It is not itself a live region, and should not be put inside one -- a reply announced token by token as it grows is unusable, and the thing worth announcing is that it started and that it finished.