diff --git a/www/app/(app)/transcripts/buildTranscriptWithTopics.ts b/www/app/(app)/transcripts/buildTranscriptWithTopics.ts new file mode 100644 index 00000000..71553d31 --- /dev/null +++ b/www/app/(app)/transcripts/buildTranscriptWithTopics.ts @@ -0,0 +1,60 @@ +import type { components } from "../../reflector-api"; +import { formatTime } from "../../lib/time"; + +type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"]; +type Participant = components["schemas"]["Participant"]; + +function getSpeakerName( + speakerNumber: number, + participants?: Participant[] | null, +): string { + const name = participants?.find((p) => p.speaker === speakerNumber)?.name; + return name && name.trim().length > 0 ? name : `Speaker ${speakerNumber}`; +} + +export function buildTranscriptWithTopics( + topics: GetTranscriptTopic[], + participants?: Participant[] | null, + transcriptTitle?: string | null, +): string { + const blocks: string[] = []; + + if (transcriptTitle && transcriptTitle.trim()) { + blocks.push(`# ${transcriptTitle.trim()}`); + blocks.push(""); + } + + for (const topic of topics) { + // Topic header + const topicTime = formatTime(Math.floor(topic.timestamp || 0)); + const title = topic.title?.trim() || "Untitled Topic"; + blocks.push(`## ${title} [${topicTime}]`); + + if (topic.segments && topic.segments.length > 0) { + for (const seg of topic.segments) { + const ts = formatTime(Math.floor(seg.start || 0)); + const speaker = getSpeakerName(seg.speaker as number, participants); + const text = (seg.text || "").replace(/\s+/g, " ").trim(); + if (text) { + blocks.push(`[${ts}] ${speaker}: ${text}`); + } + } + } else if (topic.transcript) { + // Fallback: plain transcript when segments are not present + const text = topic.transcript.replace(/\s+/g, " ").trim(); + if (text) { + blocks.push(text); + } + } + + // Blank line between topics + blocks.push(""); + } + + // Trim trailing blank line + while (blocks.length > 0 && blocks[blocks.length - 1] === "") { + blocks.pop(); + } + + return blocks.join("\n"); +} diff --git a/www/app/(app)/transcripts/shareCopy.tsx b/www/app/(app)/transcripts/shareCopy.tsx index fb1b5f68..bdbff5f4 100644 --- a/www/app/(app)/transcripts/shareCopy.tsx +++ b/www/app/(app)/transcripts/shareCopy.tsx @@ -3,6 +3,8 @@ import type { components } from "../../reflector-api"; type GetTranscript = components["schemas"]["GetTranscript"]; type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"]; import { Button, BoxProps, Box } from "@chakra-ui/react"; +import { buildTranscriptWithTopics } from "./buildTranscriptWithTopics"; +import { useTranscriptParticipants } from "../../lib/apiHooks"; type ShareCopyProps = { finalSummaryElement: HTMLDivElement | null; @@ -18,6 +20,7 @@ export default function ShareCopy({ }: ShareCopyProps & BoxProps) { const [isCopiedSummary, setIsCopiedSummary] = useState(false); const [isCopiedTranscript, setIsCopiedTranscript] = useState(false); + const participantsQuery = useTranscriptParticipants(transcript?.id || null); const onCopySummaryClick = () => { const text_to_copy = finalSummaryElement?.innerText; @@ -32,12 +35,12 @@ export default function ShareCopy({ }; const onCopyTranscriptClick = () => { - let text_to_copy = - topics - ?.map((topic) => topic.transcript) - .join("\n\n") - .replace(/ +/g, " ") - .trim() || ""; + const text_to_copy = + buildTranscriptWithTopics( + topics || [], + participantsQuery?.data || null, + transcript?.title || null, + ) || ""; text_to_copy && navigator.clipboard.writeText(text_to_copy).then(() => { diff --git a/www/app/(app)/transcripts/transcriptTitle.tsx b/www/app/(app)/transcripts/transcriptTitle.tsx index 1ac32b02..49a22c71 100644 --- a/www/app/(app)/transcripts/transcriptTitle.tsx +++ b/www/app/(app)/transcripts/transcriptTitle.tsx @@ -4,10 +4,15 @@ import type { components } from "../../reflector-api"; type UpdateTranscript = components["schemas"]["UpdateTranscript"]; type GetTranscript = components["schemas"]["GetTranscript"]; type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"]; -import { useTranscriptUpdate } from "../../lib/apiHooks"; +import { + useTranscriptUpdate, + useTranscriptParticipants, +} from "../../lib/apiHooks"; import { Heading, IconButton, Input, Flex, Spacer } from "@chakra-ui/react"; -import { LuPen } from "react-icons/lu"; +import { LuPen, LuCopy, LuCheck } from "react-icons/lu"; import ShareAndPrivacy from "./shareAndPrivacy"; +import { buildTranscriptWithTopics } from "./buildTranscriptWithTopics"; +import { toaster } from "../../components/ui/toaster"; type TranscriptTitle = { title: string; @@ -25,6 +30,9 @@ const TranscriptTitle = (props: TranscriptTitle) => { const [preEditTitle, setPreEditTitle] = useState(props.title); const [isEditing, setIsEditing] = useState(false); const updateTranscriptMutation = useTranscriptUpdate(); + const participantsQuery = useTranscriptParticipants( + props.transcript?.id || null, + ); const updateTitle = async (newTitle: string, transcriptId: string) => { try { @@ -118,11 +126,57 @@ const TranscriptTitle = (props: TranscriptTitle) => { {props.transcript && props.topics && ( - + <> + { + const text = buildTranscriptWithTopics( + props.topics || [], + participantsQuery?.data || null, + props.transcript?.title || null, + ); + if (!text) return; + navigator.clipboard + .writeText(text) + .then(() => { + toaster + .create({ + placement: "top", + duration: 2500, + render: () => ( +
+
+ Transcript copied +
+
+ ), + }) + .then(() => {}); + }) + .catch(() => {}); + }} + > + +
+ + )} )}