mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-02-09 02:29:45 +00:00
Compare commits
12 Commits
v0.8.5-rc1
...
v0.8.5-rc7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ddafee2cc | ||
|
|
24fc9d1bff | ||
|
|
08ab494cef | ||
|
|
75e7785061 | ||
|
|
72de9178a2 | ||
|
|
9741e5601f | ||
|
|
346ac66440 | ||
|
|
843d1eda1e | ||
|
|
27f02c2bb3 | ||
|
|
bb18556a6c | ||
|
|
393dd7374f | ||
|
|
4ad2154337 |
@@ -6,7 +6,7 @@
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Simple provisioning webapp for jambonz."
|
||||
content="Build innovative voice and collaboration services with jambonz, the open-source communication platform for conversational AI providers and CSPs."
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
@@ -46,7 +46,7 @@
|
||||
as="font"
|
||||
type="font/woff"
|
||||
/>
|
||||
<title>Jambonz Web App</title>
|
||||
<title>Jambonz Portal | Jambonz CPaaS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -161,7 +161,9 @@ export const SIP_GATEWAY_PROTOCOL_OPTIONS = [
|
||||
* Record bucket type
|
||||
*/
|
||||
export const BUCKET_VENDOR_AWS = "aws_s3";
|
||||
export const BUCKET_VENDOR_S3_COMPATIBLE = "s3_compatible";
|
||||
export const BUCKET_VENDOR_GOOGLE = "google";
|
||||
export const BUCKET_VENDOR_AZURE = "azure";
|
||||
export const BUCKET_VENDOR_OPTIONS = [
|
||||
{
|
||||
name: "NONE",
|
||||
@@ -171,6 +173,14 @@ export const BUCKET_VENDOR_OPTIONS = [
|
||||
name: "AWS S3",
|
||||
value: BUCKET_VENDOR_AWS,
|
||||
},
|
||||
{
|
||||
name: "AWS S3 Compatible",
|
||||
value: BUCKET_VENDOR_S3_COMPATIBLE,
|
||||
},
|
||||
{
|
||||
name: "Azure Cloud Storage",
|
||||
value: BUCKET_VENDOR_AZURE,
|
||||
},
|
||||
{
|
||||
name: "Google Cloud Storage",
|
||||
value: BUCKET_VENDOR_GOOGLE,
|
||||
|
||||
@@ -685,6 +685,10 @@ export const deleteAccountTtsCache = (sid: string) => {
|
||||
export const deleteClient = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_CLIENTS}/${sid}`);
|
||||
};
|
||||
|
||||
export const deleteRecord = (url: string) => {
|
||||
return deleteFetch<EmptyResponse>(url);
|
||||
};
|
||||
/** Named wrappers for `getFetch` */
|
||||
|
||||
export const getUser = (sid: string) => {
|
||||
|
||||
@@ -305,6 +305,8 @@ export interface BucketCredential {
|
||||
secret_access_key?: null | string;
|
||||
tags?: null | AwsTag[];
|
||||
service_key?: null | string;
|
||||
connection_string?: null | string;
|
||||
endpoint?: null | string;
|
||||
}
|
||||
|
||||
export interface Application {
|
||||
@@ -318,9 +320,19 @@ export interface Application {
|
||||
speech_synthesis_voice: null | string;
|
||||
speech_synthesis_vendor: null | Lowercase<Vendor>;
|
||||
speech_synthesis_language: null | string;
|
||||
speech_synthesis_label: null | string;
|
||||
speech_recognizer_vendor: null | Lowercase<Vendor>;
|
||||
speech_recognizer_language: null | string;
|
||||
speech_recognizer_label: null | string;
|
||||
record_all_calls: number;
|
||||
use_for_fallback_speech: number;
|
||||
fallback_speech_synthesis_vendor: null | string;
|
||||
fallback_speech_synthesis_language: null | string;
|
||||
fallback_speech_synthesis_voice: null | string;
|
||||
fallback_speech_synthesis_label: null | string;
|
||||
fallback_speech_recognizer_vendor: null | string;
|
||||
fallback_speech_recognizer_language: null | string;
|
||||
fallback_speech_recognizer_label: null | string;
|
||||
}
|
||||
|
||||
export interface PhoneNumber {
|
||||
@@ -375,8 +387,10 @@ export interface SpeechCredential {
|
||||
secret_access_key: null | string;
|
||||
service_key: null | string;
|
||||
use_custom_tts: number;
|
||||
custom_tts_endpoint_url: null | string;
|
||||
custom_tts_endpoint: null | string;
|
||||
use_custom_stt: number;
|
||||
custom_stt_endpoint_url: null | string;
|
||||
custom_stt_endpoint: null | string;
|
||||
client_id: null | string;
|
||||
secret: null | string;
|
||||
@@ -391,6 +405,7 @@ export interface SpeechCredential {
|
||||
auth_token: null | string;
|
||||
custom_stt_url: null | string;
|
||||
custom_tts_url: null | string;
|
||||
label: null | string;
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
|
||||
48
src/components/domain-input/index.tsx
Normal file
48
src/components/domain-input/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
import { Icons } from "../icons";
|
||||
import "./styles.scss";
|
||||
|
||||
type DomainInputProbs = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
value: string;
|
||||
setValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
root_domain: string;
|
||||
placeholder?: string;
|
||||
is_valid: boolean;
|
||||
};
|
||||
|
||||
export const DomainInput = ({
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
setValue,
|
||||
root_domain,
|
||||
is_valid,
|
||||
placeholder,
|
||||
}: DomainInputProbs) => {
|
||||
return (
|
||||
<>
|
||||
<div className="clipboard clipboard-domain">
|
||||
<div className="input-container">
|
||||
<input
|
||||
id={id}
|
||||
name={name}
|
||||
type="text"
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
<div className={`input-icon txt--${is_valid ? "teal" : "red"}`}>
|
||||
{is_valid ? <Icons.CheckCircle /> : <Icons.XCircle />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="root-domain">
|
||||
<p>{root_domain}</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomainInput;
|
||||
55
src/components/domain-input/styles.scss
Normal file
55
src/components/domain-input/styles.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
@use "../../styles/vars";
|
||||
@use "../../styles/mixins";
|
||||
@use "@jambonz/ui-kit/src/styles/vars" as ui-vars;
|
||||
@use "@jambonz/ui-kit/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.input-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.clipboard-domain {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
width: 100%;
|
||||
height: vars.$clipheight;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.internal form & {
|
||||
max-width: calc(#{vars.$widthinput} - #{vars.$clipheight});
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
right: 5%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.root-domain {
|
||||
height: vars.$clipheight;
|
||||
border-bottom-right-radius: ui-vars.$px01;
|
||||
border-top-right-radius: ui-vars.$px01;
|
||||
border: 2px solid ui-vars.$grey;
|
||||
border-left: 0;
|
||||
background-color: ui-vars.$pink;
|
||||
padding: ui-vars.$px01;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&[disabled] {
|
||||
@include mixins.disabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export const Selector = forwardRef<SelectorRef, SelectorProps>(
|
||||
{...restProps}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
<option key={`${id}_${option.value}`} value={option.value}>
|
||||
{option.name}
|
||||
</option>
|
||||
))}
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
Smartphone,
|
||||
Youtube,
|
||||
Mail,
|
||||
Tag,
|
||||
} from "react-feather";
|
||||
|
||||
import type { Icon } from "react-feather";
|
||||
@@ -110,4 +111,5 @@ export const Icons: IconMap = {
|
||||
Smartphone,
|
||||
Youtube,
|
||||
Mail,
|
||||
Tag,
|
||||
};
|
||||
|
||||
@@ -1,41 +1,59 @@
|
||||
import { Button, ButtonGroup, H1, MS } from "@jambonz/ui-kit";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { getAvailability, postSipRealms, useApiData } from "src/api";
|
||||
import { CurrentUserData } from "src/api/types";
|
||||
import { Section } from "src/components";
|
||||
import DomainInput from "src/components/domain-input";
|
||||
import { Message } from "src/components/forms";
|
||||
import { ROUTE_INTERNAL_ACCOUNTS } from "src/router/routes";
|
||||
import { hasValue } from "src/utils";
|
||||
|
||||
export const EditSipRealm = () => {
|
||||
const [name, setName] = useState("");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const [userData] = useApiData<CurrentUserData>("Users/me");
|
||||
const typingTimeoutRef = useRef<number | null>(null);
|
||||
const [isValidDomain, setIsValidDomain] = useState(false);
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const rootDomain = userData?.account?.root_domain;
|
||||
const account_sid = userData?.account?.account_sid;
|
||||
|
||||
getAvailability(`${name}.${rootDomain}`)
|
||||
.then(({ json }) => {
|
||||
if (!json.available) {
|
||||
setErrorMessage("That subdomain is not available.");
|
||||
return;
|
||||
}
|
||||
postSipRealms(account_sid || "", `${name}.${rootDomain}`)
|
||||
.then(() => {
|
||||
navigate(`${ROUTE_INTERNAL_ACCOUNTS}/${account_sid}/edit`);
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.msg);
|
||||
});
|
||||
postSipRealms(account_sid || "", `${name}.${rootDomain}`)
|
||||
.then(() => {
|
||||
navigate(`${ROUTE_INTERNAL_ACCOUNTS}/${account_sid}/edit`);
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.msg);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typingTimeoutRef.current) {
|
||||
clearTimeout(typingTimeoutRef.current);
|
||||
}
|
||||
if (!name || name.length < 3) {
|
||||
setIsValidDomain(false);
|
||||
return;
|
||||
}
|
||||
setIsValidDomain(false);
|
||||
typingTimeoutRef.current = setTimeout(() => {
|
||||
getAvailability(`${name}.${userData?.account?.root_domain}`)
|
||||
.then(({ json }) =>
|
||||
setIsValidDomain(
|
||||
Boolean(json.available) && hasValue(name) && name.length != 0
|
||||
)
|
||||
)
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.msg);
|
||||
setIsValidDomain(false);
|
||||
});
|
||||
}, 500);
|
||||
}, [name]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Edit Sip Realm</H1>
|
||||
@@ -48,18 +66,15 @@ export const EditSipRealm = () => {
|
||||
</MS>
|
||||
{errorMessage && <Message message={errorMessage} />}
|
||||
<br />
|
||||
<input
|
||||
id="name"
|
||||
required
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
<DomainInput
|
||||
id="sip_realm"
|
||||
name="sip_realm"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
setValue={setName}
|
||||
placeholder="Your name here"
|
||||
root_domain={`.${userData?.account?.root_domain || ""}`}
|
||||
is_valid={isValidDomain}
|
||||
/>
|
||||
<label htmlFor="fqdn">
|
||||
FQDN: {name}.{userData?.account?.root_domain}
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<ButtonGroup left>
|
||||
@@ -71,7 +86,7 @@ export const EditSipRealm = () => {
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" small>
|
||||
<Button type="submit" small disabled={!isValidDomain}>
|
||||
Change Sip Realm
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -28,6 +28,8 @@ import { ROUTE_INTERNAL_ACCOUNTS } from "src/router/routes";
|
||||
import {
|
||||
AUDIO_FORMAT_OPTIONS,
|
||||
BUCKET_VENDOR_AWS,
|
||||
BUCKET_VENDOR_S3_COMPATIBLE,
|
||||
BUCKET_VENDOR_AZURE,
|
||||
BUCKET_VENDOR_GOOGLE,
|
||||
BUCKET_VENDOR_OPTIONS,
|
||||
CRED_OK,
|
||||
@@ -129,6 +131,8 @@ export const AccountForm = ({
|
||||
useState(false);
|
||||
const deleteMessageRef = useRef<HTMLInputElement | null>(null);
|
||||
const [isShowModalLoader, setIsShowModalLoader] = useState(false);
|
||||
const [azureConnectionString, setAzureConnectionString] = useState("");
|
||||
const [endpoint, setEndpoint] = useState("");
|
||||
|
||||
/** This lets us map and render the same UI for each... */
|
||||
const webhooks = [
|
||||
@@ -260,6 +264,14 @@ export const AccountForm = ({
|
||||
...(bucketVendor === BUCKET_VENDOR_GOOGLE && {
|
||||
service_key: JSON.stringify(bucketGoogleServiceKey),
|
||||
}),
|
||||
...(bucketVendor === BUCKET_VENDOR_AZURE && {
|
||||
connection_string: azureConnectionString,
|
||||
}),
|
||||
...(bucketVendor === BUCKET_VENDOR_S3_COMPATIBLE && {
|
||||
endpoint: endpoint,
|
||||
access_key_id: bucketAccessKeyId,
|
||||
secret_access_key: bucketSecretAccessKey,
|
||||
}),
|
||||
};
|
||||
|
||||
postAccountBucketCredentialTest(account?.data?.account_sid, cred).then(
|
||||
@@ -391,6 +403,23 @@ export const AccountForm = ({
|
||||
...(hasLength(bucketTags) && { tags: bucketTags }),
|
||||
},
|
||||
}),
|
||||
...(bucketVendor === BUCKET_VENDOR_AZURE && {
|
||||
bucket_credential: {
|
||||
vendor: bucketVendor || null,
|
||||
name: bucketName || null,
|
||||
connection_string: azureConnectionString || null,
|
||||
},
|
||||
}),
|
||||
...(bucketVendor === BUCKET_VENDOR_S3_COMPATIBLE && {
|
||||
bucket_credential: {
|
||||
vendor: bucketVendor || null,
|
||||
endpoint: endpoint || null,
|
||||
name: bucketName || null,
|
||||
access_key_id: bucketAccessKeyId || null,
|
||||
secret_access_key: bucketSecretAccessKey || null,
|
||||
...(hasLength(bucketTags) && { tags: bucketTags }),
|
||||
},
|
||||
}),
|
||||
...(!bucketCredentialChecked && {
|
||||
record_all_calls: 0,
|
||||
bucket_credential: {
|
||||
@@ -495,6 +524,14 @@ export const AccountForm = ({
|
||||
if (account.data.bucket_credential?.region) {
|
||||
setBucketRegion(account.data.bucket_credential?.region);
|
||||
}
|
||||
if (account.data.bucket_credential?.connection_string) {
|
||||
setAzureConnectionString(
|
||||
account.data.bucket_credential.connection_string
|
||||
);
|
||||
}
|
||||
if (account.data.bucket_credential?.endpoint) {
|
||||
setEndpoint(account.data.bucket_credential.endpoint);
|
||||
}
|
||||
if (account.data.record_all_calls) {
|
||||
setRecordAllCalls(account.data.record_all_calls ? true : false);
|
||||
}
|
||||
@@ -1012,43 +1049,71 @@ export const AccountForm = ({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{bucketVendor === BUCKET_VENDOR_S3_COMPATIBLE && (
|
||||
<>
|
||||
<label htmlFor="endpoint">
|
||||
Endpoint URI<span>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="endpoint"
|
||||
required
|
||||
type="text"
|
||||
name="endpoint"
|
||||
placeholder="https://domain.com"
|
||||
value={endpoint}
|
||||
onChange={(e) => {
|
||||
setEndpoint(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<label htmlFor="bucket_name">
|
||||
Bucket Name<span>*</span>
|
||||
{bucketVendor === BUCKET_VENDOR_AZURE
|
||||
? "Container"
|
||||
: "Bucket"}{" "}
|
||||
Name<span>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="bucket_name"
|
||||
required
|
||||
type="text"
|
||||
name="bucket_name"
|
||||
placeholder="Bucket"
|
||||
placeholder={
|
||||
bucketVendor === BUCKET_VENDOR_AZURE
|
||||
? "Container"
|
||||
: "Bucket"
|
||||
}
|
||||
value={bucketName}
|
||||
onChange={(e) => {
|
||||
setBucketName(e.target.value);
|
||||
setTmpBucketName(e.target.value);
|
||||
}}
|
||||
/>
|
||||
{bucketVendor === BUCKET_VENDOR_AWS && (
|
||||
{(bucketVendor === BUCKET_VENDOR_AWS ||
|
||||
bucketVendor === BUCKET_VENDOR_S3_COMPATIBLE) && (
|
||||
<>
|
||||
{regions && regions["aws"] && (
|
||||
<>
|
||||
<label htmlFor="bucket_aws_region">
|
||||
Region<span>*</span>
|
||||
</label>
|
||||
<Selector
|
||||
id="region"
|
||||
name="region"
|
||||
value={bucketRegion}
|
||||
required
|
||||
options={[
|
||||
{
|
||||
name: "Select a region",
|
||||
value: "",
|
||||
},
|
||||
].concat(regions["aws"])}
|
||||
onChange={(e) => setBucketRegion(e.target.value)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{bucketVendor === BUCKET_VENDOR_AWS &&
|
||||
regions &&
|
||||
regions["aws"] && (
|
||||
<>
|
||||
<label htmlFor="bucket_aws_region">
|
||||
Region<span>*</span>
|
||||
</label>
|
||||
<Selector
|
||||
id="region"
|
||||
name="region"
|
||||
value={bucketRegion}
|
||||
required
|
||||
options={[
|
||||
{
|
||||
name: "Select a region",
|
||||
value: "",
|
||||
},
|
||||
].concat(regions["aws"])}
|
||||
onChange={(e) => setBucketRegion(e.target.value)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<label htmlFor="bucket_aws_access_key">
|
||||
Access key ID<span>*</span>
|
||||
</label>
|
||||
@@ -1108,11 +1173,32 @@ export const AccountForm = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{bucketVendor === BUCKET_VENDOR_AZURE && (
|
||||
<>
|
||||
<label htmlFor="bucket_azure_connection_string">
|
||||
Connection String<span>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="bucket_azure_connection_string"
|
||||
required
|
||||
type="text"
|
||||
name="bucket_azure_connection_string"
|
||||
placeholder="Connection string"
|
||||
value={azureConnectionString}
|
||||
onChange={(e) => {
|
||||
setAzureConnectionString(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<label htmlFor="aws_s3_tags">
|
||||
{bucketVendor === BUCKET_VENDOR_AWS
|
||||
{bucketVendor === BUCKET_VENDOR_AWS ||
|
||||
bucketVendor === BUCKET_VENDOR_S3_COMPATIBLE
|
||||
? "S3"
|
||||
: bucketVendor === BUCKET_VENDOR_GOOGLE
|
||||
? "Google Cloud Storage"
|
||||
: bucketVendor === BUCKET_VENDOR_AZURE
|
||||
? "Azure Cloud Storage"
|
||||
: ""}{" "}
|
||||
Tags
|
||||
</label>
|
||||
@@ -1184,7 +1270,13 @@ export const AccountForm = ({
|
||||
(bucketVendor === BUCKET_VENDOR_AWS &&
|
||||
(!bucketAccessKeyId || !bucketSecretAccessKey)) ||
|
||||
(bucketVendor === BUCKET_VENDOR_GOOGLE &&
|
||||
!bucketGoogleServiceKey)
|
||||
!bucketGoogleServiceKey) ||
|
||||
(bucketVendor === BUCKET_VENDOR_AZURE &&
|
||||
!azureConnectionString) ||
|
||||
(bucketVendor === BUCKET_VENDOR_S3_COMPATIBLE &&
|
||||
(!endpoint ||
|
||||
!bucketAccessKeyId ||
|
||||
!bucketSecretAccessKey))
|
||||
}
|
||||
>
|
||||
Test
|
||||
|
||||
@@ -148,7 +148,11 @@ export const ManagePaymentForm = () => {
|
||||
<div className="grid__row">
|
||||
<div></div>
|
||||
<div>
|
||||
<PaymentElement />
|
||||
<PaymentElement
|
||||
options={{
|
||||
paymentMethodOrder: ["card"],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -577,7 +577,11 @@ const SubscriptionForm = () => {
|
||||
<div className="grid__row">
|
||||
<div></div>
|
||||
<div>
|
||||
<PaymentElement />
|
||||
<PaymentElement
|
||||
options={{
|
||||
paymentMethodOrder: ["card"],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import React from "react";
|
||||
import { STRIPE_PUBLISHABLE_KEY } from "src/api/constants";
|
||||
import {
|
||||
ENABLE_HOSTED_SYSTEM,
|
||||
STRIPE_PUBLISHABLE_KEY,
|
||||
} from "src/api/constants";
|
||||
|
||||
import { Elements } from "@stripe/react-stripe-js";
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import SubscriptionForm from "./subscription-form";
|
||||
|
||||
export const stripePromise = loadStripe(STRIPE_PUBLISHABLE_KEY);
|
||||
export const stripePromise = ENABLE_HOSTED_SYSTEM
|
||||
? loadStripe(STRIPE_PUBLISHABLE_KEY)
|
||||
: null;
|
||||
|
||||
export const Subscription = () => {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Button, ButtonGroup, MS } from "@jambonz/ui-kit";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
@@ -16,11 +16,7 @@ import {
|
||||
LANG_EN_US,
|
||||
VENDOR_GOOGLE,
|
||||
LANG_EN_US_STANDARD_C,
|
||||
VENDOR_AWS,
|
||||
VENDOR_WELLSAID,
|
||||
useSpeechVendors,
|
||||
VENDOR_DEEPGRAM,
|
||||
VENDOR_SONIOX,
|
||||
VENDOR_CUSTOM,
|
||||
} from "src/vendor";
|
||||
import {
|
||||
@@ -42,10 +38,8 @@ import {
|
||||
import type {
|
||||
RecognizerVendors,
|
||||
SynthesisVendors,
|
||||
Voice,
|
||||
VoiceLanguage,
|
||||
Language,
|
||||
VendorOptions,
|
||||
LabelOptions,
|
||||
} from "src/vendor/types";
|
||||
|
||||
import type {
|
||||
@@ -59,6 +53,7 @@ import type {
|
||||
import { MSG_REQUIRED_FIELDS, MSG_WEBHOOK_FIELDS } from "src/constants";
|
||||
import { hasLength, isUserAccountScope, useRedirect } from "src/utils";
|
||||
import { setAccountFilter, setLocation } from "src/store/localStore";
|
||||
import SpeechProviderSelection from "./speech-selection";
|
||||
|
||||
type ApplicationFormProps = {
|
||||
application?: UseApiDataMap<Application>;
|
||||
@@ -98,10 +93,42 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
const [message, setMessage] = useState("");
|
||||
const [apiUrl, setApiUrl] = useState("");
|
||||
const [credentials] = useApiData<SpeechCredential[]>(apiUrl);
|
||||
const [softTtsVendor, setSoftTtsVendor] = useState<VendorOptions[]>(vendors);
|
||||
const [softSttVendor, setSoftSttVendor] = useState<VendorOptions[]>(vendors);
|
||||
const [ttsVendorOptions, setttsVendorOptions] =
|
||||
useState<VendorOptions[]>(vendors);
|
||||
const [sttVendorOptions, setSttVendorOptions] =
|
||||
useState<VendorOptions[]>(vendors);
|
||||
const [recogLabel, setRecogLabel] = useState("");
|
||||
const [ttsLabelOptions, setTtsLabelOptions] = useState<LabelOptions[]>([]);
|
||||
const [sttLabelOptions, setSttLabelOptions] = useState<LabelOptions[]>([]);
|
||||
const [fallbackTtsLabelOptions, setFallbackTtsLabelOptions] = useState<
|
||||
LabelOptions[]
|
||||
>([]);
|
||||
const [fallbackSttLabelOptions, setFallbackSttLabelOptions] = useState<
|
||||
LabelOptions[]
|
||||
>([]);
|
||||
const [synthLabel, setSynthLabel] = useState("");
|
||||
const [recordAllCalls, setRecordAllCalls] = useState(false);
|
||||
|
||||
const [useForFallbackSpeech, setUseForFallbackSpeech] = useState(false);
|
||||
const [fallbackSpeechSynthsisVendor, setFallbackSpeechSynthsisVendor] =
|
||||
useState<keyof SynthesisVendors>(VENDOR_GOOGLE);
|
||||
const [fallbackSpeechSynthsisLanguage, setFallbackSpeechSynthsisLanguage] =
|
||||
useState(LANG_EN_US);
|
||||
const [fallbackSpeechSynthsisVoice, setFallbackSpeechSynthsisVoice] =
|
||||
useState(LANG_EN_US_STANDARD_C);
|
||||
const [fallbackSpeechSynthsisLabel, setFallbackSpeechSynthsisLabel] =
|
||||
useState("");
|
||||
const [fallbackSpeechRecognizerVendor, setFallbackSpeechRecognizerVendor] =
|
||||
useState<keyof RecognizerVendors>(VENDOR_GOOGLE);
|
||||
const [
|
||||
fallbackSpeechRecognizerLanguage,
|
||||
setFallbackSpeechRecognizerLanguage,
|
||||
] = useState(LANG_EN_US);
|
||||
const [fallbackSpeechRecognizerLabel, setFallbackSpeechRecognizerLabel] =
|
||||
useState("");
|
||||
const [initalCheckFallbackSpeech, setInitalCheckFallbackSpeech] =
|
||||
useState(false);
|
||||
|
||||
/** This lets us map and render the same UI for each... */
|
||||
const webhooks = [
|
||||
{
|
||||
@@ -171,7 +198,7 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
const payload: Partial<Application> = {
|
||||
name: applicationName,
|
||||
app_json: applicationJson || null,
|
||||
call_hook: callWebhook || null,
|
||||
@@ -180,10 +207,34 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
call_status_hook: statusWebhook || null,
|
||||
speech_synthesis_vendor: synthVendor || null,
|
||||
speech_synthesis_language: synthLang || null,
|
||||
speech_synthesis_label: synthLabel || null,
|
||||
speech_synthesis_voice: synthVoice || null,
|
||||
speech_recognizer_vendor: recogVendor || null,
|
||||
speech_recognizer_language: recogLang || null,
|
||||
speech_recognizer_label: recogLabel || null,
|
||||
record_all_calls: recordAllCalls ? 1 : 0,
|
||||
use_for_fallback_speech: useForFallbackSpeech ? 1 : 0,
|
||||
fallback_speech_synthesis_vendor: useForFallbackSpeech
|
||||
? fallbackSpeechSynthsisVendor || null
|
||||
: null,
|
||||
fallback_speech_synthesis_language: useForFallbackSpeech
|
||||
? fallbackSpeechSynthsisLanguage || null
|
||||
: null,
|
||||
fallback_speech_synthesis_voice: useForFallbackSpeech
|
||||
? fallbackSpeechSynthsisVoice || null
|
||||
: null,
|
||||
fallback_speech_synthesis_label: useForFallbackSpeech
|
||||
? fallbackSpeechSynthsisLabel || null
|
||||
: null,
|
||||
fallback_speech_recognizer_vendor: useForFallbackSpeech
|
||||
? fallbackSpeechRecognizerVendor || null
|
||||
: null,
|
||||
fallback_speech_recognizer_language: useForFallbackSpeech
|
||||
? fallbackSpeechRecognizerLanguage || null
|
||||
: null,
|
||||
fallback_speech_recognizer_label: useForFallbackSpeech
|
||||
? fallbackSpeechRecognizerLabel || null
|
||||
: null,
|
||||
};
|
||||
|
||||
if (application && application.data) {
|
||||
@@ -211,7 +262,7 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
useMemo(() => {
|
||||
if (credentials && hasLength(credentials)) {
|
||||
const v = credentials
|
||||
.filter((tv) => tv.vendor.startsWith(VENDOR_CUSTOM) && tv.use_for_tts)
|
||||
@@ -223,7 +274,7 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
value: tv.vendor,
|
||||
})
|
||||
);
|
||||
setSoftTtsVendor(vendors.concat(v));
|
||||
setttsVendorOptions(vendors.concat(v));
|
||||
|
||||
const v2 = credentials
|
||||
.filter((tv) => tv.vendor.startsWith(VENDOR_CUSTOM) && tv.use_for_stt)
|
||||
@@ -235,9 +286,100 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
value: tv.vendor,
|
||||
})
|
||||
);
|
||||
setSoftSttVendor(vendors.concat(v2));
|
||||
setSttVendorOptions(vendors.concat(v2));
|
||||
|
||||
const noneLabelObject = {
|
||||
name: "None",
|
||||
value: "",
|
||||
};
|
||||
|
||||
let c1 = credentials.filter(
|
||||
(c) =>
|
||||
c.vendor === synthVendor &&
|
||||
(!c.account_sid || c.account_sid === accountSid) &&
|
||||
c.use_for_tts
|
||||
);
|
||||
let c2 = c1
|
||||
.filter((c) => c.label)
|
||||
.map((c) =>
|
||||
Object.assign({
|
||||
name: c.label,
|
||||
value: c.label,
|
||||
})
|
||||
);
|
||||
|
||||
setTtsLabelOptions(
|
||||
c1.length !== c2.length ? [noneLabelObject, ...c2] : c2
|
||||
);
|
||||
|
||||
c1 = fallbackSpeechSynthsisVendor
|
||||
? credentials.filter(
|
||||
(c) =>
|
||||
c.vendor === fallbackSpeechSynthsisVendor &&
|
||||
(!c.account_sid || c.account_sid === accountSid) &&
|
||||
c.use_for_tts
|
||||
)
|
||||
: [];
|
||||
|
||||
c2 = c1
|
||||
.filter((c) => c.label)
|
||||
.map((c) =>
|
||||
Object.assign({
|
||||
name: c.label,
|
||||
value: c.label,
|
||||
})
|
||||
);
|
||||
setFallbackTtsLabelOptions(
|
||||
c1.length !== c2.length ? [noneLabelObject, ...c2] : c2
|
||||
);
|
||||
|
||||
c1 = credentials.filter(
|
||||
(c) =>
|
||||
c.vendor === recogVendor &&
|
||||
(!c.account_sid || c.account_sid === accountSid) &&
|
||||
c.use_for_stt
|
||||
);
|
||||
c2 = c1
|
||||
.filter((c) => c.label)
|
||||
.map((c) =>
|
||||
Object.assign({
|
||||
name: c.label,
|
||||
value: c.label,
|
||||
})
|
||||
);
|
||||
|
||||
setSttLabelOptions(
|
||||
c1.length !== c2.length ? [noneLabelObject, ...c2] : c2
|
||||
);
|
||||
|
||||
c1 = fallbackSpeechRecognizerVendor
|
||||
? credentials.filter(
|
||||
(c) =>
|
||||
c.vendor === fallbackSpeechRecognizerVendor &&
|
||||
(!c.account_sid || c.account_sid === accountSid) &&
|
||||
c.use_for_stt
|
||||
)
|
||||
: [];
|
||||
c2 = c1
|
||||
.filter((c) => c.label)
|
||||
.map((c) =>
|
||||
Object.assign({
|
||||
name: c.label,
|
||||
value: c.label,
|
||||
})
|
||||
);
|
||||
|
||||
setFallbackSttLabelOptions(
|
||||
c1.length !== c2.length ? [noneLabelObject, ...c2] : c2
|
||||
);
|
||||
}
|
||||
}, [credentials]);
|
||||
}, [
|
||||
credentials,
|
||||
synthVendor,
|
||||
recogVendor,
|
||||
fallbackSpeechRecognizerVendor,
|
||||
fallbackSpeechSynthsisVendor,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (accountSid) {
|
||||
@@ -321,9 +463,90 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
|
||||
if (application.data.speech_recognizer_language)
|
||||
setRecogLang(application.data.speech_recognizer_language);
|
||||
if (application.data.speech_synthesis_label) {
|
||||
setSynthLabel(application.data.speech_synthesis_label);
|
||||
}
|
||||
if (application.data.speech_recognizer_label) {
|
||||
setRecogLabel(application.data.speech_recognizer_label);
|
||||
}
|
||||
if (application.data.use_for_fallback_speech) {
|
||||
setUseForFallbackSpeech(application.data.use_for_fallback_speech > 0);
|
||||
setInitalCheckFallbackSpeech(
|
||||
application.data.use_for_fallback_speech > 0
|
||||
);
|
||||
}
|
||||
if (application.data.fallback_speech_recognizer_vendor) {
|
||||
setFallbackSpeechRecognizerVendor(
|
||||
application.data
|
||||
.fallback_speech_recognizer_vendor as keyof RecognizerVendors
|
||||
);
|
||||
}
|
||||
if (application.data.fallback_speech_recognizer_language) {
|
||||
setFallbackSpeechRecognizerLanguage(
|
||||
application.data.fallback_speech_recognizer_language
|
||||
);
|
||||
}
|
||||
if (application.data.fallback_speech_recognizer_label) {
|
||||
setFallbackSpeechRecognizerLabel(
|
||||
application.data.fallback_speech_recognizer_label
|
||||
);
|
||||
}
|
||||
if (application.data.fallback_speech_synthesis_vendor) {
|
||||
setFallbackSpeechSynthsisVendor(
|
||||
application.data
|
||||
.fallback_speech_synthesis_vendor as keyof SynthesisVendors
|
||||
);
|
||||
}
|
||||
if (application.data.fallback_speech_synthesis_language) {
|
||||
setFallbackSpeechSynthsisLanguage(
|
||||
application.data.fallback_speech_synthesis_language
|
||||
);
|
||||
}
|
||||
if (application.data.fallback_speech_synthesis_voice) {
|
||||
setFallbackSpeechSynthsisVoice(
|
||||
application.data.fallback_speech_synthesis_voice
|
||||
);
|
||||
}
|
||||
if (application.data.fallback_speech_synthesis_label) {
|
||||
setFallbackSpeechSynthsisLabel(
|
||||
application.data.fallback_speech_synthesis_label
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [application]);
|
||||
|
||||
const swapPrimaryAndfalloverSpeech = () => {
|
||||
let tmp;
|
||||
|
||||
tmp = synthVendor;
|
||||
setSynthVendor(fallbackSpeechSynthsisVendor);
|
||||
setFallbackSpeechSynthsisVendor(tmp);
|
||||
|
||||
tmp = synthLang;
|
||||
setSynthLang(fallbackSpeechSynthsisLanguage);
|
||||
setFallbackSpeechSynthsisLanguage(synthLang);
|
||||
|
||||
tmp = synthVoice;
|
||||
setSynthVoice(fallbackSpeechSynthsisVoice);
|
||||
setFallbackSpeechSynthsisVoice(tmp);
|
||||
|
||||
tmp = synthLabel;
|
||||
setSynthLabel(fallbackSpeechSynthsisLabel);
|
||||
setFallbackSpeechSynthsisLabel(tmp);
|
||||
|
||||
tmp = recogVendor;
|
||||
setRecogVendor(fallbackSpeechRecognizerVendor);
|
||||
setFallbackSpeechRecognizerVendor(tmp);
|
||||
|
||||
tmp = recogLang;
|
||||
setRecogLang(fallbackSpeechRecognizerLanguage);
|
||||
setFallbackSpeechRecognizerLanguage(tmp);
|
||||
|
||||
tmp = recogLabel;
|
||||
setRecogLabel(fallbackSpeechRecognizerLabel);
|
||||
setFallbackSpeechRecognizerLabel(tmp);
|
||||
};
|
||||
|
||||
return (
|
||||
<Section slim>
|
||||
<form className="form form--internal" onSubmit={handleSubmit}>
|
||||
@@ -450,216 +673,81 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
</fieldset>
|
||||
);
|
||||
})}
|
||||
{synthesis && (
|
||||
<fieldset>
|
||||
<label htmlFor="synthesis_vendor">Speech synthesis vendor</label>
|
||||
<Selector
|
||||
id="synthesis_vendor"
|
||||
name="synthesis_vendor"
|
||||
value={synthVendor}
|
||||
options={softTtsVendor.filter(
|
||||
(vendor) =>
|
||||
vendor.value != VENDOR_DEEPGRAM &&
|
||||
vendor.value != VENDOR_SONIOX &&
|
||||
vendor.value !== VENDOR_CUSTOM
|
||||
)}
|
||||
onChange={(e) => {
|
||||
const vendor = e.target.value as keyof SynthesisVendors;
|
||||
setSynthVendor(vendor);
|
||||
<SpeechProviderSelection
|
||||
credentials={credentials}
|
||||
synthesis={synthesis}
|
||||
ttsVendor={[synthVendor, setSynthVendor]}
|
||||
ttsVendorOptions={ttsVendorOptions}
|
||||
ttsVoice={[synthVoice, setSynthVoice]}
|
||||
ttsLang={[synthLang, setSynthLang]}
|
||||
ttsLabelOptions={ttsLabelOptions}
|
||||
ttsLabel={[synthLabel, setSynthLabel]}
|
||||
recognizers={recognizers}
|
||||
sttVendor={[recogVendor, setRecogVendor]}
|
||||
sttVendorOptions={sttVendorOptions}
|
||||
sttLang={[recogLang, setRecogLang]}
|
||||
sttLabelOptions={sttLabelOptions}
|
||||
sttLabel={[recogLabel, setRecogLabel]}
|
||||
/>
|
||||
|
||||
/** When Custom Vendor is used, user you have to input the lange and voice. */
|
||||
if (vendor.toString().startsWith(VENDOR_CUSTOM)) {
|
||||
setSynthVoice("");
|
||||
return;
|
||||
}
|
||||
|
||||
/** When using Google and en-US, ensure "Standard-C" is used as default */
|
||||
if (
|
||||
e.target.value === VENDOR_GOOGLE &&
|
||||
synthLang === LANG_EN_US
|
||||
) {
|
||||
setSynthVoice(LANG_EN_US_STANDARD_C);
|
||||
return;
|
||||
}
|
||||
|
||||
/** Google and AWS have different language lists */
|
||||
/** If the new language doesn't map then default to "en-US" */
|
||||
let newLang = synthesis[vendor].find(
|
||||
(lang) => lang.code === synthLang
|
||||
);
|
||||
|
||||
if (newLang) {
|
||||
setSynthVoice(newLang.voices[0].value);
|
||||
return;
|
||||
}
|
||||
|
||||
newLang = synthesis[vendor].find(
|
||||
(lang) => lang.code === LANG_EN_US
|
||||
);
|
||||
|
||||
setSynthLang(LANG_EN_US);
|
||||
setSynthVoice(newLang!.voices[0].value);
|
||||
}}
|
||||
<fieldset>
|
||||
<Checkzone
|
||||
hidden
|
||||
name="cz_fallback_speech"
|
||||
label="Use a fallback speech vendor if primary fails"
|
||||
initialCheck={initalCheckFallbackSpeech}
|
||||
handleChecked={(e) => {
|
||||
setUseForFallbackSpeech(e.target.checked);
|
||||
}}
|
||||
>
|
||||
<SpeechProviderSelection
|
||||
credentials={credentials}
|
||||
synthesis={synthesis}
|
||||
ttsVendor={[
|
||||
fallbackSpeechSynthsisVendor,
|
||||
setFallbackSpeechSynthsisVendor,
|
||||
]}
|
||||
ttsVendorOptions={ttsVendorOptions}
|
||||
ttsVoice={[
|
||||
fallbackSpeechSynthsisVoice,
|
||||
setFallbackSpeechSynthsisVoice,
|
||||
]}
|
||||
ttsLang={[
|
||||
fallbackSpeechSynthsisLanguage,
|
||||
setFallbackSpeechSynthsisLanguage,
|
||||
]}
|
||||
ttsLabelOptions={fallbackTtsLabelOptions}
|
||||
ttsLabel={[
|
||||
fallbackSpeechSynthsisLabel,
|
||||
setFallbackSpeechSynthsisLabel,
|
||||
]}
|
||||
recognizers={recognizers}
|
||||
sttVendor={[
|
||||
fallbackSpeechRecognizerVendor,
|
||||
setFallbackSpeechRecognizerVendor,
|
||||
]}
|
||||
sttVendorOptions={sttVendorOptions}
|
||||
sttLang={[
|
||||
fallbackSpeechRecognizerLanguage,
|
||||
setFallbackSpeechRecognizerLanguage,
|
||||
]}
|
||||
sttLabelOptions={fallbackSttLabelOptions}
|
||||
sttLabel={[
|
||||
fallbackSpeechRecognizerLabel,
|
||||
setFallbackSpeechRecognizerLabel,
|
||||
]}
|
||||
/>
|
||||
{synthVendor &&
|
||||
!synthVendor.toString().startsWith(VENDOR_CUSTOM) &&
|
||||
synthLang && (
|
||||
<>
|
||||
<label htmlFor="synthesis_lang">Language</label>
|
||||
<Selector
|
||||
id="synthesis_lang"
|
||||
name="synthesis_lang"
|
||||
value={synthLang}
|
||||
options={synthesis[
|
||||
synthVendor as keyof SynthesisVendors
|
||||
].map((lang: VoiceLanguage) => ({
|
||||
name: lang.name,
|
||||
value: lang.code,
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
const language = e.target.value;
|
||||
setSynthLang(language);
|
||||
|
||||
/** When using Google and en-US, ensure "Standard-C" is used as default */
|
||||
if (
|
||||
synthVendor === VENDOR_GOOGLE &&
|
||||
language === LANG_EN_US
|
||||
) {
|
||||
setSynthVoice(LANG_EN_US_STANDARD_C);
|
||||
return;
|
||||
}
|
||||
|
||||
const newLang = synthesis[
|
||||
synthVendor as keyof SynthesisVendors
|
||||
].find((lang) => lang.code === language);
|
||||
|
||||
setSynthVoice(newLang!.voices[0].value);
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="synthesis_voice">Voice</label>
|
||||
<Selector
|
||||
id="synthesis_voice"
|
||||
name="synthesis_voice"
|
||||
value={synthVoice}
|
||||
options={
|
||||
synthesis[synthVendor as keyof SynthesisVendors]
|
||||
.filter(
|
||||
(lang: VoiceLanguage) => lang.code === synthLang
|
||||
)
|
||||
.flatMap((lang: VoiceLanguage) =>
|
||||
lang.voices.map((voice: Voice) => ({
|
||||
name: voice.name,
|
||||
value: voice.value,
|
||||
}))
|
||||
) as Voice[]
|
||||
}
|
||||
onChange={(e) => setSynthVoice(e.target.value)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{synthVendor.toString().startsWith(VENDOR_CUSTOM) && (
|
||||
<>
|
||||
<label htmlFor="custom_vendor_synthesis_lang">Language</label>
|
||||
<input
|
||||
id="custom_vendor_synthesis_lang"
|
||||
type="text"
|
||||
name="custom_vendor_synthesis_lang"
|
||||
placeholder="Required"
|
||||
required
|
||||
value={synthLang}
|
||||
onChange={(e) => {
|
||||
setSynthLang(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<label htmlFor="custom_vendor_synthesis_voice">Voice</label>
|
||||
<input
|
||||
id="custom_vendor_synthesis_voice"
|
||||
type="text"
|
||||
name="custom_vendor_synthesis_voice"
|
||||
placeholder="Required"
|
||||
required
|
||||
value={synthVoice}
|
||||
onChange={(e) => {
|
||||
setSynthVoice(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</fieldset>
|
||||
)}
|
||||
{recognizers && (
|
||||
<fieldset>
|
||||
<label htmlFor="recognizer_vendor">Speech recognizer vendor</label>
|
||||
<Selector
|
||||
id="recognizer_vendor"
|
||||
name="recognizer_vendor"
|
||||
value={recogVendor}
|
||||
options={softSttVendor.filter(
|
||||
(vendor) =>
|
||||
vendor.value != VENDOR_WELLSAID &&
|
||||
vendor.value !== VENDOR_CUSTOM
|
||||
)}
|
||||
onChange={(e) => {
|
||||
const vendor = e.target.value as keyof RecognizerVendors;
|
||||
setRecogVendor(vendor);
|
||||
|
||||
/**When vendor is custom, Language is input by user */
|
||||
if (vendor.toString() === VENDOR_CUSTOM) return;
|
||||
|
||||
/** Google and AWS have different language lists */
|
||||
/** If the new language doesn't map then default to "en-US" */
|
||||
const newLang = recognizers[vendor].find(
|
||||
(lang: Language) => lang.code === recogLang
|
||||
);
|
||||
|
||||
if (
|
||||
(vendor === VENDOR_GOOGLE || vendor === VENDOR_AWS) &&
|
||||
!newLang
|
||||
) {
|
||||
setRecogLang(LANG_EN_US);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{recogVendor &&
|
||||
!recogVendor.toString().startsWith(VENDOR_CUSTOM) &&
|
||||
recogLang && (
|
||||
<>
|
||||
<label htmlFor="recognizer_lang">Language</label>
|
||||
<Selector
|
||||
id="recognizer_lang"
|
||||
name="recognizer_lang"
|
||||
value={recogLang}
|
||||
options={recognizers[
|
||||
recogVendor as keyof RecognizerVendors
|
||||
].map((lang: Language) => ({
|
||||
name: lang.name,
|
||||
value: lang.code,
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
setRecogLang(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{recogVendor.toString().startsWith(VENDOR_CUSTOM) && (
|
||||
<>
|
||||
<label htmlFor="custom_vendor_recognizer_voice">Language</label>
|
||||
<input
|
||||
id="custom_vendor_recognizer_voice"
|
||||
type="text"
|
||||
name="custom_vendor_recognizer_voice"
|
||||
placeholder="Required"
|
||||
required
|
||||
value={recogLang}
|
||||
onChange={(e) => {
|
||||
setRecogLang(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</fieldset>
|
||||
)}
|
||||
<fieldset>
|
||||
<Button
|
||||
type="button"
|
||||
small
|
||||
onClick={swapPrimaryAndfalloverSpeech}
|
||||
>
|
||||
Swap primary and fallback
|
||||
</Button>
|
||||
</fieldset>
|
||||
</Checkzone>
|
||||
</fieldset>
|
||||
{(import.meta.env.INITIAL_APP_JSON_ENABLED === undefined ||
|
||||
import.meta.env.INITIAL_APP_JSON_ENABLED) && (
|
||||
<fieldset>
|
||||
|
||||
339
src/containers/internal/views/applications/speech-selection.tsx
Normal file
339
src/containers/internal/views/applications/speech-selection.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { SpeechCredential } from "src/api/types";
|
||||
import { Selector } from "src/components/forms";
|
||||
import { hasLength } from "src/utils";
|
||||
import {
|
||||
LANG_EN_US,
|
||||
LANG_EN_US_STANDARD_C,
|
||||
VENDOR_AWS,
|
||||
VENDOR_CUSTOM,
|
||||
VENDOR_DEEPGRAM,
|
||||
VENDOR_GOOGLE,
|
||||
VENDOR_MICROSOFT,
|
||||
VENDOR_SONIOX,
|
||||
VENDOR_WELLSAID,
|
||||
} from "src/vendor";
|
||||
import {
|
||||
LabelOptions,
|
||||
Language,
|
||||
RecognizerVendors,
|
||||
SynthesisVendors,
|
||||
VendorOptions,
|
||||
Voice,
|
||||
VoiceLanguage,
|
||||
} from "src/vendor/types";
|
||||
type SpeechProviderSelectionProbs = {
|
||||
credentials: SpeechCredential[] | undefined;
|
||||
synthesis: SynthesisVendors | undefined;
|
||||
ttsVendor: [
|
||||
keyof SynthesisVendors,
|
||||
React.Dispatch<React.SetStateAction<keyof SynthesisVendors>>
|
||||
];
|
||||
ttsVendorOptions: VendorOptions[];
|
||||
ttsVoice: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
ttsLang: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
ttsLabelOptions: LabelOptions[];
|
||||
ttsLabel: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
recognizers: RecognizerVendors | undefined;
|
||||
sttVendor: [
|
||||
keyof RecognizerVendors,
|
||||
React.Dispatch<React.SetStateAction<keyof RecognizerVendors>>
|
||||
];
|
||||
sttVendorOptions: VendorOptions[];
|
||||
sttLang: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
sttLabelOptions: LabelOptions[];
|
||||
sttLabel: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
};
|
||||
|
||||
export const SpeechProviderSelection = ({
|
||||
credentials,
|
||||
synthesis,
|
||||
ttsVendor: [synthVendor, setSynthVendor],
|
||||
ttsVendorOptions,
|
||||
ttsVoice: [synthVoice, setSynthVoice],
|
||||
ttsLang: [synthLang, setSynthLang],
|
||||
ttsLabelOptions,
|
||||
ttsLabel: [synthLabel, setSynthLabel],
|
||||
recognizers,
|
||||
sttVendor: [recogVendor, setRecogVendor],
|
||||
sttVendorOptions,
|
||||
sttLang: [recogLang, setRecogLang],
|
||||
sttLabelOptions,
|
||||
sttLabel: [recogLabel, setRecogLabel],
|
||||
}: SpeechProviderSelectionProbs) => {
|
||||
const [selectedCredential, setSelectedCredential] = useState<
|
||||
SpeechCredential | undefined
|
||||
>();
|
||||
|
||||
useEffect(() => {
|
||||
if (credentials) {
|
||||
setSelectedCredential(
|
||||
credentials.find(
|
||||
(c) => c.vendor === synthVendor && c.label === synthLabel
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [synthVendor, synthLabel, credentials]);
|
||||
return (
|
||||
<>
|
||||
{synthesis && (
|
||||
<fieldset>
|
||||
<label htmlFor="synthesis_vendor">Speech synthesis vendor</label>
|
||||
<Selector
|
||||
id="synthesis_vendor"
|
||||
name="synthesis_vendor"
|
||||
value={synthVendor}
|
||||
options={ttsVendorOptions.filter(
|
||||
(vendor) =>
|
||||
vendor.value != VENDOR_DEEPGRAM &&
|
||||
vendor.value != VENDOR_SONIOX &&
|
||||
vendor.value !== VENDOR_CUSTOM
|
||||
)}
|
||||
onChange={(e) => {
|
||||
const vendor = e.target.value as keyof SynthesisVendors;
|
||||
setSynthVendor(vendor);
|
||||
setSynthLabel("");
|
||||
|
||||
/** When Custom Vendor is used, user you have to input the lange and voice. */
|
||||
if (vendor.toString().startsWith(VENDOR_CUSTOM)) {
|
||||
setSynthVoice("");
|
||||
return;
|
||||
}
|
||||
|
||||
/** When using Google and en-US, ensure "Standard-C" is used as default */
|
||||
if (
|
||||
e.target.value === VENDOR_GOOGLE &&
|
||||
synthLang === LANG_EN_US
|
||||
) {
|
||||
setSynthVoice(LANG_EN_US_STANDARD_C);
|
||||
return;
|
||||
}
|
||||
|
||||
/** Google and AWS have different language lists */
|
||||
/** If the new language doesn't map then default to "en-US" */
|
||||
let newLang = synthesis[vendor].find(
|
||||
(lang) => lang.code === synthLang
|
||||
);
|
||||
|
||||
if (newLang) {
|
||||
setSynthVoice(newLang.voices[0].value);
|
||||
return;
|
||||
}
|
||||
|
||||
newLang = synthesis[vendor].find(
|
||||
(lang) => lang.code === LANG_EN_US
|
||||
);
|
||||
|
||||
setSynthLang(LANG_EN_US);
|
||||
setSynthVoice(newLang!.voices[0].value);
|
||||
}}
|
||||
/>
|
||||
{hasLength(ttsLabelOptions) && ttsLabelOptions.length > 1 && (
|
||||
<>
|
||||
<label htmlFor="synthesis_label">Label</label>
|
||||
<Selector
|
||||
id="systhesis_label"
|
||||
name="systhesis_label"
|
||||
value={synthLabel}
|
||||
options={ttsLabelOptions}
|
||||
onChange={(e) => {
|
||||
setSynthLabel(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{synthVendor &&
|
||||
!synthVendor.toString().startsWith(VENDOR_CUSTOM) &&
|
||||
synthLang && (
|
||||
<>
|
||||
<label htmlFor="synthesis_lang">Language</label>
|
||||
<Selector
|
||||
id="synthesis_lang"
|
||||
name="synthesis_lang"
|
||||
value={synthLang}
|
||||
options={synthesis[synthVendor as keyof SynthesisVendors].map(
|
||||
(lang: VoiceLanguage) => ({
|
||||
name: lang.name,
|
||||
value: lang.code,
|
||||
})
|
||||
)}
|
||||
onChange={(e) => {
|
||||
const language = e.target.value;
|
||||
setSynthLang(language);
|
||||
|
||||
/** When using Google and en-US, ensure "Standard-C" is used as default */
|
||||
if (
|
||||
synthVendor === VENDOR_GOOGLE &&
|
||||
language === LANG_EN_US
|
||||
) {
|
||||
setSynthVoice(LANG_EN_US_STANDARD_C);
|
||||
return;
|
||||
}
|
||||
|
||||
const newLang = synthesis[
|
||||
synthVendor as keyof SynthesisVendors
|
||||
].find((lang) => lang.code === language);
|
||||
|
||||
setSynthVoice(newLang!.voices[0].value);
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="synthesis_voice">Voice</label>
|
||||
{synthVendor === VENDOR_MICROSOFT &&
|
||||
selectedCredential &&
|
||||
selectedCredential.use_custom_tts ? (
|
||||
<input
|
||||
id="custom_microsoft_synthesis_voice"
|
||||
type="text"
|
||||
name="custom_microsoft_synthesis_voice"
|
||||
placeholder="Required"
|
||||
required
|
||||
value={synthVoice}
|
||||
onChange={(e) => {
|
||||
setSynthVoice(e.target.value);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Selector
|
||||
id="synthesis_voice"
|
||||
name="synthesis_voice"
|
||||
value={synthVoice}
|
||||
options={
|
||||
synthesis[synthVendor as keyof SynthesisVendors]
|
||||
.filter(
|
||||
(lang: VoiceLanguage) => lang.code === synthLang
|
||||
)
|
||||
.flatMap((lang: VoiceLanguage) =>
|
||||
lang.voices.map((voice: Voice) => ({
|
||||
name: voice.name,
|
||||
value: voice.value,
|
||||
}))
|
||||
) as Voice[]
|
||||
}
|
||||
onChange={(e) => setSynthVoice(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{synthVendor.toString().startsWith(VENDOR_CUSTOM) && (
|
||||
<>
|
||||
<label htmlFor="custom_vendor_synthesis_lang">Language</label>
|
||||
<input
|
||||
id="custom_vendor_synthesis_lang"
|
||||
type="text"
|
||||
name="custom_vendor_synthesis_lang"
|
||||
placeholder="Required"
|
||||
required
|
||||
value={synthLang}
|
||||
onChange={(e) => {
|
||||
setSynthLang(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<label htmlFor="custom_vendor_synthesis_voice">Voice</label>
|
||||
<input
|
||||
id="custom_vendor_synthesis_voice"
|
||||
type="text"
|
||||
name="custom_vendor_synthesis_voice"
|
||||
placeholder="Required"
|
||||
required
|
||||
value={synthVoice}
|
||||
onChange={(e) => {
|
||||
setSynthVoice(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</fieldset>
|
||||
)}
|
||||
{recognizers && (
|
||||
<fieldset>
|
||||
<label htmlFor="recognizer_vendor">Speech recognizer vendor</label>
|
||||
<Selector
|
||||
id="recognizer_vendor"
|
||||
name="recognizer_vendor"
|
||||
value={recogVendor}
|
||||
options={sttVendorOptions.filter(
|
||||
(vendor) =>
|
||||
vendor.value != VENDOR_WELLSAID &&
|
||||
vendor.value !== VENDOR_CUSTOM
|
||||
)}
|
||||
onChange={(e) => {
|
||||
const vendor = e.target.value as keyof RecognizerVendors;
|
||||
setRecogVendor(vendor);
|
||||
setRecogLabel("");
|
||||
|
||||
/**When vendor is custom, Language is input by user */
|
||||
if (vendor.toString() === VENDOR_CUSTOM) return;
|
||||
|
||||
/** Google and AWS have different language lists */
|
||||
/** If the new language doesn't map then default to "en-US" */
|
||||
const newLang = recognizers[vendor].find(
|
||||
(lang: Language) => lang.code === recogLang
|
||||
);
|
||||
|
||||
if (
|
||||
(vendor === VENDOR_GOOGLE || vendor === VENDOR_AWS) &&
|
||||
!newLang
|
||||
) {
|
||||
setRecogLang(LANG_EN_US);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{hasLength(sttLabelOptions) && sttLabelOptions.length > 1 && (
|
||||
<>
|
||||
<label htmlFor="recog_label">Label</label>
|
||||
<Selector
|
||||
id="recog_label"
|
||||
name="recog_label"
|
||||
value={recogLabel}
|
||||
options={sttLabelOptions}
|
||||
onChange={(e) => {
|
||||
setRecogLabel(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{recogVendor &&
|
||||
!recogVendor.toString().startsWith(VENDOR_CUSTOM) &&
|
||||
recogLang && (
|
||||
<>
|
||||
<label htmlFor="recognizer_lang">Language</label>
|
||||
<Selector
|
||||
id="recognizer_lang"
|
||||
name="recognizer_lang"
|
||||
value={recogLang}
|
||||
options={recognizers[
|
||||
recogVendor as keyof RecognizerVendors
|
||||
].map((lang: Language) => ({
|
||||
name: lang.name,
|
||||
value: lang.code,
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
setRecogLang(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{recogVendor.toString().startsWith(VENDOR_CUSTOM) && (
|
||||
<>
|
||||
<label htmlFor="custom_vendor_recognizer_voice">Language</label>
|
||||
<input
|
||||
id="custom_vendor_recognizer_voice"
|
||||
type="text"
|
||||
name="custom_vendor_recognizer_voice"
|
||||
placeholder="Required"
|
||||
required
|
||||
value={recogLang}
|
||||
onChange={(e) => {
|
||||
setRecogLang(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</fieldset>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeechProviderSelection;
|
||||
@@ -30,16 +30,24 @@ import {
|
||||
API_SIP_GATEWAY,
|
||||
API_SMPP_GATEWAY,
|
||||
CARRIER_REG_OK,
|
||||
ENABLE_HOSTED_SYSTEM,
|
||||
USER_ACCOUNT,
|
||||
} from "src/api/constants";
|
||||
import { DeleteCarrier } from "./delete";
|
||||
|
||||
import type { Account, Carrier, SipGateway, SmppGateway } from "src/api/types";
|
||||
import type {
|
||||
Account,
|
||||
Carrier,
|
||||
CurrentUserData,
|
||||
SipGateway,
|
||||
SmppGateway,
|
||||
} from "src/api/types";
|
||||
import { Scope } from "src/store/types";
|
||||
import { getAccountFilter, setLocation } from "src/store/localStore";
|
||||
|
||||
export const Carriers = () => {
|
||||
const user = useSelectState("user");
|
||||
const [userData] = useApiData<CurrentUserData>("Users/me");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [apiUrl, setApiUrl] = useState("");
|
||||
const [carrier, setCarrier] = useState<Carrier | null>(null);
|
||||
@@ -130,7 +138,16 @@ export const Carriers = () => {
|
||||
return (
|
||||
<>
|
||||
<section className="mast">
|
||||
<H1 className="h2">Carriers</H1>
|
||||
<div>
|
||||
<H1 className="h2">Carriers</H1>
|
||||
{ENABLE_HOSTED_SYSTEM && (
|
||||
<M>
|
||||
Have your carrier send calls to{" "}
|
||||
<span>{userData?.account?.sip_realm}</span>
|
||||
</M>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link to={`${ROUTE_INTERNAL_CARRIERS}/add`} title="Add a Carrier">
|
||||
{" "}
|
||||
<Icon>
|
||||
|
||||
@@ -5,7 +5,7 @@ import ClientsForm from "./form";
|
||||
export const ClientsAdd = () => {
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Add client</H1>
|
||||
<H1 className="h2">Add sip client</H1>
|
||||
<ClientsForm />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ClientsDelete = ({
|
||||
<>
|
||||
<Modal handleCancel={handleCancel} handleSubmit={handleSubmit}>
|
||||
<P>
|
||||
Are you sure you want to delete the client{" "}
|
||||
Are you sure you want to delete the sip client{" "}
|
||||
<strong>{client.username}</strong>?
|
||||
</P>
|
||||
</Modal>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const ClientsEdit = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Edit client</H1>
|
||||
<H1 className="h2">Edit sip client</H1>
|
||||
<ClientsForm client={{ data, refetch, error }} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Button, H1, Icon, M } from "@jambonz/ui-kit";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { deleteClient, useApiData, useServiceProviderData } from "src/api";
|
||||
import { Account, Client } from "src/api/types";
|
||||
import { Account, Client, CurrentUserData } from "src/api/types";
|
||||
import {
|
||||
AccountFilter,
|
||||
Icons,
|
||||
@@ -20,10 +20,14 @@ import { USER_ACCOUNT } from "src/api/constants";
|
||||
|
||||
export const Clients = () => {
|
||||
const user = useSelectState("user");
|
||||
const [userData] = useApiData<CurrentUserData>("Users/me");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [clients, refetch] = useApiData<Client[]>("Clients");
|
||||
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [selectedAccount, setSelectedAccount] = useState<
|
||||
Account | null | undefined
|
||||
>(null);
|
||||
const [filter, setFilter] = useState("");
|
||||
const [client, setClient] = useState<Client | null>();
|
||||
|
||||
@@ -33,6 +37,12 @@ export const Clients = () => {
|
||||
return clients;
|
||||
}
|
||||
|
||||
setSelectedAccount(
|
||||
accountSid
|
||||
? accounts?.find((a: Account) => a.account_sid === accountSid)
|
||||
: null
|
||||
);
|
||||
|
||||
return clients
|
||||
? clients.filter((c) => {
|
||||
return accountSid
|
||||
@@ -52,7 +62,7 @@ export const Clients = () => {
|
||||
.then(() => {
|
||||
toastSuccess(
|
||||
<>
|
||||
Deleted outbound call route <strong>{client.username}</strong>
|
||||
Deleted sip client <strong>{client.username}</strong>
|
||||
</>
|
||||
);
|
||||
setClient(null);
|
||||
@@ -67,8 +77,48 @@ export const Clients = () => {
|
||||
return (
|
||||
<>
|
||||
<section className="mast">
|
||||
<H1 className="h2">Clients</H1>
|
||||
<Link to={`${ROUTE_INTERNAL_CLIENTS}/add`} title="Add a client">
|
||||
<div>
|
||||
<H1 className="h2">SIP client credentials</H1>
|
||||
{user?.scope === USER_ACCOUNT ? (
|
||||
userData?.account?.sip_realm ? (
|
||||
<>
|
||||
<M>
|
||||
Your sip realm is <span>{userData?.account?.sip_realm}</span>
|
||||
</M>
|
||||
<M>
|
||||
You can add sip credentials below to allow sip devices to
|
||||
register to this realm and make calls.
|
||||
</M>
|
||||
</>
|
||||
) : (
|
||||
<M>
|
||||
You need to associate a sip realm to this account in order to
|
||||
add sip credentials.
|
||||
</M>
|
||||
)
|
||||
) : selectedAccount ? (
|
||||
selectedAccount?.sip_realm ? (
|
||||
<>
|
||||
<M>
|
||||
Your sip realm is <span>{selectedAccount.sip_realm}</span>
|
||||
</M>
|
||||
<M>
|
||||
You can add sip credentials below to allow sip devices to
|
||||
register to this realm and make calls.
|
||||
</M>
|
||||
</>
|
||||
) : (
|
||||
<M>
|
||||
You need to associate a sip realm to this account in order to
|
||||
add sip credentials.
|
||||
</M>
|
||||
)
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link to={`${ROUTE_INTERNAL_CLIENTS}/add`} title="Add sip client">
|
||||
{" "}
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
@@ -156,13 +206,13 @@ export const Clients = () => {
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<M>No Clients.</M>
|
||||
<M>No sip clients.</M>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
<Section clean>
|
||||
<Button small as={Link} to={`${ROUTE_INTERNAL_CLIENTS}/add`}>
|
||||
Add client
|
||||
Add sip client
|
||||
</Button>
|
||||
</Section>
|
||||
{client && (
|
||||
|
||||
@@ -3,8 +3,8 @@ import React from "react";
|
||||
import WaveSurfer from "wavesurfer.js";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Icon, P } from "@jambonz/ui-kit";
|
||||
import { Icons, ModalClose } from "src/components";
|
||||
import { getBlob, getJaegerTrace } from "src/api";
|
||||
import { Icons, Modal, ModalClose } from "src/components";
|
||||
import { deleteRecord, getBlob, getJaegerTrace } from "src/api";
|
||||
import { DownloadedBlob, RecentCall } from "src/api/types";
|
||||
import RegionsPlugin, { Region } from "wavesurfer.js/src/plugin/regions";
|
||||
import TimelinePlugin from "wavesurfer.js/src/plugin/timeline";
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
getSpansByNameRegex,
|
||||
getSpansFromJaegerRoot,
|
||||
} from "./utils";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
|
||||
type PlayerProps = {
|
||||
call: RecentCall;
|
||||
@@ -49,6 +50,8 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
|
||||
const [record, setRecord] = useState<DownloadedBlob | null>(null);
|
||||
|
||||
const [deleteRecordUrl, setDeleteRecordUrl] = useState("");
|
||||
|
||||
const drawDtmfRegionForSpan = (s: JaegerSpan, startPoint: JaegerSpan) => {
|
||||
if (waveSufferRef.current) {
|
||||
const r = waveSufferRef.current.regions.list[s.spanId];
|
||||
@@ -201,6 +204,19 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRecordSubmit = () => {
|
||||
if (deleteRecordUrl) {
|
||||
deleteRecord(deleteRecordUrl)
|
||||
.then(() => {
|
||||
setDeleteRecordUrl("");
|
||||
toastSuccess("Successfully deleted record");
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
buildWavesufferRegion();
|
||||
}, [jaegerRoot, isReady]);
|
||||
@@ -314,53 +330,71 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
<div className="media-container__center">
|
||||
<strong>{playBackTime}</strong>
|
||||
</div>
|
||||
<div className="media-container__center">
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPlaybackJump(-JUMP_DURATION);
|
||||
}}
|
||||
title="Jump left"
|
||||
disabled={!isReady}
|
||||
>
|
||||
<Icon>
|
||||
<Icons.ChevronsLeft />
|
||||
</Icon>
|
||||
</button>
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
onClick={togglePlayback}
|
||||
title="play/pause"
|
||||
disabled={!isReady}
|
||||
>
|
||||
<Icon>{isPlaying ? <Icons.Pause /> : <Icons.Play />}</Icon>
|
||||
</button>
|
||||
<div className="controll-btn-container">
|
||||
<div className="controll-btn-container__placeholder"></div>
|
||||
<div className="controll-btn-container__center">
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPlaybackJump(-JUMP_DURATION);
|
||||
}}
|
||||
title="Jump left"
|
||||
disabled={!isReady}
|
||||
>
|
||||
<Icon>
|
||||
<Icons.ChevronsLeft />
|
||||
</Icon>
|
||||
</button>
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
onClick={togglePlayback}
|
||||
title="play/pause"
|
||||
disabled={!isReady}
|
||||
>
|
||||
<Icon>{isPlaying ? <Icons.Pause /> : <Icons.Play />}</Icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPlaybackJump(JUMP_DURATION);
|
||||
}}
|
||||
title="Jump right"
|
||||
disabled={!isReady}
|
||||
>
|
||||
<Icon>
|
||||
<Icons.ChevronsRight />
|
||||
</Icon>
|
||||
</button>
|
||||
<a
|
||||
href={record.data_url}
|
||||
download={record.file_name}
|
||||
className="btnty"
|
||||
title="Download record file"
|
||||
>
|
||||
<Icon>
|
||||
<Icons.Download />
|
||||
</Icon>
|
||||
</a>
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPlaybackJump(JUMP_DURATION);
|
||||
}}
|
||||
title="Jump right"
|
||||
disabled={!isReady}
|
||||
>
|
||||
<Icon>
|
||||
<Icons.ChevronsRight />
|
||||
</Icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="controll-btn-container__right">
|
||||
<a
|
||||
href={record.data_url}
|
||||
download={record.file_name}
|
||||
className="btnty"
|
||||
title="Download record file"
|
||||
>
|
||||
<Icon>
|
||||
<Icons.Download />
|
||||
</Icon>
|
||||
</a>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setDeleteRecordUrl(url || "");
|
||||
}}
|
||||
title="Delete record file"
|
||||
>
|
||||
<Icon>
|
||||
<Icons.Trash2 />
|
||||
</Icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<label htmlFor="is_active" className="chk">
|
||||
<input
|
||||
@@ -464,6 +498,17 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
</div>
|
||||
</ModalClose>
|
||||
)}
|
||||
{deleteRecordUrl && (
|
||||
<Modal
|
||||
handleCancel={() => setDeleteRecordUrl("")}
|
||||
handleSubmit={handleDeleteRecordSubmit}
|
||||
>
|
||||
<P>
|
||||
Are you sure you want to delete the record for call{" "}
|
||||
<strong>{call_sid}</strong>?
|
||||
</P>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,6 +19,57 @@
|
||||
}
|
||||
}
|
||||
|
||||
.controll-btn-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
padding: 13px;
|
||||
|
||||
&__center {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
button:not(:last-child) {
|
||||
margin-right: ui-vars.$px01;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.ico {
|
||||
color: ui-vars.$white;
|
||||
@include mixins.icosize();
|
||||
}
|
||||
}
|
||||
|
||||
&__right {
|
||||
a:not(:last-child) {
|
||||
margin-right: ui-vars.$px01;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.ico {
|
||||
color: ui-vars.$white;
|
||||
@include mixins.icosize();
|
||||
}
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.media-container {
|
||||
border: 1px solid black;
|
||||
border-radius: ui-vars.$px01;
|
||||
@@ -31,18 +82,5 @@
|
||||
justify-content: center;
|
||||
grid-gap: ui-vars.$px01;
|
||||
margin-top: ui-vars.$px01;
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ico {
|
||||
color: ui-vars.$white;
|
||||
@include mixins.icosize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,11 @@ export const DeleteSpeechService = ({
|
||||
return (
|
||||
<Modal handleCancel={handleCancel} handleSubmit={handleSubmit}>
|
||||
<P>
|
||||
Are you sure you want to delete the <strong>{credential.vendor}</strong>{" "}
|
||||
Are you sure you want to delete the{" "}
|
||||
<strong>
|
||||
{credential.vendor}
|
||||
{credential.label ? ` (${credential.label})` : ""}
|
||||
</strong>{" "}
|
||||
speech service?
|
||||
</P>
|
||||
</Modal>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button, ButtonGroup, MS } from "@jambonz/ui-kit";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import { ROUTE_INTERNAL_SPEECH } from "src/router/routes";
|
||||
import { Section } from "src/components";
|
||||
import { Section, Tooltip } from "src/components";
|
||||
import {
|
||||
FileUpload,
|
||||
Selector,
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
checkSelectOptions,
|
||||
getObscuredSecret,
|
||||
isUserAccountScope,
|
||||
isNotBlank,
|
||||
} from "src/utils";
|
||||
import { getObscuredGoogleServiceKey } from "./utils";
|
||||
import { CredentialStatus } from "./status";
|
||||
@@ -76,10 +77,18 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
const [ttsRegion, setTtsRegion] = useState("");
|
||||
const [ttsApiKey, setTtsApiKey] = useState("");
|
||||
const [instanceId, setInstanceId] = useState("");
|
||||
const [initialCheckCustomTts, setInitialCheckCustomTts] = useState(false);
|
||||
const [initialCheckCustomStt, setInitialCheckCustomStt] = useState(false);
|
||||
const [initialCheckOnpremAzureService, setInitialCheckOnpremAzureService] =
|
||||
useState(false);
|
||||
const [useCustomTts, setUseCustomTts] = useState(false);
|
||||
const [useCustomStt, setUseCustomStt] = useState(false);
|
||||
const [customTtsEndpointUrl, setCustomTtsEndpointUrl] = useState("");
|
||||
const [tmpCustomTtsEndpointUrl, setTmpCustomTtsEndpointUrl] = useState("");
|
||||
const [customTtsEndpoint, setCustomTtsEndpoint] = useState("");
|
||||
const [tmpCustomTtsEndpoint, setTmpCustomTtsEndpoint] = useState("");
|
||||
const [customSttEndpointUrl, setCustomSttEndpointUrl] = useState("");
|
||||
const [tmpCustomSttEndpointUrl, setTmpCustomSttEndpointUrl] = useState("");
|
||||
const [customSttEndpoint, setCustomSttEndpoint] = useState("");
|
||||
const [tmpCustomSttEndpoint, setTmpCustomSttEndpoint] = useState("");
|
||||
const [rivaServerUri, setRivaServerUri] = useState("");
|
||||
@@ -99,6 +108,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
const [onPremNuanceSttCheck, setOnPremNuanceSttCheck] = useState(false);
|
||||
const [tmpOnPremNuanceSttUrl, setTmpOnPremNuanceSttUrl] = useState("");
|
||||
const [onPremNuanceSttUrl, setOnPremNuanceSttUrl] = useState("");
|
||||
const [label, setLabel] = useState("");
|
||||
|
||||
const handleFile = (file: File) => {
|
||||
const handleError = () => {
|
||||
@@ -143,14 +153,19 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
service_provider_sid: currentServiceProvider.service_provider_sid,
|
||||
use_for_tts: ttsCheck ? 1 : 0,
|
||||
use_for_stt: sttCheck ? 1 : 0,
|
||||
label: label || null,
|
||||
...(vendor === VENDOR_AWS && {
|
||||
aws_region: region || null,
|
||||
}),
|
||||
...(vendor === VENDOR_MICROSOFT && {
|
||||
region: region || null,
|
||||
use_custom_tts: useCustomTts ? 1 : 0,
|
||||
use_custom_tts:
|
||||
useCustomTts || isNotBlank(customTtsEndpointUrl) ? 1 : 0,
|
||||
custom_tts_endpoint_url: customTtsEndpointUrl || null,
|
||||
custom_tts_endpoint: customTtsEndpoint || null,
|
||||
use_custom_stt: useCustomStt ? 1 : 0,
|
||||
use_custom_stt:
|
||||
useCustomStt || isNotBlank(customSttEndpointUrl) ? 1 : 0,
|
||||
custom_stt_endpoint_url: customSttEndpointUrl || null,
|
||||
custom_stt_endpoint: customSttEndpoint || null,
|
||||
}),
|
||||
...(vendor === VENDOR_IBM && {
|
||||
@@ -206,13 +221,15 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
vendor === VENDOR_GOOGLE ? JSON.stringify(googleServiceKey) : null,
|
||||
access_key_id: vendor === VENDOR_AWS ? accessKeyId : null,
|
||||
secret_access_key: vendor === VENDOR_AWS ? secretAccessKey : null,
|
||||
api_key:
|
||||
vendor === VENDOR_MICROSOFT ||
|
||||
vendor === VENDOR_WELLSAID ||
|
||||
vendor === VENDOR_DEEPGRAM ||
|
||||
vendor === VENDOR_SONIOX
|
||||
? apiKey
|
||||
: null,
|
||||
...(apiKey && {
|
||||
api_key:
|
||||
vendor === VENDOR_MICROSOFT ||
|
||||
vendor === VENDOR_WELLSAID ||
|
||||
vendor === VENDOR_DEEPGRAM ||
|
||||
vendor === VENDOR_SONIOX
|
||||
? apiKey
|
||||
: null,
|
||||
}),
|
||||
riva_server_uri: vendor == VENDOR_NVIDIA ? rivaServerUri : null,
|
||||
})
|
||||
.then(() => {
|
||||
@@ -330,13 +347,26 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
if (credential.data.riva_server_uri) {
|
||||
setRivaServerUri(credential.data.riva_server_uri);
|
||||
}
|
||||
|
||||
setUseCustomTts(credential.data.use_custom_tts > 0 ? true : false);
|
||||
setUseCustomStt(credential.data.use_custom_stt > 0 ? true : false);
|
||||
|
||||
setCustomTtsEndpointUrl(credential.data.custom_tts_endpoint_url || "");
|
||||
setCustomSttEndpointUrl(credential.data.custom_stt_endpoint_url || "");
|
||||
setTmpCustomTtsEndpointUrl(credential.data.custom_tts_endpoint_url || "");
|
||||
setTmpCustomSttEndpointUrl(credential.data.custom_stt_endpoint_url || "");
|
||||
|
||||
setCustomTtsEndpoint(credential.data.custom_tts_endpoint || "");
|
||||
setCustomSttEndpoint(credential.data.custom_stt_endpoint || "");
|
||||
setTmpCustomTtsEndpoint(credential.data.custom_tts_endpoint || "");
|
||||
setTmpCustomSttEndpoint(credential.data.custom_stt_endpoint || "");
|
||||
|
||||
setInitialCheckCustomTts(isNotBlank(credential.data.custom_tts_endpoint));
|
||||
setInitialCheckCustomStt(isNotBlank(credential.data.custom_stt_endpoint));
|
||||
setInitialCheckOnpremAzureService(
|
||||
isNotBlank(credential.data.custom_tts_endpoint_url) ||
|
||||
isNotBlank(credential.data.custom_stt_endpoint_url)
|
||||
);
|
||||
|
||||
setCustomVendorName(
|
||||
credential.data.vendor.startsWith(VENDOR_CUSTOM)
|
||||
? credential.data.vendor.substring(VENDOR_CUSTOM.length + 1)
|
||||
@@ -347,6 +377,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
setTmpCustomVendorSttUrl(credential.data.custom_stt_url || "");
|
||||
setCustomVendorTtsUrl(credential.data.custom_tts_url || "");
|
||||
setTmpCustomVendorTtsUrl(credential.data.custom_tts_url || "");
|
||||
if (credential.data.label) {
|
||||
setLabel(credential.data.label);
|
||||
}
|
||||
}
|
||||
}, [credential]);
|
||||
|
||||
@@ -416,6 +449,22 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
disabled={credential ? true : false}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label htmlFor="speech_label">
|
||||
Label
|
||||
<Tooltip text="Assign a label only if you need to create multiple speech services from the same vendor. Then use the label in your application to specify which service to use.">
|
||||
{" "}
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
id="speech_label"
|
||||
type="text"
|
||||
name="speech_label"
|
||||
value={label}
|
||||
disabled={credential ? true : false}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
/>
|
||||
</fieldset>
|
||||
{vendor && (
|
||||
<fieldset>
|
||||
{vendor !== VENDOR_DEEPGRAM &&
|
||||
@@ -695,8 +744,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
{(vendor === VENDOR_MICROSOFT ||
|
||||
vendor === VENDOR_WELLSAID ||
|
||||
{(vendor === VENDOR_WELLSAID ||
|
||||
vendor === VENDOR_DEEPGRAM ||
|
||||
vendor === VENDOR_SONIOX) && (
|
||||
<fieldset>
|
||||
@@ -716,7 +764,8 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
)}
|
||||
{regions &&
|
||||
regions[vendor as keyof RegionVendors] &&
|
||||
vendor !== VENDOR_IBM && (
|
||||
vendor !== VENDOR_IBM &&
|
||||
vendor !== VENDOR_MICROSOFT && (
|
||||
<fieldset>
|
||||
<label htmlFor="region">
|
||||
Region<span>*</span>
|
||||
@@ -813,76 +862,184 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
{vendor === VENDOR_MICROSOFT && (
|
||||
<React.Fragment>
|
||||
<fieldset>
|
||||
<label htmlFor="use_custom_tts" className="chk">
|
||||
<input
|
||||
id="use_custom_tts"
|
||||
name="use_custom_tts"
|
||||
type="checkbox"
|
||||
onChange={(e) => {
|
||||
setUseCustomTts(e.target.checked);
|
||||
|
||||
if (e.target.checked && tmpCustomTtsEndpoint) {
|
||||
setCustomTtsEndpoint(tmpCustomTtsEndpoint);
|
||||
}
|
||||
|
||||
if (!e.target.checked) {
|
||||
setTmpCustomTtsEndpoint(customTtsEndpoint);
|
||||
setCustomTtsEndpoint("");
|
||||
}
|
||||
}}
|
||||
checked={useCustomTts}
|
||||
<Checkzone
|
||||
hidden
|
||||
name="use_hosted_azure_service"
|
||||
label="Use hosted Azure service"
|
||||
initialCheck={!initialCheckOnpremAzureService}
|
||||
handleChecked={(e) => {
|
||||
setInitialCheckOnpremAzureService(!e.target.checked);
|
||||
}}
|
||||
>
|
||||
{regions && (
|
||||
<>
|
||||
<label htmlFor="region">
|
||||
Region<span>*</span>
|
||||
</label>
|
||||
<Selector
|
||||
id="region"
|
||||
name="region"
|
||||
value={region}
|
||||
required
|
||||
options={[
|
||||
{
|
||||
name: "Select a region",
|
||||
value: "",
|
||||
},
|
||||
].concat(regions[vendor as keyof RegionVendors])}
|
||||
onChange={(e) => setRegion(e.target.value)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<label htmlFor={`${vendor}_apikey`}>
|
||||
API key<span>*</span>
|
||||
</label>
|
||||
<Passwd
|
||||
id={`${vendor}_apikey`}
|
||||
required
|
||||
name={`${vendor}_apikey`}
|
||||
placeholder="API key"
|
||||
value={apiKey ? getObscuredSecret(apiKey) : apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
disabled={credential ? true : false}
|
||||
/>
|
||||
<div>Use for custom voice</div>
|
||||
</label>
|
||||
<label htmlFor="use_custom_tts">
|
||||
Custom voice endpoint{useCustomTts && <span>*</span>}
|
||||
</label>
|
||||
<input
|
||||
id="custom_tts_endpoint"
|
||||
required={useCustomTts}
|
||||
disabled={!useCustomTts}
|
||||
type="text"
|
||||
name="custom_tts_endpoint"
|
||||
placeholder="Custom voice endpoint"
|
||||
value={customTtsEndpoint}
|
||||
onChange={(e) => setCustomTtsEndpoint(e.target.value)}
|
||||
/>
|
||||
</Checkzone>
|
||||
|
||||
<Checkzone
|
||||
hidden
|
||||
name="use_azure_docker_container_on_prem"
|
||||
label="Use Azure Docker container (on-prem)"
|
||||
initialCheck={initialCheckOnpremAzureService}
|
||||
handleChecked={(e) => {
|
||||
setInitialCheckOnpremAzureService(e.target.checked);
|
||||
|
||||
if (e.target.checked && tmpCustomTtsEndpointUrl) {
|
||||
setCustomTtsEndpointUrl(tmpCustomTtsEndpointUrl);
|
||||
}
|
||||
|
||||
if (!e.target.checked) {
|
||||
setTmpCustomTtsEndpointUrl(customTtsEndpointUrl);
|
||||
setCustomTtsEndpointUrl("");
|
||||
}
|
||||
|
||||
if (e.target.checked && tmpCustomSttEndpointUrl) {
|
||||
setCustomSttEndpointUrl(tmpCustomSttEndpointUrl);
|
||||
}
|
||||
|
||||
if (!e.target.checked) {
|
||||
setTmpCustomSttEndpointUrl(customSttEndpointUrl);
|
||||
setCustomSttEndpointUrl("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<label htmlFor="container_url_for_tts">
|
||||
Container URL for TTS<span>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="container_url_for_tts"
|
||||
required
|
||||
type="text"
|
||||
name="container_url_for_tts"
|
||||
placeholder="Container URL for TTS"
|
||||
value={customTtsEndpointUrl}
|
||||
onChange={(e) => setCustomTtsEndpointUrl(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="container_url_for_stt">
|
||||
Container URL for STT<span>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="container_url_for_stt"
|
||||
required
|
||||
type="text"
|
||||
name="container_url_for_stt"
|
||||
placeholder="Container URL for STT"
|
||||
value={customSttEndpointUrl}
|
||||
onChange={(e) => setCustomSttEndpointUrl(e.target.value)}
|
||||
/>
|
||||
<label htmlFor={`${vendor}_apikey`}>
|
||||
Subscription key (if required)
|
||||
</label>
|
||||
<Passwd
|
||||
id={`${vendor}_apikey`}
|
||||
name={`${vendor}_apikey`}
|
||||
placeholder="API key"
|
||||
value={apiKey ? getObscuredSecret(apiKey) : apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
disabled={credential ? true : false}
|
||||
/>
|
||||
</Checkzone>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label htmlFor="use_custom_stt" className="chk">
|
||||
<Checkzone
|
||||
hidden
|
||||
name="use_custom_tts_endpoint_id"
|
||||
label="I want to use a custom voice for TTS"
|
||||
initialCheck={initialCheckCustomTts}
|
||||
handleChecked={(e) => {
|
||||
setUseCustomTts(e.target.checked);
|
||||
|
||||
if (e.target.checked && tmpCustomTtsEndpoint) {
|
||||
setCustomTtsEndpoint(tmpCustomTtsEndpoint);
|
||||
}
|
||||
|
||||
if (!e.target.checked) {
|
||||
setTmpCustomTtsEndpoint(customTtsEndpoint);
|
||||
setCustomTtsEndpoint("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<label htmlFor="use_custom_tts_id">
|
||||
Custom voice deployment ID<span>*</span>
|
||||
<Tooltip text="This is the value shown as the deploymentId parameter in the custom URL generated when you deploy a custom voice">
|
||||
{" "}
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
id="use_custom_stt"
|
||||
name="use_custom_stt"
|
||||
type="checkbox"
|
||||
onChange={(e) => {
|
||||
setUseCustomStt(e.target.checked);
|
||||
|
||||
if (e.target.checked && tmpCustomSttEndpoint) {
|
||||
setCustomSttEndpoint(tmpCustomSttEndpoint);
|
||||
}
|
||||
|
||||
if (!e.target.checked) {
|
||||
setTmpCustomSttEndpoint(customSttEndpoint);
|
||||
setCustomSttEndpoint("");
|
||||
}
|
||||
}}
|
||||
checked={useCustomStt}
|
||||
id="custom_tts_endpoint_id"
|
||||
required
|
||||
disabled={initialCheckOnpremAzureService}
|
||||
type="text"
|
||||
name="custom_tts_endpoint_id"
|
||||
placeholder="Custom voice endpoint id"
|
||||
value={customTtsEndpoint}
|
||||
onChange={(e) => setCustomTtsEndpoint(e.target.value)}
|
||||
/>
|
||||
<div>Use for custom speech model</div>
|
||||
</label>
|
||||
<label htmlFor="use_custom_stt">
|
||||
Custom speech endpoint id{useCustomStt && <span>*</span>}
|
||||
</label>
|
||||
<input
|
||||
id="custom_stt_endpoint"
|
||||
required={useCustomStt}
|
||||
disabled={!useCustomStt}
|
||||
type="text"
|
||||
name="custom_stt_endpoint"
|
||||
placeholder="Custom speech endpoint ID"
|
||||
value={customSttEndpoint}
|
||||
onChange={(e) => setCustomSttEndpoint(e.target.value)}
|
||||
/>
|
||||
</Checkzone>
|
||||
<Checkzone
|
||||
hidden
|
||||
name="use_custom_stt_endpoint_id"
|
||||
label="I want to use a custom speech model for STT"
|
||||
initialCheck={initialCheckCustomStt}
|
||||
handleChecked={(e) => {
|
||||
setUseCustomStt(e.target.checked);
|
||||
|
||||
if (e.target.checked && tmpCustomSttEndpoint) {
|
||||
setCustomSttEndpoint(tmpCustomSttEndpoint);
|
||||
}
|
||||
|
||||
if (!e.target.checked) {
|
||||
setTmpCustomSttEndpoint(customSttEndpoint);
|
||||
setCustomSttEndpoint("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<label htmlFor="use_custom_stt_id">
|
||||
Custom speech endpoint ID<span>*</span>
|
||||
<Tooltip text="This is the value shown as the Endpoint ID when you deploy a custom speech model">
|
||||
{" "}
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
id="custom_stt_endpoint_id"
|
||||
required={useCustomStt}
|
||||
disabled={initialCheckOnpremAzureService}
|
||||
type="text"
|
||||
name="custom_stt_endpoint_id"
|
||||
placeholder="Custom speech endpoint ID"
|
||||
value={customSttEndpoint}
|
||||
onChange={(e) => setCustomSttEndpoint(e.target.value)}
|
||||
/>
|
||||
</Checkzone>
|
||||
</fieldset>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
@@ -76,7 +76,11 @@ export const SpeechServices = () => {
|
||||
refetch();
|
||||
toastSuccess(
|
||||
<>
|
||||
Deleted speech service <strong>{credential.vendor}</strong>
|
||||
Deleted speech service{" "}
|
||||
<strong>
|
||||
{credential.vendor}
|
||||
{credential.label ? ` (${credential.label})` : ""}
|
||||
</strong>{" "}
|
||||
</>
|
||||
);
|
||||
})
|
||||
@@ -195,6 +199,14 @@ export const SpeechServices = () => {
|
||||
<div>
|
||||
<CredentialStatus cred={credential} />
|
||||
</div>
|
||||
{credential.label && (
|
||||
<div>
|
||||
<div className="i txt--teal">
|
||||
<Icons.Tag />
|
||||
<span>{credential.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ScopedAccess
|
||||
|
||||
@@ -1,41 +1,58 @@
|
||||
import { Button, H1, MS } from "@jambonz/ui-kit";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { getAvailability, postSipRealms } from "src/api";
|
||||
import DomainInput from "src/components/domain-input";
|
||||
import { Message } from "src/components/forms";
|
||||
import { getToken, parseJwt } from "src/router/auth";
|
||||
import { ROUTE_INTERNAL_ACCOUNTS } from "src/router/routes";
|
||||
import { getRootDomain } from "src/store/localStore";
|
||||
import { UserData } from "src/store/types";
|
||||
import { hasValue } from "src/utils";
|
||||
|
||||
export const RegisterChooseSubdomain = () => {
|
||||
const [name, setName] = useState("");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [isValidDomain, setIsValidDomain] = useState(false);
|
||||
const rootDomain = getRootDomain();
|
||||
const userData: UserData = parseJwt(getToken());
|
||||
const navigate = useNavigate();
|
||||
const typingTimeoutRef = useRef<number | null>(null);
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrorMessage("");
|
||||
|
||||
getAvailability(`${name}.${rootDomain}`)
|
||||
.then(({ json }) => {
|
||||
if (!json.available) {
|
||||
setErrorMessage("That subdomain is not available.");
|
||||
return;
|
||||
}
|
||||
postSipRealms(userData.account_sid || "", `${name}.${rootDomain}`)
|
||||
.then(() => {
|
||||
navigate(`${ROUTE_INTERNAL_ACCOUNTS}/${userData.account_sid}/edit`);
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.msg);
|
||||
});
|
||||
postSipRealms(userData.account_sid || "", `${name}.${rootDomain}`)
|
||||
.then(() => {
|
||||
navigate(`${ROUTE_INTERNAL_ACCOUNTS}/${userData.account_sid}/edit`);
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.msg);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typingTimeoutRef.current) {
|
||||
clearTimeout(typingTimeoutRef.current);
|
||||
}
|
||||
if (!name || name.length < 3) {
|
||||
setIsValidDomain(false);
|
||||
return;
|
||||
}
|
||||
setIsValidDomain(false);
|
||||
typingTimeoutRef.current = setTimeout(() => {
|
||||
getAvailability(`${name}.${rootDomain}`)
|
||||
.then(({ json }) =>
|
||||
setIsValidDomain(
|
||||
Boolean(json.available) && hasValue(name) && name.length != 0
|
||||
)
|
||||
)
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.msg);
|
||||
setIsValidDomain(false);
|
||||
});
|
||||
}, 500);
|
||||
}, [name]);
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Choose a subdomain</H1>
|
||||
@@ -46,15 +63,18 @@ export const RegisterChooseSubdomain = () => {
|
||||
This will be the FQDN where your carrier will send calls, and where
|
||||
you can register devices to. This can be changed at any time.
|
||||
</MS>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder="Your Name"
|
||||
<DomainInput
|
||||
id="subdomain"
|
||||
name="subdomain"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
setValue={setName}
|
||||
placeholder="Your name here"
|
||||
root_domain={rootDomain ? `.${rootDomain}` : ""}
|
||||
is_valid={isValidDomain}
|
||||
/>
|
||||
<Button type="submit">Complete Registration →</Button>
|
||||
<Button type="submit" disabled={!isValidDomain}>
|
||||
Complete Registration →
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -41,6 +41,10 @@ export const hasLength = <Type>(
|
||||
return hasValue(variable) && variable.length > minlength;
|
||||
};
|
||||
|
||||
export const isNotBlank = (variable: string | null | undefined) => {
|
||||
return hasValue(variable) && variable.length > 0;
|
||||
};
|
||||
|
||||
export const isObject = (obj: unknown) => {
|
||||
/** null | undefined | Array will be "object" so exclude them */
|
||||
return typeof obj === "object" && hasValue(obj) && !Array.isArray(obj);
|
||||
|
||||
5
src/vendor/types.ts
vendored
5
src/vendor/types.ts
vendored
@@ -15,6 +15,11 @@ export interface VendorOptions {
|
||||
value: Lowercase<Vendor>;
|
||||
}
|
||||
|
||||
export interface LabelOptions {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Region {
|
||||
name: string;
|
||||
value: string;
|
||||
|
||||
Reference in New Issue
Block a user