Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion www/app/(app)/transcripts/[transcriptId]/finalSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import {
Textarea,
Spacer,
} from "@chakra-ui/react";
import { LuPen } from "react-icons/lu";
import { LuPen, LuCopy, LuCheck } from "react-icons/lu";
import { buildTranscriptWithTopics } from "../buildTranscriptWithTopics";
import { useTranscriptParticipants } from "../../../lib/apiHooks";
import { toaster } from "../../../components/ui/toaster";
import { useError } from "../../../(errors)/errorContext";
import ShareAndPrivacy from "../shareAndPrivacy";

Expand All @@ -33,6 +36,9 @@ export default function FinalSummary(props: FinalSummaryProps) {

const { setError } = useError();
const updateTranscriptMutation = useTranscriptUpdate();
const participantsQuery = useTranscriptParticipants(
props.transcriptResponse?.id || null,
);

useEffect(() => {
setEditedSummary(props.transcriptResponse?.long_summary || "");
Expand Down Expand Up @@ -125,6 +131,50 @@ export default function FinalSummary(props: FinalSummaryProps) {
{!isEditMode && (
<>
<Spacer />
<IconButton
aria-label="Copy Transcript"
size="sm"
variant="subtle"
onClick={() => {
const text = buildTranscriptWithTopics(
props.topicsResponse || [],
participantsQuery?.data || null,
props.transcriptResponse?.title || null,
);
if (!text) return;
navigator.clipboard
Comment on lines +139 to +145
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Check if participantsQuery is still loading before using its data. The current implementation might cause issues if the query hasn't completed when the copy function is triggered. [possible issue, importance: 8]

Suggested change
const text = buildTranscriptWithTopics(
props.topicsResponse || [],
participantsQuery?.data || null,
props.transcriptResponse?.title || null,
);
if (!text) return;
navigator.clipboard
if (participantsQuery.isLoading) {
toaster.create({
placement: "top",
duration: 2500,
render: () => (
<div className="chakra-ui-light">
<div style={{
background: "#ED8936",
color: "white",
padding: "8px 12px",
borderRadius: 6,
display: "flex",
alignItems: "center",
gap: 8,
boxShadow: "rgba(0,0,0,0.25) 0px 4px 12px",
}}>
Please wait for participant data to load
</div>
</div>
),
});
return;
}
const text = buildTranscriptWithTopics(
props.topicsResponse || [],
participantsQuery?.data || null,
props.transcriptResponse?.title || null,
);
if (!text) return;

.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(() => {});
Comment on lines +145 to +173
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add proper error handling in the clipboard operation's catch block to inform the user when copying fails, rather than silently ignoring errors. [general, importance: 7]

Suggested change
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(() => {});
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((error) => {
toaster.create({
placement: "top",
duration: 2500,
render: () => (
<div className="chakra-ui-light">
<div
style={{
background: "#E53E3E",
color: "white",
padding: "8px 12px",
borderRadius: 6,
display: "flex",
alignItems: "center",
gap: 8,
boxShadow: "rgba(0,0,0,0.25) 0px 4px 12px",
}}
>
Failed to copy transcript
</div>
</div>
),
});
});

}}
>
<LuCopy />
</IconButton>
<IconButton
aria-label="Edit Summary"
onClick={onEditClick}
Expand Down
60 changes: 60 additions & 0 deletions www/app/(app)/transcripts/buildTranscriptWithTopics.ts
Original file line number Diff line number Diff line change
@@ -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}`;
}
Comment on lines +7 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add a type check for speakerNumber before using it to find a participant. If speakerNumber is undefined or null, the function might return incorrect results. [general, importance: 6]

Suggested change
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}`;
}
function getSpeakerName(
speakerNumber: number,
participants?: Participant[] | null,
): string {
if (speakerNumber === undefined || speakerNumber === null) {
return "Unknown Speaker";
}
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");
}
17 changes: 11 additions & 6 deletions www/app/(app)/transcripts/shareCopy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
finalSummaryRef: any;
Expand All @@ -18,6 +20,9 @@ export default function ShareCopy({
}: ShareCopyProps & BoxProps) {
const [isCopiedSummary, setIsCopiedSummary] = useState(false);
const [isCopiedTranscript, setIsCopiedTranscript] = useState(false);
const participantsQuery = useTranscriptParticipants(
transcriptResponse?.id || null,
);

const onCopySummaryClick = () => {
let text_to_copy = finalSummaryRef.current?.innerText;
Expand All @@ -31,12 +36,12 @@ export default function ShareCopy({
};

const onCopyTranscriptClick = () => {
let text_to_copy =
topicsResponse
?.map((topic) => topic.transcript)
.join("\n\n")
.replace(/ +/g, " ")
.trim() || "";
const text_to_copy =
buildTranscriptWithTopics(
topicsResponse || [],
participantsQuery?.data || null,
transcriptResponse?.title || null,
) || "";

text_to_copy &&
navigator.clipboard.writeText(text_to_copy).then(() => {
Expand Down
Loading