error handling and clean up

This commit is contained in:
Sara
2023-12-13 12:49:36 +01:00
parent 151ba0bb4e
commit b4322c22cc
5 changed files with 183 additions and 150 deletions

View File

@@ -1,6 +1,5 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useState } from "react";
import useTranscript from "../../useTranscript";
import TopicHeader from "./topicHeader"; import TopicHeader from "./topicHeader";
import TopicWords from "./topicWords"; import TopicWords from "./topicWords";
import TopicPlayer from "./topicPlayer"; import TopicPlayer from "./topicPlayer";
@@ -8,6 +7,7 @@ import useParticipants from "../../useParticipants";
import useTopicWithWords from "../../useTopicWithWords"; import useTopicWithWords from "../../useTopicWithWords";
import ParticipantList from "./participantList"; import ParticipantList from "./participantList";
import { GetTranscriptTopic } from "../../../../api"; import { GetTranscriptTopic } from "../../../../api";
import { SelectedText, selectedTextIsTimeSlice } from "./types";
export type TranscriptCorrect = { export type TranscriptCorrect = {
params: { params: {
@@ -15,48 +15,15 @@ export type TranscriptCorrect = {
}; };
}; };
export type TimeSlice = { export default function TranscriptCorrect({
start: number; params: { transcriptId },
end: number; }: TranscriptCorrect) {
};
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 stateCurrentTopic = useState<GetTranscriptTopic>(); const stateCurrentTopic = useState<GetTranscriptTopic>();
const [currentTopic, _sct] = stateCurrentTopic; const [currentTopic, _sct] = stateCurrentTopic;
const topicWithWords = useTopicWithWords(currentTopic?.id, transcriptId);
const [topicTime, setTopicTime] = useState<TimeSlice>();
const participants = useParticipants(transcriptId);
const stateSelectedText = useState<SelectedText>(); const stateSelectedText = useState<SelectedText>();
const [selectedText, _sst] = stateSelectedText; const [selectedText, _sst] = stateSelectedText;
const topicWithWords = useTopicWithWords(currentTopic?.id, transcriptId);
useEffect(() => { const participants = useParticipants(transcriptId);
if (currentTopic) {
setTopicTime({
start: currentTopic.timestamp,
end: currentTopic.timestamp + currentTopic.duration,
});
} else {
setTopicTime(undefined);
}
}, [currentTopic]);
return ( return (
<div className="h-full grid grid-cols-2 gap-4"> <div className="h-full grid grid-cols-2 gap-4">
@@ -73,21 +40,30 @@ export default function TranscriptCorrect(details: TranscriptCorrect) {
/> />
</div> </div>
<div className="flex flex-col justify-stretch"> <div className="flex flex-col justify-stretch">
<TopicPlayer {currentTopic ? (
transcriptId={transcriptId} <TopicPlayer
selectedTime={ transcriptId={transcriptId}
selectedTextIsTimeSlice(selectedText) ? selectedText : undefined selectedTime={
} selectedTextIsTimeSlice(selectedText) ? selectedText : undefined
topicTime={topicTime} }
/> topicTime={{
<ParticipantList start: currentTopic?.timestamp,
{...{ end: currentTopic?.timestamp + currentTopic?.duration,
transcriptId, }}
participants, />
topicWithWords, ) : (
stateSelectedText, <div></div>
}} )}
/> {participants.response && (
<ParticipantList
{...{
transcriptId,
participants,
topicWithWords,
stateSelectedText,
}}
/>
)}
</div> </div>
</div> </div>
); );

View File

@@ -4,7 +4,8 @@ import { ChangeEvent, useEffect, useRef, useState } from "react";
import { Participant } from "../../../../api"; import { Participant } from "../../../../api";
import getApi from "../../../../lib/getApi"; import getApi from "../../../../lib/getApi";
import { UseParticipants } from "../../useParticipants"; import { UseParticipants } from "../../useParticipants";
import { selectedTextIsSpeaker, selectedTextIsTimeSlice } from "./page"; import { selectedTextIsSpeaker, selectedTextIsTimeSlice } from "./types";
import { useError } from "../../../../(errors)/errorContext";
type ParticipantList = { type ParticipantList = {
participants: UseParticipants; participants: UseParticipants;
@@ -13,6 +14,7 @@ type ParticipantList = {
stateSelectedText: any; stateSelectedText: any;
}; };
// NTH re-order list when searching // NTH re-order list when searching
// HTH case-insensitive matching
const ParticipantList = ({ const ParticipantList = ({
transcriptId, transcriptId,
participants, participants,
@@ -20,7 +22,7 @@ const ParticipantList = ({
stateSelectedText, stateSelectedText,
}: ParticipantList) => { }: ParticipantList) => {
const api = getApi(); const api = getApi();
const { setError } = useError();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [participantInput, setParticipantInput] = useState(""); const [participantInput, setParticipantInput] = useState("");
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@@ -40,40 +42,42 @@ const ParticipantList = ({
); );
if (participant) { if (participant) {
setParticipantInput(participant.name); setParticipantInput(participant.name);
setOneMatch(undefined);
setSelectedParticipant(participant); setSelectedParticipant(participant);
inputRef.current?.focus();
setAction("Rename"); setAction("Rename");
} else if (!selectedParticipant) { } else if (!selectedParticipant) {
setSelectedParticipant(undefined); setSelectedParticipant(undefined);
setParticipantInput(""); setParticipantInput("");
inputRef.current?.focus(); setOneMatch(undefined);
setAction("Create to rename"); setAction("Create to rename");
} }
} }
if (selectedTextIsTimeSlice(selectedText)) { if (selectedTextIsTimeSlice(selectedText)) {
setParticipantInput("");
inputRef.current?.focus(); inputRef.current?.focus();
setParticipantInput("");
setOneMatch(undefined);
setAction("Create and assign"); setAction("Create and assign");
setSelectedParticipant(undefined); setSelectedParticipant(undefined);
} }
if (typeof selectedText == undefined) { if (typeof selectedText == undefined) {
inputRef.current?.blur();
setAction(null); setAction(null);
} }
} }
}, [selectedText, participants]); }, [selectedText, participants.response]);
useEffect(() => { useEffect(() => {
document.onkeyup = (e) => { document.onkeyup = (e) => {
if (loading || participants.loading || topicWithWords.loading) return; if (loading || participants.loading || topicWithWords.loading) return;
if (e.key === "Enter" && e.ctrlKey) { if (e.key === "Enter" && e.ctrlKey) {
if (oneMatch) { if (oneMatch) {
if (action == "Create and assign") { if (
assignTo(oneMatch)(); action == "Create and assign" &&
setOneMatch(undefined); selectedTextIsTimeSlice(selectedText)
setParticipantInput(""); ) {
assignTo(oneMatch)().catch(() => {});
} else if ( } else if (
action == "Create to rename" && action == "Create to rename" &&
oneMatch &&
selectedTextIsSpeaker(selectedText) selectedTextIsSpeaker(selectedText)
) { ) {
mergeSpeaker(selectedText, oneMatch)(); mergeSpeaker(selectedText, oneMatch)();
@@ -85,33 +89,78 @@ const ParticipantList = ({
}; };
}); });
const onSuccess = () => {
topicWithWords.refetch();
participants.refetch();
setLoading(false);
setAction(null);
setSelectedText(undefined);
setSelectedParticipant(undefined);
setParticipantInput("");
setOneMatch(undefined);
inputRef?.current?.blur();
};
const assignTo =
(participant) => async (e?: React.MouseEvent<HTMLButtonElement>) => {
e?.preventDefault();
e?.stopPropagation();
if (loading || participants.loading || topicWithWords.loading) return;
if (!selectedTextIsTimeSlice(selectedText)) return;
setLoading(true);
try {
await api?.v1TranscriptAssignSpeaker({
speakerAssignment: {
participant: participant.id,
timestampFrom: selectedText.start,
timestampTo: selectedText.end,
},
transcriptId,
});
onSuccess();
} catch (error) {
setError(error, "There was an error assigning");
setLoading(false);
throw error;
}
};
const mergeSpeaker = const mergeSpeaker =
(speakerFrom, participantTo: Participant) => async () => { (speakerFrom, participantTo: Participant) => async () => {
if (loading || participants.loading || topicWithWords.loading) return; if (loading || participants.loading || topicWithWords.loading) return;
setLoading(true);
if (participantTo.speaker) { if (participantTo.speaker) {
setLoading(true); try {
await api?.v1TranscriptMergeSpeaker({ await api?.v1TranscriptMergeSpeaker({
transcriptId, transcriptId,
speakerMerge: { speakerMerge: {
speakerFrom: speakerFrom, speakerFrom: speakerFrom,
speakerTo: participantTo.speaker, speakerTo: participantTo.speaker,
}, },
}); });
onSuccess();
} catch (error) {
setError(error, "There was an error merging");
setLoading(false);
}
} else { } else {
await api?.v1TranscriptUpdateParticipant({ try {
transcriptId, await api?.v1TranscriptUpdateParticipant({
participantId: participantTo.id, transcriptId,
updateParticipant: { speaker: speakerFrom }, participantId: participantTo.id,
}); updateParticipant: { speaker: speakerFrom },
});
onSuccess();
} catch (error) {
setError(error, "There was an error merging (update)");
setLoading(false);
}
} }
participants.refetch();
topicWithWords.refetch();
setAction(null);
setParticipantInput("");
setLoading(false);
}; };
const doAction = (e?) => { const doAction = async (e?) => {
e?.preventDefault(); e?.preventDefault();
e?.stopPropagation(); e?.stopPropagation();
if ( if (
@@ -138,6 +187,10 @@ const ParticipantList = ({
.then(() => { .then(() => {
participants.refetch(); participants.refetch();
setLoading(false); setLoading(false);
})
.catch((e) => {
setError(e, "There was an error renaming");
setLoading(false);
}); });
} }
} else if ( } else if (
@@ -156,6 +209,11 @@ const ParticipantList = ({
.then(() => { .then(() => {
participants.refetch(); participants.refetch();
setParticipantInput(""); setParticipantInput("");
setOneMatch(undefined);
setLoading(false);
})
.catch((e) => {
setError(e, "There was an error creating");
setLoading(false); setLoading(false);
}); });
} else if ( } else if (
@@ -163,18 +221,22 @@ const ParticipantList = ({
selectedTextIsTimeSlice(selectedText) selectedTextIsTimeSlice(selectedText)
) { ) {
setLoading(true); setLoading(true);
api try {
?.v1TranscriptAddParticipant({ const participant = await api?.v1TranscriptAddParticipant({
createParticipant: { createParticipant: {
name: participantInput, name: participantInput,
}, },
transcriptId, transcriptId,
})
.then((participant) => {
setLoading(false);
assignTo(participant)();
setParticipantInput("");
}); });
setLoading(false);
assignTo(participant)().catch(() => {
// error and loading are handled by assignTo catch
participants.refetch();
});
} catch (error) {
setError(e, "There was an error creating");
setLoading(false);
}
} else if (action == "Create") { } else if (action == "Create") {
setLoading(true); setLoading(true);
api api
@@ -188,6 +250,10 @@ const ParticipantList = ({
participants.refetch(); participants.refetch();
setParticipantInput(""); setParticipantInput("");
setLoading(false); setLoading(false);
})
.catch((e) => {
setError(e, "There was an error creating");
setLoading(false);
}); });
} }
}; };
@@ -204,41 +270,19 @@ const ParticipantList = ({
.then(() => { .then(() => {
participants.refetch(); participants.refetch();
setLoading(false); setLoading(false);
})
.catch((e) => {
setError(e, "There was an error deleting");
setLoading(false);
}); });
}; };
const assignTo =
(participant) => (e?: React.MouseEvent<HTMLButtonElement>) => {
e?.preventDefault();
e?.stopPropagation();
if (loading || participants.loading || topicWithWords.loading) return;
if (!selectedTextIsTimeSlice(selectedText)) return;
setLoading(true);
api
?.v1TranscriptAssignSpeaker({
speakerAssignment: {
participant: participant.id,
timestampFrom: selectedText.start,
timestampTo: selectedText.end,
},
transcriptId,
})
.then(() => {
topicWithWords.refetch();
participants.refetch();
setLoading(false);
setAction(null);
setSelectedText(undefined);
setSelectedParticipant(undefined);
});
};
const selectParticipant = (participant) => (e) => { const selectParticipant = (participant) => (e) => {
setSelectedParticipant(participant); setSelectedParticipant(participant);
setSelectedText(participant.speaker); setSelectedText(participant.speaker);
setAction("Rename"); setAction("Rename");
setParticipantInput(participant.name); setParticipantInput(participant.name);
oneMatch && setOneMatch(undefined);
}; };
const clearSelection = () => { const clearSelection = () => {
@@ -246,6 +290,7 @@ const ParticipantList = ({
setSelectedText(undefined); setSelectedText(undefined);
setAction(null); setAction(null);
setParticipantInput(""); setParticipantInput("");
oneMatch && setOneMatch(undefined);
}; };
const preventClick = (e) => { const preventClick = (e) => {
e?.stopPropagation(); e?.stopPropagation();

View File

@@ -2,8 +2,19 @@ import { useEffect, useRef, useState } from "react";
import useMp3 from "../../useMp3"; import useMp3 from "../../useMp3";
import { formatTime } from "../../../../lib/time"; import { formatTime } from "../../../../lib/time";
import SoundWaveCss from "./soundWaveCss"; import SoundWaveCss from "./soundWaveCss";
import { TimeSlice } from "./types";
const TopicPlayer = ({ transcriptId, selectedTime, topicTime }) => { type TopicPlayer = {
transcriptId: string;
selectedTime: TimeSlice | undefined;
topicTime: TimeSlice;
};
const TopicPlayer = ({
transcriptId,
selectedTime,
topicTime,
}: TopicPlayer) => {
const mp3 = useMp3(transcriptId); const mp3 = useMp3(transcriptId);
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [endTopicCallback, setEndTopicCallback] = useState<() => void>(); const [endTopicCallback, setEndTopicCallback] = useState<() => void>();
@@ -56,11 +67,7 @@ const TopicPlayer = ({ transcriptId, selectedTime, topicTime }) => {
setEndTopicCallback( setEndTopicCallback(
() => () =>
function () { function () {
if ( if (mp3.media && mp3.media.currentTime >= topicTime.end) {
mp3.media &&
topicTime.end &&
mp3.media.currentTime >= topicTime.end
) {
mp3.media.pause(); mp3.media.pause();
setIsPlaying(false); setIsPlaying(false);
mp3.media.currentTime = topicTime.start; mp3.media.currentTime = topicTime.start;
@@ -68,7 +75,7 @@ const TopicPlayer = ({ transcriptId, selectedTime, topicTime }) => {
} }
}, },
); );
if (mp3.media && topicTime) { if (mp3.media) {
playButton.current?.focus(); playButton.current?.focus();
mp3.media?.pause(); mp3.media?.pause();
// there's no callback on pause but apparently changing the time while palying doesn't work... so here is a timeout // there's no callback on pause but apparently changing the time while palying doesn't work... so here is a timeout
@@ -80,7 +87,7 @@ const TopicPlayer = ({ transcriptId, selectedTime, topicTime }) => {
}, 10); }, 10);
setIsPlaying(false); setIsPlaying(false);
} }
}, [topicTime, mp3.media]); }, [!mp3.media, topicTime.start, topicTime.end]);
useEffect(() => { useEffect(() => {
endTopicCallback && endTopicCallback &&
@@ -164,11 +171,9 @@ const TopicPlayer = ({ transcriptId, selectedTime, topicTime }) => {
<div className="mb-4 grid grid-cols-3 gap-2"> <div className="mb-4 grid grid-cols-3 gap-2">
<SoundWaveCss playing={isPlaying} /> <SoundWaveCss playing={isPlaying} />
<div className="col-span-2">{showTime}</div> <div className="col-span-2">{showTime}</div>
{topicTime && ( <button className="p-2 bg-blue-200 w-full" onClick={playTopic}>
<button className="p-2 bg-blue-200 w-full" onClick={playTopic}> Play From Start
Play From Start </button>
</button>
)}
{!isPlaying ? ( {!isPlaying ? (
<button <button
ref={playButton} ref={playButton}

View File

@@ -1,22 +1,8 @@
import { import { Dispatch, SetStateAction, useEffect } from "react";
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from "react";
import WaveformLoading from "../../waveformLoading"; import WaveformLoading from "../../waveformLoading";
import { UseParticipants } from "../../useParticipants"; import { UseParticipants } from "../../useParticipants";
import { Participant } from "../../../../api";
import { UseTopicWithWords } from "../../useTopicWithWords"; import { UseTopicWithWords } from "../../useTopicWithWords";
import { import { TimeSlice, selectedTextIsTimeSlice } from "./types";
TimeSlice,
selectedTextIsSpeaker,
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 = { type TopicWordsProps = {
stateSelectedText: [ stateSelectedText: [
@@ -199,8 +185,9 @@ const topicWords = ({
</div> </div>
); );
} }
if (topicWithWords.loading) return <WaveformLoading />; if (topicWithWords.loading || participants.loading)
if (topicWithWords.error) return <p>error</p>; return <WaveformLoading />;
if (topicWithWords.error || participants.error) return <p>error</p>;
return null; return null;
}; };

View File

@@ -0,0 +1,20 @@
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"
);
}