diff --git a/src/frontend/client/src/pages/knowledge/FilePreview/MediaPlayer.tsx b/src/frontend/client/src/pages/knowledge/FilePreview/MediaPlayer.tsx index 4a00a7188..4f9985da3 100644 --- a/src/frontend/client/src/pages/knowledge/FilePreview/MediaPlayer.tsx +++ b/src/frontend/client/src/pages/knowledge/FilePreview/MediaPlayer.tsx @@ -402,7 +402,7 @@ export function MediaPlayer({ kind, src, allowDownload = false, onDownload }: Me className={cn( "relative overflow-hidden", onDarkStage ? "bg-black" : "bg-fill-2", - fullscreen ? "flex h-full w-full flex-col justify-center" : "rounded-[6px]", + fullscreen ? "flex h-full w-full flex-col justify-center" : "rounded-[12px]", )} > {isVideo ? ( @@ -437,7 +437,8 @@ export function MediaPlayer({ kind, src, allowDownload = false, onDownload }: Me )}
= 0 ? rest.slice(0, nextHeading) : rest).trim(); } -function MarkdownBody({ content }: { content: string }) { +interface TranscriptCue { + /** Raw timestamp label, e.g. "00:00:03 - 00:00:05". */ + time: string; + text: string; +} + +/** A cue line opens with its timestamp in brackets: "[00:00:03 - 00:00:05] …". */ +const CUE_PATTERN = /^\[(\d{1,2}:\d{2}(?::\d{2})?(?:\s*-\s*\d{1,2}:\d{2}(?::\d{2})?)?)\]\s*/; + +/** + * Split a transcript into timestamp/text pairs so the time can be rendered as its + * own label instead of running inline with the sentence. Lines without a leading + * timestamp continue the cue above them; text before the first cue is returned as + * `preamble`. No cue found → the caller falls back to plain markdown. + */ +function parseTranscriptCues(markdown: string): { preamble: string; cues: TranscriptCue[] } { + const cues: TranscriptCue[] = []; + const preambleLines: string[] = []; + + for (const rawLine of markdown.split("\n")) { + const line = rawLine.trim(); + if (!line) continue; + const match = line.match(CUE_PATTERN); + if (match) { + cues.push({ time: match[1].replace(/\s*-\s*/, " - "), text: line.slice(match[0].length).trim() }); + } else if (cues.length) { + const last = cues[cues.length - 1]; + last.text = last.text ? `${last.text} ${line}` : line; + } else { + preambleLines.push(line); + } + } + + return { preamble: preambleLines.join("\n\n"), cues }; +} + +/* Transcript markdown matches the cue text size. `.prose` (vendored typography CSS) + hard-sets its own font-size from --markdown-font-size — the global chat font-size + preference — and is emitted after the utilities, so the override needs `!`. */ +const TRANSCRIPT_MARKDOWN_SIZE = "!text-body"; + +function TranscriptCueList({ cues }: { cues: TranscriptCue[] }) { return ( -
+
    + {cues.map((cue, index) => ( +
  1. +
    {cue.time}
    +

    {cue.text}

    +
  2. + ))} +
+ ); +} + +function MarkdownBody({ content, className }: { content: string; className?: string }) { + return ( +
-
+ {/* 12px inset: the segmented control's own 3px padding lines its label + up with the 16px-inset transcript text below. */} +
{/* Segmented control — mirrors the include/exclude tabs in Subscription/CreateChannel/FilterConditionEditor. */}
@@ -224,8 +282,16 @@ function MediaTranscriptTabs({ fileUrl }: { fileUrl: string }) { ) : error ? (
{error}
) : ( -
- + // pb clears the AI dock pinned at the bottom of the page. +
+ {cues.length ? ( + <> + {preamble ? : null} + + + ) : ( + + )}
)} @@ -263,7 +329,7 @@ export function RichKnowledgePreview({ {/* Side-by-side on md+: player left, transcript right, split by a single divider line (no card border/shadow). Stacked on narrow screens. */}
-
+