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

View File

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

View File

@@ -2,8 +2,19 @@ import { useEffect, useRef, useState } from "react";
import useMp3 from "../../useMp3";
import { formatTime } from "../../../../lib/time";
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 [isPlaying, setIsPlaying] = useState(false);
const [endTopicCallback, setEndTopicCallback] = useState<() => void>();
@@ -56,11 +67,7 @@ const TopicPlayer = ({ transcriptId, selectedTime, topicTime }) => {
setEndTopicCallback(
() =>
function () {
if (
mp3.media &&
topicTime.end &&
mp3.media.currentTime >= topicTime.end
) {
if (mp3.media && mp3.media.currentTime >= topicTime.end) {
mp3.media.pause();
setIsPlaying(false);
mp3.media.currentTime = topicTime.start;
@@ -68,7 +75,7 @@ const TopicPlayer = ({ transcriptId, selectedTime, topicTime }) => {
}
},
);
if (mp3.media && topicTime) {
if (mp3.media) {
playButton.current?.focus();
mp3.media?.pause();
// 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);
setIsPlaying(false);
}
}, [topicTime, mp3.media]);
}, [!mp3.media, topicTime.start, topicTime.end]);
useEffect(() => {
endTopicCallback &&
@@ -164,11 +171,9 @@ const TopicPlayer = ({ transcriptId, selectedTime, topicTime }) => {
<div className="mb-4 grid grid-cols-3 gap-2">
<SoundWaveCss playing={isPlaying} />
<div className="col-span-2">{showTime}</div>
{topicTime && (
<button className="p-2 bg-blue-200 w-full" onClick={playTopic}>
Play From Start
</button>
)}
<button className="p-2 bg-blue-200 w-full" onClick={playTopic}>
Play From Start
</button>
{!isPlaying ? (
<button
ref={playButton}

View File

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