AI
ResponseAI
Renders an assistant answer from markdown, with a caret and soft word fade while it streams and citations from the sources it used.
npx prodular-ui add responseStreaming
Pass the text received so far and streaming. Half written tables, bold and links render cleanly.
"use client"
import * as React from "react"import { RotateCcwIcon } from "lucide-react"
import { Button } from "@/components/ui/button"import { Response } from "@/components/ui/response"import type { SourceData } from "@/components/ui/sources"
const ANSWER = `Days sales outstanding rose from **38 to 46 days** in Q3. Most of the change comes from two enterprise accounts that moved to net 60 terms in July [1].
| Customer | Terms | Open balance | Days late || --- | --- | ---: | ---: || Halcyon Health | Net 60 | $412,800 | 18 || Brightline Freight | Net 60 | $268,350 | 11 || Orbis Retail | Net 30 | $96,120 | 4 |
Excluding those two accounts, DSO is **39 days**, in line with Q2. The collections policy still assumes net 30 for every invoice, so reminders go out a month early [2].
- Update the reminder schedule for accounts on net 60.- Ask Sales to flag term changes in the deal desk form.`
const SOURCES: SourceData[] = [ { title: "Halcyon Health order form, July 2026", url: "https://app.ledgerline.com/contracts/halcyon-2026", siteName: "Ledgerline Contracts", snippet: "Payment terms: net 60 from invoice date, effective July 1, 2026.", }, { title: "Collections policy v3", url: "https://www.notion.so/ledgerline/collections-policy", siteName: "Notion", snippet: "First reminder 7 days after due date. Due date is calculated as invoice date plus 30 days.", },]
const TOKENS = ANSWER.match(/\S+\s*/g) ?? []
export default function ResponseStreaming() { const [count, setCount] = React.useState(0) const [run, setRun] = React.useState(0)
React.useEffect(() => { let n = 0 const id = setInterval(() => { n = Math.min(TOKENS.length, n + 2) setCount(n) if (n >= TOKENS.length) clearInterval(id) }, 50) return () => clearInterval(id) }, [run])
const streaming = count < TOKENS.length
return ( <div className="flex w-full max-w-2xl flex-col gap-4"> <Response streaming={streaming} sources={SOURCES} className="min-h-80"> {TOKENS.slice(0, count).join("")} </Response> <Button variant="outline" size="sm" className="self-start" onClick={() => { setCount(0) setRun((r) => r + 1) }} > <RotateCcwIcon data-icon="inline-start" /> Stream again </Button> </div> )}Markdown
Headings, lists, quotes, links, inline code and code blocks, tuned for reading at 15px.
Close checklist for October
Accruals are posted for 14 of 16 vendors. The two open ones are below, followed by the journal entry to book once the invoices arrive.
Still open
- Northbeam Logistics: invoice expected on October 28, estimate $38,200.
- Kestrel Analytics: usage true up, estimate $6,450. See the usage report.
Accrue at the estimate if the invoice is not in by day 3 of close, then reverse in November.
csv
date,account,debit,credit,memo
2026-10-31,6120 Freight,38200,,Northbeam October accrual
2026-10-31,2110 Accrued liabilities,,38200,Northbeam October accrualRun close status --period 2026-10 to see what is left.
import { Response } from "@/components/ui/response"
const MARKDOWN = `## Close checklist for October
Accruals are posted for **14 of 16** vendors. The two open ones are below, followed by the journal entry to book once the invoices arrive.
### Still open
1. **Northbeam Logistics**: invoice expected on October 28, estimate $38,200.2. **Kestrel Analytics**: usage true up, estimate $6,450. See the [usage report](https://app.ledgerline.com/reports/kestrel-usage).
> Accrue at the estimate if the invoice is not in by day 3 of close, then reverse in November.
\`\`\`csvdate,account,debit,credit,memo2026-10-31,6120 Freight,38200,,Northbeam October accrual2026-10-31,2110 Accrued liabilities,,38200,Northbeam October accrual\`\`\`
Run \`close status --period 2026-10\` to see what is left.`
export default function ResponseMarkdown() { return ( <div className="w-full max-w-2xl"> <Response>{MARKDOWN}</Response> </div> )}Anatomy
<Message from="assistant"> <MessageContent> <Response streaming={status === "streaming"} sources={sources}> {text} </Response> </MessageContent></Message>Guidance
Use it when
- For every assistant answer, streamed or loaded from history.
- Inside Message from="assistant", after Reasoning and tool calls.
Avoid
- Wrapping it in a card or bubble. Assistant text sits on the page so it reads like a document.
- Long tables in chat. Over eight rows, link to a report instead.
- Rendering user messages with it. Users write plain text, show it in a Bubble.
Rules for AI agents
Published in llms-full.txt and the registry, so coding agents follow them when they build with this component.
- Pass the markdown as children and set streaming only while tokens are still arriving. Set it to false the moment the stream ends, so the caret disappears.
- Never fake streaming for retrieved or cached content. Render history and saved answers with streaming={false}.
- Pass the sources the model cited as sources. [1] style markers in the text become citation chips in that order. Do not invent sources.
- Do not repair or buffer partial markdown yourself. Response closes open code fences, bold and links and holds back half written table rows.
- Render Reasoning and tool calls before Response, never after it.
- Do not add your own text size or prose classes. The type scale is set for reading.
API
Response
| Prop | Type | Default | Description |
|---|---|---|---|
| children | string | Markdown with GitHub flavored tables, task lists and strikethrough. | |
| streaming | boolean | false | Shows the caret and repairs incomplete markdown. |
| animate | boolean | streaming | Fades new words in. Respects reduced motion. |
| sources | SourceData[] | Turns [n] markers into citation chips with a preview card. |