From 84e79507233356f94021da912bd0d06b37470da4 Mon Sep 17 00:00:00 2001 From: Kinyoo Date: Wed, 29 Jul 2026 09:44:10 +0800 Subject: [PATCH] style(knowledge): render transcript timestamps as their own labels Parse the transcript into timestamp/text cues so the time reads as a caption-sized label above each line instead of running inline with the sentence; lines without a leading timestamp continue the cue above, and text with no cue at all falls back to plain markdown. Also align the segmented control with the transcript inset, clear the AI dock with bottom padding, and round the player to 12px. Co-Authored-By: Claude Opus 4.8 --- .../knowledge/FilePreview/MediaPlayer.tsx | 5 +- .../FilePreview/RichKnowledgePreview.tsx | 78 +++++++++++++++++-- 2 files changed, 75 insertions(+), 8 deletions(-) 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. */}
-
+