63 / Agent UI
Transcript Viewer
A recording as text, where every line is also the way back to the moment it was said.
"use client"
import * as React from "react"
import { ScrubBar } from "mischief-ui/scrub-bar"
import { TranscriptViewer } from "mischief-ui/transcript-viewer"
const cues = [
{ id: "1", start: 0, speaker: "Ana", text: "Right, the renewal window." },
{
id: "2",
start: 6,
speaker: "Sam",
text: "Sixty days before the term ends.",
},
{ id: "3", start: 14, speaker: "Ana", text: "And if nobody sends notice?" },
{ id: "4", start: 21, speaker: "Sam", text: "It renews for another year." },
{
id: "5",
start: 29,
speaker: "Ana",
text: "That is the bit to flag, then.",
},
{
id: "6",
start: 36,
speaker: "Sam",
text: "Agreed. I will pull the clause.",
},
]
export function TranscriptViewerDemo() {
const [time, setTime] = React.useState(14)
return (
<div className="w-full max-w-md space-y-3">
<TranscriptViewer
cues={cues}
time={time}
onSeek={(cue) => setTime(cue.start)}
/>
<ScrubBar duration={44} value={time} onValueChange={setTime} />
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/transcript-viewerimport { TranscriptViewer } from "mischief-ui/transcript-viewer"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 { cn } from "@/lib/utils" export type TranscriptCue = { id: string /** Seconds from the start of the recording. */ start: number end?: number speaker?: string text: string}Usage
export function Recording() {
return (
<TranscriptViewer
cues={cues}
time={time}
onSeek={(cue) => player.seek(cue.start)}
/>
)
}Reading and scrubbing are one act
Give it the position and it marks the line being spoken; click a line and it hands you the second to seek to. Neither is a separate mode, which is the whole point: people look for a sentence they remember, not for a timestamp they never knew.
A cue with no end is treated as running until the next one begins, so a transcript from a service that only reports start times needs no preparation.
Following without hijacking
While it follows, the active line is scrolled into view by the nearest amount that puts it on screen, never centred. Centring on every cue drags the page under a reader who was looking at something else a moment ago.
API
cuesTranscriptCue[]The transcript, in order.timenumberWhere the recording is now, in seconds.onSeek(cue: TranscriptCue) => voidA line was chosen.followbooleanScrolls to the active line. On by default.labelstringNames the list. Defaults to "Transcript".TranscriptCue
idstringDistinct within the transcript.startnumberSeconds from the beginning.endnumberOptional. Defaults to the next cue's start.speakerstringShown before the line.textstringWhat was said.Accessibility
Every line is a real button, so the transcript is operable from the keyboard without any arrow-key handling of its own, and the line being spoken carries aria-current rather than only a background colour. Times are rendered in minutes and seconds instead of as a raw number of seconds, which is what a screen reader would otherwise read aloud.