Skip to content

Commit

Permalink
feat: dictaphone
Browse files Browse the repository at this point in the history
  • Loading branch information
ledouxm committed Feb 24, 2025
1 parent 775c56c commit 2a17638
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 14 deletions.
72 changes: 58 additions & 14 deletions packages/frontend/src/features/NotesForm.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Center, Divider, Flex, Stack, styled } from "#styled-system/jsx";
import { useFormContext } from "react-hook-form";
import { InputGroupWithTitle } from "#components/InputGroup";
import Input from "@codegouvfr/react-dsfr/Input";
import { DecisionChips } from "#components/chips/DecisionChips";
import { ContactChips } from "#components/chips/ContactChips";
import { css } from "#styled-system/css";
import { DecisionChips } from "#components/chips/DecisionChips";
import { FurtherInfoChips } from "#components/chips/FurtherInfoChips";
import { InputGroupWithTitle } from "#components/InputGroup";
import { css } from "#styled-system/css";
import { Center, Divider, Flex, Stack } from "#styled-system/jsx";
import Button from "@codegouvfr/react-dsfr/Button";
import { useIsFormDisabled } from "./DisabledContext";
import Input from "@codegouvfr/react-dsfr/Input";
import { useRef, useState } from "react";
import { useFormContext } from "react-hook-form";
import { Report } from "../db/AppSchema";
import { SpeechRecorder } from "./audio-record/SpeechRecorder";
import { useIsFormDisabled } from "./DisabledContext";
import { UploadImage } from "./upload/UploadImage";

export const NotesForm = () => {
Expand All @@ -20,13 +22,7 @@ export const NotesForm = () => {
<Flex direction="column" w="100%" padding="16px">
<InputGroupWithTitle title="Décision & suite à donner">
<DecisionChips disabled={isFormDisabled} />
<Input
className={css({ mt: "24px" })}
disabled={isFormDisabled}
label="Commentaire"
textArea
nativeTextAreaProps={{ ...form.register("precisions"), rows: 5 }}
/>
<PrecisionsTextArea />
</InputGroupWithTitle>

<UploadImage reportId={form.getValues().id} />
Expand Down Expand Up @@ -56,3 +52,51 @@ export const NotesForm = () => {
</Flex>
);
};

const PrecisionsTextArea = () => {
const [isRecording, setIsRecording] = useState(false);
const isFormDisabled = useIsFormDisabled();
const form = useFormContext<Report>();

const [interimText, setInterimText] = useState("");
const valueRef = useRef("");

const isRecordingProps = {
value: valueRef.current + " " + interimText,
};

const isIdleProps = form.register("precisions");

return (
<Input
className={css({ mt: "24px", "& > textarea": { mt: "0 !important" } })}
disabled={isFormDisabled}
label={
<Flex justifyContent="space-between" w="100%">
<span>Commentaire</span>
<SpeechRecorder
onStart={() => {
setIsRecording(true);
valueRef.current = form.watch("precisions") ?? "";
}}
onStop={() => setIsRecording(false)}
onInterimText={(text) => {
setInterimText(text);
}}
onFinalText={(text) => {
form.setValue("precisions", valueRef.current + " " + text);
valueRef.current = valueRef.current + " " + text;
setInterimText("");
}}
/>
</Flex>
}
textArea
nativeTextAreaProps={{
...(isRecording ? isRecordingProps : isIdleProps),
id: "precisions",
rows: 5,
}}
/>
);
};
110 changes: 110 additions & 0 deletions packages/frontend/src/features/audio-record/SpeechRecorder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import Button from "@codegouvfr/react-dsfr/Button";
import { useState, useRef, useEffect } from "react";

export const SpeechRecorder = ({
onInterimText,
onFinalText,
onStart,
onStop,
}: {
onInterimText: (text: string) => void;
onFinalText: (text: string) => void;
onStart: () => void;
onStop: () => void;
}) => {
const [isRecording, setIsRecording] = useState(false);
const [error, setError] = useState("");
const recognitionRef = useRef<any>(null);

useEffect(() => {
if (recognitionRef.current) {
recognitionRef.current.stop();
}
const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;

if (!SpeechRecognition) {
setError("Speech recognition not supported");
return;
}

recognitionRef.current = new SpeechRecognition();
recognitionRef.current.continuous = true;
recognitionRef.current.interimResults = true;
recognitionRef.current.lang = "fr-FR";

recognitionRef.current.onresult = (event: any) => {
let interimTranscript = "";
let finalTranscript = "";

for (let i = event.resultIndex; i < event.results.length; i++) {
const transcriptPiece = event.results[i][0].transcript;
if (event.results[i].isFinal) {
finalTranscript += transcriptPiece;
} else {
interimTranscript += transcriptPiece;
}
}

if (finalTranscript) {
onFinalText(finalTranscript);
}
if (interimTranscript) {
onInterimText(interimTranscript);
}
};

recognitionRef.current.onend = () => {
setIsRecording(false);
onStop();
};

recognitionRef.current.onerror = (event: any) => {
setError(`Error: ${event.error}`);
stopRecording();
};

return () => {
if (recognitionRef.current) {
recognitionRef.current.stop();
}
};
}, []);

if (error) console.error(error);

const toggleRecording = async () => {
try {
if (!isRecording) {
await navigator.mediaDevices.getUserMedia({ audio: true });
recognitionRef.current.start();
setIsRecording(true);
setError("");
onStart();
} else {
stopRecording();
}
} catch (err: any) {
console.log(err);
setError(`Microphone error: ${err.message}`);
}
};

const stopRecording = () => {
if (recognitionRef.current) {
recognitionRef.current.stop();
setIsRecording(false);
}
onStop();
};

return (
<Button
type="button"
priority={isRecording ? "primary" : "tertiary"}
iconId="ri-mic-fill"
onClick={toggleRecording}
>
{isRecording ? <>En cours</> : <>Dicter</>}
</Button>
);
};

0 comments on commit 2a17638

Please sign in to comment.