mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
changes types, assign to participant instead of speaker
This commit is contained in:
@@ -1,59 +1,77 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import useTranscript from "../../useTranscript";
|
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";
|
||||||
import getApi from "../../../../lib/getApi";
|
|
||||||
import useParticipants from "../../useParticipants";
|
import useParticipants from "../../useParticipants";
|
||||||
import useTopicWithWords from "../../useTopicWithWords";
|
import useTopicWithWords from "../../useTopicWithWords";
|
||||||
import ParticipantList from "./participantList";
|
import ParticipantList from "./participantList";
|
||||||
|
import { GetTranscriptTopic } from "../../../../api";
|
||||||
|
|
||||||
type TranscriptCorrect = {
|
export type TranscriptCorrect = {
|
||||||
params: {
|
params: {
|
||||||
transcriptId: string;
|
transcriptId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type TimeSlice = {
|
export type TimeSlice = {
|
||||||
start: number;
|
start: number;
|
||||||
end: 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) {
|
export default function TranscriptCorrect(details: TranscriptCorrect) {
|
||||||
const transcriptId = details.params.transcriptId;
|
const transcriptId = details.params.transcriptId;
|
||||||
const transcript = useTranscript(transcriptId);
|
const transcript = useTranscript(transcriptId);
|
||||||
const [currentTopic, setCurrentTopic] = useState("");
|
const stateCurrentTopic = useState<GetTranscriptTopic>();
|
||||||
const topicWithWords = useTopicWithWords(currentTopic, transcriptId);
|
const [currentTopic, _sct] = stateCurrentTopic;
|
||||||
|
const topicWithWords = useTopicWithWords(currentTopic?.id, transcriptId);
|
||||||
|
|
||||||
const [selectedTime, setSelectedTime] = useState<TimeSlice>();
|
|
||||||
const [topicTime, setTopicTime] = useState<TimeSlice>();
|
const [topicTime, setTopicTime] = useState<TimeSlice>();
|
||||||
const participants = useParticipants(transcriptId);
|
const participants = useParticipants(transcriptId);
|
||||||
const stateSelectedSpeaker = useState<number>();
|
const stateSelectedText = useState<SelectedText>();
|
||||||
|
const [selectedText, _sst] = stateSelectedText;
|
||||||
|
console.log(selectedText);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentTopic) {
|
||||||
|
setTopicTime({
|
||||||
|
start: currentTopic.timestamp,
|
||||||
|
end: currentTopic.timestamp + currentTopic.duration,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTopicTime(undefined);
|
||||||
|
}
|
||||||
|
}, [currentTopic]);
|
||||||
|
|
||||||
// TODO BE
|
// TODO BE
|
||||||
// Get one topic with words
|
|
||||||
// -> fix useTopicWithWords.ts
|
|
||||||
// Add start and end time of each topic in the topic list
|
|
||||||
// -> use full current topic instead of topicId here
|
|
||||||
// -> remove time calculation and setting from TopicHeader
|
|
||||||
// -> pass in topicTime to player directly
|
|
||||||
// Should we have participants by default, one for each speaker ?
|
|
||||||
// Creating a participant and a speaker ?
|
// Creating a participant and a speaker ?
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full grid grid-cols-2 gap-4">
|
<div className="h-full grid grid-cols-2 gap-4">
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<TopicHeader
|
<TopicHeader
|
||||||
currentTopic={currentTopic}
|
stateCurrentTopic={stateCurrentTopic}
|
||||||
setCurrentTopic={setCurrentTopic}
|
|
||||||
transcriptId={transcriptId}
|
transcriptId={transcriptId}
|
||||||
/>
|
/>
|
||||||
<TopicWords
|
<TopicWords
|
||||||
setSelectedTime={setSelectedTime}
|
stateSelectedText={stateSelectedText}
|
||||||
selectedTime={selectedTime}
|
|
||||||
setTopicTime={setTopicTime}
|
|
||||||
stateSelectedSpeaker={stateSelectedSpeaker}
|
|
||||||
participants={participants}
|
participants={participants}
|
||||||
topicWithWords={topicWithWords}
|
topicWithWords={topicWithWords}
|
||||||
/>
|
/>
|
||||||
@@ -61,16 +79,17 @@ export default function TranscriptCorrect(details: TranscriptCorrect) {
|
|||||||
<div className="flex flex-col justify-stretch">
|
<div className="flex flex-col justify-stretch">
|
||||||
<TopicPlayer
|
<TopicPlayer
|
||||||
transcriptId={transcriptId}
|
transcriptId={transcriptId}
|
||||||
selectedTime={selectedTime}
|
selectedTime={
|
||||||
|
selectedTextIsTimeSlice(selectedText) ? selectedText : undefined
|
||||||
|
}
|
||||||
topicTime={topicTime}
|
topicTime={topicTime}
|
||||||
/>
|
/>
|
||||||
<ParticipantList
|
<ParticipantList
|
||||||
{...{
|
{...{
|
||||||
transcriptId,
|
transcriptId,
|
||||||
participants,
|
participants,
|
||||||
selectedTime,
|
|
||||||
topicWithWords,
|
topicWithWords,
|
||||||
stateSelectedSpeaker,
|
stateSelectedText,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,28 +4,27 @@ import { 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";
|
||||||
|
|
||||||
type ParticipantList = {
|
type ParticipantList = {
|
||||||
participants: UseParticipants;
|
participants: UseParticipants;
|
||||||
transcriptId: string;
|
transcriptId: string;
|
||||||
selectedTime: any;
|
|
||||||
topicWithWords: any;
|
topicWithWords: any;
|
||||||
stateSelectedSpeaker: any;
|
stateSelectedText: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ParticipantList = ({
|
const ParticipantList = ({
|
||||||
transcriptId,
|
transcriptId,
|
||||||
participants,
|
participants,
|
||||||
selectedTime,
|
|
||||||
topicWithWords,
|
topicWithWords,
|
||||||
stateSelectedSpeaker,
|
stateSelectedText,
|
||||||
}: ParticipantList) => {
|
}: ParticipantList) => {
|
||||||
const api = getApi();
|
const api = getApi();
|
||||||
|
|
||||||
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);
|
||||||
const [selectedSpeaker, setSelectedSpeaker] = stateSelectedSpeaker;
|
const [selectedText, setSelectedText] = stateSelectedText;
|
||||||
const [selectedParticipant, setSelectedParticipant] = useState<Participant>();
|
const [selectedParticipant, setSelectedParticipant] = useState<Participant>();
|
||||||
const [action, setAction] = useState<
|
const [action, setAction] = useState<
|
||||||
"Create" | "Create to rename" | "Create and assign" | "Rename" | null
|
"Create" | "Create to rename" | "Create and assign" | "Rename" | null
|
||||||
@@ -40,10 +39,10 @@ const ParticipantList = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (participants.response) {
|
if (participants.response) {
|
||||||
if (selectedSpeaker !== undefined) {
|
if (selectedTextIsSpeaker(selectedText)) {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
const participant = participants.response.find(
|
const participant = participants.response.find(
|
||||||
(p) => p.speaker == selectedSpeaker,
|
(p) => p.speaker == selectedText,
|
||||||
);
|
);
|
||||||
if (participant) {
|
if (participant) {
|
||||||
setParticipantInput(participant.name);
|
setParticipantInput(participant.name);
|
||||||
@@ -57,20 +56,23 @@ const ParticipantList = ({
|
|||||||
setAction("Create to rename");
|
setAction("Create to rename");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (selectedTime) {
|
if (selectedTextIsTimeSlice(selectedText)) {
|
||||||
setParticipantInput("");
|
setParticipantInput("");
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
setAction("Create and assign");
|
setAction("Create and assign");
|
||||||
setSelectedParticipant(undefined);
|
setSelectedParticipant(undefined);
|
||||||
}
|
}
|
||||||
if (!selectedTime && !selectedSpeaker) {
|
if (typeof selectedText == undefined) {
|
||||||
setAction(null);
|
setAction(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [selectedTime, selectedSpeaker]);
|
}, [selectedText]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (participants.response && action == "Create and assign") {
|
if (
|
||||||
|
participants.response &&
|
||||||
|
(action == "Create and assign" || action == "Create to rename")
|
||||||
|
) {
|
||||||
if (
|
if (
|
||||||
participants.response.filter((p) => p.name.startsWith(participantInput))
|
participants.response.filter((p) => p.name.startsWith(participantInput))
|
||||||
.length == 1
|
.length == 1
|
||||||
@@ -87,18 +89,23 @@ const ParticipantList = ({
|
|||||||
if (participantInput && !action) {
|
if (participantInput && !action) {
|
||||||
setAction("Create");
|
setAction("Create");
|
||||||
}
|
}
|
||||||
if (!participantInput) {
|
|
||||||
setAction(null);
|
|
||||||
}
|
|
||||||
}, [participantInput]);
|
}, [participantInput]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.onkeyup = (e) => {
|
document.onkeyup = (e) => {
|
||||||
if (e.key === "Enter" && e.ctrlKey) {
|
if (e.key === "Enter" && e.ctrlKey) {
|
||||||
if (oneMatch) {
|
if (oneMatch) {
|
||||||
assignTo(oneMatch)();
|
if (action == "Create and assign") {
|
||||||
setOneMatch(undefined);
|
assignTo(oneMatch)();
|
||||||
setParticipantInput("");
|
setOneMatch(undefined);
|
||||||
|
setParticipantInput("");
|
||||||
|
} else if (
|
||||||
|
action == "Create to rename" &&
|
||||||
|
oneMatch &&
|
||||||
|
selectedTextIsSpeaker(selectedText)
|
||||||
|
) {
|
||||||
|
mergeSpeaker(selectedText, oneMatch)();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (e.key === "Enter") {
|
} else if (e.key === "Enter") {
|
||||||
doAction();
|
doAction();
|
||||||
@@ -106,13 +113,36 @@ const ParticipantList = ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mergeSpeaker =
|
||||||
|
(speakerFrom, participantTo: Participant) => async () => {
|
||||||
|
if (participantTo.speaker) {
|
||||||
|
await api?.v1TranscriptMergeSpeaker({
|
||||||
|
transcriptId,
|
||||||
|
speakerMerge: {
|
||||||
|
speakerFrom: speakerFrom,
|
||||||
|
speakerTo: participantTo.speaker,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await api?.v1TranscriptUpdateParticipant({
|
||||||
|
transcriptId,
|
||||||
|
participantId: participantTo.id,
|
||||||
|
updateParticipant: { speaker: speakerFrom },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
participants.refetch();
|
||||||
|
topicWithWords.refetch();
|
||||||
|
setAction(null);
|
||||||
|
setParticipantInput("");
|
||||||
|
};
|
||||||
|
|
||||||
const doAction = (e?) => {
|
const doAction = (e?) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
if (!participants.response) return;
|
if (!participants.response) return;
|
||||||
if (action == "Rename") {
|
if (action == "Rename" && selectedTextIsSpeaker(selectedText)) {
|
||||||
const participant = participants.response.find(
|
const participant = participants.response.find(
|
||||||
(p) => p.speaker == selectedSpeaker,
|
(p) => p.speaker == selectedText,
|
||||||
);
|
);
|
||||||
if (participant && participant.name !== participantInput) {
|
if (participant && participant.name !== participantInput) {
|
||||||
api
|
api
|
||||||
@@ -127,14 +157,16 @@ const ParticipantList = ({
|
|||||||
participants.refetch();
|
participants.refetch();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (action == "Create to rename") {
|
} else if (
|
||||||
|
action == "Create to rename" &&
|
||||||
|
selectedTextIsSpeaker(selectedText)
|
||||||
|
) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
console.log(participantInput, selectedSpeaker);
|
|
||||||
api
|
api
|
||||||
?.v1TranscriptAddParticipant({
|
?.v1TranscriptAddParticipant({
|
||||||
createParticipant: {
|
createParticipant: {
|
||||||
name: participantInput,
|
name: participantInput,
|
||||||
speaker: selectedSpeaker,
|
speaker: selectedText,
|
||||||
},
|
},
|
||||||
transcriptId,
|
transcriptId,
|
||||||
})
|
})
|
||||||
@@ -142,13 +174,15 @@ const ParticipantList = ({
|
|||||||
participants.refetch();
|
participants.refetch();
|
||||||
setParticipantInput("");
|
setParticipantInput("");
|
||||||
});
|
});
|
||||||
} else if (action == "Create and assign") {
|
} else if (
|
||||||
|
action == "Create and assign" &&
|
||||||
|
selectedTextIsTimeSlice(selectedText)
|
||||||
|
) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
api
|
api
|
||||||
?.v1TranscriptAddParticipant({
|
?.v1TranscriptAddParticipant({
|
||||||
createParticipant: {
|
createParticipant: {
|
||||||
name: participantInput,
|
name: participantInput,
|
||||||
speaker: Math.floor(Math.random() * 100 + 10),
|
|
||||||
},
|
},
|
||||||
transcriptId,
|
transcriptId,
|
||||||
})
|
})
|
||||||
@@ -191,14 +225,14 @@ const ParticipantList = ({
|
|||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
// fix participant that doesnt have a speaker (wait API)
|
// fix participant that doesnt have a speaker (wait API)
|
||||||
if (selectedTime?.start == undefined || selectedTime?.end == undefined)
|
if (!selectedTextIsTimeSlice(selectedText)) return;
|
||||||
return;
|
|
||||||
api
|
api
|
||||||
?.v1TranscriptAssignSpeaker({
|
?.v1TranscriptAssignSpeaker({
|
||||||
speakerAssignment: {
|
speakerAssignment: {
|
||||||
speaker: participant.speaker,
|
participant: participant.id,
|
||||||
timestampFrom: selectedTime.start,
|
timestampFrom: selectedText.start,
|
||||||
timestampTo: selectedTime.end,
|
timestampTo: selectedText.end,
|
||||||
},
|
},
|
||||||
transcriptId,
|
transcriptId,
|
||||||
})
|
})
|
||||||
@@ -209,82 +243,113 @@ const ParticipantList = ({
|
|||||||
|
|
||||||
const selectParticipant = (participant) => (e) => {
|
const selectParticipant = (participant) => (e) => {
|
||||||
setSelectedParticipant(participant);
|
setSelectedParticipant(participant);
|
||||||
setSelectedSpeaker(participant.speaker);
|
setSelectedText(participant.speaker);
|
||||||
setAction("Rename");
|
setAction("Rename");
|
||||||
setParticipantInput(participant.name);
|
setParticipantInput(participant.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearSelection = () => {
|
||||||
|
setSelectedParticipant(undefined);
|
||||||
|
setSelectedText(undefined);
|
||||||
|
setAction(null);
|
||||||
|
};
|
||||||
|
const preventClick = (e) => {
|
||||||
|
e?.stopPropagation();
|
||||||
|
e?.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="h-full" onClick={clearSelection}>
|
||||||
<div>
|
<div onClick={preventClick}>
|
||||||
<input
|
<div>
|
||||||
ref={inputRef}
|
<input
|
||||||
onChange={(e) => setParticipantInput(e.target.value)}
|
ref={inputRef}
|
||||||
value={participantInput}
|
onChange={(e) => setParticipantInput(e.target.value)}
|
||||||
/>
|
value={participantInput}
|
||||||
{action && (
|
/>
|
||||||
<button onClick={doAction}>
|
{action && (
|
||||||
<FontAwesomeIcon
|
<button onClick={doAction}>
|
||||||
icon={faArrowTurnDown}
|
<FontAwesomeIcon
|
||||||
className="rotate-90 mr-2"
|
icon={faArrowTurnDown}
|
||||||
/>
|
className="rotate-90 mr-2"
|
||||||
{action}
|
/>
|
||||||
</button>
|
{action}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{participants.loading && (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSpinner}
|
||||||
|
className="animate-spin-slow text-gray-300 h-8"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{participants.response && (
|
||||||
|
<ul>
|
||||||
|
{participants.response.map((participant: Participant) => (
|
||||||
|
<li
|
||||||
|
onClick={selectParticipant(participant)}
|
||||||
|
className={
|
||||||
|
"flex flex-row justify-between " +
|
||||||
|
(participantInput.length > 0 &&
|
||||||
|
selectedText &&
|
||||||
|
participant.name.startsWith(participantInput)
|
||||||
|
? "bg-blue-100 "
|
||||||
|
: "") +
|
||||||
|
(participant.id == selectedParticipant?.id
|
||||||
|
? "border-blue-400 border"
|
||||||
|
: "")
|
||||||
|
}
|
||||||
|
key={participant.id}
|
||||||
|
>
|
||||||
|
<span>{participant.name}</span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{selectedTextIsSpeaker(selectedText) && !loading && (
|
||||||
|
<button onClick={mergeSpeaker(selectedText, participant)}>
|
||||||
|
{oneMatch &&
|
||||||
|
action == "Create to rename" &&
|
||||||
|
participant.name.startsWith(participantInput) && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
<span>CTRL + </span>{" "}
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowTurnDown}
|
||||||
|
className="rotate-90 mr-2"
|
||||||
|
/>{" "}
|
||||||
|
</>
|
||||||
|
)}{" "}
|
||||||
|
Merge
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{selectedTextIsTimeSlice(selectedText) && !loading && (
|
||||||
|
<button onClick={assignTo(participant)}>
|
||||||
|
{oneMatch &&
|
||||||
|
action == "Create and assign" &&
|
||||||
|
participant.name.startsWith(participantInput) && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
<span>CTRL + </span>{" "}
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowTurnDown}
|
||||||
|
className="rotate-90 mr-2"
|
||||||
|
/>{" "}
|
||||||
|
</>
|
||||||
|
)}{" "}
|
||||||
|
Assign
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button onClick={deleteParticipant(participant.id)}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{participants.loading && (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faSpinner}
|
|
||||||
className="animate-spin-slow text-gray-300 h-8"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{participants.response && (
|
|
||||||
<ul>
|
|
||||||
{participants.response.map((participant: Participant) => (
|
|
||||||
<li
|
|
||||||
onClick={selectParticipant(participant)}
|
|
||||||
className={
|
|
||||||
"flex flex-row justify-between " +
|
|
||||||
(participantInput.length > 0 &&
|
|
||||||
selectedTime &&
|
|
||||||
participant.name.startsWith(participantInput)
|
|
||||||
? "bg-blue-100 "
|
|
||||||
: "") +
|
|
||||||
(participant.id == selectedParticipant?.id
|
|
||||||
? "border-blue-400 border"
|
|
||||||
: "")
|
|
||||||
}
|
|
||||||
key={participant.id}
|
|
||||||
>
|
|
||||||
<span>{participant.name}</span>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{selectedTime && !loading && (
|
|
||||||
<button onClick={assignTo(participant)}>
|
|
||||||
{oneMatch &&
|
|
||||||
participant.name.startsWith(participantInput) && (
|
|
||||||
<>
|
|
||||||
{" "}
|
|
||||||
<span>CTRL + </span>{" "}
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faArrowTurnDown}
|
|
||||||
className="rotate-90 mr-2"
|
|
||||||
/>{" "}
|
|
||||||
</>
|
|
||||||
)}{" "}
|
|
||||||
Assign
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button onClick={deleteParticipant(participant.id)}>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,41 @@
|
|||||||
import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import useTopics from "../../useTopics";
|
import useTopics from "../../useTopics";
|
||||||
import { useEffect } from "react";
|
import { Dispatch, SetStateAction, useEffect } from "react";
|
||||||
|
import { TranscriptTopic } from "../../../../api/models/TranscriptTopic";
|
||||||
|
import { GetTranscriptTopic } from "../../../../api";
|
||||||
|
|
||||||
|
type TopicHeader = {
|
||||||
|
stateCurrentTopic: [
|
||||||
|
GetTranscriptTopic | undefined,
|
||||||
|
Dispatch<SetStateAction<GetTranscriptTopic | undefined>>,
|
||||||
|
];
|
||||||
|
transcriptId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function TopicHeader({
|
export default function TopicHeader({
|
||||||
currentTopic,
|
stateCurrentTopic,
|
||||||
setCurrentTopic,
|
|
||||||
transcriptId,
|
transcriptId,
|
||||||
}) {
|
}: TopicHeader) {
|
||||||
|
const [currentTopic, setCurrentTopic] = stateCurrentTopic;
|
||||||
const topics = useTopics(transcriptId);
|
const topics = useTopics(transcriptId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!topics.loading && !currentTopic) {
|
if (!topics.loading && !currentTopic) {
|
||||||
setCurrentTopic(topics?.topics?.at(0)?.id);
|
setCurrentTopic(topics?.topics?.at(0));
|
||||||
}
|
}
|
||||||
}, [topics.loading]);
|
}, [topics.loading]);
|
||||||
|
|
||||||
if (topics.topics) {
|
if (topics.topics && currentTopic) {
|
||||||
const title = topics.topics.find((topic) => topic.id == currentTopic)
|
const number = topics.topics.findIndex(
|
||||||
?.title;
|
(topic) => topic.id == currentTopic.id,
|
||||||
const number = topics.topics.findIndex((topic) => topic.id == currentTopic);
|
);
|
||||||
const canGoPrevious = number > 0;
|
const canGoPrevious = number > 0;
|
||||||
const total = topics.topics.length;
|
const total = topics.topics.length;
|
||||||
const canGoNext = total && number < total + 1;
|
const canGoNext = total && number < total + 1;
|
||||||
|
|
||||||
const onPrev = () =>
|
const onPrev = () => setCurrentTopic(topics.topics?.at(number - 1));
|
||||||
setCurrentTopic(topics.topics?.at(number - 1)?.id || "");
|
const onNext = () => setCurrentTopic(topics.topics?.at(number + 1));
|
||||||
const onNext = () =>
|
|
||||||
setCurrentTopic(topics.topics?.at(number + 1)?.id || "");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
@@ -40,7 +49,7 @@ export default function TopicHeader({
|
|||||||
<FontAwesomeIcon icon={faArrowLeft} />
|
<FontAwesomeIcon icon={faArrowLeft} />
|
||||||
</button>
|
</button>
|
||||||
<h1 className="flex-grow">
|
<h1 className="flex-grow">
|
||||||
{title}{" "}
|
{currentTopic.title}{" "}
|
||||||
<span>
|
<span>
|
||||||
{number + 1}/{total}
|
{number + 1}/{total}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,68 +1,43 @@
|
|||||||
import { SetStateAction, useCallback, useEffect, useState } from "react";
|
import {
|
||||||
|
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 { Participant } from "../../../../api";
|
||||||
|
import { UseTopicWithWords } from "../../useTopicWithWords";
|
||||||
type Word = {
|
import { TimeSlice, selectedTextIsTimeSlice } from "./page";
|
||||||
end: number;
|
|
||||||
speaker: number;
|
|
||||||
start: number;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type WordBySpeaker = { speaker: number; words: Word[] }[];
|
|
||||||
|
|
||||||
// TODO shortcuts ?
|
// TODO shortcuts ?
|
||||||
// TODO fix key (using indexes might act up, not sure as we don't re-order per say)
|
// TODO fix key (using indexes might act up, not sure as we don't re-order per say)
|
||||||
|
|
||||||
type TopicWordsProps = {
|
type TopicWordsProps = {
|
||||||
setSelectedTime: SetStateAction<any>;
|
stateSelectedText: [
|
||||||
selectedTime: any;
|
number | TimeSlice | undefined,
|
||||||
setTopicTime: SetStateAction<any>;
|
Dispatch<SetStateAction<number | TimeSlice | undefined>>,
|
||||||
stateSelectedSpeaker: any;
|
];
|
||||||
participants: UseParticipants;
|
participants: UseParticipants;
|
||||||
topicWithWords: any;
|
topicWithWords: UseTopicWithWords;
|
||||||
};
|
};
|
||||||
|
|
||||||
const topicWords = ({
|
const topicWords = ({
|
||||||
setSelectedTime,
|
stateSelectedText,
|
||||||
selectedTime,
|
|
||||||
setTopicTime,
|
|
||||||
stateSelectedSpeaker,
|
|
||||||
participants,
|
participants,
|
||||||
topicWithWords,
|
topicWithWords,
|
||||||
}: TopicWordsProps) => {
|
}: TopicWordsProps) => {
|
||||||
const [wordsBySpeaker, setWordsBySpeaker] = useState<WordBySpeaker>();
|
const [selectedText, setSelectedText] = stateSelectedText;
|
||||||
const [selectedSpeaker, setSelectedSpeaker] = stateSelectedSpeaker;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (topicWithWords.loading) {
|
if (topicWithWords.loading) {
|
||||||
setWordsBySpeaker([]);
|
// setWordsBySpeaker([]);
|
||||||
setSelectedTime(undefined);
|
setSelectedText(undefined);
|
||||||
console.log("unsetting topic changed");
|
console.log("unsetting topic changed");
|
||||||
}
|
}
|
||||||
}, [topicWithWords.loading]);
|
}, [topicWithWords.loading]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!topicWithWords.loading && !topicWithWords.error) {
|
|
||||||
const wordsFlat = topicWithWords.response.words as Word[];
|
|
||||||
const wordsSorted = wordsFlat.reduce((acc, curr) => {
|
|
||||||
if (acc.length > 0 && acc[acc.length - 1].speaker == curr.speaker) {
|
|
||||||
acc[acc.length - 1].words.push(curr);
|
|
||||||
return acc;
|
|
||||||
} else {
|
|
||||||
acc?.push({ speaker: curr.speaker, words: [curr] });
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
}, [] as WordBySpeaker);
|
|
||||||
setWordsBySpeaker(wordsSorted);
|
|
||||||
setTopicTime({
|
|
||||||
start: wordsFlat.at(0)?.start,
|
|
||||||
end: wordsFlat.at(wordsFlat.length - 1)?.end,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [topicWithWords.response]);
|
|
||||||
|
|
||||||
const getStartTimeFromFirstNode = (node, offset, reverse) => {
|
const getStartTimeFromFirstNode = (node, offset, reverse) => {
|
||||||
// if the first element is a word
|
// if the first element is a word
|
||||||
return node.parentElement?.dataset["start"]
|
return node.parentElement?.dataset["start"]
|
||||||
@@ -91,7 +66,7 @@ const topicWords = ({
|
|||||||
selection.anchorNode == selection.focusNode &&
|
selection.anchorNode == selection.focusNode &&
|
||||||
selection.anchorOffset == selection.focusOffset
|
selection.anchorOffset == selection.focusOffset
|
||||||
) {
|
) {
|
||||||
setSelectedTime(undefined);
|
setSelectedText(undefined);
|
||||||
selection.empty();
|
selection.empty();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -114,9 +89,11 @@ const topicWords = ({
|
|||||||
!focusIsWord &&
|
!focusIsWord &&
|
||||||
anchorNode.parentElement == focusNode.parentElement
|
anchorNode.parentElement == focusNode.parentElement
|
||||||
) {
|
) {
|
||||||
setSelectedSpeaker(focusNode.parentElement?.dataset["speaker"]);
|
setSelectedText(
|
||||||
setSelectedTime(undefined);
|
focusNode.parentElement?.dataset["speaker"]
|
||||||
selection.empty();
|
? parseInt(focusNode.parentElement?.dataset["speaker"])
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
console.log("Unset Time : selected Speaker");
|
console.log("Unset Time : selected Speaker");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -139,7 +116,7 @@ const topicWords = ({
|
|||||||
const reverse = parseFloat(anchorStart) > parseFloat(focusEnd);
|
const reverse = parseFloat(anchorStart) > parseFloat(focusEnd);
|
||||||
|
|
||||||
if (!reverse) {
|
if (!reverse) {
|
||||||
setSelectedTime({
|
setSelectedText({
|
||||||
start: parseFloat(anchorStart),
|
start: parseFloat(anchorStart),
|
||||||
end: parseFloat(focusEnd),
|
end: parseFloat(focusEnd),
|
||||||
});
|
});
|
||||||
@@ -158,13 +135,14 @@ const topicWords = ({
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
setSelectedTime({ start: focusStart, end: anchorEnd });
|
setSelectedText({
|
||||||
|
start: parseFloat(focusStart),
|
||||||
|
end: parseFloat(anchorEnd),
|
||||||
|
});
|
||||||
console.log("setting reverse");
|
console.log("setting reverse");
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedSpeaker();
|
|
||||||
selection.empty();
|
|
||||||
}
|
}
|
||||||
|
selection && selection.empty();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSpeakerName = (speakerNumber: number) => {
|
const getSpeakerName = (speakerNumber: number) => {
|
||||||
@@ -176,39 +154,45 @@ const topicWords = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!topicWithWords.loading && wordsBySpeaker && participants.response) {
|
if (
|
||||||
|
!topicWithWords.loading &&
|
||||||
|
topicWithWords.response &&
|
||||||
|
participants.response
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div onMouseUp={onMouseUp} className="p-5 h-full w-full">
|
<div onMouseUp={onMouseUp} className="p-5 h-full w-full">
|
||||||
{wordsBySpeaker?.map((speakerWithWords, index) => (
|
{topicWithWords.response.wordsPerSpeaker.map(
|
||||||
<p key={index}>
|
(speakerWithWords, index) => (
|
||||||
<span
|
<p key={index}>
|
||||||
data-speaker={speakerWithWords.speaker}
|
|
||||||
className={
|
|
||||||
selectedSpeaker == speakerWithWords.speaker
|
|
||||||
? "bg-yellow-200"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{getSpeakerName(speakerWithWords.speaker)} :
|
|
||||||
</span>
|
|
||||||
{speakerWithWords.words.map((word, index) => (
|
|
||||||
<span
|
<span
|
||||||
data-start={word.start}
|
data-speaker={speakerWithWords.speaker}
|
||||||
data-end={word.end}
|
|
||||||
key={index}
|
|
||||||
className={
|
className={
|
||||||
selectedTime &&
|
selectedText == speakerWithWords.speaker
|
||||||
selectedTime.start <= word.start &&
|
|
||||||
selectedTime.end >= word.end
|
|
||||||
? "bg-yellow-200"
|
? "bg-yellow-200"
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{word.text}
|
{getSpeakerName(speakerWithWords.speaker)} :
|
||||||
</span>
|
</span>
|
||||||
))}
|
{speakerWithWords.words.map((word, index) => (
|
||||||
</p>
|
<span
|
||||||
))}
|
data-start={word.start}
|
||||||
|
data-end={word.end}
|
||||||
|
key={index}
|
||||||
|
className={
|
||||||
|
selectedTextIsTimeSlice(selectedText) &&
|
||||||
|
selectedText.start <= word.start &&
|
||||||
|
selectedText.end >= word.end
|
||||||
|
? "bg-yellow-200"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{word.text}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"summary": "The team discusses the first issue in the list",
|
"summary": "The team discusses the first issue in the list",
|
||||||
"timestamp": 0.0,
|
"timestamp": 0.0,
|
||||||
"transcript": "",
|
"transcript": "",
|
||||||
|
"duration": 33,
|
||||||
"segments": [
|
"segments": [
|
||||||
{
|
{
|
||||||
"text": "Let's start with issue one, Alice you've been working on that, can you give an update ?",
|
"text": "Let's start with issue one, Alice you've been working on that, can you give an update ?",
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { V1TranscriptGetTopicsWithWordsRequest } from "../../api/apis/DefaultApi";
|
import {
|
||||||
import { GetTranscript, GetTranscriptTopicWithWords } from "../../api";
|
V1TranscriptGetTopicsWithWordsPerSpeakerRequest,
|
||||||
|
V1TranscriptGetTopicsWithWordsRequest,
|
||||||
|
} from "../../api/apis/DefaultApi";
|
||||||
|
import {
|
||||||
|
GetTranscript,
|
||||||
|
GetTranscriptTopicWithWords,
|
||||||
|
GetTranscriptTopicWithWordsPerSpeaker,
|
||||||
|
} from "../../api";
|
||||||
import { useError } from "../../(errors)/errorContext";
|
import { useError } from "../../(errors)/errorContext";
|
||||||
import getApi from "../../lib/getApi";
|
import getApi from "../../lib/getApi";
|
||||||
import { shouldShowError } from "../../lib/errorUtils";
|
import { shouldShowError } from "../../lib/errorUtils";
|
||||||
@@ -8,22 +15,22 @@ import { shouldShowError } from "../../lib/errorUtils";
|
|||||||
type ErrorTopicWithWords = {
|
type ErrorTopicWithWords = {
|
||||||
error: Error;
|
error: Error;
|
||||||
loading: false;
|
loading: false;
|
||||||
response: any;
|
response: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LoadingTopicWithWords = {
|
type LoadingTopicWithWords = {
|
||||||
response: any;
|
response: GetTranscriptTopicWithWordsPerSpeaker | null;
|
||||||
loading: true;
|
loading: true;
|
||||||
error: false;
|
error: false;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SuccessTopicWithWords = {
|
type SuccessTopicWithWords = {
|
||||||
response: GetTranscriptTopicWithWords;
|
response: GetTranscriptTopicWithWordsPerSpeaker;
|
||||||
loading: false;
|
loading: false;
|
||||||
error: null;
|
error: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UseTopicWithWords = { refetch: () => void } & (
|
export type UseTopicWithWords = { refetch: () => void } & (
|
||||||
| ErrorTopicWithWords
|
| ErrorTopicWithWords
|
||||||
| LoadingTopicWithWords
|
| LoadingTopicWithWords
|
||||||
| SuccessTopicWithWords
|
| SuccessTopicWithWords
|
||||||
@@ -33,7 +40,8 @@ const useTopicWithWords = (
|
|||||||
topicId: string | null,
|
topicId: string | null,
|
||||||
transcriptId: string,
|
transcriptId: string,
|
||||||
): UseTopicWithWords => {
|
): UseTopicWithWords => {
|
||||||
const [response, setResponse] = useState<GetTranscript | null>(null);
|
const [response, setResponse] =
|
||||||
|
useState<GetTranscriptTopicWithWordsPerSpeaker | null>(null);
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setErrorState] = useState<Error | null>(null);
|
const [error, setErrorState] = useState<Error | null>(null);
|
||||||
const { setError } = useError();
|
const { setError } = useError();
|
||||||
@@ -55,13 +63,14 @@ const useTopicWithWords = (
|
|||||||
if (!transcriptId || !topicId || !api) return;
|
if (!transcriptId || !topicId || !api) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const requestParameters: V1TranscriptGetTopicsWithWordsRequest = {
|
const requestParameters: V1TranscriptGetTopicsWithWordsPerSpeakerRequest = {
|
||||||
transcriptId,
|
transcriptId,
|
||||||
|
topicId,
|
||||||
};
|
};
|
||||||
api
|
api
|
||||||
.v1TranscriptGetTopicsWithWords(requestParameters)
|
.v1TranscriptGetTopicsWithWordsPerSpeaker(requestParameters)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
setResponse(result.find((topic) => topic.id == topicId));
|
setResponse(result);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
console.debug("Topics with words Loaded:", result);
|
console.debug("Topics with words Loaded:", result);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import { Topic } from "./webSocketTypes";
|
|||||||
import getApi from "../../lib/getApi";
|
import getApi from "../../lib/getApi";
|
||||||
import { shouldShowError } from "../../lib/errorUtils";
|
import { shouldShowError } from "../../lib/errorUtils";
|
||||||
import mockTopics from "./mockTopics.json";
|
import mockTopics from "./mockTopics.json";
|
||||||
|
import { GetTranscriptTopic } from "../../api";
|
||||||
|
|
||||||
type TranscriptTopics = {
|
type TranscriptTopics = {
|
||||||
topics: Topic[] | null;
|
topics: GetTranscriptTopic[] | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user