mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
start participants, fix selection
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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)} :
|
||||
</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>
|
||||
))}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user