44 / 122 · Knowledge
Research report
An outline that fills in section by section, each carrying the sources behind it.
Draft ownership in 0.140/4 sections · 9 sources read
What changed in 0.14
Who is affected
Migration path
Open questions
Installation
npx shadcn@latest add "@assistant-ui/elements-research-report"
Usage
import { ResearchReport } from "@/components/elements/research-report";
<ResearchReport title="Draft ownership in 0.14" sections={sections} sourcesRead={12} />Props
ResearchReport
title*stringWhat the report is about.
sections*ReportSection[]The outline. It exists from the start and fills in, so the shape is legible before the prose lands.
sourcesRead*numberRunning count of sources consumed.
classNamestringExtra classes merged onto the root.
ReportSection
id*stringStable identity for the row key.
heading*stringSection title, shown before any prose exists.
state*"pending" | "writing" | "done"Only one section is usually writing; pending sections stay visible but dim.
sources*numberHow many sources back this section. Hidden at zero.
previewstringFirst lines of the written section, shown once there is something to show.
Source
research-report.tsx"use client";
import { CheckIcon, Loader2Icon } from "lucide-react";
import { cn } from "@/lib/utils";
import { mono, paper } from "./surfaces";
export type SectionState = "pending" | "writing" | "done";
export interface ReportSection {
id: string;
heading: string;
state: SectionState;
sources: number;
preview?: string;
}
export function ResearchReport({
title,
sections,
sourcesRead,
className,
}: {
title: string;
sections: readonly ReportSection[];
sourcesRead: number;
className?: string;
}) {
const done = sections.filter((section) => section.state === "done").length;
return (
<div
className={cn(
paper,
"flex w-full max-w-sm flex-col gap-3 rounded-2xl p-4",
className,
)}
>
<div className="flex flex-col gap-1">
<span className="text-[13.5px] font-medium">{title}</span>
<span className={cn(mono, "text-foreground/30 tabular-nums")}>
{done}/{sections.length} sections · {sourcesRead} sources read
</span>
</div>
<div className="flex flex-col">
{sections.map((section) => (
<div
key={section.id}
className="border-foreground/[0.06] flex flex-col gap-1 border-t py-2 first:border-t-0 first:pt-0"
>
<div className="flex items-center gap-2">
<span className="flex size-3.5 shrink-0 items-center justify-center">
{section.state === "done" ? (
<CheckIcon className="text-foreground/35 size-3" />
) : section.state === "writing" ? (
<Loader2Icon className="size-3 animate-spin text-blue-500 motion-reduce:animate-none dark:text-blue-400" />
) : (
<span
aria-hidden
className="bg-foreground/15 size-1.5 rounded-full"
/>
)}
</span>
<span
className={cn(
"min-w-0 flex-1 truncate text-[13px]",
section.state === "pending"
? "text-foreground/35"
: "text-foreground/85",
)}
>
{section.heading}
</span>
{section.sources > 0 && (
<span className={cn(mono, "text-foreground/25 shrink-0")}>
{section.sources} src
</span>
)}
</div>
{section.preview && (
<p className="text-foreground/50 fade-in animate-in ps-5.5 text-xs leading-relaxed duration-300">
{section.preview}
</p>
)}
</div>
))}
</div>
</div>
);
}