start participants, fix selection

This commit is contained in:
Sara
2023-12-11 17:15:59 +01:00
parent f1964b0542
commit 6d44cdab9f
5 changed files with 269 additions and 119 deletions

View File

@@ -1,11 +1,13 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import useTranscript from "../../useTranscript";
import TopicHeader from "./topicHeader";
import TopicWords from "./topicWords";
import TopicPlayer from "./topicPlayer";
import getApi from "../../../../lib/getApi";
import useParticipants from "../../useParticipants";
import useTopicWithWords from "../../useTopicWithWords";
import ParticipantList from "./participantList";
type TranscriptCorrect = {
params: {
@@ -22,6 +24,8 @@ export default function TranscriptCorrect(details: TranscriptCorrect) {
const transcriptId = details.params.transcriptId;
const transcript = useTranscript(transcriptId);
const [currentTopic, setCurrentTopic] = useState("");
const topicWithWords = useTopicWithWords(currentTopic, transcriptId);
const [selectedTime, setSelectedTime] = useState<TimeSlice>();
const [topicTime, setTopicTime] = useState<TimeSlice>();
const api = getApi();
@@ -36,13 +40,7 @@ export default function TranscriptCorrect(details: TranscriptCorrect) {
// -> remove time calculation and setting from TopicHeader
// -> pass in topicTime to player directly
// Should we have participants by default, one for each speaker ?
const createParticipant = () => {
api?.v1TranscriptAddParticipant({
createParticipant: { name: "Samantha" },
transcriptId,
});
};
// Creating a participant and a speaker ?
return (
<div className="h-full grid grid-cols-2 gap-4">
@@ -54,20 +52,22 @@ export default function TranscriptCorrect(details: TranscriptCorrect) {
/>
<TopicWords
setSelectedTime={setSelectedTime}
currentTopic={currentTopic}
transcriptId={transcriptId}
selectedTime={selectedTime}
setTopicTime={setTopicTime}
stateSelectedSpeaker={stateSelectedSpeaker}
participants={participants}
topicWithWords={topicWithWords}
/>
</div>
<div>
<div className="flex flex-col justify-stretch">
<TopicPlayer
transcriptId={transcriptId}
selectedTime={selectedTime}
topicTime={topicTime}
/>
<button onClick={createParticipant}>Create</button>
<ParticipantList
{...{ transcriptId, participants, selectedTime, topicWithWords }}
/>
</div>
</div>
);

View File

@@ -0,0 +1,117 @@
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useEffect, useRef, useState } from "react";
import { Participant } from "../../../../api";
import getApi from "../../../../lib/getApi";
const ParticipantList = ({
transcriptId,
participants,
selectedTime,
topicWithWords,
}) => {
const api = getApi();
const [loading, setLoading] = useState(false);
const [participantInput, setParticipantInput] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const createParticipant = () => {
if (!loading) {
setLoading(true);
api
?.v1TranscriptAddParticipant({
createParticipant: { name: participantInput, speaker: 99 },
transcriptId,
})
.then((participant) => {
participants.refetch();
assignTo(participant)();
});
}
};
useEffect(() => {
if (loading) {
setLoading(false);
}
}, [participants.loading]);
useEffect(() => {
if (selectedTime) {
inputRef.current?.focus();
}
}, [selectedTime]);
const deleteParticipant = (participantId) => () => {
if (!loading) {
api
?.v1TranscriptDeleteParticipant({
transcriptId,
participantId,
})
.then(() => {
participants.refetch();
});
}
};
const assignTo =
(participant) => (e?: React.MouseEvent<HTMLButtonElement>) => {
e?.preventDefault();
e?.stopPropagation();
if (selectedTime?.start == undefined || selectedTime?.end == undefined)
return;
api
?.v1TranscriptAssignSpeaker({
speakerAssignment: {
speaker: participant.speaker,
timestampFrom: selectedTime.start,
timestampTo: selectedTime.end,
},
transcriptId,
})
.then(() => {
topicWithWords.refetch();
});
};
return (
<>
<input
ref={inputRef}
onChange={(e) => setParticipantInput(e.target.value)}
/>
<button onClick={createParticipant}>Create</button>
{participants.loading && (
<FontAwesomeIcon
icon={faSpinner}
className="animate-spin-slow text-gray-300 h-8"
/>
)}
{participants.response && (
<ul>
{participants.response.map((participant: Participant) => (
<li className="flex flex-row justify-between" key={participant.id}>
<span>{participant.name}</span>
<div>
<button
className={
selectedTime && !loading ? "bg-blue-400" : "bg-gray-400"
}
onClick={assignTo(participant)}
>
Assign
</button>
<button onClick={deleteParticipant(participant.id)}>
Delete
</button>
</div>
</li>
))}
</ul>
)}
</>
);
};
export default ParticipantList;

View File

@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from "react";
import useTopicWithWords from "../../useTopicWithWords";
import { SetStateAction, useCallback, useEffect, useState } from "react";
import WaveformLoading from "../../waveformLoading";
import { UseParticipants } from "../../useParticipants";
import { Participant } from "../../../../api";
type Word = {
end: number;
@@ -11,19 +12,26 @@ type Word = {
type WordBySpeaker = { speaker: number; words: Word[] }[];
// TODO fix selection reversed
// TODO shortcuts
// TODO fix key (using indexes might act up, not sure as we don't re-order per say)
type TopicWordsProps = {
setSelectedTime: SetStateAction<any>;
selectedTime: any;
setTopicTime: SetStateAction<any>;
stateSelectedSpeaker: any;
participants: UseParticipants;
topicWithWords: any;
};
const topicWords = ({
setSelectedTime,
currentTopic,
transcriptId,
selectedTime,
setTopicTime,
stateSelectedSpeaker,
participants,
}) => {
const topicWithWords = useTopicWithWords(currentTopic, transcriptId);
topicWithWords,
}: TopicWordsProps) => {
const [wordsBySpeaker, setWordsBySpeaker] = useState<WordBySpeaker>();
const [selectedSpeaker, setSelectedSpeaker] = stateSelectedSpeaker;
@@ -31,6 +39,7 @@ const topicWords = ({
if (topicWithWords.loading) {
setWordsBySpeaker([]);
setSelectedTime(undefined);
console.log("unsetting topic changed");
}
}, [topicWithWords.loading]);
@@ -54,98 +63,92 @@ const topicWords = ({
}
}, [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;
const onMouseUp = (e) => {
let selection = window.getSelection();
if (
selection &&
selection.anchorNode &&
selection.focusNode &&
selection.anchorNode == selection.focusNode &&
selection.anchorOffset == selection.focusOffset
) {
setSelectedTime(undefined);
selection.empty();
return;
}
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 focusNode = selection.focusNode;
const focusIsWord = !!selection.focusNode.parentElement?.dataset["end"];
if (
correctedAnchor &&
anchorOffset !== undefined &&
correctedfocus &&
focusOffset !== undefined
) {
selection.setBaseAndExtent(
correctedAnchor,
anchorOffset,
correctedfocus,
focusOffset,
);
if (
!anchorIsWord &&
!focusIsWord &&
anchorNode.parentElement == focusNode.parentElement
) {
console.log(focusNode.parentElement?.dataset);
setSelectedSpeaker(focusNode.parentElement?.dataset["speaker"]);
setSelectedTime(undefined);
} else {
setSelectedSpeaker(undefined);
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 selected a speaker :
if (
selection &&
selection.anchorNode &&
selection.focusNode &&
selection.anchorNode == selection.focusNode &&
selection.anchorOffset == selection.focusOffset
!anchorIsWord &&
!focusIsWord &&
anchorNode.parentElement == focusNode.parentElement
) {
setSelectedSpeaker(focusNode.parentElement?.dataset["speaker"]);
setSelectedTime(undefined);
console.log("Unset Time : selected Speaker");
return;
}
};
}, []);
const getSpeakerName = useCallback(
(speakerNumber: number) => {
return (
participants.response.find((participant) => {
participant.speaker == speakerNumber;
}) || `Speaker ${speakerNumber}`
);
},
[participants],
);
const anchorStart = anchorIsWord
? anchorNode.parentElement?.dataset["start"]
: (selection.anchorNode.parentElement?.nextElementSibling as any)
?.dataset["start"];
const focusEnd =
selection.focusNode.parentElement?.dataset["end"] ||
(
selection.focusNode.parentElement?.parentElement
?.previousElementSibling?.lastElementChild as any
)?.dataset["end"];
if (!topicWithWords.loading && wordsBySpeaker && participants) {
const reverse = anchorStart > focusEnd;
setSelectedTime(undefined);
if (!reverse) {
setSelectedTime({ start: anchorStart, end: focusEnd });
console.log("setting right");
} else {
const anchorEnd = anchorIsWord
? anchorNode.parentElement?.dataset["end"]
: (selection.anchorNode.parentElement?.nextElementSibling as any)
?.dataset["end"];
const focusStart =
selection.focusNode.parentElement?.dataset["start"] ||
(
selection.focusNode.parentElement?.parentElement
?.previousElementSibling?.lastElementChild as any
)?.dataset["start"];
setSelectedTime({ start: focusStart, end: anchorEnd });
console.log("setting reverse");
}
setSelectedSpeaker();
selection.empty();
}
};
const getSpeakerName = (speakerNumber: number) => {
if (!participants.response) return;
return (
<div>
(participants.response as Participant[]).find(
(participant) => participant.speaker == speakerNumber,
)?.name || `Speaker ${speakerNumber}`
);
};
if (!topicWithWords.loading && wordsBySpeaker && participants.response) {
return (
<div onMouseUp={onMouseUp} onBlur={(e) => console.log(e)}>
{wordsBySpeaker?.map((speakerWithWords, index) => (
<p key={index}>
<span
@@ -159,7 +162,18 @@ const topicWords = ({
{getSpeakerName(speakerWithWords.speaker)}&nbsp;:&nbsp;
</span>
{speakerWithWords.words.map((word, index) => (
<span data-start={word.start} data-end={word.end} key={index}>
<span
data-start={word.start}
data-end={word.end}
key={index}
className={
selectedTime &&
selectedTime.start <= word.start &&
selectedTime.end >= word.end
? "bg-yellow-200"
: ""
}
>
{word.text}
</span>
))}

View File

@@ -19,18 +19,29 @@ type LoadingParticipants = {
type SuccessParticipants = {
response: Participant[];
loading: false;
loading: boolean;
error: null;
};
const useParticipants = (
transcriptId: string,
): ErrorParticipants | LoadingParticipants | SuccessParticipants => {
export type UseParticipants = (
| ErrorParticipants
| LoadingParticipants
| SuccessParticipants
) & { refetch: () => void };
const useParticipants = (transcriptId: string): UseParticipants => {
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();
const [count, setCount] = useState(0);
const refetch = () => {
setCount(count + 1);
setLoading(true);
setErrorState(null);
};
useEffect(() => {
if (!transcriptId || !api) return;
@@ -55,12 +66,9 @@ const useParticipants = (
}
setErrorState(error);
});
}, [transcriptId, !api]);
}, [transcriptId, !api, count]);
return { response, loading, error } as
| ErrorParticipants
| LoadingParticipants
| SuccessParticipants;
return { response, loading, error, refetch } as UseParticipants;
};
export default useParticipants;

View File

@@ -23,16 +23,30 @@ type SuccessTopicWithWords = {
error: null;
};
type UseTopicWithWords = { refetch: () => void } & (
| ErrorTopicWithWords
| LoadingTopicWithWords
| SuccessTopicWithWords
);
const useTopicWithWords = (
topicId: string | null,
transcriptId: string,
): ErrorTopicWithWords | LoadingTopicWithWords | SuccessTopicWithWords => {
): UseTopicWithWords => {
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();
const [count, setCount] = useState(0);
const refetch = () => {
setCount(count + 1);
setLoading(true);
setErrorState(null);
};
useEffect(() => {
setLoading(true);
}, [transcriptId, topicId]);
@@ -60,12 +74,9 @@ const useTopicWithWords = (
}
setErrorState(error);
});
}, [transcriptId, !api, topicId]);
}, [transcriptId, !api, topicId, count]);
return { response, loading, error } as
| ErrorTopicWithWords
| LoadingTopicWithWords
| SuccessTopicWithWords;
return { response, loading, error, refetch } as UseTopicWithWords;
};
export default useTopicWithWords;