Viewing File: /home/ubuntu/voice-assistant-frontend/src/components/Voice/AddVoiceModal.jsx

import React, { useState, useEffect, useRef } from "react";
import { Form, Button, Modal, InputGroup, Image } from "react-bootstrap";
import { Formik, Form as FORM, Field, ErrorMessage } from "formik";
import { AudioRecorder, useAudioRecorder } from 'react-audio-voice-recorder';
import { getErrorNotificationMessage } from "../Helper/NotificationMessage";
import { useTranslation } from "react-multi-lang";
import { useDispatch, useSelector } from "react-redux";
import * as Yup from "yup";
import { setSelectedVoice, voiceStoreStart } from "../../store/slices/VoiceSlice";
import { v4 as uuidv4 } from 'uuid';
import { ButtonLoader } from "../Helper/Loader";
import { recordVoiceCreateStart } from "../../store/slices/VoiceSlice";
import { Link } from "react-router-dom";
import Skeleton from "react-loading-skeleton";
import SomethingWentWrong from "../Helper/SomethingWentWrong";
import { phraseListStart } from "../../store/slices/PhraseSlice";


const AddVoiceModal = (props) => {
  const t = useTranslation("add_voice");
  const recorderControls = useAudioRecorder()
  const audioRef = useRef(null);
  const dispatch = useDispatch();
  const recordVoice = useSelector((state) => state.voice.recordVoice);
  const phraseList = useSelector((state) => state.phrase.phraseList);
  const voiceStore = useSelector((state) => state.voice.voiceStore);
  const [step, setStep] = useState(1);
  const [record, setRecord] = useState(false);
  const [audioPlaying, setAudioPlaying] = useState(false);
  const [previewState, setPreviewState] = useState("Preview");
  const [currentPhraseIndex, setCurrentPhraseIndex] = useState(0);
  const [voiceId, setVoice_Id] = useState(uuidv4());
  const [skipRender, setSkipRender] = useState(true);

  useEffect(() => {
    setStep(1);
    dispatch(phraseListStart())
  }, []);

  const addAudioElement = (data) => {
    setRecord(data);
  };

  const stopRecording = () => {
    recorderControls.stopRecording();
    setStep(3);
  };

  const startRecording = async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      stream.getTracks().forEach(track => track.stop());
      if (stream) {
        setStep(2);
        recorderControls.startRecording();
      }
    } catch (error) {
      getErrorNotificationMessage("MicroPhone Access denied , Please allow to continue");
    }
  }

  const handleSubmit = () => {
    const data = new File([record], `VoiceFile${currentPhraseIndex}`);
    dispatch(recordVoiceCreateStart({
      phrase_voice: data,
      phrase_id: phraseList.data.phrases[currentPhraseIndex].id,
      ai_voice_id: voiceId
    }));

  };

  useEffect(() => {
    if (
      !skipRender &&
      !recordVoice.loading &&
      Object.keys(recordVoice.data).length > 0
    )
      if (currentPhraseIndex < phraseList.data.total_phrases - 1) {
        setCurrentPhraseIndex(currentPhraseIndex + 1);
        setStep(1);
      } else {
        setStep(4);
      }
    if (currentPhraseIndex == phraseList.data.total_phrases) {
      setStep(4);
    }

    setSkipRender(false);
  }, [recordVoice]);

  useEffect(() => {
    if (audioRef.current) {
      audioRef.current.addEventListener('ended', () => {
        setAudioPlaying(false);
        setPreviewState("Preview");
        audioRef.current = null
      });
    }
  }, [audioRef.current]);

  const togglePlayback = () => {
    if (!audioRef.current) {
      const url = URL.createObjectURL(record);
      audioRef.current = new Audio(url);
    }

    if (!audioPlaying) {
      audioRef.current.play();
      setAudioPlaying(true);
      setPreviewState("Pause")
    } else {
      audioRef.current.pause();
      setAudioPlaying(false);
      setPreviewState("Play")
    }
  }

  const previewSubmit = () => {
    togglePlayback();
  }

  const handleRepeat = () => {
    if (audioRef.current) {
      audioRef.current.pause();
      audioRef.current = null;
      setAudioPlaying(false);
      setPreviewState("Preview");
    }
    setStep(1);
  };

  const AddVoiceSchema = Yup.object().shape({
    name: Yup.string()
      .required(t("required"))
      .min(5, t("invalid")),
  });

  const handleAddVoice = (values) => {
    dispatch(voiceStoreStart({ ...values, ai_voice_id: voiceId }))
  }

  useEffect(() => {
    if (
      !skipRender &&
      !voiceStore.loading &&
      Object.keys(voiceStore.data).length > 0
    ) {
      setStep(5)
    }
    setSkipRender(false);
  }, [voiceStore]);


  return (
    <>
      <Modal
        className="modal-dialog-center add-voice-modal"
        size="md"
        centered
        show={props.addVoiceModal}
        onHide={props.closeAddVoiceModal}
        backdrop="static"
      >
        <Modal.Body>
          <div className="hidden-recorder">
            <AudioRecorder
              onRecordingComplete={addAudioElement}
              recorderControls={recorderControls}
              audioTrackConstraints={{
                noiseSuppression: true,
                echoCancellation: true,
              }}
            //  downloadOnSavePress={true}
            //  downloadFileExtension="webm"
            />
          </div>
          <h4>{t("title")}</h4>
          <Button
            className="modal-close"
            onClick={() => {
              // eslint-disable-next-line no-restricted-globals
              currentPhraseIndex > 0 ? confirm("Are you sure to close? Unsaved changes will be lost.") && props.closeAddVoiceModal() : props.closeAddVoiceModal()
            }}
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="15"
              height="15"
              fill="none"
              viewBox="0 0 11 11"
            >
              <path
                fill="#979BA2"
                d="M10.756.252a.83.83 0 00-1.176 0L5.5 4.324 1.42.244A.83.83 0 10.244 1.42l4.08 4.08-4.08 4.08a.83.83 0 101.176 1.176l4.08-4.08 4.08 4.08a.831.831 0 101.176-1.176L6.676 5.5l4.08-4.08a.836.836 0 000-1.168z"
              ></path>
            </svg>
          </Button>
          <>
          {phraseList.loading ? 
            <div className="step-1">
              <Skeleton height={20} className="mb-3" />
              <Skeleton height={40} width={40} className="mb-2" />
              <Skeleton height={120} className="mb-2" />
              <div style={{display:"flex", alignItems:"center", justifyContent:"center"}}>
                <Skeleton circle width={60} height={60} />
              </div>
            </div> : (Object.keys(phraseList.data).length > 0) ? <React.Fragment>
              {step == 1 && (
                <div className="step-1">
                  <div className="add-voice-sec">
                    <p>{t("message")}</p>
                    <div className="add-voice-count-sec">{currentPhraseIndex+1}/{phraseList.data.total_phrases}</div>
                    <div className="add-voice-text-speech">
                      <h3>
                        {phraseList.data?.phrases[currentPhraseIndex]?.phrase}
                      </h3>
                    </div>
                    <div className="speak-btn-sec">
                      <Button className="speak-btn" onClick={startRecording}>
                        <Image
                          className="speak-btn-icon"
                          src={
                            window.location.origin + "/img/speak-btn-icon-1.png"
                          }
                          type="image/png"
                        />
                      </Button>
                    </div>
                  </div>
                </div>
              )}
              {step == 2 && (
                <div className="step-2">
                  <div className="add-voice-sec">
                    <p>{t("message")}</p>
                    <div className="add-voice-count-sec">{currentPhraseIndex+1}/{phraseList.data.total_phrases}</div>
                    <div className="add-voice-text-speech">
                      <h3>
                        {phraseList.data?.phrases[currentPhraseIndex]?.phrase}
                      </h3>
                    </div>
                    <div className="speak-btn-sec">
                      <Button className="speak-btn" onClick={stopRecording}>
                        <Image
                          className="speak-btn-icon"
                          src={
                            window.location.origin + "/img/speak-btn-icon-2.png"
                          }
                          type="image/png"
                        />

                      </Button>
                    </div>
                  </div>
                </div>
              )}
              {step == 3 && (
                <div className="step-3">
                  <div className="add-voice-sec">
                    <p>{t("message")}</p>
                    <div className="add-voice-count-sec">{currentPhraseIndex+1}/{phraseList.data.total_phrases}</div>
                    <div className="add-voice-text-speech">
                      <h3>
                        {phraseList.data?.phrases[currentPhraseIndex]?.phrase}
                      </h3>
                    </div>
                    <div className="add-voice-action-btn-sec">
                      <div className="add-voice-action-btn-left-sec">
                        <Button disabled={recordVoice.buttonDisable} className="preview-btn" onClick={previewSubmit}>
                          <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-player-play-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M6 4v16a1 1 0 0 0 1.524 .852l13 -8a1 1 0 0 0 0 -1.704l-13 -8a1 1 0 0 0 -1.524 .852z" stroke-width="0" fill="#5575FF" /></svg>
                          <span>{previewState}</span>
                        </Button>
                        <Button disabled={recordVoice.buttonDisable} className="repeat-btn" onClick={handleRepeat}>
                          <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-reload" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="#5575FF" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747" /><path d="M20 4v5h-5" /></svg>
                          <span>{t("repeat")}</span>
                        </Button>
                      </div>
                      <div className="add-voice-action-btn-right-sec">
                        <Button className="next-btn" type="submit" disabled={recordVoice.buttonDisable} onClick={handleSubmit}>
                          {recordVoice.buttonDisable ? (
                            <ButtonLoader varient="black" />
                          ) : (
                            <span> {t("next")}</span>
                          )}
                        </Button>
                      </div>
                    </div>
                  </div>
                </div>
              )}
              {step == 4 && (
                <div className="step-4">
                  <div className="add-voice-sec">
                    <p>{t("message")}</p>
                    <Formik
                      initialValues={{
                        name: "",
                        description: "",
                      }}
                      validationSchema={AddVoiceSchema}
                      onSubmit={handleAddVoice}
                    >
                      {({ setFieldValue, values }) => (
                        <FORM className="add-voice-form-sec">
                          <Form.Group className="mb-3" controlId="formBasicEmail">
                            <Form.Label>{t("name")}</Form.Label>
                            <Field
                              className="form-control"
                              placeholder={"Enter name"}
                              type="text"
                              name="name"
                              aria-label="Username"
                              aria-describedby="basic-addon1"
                            />
                          </Form.Group>
                          <ErrorMessage
                              component={"div"}
                              name="name"
                              className="errorMsg"
                            />
                          <Form.Group className="mb-3" controlId="exampleForm.ControlTextarea1">
                            <Form.Label>{t("description")}</Form.Label>
                            <Field
                              className="form-control"
                              placeholder="How would you describe the voice?"
                              as="textarea"
                              name="description"
                              aria-label="Username"
                              aria-describedby="basic-addon1"
                            />
                          </Form.Group>
                          <div className="add-voice-form-btn-sec">
                            <Button className="default-btn" type="submit" disabled={voiceStore.buttonDisable}>
                              {voiceStore.buttonDisable ? (
                                <ButtonLoader varient="black" />
                              ) : (
                                <span> {t("add")}</span>
                              )}
                            </Button>
                          </div>
                        </FORM>
                      )}</Formik>
                  </div>
                </div>
              )}
              {step == 5 && (
                <div className="step-5">
                  <div className="add-voice-success-card">
                    <div className="add-voice-success-img-sec">
                      <Image
                        className="add-voice-success-img"
                        src={
                          window.location.origin + "/img/voice-success-img.png"
                        }
                        type="image/png"
                      />
                    </div>
                    <h3>
                      {t("success")}
                    </h3>
                    <p>{t("content")}</p>
                    <div className="add-voice-suucess-btn-sec">
                      <Link to="/generate-voice" onClick={()=> dispatch(setSelectedVoice(voiceStore.data.voice))} className="default-btn">
                        {t("use")}
                      </Link>
                    </div>
                  </div>
                </div>
              )}
          </React.Fragment> : <SomethingWentWrong handleClick={()=> dispatch(phraseListStart())} />}
          </>
        </Modal.Body>
      </Modal>
    </>
  );
};

export default AddVoiceModal;
Back to Directory File Manager