switch to chackra, bugfixes

This commit is contained in:
Sara
2023-12-21 13:56:37 +01:00
parent 5535f20fab
commit 89dc6ebb01
8 changed files with 384 additions and 201 deletions

View File

@@ -41,6 +41,9 @@ async def transcript_get_participants(
transcript_id, user_id=user_id
)
if transcript.participants is None:
return []
return [
Participant.model_validate(participant)
for participant in transcript.participants
@@ -59,7 +62,7 @@ async def transcript_add_participant(
)
# 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:
if p.speaker == participant.speaker:
raise HTTPException(

View File

@@ -91,7 +91,7 @@ export default async function RootLayout({ children, params }: LayoutProps) {
<Providers>
<div
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">
{/* Logo on the left */}

View File

@@ -1,5 +1,5 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import TopicHeader from "./topicHeader";
import TopicWords from "./topicWords";
import TopicPlayer from "./topicPlayer";
@@ -12,6 +12,7 @@ import getApi from "../../../../lib/getApi";
import useTranscript from "../../useTranscript";
import { useError } from "../../../../(errors)/errorContext";
import { useRouter } from "next/navigation";
import { Box, Grid } from "@chakra-ui/react";
export type TranscriptCorrect = {
params: {
@@ -50,57 +51,76 @@ export default function TranscriptCorrect({
};
return (
<div className="h-full grid grid-cols-2 gap-4">
<div className="flex flex-col h-full ">
<Grid
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
minW="100%"
stateCurrentTopic={stateCurrentTopic}
transcriptId={transcriptId}
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
stateSelectedText={stateSelectedText}
participants={participants}
topicWithWords={topicWithWords}
mb={{ md: "-3rem" }}
/>
</div>
<div className="flex flex-col justify-stretch">
{currentTopic ? (
<TopicPlayer
transcriptId={transcriptId}
selectedTime={
selectedTextIsTimeSlice(selectedText) ? selectedText : undefined
}
topicTime={{
start: currentTopic?.timestamp,
end: currentTopic?.timestamp + currentTopic?.duration,
}}
/>
) : (
<div></div>
)}
{participants.response && (
<div className="h-full flex flex-col justify-between">
<ParticipantList
{...{
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>
<ParticipantList
{...{
transcriptId,
participants,
topicWithWords,
stateSelectedText,
}}
/>
</Grid>
{transcript.response && !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>
)}
</Grid>
);
}

View File

@@ -6,6 +6,18 @@ import getApi from "../../../../lib/getApi";
import { UseParticipants } from "../../useParticipants";
import { selectedTextIsSpeaker, selectedTextIsTimeSlice } from "./types";
import { useError } from "../../../../(errors)/errorContext";
import {
Box,
Button,
Flex,
Text,
UnorderedList,
Input,
Kbd,
Spinner,
ListItem,
Grid,
} from "@chakra-ui/react";
type ParticipantList = {
participants: UseParticipants;
@@ -13,7 +25,6 @@ type ParticipantList = {
topicWithWords: any;
stateSelectedText: any;
};
// NTH re-order list when searching
const ParticipantList = ({
transcriptId,
participants,
@@ -58,12 +69,14 @@ const ParticipantList = ({
setAction("Create and assign");
setSelectedParticipant(undefined);
}
if (typeof selectedText == undefined) {
if (typeof selectedText == "undefined") {
inputRef.current?.blur();
setSelectedParticipant(undefined);
setAction(null);
}
}
}, [selectedText, participants.response]);
}, [selectedText, !participants.response]);
useEffect(() => {
document.onkeyup = (e) => {
@@ -250,6 +263,7 @@ const ParticipantList = ({
participants.refetch();
setParticipantInput("");
setLoading(false);
inputRef.current?.focus();
})
.catch((e) => {
setError(e, "There was an error creating");
@@ -320,48 +334,56 @@ const ParticipantList = ({
}
};
const anyLoading = loading || participants.loading || topicWithWords.loading;
return (
<div className="h-full" onClick={clearSelection}>
<div onClick={preventClick}>
<div className="grid grid-cols-7 gap-2 mb-2">
<input
<Box h="100%" onClick={clearSelection} width="100%">
<Grid
onClick={preventClick}
maxH="100%"
templateRows="auto minmax(0, 1fr)"
min-w="100%"
>
<Flex direction="column" p="2">
<Input
ref={inputRef}
onChange={changeParticipantInput}
value={participantInput}
className="border col-span-3 border-blue-400 p-1"
mb="2"
placeholder="Participant Name"
/>
{action ? (
<button
onClick={doAction}
className="p-2 bg-blue-200 w-full col-span-3"
>
[
<FontAwesomeIcon
icon={faArrowTurnDown}
className="rotate-90 h-2"
/>
]{" " + action}
</button>
) : (
<div className="col-span-3" />
)}
{loading ||
participants.loading ||
(topicWithWords.loading && (
<FontAwesomeIcon
icon={faSpinner}
className="animate-spin-slow text-gray-300 h-8"
/>
))}
</div>
<Button
onClick={doAction}
colorScheme="blue"
disabled={!action || anyLoading}
>
{action || !anyLoading ? (
<>
<Kbd color="blue.500" pt="1" mr="1">
<FontAwesomeIcon
icon={faArrowTurnDown}
className="rotate-90 h-3"
/>
</Kbd>
{action || "Create"}
</>
) : (
<Spinner />
)}
</Button>
</Flex>
{participants.response && (
<ul>
<UnorderedList
mx="0"
mb={{ base: 2, md: 4 }}
maxH="100%"
overflow="scroll"
>
{participants.response.map((participant: Participant) => (
<li
<ListItem
onClick={selectParticipant(participant)}
className={
"flex flex-row justify-between border-b last:border-b-0 py-2 " +
(participantInput.length > 0 &&
selectedText &&
participant.name.startsWith(participantInput)
@@ -371,69 +393,88 @@ const ParticipantList = ({
? "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) &&
!selectedParticipant &&
!loading && (
<button
<Button
onClick={mergeSpeaker(selectedText, participant)}
className="bg-blue-400 px-2 ml-2"
colorScheme="blue"
ml="2"
size="sm"
>
{oneMatch?.id == participant.id &&
action == "Create to rename" && (
<>
<span className="text-xs">
[CTRL +{" "}
<FontAwesomeIcon
icon={faArrowTurnDown}
className="rotate-90 mr-2 h-2"
/>
]
</span>
</>
)}{" "}
<Kbd
letterSpacing="-1px"
color="blue.500"
mr="1"
pt="3px"
>
Ctrl +&nbsp;
<FontAwesomeIcon
icon={faArrowTurnDown}
className="rotate-90 h-2"
/>
</Kbd>
)}
Merge
</button>
</Button>
)}
{selectedTextIsTimeSlice(selectedText) && !loading && (
<button
<Button
onClick={assignTo(participant)}
className="bg-blue-400 px-2 ml-2"
colorScheme="blue"
ml="2"
size="sm"
>
{oneMatch?.id == participant.id &&
action == "Create and assign" && (
<>
<span className="text-xs">
[CTRL +{" "}
<FontAwesomeIcon
icon={faArrowTurnDown}
className="rotate-90 mr-2 h-2"
/>
]
</span>
</>
<Kbd
letterSpacing="-1px"
color="blue.500"
mr="1"
pt="3px"
>
Ctrl +&nbsp;
<FontAwesomeIcon
icon={faArrowTurnDown}
className="rotate-90 h-2"
/>
</Kbd>
)}{" "}
Assign
</button>
</Button>
)}
<button
<Button
onClick={deleteParticipant(participant.id)}
className="bg-blue-400 px-2 ml-2"
colorScheme="blue"
ml="2"
size="sm"
>
Delete
</button>
</div>
</li>
</Button>
</Box>
</ListItem>
))}
</ul>
</UnorderedList>
)}
</div>
</div>
</Grid>
</Box>
);
};

View File

@@ -1,5 +1,5 @@
export default ({ playing }) => (
<div className="flex justify-between w-16 h-8 m-auto">
<div className="flex justify-between w-14 h-6">
<div
className={`bg-blue-400 rounded w-2 ${
playing ? "animate-wave-quiet" : ""

View File

@@ -1,8 +1,19 @@
import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import useTopics from "../../useTopics";
import { Dispatch, SetStateAction, useEffect } from "react";
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 = {
stateCurrentTopic: [
@@ -17,7 +28,8 @@ export default function TopicHeader({
stateCurrentTopic,
transcriptId,
topicWithWordsLoading,
}: TopicHeader) {
...chakraProps
}: TopicHeader & BoxProps) {
const [currentTopic, setCurrentTopic] = stateCurrentTopic;
const topics = useTopics(transcriptId);
@@ -26,17 +38,13 @@ export default function TopicHeader({
const sessionTopic = window.localStorage.getItem(
transcriptId + "correct",
);
console.log(sessionTopic, window.localStorage);
if (sessionTopic && topics?.topics?.find((t) => t.id == sessionTopic)) {
setCurrentTopic(topics?.topics?.find((t) => t.id == sessionTopic));
console.log("he", sessionTopic, !!sessionTopic);
} else {
setCurrentTopic(topics?.topics?.at(0));
console.log("hi");
}
}
}, [topics.loading]);
// console.log(currentTopic)
const number = topics.topics?.findIndex(
(topic) => topic.id == currentTopic?.id,
@@ -75,35 +83,85 @@ export default function TopicHeader({
};
});
if (topics.topics && currentTopic && typeof number == "number") {
return (
<div className="flex flex-row">
<button
className={`w-10 h-10 rounded-full p-2 border border-gray-300 disabled:bg-white ${
canGoPrevious ? "text-gray-500" : "text-gray-300"
}`}
const isLoaded = !!(
topics.topics &&
currentTopic &&
typeof number == "number"
);
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}
disabled={!canGoPrevious}
size="40px"
border="1px"
color={canGoPrevious ? "inherit" : "gray"}
borderColor={canGoNext ? "body-text" : "gray"}
>
<FontAwesomeIcon icon={faArrowLeft} />
</button>
<h1 className="flex-grow">
{currentTopic.title}{" "}
<span>
{number + 1}/{total}
</span>
</h1>
<button
className={`w-10 h-10 rounded-full p-2 border border-gray-300 disabled:bg-white ${
canGoNext ? "text-gray-500" : "text-gray-300"
}`}
{canGoPrevious ? (
<Kbd>
<ChevronLeftIcon />
</Kbd>
) : (
<ChevronLeftIcon />
)}
</Circle>
</SkeletonCircle>
<Skeleton
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}
disabled={!canGoNext}
size="40px"
border="1px"
color={canGoNext ? "inherit" : "gray"}
borderColor={canGoNext ? "body-text" : "gray"}
>
<FontAwesomeIcon icon={faArrowRight} />
</button>
</div>
);
}
return null;
{canGoNext ? (
<Kbd>
<ChevronRightIcon />
</Kbd>
) : (
<ChevronRightIcon />
)}
</Circle>
</SkeletonCircle>
</Box>
);
}

View File

@@ -3,18 +3,29 @@ import useMp3 from "../../useMp3";
import { formatTime } from "../../../../lib/time";
import SoundWaveCss from "./soundWaveCss";
import { TimeSlice } from "./types";
import {
BoxProps,
Button,
Wrap,
Text,
WrapItem,
Kbd,
Skeleton,
chakra,
} from "@chakra-ui/react";
type TopicPlayer = {
transcriptId: string;
selectedTime: TimeSlice | undefined;
topicTime: TimeSlice;
topicTime: TimeSlice | undefined;
};
const TopicPlayer = ({
transcriptId,
selectedTime,
topicTime,
}: TopicPlayer) => {
...chakraProps
}: TopicPlayer & BoxProps) => {
const mp3 = useMp3(transcriptId);
const [isPlaying, setIsPlaying] = useState(false);
const [endTopicCallback, setEndTopicCallback] = useState<() => void>();
@@ -46,6 +57,7 @@ const TopicPlayer = ({
});
const calcShowTime = () => {
if (!topicTime) return;
setShowTime(
`${
mp3.media?.currentTime
@@ -67,6 +79,7 @@ const TopicPlayer = ({
setEndTopicCallback(
() =>
function () {
if (!topicTime) return;
if (mp3.media && mp3.media.currentTime >= topicTime.end) {
mp3.media.pause();
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
setTimeout(() => {
if (mp3.media) {
if (!topicTime) return;
mp3.media.currentTime = topicTime.start;
setShowTime(`00:00/${formatTime(topicTime.end - topicTime.start)}`);
}
}, 10);
setIsPlaying(false);
}
}, [!mp3.media, topicTime.start, topicTime.end]);
}, [!mp3.media, topicTime?.start, topicTime?.end]);
useEffect(() => {
endTopicCallback &&
@@ -141,6 +155,7 @@ const TopicPlayer = ({
const playTopic = (e) => {
e?.preventDefault();
e?.target?.blur();
if (!topicTime) return;
if (mp3.media) {
mp3.media.currentTime = topicTime.start;
mp3.media.play();
@@ -165,37 +180,65 @@ const TopicPlayer = ({
mp3.media?.pause();
setIsPlaying(false);
};
console.log(topicTime);
if (mp3.media) {
return (
<div className="mb-4 grid grid-cols-3 gap-2">
<SoundWaveCss playing={isPlaying} />
<div className="col-span-2">{showTime}</div>
<button className="p-2 bg-blue-200 w-full" onClick={playTopic}>
Play From Start
</button>
{!isPlaying ? (
<button
ref={playButton}
id="playButton"
className="p-2 bg-blue-200 w-full"
onClick={playCurrent}
const isLoaded = !!(mp3.media && topicTime);
return (
<Skeleton
isLoaded={isLoaded}
h={isLoaded ? "auto" : "40px"}
fadeDuration={1}
w={isLoaded ? "auto" : "container.md"}
margin="auto"
{...chakraProps}
>
<Wrap spacing="4" justify="center" align="center">
<WrapItem>
<SoundWaveCss playing={isPlaying} />
<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>&nbsp;Play
</Button>
) : (
<Button
onClick={pause}
ref={playButton}
id="playButton"
colorScheme="blue"
w="120px"
>
<Kbd color="blue.600">Space</Kbd>&nbsp;Pause
</Button>
)}
</WrapItem>
<WrapItem visibility={selectedTime ? "visible" : "hidden"}>
<Button
disabled={!selectedTime}
onClick={playSelection}
colorScheme="blue"
>
<span className="text-xs">[SPACE]</span> Play
</button>
) : (
<button className="p-2 bg-blue-200 w-full" onClick={pause}>
<span className="text-xs">[SPACE]</span> Pause
</button>
)}
{selectedTime && (
<button className="p-2 bg-blue-200 w-full" onClick={playSelection}>
<span className="text-xs">[,]</span>Play Selection
</button>
)}
</div>
);
}
<Kbd color="blue.600">,</Kbd>&nbsp;Play selection
</Button>
</WrapItem>
</Wrap>
</Skeleton>
);
};
export default TopicPlayer;

View File

@@ -3,6 +3,14 @@ import WaveformLoading from "../../waveformLoading";
import { UseParticipants } from "../../useParticipants";
import { UseTopicWithWords } from "../../useTopicWithWords";
import { TimeSlice, selectedTextIsTimeSlice } from "./types";
import {
BoxProps,
Box,
Container,
Text,
chakra,
Spinner,
} from "@chakra-ui/react";
type TopicWordsProps = {
stateSelectedText: [
@@ -17,7 +25,8 @@ const topicWords = ({
stateSelectedText,
participants,
topicWithWords,
}: TopicWordsProps) => {
...chakraProps
}: TopicWordsProps & BoxProps) => {
const [selectedText, setSelectedText] = stateSelectedText;
useEffect(() => {
@@ -150,45 +159,54 @@ const topicWords = ({
participants.response
) {
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(
(speakerWithWords, index) => (
<p key={index} className="mb-2 last:mb-0">
<span
<Text key={index} className="mb-2 last:mb-0">
<Box
as="span"
data-speaker={speakerWithWords.speaker}
className={`
font-semibold ${
selectedText == speakerWithWords.speaker
? "bg-yellow-200"
: ""
}`}
pt="1"
fontWeight="semibold"
bgColor={
selectedText == speakerWithWords.speaker ? "yellow.200" : ""
}
>
{getSpeakerName(speakerWithWords.speaker)}&nbsp;:&nbsp;
</span>
</Box>
{speakerWithWords.words.map((word, index) => (
<span
<Box
as="span"
data-start={word.start}
data-end={word.end}
key={index}
className={
pt="1"
bgColor={
selectedTextIsTimeSlice(selectedText) &&
selectedText.start <= word.start &&
selectedText.end >= word.end
? "bg-yellow-200"
? "yellow.200"
: ""
}
>
{word.text}
</span>
</Box>
))}
</p>
</Text>
),
)}
</div>
</Container>
);
}
if (topicWithWords.loading || participants.loading)
return <WaveformLoading />;
return <Spinner size="xl" margin="auto" />;
if (topicWithWords.error || participants.error) return <p>error</p>;
return null;
};