From c02c3b190cab44bac50c7bf63ecd3a0de40fe817 Mon Sep 17 00:00:00 2001 From: Sara Date: Tue, 12 Dec 2023 13:29:55 +0100 Subject: [PATCH] changes types, assign to participant instead of speaker --- .../[transcriptId]/correct/page.tsx | 67 +++-- .../correct/participantList.tsx | 263 +++++++++++------- .../[transcriptId]/correct/topicHeader.tsx | 37 ++- .../[transcriptId]/correct/topicWords.tsx | 136 ++++----- www/app/[domain]/transcripts/mockTopics.json | 1 + .../[domain]/transcripts/useTopicWithWords.ts | 29 +- www/app/[domain]/transcripts/useTopics.ts | 3 +- 7 files changed, 312 insertions(+), 224 deletions(-) diff --git a/www/app/[domain]/transcripts/[transcriptId]/correct/page.tsx b/www/app/[domain]/transcripts/[transcriptId]/correct/page.tsx index c27abef7..839a5807 100644 --- a/www/app/[domain]/transcripts/[transcriptId]/correct/page.tsx +++ b/www/app/[domain]/transcripts/[transcriptId]/correct/page.tsx @@ -1,59 +1,77 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, 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"; +import { GetTranscriptTopic } from "../../../../api"; -type TranscriptCorrect = { +export type TranscriptCorrect = { params: { transcriptId: string; }; }; -type TimeSlice = { +export type TimeSlice = { start: number; end: number; }; +export type SelectedText = number | TimeSlice | undefined; + +export function selectedTextIsSpeaker( + selectedText: SelectedText, +): selectedText is number { + return typeof selectedText == "number"; +} +export function selectedTextIsTimeSlice( + selectedText: SelectedText, +): selectedText is TimeSlice { + return ( + typeof (selectedText as any)?.start == "number" && + typeof (selectedText as any)?.end == "number" + ); +} + 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 stateCurrentTopic = useState(); + const [currentTopic, _sct] = stateCurrentTopic; + const topicWithWords = useTopicWithWords(currentTopic?.id, transcriptId); - const [selectedTime, setSelectedTime] = useState(); const [topicTime, setTopicTime] = useState(); const participants = useParticipants(transcriptId); - const stateSelectedSpeaker = useState(); + const stateSelectedText = useState(); + const [selectedText, _sst] = stateSelectedText; + console.log(selectedText); + + useEffect(() => { + if (currentTopic) { + setTopicTime({ + start: currentTopic.timestamp, + end: currentTopic.timestamp + currentTopic.duration, + }); + } else { + setTopicTime(undefined); + } + }, [currentTopic]); // TODO BE - // Get one topic with words - // -> fix useTopicWithWords.ts - // Add start and end time of each topic in the topic list - // -> use full current topic instead of topicId here - // -> remove time calculation and setting from TopicHeader - // -> pass in topicTime to player directly - // Should we have participants by default, one for each speaker ? // Creating a participant and a speaker ? return (
@@ -61,16 +79,17 @@ 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 index 54f34603..b9d7fbd4 100644 --- a/www/app/[domain]/transcripts/[transcriptId]/correct/participantList.tsx +++ b/www/app/[domain]/transcripts/[transcriptId]/correct/participantList.tsx @@ -4,28 +4,27 @@ import { useEffect, useRef, useState } from "react"; import { Participant } from "../../../../api"; import getApi from "../../../../lib/getApi"; import { UseParticipants } from "../../useParticipants"; +import { selectedTextIsSpeaker, selectedTextIsTimeSlice } from "./page"; type ParticipantList = { participants: UseParticipants; transcriptId: string; - selectedTime: any; topicWithWords: any; - stateSelectedSpeaker: any; + stateSelectedText: any; }; const ParticipantList = ({ transcriptId, participants, - selectedTime, topicWithWords, - stateSelectedSpeaker, + stateSelectedText, }: ParticipantList) => { const api = getApi(); const [loading, setLoading] = useState(false); const [participantInput, setParticipantInput] = useState(""); const inputRef = useRef(null); - const [selectedSpeaker, setSelectedSpeaker] = stateSelectedSpeaker; + const [selectedText, setSelectedText] = stateSelectedText; const [selectedParticipant, setSelectedParticipant] = useState(); const [action, setAction] = useState< "Create" | "Create to rename" | "Create and assign" | "Rename" | null @@ -40,10 +39,10 @@ const ParticipantList = ({ useEffect(() => { if (participants.response) { - if (selectedSpeaker !== undefined) { + if (selectedTextIsSpeaker(selectedText)) { inputRef.current?.focus(); const participant = participants.response.find( - (p) => p.speaker == selectedSpeaker, + (p) => p.speaker == selectedText, ); if (participant) { setParticipantInput(participant.name); @@ -57,20 +56,23 @@ const ParticipantList = ({ setAction("Create to rename"); } } - if (selectedTime) { + if (selectedTextIsTimeSlice(selectedText)) { setParticipantInput(""); inputRef.current?.focus(); setAction("Create and assign"); setSelectedParticipant(undefined); } - if (!selectedTime && !selectedSpeaker) { + if (typeof selectedText == undefined) { setAction(null); } } - }, [selectedTime, selectedSpeaker]); + }, [selectedText]); useEffect(() => { - if (participants.response && action == "Create and assign") { + if ( + participants.response && + (action == "Create and assign" || action == "Create to rename") + ) { if ( participants.response.filter((p) => p.name.startsWith(participantInput)) .length == 1 @@ -87,18 +89,23 @@ const ParticipantList = ({ if (participantInput && !action) { setAction("Create"); } - if (!participantInput) { - setAction(null); - } }, [participantInput]); useEffect(() => { document.onkeyup = (e) => { if (e.key === "Enter" && e.ctrlKey) { if (oneMatch) { - assignTo(oneMatch)(); - setOneMatch(undefined); - setParticipantInput(""); + if (action == "Create and assign") { + assignTo(oneMatch)(); + setOneMatch(undefined); + setParticipantInput(""); + } else if ( + action == "Create to rename" && + oneMatch && + selectedTextIsSpeaker(selectedText) + ) { + mergeSpeaker(selectedText, oneMatch)(); + } } } else if (e.key === "Enter") { doAction(); @@ -106,13 +113,36 @@ const ParticipantList = ({ }; }); + const mergeSpeaker = + (speakerFrom, participantTo: Participant) => async () => { + if (participantTo.speaker) { + await api?.v1TranscriptMergeSpeaker({ + transcriptId, + speakerMerge: { + speakerFrom: speakerFrom, + speakerTo: participantTo.speaker, + }, + }); + } else { + await api?.v1TranscriptUpdateParticipant({ + transcriptId, + participantId: participantTo.id, + updateParticipant: { speaker: speakerFrom }, + }); + } + participants.refetch(); + topicWithWords.refetch(); + setAction(null); + setParticipantInput(""); + }; + const doAction = (e?) => { e?.preventDefault(); e?.stopPropagation(); if (!participants.response) return; - if (action == "Rename") { + if (action == "Rename" && selectedTextIsSpeaker(selectedText)) { const participant = participants.response.find( - (p) => p.speaker == selectedSpeaker, + (p) => p.speaker == selectedText, ); if (participant && participant.name !== participantInput) { api @@ -127,14 +157,16 @@ const ParticipantList = ({ participants.refetch(); }); } - } else if (action == "Create to rename") { + } else if ( + action == "Create to rename" && + selectedTextIsSpeaker(selectedText) + ) { setLoading(true); - console.log(participantInput, selectedSpeaker); api ?.v1TranscriptAddParticipant({ createParticipant: { name: participantInput, - speaker: selectedSpeaker, + speaker: selectedText, }, transcriptId, }) @@ -142,13 +174,15 @@ const ParticipantList = ({ participants.refetch(); setParticipantInput(""); }); - } else if (action == "Create and assign") { + } else if ( + action == "Create and assign" && + selectedTextIsTimeSlice(selectedText) + ) { setLoading(true); api ?.v1TranscriptAddParticipant({ createParticipant: { name: participantInput, - speaker: Math.floor(Math.random() * 100 + 10), }, transcriptId, }) @@ -191,14 +225,14 @@ const ParticipantList = ({ e?.preventDefault(); e?.stopPropagation(); // fix participant that doesnt have a speaker (wait API) - if (selectedTime?.start == undefined || selectedTime?.end == undefined) - return; + if (!selectedTextIsTimeSlice(selectedText)) return; + api ?.v1TranscriptAssignSpeaker({ speakerAssignment: { - speaker: participant.speaker, - timestampFrom: selectedTime.start, - timestampTo: selectedTime.end, + participant: participant.id, + timestampFrom: selectedText.start, + timestampTo: selectedText.end, }, transcriptId, }) @@ -209,82 +243,113 @@ const ParticipantList = ({ const selectParticipant = (participant) => (e) => { setSelectedParticipant(participant); - setSelectedSpeaker(participant.speaker); + setSelectedText(participant.speaker); setAction("Rename"); setParticipantInput(participant.name); }; + + const clearSelection = () => { + setSelectedParticipant(undefined); + setSelectedText(undefined); + setAction(null); + }; + const preventClick = (e) => { + e?.stopPropagation(); + e?.preventDefault(); + }; + return ( - <> -
- setParticipantInput(e.target.value)} - value={participantInput} - /> - {action && ( - +
+
+
+ setParticipantInput(e.target.value)} + value={participantInput} + /> + {action && ( + + )} +
+ + {participants.loading && ( + + )} + {participants.response && ( +
    + {participants.response.map((participant: Participant) => ( +
  • 0 && + selectedText && + participant.name.startsWith(participantInput) + ? "bg-blue-100 " + : "") + + (participant.id == selectedParticipant?.id + ? "border-blue-400 border" + : "") + } + key={participant.id} + > + {participant.name} + +
    + {selectedTextIsSpeaker(selectedText) && !loading && ( + + )} + {selectedTextIsTimeSlice(selectedText) && !loading && ( + + )} + + +
    +
  • + ))} +
)}
- - {participants.loading && ( - - )} - {participants.response && ( -
    - {participants.response.map((participant: Participant) => ( -
  • 0 && - selectedTime && - participant.name.startsWith(participantInput) - ? "bg-blue-100 " - : "") + - (participant.id == selectedParticipant?.id - ? "border-blue-400 border" - : "") - } - key={participant.id} - > - {participant.name} - -
    - {selectedTime && !loading && ( - - )} - - -
    -
  • - ))} -
- )} - +
); }; diff --git a/www/app/[domain]/transcripts/[transcriptId]/correct/topicHeader.tsx b/www/app/[domain]/transcripts/[transcriptId]/correct/topicHeader.tsx index 335d9bde..15328157 100644 --- a/www/app/[domain]/transcripts/[transcriptId]/correct/topicHeader.tsx +++ b/www/app/[domain]/transcripts/[transcriptId]/correct/topicHeader.tsx @@ -1,32 +1,41 @@ import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import useTopics from "../../useTopics"; -import { useEffect } from "react"; +import { Dispatch, SetStateAction, useEffect } from "react"; +import { TranscriptTopic } from "../../../../api/models/TranscriptTopic"; +import { GetTranscriptTopic } from "../../../../api"; + +type TopicHeader = { + stateCurrentTopic: [ + GetTranscriptTopic | undefined, + Dispatch>, + ]; + transcriptId: string; +}; export default function TopicHeader({ - currentTopic, - setCurrentTopic, + stateCurrentTopic, transcriptId, -}) { +}: TopicHeader) { + const [currentTopic, setCurrentTopic] = stateCurrentTopic; const topics = useTopics(transcriptId); + useEffect(() => { if (!topics.loading && !currentTopic) { - setCurrentTopic(topics?.topics?.at(0)?.id); + setCurrentTopic(topics?.topics?.at(0)); } }, [topics.loading]); - if (topics.topics) { - const title = topics.topics.find((topic) => topic.id == currentTopic) - ?.title; - const number = topics.topics.findIndex((topic) => topic.id == currentTopic); + if (topics.topics && currentTopic) { + const number = topics.topics.findIndex( + (topic) => topic.id == currentTopic.id, + ); const canGoPrevious = number > 0; const total = topics.topics.length; const canGoNext = total && number < total + 1; - const onPrev = () => - setCurrentTopic(topics.topics?.at(number - 1)?.id || ""); - const onNext = () => - setCurrentTopic(topics.topics?.at(number + 1)?.id || ""); + const onPrev = () => setCurrentTopic(topics.topics?.at(number - 1)); + const onNext = () => setCurrentTopic(topics.topics?.at(number + 1)); return (
@@ -40,7 +49,7 @@ export default function TopicHeader({

- {title}{" "} + {currentTopic.title}{" "} {number + 1}/{total} diff --git a/www/app/[domain]/transcripts/[transcriptId]/correct/topicWords.tsx b/www/app/[domain]/transcripts/[transcriptId]/correct/topicWords.tsx index a7e2a587..222fef1a 100644 --- a/www/app/[domain]/transcripts/[transcriptId]/correct/topicWords.tsx +++ b/www/app/[domain]/transcripts/[transcriptId]/correct/topicWords.tsx @@ -1,68 +1,43 @@ -import { SetStateAction, useCallback, useEffect, useState } from "react"; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useState, +} from "react"; import WaveformLoading from "../../waveformLoading"; import { UseParticipants } from "../../useParticipants"; import { Participant } from "../../../../api"; - -type Word = { - end: number; - speaker: number; - start: number; - text: string; -}; - -type WordBySpeaker = { speaker: number; words: Word[] }[]; +import { UseTopicWithWords } from "../../useTopicWithWords"; +import { TimeSlice, selectedTextIsTimeSlice } from "./page"; // 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; + stateSelectedText: [ + number | TimeSlice | undefined, + Dispatch>, + ]; participants: UseParticipants; - topicWithWords: any; + topicWithWords: UseTopicWithWords; }; const topicWords = ({ - setSelectedTime, - selectedTime, - setTopicTime, - stateSelectedSpeaker, + stateSelectedText, participants, topicWithWords, }: TopicWordsProps) => { - const [wordsBySpeaker, setWordsBySpeaker] = useState(); - const [selectedSpeaker, setSelectedSpeaker] = stateSelectedSpeaker; + const [selectedText, setSelectedText] = stateSelectedText; useEffect(() => { if (topicWithWords.loading) { - setWordsBySpeaker([]); - setSelectedTime(undefined); + // setWordsBySpeaker([]); + setSelectedText(undefined); console.log("unsetting topic changed"); } }, [topicWithWords.loading]); - useEffect(() => { - if (!topicWithWords.loading && !topicWithWords.error) { - const wordsFlat = topicWithWords.response.words as Word[]; - const wordsSorted = wordsFlat.reduce((acc, curr) => { - if (acc.length > 0 && acc[acc.length - 1].speaker == curr.speaker) { - acc[acc.length - 1].words.push(curr); - return acc; - } else { - acc?.push({ speaker: curr.speaker, words: [curr] }); - return acc; - } - }, [] as WordBySpeaker); - setWordsBySpeaker(wordsSorted); - setTopicTime({ - start: wordsFlat.at(0)?.start, - end: wordsFlat.at(wordsFlat.length - 1)?.end, - }); - } - }, [topicWithWords.response]); - const getStartTimeFromFirstNode = (node, offset, reverse) => { // if the first element is a word return node.parentElement?.dataset["start"] @@ -91,7 +66,7 @@ const topicWords = ({ selection.anchorNode == selection.focusNode && selection.anchorOffset == selection.focusOffset ) { - setSelectedTime(undefined); + setSelectedText(undefined); selection.empty(); return; } @@ -114,9 +89,11 @@ const topicWords = ({ !focusIsWord && anchorNode.parentElement == focusNode.parentElement ) { - setSelectedSpeaker(focusNode.parentElement?.dataset["speaker"]); - setSelectedTime(undefined); - selection.empty(); + setSelectedText( + focusNode.parentElement?.dataset["speaker"] + ? parseInt(focusNode.parentElement?.dataset["speaker"]) + : undefined, + ); console.log("Unset Time : selected Speaker"); return; } @@ -139,7 +116,7 @@ const topicWords = ({ const reverse = parseFloat(anchorStart) > parseFloat(focusEnd); if (!reverse) { - setSelectedTime({ + setSelectedText({ start: parseFloat(anchorStart), end: parseFloat(focusEnd), }); @@ -158,13 +135,14 @@ const topicWords = ({ true, ); - setSelectedTime({ start: focusStart, end: anchorEnd }); + setSelectedText({ + start: parseFloat(focusStart), + end: parseFloat(anchorEnd), + }); console.log("setting reverse"); } - - setSelectedSpeaker(); - selection.empty(); } + selection && selection.empty(); }; const getSpeakerName = (speakerNumber: number) => { @@ -176,39 +154,45 @@ const topicWords = ({ ); }; - if (!topicWithWords.loading && wordsBySpeaker && participants.response) { + if ( + !topicWithWords.loading && + topicWithWords.response && + participants.response + ) { return (
- {wordsBySpeaker?.map((speakerWithWords, index) => ( -

- - {getSpeakerName(speakerWithWords.speaker)} :  - - {speakerWithWords.words.map((word, index) => ( + {topicWithWords.response.wordsPerSpeaker.map( + (speakerWithWords, index) => ( +

= word.end + selectedText == speakerWithWords.speaker ? "bg-yellow-200" : "" } > - {word.text} + {getSpeakerName(speakerWithWords.speaker)} :  - ))} -

- ))} + {speakerWithWords.words.map((word, index) => ( + = word.end + ? "bg-yellow-200" + : "" + } + > + {word.text} + + ))} +

+ ), + )}
); } diff --git a/www/app/[domain]/transcripts/mockTopics.json b/www/app/[domain]/transcripts/mockTopics.json index 2e974dac..ebe9c1cf 100644 --- a/www/app/[domain]/transcripts/mockTopics.json +++ b/www/app/[domain]/transcripts/mockTopics.json @@ -5,6 +5,7 @@ "summary": "The team discusses the first issue in the list", "timestamp": 0.0, "transcript": "", + "duration": 33, "segments": [ { "text": "Let's start with issue one, Alice you've been working on that, can you give an update ?", diff --git a/www/app/[domain]/transcripts/useTopicWithWords.ts b/www/app/[domain]/transcripts/useTopicWithWords.ts index 887488ad..de8cb66c 100644 --- a/www/app/[domain]/transcripts/useTopicWithWords.ts +++ b/www/app/[domain]/transcripts/useTopicWithWords.ts @@ -1,6 +1,13 @@ import { useEffect, useState } from "react"; -import { V1TranscriptGetTopicsWithWordsRequest } from "../../api/apis/DefaultApi"; -import { GetTranscript, GetTranscriptTopicWithWords } from "../../api"; +import { + V1TranscriptGetTopicsWithWordsPerSpeakerRequest, + V1TranscriptGetTopicsWithWordsRequest, +} from "../../api/apis/DefaultApi"; +import { + GetTranscript, + GetTranscriptTopicWithWords, + GetTranscriptTopicWithWordsPerSpeaker, +} from "../../api"; import { useError } from "../../(errors)/errorContext"; import getApi from "../../lib/getApi"; import { shouldShowError } from "../../lib/errorUtils"; @@ -8,22 +15,22 @@ import { shouldShowError } from "../../lib/errorUtils"; type ErrorTopicWithWords = { error: Error; loading: false; - response: any; + response: null; }; type LoadingTopicWithWords = { - response: any; + response: GetTranscriptTopicWithWordsPerSpeaker | null; loading: true; error: false; }; type SuccessTopicWithWords = { - response: GetTranscriptTopicWithWords; + response: GetTranscriptTopicWithWordsPerSpeaker; loading: false; error: null; }; -type UseTopicWithWords = { refetch: () => void } & ( +export type UseTopicWithWords = { refetch: () => void } & ( | ErrorTopicWithWords | LoadingTopicWithWords | SuccessTopicWithWords @@ -33,7 +40,8 @@ const useTopicWithWords = ( topicId: string | null, transcriptId: string, ): UseTopicWithWords => { - const [response, setResponse] = useState(null); + const [response, setResponse] = + useState(null); const [loading, setLoading] = useState(true); const [error, setErrorState] = useState(null); const { setError } = useError(); @@ -55,13 +63,14 @@ const useTopicWithWords = ( if (!transcriptId || !topicId || !api) return; setLoading(true); - const requestParameters: V1TranscriptGetTopicsWithWordsRequest = { + const requestParameters: V1TranscriptGetTopicsWithWordsPerSpeakerRequest = { transcriptId, + topicId, }; api - .v1TranscriptGetTopicsWithWords(requestParameters) + .v1TranscriptGetTopicsWithWordsPerSpeaker(requestParameters) .then((result) => { - setResponse(result.find((topic) => topic.id == topicId)); + setResponse(result); setLoading(false); console.debug("Topics with words Loaded:", result); }) diff --git a/www/app/[domain]/transcripts/useTopics.ts b/www/app/[domain]/transcripts/useTopics.ts index c5cbce55..feffd177 100644 --- a/www/app/[domain]/transcripts/useTopics.ts +++ b/www/app/[domain]/transcripts/useTopics.ts @@ -5,9 +5,10 @@ import { Topic } from "./webSocketTypes"; import getApi from "../../lib/getApi"; import { shouldShowError } from "../../lib/errorUtils"; import mockTopics from "./mockTopics.json"; +import { GetTranscriptTopic } from "../../api"; type TranscriptTopics = { - topics: Topic[] | null; + topics: GetTranscriptTopic[] | null; loading: boolean; error: Error | null; };