mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
error handling and clean up
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
20
www/app/[domain]/transcripts/[transcriptId]/correct/types.ts
Normal file
20
www/app/[domain]/transcripts/[transcriptId]/correct/types.ts
Normal 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"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user