From 6d44cdab9ff5f8818efc4349719760bc037636ba Mon Sep 17 00:00:00 2001 From: Sara Date: Mon, 11 Dec 2023 17:15:59 +0100 Subject: [PATCH] start participants, fix selection --- .../[transcriptId]/correct/page.tsx | 24 +-- .../correct/participantList.tsx | 117 +++++++++++ .../[transcriptId]/correct/topicWords.tsx | 198 ++++++++++-------- .../[domain]/transcripts/useParticipants.ts | 26 ++- .../[domain]/transcripts/useTopicWithWords.ts | 23 +- 5 files changed, 269 insertions(+), 119 deletions(-) create mode 100644 www/app/[domain]/transcripts/[transcriptId]/correct/participantList.tsx diff --git a/www/app/[domain]/transcripts/[transcriptId]/correct/page.tsx b/www/app/[domain]/transcripts/[transcriptId]/correct/page.tsx index c656b151..4277c18e 100644 --- a/www/app/[domain]/transcripts/[transcriptId]/correct/page.tsx +++ b/www/app/[domain]/transcripts/[transcriptId]/correct/page.tsx @@ -1,11 +1,13 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import useTranscript from "../../useTranscript"; import TopicHeader from "./topicHeader"; import TopicWords from "./topicWords"; import TopicPlayer from "./topicPlayer"; import getApi from "../../../../lib/getApi"; import useParticipants from "../../useParticipants"; +import useTopicWithWords from "../../useTopicWithWords"; +import ParticipantList from "./participantList"; type TranscriptCorrect = { params: { @@ -22,6 +24,8 @@ export default function TranscriptCorrect(details: TranscriptCorrect) { const transcriptId = details.params.transcriptId; const transcript = useTranscript(transcriptId); const [currentTopic, setCurrentTopic] = useState(""); + const topicWithWords = useTopicWithWords(currentTopic, transcriptId); + const [selectedTime, setSelectedTime] = useState(); const [topicTime, setTopicTime] = useState(); const api = getApi(); @@ -36,13 +40,7 @@ export default function TranscriptCorrect(details: TranscriptCorrect) { // -> remove time calculation and setting from TopicHeader // -> pass in topicTime to player directly // Should we have participants by default, one for each speaker ? - - const createParticipant = () => { - api?.v1TranscriptAddParticipant({ - createParticipant: { name: "Samantha" }, - transcriptId, - }); - }; + // Creating a participant and a speaker ? return (
@@ -54,20 +52,22 @@ export default function TranscriptCorrect(details: TranscriptCorrect) { />
-
+
- +
); diff --git a/www/app/[domain]/transcripts/[transcriptId]/correct/participantList.tsx b/www/app/[domain]/transcripts/[transcriptId]/correct/participantList.tsx new file mode 100644 index 00000000..09e1b3d6 --- /dev/null +++ b/www/app/[domain]/transcripts/[transcriptId]/correct/participantList.tsx @@ -0,0 +1,117 @@ +import { faSpinner } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useEffect, useRef, useState } from "react"; +import { Participant } from "../../../../api"; +import getApi from "../../../../lib/getApi"; + +const ParticipantList = ({ + transcriptId, + participants, + selectedTime, + topicWithWords, +}) => { + const api = getApi(); + + const [loading, setLoading] = useState(false); + const [participantInput, setParticipantInput] = useState(""); + const inputRef = useRef(null); + + const createParticipant = () => { + if (!loading) { + setLoading(true); + api + ?.v1TranscriptAddParticipant({ + createParticipant: { name: participantInput, speaker: 99 }, + transcriptId, + }) + .then((participant) => { + participants.refetch(); + assignTo(participant)(); + }); + } + }; + + useEffect(() => { + if (loading) { + setLoading(false); + } + }, [participants.loading]); + + useEffect(() => { + if (selectedTime) { + inputRef.current?.focus(); + } + }, [selectedTime]); + + const deleteParticipant = (participantId) => () => { + if (!loading) { + api + ?.v1TranscriptDeleteParticipant({ + transcriptId, + participantId, + }) + .then(() => { + participants.refetch(); + }); + } + }; + + const assignTo = + (participant) => (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + if (selectedTime?.start == undefined || selectedTime?.end == undefined) + return; + api + ?.v1TranscriptAssignSpeaker({ + speakerAssignment: { + speaker: participant.speaker, + timestampFrom: selectedTime.start, + timestampTo: selectedTime.end, + }, + transcriptId, + }) + .then(() => { + topicWithWords.refetch(); + }); + }; + return ( + <> + setParticipantInput(e.target.value)} + /> + + {participants.loading && ( + + )} + {participants.response && ( +
    + {participants.response.map((participant: Participant) => ( +
  • + {participant.name} +
    + + +
    +
  • + ))} +
+ )} + + ); +}; + +export default ParticipantList; diff --git a/www/app/[domain]/transcripts/[transcriptId]/correct/topicWords.tsx b/www/app/[domain]/transcripts/[transcriptId]/correct/topicWords.tsx index ea2f243d..1166d4a6 100644 --- a/www/app/[domain]/transcripts/[transcriptId]/correct/topicWords.tsx +++ b/www/app/[domain]/transcripts/[transcriptId]/correct/topicWords.tsx @@ -1,6 +1,7 @@ -import { useCallback, useEffect, useState } from "react"; -import useTopicWithWords from "../../useTopicWithWords"; +import { SetStateAction, useCallback, useEffect, useState } from "react"; import WaveformLoading from "../../waveformLoading"; +import { UseParticipants } from "../../useParticipants"; +import { Participant } from "../../../../api"; type Word = { end: number; @@ -11,19 +12,26 @@ type Word = { type WordBySpeaker = { speaker: number; words: Word[] }[]; -// TODO fix selection reversed // TODO shortcuts // TODO fix key (using indexes might act up, not sure as we don't re-order per say) +type TopicWordsProps = { + setSelectedTime: SetStateAction; + selectedTime: any; + setTopicTime: SetStateAction; + stateSelectedSpeaker: any; + participants: UseParticipants; + topicWithWords: any; +}; + const topicWords = ({ setSelectedTime, - currentTopic, - transcriptId, + selectedTime, setTopicTime, stateSelectedSpeaker, participants, -}) => { - const topicWithWords = useTopicWithWords(currentTopic, transcriptId); + topicWithWords, +}: TopicWordsProps) => { const [wordsBySpeaker, setWordsBySpeaker] = useState(); const [selectedSpeaker, setSelectedSpeaker] = stateSelectedSpeaker; @@ -31,6 +39,7 @@ const topicWords = ({ if (topicWithWords.loading) { setWordsBySpeaker([]); setSelectedTime(undefined); + console.log("unsetting topic changed"); } }, [topicWithWords.loading]); @@ -54,98 +63,92 @@ const topicWords = ({ } }, [topicWithWords.response]); - useEffect(() => { - document.onmouseup = (e) => { - let selection = window.getSelection(); - if ( - selection && - selection.anchorNode && - selection.focusNode && - (selection.anchorNode !== selection.focusNode || - selection.anchorOffset !== selection.focusOffset) - ) { - const anchorNode = selection.anchorNode; - const anchorIsWord = - !!selection.anchorNode.parentElement?.dataset["start"]; - const correctedAnchor = anchorIsWord - ? anchorNode - : anchorNode.parentNode?.firstChild; - const anchorOffset = anchorIsWord ? 1 : 0; - const focusNode = selection.focusNode; - const focusIsWord = !!selection.focusNode.parentElement?.dataset["end"]; - const correctedfocus = focusIsWord - ? focusNode - : focusNode.parentNode?.lastChild; - const focusOffset = focusIsWord - ? focusNode.textContent?.length - : focusNode.parentNode?.lastChild?.textContent?.length; + const onMouseUp = (e) => { + let selection = window.getSelection(); + if ( + selection && + selection.anchorNode && + selection.focusNode && + selection.anchorNode == selection.focusNode && + selection.anchorOffset == selection.focusOffset + ) { + setSelectedTime(undefined); + selection.empty(); + return; + } + if ( + selection && + selection.anchorNode && + selection.focusNode && + (selection.anchorNode !== selection.focusNode || + selection.anchorOffset !== selection.focusOffset) + ) { + const anchorNode = selection.anchorNode; + const anchorIsWord = + !!selection.anchorNode.parentElement?.dataset["start"]; + const focusNode = selection.focusNode; + const focusIsWord = !!selection.focusNode.parentElement?.dataset["end"]; - if ( - correctedAnchor && - anchorOffset !== undefined && - correctedfocus && - focusOffset !== undefined - ) { - selection.setBaseAndExtent( - correctedAnchor, - anchorOffset, - correctedfocus, - focusOffset, - ); - - if ( - !anchorIsWord && - !focusIsWord && - anchorNode.parentElement == focusNode.parentElement - ) { - console.log(focusNode.parentElement?.dataset); - setSelectedSpeaker(focusNode.parentElement?.dataset["speaker"]); - setSelectedTime(undefined); - } else { - setSelectedSpeaker(undefined); - setSelectedTime({ - start: - selection.anchorNode.parentElement?.dataset["start"] || - (selection.anchorNode.parentElement?.nextElementSibling as any) - ?.dataset["start"] || - 0, - end: - selection.focusNode.parentElement?.dataset["end"] || - ( - selection.focusNode.parentElement?.parentElement - ?.previousElementSibling?.lastElementChild as any - )?.dataset || - 0, - }); - } - } - } + // If selected a speaker : if ( - selection && - selection.anchorNode && - selection.focusNode && - selection.anchorNode == selection.focusNode && - selection.anchorOffset == selection.focusOffset + !anchorIsWord && + !focusIsWord && + anchorNode.parentElement == focusNode.parentElement ) { + setSelectedSpeaker(focusNode.parentElement?.dataset["speaker"]); setSelectedTime(undefined); + console.log("Unset Time : selected Speaker"); + return; } - }; - }, []); - const getSpeakerName = useCallback( - (speakerNumber: number) => { - return ( - participants.response.find((participant) => { - participant.speaker == speakerNumber; - }) || `Speaker ${speakerNumber}` - ); - }, - [participants], - ); + const anchorStart = anchorIsWord + ? anchorNode.parentElement?.dataset["start"] + : (selection.anchorNode.parentElement?.nextElementSibling as any) + ?.dataset["start"]; + const focusEnd = + selection.focusNode.parentElement?.dataset["end"] || + ( + selection.focusNode.parentElement?.parentElement + ?.previousElementSibling?.lastElementChild as any + )?.dataset["end"]; - if (!topicWithWords.loading && wordsBySpeaker && participants) { + const reverse = anchorStart > focusEnd; + setSelectedTime(undefined); + + if (!reverse) { + setSelectedTime({ start: anchorStart, end: focusEnd }); + console.log("setting right"); + } else { + const anchorEnd = anchorIsWord + ? anchorNode.parentElement?.dataset["end"] + : (selection.anchorNode.parentElement?.nextElementSibling as any) + ?.dataset["end"]; + const focusStart = + selection.focusNode.parentElement?.dataset["start"] || + ( + selection.focusNode.parentElement?.parentElement + ?.previousElementSibling?.lastElementChild as any + )?.dataset["start"]; + setSelectedTime({ start: focusStart, end: anchorEnd }); + console.log("setting reverse"); + } + setSelectedSpeaker(); + selection.empty(); + } + }; + + const getSpeakerName = (speakerNumber: number) => { + if (!participants.response) return; return ( -
+ (participants.response as Participant[]).find( + (participant) => participant.speaker == speakerNumber, + )?.name || `Speaker ${speakerNumber}` + ); + }; + + if (!topicWithWords.loading && wordsBySpeaker && participants.response) { + return ( +
console.log(e)}> {wordsBySpeaker?.map((speakerWithWords, index) => (

{speakerWithWords.words.map((word, index) => ( - + = word.end + ? "bg-yellow-200" + : "" + } + > {word.text} ))} diff --git a/www/app/[domain]/transcripts/useParticipants.ts b/www/app/[domain]/transcripts/useParticipants.ts index cb9b556e..bcf2ac3b 100644 --- a/www/app/[domain]/transcripts/useParticipants.ts +++ b/www/app/[domain]/transcripts/useParticipants.ts @@ -19,18 +19,29 @@ type LoadingParticipants = { type SuccessParticipants = { response: Participant[]; - loading: false; + loading: boolean; error: null; }; -const useParticipants = ( - transcriptId: string, -): ErrorParticipants | LoadingParticipants | SuccessParticipants => { +export type UseParticipants = ( + | ErrorParticipants + | LoadingParticipants + | SuccessParticipants +) & { refetch: () => void }; + +const useParticipants = (transcriptId: string): UseParticipants => { const [response, setResponse] = useState(null); const [loading, setLoading] = useState(true); const [error, setErrorState] = useState(null); const { setError } = useError(); const api = getApi(); + const [count, setCount] = useState(0); + + const refetch = () => { + setCount(count + 1); + setLoading(true); + setErrorState(null); + }; useEffect(() => { if (!transcriptId || !api) return; @@ -55,12 +66,9 @@ const useParticipants = ( } setErrorState(error); }); - }, [transcriptId, !api]); + }, [transcriptId, !api, count]); - return { response, loading, error } as - | ErrorParticipants - | LoadingParticipants - | SuccessParticipants; + return { response, loading, error, refetch } as UseParticipants; }; export default useParticipants; diff --git a/www/app/[domain]/transcripts/useTopicWithWords.ts b/www/app/[domain]/transcripts/useTopicWithWords.ts index 99e0af83..887488ad 100644 --- a/www/app/[domain]/transcripts/useTopicWithWords.ts +++ b/www/app/[domain]/transcripts/useTopicWithWords.ts @@ -23,16 +23,30 @@ type SuccessTopicWithWords = { error: null; }; +type UseTopicWithWords = { refetch: () => void } & ( + | ErrorTopicWithWords + | LoadingTopicWithWords + | SuccessTopicWithWords +); + const useTopicWithWords = ( topicId: string | null, transcriptId: string, -): ErrorTopicWithWords | LoadingTopicWithWords | SuccessTopicWithWords => { +): UseTopicWithWords => { const [response, setResponse] = useState(null); const [loading, setLoading] = useState(true); const [error, setErrorState] = useState(null); const { setError } = useError(); const api = getApi(); + const [count, setCount] = useState(0); + + const refetch = () => { + setCount(count + 1); + setLoading(true); + setErrorState(null); + }; + useEffect(() => { setLoading(true); }, [transcriptId, topicId]); @@ -60,12 +74,9 @@ const useTopicWithWords = ( } setErrorState(error); }); - }, [transcriptId, !api, topicId]); + }, [transcriptId, !api, topicId, count]); - return { response, loading, error } as - | ErrorTopicWithWords - | LoadingTopicWithWords - | SuccessTopicWithWords; + return { response, loading, error, refetch } as UseTopicWithWords; }; export default useTopicWithWords;