diff --git a/www/app/[domain]/transcripts/[transcriptId]/correct/page.tsx b/www/app/[domain]/transcripts/[transcriptId]/correct/page.tsx new file mode 100644 index 00000000..4f8ac9a4 --- /dev/null +++ b/www/app/[domain]/transcripts/[transcriptId]/correct/page.tsx @@ -0,0 +1,39 @@ +"use client"; +import { useState } from "react"; +import useTranscript from "../../useTranscript"; +import TopicHeader from "./topicHeader"; +import TopicWords from "./topicWords"; + +type TranscriptCorrect = { + params: { + transcriptId: string; + }; +}; + +export default function TranscriptCorrect(details: TranscriptCorrect) { + const transcriptId = details.params.transcriptId; + const transcript = useTranscript(transcriptId); + const [currentTopic, setCurrentTopic] = useState(""); + const [selectedTime, setSelectedTime] = useState<{ + start: number; + end: number; + }>(); + + console.log(selectedTime); + return ( +
+
+ + +
+
+ ); +} diff --git a/www/app/[domain]/transcripts/[transcriptId]/correct/topicHeader.tsx b/www/app/[domain]/transcripts/[transcriptId]/correct/topicHeader.tsx new file mode 100644 index 00000000..a21a8581 --- /dev/null +++ b/www/app/[domain]/transcripts/[transcriptId]/correct/topicHeader.tsx @@ -0,0 +1,60 @@ +import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import useTopics from "../../useTopics"; +import { useEffect } from "react"; + +export default function TopicHeader({ + currentTopic, + setCurrentTopic, + transcriptId, +}) { + const topics = useTopics(transcriptId); + useEffect(() => { + !topics.loading && setCurrentTopic(topics?.topics?.at(0)?.id); + console.log(currentTopic); + }, [topics.loading]); + + if (topics.topics) { + const title = topics.topics.find((topic) => topic.id == currentTopic) + ?.title; + const number = topics.topics.findIndex((topic) => topic.id == currentTopic); + 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 || ""); + + return ( +
+ +

+ {title}{" "} + + {number + 1}/{total} + +

+ +
+ ); + } + return null; +} diff --git a/www/app/[domain]/transcripts/[transcriptId]/correct/topicWords.tsx b/www/app/[domain]/transcripts/[transcriptId]/correct/topicWords.tsx new file mode 100644 index 00000000..aad04924 --- /dev/null +++ b/www/app/[domain]/transcripts/[transcriptId]/correct/topicWords.tsx @@ -0,0 +1,127 @@ +import { useEffect, useState } from "react"; +import useTopicWithWords from "../../useTopicWithWords"; +import WaveformLoading from "../../waveformLoading"; + +type Word = { + end: number; + speaker: number; + start: number; + text: string; +}; + +type WordBySpeaker = { speaker: number; words: Word[] }[]; + +const topicWords = ({ setSelectedTime, currentTopic, transcriptId }) => { + const topicWithWords = useTopicWithWords(currentTopic, transcriptId); + const [wordsBySpeaker, setWordsBySpeaker] = useState(); + + useEffect(() => { + if (topicWithWords.loading) { + setWordsBySpeaker([]); + setSelectedTime(undefined); + } + }, [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); + } + }, [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; + if ( + correctedAnchor && + anchorOffset !== undefined && + correctedfocus && + focusOffset !== undefined + ) { + selection.setBaseAndExtent( + correctedAnchor, + anchorOffset, + correctedfocus, + focusOffset, + ); + 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 ( + selection && + selection.anchorNode && + selection.focusNode && + selection.anchorNode == selection.focusNode && + selection.anchorOffset == selection.focusOffset + ) { + setSelectedTime(undefined); + } + }; + }, []); + + if (!topicWithWords.loading && wordsBySpeaker) { + return ( +
+ {wordsBySpeaker?.map((speakerWithWords) => ( +

+ Speaker {speakerWithWords.speaker} :  + {speakerWithWords.words.map((word) => ( + + {word.text} + + ))} +

+ ))} +
+ ); + } + if (topicWithWords.loading) return ; + if (topicWithWords.error) return

error

; + return null; +}; + +export default topicWords; diff --git a/www/app/[domain]/transcripts/useTopicWithWords.ts b/www/app/[domain]/transcripts/useTopicWithWords.ts new file mode 100644 index 00000000..99e0af83 --- /dev/null +++ b/www/app/[domain]/transcripts/useTopicWithWords.ts @@ -0,0 +1,71 @@ +import { useEffect, useState } from "react"; +import { V1TranscriptGetTopicsWithWordsRequest } from "../../api/apis/DefaultApi"; +import { GetTranscript, GetTranscriptTopicWithWords } from "../../api"; +import { useError } from "../../(errors)/errorContext"; +import getApi from "../../lib/getApi"; +import { shouldShowError } from "../../lib/errorUtils"; + +type ErrorTopicWithWords = { + error: Error; + loading: false; + response: any; +}; + +type LoadingTopicWithWords = { + response: any; + loading: true; + error: false; +}; + +type SuccessTopicWithWords = { + response: GetTranscriptTopicWithWords; + loading: false; + error: null; +}; + +const useTopicWithWords = ( + topicId: string | null, + transcriptId: string, +): ErrorTopicWithWords | LoadingTopicWithWords | SuccessTopicWithWords => { + const [response, setResponse] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setErrorState] = useState(null); + const { setError } = useError(); + const api = getApi(); + + useEffect(() => { + setLoading(true); + }, [transcriptId, topicId]); + + useEffect(() => { + if (!transcriptId || !topicId || !api) return; + + setLoading(true); + const requestParameters: V1TranscriptGetTopicsWithWordsRequest = { + transcriptId, + }; + api + .v1TranscriptGetTopicsWithWords(requestParameters) + .then((result) => { + setResponse(result.find((topic) => topic.id == topicId)); + setLoading(false); + console.debug("Topics with words Loaded:", result); + }) + .catch((error) => { + const shouldShowHuman = shouldShowError(error); + if (shouldShowHuman) { + setError(error, "There was an error loading the topics with words"); + } else { + setError(error); + } + setErrorState(error); + }); + }, [transcriptId, !api, topicId]); + + return { response, loading, error } as + | ErrorTopicWithWords + | LoadingTopicWithWords + | SuccessTopicWithWords; +}; + +export default useTopicWithWords; diff --git a/www/app/[domain]/transcripts/useTopics.ts b/www/app/[domain]/transcripts/useTopics.ts index 4f640024..c5cbce55 100644 --- a/www/app/[domain]/transcripts/useTopics.ts +++ b/www/app/[domain]/transcripts/useTopics.ts @@ -14,7 +14,7 @@ type TranscriptTopics = { const useTopics = (id: string): TranscriptTopics => { const [topics, setTopics] = useState(null); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); const [error, setErrorState] = useState(null); const { setError } = useError(); const api = getApi();