mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
correct page and word selection
This commit is contained in:
39
www/app/[domain]/transcripts/[transcriptId]/correct/page.tsx
Normal file
39
www/app/[domain]/transcripts/[transcriptId]/correct/page.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="h-full grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<TopicHeader
|
||||||
|
currentTopic={currentTopic}
|
||||||
|
setCurrentTopic={setCurrentTopic}
|
||||||
|
transcriptId={transcriptId}
|
||||||
|
/>
|
||||||
|
<TopicWords
|
||||||
|
setSelectedTime={setSelectedTime}
|
||||||
|
currentTopic={currentTopic}
|
||||||
|
transcriptId={transcriptId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<button
|
||||||
|
className={`w-10 h-10 rounded-full p-2 border border-gray-300 disabled:bg-white ${
|
||||||
|
canGoPrevious ? "text-gray-500" : "text-gray-300"
|
||||||
|
}`}
|
||||||
|
onClick={onPrev}
|
||||||
|
disabled={!canGoPrevious}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faArrowLeft} />
|
||||||
|
</button>
|
||||||
|
<h1 className="flex-grow">
|
||||||
|
{title}{" "}
|
||||||
|
<span>
|
||||||
|
{number + 1}/{total}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
className={`w-10 h-10 rounded-full p-2 border border-gray-300 disabled:bg-white ${
|
||||||
|
canGoNext ? "text-gray-500" : "text-gray-300"
|
||||||
|
}`}
|
||||||
|
onClick={onNext}
|
||||||
|
disabled={!canGoNext}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faArrowRight} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -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<WordBySpeaker>();
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
{wordsBySpeaker?.map((speakerWithWords) => (
|
||||||
|
<p>
|
||||||
|
<span>Speaker {speakerWithWords.speaker} : </span>
|
||||||
|
{speakerWithWords.words.map((word) => (
|
||||||
|
<span data-start={word.start} data-end={word.end}>
|
||||||
|
{word.text}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (topicWithWords.loading) return <WaveformLoading />;
|
||||||
|
if (topicWithWords.error) return <p>error</p>;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default topicWords;
|
||||||
71
www/app/[domain]/transcripts/useTopicWithWords.ts
Normal file
71
www/app/[domain]/transcripts/useTopicWithWords.ts
Normal file
@@ -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<GetTranscript | null>(null);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
const [error, setErrorState] = useState<Error | null>(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;
|
||||||
@@ -14,7 +14,7 @@ type TranscriptTopics = {
|
|||||||
|
|
||||||
const useTopics = (id: string): TranscriptTopics => {
|
const useTopics = (id: string): TranscriptTopics => {
|
||||||
const [topics, setTopics] = useState<Topic[] | null>(null);
|
const [topics, setTopics] = useState<Topic[] | null>(null);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setErrorState] = useState<Error | null>(null);
|
const [error, setErrorState] = useState<Error | null>(null);
|
||||||
const { setError } = useError();
|
const { setError } = useError();
|
||||||
const api = getApi();
|
const api = getApi();
|
||||||
|
|||||||
Reference in New Issue
Block a user