mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
switch to chackra, bugfixes
This commit is contained in:
@@ -41,6 +41,9 @@ async def transcript_get_participants(
|
|||||||
transcript_id, user_id=user_id
|
transcript_id, user_id=user_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if transcript.participants is None:
|
||||||
|
return []
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Participant.model_validate(participant)
|
Participant.model_validate(participant)
|
||||||
for participant in transcript.participants
|
for participant in transcript.participants
|
||||||
@@ -59,7 +62,7 @@ async def transcript_add_participant(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ensure the speaker is unique
|
# ensure the speaker is unique
|
||||||
if participant.speaker is not None:
|
if participant.speaker is not None and transcript.participants is not None:
|
||||||
for p in transcript.participants:
|
for p in transcript.participants:
|
||||||
if p.speaker == participant.speaker:
|
if p.speaker == participant.speaker:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export default async function RootLayout({ children, params }: LayoutProps) {
|
|||||||
<Providers>
|
<Providers>
|
||||||
<div
|
<div
|
||||||
id="container"
|
id="container"
|
||||||
className="items-center h-[100svh] w-[100svw] p-2 md:p-4 grid grid-rows-layout gap-2 md:gap-4"
|
className="items-center h-[100svh] w-[100svw] p-2 md:p-4 grid grid-rows-layout-topbar gap-2 md:gap-4"
|
||||||
>
|
>
|
||||||
<header className="flex justify-between items-center w-full">
|
<header className="flex justify-between items-center w-full">
|
||||||
{/* Logo on the left */}
|
{/* Logo on the left */}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import TopicHeader from "./topicHeader";
|
import TopicHeader from "./topicHeader";
|
||||||
import TopicWords from "./topicWords";
|
import TopicWords from "./topicWords";
|
||||||
import TopicPlayer from "./topicPlayer";
|
import TopicPlayer from "./topicPlayer";
|
||||||
@@ -12,6 +12,7 @@ import getApi from "../../../../lib/getApi";
|
|||||||
import useTranscript from "../../useTranscript";
|
import useTranscript from "../../useTranscript";
|
||||||
import { useError } from "../../../../(errors)/errorContext";
|
import { useError } from "../../../../(errors)/errorContext";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Box, Grid } from "@chakra-ui/react";
|
||||||
|
|
||||||
export type TranscriptCorrect = {
|
export type TranscriptCorrect = {
|
||||||
params: {
|
params: {
|
||||||
@@ -50,57 +51,76 @@ export default function TranscriptCorrect({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full grid grid-cols-2 gap-4">
|
<Grid
|
||||||
<div className="flex flex-col h-full ">
|
templateRows="auto minmax(0, 1fr)"
|
||||||
|
h="100%"
|
||||||
|
maxW={{ lg: "container.lg" }}
|
||||||
|
mx="auto"
|
||||||
|
minW={{ base: "100%", lg: "container.lg" }}
|
||||||
|
>
|
||||||
|
<Box display="flex" flexDir="column" minW="100%" mb={{ base: 4, lg: 10 }}>
|
||||||
<TopicHeader
|
<TopicHeader
|
||||||
|
minW="100%"
|
||||||
stateCurrentTopic={stateCurrentTopic}
|
stateCurrentTopic={stateCurrentTopic}
|
||||||
transcriptId={transcriptId}
|
transcriptId={transcriptId}
|
||||||
topicWithWordsLoading={topicWithWords.loading}
|
topicWithWordsLoading={topicWithWords.loading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TopicPlayer
|
||||||
|
transcriptId={transcriptId}
|
||||||
|
selectedTime={
|
||||||
|
selectedTextIsTimeSlice(selectedText) ? selectedText : undefined
|
||||||
|
}
|
||||||
|
topicTime={
|
||||||
|
currentTopic
|
||||||
|
? {
|
||||||
|
start: currentTopic?.timestamp,
|
||||||
|
end: currentTopic?.timestamp + currentTopic?.duration,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Grid
|
||||||
|
templateColumns={{
|
||||||
|
base: "minmax(0, 1fr)",
|
||||||
|
md: "4fr 3fr",
|
||||||
|
lg: "2fr 1fr",
|
||||||
|
}}
|
||||||
|
templateRows={{
|
||||||
|
base: "repeat(2, minmax(0, 1fr)) auto",
|
||||||
|
md: "minmax(0, 1fr)",
|
||||||
|
}}
|
||||||
|
gap={{ base: "2", md: "4", lg: "4" }}
|
||||||
|
h="100%"
|
||||||
|
maxH="100%"
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
<TopicWords
|
<TopicWords
|
||||||
stateSelectedText={stateSelectedText}
|
stateSelectedText={stateSelectedText}
|
||||||
participants={participants}
|
participants={participants}
|
||||||
topicWithWords={topicWithWords}
|
topicWithWords={topicWithWords}
|
||||||
|
mb={{ md: "-3rem" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
<ParticipantList
|
||||||
<div className="flex flex-col justify-stretch">
|
{...{
|
||||||
{currentTopic ? (
|
transcriptId,
|
||||||
<TopicPlayer
|
participants,
|
||||||
transcriptId={transcriptId}
|
topicWithWords,
|
||||||
selectedTime={
|
stateSelectedText,
|
||||||
selectedTextIsTimeSlice(selectedText) ? selectedText : undefined
|
}}
|
||||||
}
|
/>
|
||||||
topicTime={{
|
</Grid>
|
||||||
start: currentTopic?.timestamp,
|
{transcript.response && !transcript.response?.reviewed && (
|
||||||
end: currentTopic?.timestamp + currentTopic?.duration,
|
<div className="flex flex-row justify-end">
|
||||||
}}
|
<button
|
||||||
/>
|
className="p-2 px-4 rounded bg-green-400"
|
||||||
) : (
|
onClick={markAsDone}
|
||||||
<div></div>
|
>
|
||||||
)}
|
Done
|
||||||
{participants.response && (
|
</button>
|
||||||
<div className="h-full flex flex-col justify-between">
|
</div>
|
||||||
<ParticipantList
|
)}
|
||||||
{...{
|
</Grid>
|
||||||
transcriptId,
|
|
||||||
participants,
|
|
||||||
topicWithWords,
|
|
||||||
stateSelectedText,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{!transcript.response?.reviewed && (
|
|
||||||
<div className="flex flex-row justify-end">
|
|
||||||
<button
|
|
||||||
className="p-2 px-4 rounded bg-green-400"
|
|
||||||
onClick={markAsDone}
|
|
||||||
>
|
|
||||||
Done
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,18 @@ import getApi from "../../../../lib/getApi";
|
|||||||
import { UseParticipants } from "../../useParticipants";
|
import { UseParticipants } from "../../useParticipants";
|
||||||
import { selectedTextIsSpeaker, selectedTextIsTimeSlice } from "./types";
|
import { selectedTextIsSpeaker, selectedTextIsTimeSlice } from "./types";
|
||||||
import { useError } from "../../../../(errors)/errorContext";
|
import { useError } from "../../../../(errors)/errorContext";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Text,
|
||||||
|
UnorderedList,
|
||||||
|
Input,
|
||||||
|
Kbd,
|
||||||
|
Spinner,
|
||||||
|
ListItem,
|
||||||
|
Grid,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
type ParticipantList = {
|
type ParticipantList = {
|
||||||
participants: UseParticipants;
|
participants: UseParticipants;
|
||||||
@@ -13,7 +25,6 @@ type ParticipantList = {
|
|||||||
topicWithWords: any;
|
topicWithWords: any;
|
||||||
stateSelectedText: any;
|
stateSelectedText: any;
|
||||||
};
|
};
|
||||||
// NTH re-order list when searching
|
|
||||||
const ParticipantList = ({
|
const ParticipantList = ({
|
||||||
transcriptId,
|
transcriptId,
|
||||||
participants,
|
participants,
|
||||||
@@ -58,12 +69,14 @@ const ParticipantList = ({
|
|||||||
setAction("Create and assign");
|
setAction("Create and assign");
|
||||||
setSelectedParticipant(undefined);
|
setSelectedParticipant(undefined);
|
||||||
}
|
}
|
||||||
if (typeof selectedText == undefined) {
|
|
||||||
|
if (typeof selectedText == "undefined") {
|
||||||
inputRef.current?.blur();
|
inputRef.current?.blur();
|
||||||
|
setSelectedParticipant(undefined);
|
||||||
setAction(null);
|
setAction(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [selectedText, participants.response]);
|
}, [selectedText, !participants.response]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.onkeyup = (e) => {
|
document.onkeyup = (e) => {
|
||||||
@@ -250,6 +263,7 @@ const ParticipantList = ({
|
|||||||
participants.refetch();
|
participants.refetch();
|
||||||
setParticipantInput("");
|
setParticipantInput("");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
inputRef.current?.focus();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
setError(e, "There was an error creating");
|
setError(e, "There was an error creating");
|
||||||
@@ -320,48 +334,56 @@ const ParticipantList = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const anyLoading = loading || participants.loading || topicWithWords.loading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full" onClick={clearSelection}>
|
<Box h="100%" onClick={clearSelection} width="100%">
|
||||||
<div onClick={preventClick}>
|
<Grid
|
||||||
<div className="grid grid-cols-7 gap-2 mb-2">
|
onClick={preventClick}
|
||||||
<input
|
maxH="100%"
|
||||||
|
templateRows="auto minmax(0, 1fr)"
|
||||||
|
min-w="100%"
|
||||||
|
>
|
||||||
|
<Flex direction="column" p="2">
|
||||||
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
onChange={changeParticipantInput}
|
onChange={changeParticipantInput}
|
||||||
value={participantInput}
|
value={participantInput}
|
||||||
className="border col-span-3 border-blue-400 p-1"
|
mb="2"
|
||||||
|
placeholder="Participant Name"
|
||||||
/>
|
/>
|
||||||
{action ? (
|
<Button
|
||||||
<button
|
onClick={doAction}
|
||||||
onClick={doAction}
|
colorScheme="blue"
|
||||||
className="p-2 bg-blue-200 w-full col-span-3"
|
disabled={!action || anyLoading}
|
||||||
>
|
>
|
||||||
[
|
{action || !anyLoading ? (
|
||||||
<FontAwesomeIcon
|
<>
|
||||||
icon={faArrowTurnDown}
|
<Kbd color="blue.500" pt="1" mr="1">
|
||||||
className="rotate-90 h-2"
|
<FontAwesomeIcon
|
||||||
/>
|
icon={faArrowTurnDown}
|
||||||
]{" " + action}
|
className="rotate-90 h-3"
|
||||||
</button>
|
/>
|
||||||
) : (
|
</Kbd>
|
||||||
<div className="col-span-3" />
|
{action || "Create"}
|
||||||
)}
|
</>
|
||||||
{loading ||
|
) : (
|
||||||
participants.loading ||
|
<Spinner />
|
||||||
(topicWithWords.loading && (
|
)}
|
||||||
<FontAwesomeIcon
|
</Button>
|
||||||
icon={faSpinner}
|
</Flex>
|
||||||
className="animate-spin-slow text-gray-300 h-8"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{participants.response && (
|
{participants.response && (
|
||||||
<ul>
|
<UnorderedList
|
||||||
|
mx="0"
|
||||||
|
mb={{ base: 2, md: 4 }}
|
||||||
|
maxH="100%"
|
||||||
|
overflow="scroll"
|
||||||
|
>
|
||||||
{participants.response.map((participant: Participant) => (
|
{participants.response.map((participant: Participant) => (
|
||||||
<li
|
<ListItem
|
||||||
onClick={selectParticipant(participant)}
|
onClick={selectParticipant(participant)}
|
||||||
className={
|
className={
|
||||||
"flex flex-row justify-between border-b last:border-b-0 py-2 " +
|
|
||||||
(participantInput.length > 0 &&
|
(participantInput.length > 0 &&
|
||||||
selectedText &&
|
selectedText &&
|
||||||
participant.name.startsWith(participantInput)
|
participant.name.startsWith(participantInput)
|
||||||
@@ -371,69 +393,88 @@ const ParticipantList = ({
|
|||||||
? "bg-blue-200 border"
|
? "bg-blue-200 border"
|
||||||
: "")
|
: "")
|
||||||
}
|
}
|
||||||
key={participant.id}
|
display="flex"
|
||||||
|
flexDirection="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
borderBottom="1px"
|
||||||
|
borderColor="gray.300"
|
||||||
|
py="2"
|
||||||
|
mx="2"
|
||||||
|
_last={{ borderBottom: "0" }}
|
||||||
|
key={participant.name}
|
||||||
>
|
>
|
||||||
<span>{participant.name}</span>
|
<Text mt="1">{participant.name}</Text>
|
||||||
|
|
||||||
<div>
|
<Box>
|
||||||
{selectedTextIsSpeaker(selectedText) &&
|
{selectedTextIsSpeaker(selectedText) &&
|
||||||
!selectedParticipant &&
|
!selectedParticipant &&
|
||||||
!loading && (
|
!loading && (
|
||||||
<button
|
<Button
|
||||||
onClick={mergeSpeaker(selectedText, participant)}
|
onClick={mergeSpeaker(selectedText, participant)}
|
||||||
className="bg-blue-400 px-2 ml-2"
|
colorScheme="blue"
|
||||||
|
ml="2"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
{oneMatch?.id == participant.id &&
|
{oneMatch?.id == participant.id &&
|
||||||
action == "Create to rename" && (
|
action == "Create to rename" && (
|
||||||
<>
|
<Kbd
|
||||||
<span className="text-xs">
|
letterSpacing="-1px"
|
||||||
[CTRL +{" "}
|
color="blue.500"
|
||||||
<FontAwesomeIcon
|
mr="1"
|
||||||
icon={faArrowTurnDown}
|
pt="3px"
|
||||||
className="rotate-90 mr-2 h-2"
|
>
|
||||||
/>
|
Ctrl +
|
||||||
]
|
<FontAwesomeIcon
|
||||||
</span>
|
icon={faArrowTurnDown}
|
||||||
</>
|
className="rotate-90 h-2"
|
||||||
)}{" "}
|
/>
|
||||||
|
</Kbd>
|
||||||
|
)}
|
||||||
Merge
|
Merge
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{selectedTextIsTimeSlice(selectedText) && !loading && (
|
{selectedTextIsTimeSlice(selectedText) && !loading && (
|
||||||
<button
|
<Button
|
||||||
onClick={assignTo(participant)}
|
onClick={assignTo(participant)}
|
||||||
className="bg-blue-400 px-2 ml-2"
|
colorScheme="blue"
|
||||||
|
ml="2"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
{oneMatch?.id == participant.id &&
|
{oneMatch?.id == participant.id &&
|
||||||
action == "Create and assign" && (
|
action == "Create and assign" && (
|
||||||
<>
|
<Kbd
|
||||||
<span className="text-xs">
|
letterSpacing="-1px"
|
||||||
[CTRL +{" "}
|
color="blue.500"
|
||||||
<FontAwesomeIcon
|
mr="1"
|
||||||
icon={faArrowTurnDown}
|
pt="3px"
|
||||||
className="rotate-90 mr-2 h-2"
|
>
|
||||||
/>
|
Ctrl +
|
||||||
]
|
<FontAwesomeIcon
|
||||||
</span>
|
icon={faArrowTurnDown}
|
||||||
</>
|
className="rotate-90 h-2"
|
||||||
|
/>
|
||||||
|
</Kbd>
|
||||||
)}{" "}
|
)}{" "}
|
||||||
Assign
|
Assign
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={deleteParticipant(participant.id)}
|
onClick={deleteParticipant(participant.id)}
|
||||||
className="bg-blue-400 px-2 ml-2"
|
colorScheme="blue"
|
||||||
|
ml="2"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</Box>
|
||||||
</li>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</UnorderedList>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Grid>
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export default ({ playing }) => (
|
export default ({ playing }) => (
|
||||||
<div className="flex justify-between w-16 h-8 m-auto">
|
<div className="flex justify-between w-14 h-6">
|
||||||
<div
|
<div
|
||||||
className={`bg-blue-400 rounded w-2 ${
|
className={`bg-blue-400 rounded w-2 ${
|
||||||
playing ? "animate-wave-quiet" : ""
|
playing ? "animate-wave-quiet" : ""
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import useTopics from "../../useTopics";
|
import useTopics from "../../useTopics";
|
||||||
import { Dispatch, SetStateAction, useEffect } from "react";
|
import { Dispatch, SetStateAction, useEffect } from "react";
|
||||||
import { GetTranscriptTopic } from "../../../../api";
|
import { GetTranscriptTopic } from "../../../../api";
|
||||||
|
import {
|
||||||
|
BoxProps,
|
||||||
|
Box,
|
||||||
|
Circle,
|
||||||
|
Heading,
|
||||||
|
Kbd,
|
||||||
|
Skeleton,
|
||||||
|
SkeletonCircle,
|
||||||
|
chakra,
|
||||||
|
Flex,
|
||||||
|
Center,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { ChevronLeftIcon, ChevronRightIcon } from "@chakra-ui/icons";
|
||||||
|
|
||||||
type TopicHeader = {
|
type TopicHeader = {
|
||||||
stateCurrentTopic: [
|
stateCurrentTopic: [
|
||||||
@@ -17,7 +28,8 @@ export default function TopicHeader({
|
|||||||
stateCurrentTopic,
|
stateCurrentTopic,
|
||||||
transcriptId,
|
transcriptId,
|
||||||
topicWithWordsLoading,
|
topicWithWordsLoading,
|
||||||
}: TopicHeader) {
|
...chakraProps
|
||||||
|
}: TopicHeader & BoxProps) {
|
||||||
const [currentTopic, setCurrentTopic] = stateCurrentTopic;
|
const [currentTopic, setCurrentTopic] = stateCurrentTopic;
|
||||||
const topics = useTopics(transcriptId);
|
const topics = useTopics(transcriptId);
|
||||||
|
|
||||||
@@ -26,17 +38,13 @@ export default function TopicHeader({
|
|||||||
const sessionTopic = window.localStorage.getItem(
|
const sessionTopic = window.localStorage.getItem(
|
||||||
transcriptId + "correct",
|
transcriptId + "correct",
|
||||||
);
|
);
|
||||||
console.log(sessionTopic, window.localStorage);
|
|
||||||
if (sessionTopic && topics?.topics?.find((t) => t.id == sessionTopic)) {
|
if (sessionTopic && topics?.topics?.find((t) => t.id == sessionTopic)) {
|
||||||
setCurrentTopic(topics?.topics?.find((t) => t.id == sessionTopic));
|
setCurrentTopic(topics?.topics?.find((t) => t.id == sessionTopic));
|
||||||
console.log("he", sessionTopic, !!sessionTopic);
|
|
||||||
} else {
|
} else {
|
||||||
setCurrentTopic(topics?.topics?.at(0));
|
setCurrentTopic(topics?.topics?.at(0));
|
||||||
console.log("hi");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [topics.loading]);
|
}, [topics.loading]);
|
||||||
// console.log(currentTopic)
|
|
||||||
|
|
||||||
const number = topics.topics?.findIndex(
|
const number = topics.topics?.findIndex(
|
||||||
(topic) => topic.id == currentTopic?.id,
|
(topic) => topic.id == currentTopic?.id,
|
||||||
@@ -75,35 +83,85 @@ export default function TopicHeader({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (topics.topics && currentTopic && typeof number == "number") {
|
const isLoaded = !!(
|
||||||
return (
|
topics.topics &&
|
||||||
<div className="flex flex-row">
|
currentTopic &&
|
||||||
<button
|
typeof number == "number"
|
||||||
className={`w-10 h-10 rounded-full p-2 border border-gray-300 disabled:bg-white ${
|
);
|
||||||
canGoPrevious ? "text-gray-500" : "text-gray-300"
|
return (
|
||||||
}`}
|
<Box
|
||||||
|
display="flex"
|
||||||
|
w="100%"
|
||||||
|
justifyContent="space-between"
|
||||||
|
{...chakraProps}
|
||||||
|
>
|
||||||
|
<SkeletonCircle
|
||||||
|
isLoaded={isLoaded}
|
||||||
|
h={isLoaded ? "auto" : "40px"}
|
||||||
|
w={isLoaded ? "auto" : "40px"}
|
||||||
|
mb="2"
|
||||||
|
fadeDuration={1}
|
||||||
|
>
|
||||||
|
<Circle
|
||||||
|
as="button"
|
||||||
onClick={onPrev}
|
onClick={onPrev}
|
||||||
disabled={!canGoPrevious}
|
disabled={!canGoPrevious}
|
||||||
|
size="40px"
|
||||||
|
border="1px"
|
||||||
|
color={canGoPrevious ? "inherit" : "gray"}
|
||||||
|
borderColor={canGoNext ? "body-text" : "gray"}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faArrowLeft} />
|
{canGoPrevious ? (
|
||||||
</button>
|
<Kbd>
|
||||||
<h1 className="flex-grow">
|
<ChevronLeftIcon />
|
||||||
{currentTopic.title}{" "}
|
</Kbd>
|
||||||
<span>
|
) : (
|
||||||
{number + 1}/{total}
|
<ChevronLeftIcon />
|
||||||
</span>
|
)}
|
||||||
</h1>
|
</Circle>
|
||||||
<button
|
</SkeletonCircle>
|
||||||
className={`w-10 h-10 rounded-full p-2 border border-gray-300 disabled:bg-white ${
|
<Skeleton
|
||||||
canGoNext ? "text-gray-500" : "text-gray-300"
|
isLoaded={isLoaded}
|
||||||
}`}
|
h={isLoaded ? "auto" : "40px"}
|
||||||
|
mb="2"
|
||||||
|
fadeDuration={1}
|
||||||
|
flexGrow={1}
|
||||||
|
mx={6}
|
||||||
|
>
|
||||||
|
<Flex wrap="nowrap" justifyContent="center">
|
||||||
|
<Heading size="lg" textAlign="center" noOfLines={1}>
|
||||||
|
{currentTopic?.title}{" "}
|
||||||
|
</Heading>
|
||||||
|
<Heading size="lg" ml="3">
|
||||||
|
{(number || 0) + 1}/{total}
|
||||||
|
</Heading>
|
||||||
|
</Flex>
|
||||||
|
</Skeleton>
|
||||||
|
<SkeletonCircle
|
||||||
|
isLoaded={isLoaded}
|
||||||
|
h={isLoaded ? "auto" : "40px"}
|
||||||
|
w={isLoaded ? "auto" : "40px"}
|
||||||
|
mb="2"
|
||||||
|
fadeDuration={1}
|
||||||
|
>
|
||||||
|
<Circle
|
||||||
|
as="button"
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
disabled={!canGoNext}
|
disabled={!canGoNext}
|
||||||
|
size="40px"
|
||||||
|
border="1px"
|
||||||
|
color={canGoNext ? "inherit" : "gray"}
|
||||||
|
borderColor={canGoNext ? "body-text" : "gray"}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faArrowRight} />
|
{canGoNext ? (
|
||||||
</button>
|
<Kbd>
|
||||||
</div>
|
<ChevronRightIcon />
|
||||||
);
|
</Kbd>
|
||||||
}
|
) : (
|
||||||
return null;
|
<ChevronRightIcon />
|
||||||
|
)}
|
||||||
|
</Circle>
|
||||||
|
</SkeletonCircle>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,29 @@ import useMp3 from "../../useMp3";
|
|||||||
import { formatTime } from "../../../../lib/time";
|
import { formatTime } from "../../../../lib/time";
|
||||||
import SoundWaveCss from "./soundWaveCss";
|
import SoundWaveCss from "./soundWaveCss";
|
||||||
import { TimeSlice } from "./types";
|
import { TimeSlice } from "./types";
|
||||||
|
import {
|
||||||
|
BoxProps,
|
||||||
|
Button,
|
||||||
|
Wrap,
|
||||||
|
Text,
|
||||||
|
WrapItem,
|
||||||
|
Kbd,
|
||||||
|
Skeleton,
|
||||||
|
chakra,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
type TopicPlayer = {
|
type TopicPlayer = {
|
||||||
transcriptId: string;
|
transcriptId: string;
|
||||||
selectedTime: TimeSlice | undefined;
|
selectedTime: TimeSlice | undefined;
|
||||||
topicTime: TimeSlice;
|
topicTime: TimeSlice | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TopicPlayer = ({
|
const TopicPlayer = ({
|
||||||
transcriptId,
|
transcriptId,
|
||||||
selectedTime,
|
selectedTime,
|
||||||
topicTime,
|
topicTime,
|
||||||
}: TopicPlayer) => {
|
...chakraProps
|
||||||
|
}: TopicPlayer & BoxProps) => {
|
||||||
const mp3 = useMp3(transcriptId);
|
const mp3 = useMp3(transcriptId);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [endTopicCallback, setEndTopicCallback] = useState<() => void>();
|
const [endTopicCallback, setEndTopicCallback] = useState<() => void>();
|
||||||
@@ -46,6 +57,7 @@ const TopicPlayer = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const calcShowTime = () => {
|
const calcShowTime = () => {
|
||||||
|
if (!topicTime) return;
|
||||||
setShowTime(
|
setShowTime(
|
||||||
`${
|
`${
|
||||||
mp3.media?.currentTime
|
mp3.media?.currentTime
|
||||||
@@ -67,6 +79,7 @@ const TopicPlayer = ({
|
|||||||
setEndTopicCallback(
|
setEndTopicCallback(
|
||||||
() =>
|
() =>
|
||||||
function () {
|
function () {
|
||||||
|
if (!topicTime) return;
|
||||||
if (mp3.media && mp3.media.currentTime >= topicTime.end) {
|
if (mp3.media && mp3.media.currentTime >= topicTime.end) {
|
||||||
mp3.media.pause();
|
mp3.media.pause();
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
@@ -81,13 +94,14 @@ const TopicPlayer = ({
|
|||||||
// there's no callback on pause but apparently changing the time while palying doesn't work... so here is a timeout
|
// there's no callback on pause but apparently changing the time while palying doesn't work... so here is a timeout
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (mp3.media) {
|
if (mp3.media) {
|
||||||
|
if (!topicTime) return;
|
||||||
mp3.media.currentTime = topicTime.start;
|
mp3.media.currentTime = topicTime.start;
|
||||||
setShowTime(`00:00/${formatTime(topicTime.end - topicTime.start)}`);
|
setShowTime(`00:00/${formatTime(topicTime.end - topicTime.start)}`);
|
||||||
}
|
}
|
||||||
}, 10);
|
}, 10);
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
}
|
}
|
||||||
}, [!mp3.media, topicTime.start, topicTime.end]);
|
}, [!mp3.media, topicTime?.start, topicTime?.end]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
endTopicCallback &&
|
endTopicCallback &&
|
||||||
@@ -141,6 +155,7 @@ const TopicPlayer = ({
|
|||||||
const playTopic = (e) => {
|
const playTopic = (e) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
e?.target?.blur();
|
e?.target?.blur();
|
||||||
|
if (!topicTime) return;
|
||||||
if (mp3.media) {
|
if (mp3.media) {
|
||||||
mp3.media.currentTime = topicTime.start;
|
mp3.media.currentTime = topicTime.start;
|
||||||
mp3.media.play();
|
mp3.media.play();
|
||||||
@@ -165,37 +180,65 @@ const TopicPlayer = ({
|
|||||||
mp3.media?.pause();
|
mp3.media?.pause();
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
};
|
};
|
||||||
|
console.log(topicTime);
|
||||||
|
|
||||||
if (mp3.media) {
|
const isLoaded = !!(mp3.media && topicTime);
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 grid grid-cols-3 gap-2">
|
<Skeleton
|
||||||
<SoundWaveCss playing={isPlaying} />
|
isLoaded={isLoaded}
|
||||||
<div className="col-span-2">{showTime}</div>
|
h={isLoaded ? "auto" : "40px"}
|
||||||
<button className="p-2 bg-blue-200 w-full" onClick={playTopic}>
|
fadeDuration={1}
|
||||||
Play From Start
|
w={isLoaded ? "auto" : "container.md"}
|
||||||
</button>
|
margin="auto"
|
||||||
{!isPlaying ? (
|
{...chakraProps}
|
||||||
<button
|
>
|
||||||
ref={playButton}
|
<Wrap spacing="4" justify="center" align="center">
|
||||||
id="playButton"
|
<WrapItem>
|
||||||
className="p-2 bg-blue-200 w-full"
|
<SoundWaveCss playing={isPlaying} />
|
||||||
onClick={playCurrent}
|
<Text fontSize="sm" pt="1" pl="2">
|
||||||
|
{showTime}
|
||||||
|
</Text>
|
||||||
|
</WrapItem>
|
||||||
|
<WrapItem>
|
||||||
|
<Button onClick={playTopic} colorScheme="blue">
|
||||||
|
Play from start
|
||||||
|
</Button>
|
||||||
|
</WrapItem>
|
||||||
|
<WrapItem>
|
||||||
|
{!isPlaying ? (
|
||||||
|
<Button
|
||||||
|
onClick={playCurrent}
|
||||||
|
ref={playButton}
|
||||||
|
id="playButton"
|
||||||
|
colorScheme="blue"
|
||||||
|
w="120px"
|
||||||
|
>
|
||||||
|
<Kbd color="blue.600">Space</Kbd> Play
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={pause}
|
||||||
|
ref={playButton}
|
||||||
|
id="playButton"
|
||||||
|
colorScheme="blue"
|
||||||
|
w="120px"
|
||||||
|
>
|
||||||
|
<Kbd color="blue.600">Space</Kbd> Pause
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</WrapItem>
|
||||||
|
<WrapItem visibility={selectedTime ? "visible" : "hidden"}>
|
||||||
|
<Button
|
||||||
|
disabled={!selectedTime}
|
||||||
|
onClick={playSelection}
|
||||||
|
colorScheme="blue"
|
||||||
>
|
>
|
||||||
<span className="text-xs">[SPACE]</span> Play
|
<Kbd color="blue.600">,</Kbd> Play selection
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
</WrapItem>
|
||||||
<button className="p-2 bg-blue-200 w-full" onClick={pause}>
|
</Wrap>
|
||||||
<span className="text-xs">[SPACE]</span> Pause
|
</Skeleton>
|
||||||
</button>
|
);
|
||||||
)}
|
|
||||||
{selectedTime && (
|
|
||||||
<button className="p-2 bg-blue-200 w-full" onClick={playSelection}>
|
|
||||||
<span className="text-xs">[,]</span>Play Selection
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TopicPlayer;
|
export default TopicPlayer;
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ import WaveformLoading from "../../waveformLoading";
|
|||||||
import { UseParticipants } from "../../useParticipants";
|
import { UseParticipants } from "../../useParticipants";
|
||||||
import { UseTopicWithWords } from "../../useTopicWithWords";
|
import { UseTopicWithWords } from "../../useTopicWithWords";
|
||||||
import { TimeSlice, selectedTextIsTimeSlice } from "./types";
|
import { TimeSlice, selectedTextIsTimeSlice } from "./types";
|
||||||
|
import {
|
||||||
|
BoxProps,
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Text,
|
||||||
|
chakra,
|
||||||
|
Spinner,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
type TopicWordsProps = {
|
type TopicWordsProps = {
|
||||||
stateSelectedText: [
|
stateSelectedText: [
|
||||||
@@ -17,7 +25,8 @@ const topicWords = ({
|
|||||||
stateSelectedText,
|
stateSelectedText,
|
||||||
participants,
|
participants,
|
||||||
topicWithWords,
|
topicWithWords,
|
||||||
}: TopicWordsProps) => {
|
...chakraProps
|
||||||
|
}: TopicWordsProps & BoxProps) => {
|
||||||
const [selectedText, setSelectedText] = stateSelectedText;
|
const [selectedText, setSelectedText] = stateSelectedText;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -150,45 +159,54 @@ const topicWords = ({
|
|||||||
participants.response
|
participants.response
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div onMouseUp={onMouseUp} className="p-5 h-full w-full overflow-scroll">
|
<Container
|
||||||
|
onMouseUp={onMouseUp}
|
||||||
|
max-h="100%"
|
||||||
|
width="100%"
|
||||||
|
overflow="scroll"
|
||||||
|
maxW={{ lg: "container.md" }}
|
||||||
|
{...chakraProps}
|
||||||
|
>
|
||||||
{topicWithWords.response.wordsPerSpeaker.map(
|
{topicWithWords.response.wordsPerSpeaker.map(
|
||||||
(speakerWithWords, index) => (
|
(speakerWithWords, index) => (
|
||||||
<p key={index} className="mb-2 last:mb-0">
|
<Text key={index} className="mb-2 last:mb-0">
|
||||||
<span
|
<Box
|
||||||
|
as="span"
|
||||||
data-speaker={speakerWithWords.speaker}
|
data-speaker={speakerWithWords.speaker}
|
||||||
className={`
|
pt="1"
|
||||||
font-semibold ${
|
fontWeight="semibold"
|
||||||
selectedText == speakerWithWords.speaker
|
bgColor={
|
||||||
? "bg-yellow-200"
|
selectedText == speakerWithWords.speaker ? "yellow.200" : ""
|
||||||
: ""
|
}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{getSpeakerName(speakerWithWords.speaker)} :
|
{getSpeakerName(speakerWithWords.speaker)} :
|
||||||
</span>
|
</Box>
|
||||||
{speakerWithWords.words.map((word, index) => (
|
{speakerWithWords.words.map((word, index) => (
|
||||||
<span
|
<Box
|
||||||
|
as="span"
|
||||||
data-start={word.start}
|
data-start={word.start}
|
||||||
data-end={word.end}
|
data-end={word.end}
|
||||||
key={index}
|
key={index}
|
||||||
className={
|
pt="1"
|
||||||
|
bgColor={
|
||||||
selectedTextIsTimeSlice(selectedText) &&
|
selectedTextIsTimeSlice(selectedText) &&
|
||||||
selectedText.start <= word.start &&
|
selectedText.start <= word.start &&
|
||||||
selectedText.end >= word.end
|
selectedText.end >= word.end
|
||||||
? "bg-yellow-200"
|
? "yellow.200"
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{word.text}
|
{word.text}
|
||||||
</span>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</p>
|
</Text>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</div>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (topicWithWords.loading || participants.loading)
|
if (topicWithWords.loading || participants.loading)
|
||||||
return <WaveformLoading />;
|
return <Spinner size="xl" margin="auto" />;
|
||||||
if (topicWithWords.error || participants.error) return <p>error</p>;
|
if (topicWithWords.error || participants.error) return <p>error</p>;
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user