transcript ui copy button placement (#712)

Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
This commit is contained in:
Igor Monadical
2025-10-24 16:52:02 -04:00
committed by GitHub
parent 962c40e2b6
commit 0baff7abf7
6 changed files with 67 additions and 56 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import React from "react"; import React from "react";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import "../../../styles/markdown.css"; import "../../../styles/markdown.css";
@@ -16,17 +16,15 @@ import {
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { LuPen } from "react-icons/lu"; import { LuPen } from "react-icons/lu";
import { useError } from "../../../(errors)/errorContext"; import { useError } from "../../../(errors)/errorContext";
import ShareAndPrivacy from "../shareAndPrivacy";
type FinalSummaryProps = { type FinalSummaryProps = {
transcriptResponse: GetTranscript; transcript: GetTranscript;
topicsResponse: GetTranscriptTopic[]; topics: GetTranscriptTopic[];
onUpdate?: (newSummary) => void; onUpdate: (newSummary: string) => void;
finalSummaryRef: React.Dispatch<React.SetStateAction<HTMLDivElement | null>>;
}; };
export default function FinalSummary(props: FinalSummaryProps) { export default function FinalSummary(props: FinalSummaryProps) {
const finalSummaryRef = useRef<HTMLParagraphElement>(null);
const [isEditMode, setIsEditMode] = useState(false); const [isEditMode, setIsEditMode] = useState(false);
const [preEditSummary, setPreEditSummary] = useState(""); const [preEditSummary, setPreEditSummary] = useState("");
const [editedSummary, setEditedSummary] = useState(""); const [editedSummary, setEditedSummary] = useState("");
@@ -35,10 +33,10 @@ export default function FinalSummary(props: FinalSummaryProps) {
const updateTranscriptMutation = useTranscriptUpdate(); const updateTranscriptMutation = useTranscriptUpdate();
useEffect(() => { useEffect(() => {
setEditedSummary(props.transcriptResponse?.long_summary || ""); setEditedSummary(props.transcript?.long_summary || "");
}, [props.transcriptResponse?.long_summary]); }, [props.transcript?.long_summary]);
if (!props.topicsResponse || !props.transcriptResponse) { if (!props.topics || !props.transcript) {
return null; return null;
} }
@@ -54,9 +52,7 @@ export default function FinalSummary(props: FinalSummaryProps) {
long_summary: newSummary, long_summary: newSummary,
}, },
}); });
if (props.onUpdate) { props.onUpdate(newSummary);
props.onUpdate(newSummary);
}
console.log("Updated long summary:", updatedTranscript); console.log("Updated long summary:", updatedTranscript);
} catch (err) { } catch (err) {
console.error("Failed to update long summary:", err); console.error("Failed to update long summary:", err);
@@ -75,7 +71,7 @@ export default function FinalSummary(props: FinalSummaryProps) {
}; };
const onSaveClick = () => { const onSaveClick = () => {
updateSummary(editedSummary, props.transcriptResponse.id); updateSummary(editedSummary, props.transcript.id);
setIsEditMode(false); setIsEditMode(false);
}; };
@@ -133,11 +129,6 @@ export default function FinalSummary(props: FinalSummaryProps) {
> >
<LuPen /> <LuPen />
</IconButton> </IconButton>
<ShareAndPrivacy
finalSummaryRef={finalSummaryRef}
transcriptResponse={props.transcriptResponse}
topicsResponse={props.topicsResponse}
/>
</> </>
)} )}
</Flex> </Flex>
@@ -153,7 +144,7 @@ export default function FinalSummary(props: FinalSummaryProps) {
mt={2} mt={2}
/> />
) : ( ) : (
<div ref={finalSummaryRef} className="markdown"> <div ref={props.finalSummaryRef} className="markdown">
<Markdown>{editedSummary}</Markdown> <Markdown>{editedSummary}</Markdown>
</div> </div>
)} )}

View File

@@ -41,6 +41,8 @@ export default function TranscriptDetails(details: TranscriptDetails) {
waiting || mp3.audioDeleted === true, waiting || mp3.audioDeleted === true,
); );
const useActiveTopic = useState<Topic | null>(null); const useActiveTopic = useState<Topic | null>(null);
const [finalSummaryElement, setFinalSummaryElement] =
useState<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {
if (waiting) { if (waiting) {
@@ -124,9 +126,12 @@ export default function TranscriptDetails(details: TranscriptDetails) {
<TranscriptTitle <TranscriptTitle
title={transcript.data?.title || "Unnamed Transcript"} title={transcript.data?.title || "Unnamed Transcript"}
transcriptId={transcriptId} transcriptId={transcriptId}
onUpdate={(newTitle) => { onUpdate={() => {
transcript.refetch().then(() => {}); transcript.refetch().then(() => {});
}} }}
transcript={transcript.data || null}
topics={topics.topics}
finalSummaryElement={finalSummaryElement}
/> />
</Flex> </Flex>
{mp3.audioDeleted && ( {mp3.audioDeleted && (
@@ -148,11 +153,12 @@ export default function TranscriptDetails(details: TranscriptDetails) {
{transcript.data && topics.topics ? ( {transcript.data && topics.topics ? (
<> <>
<FinalSummary <FinalSummary
transcriptResponse={transcript.data} transcript={transcript.data}
topicsResponse={topics.topics} topics={topics.topics}
onUpdate={() => { onUpdate={() => {
transcript.refetch(); transcript.refetch().then(() => {});
}} }}
finalSummaryRef={setFinalSummaryElement}
/> />
</> </>
) : ( ) : (

View File

@@ -26,9 +26,9 @@ import { useAuth } from "../../lib/AuthProvider";
import { featureEnabled } from "../../lib/features"; import { featureEnabled } from "../../lib/features";
type ShareAndPrivacyProps = { type ShareAndPrivacyProps = {
finalSummaryRef: any; finalSummaryElement: HTMLDivElement | null;
transcriptResponse: GetTranscript; transcript: GetTranscript;
topicsResponse: GetTranscriptTopic[]; topics: GetTranscriptTopic[];
}; };
type ShareOption = { value: ShareMode; label: string }; type ShareOption = { value: ShareMode; label: string };
@@ -48,7 +48,7 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
const [isOwner, setIsOwner] = useState(false); const [isOwner, setIsOwner] = useState(false);
const [shareMode, setShareMode] = useState<ShareOption>( const [shareMode, setShareMode] = useState<ShareOption>(
shareOptionsData.find( shareOptionsData.find(
(option) => option.value === props.transcriptResponse.share_mode, (option) => option.value === props.transcript.share_mode,
) || shareOptionsData[0], ) || shareOptionsData[0],
); );
const [shareLoading, setShareLoading] = useState(false); const [shareLoading, setShareLoading] = useState(false);
@@ -70,7 +70,7 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
try { try {
const updatedTranscript = await updateTranscriptMutation.mutateAsync({ const updatedTranscript = await updateTranscriptMutation.mutateAsync({
params: { params: {
path: { transcript_id: props.transcriptResponse.id }, path: { transcript_id: props.transcript.id },
}, },
body: requestBody, body: requestBody,
}); });
@@ -90,8 +90,8 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
const userId = auth.status === "authenticated" ? auth.user?.id : null; const userId = auth.status === "authenticated" ? auth.user?.id : null;
useEffect(() => { useEffect(() => {
setIsOwner(!!(requireLogin && userId === props.transcriptResponse.user_id)); setIsOwner(!!(requireLogin && userId === props.transcript.user_id));
}, [userId, props.transcriptResponse.user_id]); }, [userId, props.transcript.user_id]);
return ( return (
<> <>
@@ -171,19 +171,19 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
<Flex gap={2} mb={2}> <Flex gap={2} mb={2}>
{requireLogin && ( {requireLogin && (
<ShareZulip <ShareZulip
transcriptResponse={props.transcriptResponse} transcript={props.transcript}
topicsResponse={props.topicsResponse} topics={props.topics}
disabled={toShareMode(shareMode.value) === "private"} disabled={toShareMode(shareMode.value) === "private"}
/> />
)} )}
<ShareCopy <ShareCopy
finalSummaryRef={props.finalSummaryRef} finalSummaryElement={props.finalSummaryElement}
transcriptResponse={props.transcriptResponse} transcript={props.transcript}
topicsResponse={props.topicsResponse} topics={props.topics}
/> />
</Flex> </Flex>
<ShareLink transcriptId={props.transcriptResponse.id} /> <ShareLink transcriptId={props.transcript.id} />
</Dialog.Body> </Dialog.Body>
</Dialog.Content> </Dialog.Content>
</Dialog.Positioner> </Dialog.Positioner>

View File

@@ -5,34 +5,35 @@ type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"];
import { Button, BoxProps, Box } from "@chakra-ui/react"; import { Button, BoxProps, Box } from "@chakra-ui/react";
type ShareCopyProps = { type ShareCopyProps = {
finalSummaryRef: any; finalSummaryElement: HTMLDivElement | null;
transcriptResponse: GetTranscript; transcript: GetTranscript;
topicsResponse: GetTranscriptTopic[]; topics: GetTranscriptTopic[];
}; };
export default function ShareCopy({ export default function ShareCopy({
finalSummaryRef, finalSummaryElement,
transcriptResponse, transcript,
topicsResponse, topics,
...boxProps ...boxProps
}: ShareCopyProps & BoxProps) { }: ShareCopyProps & BoxProps) {
const [isCopiedSummary, setIsCopiedSummary] = useState(false); const [isCopiedSummary, setIsCopiedSummary] = useState(false);
const [isCopiedTranscript, setIsCopiedTranscript] = useState(false); const [isCopiedTranscript, setIsCopiedTranscript] = useState(false);
const onCopySummaryClick = () => { const onCopySummaryClick = () => {
let text_to_copy = finalSummaryRef.current?.innerText; const text_to_copy = finalSummaryElement?.innerText;
text_to_copy && if (text_to_copy) {
navigator.clipboard.writeText(text_to_copy).then(() => { navigator.clipboard.writeText(text_to_copy).then(() => {
setIsCopiedSummary(true); setIsCopiedSummary(true);
// Reset the copied state after 2 seconds // Reset the copied state after 2 seconds
setTimeout(() => setIsCopiedSummary(false), 2000); setTimeout(() => setIsCopiedSummary(false), 2000);
}); });
}
}; };
const onCopyTranscriptClick = () => { const onCopyTranscriptClick = () => {
let text_to_copy = let text_to_copy =
topicsResponse topics
?.map((topic) => topic.transcript) ?.map((topic) => topic.transcript)
.join("\n\n") .join("\n\n")
.replace(/ +/g, " ") .replace(/ +/g, " ")

View File

@@ -26,8 +26,8 @@ import {
import { featureEnabled } from "../../lib/features"; import { featureEnabled } from "../../lib/features";
type ShareZulipProps = { type ShareZulipProps = {
transcriptResponse: GetTranscript; transcript: GetTranscript;
topicsResponse: GetTranscriptTopic[]; topics: GetTranscriptTopic[];
disabled: boolean; disabled: boolean;
}; };
@@ -88,14 +88,14 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
}, [stream, streams]); }, [stream, streams]);
const handleSendToZulip = async () => { const handleSendToZulip = async () => {
if (!props.transcriptResponse) return; if (!props.transcript) return;
if (stream && topic) { if (stream && topic) {
try { try {
await postToZulipMutation.mutateAsync({ await postToZulipMutation.mutateAsync({
params: { params: {
path: { path: {
transcript_id: props.transcriptResponse.id, transcript_id: props.transcript.id,
}, },
query: { query: {
stream, stream,

View File

@@ -2,14 +2,22 @@ import { useState } from "react";
import type { components } from "../../reflector-api"; import type { components } from "../../reflector-api";
type UpdateTranscript = components["schemas"]["UpdateTranscript"]; type UpdateTranscript = components["schemas"]["UpdateTranscript"];
type GetTranscript = components["schemas"]["GetTranscript"];
type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"];
import { useTranscriptUpdate } from "../../lib/apiHooks"; import { useTranscriptUpdate } from "../../lib/apiHooks";
import { Heading, IconButton, Input, Flex, Spacer } from "@chakra-ui/react"; import { Heading, IconButton, Input, Flex, Spacer } from "@chakra-ui/react";
import { LuPen } from "react-icons/lu"; import { LuPen } from "react-icons/lu";
import ShareAndPrivacy from "./shareAndPrivacy";
type TranscriptTitle = { type TranscriptTitle = {
title: string; title: string;
transcriptId: string; transcriptId: string;
onUpdate?: (newTitle: string) => void; onUpdate: (newTitle: string) => void;
// share props
transcript: GetTranscript | null;
topics: GetTranscriptTopic[] | null;
finalSummaryElement: HTMLDivElement | null;
}; };
const TranscriptTitle = (props: TranscriptTitle) => { const TranscriptTitle = (props: TranscriptTitle) => {
@@ -29,9 +37,7 @@ const TranscriptTitle = (props: TranscriptTitle) => {
}, },
body: requestBody, body: requestBody,
}); });
if (props.onUpdate) { props.onUpdate(newTitle);
props.onUpdate(newTitle);
}
console.log("Updated transcript title:", newTitle); console.log("Updated transcript title:", newTitle);
} catch (err) { } catch (err) {
console.error("Failed to update transcript:", err); console.error("Failed to update transcript:", err);
@@ -62,11 +68,11 @@ const TranscriptTitle = (props: TranscriptTitle) => {
} }
setIsEditing(false); setIsEditing(false);
}; };
const handleChange = (e) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setDisplayedTitle(e.target.value); setDisplayedTitle(e.target.value);
}; };
const handleKeyDown = (e) => { const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") { if (e.key === "Enter") {
updateTitle(displayedTitle, props.transcriptId); updateTitle(displayedTitle, props.transcriptId);
setIsEditing(false); setIsEditing(false);
@@ -111,6 +117,13 @@ const TranscriptTitle = (props: TranscriptTitle) => {
> >
<LuPen /> <LuPen />
</IconButton> </IconButton>
{props.transcript && props.topics && (
<ShareAndPrivacy
finalSummaryElement={props.finalSummaryElement}
transcript={props.transcript}
topics={props.topics}
/>
)}
</Flex> </Flex>
)} )}
</> </>