fix: copy transcript (#674)

* Copy transcript

* Fix share copy transcript

* Move copy button above transcript
This commit is contained in:
2025-11-14 13:36:25 +01:00
committed by GitHub
parent 857e035562
commit a9a4f32324
3 changed files with 130 additions and 13 deletions

View File

@@ -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");
}

View File

@@ -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(() => {

View File

@@ -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) => {
<LuPen />
</IconButton>
{props.transcript && props.topics && (
<ShareAndPrivacy
finalSummaryElement={props.finalSummaryElement}
transcript={props.transcript}
topics={props.topics}
/>
<>
<IconButton
aria-label="Copy Transcript"
size="sm"
variant="subtle"
onClick={() => {
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: () => (
<div className="chakra-ui-light">
<div
style={{
background: "#38A169",
color: "white",
padding: "8px 12px",
borderRadius: 6,
display: "flex",
alignItems: "center",
gap: 8,
boxShadow: "rgba(0,0,0,0.25) 0px 4px 12px",
}}
>
<LuCheck /> Transcript copied
</div>
</div>
),
})
.then(() => {});
})
.catch(() => {});
}}
>
<LuCopy />
</IconButton>
<ShareAndPrivacy
finalSummaryElement={props.finalSummaryElement}
transcript={props.transcript}
topics={props.topics}
/>
</>
)}
</Flex>
)}