Skip to content

Commit

Permalink
Add audio recorder item control component
Browse files Browse the repository at this point in the history
  • Loading branch information
vesnushka committed Dec 9, 2024
1 parent ea27b1e commit 4b7a2d7
Show file tree
Hide file tree
Showing 12 changed files with 341 additions and 15 deletions.
19 changes: 19 additions & 0 deletions src/components/AudioRecorder/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// eslint-disable-next-line
import { useAudioRecorder as useAudioRecorderControl } from "react-audio-voice-recorder";

export interface RecorderControls {
startRecording: () => void;
stopRecording: () => void;
togglePauseResume: () => void;
recordingBlob?: Blob;
isRecording: boolean;
isPaused: boolean;
recordingTime: number;
mediaRecorder?: MediaRecorder;
}

export function useAudioRecorder() {
const recorderControls: RecorderControls = useAudioRecorderControl();

return { recorderControls };
}
73 changes: 73 additions & 0 deletions src/components/AudioRecorder/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Trans } from '@lingui/macro';
// eslint-disable-next-line
import { AudioRecorder as AudioRecorderControl } from 'react-audio-voice-recorder';

import { RecorderControls } from './hooks';
import { S } from './styles';
import { uuid4 } from '@beda.software/fhir-react';
import { Upload, type UploadFile } from 'antd';
import React from 'react';
import { RcFile } from 'antd/lib/upload/interface';

interface AudioRecorderProps {
onChange: (url: RcFile) => Promise<void>;
recorderControls: RecorderControls;
}

export function AudioRecorder(props: AudioRecorderProps) {
const { recorderControls, onChange } = props;

const onRecordingComplete = async (blob: Blob) => {
const uuid = uuid4();
const audioFile = new File([blob], `${uuid}.webm`, { type: blob.type }) as RcFile;
audioFile.uid = uuid;
onChange(audioFile);
};

return (
<S.Scriber>
<S.Title $danger>
<Trans>Capture in progress</Trans>
</S.Title>
<AudioRecorderControl
showVisualizer
onRecordingComplete={onRecordingComplete}
audioTrackConstraints={{
noiseSuppression: true,
echoCancellation: true,
}}
recorderControls={{
...recorderControls,
}}
/>
</S.Scriber>
);
}

interface AudioPlayerProps {
files: UploadFile[];
onRemove?: (file: UploadFile) => void;
}

export function AudioPlayer(props: AudioPlayerProps) {
const { files, onRemove } = props;

return (
<S.Scriber>
<S.Title>
<Trans>Listen to the audio</Trans>
</S.Title>
{files.map((file) => (
<React.Fragment key={file.uid}>
<S.Audio controls src={file.url} />
<Upload
listType="text"
showUploadList={{ showRemoveIcon: !!onRemove }}
fileList={[file]}
onRemove={() => onRemove?.(file)}
/>
</React.Fragment>
))}
</S.Scriber>
);
}
50 changes: 50 additions & 0 deletions src/components/AudioRecorder/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import styled, { css } from 'styled-components';

import { Text } from 'src/components/Typography';

export const S = {
Scriber: styled.div`
display: flex;
flex-direction: column;
gap: 8px 0;
.audio-recorder {
width: 100%;
box-shadow: none;
border-radius: 30px;
background-color: ${({ theme }) => theme.neutralPalette.gray_2};
padding: 3px 6px 3px 18px;
}
.audio-recorder-timer,
.audio-recorder-status {
font-family: inherit;
color: ${({ theme }) => theme.neutralPalette.gray_12};
}
.audio-recorder-mic {
display: none;
}
.audio-recorder-timer {
margin-left: 0;
}
.audio-recorder-options {
filter: ${({ theme }) => (theme.mode === 'dark' ? `invert(100%)` : `invert(0%)`)};
}
`,
Title: styled(Text)<{ $danger?: boolean }>`
font-weight: 700;
${({ $danger }) =>
$danger &&
css`
color: ${({ theme }) => theme.antdTheme?.red5};
`}
`,
Audio: styled.audio`
height: 52px;
width: 100%;
`,
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import { QuestionReference } from './readonly-widgets/reference';
import { AnxietyScore, DepressionScore } from './readonly-widgets/score';
import { QuestionText, TextWithInput } from './readonly-widgets/string';
import { TimeRangePickerControl } from './readonly-widgets/TimeRangePickerControl';
import { UploadFileControlReadOnly } from './widgets/UploadFileControl';
import { UploadFile } from './readonly-widgets/UploadFile';
import { AudioAttachment } from './readonly-widgets/AudioAttachment';

interface Props extends Partial<QRFContextData> {
formData: QuestionnaireResponseFormData;
Expand Down Expand Up @@ -66,14 +67,15 @@ export function ReadonlyQuestionnaireResponseForm(props: Props) {
reference: QuestionReference,
display: Display,
boolean: QuestionBoolean,
attachment: UploadFileControlReadOnly,
attachment: UploadFile,
...questionItemComponents,
}}
itemControlQuestionItemComponents={{
'inline-choice': QuestionChoice,
'anxiety-score': AnxietyScore,
'depression-score': DepressionScore,
'input-inside-text': TextWithInput,
'audio-recorder-uploader': AudioAttachment,
...itemControlQuestionItemComponents,
}}
>
Expand Down
2 changes: 2 additions & 0 deletions src/components/BaseQuestionnaireResponseForm/controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { QuestionReference } from './widgets/reference';
import { ReferenceRadioButton } from './widgets/ReferenceRadioButton';
import { UploadFileControl } from './widgets/UploadFileControl';
import { TextWithMacroFill } from '../TextWithMacroFill';
import { AudioRecorderUploader } from './widgets/AudioRecorderUploader';

export const itemComponents: QuestionItemComponentMapping = {
text: QuestionText,
Expand Down Expand Up @@ -66,6 +67,7 @@ export const itemControlComponents: ItemControlQuestionItemComponentMapping = {
'check-box': InlineChoice,
'input-inside-text': QuestionInputInsideText,
'markdown-editor': MDEditorControl,
'audio-recorder-uploader': AudioRecorderUploader,
};

export const groupControlComponents: ItemControlGroupItemComponentMapping = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Upload } from 'antd';
import { QuestionItemProps } from 'sdc-qrf';
import classNames from 'classnames';

import s from './ReadonlyWidgets.module.scss';
import { S } from './ReadonlyWidgets.styles';
import { useUploader } from '../widgets/UploadFileControl/hooks';
import React from 'react';

export function AudioAttachment(props: QuestionItemProps) {
const { questionItem } = props;
const { text, hidden } = questionItem;
const { fileList } = useUploader(props);

if (hidden) {
return null;
}

return (
<S.Question className={classNames(s.question, s.column, 'form__question')}>
<span className={s.questionText}>{text}</span>
{fileList.length ? (
fileList.map((file) => (
<React.Fragment key={file.url}>
<S.Audio controls src={file.url} />
<Upload listType="text" showUploadList={{ showRemoveIcon: false }} fileList={fileList} />
</React.Fragment>
))
) : (
<span>-</span>
)}
</S.Question>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ export const S = {
border-top: 1px solid ${({ theme }) => theme.neutralPalette.gray_4};
}
`,
Audio: styled.audio`
height: 52px;
width: 100%;
`,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Upload } from 'antd';
import { QuestionItemProps } from 'sdc-qrf';
import classNames from 'classnames';

import s from './ReadonlyWidgets.module.scss';
import { S } from './ReadonlyWidgets.styles';
import { useUploader } from '../widgets/UploadFileControl/hooks';

export function UploadFile(props: QuestionItemProps) {
const { questionItem } = props;
const { text, hidden } = questionItem;
const { fileList } = useUploader(props);

if (hidden) {
return null;
}

return (
<S.Question className={classNames(s.question, s.column, 'form__question')}>
<span className={s.questionText}>{text}</span>
{fileList.length ? (
<Upload listType="picture" showUploadList={{ showRemoveIcon: false }} fileList={fileList} />
) : (
<span>-</span>
)}
</S.Question>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { AudioOutlined } from '@ant-design/icons';
import { Trans } from '@lingui/macro';
import { Form, UploadFile } from 'antd';
import { useCallback, useState } from 'react';
import { QuestionItemProps } from 'sdc-qrf';

import { AudioPlayer as AudioPlayerControl, AudioRecorder as AudioRecorderControl } from 'src/components/AudioRecorder';
import { useAudioRecorder } from 'src/components/AudioRecorder/hooks';

import { useUploader } from '../UploadFileControl/hooks';
import { RcFile } from 'antd/lib/upload/interface';
import { isSuccess } from '@beda.software/remote-data';
import { S } from './styles';
import { UploadFileControl } from '../UploadFileControl';

export function AudioRecorderUploader(props: QuestionItemProps) {
const { questionItem } = props;
const [showScriber, setShowScriber] = useState(false);

const { recorderControls } = useAudioRecorder();
const { formItem, customRequest, onChange, fileList } = useUploader(props);
const hasFiles = fileList.length > 0;

const onScribeChange = useCallback(
async (file: RcFile) => {
setShowScriber(false);

const fileClone = new File([file], file.name, {
type: file.type,
}) as any as UploadFile;
fileClone.uid = file.uid;
fileClone.status = 'uploading';
fileClone.percent = 0;

onChange({
fileList: [...fileList, fileClone],
file: fileClone,
});

const response = await customRequest({ file });

if (isSuccess(response)) {
fileClone.status = 'done';
fileClone.url = response.data.uploadUrl;
fileClone.percent = 100;

onChange({
fileList: [...fileList, fileClone],
file: fileClone,
});
}
},
[fileList],
);

const renderContent = () => {
if (hasFiles) {
return (
<S.Container>
<AudioPlayerControl files={fileList} />
</S.Container>
);
}

if (showScriber) {
return (
<S.Container>
<AudioRecorderControl recorderControls={recorderControls} onChange={onScribeChange} />
</S.Container>
);
}

return (
<>
<S.Button
icon={<AudioOutlined />}
type="primary"
onClick={() => {
setShowScriber(true);
recorderControls.startRecording();
}}
>
<span>
<Trans>Start scribe</Trans>
</span>
</S.Button>
<UploadFileControl
{...props}
questionItem={{
...questionItem,
text: undefined,
}}
/>
</>
);
};

return <Form.Item {...formItem}>{renderContent()}</Form.Item>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Button } from 'antd';
import styled from 'styled-components';

export const S = {
Container: styled.div`
border-radius: 10px;
padding: 12px 8px;
border: 1px solid ${({ theme }) => theme.neutralPalette.gray_4};
`,
Button: styled(Button)`
width: 100%;
`,
};
Loading

0 comments on commit 4b7a2d7

Please sign in to comment.