microphone switch and design improvements

This commit is contained in:
Sara
2023-09-20 17:08:47 +02:00
parent fe510238c0
commit 2576a6e4e2
4 changed files with 134 additions and 97 deletions

View File

@@ -23,7 +23,7 @@ const ErrorMessage: React.FC = () => {
setIsVisible(false); setIsVisible(false);
setError(null); setError(null);
}} }}
className="max-w-xs z-50 fixed top-16 right-10 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded transition-opacity duration-300 ease-out opacity-100 hover:opacity-75 cursor-pointer transform hover:scale-105" className="max-w-xs z-50 fixed bottom-5 right-5 md:bottom-10 md:right-10 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded transition-opacity duration-300 ease-out opacity-100 hover:opacity-80 cursor-pointer transform hover:scale-105"
role="alert" role="alert"
> >
<span className="block sm:inline">{error?.message}</span> <span className="block sm:inline">{error?.message}</span>

View File

@@ -1,5 +1,3 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faMicrophone } from "@fortawesome/free-solid-svg-icons";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Dropdown, { Option } from "react-dropdown"; import Dropdown, { Option } from "react-dropdown";
import "react-dropdown/style.css"; import "react-dropdown/style.css";
@@ -7,36 +5,34 @@ import "react-dropdown/style.css";
const AudioInputsDropdown: React.FC<{ const AudioInputsDropdown: React.FC<{
audioDevices: Option[]; audioDevices: Option[];
disabled: boolean; disabled: boolean;
hide: () => void;
setDeviceId: React.Dispatch<React.SetStateAction<string | null>>; setDeviceId: React.Dispatch<React.SetStateAction<string | null>>;
}> = ({ audioDevices, disabled, setDeviceId }) => { }> = (props) => {
const [ddOptions, setDdOptions] = useState<Option[]>([]); const [ddOptions, setDdOptions] = useState<Option[]>([]);
useEffect(() => { useEffect(() => {
if (audioDevices) { if (props.audioDevices) {
setDdOptions(audioDevices); setDdOptions(props.audioDevices);
setDeviceId(audioDevices.length > 0 ? audioDevices[0].value : null); props.setDeviceId(
props.audioDevices.length > 0 ? props.audioDevices[0].value : null,
);
} }
}, [audioDevices]); }, [props.audioDevices]);
const handleDropdownChange = (option: Option) => { const handleDropdownChange = (option: Option) => {
setDeviceId(option.value); props.setDeviceId(option.value);
props.hide();
}; };
if (audioDevices?.length > 0) { return (
return ( <Dropdown
<div className="flex w-full items-center"> options={ddOptions}
<FontAwesomeIcon icon={faMicrophone} className="p-2" /> onChange={handleDropdownChange}
<Dropdown value={ddOptions[0]}
options={ddOptions} className="flex-grow w-full"
onChange={handleDropdownChange} disabled={props.disabled}
value={ddOptions[0]} />
className="flex-grow" );
disabled={disabled}
/>
</div>
);
}
return null;
}; };
export default AudioInputsDropdown; export default AudioInputsDropdown;

View File

@@ -17,8 +17,6 @@ const TranscriptCreate = () => {
const [stream, setStream] = useState<MediaStream | null>(null); const [stream, setStream] = useState<MediaStream | null>(null);
const [disconnected, setDisconnected] = useState<boolean>(false); const [disconnected, setDisconnected] = useState<boolean>(false);
const useActiveTopic = useState<Topic | null>(null); const useActiveTopic = useState<Topic | null>(null);
const [deviceId, setDeviceId] = useState<string | null>(null);
const [recordStarted, setRecordStarted] = useState(false);
useEffect(() => { useEffect(() => {
if (process.env.NEXT_PUBLIC_ENV === "development") { if (process.env.NEXT_PUBLIC_ENV === "development") {
@@ -43,17 +41,6 @@ const TranscriptCreate = () => {
getAudioStream, getAudioStream,
} = useAudioDevice(); } = useAudioDevice();
const getCurrentStream = async () => {
setRecordStarted(true);
return deviceId ? await getAudioStream(deviceId) : null;
};
useEffect(() => {
if (audioDevices.length > 0) {
setDeviceId[audioDevices[0].value];
}
}, [audioDevices]);
return ( return (
<> <>
{permissionOk ? ( {permissionOk ? (
@@ -65,60 +52,74 @@ const TranscriptCreate = () => {
setStream(null); setStream(null);
}} }}
topics={webSockets.topics} topics={webSockets.topics}
getAudioStream={getCurrentStream} getAudioStream={getAudioStream}
useActiveTopic={useActiveTopic} useActiveTopic={useActiveTopic}
isPastMeeting={false} isPastMeeting={false}
audioDevices={audioDevices}
/> />
<div className="grid grid-cols-1 lg:grid-cols-2 grid-rows-2 lg:grid-rows-1 gap-2 lg:gap-4 h-full"> <div className="grid grid-cols-1 lg:grid-cols-2 grid-rows-2 lg:grid-rows-1 gap-2 lg:gap-4 h-full">
<TopicList <TopicList
topics={webSockets.topics} topics={webSockets.topics}
useActiveTopic={useActiveTopic} useActiveTopic={useActiveTopic}
/> />
<div className="h-full flex flex-col"> <section className="w-full h-full bg-blue-400/20 rounded-lg md:rounded-xl px-2 md:px-4 flex flex-col justify-center align-center">
<section className="mb-2"> <div className="py-2 h-auto">
<AudioInputsDropdown <LiveTrancription text={webSockets.transcriptText} />
setDeviceId={setDeviceId} </div>
audioDevices={audioDevices} </section>
disabled={recordStarted}
/>
</section>
<section className="w-full h-full bg-blue-400/20 rounded-lg md:rounded-xl px-2 md:px-4 flex flex-col justify-center align-center">
<div className="py-2 h-auto">
<LiveTrancription text={webSockets.transcriptText} />
</div>
</section>
</div>
{disconnected && <DisconnectedIndicator />}
</div> </div>
{disconnected && <DisconnectedIndicator />}
</> </>
) : ( ) : (
<> <>
<div className="flex flex-col w-full items-center justify-center px-6 py-8 mt-8 rounded-xl"> <div></div>
<h1 className="text-2xl font-bold">Audio Permissions</h1> <section className="flex flex-col w-full h-full items-center justify-evenly p-4 md:px-6 md:py-8">
{loading ? ( <div className="flex flex-col max-w-2xl items-center justify-center">
<p className="text-gray-500 text-center mt-5"> <h1 className="text-2xl font-bold mb-2">Reflector</h1>
Checking permission... <p className="self-start">
Meet Monadical's own Reflector, your audio ally for hassle-free
insights.
</p> </p>
) : ( <p className="mb-4 md:text-justify">
<> With real-time transcriptions, translations, and summaries,
<p className="text-gray-500 text-center mt-5"> Reflector captures and categorizes the details of your meetings
Reflector needs access to your microphone to work. and events, all while keeping your data locked down tight on
<br /> your own infrastructure. Forget the scribbled notes, endless
{permissionDenied recordings, or third-party apps. Discover Reflector, a powerful
? "Please reset microphone permissions to continue." new way to elevate knowledge management and accessibility for
: "Please grant permission to continue."} all.
</p> </p>
<button </div>
className="mt-4 bg-blue-400 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded m-auto" <div>
onClick={requestPermission} <div className="flex flex-col max-w-2xl items-center justify-center">
disabled={permissionDenied} <h2 className="text-2xl font-bold mb-2">Audio Permissions</h2>
> {loading ? (
{permissionDenied ? "Access denied" : "Grant Permission"} <p className="text-gray-500 text-center">
</button> Checking permission...
</> </p>
)} ) : (
</div> <>
<p className="text-gray-500 text-center">
Reflector needs access to your microphone to work.
<br />
{permissionDenied
? "Please reset microphone permissions to continue."
: "Please grant permission to continue."}
</p>
<button
className="mt-4 bg-blue-400 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded m-auto"
onClick={requestPermission}
disabled={permissionDenied}
>
{permissionDenied ? "Access denied" : "Grant Permission"}
</button>
</>
)}
</div>
</div>
</section>
</> </>
)} )}
</> </>

View File

@@ -5,17 +5,21 @@ import RecordPlugin from "../lib/custom-plugins/record";
import CustomRegionsPlugin from "../lib/custom-plugins/regions"; import CustomRegionsPlugin from "../lib/custom-plugins/regions";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faMicrophone } from "@fortawesome/free-solid-svg-icons";
import { faDownload } from "@fortawesome/free-solid-svg-icons"; import { faDownload } from "@fortawesome/free-solid-svg-icons";
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";
import AudioInputsDropdown from "./audioInputsDropdown";
import { Option } from "react-dropdown";
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?: () => Promise<MediaStream | null>; getAudioStream?: (deviceId) => Promise<MediaStream | null>;
audioDevices?: Option[];
useActiveTopic: [ useActiveTopic: [
Topic | null, Topic | null,
React.Dispatch<React.SetStateAction<Topic | null>>, React.Dispatch<React.SetStateAction<Topic | null>>,
@@ -38,10 +42,11 @@ export default function Recorder(props: RecorderProps) {
const [waveRegions, setWaveRegions] = useState<CustomRegionsPlugin | null>( const [waveRegions, setWaveRegions] = useState<CustomRegionsPlugin | null>(
null, null,
); );
const [deviceId, setDeviceId] = useState<string | null>(null);
const [recordStarted, setRecordStarted] = useState(false);
const [activeTopic, setActiveTopic] = props.useActiveTopic; const [activeTopic, setActiveTopic] = props.useActiveTopic;
const topicsRef = useRef(props.topics); const topicsRef = useRef(props.topics);
const [showDevices, setShowDevices] = useState(false);
useEffect(() => { useEffect(() => {
if (waveformRef.current) { if (waveformRef.current) {
@@ -186,8 +191,8 @@ export default function Recorder(props: RecorderProps) {
record.stopRecording(); record.stopRecording();
setIsRecording(false); setIsRecording(false);
setHasRecorded(true); setHasRecorded(true);
} else if (props.getAudioStream) { } else {
const stream = await props.getAudioStream(); const stream = await getCurrentStream();
if (props.setStream) props.setStream(stream); if (props.setStream) props.setStream(stream);
waveRegions?.clearRegions(); waveRegions?.clearRegions();
@@ -195,8 +200,6 @@ export default function Recorder(props: RecorderProps) {
await record.startRecording(stream); await record.startRecording(stream);
setIsRecording(true); setIsRecording(true);
} }
} else {
throw new Error("No getAudioStream function provided");
} }
}; };
@@ -210,8 +213,21 @@ export default function Recorder(props: RecorderProps) {
return ""; return "";
}; };
const getCurrentStream = async () => {
setRecordStarted(true);
return deviceId && props.getAudioStream
? await props.getAudioStream(deviceId)
: null;
};
useEffect(() => {
if (props.audioDevices && props.audioDevices.length > 0) {
setDeviceId[props.audioDevices[0].value];
}
}, [props.audioDevices]);
return ( return (
<div className="flex items-center w-full"> <div className="flex items-center w-full relative">
<div className="flex-grow items-end relative"> <div className="flex-grow items-end relative">
<div ref={waveformRef} className="flex-grow rounded-2xl h-20"></div> <div ref={waveformRef} className="flex-grow rounded-2xl h-20"></div>
<div className="absolute right-2 bottom-0"> <div className="absolute right-2 bottom-0">
@@ -259,17 +275,41 @@ export default function Recorder(props: RecorderProps) {
</> </>
)} )}
{!hasRecorded && ( {!hasRecorded && (
<button <>
className={`${ <button
isRecording className={`${
? "bg-red-400 hover:bg-red-500" isRecording
: "bg-blue-400 hover:bg-blue-500" ? "bg-red-400 hover:bg-red-500"
} text-white ml-2 md:ml:4 md:h-[78px] md:min-w-[100px] text-lg`} : "bg-blue-400 hover:bg-blue-500"
onClick={handleRecClick} } text-white ml-2 md:ml:4 md:h-[78px] md:min-w-[100px] text-lg`}
disabled={isPlaying} onClick={handleRecClick}
> disabled={isPlaying}
{isRecording ? "Stop" : "Record"} >
</button> {isRecording ? "Stop" : "Record"}
</button>
{props.audioDevices && props.audioDevices?.length > 0 && (
<>
<button
className="text-center cursor-pointer text-blue-400 hover:text-blue-700 ml-2 md:ml:4 p-2"
onClick={() => setShowDevices((prev) => !prev)}
>
<FontAwesomeIcon icon={faMicrophone} className="h-5 w-auto" />
</button>
<div
className={`absolute z-20 bottom-[-1rem] right-0 bg-white rounded ${
showDevices ? "visible" : "invisible"
}`}
>
<AudioInputsDropdown
setDeviceId={setDeviceId}
audioDevices={props.audioDevices}
disabled={recordStarted}
hide={() => setShowDevices(false)}
/>
</div>
</>
)}
</>
)} )}
</div> </div>
); );