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(() => {});
+ }}
+ >
+
+
+
+ >
)}
)}