layout changes

This commit is contained in:
Sara
2023-09-18 16:31:23 +02:00
parent b6c65805f8
commit e59dbf2df2
11 changed files with 220 additions and 187 deletions

View File

@@ -56,16 +56,16 @@ export const metadata: Metadata = {
export default function RootLayout({ children }) { export default function RootLayout({ children }) {
return ( return (
<html lang="en"> <html lang="en">
<body className={poppins.className + " flex flex-col min-h-screen"}> <body className={poppins.className + " h-screen"}>
<FiefWrapper> <FiefWrapper>
<ErrorProvider> <ErrorProvider>
<ErrorMessage /> <ErrorMessage />
{/*TODO lvh or svh ? */} {/*TODO lvh or svh ? */}
<div <div
id="container" id="container"
className="flex flex-col items-center min-h-[100svh]" className="flex flex-col items-center min-h-[100svh] p-2 md:p-4"
> >
<header className="flex justify-between items-center p-2 md:p-4 w-full"> <header className="flex justify-between items-center w-full mb-4 md:mb-4">
{/* Logo on the left */} {/* Logo on the left */}
<div className="flex"> <div className="flex">
<Image <Image

View File

@@ -8,12 +8,12 @@ button {
padding: 0; padding: 0;
cursor: pointer; cursor: pointer;
/* Visual */ /* Visual */
background-color: #3e68ff; /* background-color: #3e68ff; */
color: #fff; /* color: #fff; */
border-radius: 8px; border-radius: 8px;
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.18); /* box-shadow: 0 3px 5px rgba(0, 0, 0, 0.18); */
/* Size */ /* Size */
padding: 0.25em 0.75em; padding: 0.4em 1em;
min-width: 10ch; min-width: 10ch;
min-height: 44px; min-height: 44px;
/* Text */ /* Text */
@@ -23,11 +23,18 @@ button {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
align-self: start; /* align-self: start; */
/* Optional - see "Gotchas" */ /* Optional - see "Gotchas" */
/* Animation */ /* Animation */
transition: 220ms all ease-in-out; transition: 220ms all ease-in-out;
display: block; }
@media (max-width: 768px) {
input[type="button"],
button {
padding: 0.25em 0.75em;
min-height: 30px;
}
} }
/* Button modifiers */ /* Button modifiers */
@@ -55,13 +62,6 @@ button[disabled]:hover {
cursor: not-allowed !important; cursor: not-allowed !important;
} }
/* Custom button properties */
input[type="button"],
button {
width: 243px;
border: solid 1px #dadada;
}
/* Red button states */ /* Red button states */
input[type="button"][data-color="red"], input[type="button"][data-color="red"],
button[data-color="red"], button[data-color="red"],

View File

@@ -10,8 +10,6 @@
body { body {
background: white; background: white;
font-family: "Roboto", sans-serif;
} }
.temp-transcription { .temp-transcription {

View File

@@ -34,7 +34,7 @@ export default function TranscriptDetails(details: TranscriptDetails) {
return ( return (
<> <>
<div className="w-full flex flex-col items-center h-[100svh]"> <div className="w-full flex flex-col items-center">
{transcript?.loading === true || {transcript?.loading === true ||
waveform?.loading == true || waveform?.loading == true ||
topics?.loading == true ? ( topics?.loading == true ? (

View File

@@ -0,0 +1,36 @@
import React, { useRef, useEffect, useState } from "react";
import Dropdown, { Option } from "react-dropdown";
import "react-dropdown/style.css";
const AudioInputsDropdown: React.FC<{
audioDevices?: Option[];
setDeviceId: React.Dispatch<React.SetStateAction<string | null>>;
disabled?: boolean;
}> = (props) => {
const [ddOptions, setDdOptions] = useState<Option[]>([]);
useEffect(() => {
if (props.audioDevices) {
setDdOptions(props.audioDevices);
props.setDeviceId(
props.audioDevices.length > 0 ? props.audioDevices[0].value : null,
);
}
}, [props.audioDevices]);
const handleDropdownChange = (option: Option) => {
props.setDeviceId(option.value);
};
return (
<Dropdown
options={ddOptions}
onChange={handleDropdownChange}
value={ddOptions[0]}
disabled={props.disabled}
/>
);
};
export default AudioInputsDropdown;

View File

@@ -34,6 +34,7 @@ export function Dashboard({
useEffect(() => { useEffect(() => {
if (autoscrollEnabled) scrollToBottom(); if (autoscrollEnabled) scrollToBottom();
console.log(topics);
}, [topics.length]); }, [topics.length]);
const scrollToBottom = () => { const scrollToBottom = () => {
@@ -54,69 +55,86 @@ export function Dashboard({
} }
}; };
const faketopic = {
id: "641fbc68-dc2e-4c9d-aafd-89bdbf8cfc26",
summary:
"Explore the history of hypnotica music, a genre that has been deeply influential on modern music. From its origins in the 60s to its current status, this music has a unique and hypnotic quality that can be felt in the gut. Dive into the canon of modern hypnotica and discover its impact on music today.",
timestamp: 0,
title: "The Origins and Influence of Hypnotica Music",
transcript:
" vertically oriented music ultimately hypnotic So, that's what we're talking about. Uh, when does it start? I mean, technically, I think... It's always been here but Hypnotica, much like Exotica, which is also sort of a fraught genre. a sort of a western interpretive genre, a fetishizing genre. I would say, uh, it starts in the 60s when all these wh- weird things started, you know, and I started fucking around and... You can go into Woodstock or whatever, that's usually when these things start. Anything that ends with a at the end of a word usually started in By some dirty hippie. Yeah. By some dirty hippie. Yeah. It was like, uh. Okay. So. That's hypnotica, I don't care to explain it to be honest I think everyone can feel it in their gut. We're mostly gonna ex- Explore this kind of the canon of the modern canon of what what I might call hypnotic It's been deeply influential on me and, uh...",
};
const faketopics = new Array(10).fill(faketopic);
return ( return (
<> <div className="py-4 grid grid-cols-1 lg:grid-cols-2 gap-2 lg:gap-4 grid-rows-2 lg:grid-rows-1 h-outer-dashboard md:h-outer-dashboard-md lg:h-outer-dashboard-lg">
<div className="relative h-[60svh] w-3/4 flex flex-col"> {/* Topic Section */}
<div className="text-center pb-1 pt-4"> <section className="relative w-full h-auto max-h-full bg-blue-400/20 rounded-lg md:rounded-xl px-2 md:px-4 flex flex-col justify-center align-center">
<h1 className="text-2xl font-bold text-blue-500">Meeting Notes</h1> {faketopics.length > 0 ? (
</div> <>
<ScrollToBottom
visible={!autoscrollEnabled}
hasFinalSummary={finalSummary ? true : false}
handleScrollBottom={scrollToBottom}
/>
<ScrollToBottom <div
visible={!autoscrollEnabled} id="topics-div"
hasFinalSummary={finalSummary ? true : false} className="overflow-y-auto h-auto max-h-full"
handleScrollBottom={scrollToBottom} onScroll={handleScroll}
/> >
{faketopics.map((item, index) => (
<div <div
id="topics-div" key={index}
className="py-2 overflow-y-auto" className="border-b-2 last:border-none px-2 md:px-4 py-2 hover:bg-blue-400/20"
onScroll={handleScroll} role="button"
> onClick={() =>
{topics.map((item, index) => ( setActiveTopic(activeTopic?.id == item.id ? null : item)
<div key={index} className="border-b-2 py-2 hover:bg-[#8ec5fc30]"> }
<div >
className="flex justify-between items-center cursor-pointer px-4" <div className="flex justify-between items-center rounded-lg md:rounded-xl text-l md:text-xl font-bold">
onClick={() => <p>
setActiveTopic(activeTopic?.id == item.id ? null : item) <span className="font-light text-slate-500 pr-1">
} [{formatTime(item.timestamp)}]
> </span>
<div className="flex justify-between items-center"> &nbsp;
<span className="font-light text-slate-500 pr-1"> <span className="pr-1">{item.title}</span>
[{formatTime(item.timestamp)}] </p>
</span>{" "} <FontAwesomeIcon
<span className="pr-1">{item.title}</span> className="transform transition-transform duration-200"
<FontAwesomeIcon icon={
className={`transform transition-transform duration-200`} activeTopic?.id == item.id
icon={ ? faChevronDown
activeTopic?.id == item.id : faChevronRight
? faChevronDown }
: faChevronRight />
} </div>
/> {activeTopic?.id == item.id && (
<div className="p-2 mt-2 -mb-2 h-fit">
{item.transcript}
</div>
)}
</div> </div>
</div> ))}
{activeTopic?.id == item.id && (
<div className="p-2 mt-2 -mb-2 bg-slate-50 rounded">
{item.transcript}
</div>
)}
</div> </div>
))} </>
{topics.length === 0 && ( ) : (
<div className="text-center text-gray-500"> <div className="text-center text-gray-500 p-4">
Discussion topics will appear here after you start recording. It Discussion topics will appear here after you start recording. It may
may take up to 5 minutes of conversation for the first topic to take up to 5 minutes of conversation for the first topic to appear.
appear. </div>
</div> )}
)} </section>
</div>
{finalSummary.summary && <FinalSummary text={finalSummary.summary} />} <section className="relative w-full h-auto max-h-full bg-blue-400/20 rounded-lg md:rounded-xl px-2 md:px-4 flex flex-col justify-center align-center">
</div> {finalSummary.summary ? (
<FinalSummary text={finalSummary.summary} />
) : (
<LiveTrancription text={transcriptionText} />
)}
</section>
{disconnected && <DisconnectedIndicator />} {disconnected && <DisconnectedIndicator />}
</div>
<LiveTrancription text={transcriptionText} />
</>
); );
} }

View File

@@ -4,8 +4,8 @@ type FinalSummaryProps = {
export default function FinalSummary(props: FinalSummaryProps) { export default function FinalSummary(props: FinalSummaryProps) {
return ( return (
<div className="mt-2 p-2 bg-white temp-transcription rounded"> <div className="overflow-y-auto h-auto max-h-full">
<h2>Final Summary</h2> <h2 className="text-lg">Final Summary</h2>
<p>{props.text}</p> <p>{props.text}</p>
</div> </div>
); );

View File

@@ -4,8 +4,11 @@ type LiveTranscriptionProps = {
export default function LiveTrancription(props: LiveTranscriptionProps) { export default function LiveTrancription(props: LiveTranscriptionProps) {
return ( return (
<div className="h-[7svh] w-full bg-gray-800 text-white text-center py-4 text-2xl"> <div className="text-center p-4">
&nbsp;{props.text}&nbsp; <p className="text-lg md:text-xl font-bold">
{/* Nous allons prendre quelques appels téléphoniques et répondre à quelques questions */}
{props.text}
</p>
</div> </div>
); );
} }

View File

@@ -9,6 +9,7 @@ import useAudioDevice from "../useAudioDevice";
import "../../styles/button.css"; import "../../styles/button.css";
import { Topic } from "../webSocketTypes"; import { Topic } from "../webSocketTypes";
import getApi from "../../lib/getApi"; import getApi from "../../lib/getApi";
import AudioInputsDropdown from "../audioInputsDropdown";
const TranscriptCreate = () => { const TranscriptCreate = () => {
const [stream, setStream] = useState<MediaStream | null>(null); const [stream, setStream] = useState<MediaStream | null>(null);
@@ -38,8 +39,14 @@ const TranscriptCreate = () => {
getAudioStream, getAudioStream,
} = useAudioDevice(); } = useAudioDevice();
const getCurrentStream = async () => {
return audioDevices.length
? await getAudioStream(audioDevices[0].value)
: null;
};
return ( return (
<div className="w-full flex flex-col items-center h-[100svh]"> <div className="w-full">
{permissionOk ? ( {permissionOk ? (
<> <>
<Recorder <Recorder
@@ -49,8 +56,7 @@ const TranscriptCreate = () => {
setStream(null); setStream(null);
}} }}
topics={webSockets.topics} topics={webSockets.topics}
getAudioStream={getAudioStream} getAudioStream={getCurrentStream}
audioDevices={audioDevices}
useActiveTopic={useActiveTopic} useActiveTopic={useActiveTopic}
isPastMeeting={false} isPastMeeting={false}
/> />
@@ -65,7 +71,7 @@ const TranscriptCreate = () => {
</> </>
) : ( ) : (
<> <>
<div className="flex flex-col items-center justify-center w-fit px-6 py-8 mt-8 rounded-xl"> <div className="flex flex-col w-full items-center justify-center px-6 py-8 mt-8 rounded-xl">
<h1 className="text-2xl font-bold">Audio Permissions</h1> <h1 className="text-2xl font-bold">Audio Permissions</h1>
{loading ? ( {loading ? (
<p className="text-gray-500 text-center mt-5"> <p className="text-gray-500 text-center mt-5">

View File

@@ -7,49 +7,15 @@ import CustomRegionsPlugin from "../lib/custom-plugins/regions";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faDownload } from "@fortawesome/free-solid-svg-icons"; import { faDownload } from "@fortawesome/free-solid-svg-icons";
import Dropdown, { Option } from "react-dropdown";
import "react-dropdown/style.css";
import { formatTime } from "../lib/time"; import { formatTime } from "../lib/time";
import { Topic } from "./webSocketTypes"; import { Topic } from "./webSocketTypes";
import { AudioWaveform } from "../api"; import { AudioWaveform } from "../api";
const AudioInputsDropdown: React.FC<{
audioDevices?: Option[];
setDeviceId: React.Dispatch<React.SetStateAction<string | null>>;
disabled: boolean;
}> = (props) => {
const [ddOptions, setDdOptions] = useState<Option[]>([]);
useEffect(() => {
if (props.audioDevices) {
setDdOptions(props.audioDevices);
props.setDeviceId(
props.audioDevices.length > 0 ? props.audioDevices[0].value : null,
);
}
}, [props.audioDevices]);
const handleDropdownChange = (option: Option) => {
props.setDeviceId(option.value);
};
return (
<Dropdown
options={ddOptions}
onChange={handleDropdownChange}
value={ddOptions[0]}
disabled={props.disabled}
/>
);
};
type RecorderProps = { type RecorderProps = {
setStream?: React.Dispatch<React.SetStateAction<MediaStream | null>>; setStream?: React.Dispatch<React.SetStateAction<MediaStream | null>>;
onStop?: () => void; onStop?: () => void;
topics: Topic[]; topics: Topic[];
getAudioStream?: (deviceId: string | null) => Promise<MediaStream | null>; getAudioStream?: () => Promise<MediaStream | null>;
audioDevices?: Option[];
useActiveTopic: [ useActiveTopic: [
Topic | null, Topic | null,
React.Dispatch<React.SetStateAction<Topic | null>>, React.Dispatch<React.SetStateAction<Topic | null>>,
@@ -66,7 +32,6 @@ export default function Recorder(props: RecorderProps) {
const [isRecording, setIsRecording] = useState<boolean>(false); const [isRecording, setIsRecording] = useState<boolean>(false);
const [hasRecorded, setHasRecorded] = useState<boolean>(props.isPastMeeting); const [hasRecorded, setHasRecorded] = useState<boolean>(props.isPastMeeting);
const [isPlaying, setIsPlaying] = useState<boolean>(false); const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [deviceId, setDeviceId] = useState<string | null>(null);
const [currentTime, setCurrentTime] = useState<number>(0); const [currentTime, setCurrentTime] = useState<number>(0);
const [timeInterval, setTimeInterval] = useState<number | null>(null); const [timeInterval, setTimeInterval] = useState<number | null>(null);
const [duration, setDuration] = useState<number>(0); const [duration, setDuration] = useState<number>(0);
@@ -88,7 +53,7 @@ export default function Recorder(props: RecorderProps) {
hideScrollbar: true, hideScrollbar: true,
autoCenter: true, autoCenter: true,
barWidth: 2, barWidth: 2,
height: 90, height: "auto",
url: props.transcriptId url: props.transcriptId
? `${process.env.NEXT_PUBLIC_API_URL}/v1/transcripts/${props.transcriptId}/audio/mp3` ? `${process.env.NEXT_PUBLIC_API_URL}/v1/transcripts/${props.transcriptId}/audio/mp3`
: undefined, : undefined,
@@ -222,7 +187,7 @@ export default function Recorder(props: RecorderProps) {
setIsRecording(false); setIsRecording(false);
setHasRecorded(true); setHasRecorded(true);
} else if (props.getAudioStream) { } else if (props.getAudioStream) {
const stream = await props.getAudioStream(deviceId); const stream = await props.getAudioStream();
if (props.setStream) props.setStream(stream); if (props.setStream) props.setStream(stream);
waveRegions?.clearRegions(); waveRegions?.clearRegions();
@@ -246,68 +211,65 @@ export default function Recorder(props: RecorderProps) {
}; };
return ( return (
<div className="relative flex flex-col items-center justify-center max-w-[75vw] w-full"> <div className="flex items-center w-full">
<div className="flex my-2 mx-auto audio-source-dropdown"> <div className="flex-grow items-end relative">
{!hasRecorded && ( <div
<> ref={waveformRef}
<AudioInputsDropdown className="flex-grow shadow-lg md:shadow-xl rounded-2xl h-20"
audioDevices={props.audioDevices} ></div>
setDeviceId={setDeviceId} <div className="absolute right-2 bottom-0">
disabled={isRecording} {isRecording && (
/> <div className="inline-block bg-red-500 rounded-full w-2 h-2 my-auto mr-1 animate-ping"></div>
&nbsp; )}
<button {timeLabel()}
className="w-20" </div>
onClick={handleRecClick}
data-color={isRecording ? "red" : "blue"}
disabled={!deviceId}
>
{isRecording ? "Stop" : "Record"}
</button>
&nbsp;
</>
)}
{hasRecorded && (
<>
<button
className="w-20"
id="play-btn"
onClick={handlePlayClick}
data-color={isPlaying ? "orange" : "green"}
>
{isPlaying ? "Pause" : "Play"}
</button>
{props.transcriptId && (
<a
title="Download recording"
className="w-9 m-auto text-center cursor-pointer text-blue-300 hover:text-blue-700"
href={`${process.env.NEXT_PUBLIC_API_URL}/v1/transcripts/${props.transcriptId}/audio/mp3`}
>
<FontAwesomeIcon icon={faDownload} />
</a>
)}
{!props.transcriptId && (
<a
id="download-recording"
title="Download recording"
className="invisible w-9 m-auto text-center cursor-pointer text-blue-300 hover:text-blue-700"
>
<FontAwesomeIcon icon={faDownload} />
</a>
)}
</>
)}
</div>
<div ref={waveformRef} className="w-full shadow-xl rounded-2xl"></div>
<div className="absolute bottom-0 right-2 text-xs text-black">
{isRecording && (
<div className="inline-block bg-red-500 rounded-full w-2 h-2 my-auto mr-1 animate-ping"></div>
)}
{timeLabel()}
</div> </div>
{hasRecorded && (
<>
<button
className={`${
isPlaying ? "bg-orange-400" : "bg-green-400"
} ml-2 md:ml:4 text-white`}
id="play-btn"
onClick={handlePlayClick}
disabled={isRecording}
>
{isPlaying ? "Pause" : "Play"}
</button>
{props.transcriptId && (
<a
title="Download recording"
className="text-center cursor-pointer text-blue-400 hover:text-blue-700 ml-2 md:ml:4 p-2"
href={`${process.env.NEXT_PUBLIC_API_URL}/v1/transcripts/${props.transcriptId}/audio/mp3`}
>
<FontAwesomeIcon icon={faDownload} />
</a>
)}
{!props.transcriptId && (
<a
id="download-recording"
title="Download recording"
className="invisible text-center cursor-pointer text-blue-400 hover:text-blue-700 ml-2 md:ml:4 p-2"
>
<FontAwesomeIcon icon={faDownload} />
</a>
)}
</>
)}
{!hasRecorded && (
<button
className={`${
isRecording ? "bg-red-400" : "bg-blue-400"
} text-white ml-2 md:ml:4`}
onClick={handleRecClick}
disabled={isPlaying}
>
{isRecording ? "Stop" : "Record"}
</button>
)}
</div> </div>
); );
} }

View File

@@ -1,4 +1,14 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
// 8 margin main container Top + 40 height header + 16 margin bottom header + 80 recorder
const dashboardStart = 144;
// 8 margin main container Top + 40 height header + 16 margin bottom header + 80 recorder
const dashboardStartMd = 144;
// 16 margin main container Top + 64 height header + 16 margin bottom header + 80 recorder
const dashboardStartLg = 176;
module.exports = { module.exports = {
content: [ content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}", "./pages/**/*.{js,ts,jsx,tsx,mdx}",
@@ -7,10 +17,10 @@ module.exports = {
], ],
theme: { theme: {
extend: { extend: {
backgroundImage: { height: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))", "outer-dashboard": `calc(100svh - ${dashboardStart}px)`,
"gradient-conic": "outer-dashboard-md": `calc(100svh - ${dashboardStartMd + 34}px)`,
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", "outer-dashboard-lg": `calc(100svh - ${dashboardStartLg}px)`,
}, },
}, },
}, },