link-stack/apps/link/metamigo-add/_components/voice/voicelines/shared.tsx

304 lines
8.9 KiB
TypeScript
Raw Normal View History

2023-06-28 12:55:24 +00:00
"use client";
2023-03-15 12:17:43 +00:00
/* eslint-disable react/display-name */
2023-02-13 12:41:30 +00:00
import React, { useState, useEffect } from "react";
2023-05-25 07:03:57 +00:00
import PlayIcon from "@mui/icons-material/PlayCircleFilled";
2023-02-13 12:41:30 +00:00
import {
TextInput,
SelectInput,
required,
useTranslate,
useNotify,
ReferenceInput,
ReferenceField,
TextField,
} from "react-admin";
2023-05-25 07:03:57 +00:00
import { IconButton, CircularProgress } from "@mui/material";
2023-06-28 12:55:24 +00:00
import absoluteUrl from "../../../_lib/absolute-url";
2023-02-13 12:41:30 +00:00
import TwilioLanguages from "./twilio-languages";
type TTSProvider = (voice: any, language: any, prompt: any) => Promise<void>;
2023-03-14 17:40:24 +00:00
const tts = async (providerId: any): Promise<TTSProvider> => {
2023-02-13 12:41:30 +00:00
const r = await fetch(
2023-08-25 07:11:33 +00:00
`/api/v1/voice/twilio/text-to-speech-token/${providerId}`,
2023-02-13 12:41:30 +00:00
);
const { token } = await r.json();
const twilioClient = await import("twilio-client");
return (voice, language, prompt): Promise<void> =>
new Promise((resolve) => {
if (!voice || !language || !prompt) resolve();
2023-03-15 12:17:43 +00:00
const { Device } = twilioClient;
2023-02-13 12:41:30 +00:00
const device = new Device();
const silence = `${absoluteUrl().origin}/static/silence.mp3`;
device.setup(token, {
codecPrefences: ["opus", "pcmu"],
enableRingingState: false,
fakeLocalDTMF: true,
disableAudioContextSounds: true,
sounds: {
disconnect: silence,
incoming: silence,
outgoing: silence,
},
});
2023-03-15 12:17:43 +00:00
device.on("ready", (device: any) => {
2023-02-13 12:41:30 +00:00
device.connect({ language, voice, prompt });
});
device.on("disconnect", () => resolve());
device.on("error", () => resolve());
});
};
2023-03-14 17:40:24 +00:00
export const TextToSpeechButton = ({ form }: any) => {
2023-02-13 12:41:30 +00:00
const { providerId, language, voice, promptText: prompt } = form.formData;
const [loading, setLoading] = useState<boolean>(false);
const [ttsProvider, setTTSProvider] = useState<
undefined | { provider: TTSProvider }
>(undefined);
const [playText, setPlayText] = useState<
undefined | { func: () => Promise<void> }
>(undefined);
useEffect(() => {
(async () => {
if (providerId) {
setLoading(true);
setTTSProvider({ provider: await tts(providerId) });
setLoading(false);
}
})();
}, [providerId]);
useEffect(() => {
(async () => {
setPlayText({
2023-03-15 12:17:43 +00:00
async func() {
2023-02-13 12:41:30 +00:00
setLoading(true);
if (ttsProvider) await ttsProvider.provider(voice, language, prompt);
setLoading(false);
},
});
})();
2023-03-15 12:17:43 +00:00
}, [prompt, language, voice, ttsProvider, ttsProvider?.provider]);
2023-02-13 12:41:30 +00:00
const disabled = !(providerId && prompt?.length >= 2 && voice && language);
/* TODO add this back to IconButtonwhen we know how to extend MUI theme and appease typescript
variant="contained"
*/
return (
<IconButton onClick={playText?.func} disabled={disabled} color="primary">
{!loading && <PlayIcon />}
{loading && <CircularProgress size={20} />}
</IconButton>
);
};
2023-03-15 12:17:43 +00:00
export const PromptInput = (form: any, ...rest: any[]) => (
<TextInput
source="promptText"
multiline
2023-08-25 07:11:33 +00:00
// options={{ fullWidth: true }}
2023-03-15 12:17:43 +00:00
InputProps={{ endAdornment: <TextToSpeechButton form={form} /> }}
{...rest}
/>
);
const validateVoice = (_args: any, values: any) => {
2023-02-13 12:41:30 +00:00
if (!values.language) return "validation.language";
if (!values.voice) return "validation.voice";
const availableVoices = TwilioLanguages.voices[values.language];
const found =
2023-03-14 17:40:24 +00:00
availableVoices.filter((v: any) => v.id === values.voice).length === 1;
2023-02-13 12:41:30 +00:00
if (!found) return "validation.voice";
2023-03-14 17:40:24 +00:00
return undefined;
2023-02-13 12:41:30 +00:00
};
2023-03-14 17:40:24 +00:00
export const VoiceInput = (form: any, ...rest: any[]) => {
2023-02-13 12:41:30 +00:00
const voice = TwilioLanguages.voices[form.formData.language] || [];
return (
2023-03-14 17:40:24 +00:00
// @ts-expect-error
2023-02-13 12:41:30 +00:00
<SelectInput
source="voice"
choices={voice}
validate={[required(), validateVoice]}
{...rest}
/>
);
};
let noAvailableNumbers = false;
2023-03-14 17:40:24 +00:00
let availableNumbers: any[] = [];
2023-02-13 12:41:30 +00:00
2023-03-14 17:40:24 +00:00
const getAvailableNumbers = async (providerId: string) => {
2023-02-13 12:41:30 +00:00
try {
const r = await fetch(`/api/v1/voice/providers/${providerId}/freeNumbers`);
availableNumbers = await r.json();
noAvailableNumbers = availableNumbers.length === 0;
return availableNumbers;
} catch (error) {
console.error(
2023-08-25 07:11:33 +00:00
`Could not fetch available numbers for provider ${providerId} - ${error}`,
2023-02-13 12:41:30 +00:00
);
return [];
}
};
2023-03-15 12:17:43 +00:00
const sidToNumber = (sid: any) =>
availableNumbers.filter(({ id }) => id === sid).map(({ name }) => name)[0];
2023-02-13 12:41:30 +00:00
2023-03-15 12:17:43 +00:00
export const populateNumber = (data: any) => ({
...data,
number: sidToNumber(data.providerLineSid),
});
2023-02-13 12:41:30 +00:00
2023-03-14 17:40:24 +00:00
const hasNumbers = (
2023-03-15 12:17:43 +00:00
_args: any,
_value: any,
_values: any,
_translate: any,
..._props: any[]
2023-03-14 17:40:24 +00:00
) => {
2023-02-13 12:41:30 +00:00
if (noAvailableNumbers) return "validation.noAvailableNumbers";
2023-03-14 17:40:24 +00:00
return undefined;
2023-02-13 12:41:30 +00:00
};
2023-03-14 17:40:24 +00:00
export const AvailableNumbersInput = (form: any, ...rest: any[]) => {
2023-02-13 12:41:30 +00:00
const {
// @ts-expect-error: non-existent property
meta: { touched, error } = {},
// @ts-expect-error: non-existent property
input: { ...inputProps },
...props
} = rest;
const translate = useTranslate();
const notify = useNotify();
const [loading, setLoading] = useState(false);
const [choices, setChoices] = useState({});
// @ts-expect-error: Invalid return type
useEffect(async () => {
if (form && form.formData && form.formData.providerId) {
setLoading(true);
const choices = await getAvailableNumbers(form.formData.providerId);
setChoices({
choices,
helperText: noAvailableNumbers
? translate("validation.noAvailableNumbers")
: "",
});
2023-03-14 17:40:24 +00:00
if (noAvailableNumbers)
notify("validation.noAvailableNumbers", { type: "error" });
2023-02-13 12:41:30 +00:00
setLoading(false);
}
2023-03-15 12:17:43 +00:00
}, [form, notify, translate]);
2023-02-13 12:41:30 +00:00
return (
<>
<SelectInput
label="Number"
source="providerLineSid"
// @ts-expect-error: non-existent property
choices={choices.choices}
disabled={loading}
validate={[hasNumbers, required()]}
// @ts-expect-error: non-existent property
error={Boolean(touched && error) || Boolean(choices.helperText)}
// @ts-expect-error: non-existent property
helperText={choices.helperText}
{...inputProps}
{...props}
/>
{loading && <CircularProgress />}
</>
);
};
/*
const voiceLineName = voiceLine => {
return voiceLine.number
}
const getVoiceLineChoices = async ():Promise<any[]> => {
try {
const r = await fetch(`/api/v1/voice/voice-line`);
const lines = await r.json();
if(lines.data?.length > 0) {
return lines.data.map(voiceLine => ({"id": voiceLine.id, "name": voiceLineName(voiceLine)}))
}
return [];
} catch (error) {
console.error(
`Could not fetch voice lines error: ${error}`
);
return [];
}
}
export const AsyncSelectInput = (choiceLoader: () => Promise<any[]>, label, source, translationEmpty,) => (form, ...rest) => {
const {
meta: { touched, error } = {},
input: { ...inputProps },
...props
} = rest;
const translate = useTranslate();
const notify = useNotify();
const [loading, setLoading] = useState(false);
const [choices, setChoices] = useState({choices: []});
useEffect(() => {
(async () => {
setLoading(true);
//const items = await choiceLoader()
const items = [{"id": "testing", "name": "OMG"}]
setChoices({
choices: items,
helperText: items.length === 0
? translate(translationEmpty)
: "",
});
if (items.length === 0) notify(translationEmpty, "error");
setLoading(false);
})()}, [form && form.formData ? form.formData.providerId : undefined]);
const isNotEmpty = () => {
if (choices.choices.length === 0) return translationEmpty;
return undefined;
};
return (
<>
{choices.choices.length > 0 &&
<SelectInput
label={label}
source={source}
choices={choices.choices}
disabled={loading}
validate={[isNotEmpty, required()]}
error={Boolean(touched && error) || Boolean(choices.helperText)}
helperText={choices.helperText}
{...inputProps}
{...props}
/>}
{loading && <CircularProgress />}
</>
)
}
export const VoiceLineSelectInput = AsyncSelectInput(getVoiceLineChoices, "Voice Line", "backendId", "validation.noVoiceLines" )
*/
2023-08-25 07:11:33 +00:00
export const VoiceLineSelectInput = (source: string) => () => (
<ReferenceInput
label="Voice Line"
source={source}
reference="voiceLines"
validate={[required()]}
>
<SelectInput optionText="number" />
</ReferenceInput>
);
2023-02-13 12:41:30 +00:00
2023-08-25 07:11:33 +00:00
export const VoiceLineField = (source: string) => () => (
<ReferenceField label="Voice Line" source={source} reference="voiceLines">
<TextField source="number" />
</ReferenceField>
);