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();