mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-01-25 02:08:19 +00:00
Compare commits
20 Commits
fix/subdom
...
fix/filter
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8adb8dbe0 | ||
|
|
3a19ff6840 | ||
|
|
729cefb06c | ||
|
|
26e3856603 | ||
|
|
f5302583b5 | ||
|
|
b5c27bb096 | ||
|
|
4a2c36ebba | ||
|
|
62234f9f64 | ||
|
|
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>
|
||||
|
||||
45
package-lock.json
generated
45
package-lock.json
generated
@@ -21,7 +21,7 @@
|
||||
"react-dom": "^18.0.0",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"wavesurfer.js": "^6.6.3"
|
||||
"wavesurfer.js": "^7.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.12",
|
||||
@@ -30,7 +30,6 @@
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@types/wavesurfer.js": "^6.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
||||
"@typescript-eslint/parser": "^5.30.6",
|
||||
"@vitejs/plugin-react": "^1.3.0",
|
||||
@@ -900,12 +899,6 @@
|
||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/debounce": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz",
|
||||
"integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "4.17.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
|
||||
@@ -1014,15 +1007,6 @@
|
||||
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/wavesurfer.js": {
|
||||
"version": "6.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/wavesurfer.js/-/wavesurfer.js-6.0.6.tgz",
|
||||
"integrity": "sha512-fD54o0RXZXxkOb+69Rt6rGViaHpIc1Mmde2aOX9qPhlQhrCPepybGnsekiG407+7scPlaK+hmuPez5AnnmlzGg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/debounce": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
|
||||
@@ -6867,9 +6851,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wavesurfer.js": {
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-6.6.3.tgz",
|
||||
"integrity": "sha512-XqUOXe8+SOTe8uKCHaqW0vJ5etCCQvq/NgaPycn9HAX/nUi+2zoWD+w9i7H5vBT9UCDNawOia+vS5Ct3kZGQzA=="
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.3.4.tgz",
|
||||
"integrity": "sha512-x2Ue4Dh+4RoaWay3LOLHhXgkdxPAIoC/BcbXh0yk8WNhQH2NboPoa52XXoCo4jEfjSe1bc7nxuM5vBIxUMZyBA=="
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
@@ -7678,12 +7662,6 @@
|
||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/debounce": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz",
|
||||
"integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/express": {
|
||||
"version": "4.17.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
|
||||
@@ -7787,15 +7765,6 @@
|
||||
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/wavesurfer.js": {
|
||||
"version": "6.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/wavesurfer.js/-/wavesurfer.js-6.0.6.tgz",
|
||||
"integrity": "sha512-fD54o0RXZXxkOb+69Rt6rGViaHpIc1Mmde2aOX9qPhlQhrCPepybGnsekiG407+7scPlaK+hmuPez5AnnmlzGg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/debounce": "*"
|
||||
}
|
||||
},
|
||||
"@types/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
|
||||
@@ -11657,9 +11626,9 @@
|
||||
}
|
||||
},
|
||||
"wavesurfer.js": {
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-6.6.3.tgz",
|
||||
"integrity": "sha512-XqUOXe8+SOTe8uKCHaqW0vJ5etCCQvq/NgaPycn9HAX/nUi+2zoWD+w9i7H5vBT9UCDNawOia+vS5Ct3kZGQzA=="
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.3.4.tgz",
|
||||
"integrity": "sha512-x2Ue4Dh+4RoaWay3LOLHhXgkdxPAIoC/BcbXh0yk8WNhQH2NboPoa52XXoCo4jEfjSe1bc7nxuM5vBIxUMZyBA=="
|
||||
},
|
||||
"which": {
|
||||
"version": "2.0.2",
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/ui-kit": "^0.0.21",
|
||||
"@stripe/react-stripe-js": "^2.1.1",
|
||||
"@stripe/stripe-js": "^1.54.1",
|
||||
"dayjs": "^1.11.5",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"react": "^18.0.0",
|
||||
@@ -50,9 +52,7 @@
|
||||
"react-dom": "^18.0.0",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"wavesurfer.js": "^6.6.3",
|
||||
"@stripe/react-stripe-js": "^2.1.1",
|
||||
"@stripe/stripe-js": "^1.54.1"
|
||||
"wavesurfer.js": "^7.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.12",
|
||||
@@ -61,7 +61,6 @@
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@types/wavesurfer.js": "^6.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
||||
"@typescript-eslint/parser": "^5.30.6",
|
||||
"@vitejs/plugin-react": "^1.3.0",
|
||||
|
||||
@@ -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,
|
||||
@@ -187,6 +197,14 @@ export const AUDIO_FORMAT_OPTIONS = [
|
||||
value: "wav",
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_ELEVENLABS_MODEL = "eleven_multilingual_v2";
|
||||
|
||||
export const ELEVENLABS_MODEL_OPTIONS = [
|
||||
{ name: "Multilingual v2", value: "eleven_multilingual_v2" },
|
||||
{ name: "Multilingual v1", value: "eleven_multilingual_v1" },
|
||||
{ name: "English v1", value: "eleven_monolingual_v1" },
|
||||
];
|
||||
/** Password Length options */
|
||||
|
||||
export const PASSWORD_MIN = 8;
|
||||
|
||||
@@ -90,6 +90,10 @@ import type {
|
||||
DeleteAccount,
|
||||
ChangePassword,
|
||||
SignIn,
|
||||
GetVoices,
|
||||
LanguageOption,
|
||||
VoiceOption,
|
||||
GetLanguages,
|
||||
} from "./types";
|
||||
import { Availability, StatusCodes } from "./types";
|
||||
import { JaegerRoot } from "./jaeger-types";
|
||||
@@ -329,6 +333,32 @@ export const postSpeechService = (
|
||||
return postFetch<SidResponse, Partial<SpeechCredential>>(apiUrl, payload);
|
||||
};
|
||||
|
||||
export const postSpeechServiceVoices = (
|
||||
sid: string,
|
||||
payload: Partial<GetVoices>
|
||||
) => {
|
||||
const userData = parseJwt(getToken());
|
||||
const apiUrl =
|
||||
userData.scope === USER_ACCOUNT
|
||||
? `${API_ACCOUNTS}/${userData.account_sid}/SpeechCredentials/voices`
|
||||
: `${API_SERVICE_PROVIDERS}/${sid}/SpeechCredentials/voices`;
|
||||
|
||||
return postFetch<VoiceOption[], Partial<GetVoices>>(apiUrl, payload);
|
||||
};
|
||||
|
||||
export const postSpeechServiceLanguages = (
|
||||
sid: string,
|
||||
payload: Partial<GetLanguages>
|
||||
) => {
|
||||
const userData = parseJwt(getToken());
|
||||
const apiUrl =
|
||||
userData.scope === USER_ACCOUNT
|
||||
? `${API_ACCOUNTS}/${userData.account_sid}/SpeechCredentials/languages`
|
||||
: `${API_SERVICE_PROVIDERS}/${sid}/SpeechCredentials/languages`;
|
||||
|
||||
return postFetch<LanguageOption[], Partial<GetLanguages>>(apiUrl, payload);
|
||||
};
|
||||
|
||||
export const postMsTeamsTentant = (payload: Partial<MSTeamsTenant>) => {
|
||||
return postFetch<SidResponse, Partial<MSTeamsTenant>>(
|
||||
API_MS_TEAMS_TENANTS,
|
||||
@@ -685,6 +715,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) => {
|
||||
|
||||
@@ -37,14 +37,14 @@ export interface JaegerAttribute {
|
||||
value: JaegerValue;
|
||||
}
|
||||
|
||||
export interface WaveSufferSttResult {
|
||||
export interface WaveSurferSttResult {
|
||||
vendor: string;
|
||||
transcript: string;
|
||||
confidence: number;
|
||||
language_code: string;
|
||||
}
|
||||
|
||||
export interface WaveSufferDtmfResult {
|
||||
export interface WaveSurferDtmfResult {
|
||||
dtmf: string;
|
||||
duration: 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,9 @@ export interface SpeechCredential {
|
||||
auth_token: null | string;
|
||||
custom_stt_url: null | string;
|
||||
custom_tts_url: null | string;
|
||||
label: null | string;
|
||||
cobalt_server_uri: null | string;
|
||||
model_id: null | string;
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
@@ -444,7 +461,6 @@ export interface PredefinedCarrier extends Carrier {
|
||||
export interface Gateway {
|
||||
voip_carrier_sid: string;
|
||||
ipv4: string;
|
||||
port: number;
|
||||
netmask: number;
|
||||
inbound: number;
|
||||
outbound: number;
|
||||
@@ -454,12 +470,15 @@ export interface SipGateway extends Gateway {
|
||||
sip_gateway_sid?: null | string;
|
||||
is_active: boolean;
|
||||
protocol?: string;
|
||||
port: number | null;
|
||||
pad_crypto?: boolean;
|
||||
}
|
||||
|
||||
export interface SmppGateway extends Gateway {
|
||||
smpp_gateway_sid?: null | string;
|
||||
is_primary: boolean;
|
||||
use_tls: boolean;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface Lcr {
|
||||
@@ -657,3 +676,20 @@ export interface SignIn {
|
||||
jwt?: null | string;
|
||||
account_sid?: null | string;
|
||||
}
|
||||
|
||||
export interface GetVoices {
|
||||
vendor: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface VoiceOption extends SelectorOptions {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface GetLanguages extends GetVoices {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface LanguageOption extends SelectorOptions {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -71,7 +71,7 @@ export const Accounts = () => {
|
||||
</Icon>
|
||||
</Link>
|
||||
</section>
|
||||
<section className="filters filters--spaced">
|
||||
<section className="filters filters--multi">
|
||||
<SearchFilter
|
||||
placeholder="Filter accounts"
|
||||
filter={[filter, setFilter]}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -96,7 +96,7 @@ export const Applications = () => {
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
<section className="filters filters--spaced">
|
||||
<section className="filters filters--multi">
|
||||
<SearchFilter
|
||||
placeholder="Filter applications"
|
||||
filter={[filter, setFilter]}
|
||||
|
||||
411
src/containers/internal/views/applications/speech-selection.tsx
Normal file
411
src/containers/internal/views/applications/speech-selection.tsx
Normal file
@@ -0,0 +1,411 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { postSpeechServiceLanguages, postSpeechServiceVoices } from "src/api";
|
||||
import { SpeechCredential } from "src/api/types";
|
||||
import { Selector } from "src/components/forms";
|
||||
import { SelectorOption } from "src/components/forms/selector";
|
||||
import { useSelectState } from "src/store";
|
||||
import { hasLength } from "src/utils";
|
||||
import {
|
||||
ELEVENLABS_LANG_EN,
|
||||
LANG_COBALT_EN_US,
|
||||
LANG_EN_US,
|
||||
LANG_EN_US_STANDARD_C,
|
||||
VENDOR_AWS,
|
||||
VENDOR_COBALT,
|
||||
VENDOR_CUSTOM,
|
||||
VENDOR_DEEPGRAM,
|
||||
VENDOR_ELEVENLABS,
|
||||
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
|
||||
>();
|
||||
const [synthesisVoiceOptions, setSynthesisVoiceOptions] = useState<
|
||||
SelectorOption[]
|
||||
>([]);
|
||||
const [synthesisLanguageOptions, setSynthesisLanguageOptions] = useState<
|
||||
SelectorOption[]
|
||||
>([]);
|
||||
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
|
||||
useEffect(() => {
|
||||
if (!synthesis) {
|
||||
return;
|
||||
}
|
||||
let options = synthesis[synthVendor as keyof SynthesisVendors]
|
||||
.filter((lang: VoiceLanguage) => {
|
||||
// ELEVENLABS has same voice for all lange, take voices from the 1st language
|
||||
if (synthVendor === VENDOR_ELEVENLABS) {
|
||||
return true;
|
||||
}
|
||||
return lang.code === synthLang;
|
||||
})
|
||||
.flatMap((lang: VoiceLanguage) =>
|
||||
lang.voices.map((voice: Voice) => ({
|
||||
name: voice.name,
|
||||
value: voice.value,
|
||||
}))
|
||||
) as Voice[];
|
||||
setSynthesisVoiceOptions(options);
|
||||
|
||||
options = synthesis[synthVendor as keyof SynthesisVendors].map(
|
||||
(lang: VoiceLanguage) => ({
|
||||
name: lang.name,
|
||||
value: lang.code,
|
||||
})
|
||||
);
|
||||
setSynthesisLanguageOptions(options);
|
||||
|
||||
if (synthVendor === VENDOR_ELEVENLABS) {
|
||||
postSpeechServiceVoices(
|
||||
currentServiceProvider
|
||||
? currentServiceProvider.service_provider_sid
|
||||
: "",
|
||||
{
|
||||
vendor: synthVendor,
|
||||
label: synthLabel,
|
||||
}
|
||||
).then(({ json }) => {
|
||||
if (json.length > 0) {
|
||||
setSynthesisVoiceOptions(json);
|
||||
}
|
||||
});
|
||||
|
||||
postSpeechServiceLanguages(
|
||||
currentServiceProvider
|
||||
? currentServiceProvider.service_provider_sid
|
||||
: "",
|
||||
{
|
||||
vendor: synthVendor,
|
||||
label: synthLabel,
|
||||
}
|
||||
).then(({ json }) => {
|
||||
if (json.length > 0) {
|
||||
setSynthesisLanguageOptions(json);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [synthVendor, synthesis, synthLabel]);
|
||||
|
||||
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 &&
|
||||
vendor.value !== VENDOR_COBALT
|
||||
)}
|
||||
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;
|
||||
}
|
||||
|
||||
if (vendor === VENDOR_ELEVENLABS) {
|
||||
const newLang = synthesis[vendor].find(
|
||||
(lang) => lang.code === ELEVENLABS_LANG_EN
|
||||
);
|
||||
setSynthLang(ELEVENLABS_LANG_EN);
|
||||
setSynthVoice(newLang!.voices[0].value);
|
||||
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={synthesisLanguageOptions}
|
||||
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={synthesisVoiceOptions}
|
||||
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);
|
||||
}
|
||||
// Default colbalt language
|
||||
if (vendor === VENDOR_COBALT) {
|
||||
setRecogLang(LANG_COBALT_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;
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
FQDN,
|
||||
FQDN_TOP_LEVEL,
|
||||
INVALID,
|
||||
IP,
|
||||
NETMASK_OPTIONS,
|
||||
SIP_GATEWAY_PROTOCOL_OPTIONS,
|
||||
TCP_MAX_PORT,
|
||||
@@ -47,6 +48,8 @@ import {
|
||||
hasLength,
|
||||
isValidPort,
|
||||
disableDefaultTrunkRouting,
|
||||
hasValue,
|
||||
isNotBlank,
|
||||
} from "src/utils";
|
||||
|
||||
import type {
|
||||
@@ -236,7 +239,20 @@ export const CarrierForm = ({
|
||||
value: typeof sipGateways[number][keyof SipGateway]
|
||||
) => {
|
||||
setSipGateways(
|
||||
sipGateways.map((g, i) => (i === index ? { ...g, [key]: value } : g))
|
||||
sipGateways.map((g, i) =>
|
||||
i === index
|
||||
? {
|
||||
...g,
|
||||
[key]: value,
|
||||
// If Change to ipv4 and port is null, change port to 5060
|
||||
...(key === "ipv4" &&
|
||||
value &&
|
||||
typeof value === "string" &&
|
||||
getIpValidationType(value) === IP &&
|
||||
g.port === null && { port: 5060 }),
|
||||
}
|
||||
: g
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -409,7 +425,9 @@ export const CarrierForm = ({
|
||||
/** When to switch to `sip` tab */
|
||||
|
||||
const emptySipIp = sipGateways.find((g) => g.ipv4.trim() === "");
|
||||
const invalidSipPort = sipGateways.find((g) => !isValidPort(g.port));
|
||||
const invalidSipPort = sipGateways.find(
|
||||
(g) => hasValue(g.port) && !isValidPort(g.port)
|
||||
);
|
||||
const sipGatewayValidation = getSipValidation();
|
||||
|
||||
/** Empty SIP gateway */
|
||||
@@ -962,13 +980,21 @@ export const CarrierForm = ({
|
||||
type="number"
|
||||
min="0"
|
||||
max={TCP_MAX_PORT}
|
||||
placeholder={DEFAULT_SIP_GATEWAY.port.toString()}
|
||||
value={g.port}
|
||||
placeholder={
|
||||
g.protocol === "tls" || g.protocol === "tls/srtp"
|
||||
? ""
|
||||
: DEFAULT_SIP_GATEWAY.port?.toString()
|
||||
}
|
||||
value={g.port === null ? "" : g.port}
|
||||
onChange={(e) => {
|
||||
updateSipGateways(
|
||||
i,
|
||||
"port",
|
||||
Number(e.target.value)
|
||||
g.outbound > 0 &&
|
||||
!isNotBlank(e.target.value) &&
|
||||
getIpValidationType(g.ipv4) !== IP
|
||||
? null
|
||||
: Number(e.target.value)
|
||||
);
|
||||
}}
|
||||
ref={(ref: HTMLInputElement) =>
|
||||
@@ -1064,6 +1090,29 @@ export const CarrierForm = ({
|
||||
<div>Outbound</div>
|
||||
</label>
|
||||
</div>
|
||||
{g.outbound > 0 && g.protocol === "tls/srtp" && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={`sip_pad_crypto_${i}`}
|
||||
className="chk"
|
||||
>
|
||||
<input
|
||||
id={`sip_pad_crypto_${i}`}
|
||||
name={`sip_pad_crypto_${i}`}
|
||||
type="checkbox"
|
||||
checked={g.pad_crypto ? true : false}
|
||||
onChange={(e) => {
|
||||
updateSipGateways(
|
||||
i,
|
||||
"pad_crypto",
|
||||
e.target.checked
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div>Pad crypto</div>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
||||
@@ -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>
|
||||
@@ -138,7 +155,7 @@ export const Carriers = () => {
|
||||
</Icon>
|
||||
</Link>
|
||||
</section>
|
||||
<section className="filters filters--spaced">
|
||||
<section className="filters filters--multi">
|
||||
<SearchFilter
|
||||
placeholder="Filter carriers"
|
||||
filter={[filter, setFilter]}
|
||||
|
||||
@@ -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 />
|
||||
@@ -76,7 +126,7 @@ export const Clients = () => {
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
<section className="filters filters--spaced">
|
||||
<section className="filters filters--multi">
|
||||
<SearchFilter
|
||||
placeholder="Filter clients"
|
||||
filter={[filter, setFilter]}
|
||||
@@ -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 && (
|
||||
|
||||
@@ -99,7 +99,7 @@ export const Lcrs = () => {
|
||||
multiple carriers available.
|
||||
</M>
|
||||
</section>
|
||||
<section className="filters filters--spaced">
|
||||
<section className="filters filters--multi">
|
||||
<SearchFilter placeholder="Filter lcrs" filter={[filter, setFilter]} />
|
||||
<ScopedAccess user={user} scope={Scope.admin}>
|
||||
<AccountFilter
|
||||
|
||||
@@ -89,7 +89,7 @@ export const MSTeamsTenants = () => {
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
<section className="filters filters--spaced">
|
||||
<section className="filters filters--multi">
|
||||
<SearchFilter
|
||||
placeholder="Filter ms teams tenants"
|
||||
filter={[filter, setFilter]}
|
||||
|
||||
@@ -129,7 +129,7 @@ export const PhoneNumbers = () => {
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
<section className="filters filters--spaced">
|
||||
<section className="filters filters--multi">
|
||||
<SearchFilter
|
||||
placeholder="Filter phone numbers"
|
||||
filter={[filter, setFilter]}
|
||||
|
||||
@@ -3,17 +3,17 @@ 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";
|
||||
import RegionsPlugin, { Region } from "wavesurfer.js/dist/plugins/regions";
|
||||
import TimelinePlugin from "wavesurfer.js/dist/plugins/timeline";
|
||||
import { API_BASE_URL } from "src/api/constants";
|
||||
import {
|
||||
JaegerRoot,
|
||||
JaegerSpan,
|
||||
WaveSufferDtmfResult,
|
||||
WaveSufferSttResult,
|
||||
WaveSurferDtmfResult,
|
||||
WaveSurferSttResult,
|
||||
} from "src/api/jaeger-types";
|
||||
import {
|
||||
getSpanAttributeByName,
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
getSpansByNameRegex,
|
||||
getSpansFromJaegerRoot,
|
||||
} from "./utils";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
|
||||
type PlayerProps = {
|
||||
call: RecentCall;
|
||||
@@ -37,21 +38,26 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [playBackTime, setPlayBackTime] = useState("");
|
||||
const [jaegerRoot, setJeagerRoot] = useState<JaegerRoot>();
|
||||
const [waveSufferRegionData, setWaveSufferRegionData] =
|
||||
useState<WaveSufferSttResult | null>();
|
||||
const [waveSufferDtmfData, setWaveSufferDtmfData] =
|
||||
useState<WaveSufferDtmfResult | null>();
|
||||
const [waveSurferRegionData, setWaveSurferRegionData] =
|
||||
useState<WaveSurferSttResult | null>();
|
||||
const [waveSurferDtmfData, setWaveSurferDtmfData] =
|
||||
useState<WaveSurferDtmfResult | null>();
|
||||
const [regionChecked, setRegionChecked] = useState(false);
|
||||
|
||||
const wavesurferId = `wavesurfer--${call_sid}`;
|
||||
const wavesurferTimelineId = `timeline-${wavesurferId}`;
|
||||
const waveSufferRef = useRef<WaveSurfer | null>(null);
|
||||
const waveSurferRef = useRef<WaveSurfer | null>(null);
|
||||
const waveSurferRegionsPluginRef = useRef<RegionsPlugin | null>();
|
||||
|
||||
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];
|
||||
if (waveSurferRegionsPluginRef.current) {
|
||||
waveSurferRef.current;
|
||||
const r = waveSurferRegionsPluginRef.current
|
||||
.getRegions()
|
||||
.find((r) => r.id === s.spanId);
|
||||
if (!r) {
|
||||
const [dtmfValue] = getSpanAttributeByName(s.attributes, "dtmf");
|
||||
const [durationValue] = getSpanAttributeByName(
|
||||
@@ -64,29 +70,28 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
1_000_000_000;
|
||||
const duration =
|
||||
Number(durationValue.value.stringValue.replace("ms", "")) / 1_000;
|
||||
// as duration of DTMF is short, cannot be shown in wavesuffer,
|
||||
// as duration of DTMF is short, cannot be shown in wavesurfer,
|
||||
// adjust region width here.
|
||||
const delta = duration <= 0.1 ? 0.1 : duration;
|
||||
const end = start + delta;
|
||||
|
||||
const region = waveSufferRef.current.addRegion({
|
||||
const region = waveSurferRegionsPluginRef.current.addRegion({
|
||||
id: s.spanId,
|
||||
start,
|
||||
end,
|
||||
color: "rgba(138, 43, 226, 0.15)",
|
||||
drag: false,
|
||||
loop: false,
|
||||
resize: false,
|
||||
});
|
||||
changeRegionMouseStyle(region);
|
||||
|
||||
const att: WaveSufferDtmfResult = {
|
||||
const att: WaveSurferDtmfResult = {
|
||||
dtmf: dtmfValue.value.stringValue,
|
||||
duration: durationValue.value.stringValue,
|
||||
};
|
||||
|
||||
region.on("click", () => {
|
||||
setWaveSufferDtmfData(att);
|
||||
setWaveSurferDtmfData(att);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -112,8 +117,10 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
startPoint: JaegerSpan,
|
||||
channel = 0
|
||||
) => {
|
||||
if (waveSufferRef.current) {
|
||||
const r = waveSufferRef.current.regions.list[s.spanId];
|
||||
if (waveSurferRegionsPluginRef.current) {
|
||||
const r = waveSurferRegionsPluginRef.current
|
||||
.getRegions()
|
||||
.find((r) => r.id === s.spanId);
|
||||
if (!r) {
|
||||
const start =
|
||||
(s.startTimeUnixNano - startPoint.startTimeUnixNano) / 1_000_000_000 +
|
||||
@@ -121,18 +128,18 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
const end =
|
||||
(s.endTimeUnixNano - startPoint.startTimeUnixNano) / 1_000_000_000;
|
||||
|
||||
const region = waveSufferRef.current.addRegion({
|
||||
const region = waveSurferRegionsPluginRef.current.addRegion({
|
||||
id: s.spanId,
|
||||
start,
|
||||
end,
|
||||
color: "rgba(255, 0, 0, 0.15)",
|
||||
drag: false,
|
||||
loop: false,
|
||||
resize: false,
|
||||
});
|
||||
|
||||
changeRegionMouseStyle(region, channel);
|
||||
const [sttResult] = getSpanAttributeByName(s.attributes, "stt.result");
|
||||
let att: WaveSufferSttResult;
|
||||
let att: WaveSurferSttResult;
|
||||
if (sttResult) {
|
||||
const data = JSON.parse(sttResult.value.stringValue);
|
||||
att = {
|
||||
@@ -165,13 +172,13 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
}
|
||||
|
||||
region.on("click", () => {
|
||||
setWaveSufferRegionData(att);
|
||||
setWaveSurferRegionData(att);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const buildWavesufferRegion = () => {
|
||||
const buildWavesurferRegion = () => {
|
||||
if (jaegerRoot) {
|
||||
const spans = getSpansFromJaegerRoot(jaegerRoot);
|
||||
const [startPoint] = getSpansByName(spans, "background-listen:listen");
|
||||
@@ -201,8 +208,21 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRecordSubmit = () => {
|
||||
if (deleteRecordUrl) {
|
||||
deleteRecord(deleteRecordUrl)
|
||||
.then(() => {
|
||||
setDeleteRecordUrl("");
|
||||
toastSuccess("Successfully deleted record");
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
buildWavesufferRegion();
|
||||
buildWavesurferRegion();
|
||||
}, [jaegerRoot, isReady]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -235,8 +255,9 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (waveSufferRef.current !== null || !record) return;
|
||||
waveSufferRef.current = WaveSurfer.create({
|
||||
if (waveSurferRef.current !== null || !record) return;
|
||||
waveSurferRegionsPluginRef.current = RegionsPlugin.create();
|
||||
waveSurferRef.current = WaveSurfer.create({
|
||||
container: `#${wavesurferId}`,
|
||||
waveColor: "#da1c5c",
|
||||
progressColor: "grey",
|
||||
@@ -244,62 +265,68 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
cursorWidth: 1,
|
||||
cursorColor: "lightgray",
|
||||
normalize: true,
|
||||
responsive: true,
|
||||
fillParent: true,
|
||||
splitChannels: true,
|
||||
scrollParent: true,
|
||||
autoScroll: true,
|
||||
splitChannels: [],
|
||||
minPxPerSec: 100,
|
||||
plugins: [
|
||||
RegionsPlugin.create({}),
|
||||
waveSurferRegionsPluginRef.current,
|
||||
TimelinePlugin.create({
|
||||
container: `#${wavesurferTimelineId}`,
|
||||
timeInterval: 0.2,
|
||||
primaryLabelInterval: 5,
|
||||
secondaryLabelInterval: 1,
|
||||
style: {
|
||||
fontSize: "15px",
|
||||
color: "#000000",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
waveSufferRef.current.load(record?.data_url);
|
||||
waveSurferRef.current.load(record?.data_url);
|
||||
// All event should be after load
|
||||
waveSufferRef.current.on("finish", () => {
|
||||
waveSurferRef.current.on("finish", () => {
|
||||
setIsPlaying(false);
|
||||
});
|
||||
|
||||
waveSufferRef.current.on("play", () => {
|
||||
waveSurferRef.current.on("play", () => {
|
||||
setIsPlaying(true);
|
||||
});
|
||||
|
||||
waveSufferRef.current.on("pause", () => {
|
||||
waveSurferRef.current.on("pause", () => {
|
||||
setIsPlaying(false);
|
||||
});
|
||||
|
||||
waveSufferRef.current.on("ready", () => {
|
||||
waveSurferRef.current.on("ready", () => {
|
||||
setIsReady(true);
|
||||
setPlayBackTime(formatTime(waveSufferRef.current?.getDuration() || 0));
|
||||
setPlayBackTime(formatTime(waveSurferRef.current?.getDuration() || 0));
|
||||
});
|
||||
|
||||
waveSufferRef.current.on("audioprocess", () => {
|
||||
setPlayBackTime(formatTime(waveSufferRef.current?.getCurrentTime() || 0));
|
||||
waveSurferRef.current.on("audioprocess", () => {
|
||||
setPlayBackTime(formatTime(waveSurferRef.current?.getCurrentTime() || 0));
|
||||
});
|
||||
}, [record]);
|
||||
|
||||
const togglePlayback = () => {
|
||||
if (waveSufferRef.current) {
|
||||
if (waveSurferRef.current) {
|
||||
if (!isPlaying) {
|
||||
waveSufferRef.current.play();
|
||||
waveSurferRef.current.play();
|
||||
} else {
|
||||
waveSufferRef.current.pause();
|
||||
waveSurferRef.current.pause();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setPlaybackJump = (delta: number) => {
|
||||
if (waveSufferRef.current) {
|
||||
const idx = waveSufferRef.current.getCurrentTime() + delta;
|
||||
if (waveSurferRef.current) {
|
||||
const idx = waveSurferRef.current.getCurrentTime() + delta;
|
||||
const value =
|
||||
idx <= 0
|
||||
? 0
|
||||
: idx >= waveSufferRef.current.getDuration()
|
||||
? waveSufferRef.current.getDuration() - 1
|
||||
: idx >= waveSurferRef.current.getDuration()
|
||||
? waveSurferRef.current.getDuration() - 1
|
||||
: idx;
|
||||
waveSufferRef.current.setCurrentTime(value);
|
||||
waveSurferRef.current.setTime(value);
|
||||
setPlayBackTime(formatTime(value));
|
||||
}
|
||||
};
|
||||
@@ -310,57 +337,74 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
<>
|
||||
<div className="media-container">
|
||||
<div id={wavesurferId} />
|
||||
<div id={wavesurferTimelineId} />
|
||||
<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
|
||||
@@ -370,8 +414,9 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
checked={regionChecked}
|
||||
onChange={(e) => {
|
||||
setRegionChecked(e.target.checked);
|
||||
if (waveSufferRef.current) {
|
||||
const regionsList = waveSufferRef.current.regions.list;
|
||||
if (waveSurferRegionsPluginRef.current) {
|
||||
const regionsList =
|
||||
waveSurferRegionsPluginRef.current.getRegions();
|
||||
for (const [, region] of Object.entries(regionsList)) {
|
||||
region.element.style.display = e.target.checked ? "" : "none";
|
||||
}
|
||||
@@ -381,8 +426,8 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
<div>Overlay STT and DTMF events</div>
|
||||
</label>
|
||||
</div>
|
||||
{waveSufferRegionData && (
|
||||
<ModalClose handleClose={() => setWaveSufferRegionData(null)}>
|
||||
{waveSurferRegionData && (
|
||||
<ModalClose handleClose={() => setWaveSurferRegionData(null)}>
|
||||
<div className="spanDetailsWrapper__header">
|
||||
<P>
|
||||
<strong>Speech to text result</strong>
|
||||
@@ -390,43 +435,43 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
</div>
|
||||
<div className="spanDetailsWrapper">
|
||||
<div className="spanDetailsWrapper__detailsWrapper">
|
||||
{waveSufferRegionData.vendor && (
|
||||
{waveSurferRegionData.vendor && (
|
||||
<div className="spanDetailsWrapper__details">
|
||||
<div className="spanDetailsWrapper__details_header">
|
||||
<strong>Vendor:</strong>
|
||||
</div>
|
||||
<div className="spanDetailsWrapper__details_body">
|
||||
{waveSufferRegionData.vendor}
|
||||
{waveSurferRegionData.vendor}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{waveSufferRegionData.confidence !== 0 && (
|
||||
{waveSurferRegionData.confidence !== 0 && (
|
||||
<div className="spanDetailsWrapper__details">
|
||||
<div className="spanDetailsWrapper__details_header">
|
||||
<strong>Confidence:</strong>
|
||||
</div>
|
||||
<div className="spanDetailsWrapper__details_body">
|
||||
{waveSufferRegionData.confidence}
|
||||
{waveSurferRegionData.confidence}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{waveSufferRegionData.language_code && (
|
||||
{waveSurferRegionData.language_code && (
|
||||
<div className="spanDetailsWrapper__details">
|
||||
<div className="spanDetailsWrapper__details_header">
|
||||
<strong>Language code:</strong>
|
||||
</div>
|
||||
<div className="spanDetailsWrapper__details_body">
|
||||
{waveSufferRegionData.language_code}
|
||||
{waveSurferRegionData.language_code}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{waveSufferRegionData.transcript && (
|
||||
{waveSurferRegionData.transcript && (
|
||||
<div className="spanDetailsWrapper__details">
|
||||
<div className="spanDetailsWrapper__details_header">
|
||||
<strong>Transcript:</strong>
|
||||
</div>
|
||||
<div className="spanDetailsWrapper__details_body">
|
||||
{waveSufferRegionData.transcript}
|
||||
{waveSurferRegionData.transcript}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -434,8 +479,8 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
</div>
|
||||
</ModalClose>
|
||||
)}
|
||||
{waveSufferDtmfData && (
|
||||
<ModalClose handleClose={() => setWaveSufferDtmfData(null)}>
|
||||
{waveSurferDtmfData && (
|
||||
<ModalClose handleClose={() => setWaveSurferDtmfData(null)}>
|
||||
<div className="spanDetailsWrapper__header">
|
||||
<P>
|
||||
<strong>Dtmf result</strong>
|
||||
@@ -448,7 +493,7 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
<strong>Dtmf:</strong>
|
||||
</div>
|
||||
<div className="spanDetailsWrapper__details_body">
|
||||
{waveSufferDtmfData.dtmf}
|
||||
{waveSurferDtmfData.dtmf}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -457,13 +502,24 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
<strong>Duration:</strong>
|
||||
</div>
|
||||
<div className="spanDetailsWrapper__details_body">
|
||||
{waveSufferDtmfData.duration}
|
||||
{waveSurferDtmfData.duration}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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,7 +19,59 @@
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
overflow-x: auto;
|
||||
border: 1px solid black;
|
||||
border-radius: ui-vars.$px01;
|
||||
padding: 13px;
|
||||
@@ -31,18 +83,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,
|
||||
@@ -30,12 +30,15 @@ import {
|
||||
VENDOR_NVIDIA,
|
||||
VENDOR_SONIOX,
|
||||
VENDOR_CUSTOM,
|
||||
VENDOR_COBALT,
|
||||
VENDOR_ELEVENLABS,
|
||||
} from "src/vendor";
|
||||
import { MSG_REQUIRED_FIELDS } from "src/constants";
|
||||
import {
|
||||
checkSelectOptions,
|
||||
getObscuredSecret,
|
||||
isUserAccountScope,
|
||||
isNotBlank,
|
||||
} from "src/utils";
|
||||
import { getObscuredGoogleServiceKey } from "./utils";
|
||||
import { CredentialStatus } from "./status";
|
||||
@@ -43,7 +46,11 @@ import { CredentialStatus } from "./status";
|
||||
import type { RegionVendors, GoogleServiceKey, Vendor } from "src/vendor/types";
|
||||
import type { Account, SpeechCredential, UseApiDataMap } from "src/api/types";
|
||||
import { setAccountFilter, setLocation } from "src/store/localStore";
|
||||
import { DISABLE_CUSTOM_SPEECH } from "src/api/constants";
|
||||
import {
|
||||
DEFAULT_ELEVENLABS_MODEL,
|
||||
DISABLE_CUSTOM_SPEECH,
|
||||
ELEVENLABS_MODEL_OPTIONS,
|
||||
} from "src/api/constants";
|
||||
|
||||
type SpeechServiceFormProps = {
|
||||
credential?: UseApiDataMap<SpeechCredential>;
|
||||
@@ -75,11 +82,20 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
const [sttApiKey, setSttApiKey] = useState("");
|
||||
const [ttsRegion, setTtsRegion] = useState("");
|
||||
const [ttsApiKey, setTtsApiKey] = useState("");
|
||||
const [ttsModelId, setTtsModelId] = useState(DEFAULT_ELEVENLABS_MODEL);
|
||||
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 +115,8 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
const [onPremNuanceSttCheck, setOnPremNuanceSttCheck] = useState(false);
|
||||
const [tmpOnPremNuanceSttUrl, setTmpOnPremNuanceSttUrl] = useState("");
|
||||
const [onPremNuanceSttUrl, setOnPremNuanceSttUrl] = useState("");
|
||||
const [cobaltServerUri, setCobaltServerUri] = useState("");
|
||||
const [label, setLabel] = useState("");
|
||||
|
||||
const handleFile = (file: File) => {
|
||||
const handleError = () => {
|
||||
@@ -143,14 +161,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 && {
|
||||
@@ -177,6 +200,12 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
nuance_tts_uri: onPremNuanceTtsUrl || null,
|
||||
nuance_stt_uri: onPremNuanceSttUrl || null,
|
||||
}),
|
||||
...(vendor === VENDOR_COBALT && {
|
||||
cobalt_server_uri: cobaltServerUri || null,
|
||||
}),
|
||||
...(vendor === VENDOR_ELEVENLABS && {
|
||||
model_id: ttsModelId || null,
|
||||
}),
|
||||
};
|
||||
|
||||
if (credential && credential.data) {
|
||||
@@ -206,13 +235,16 @@ 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 ||
|
||||
vendor === VENDOR_ELEVENLABS
|
||||
? apiKey
|
||||
: null,
|
||||
}),
|
||||
riva_server_uri: vendor == VENDOR_NVIDIA ? rivaServerUri : null,
|
||||
})
|
||||
.then(() => {
|
||||
@@ -330,13 +362,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 +392,15 @@ 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);
|
||||
}
|
||||
if (credential.data.cobalt_server_uri) {
|
||||
setCobaltServerUri(credential.data.cobalt_server_uri);
|
||||
}
|
||||
if (credential.data.model_id) {
|
||||
setTtsModelId(credential.data.model_id);
|
||||
}
|
||||
}
|
||||
}, [credential]);
|
||||
|
||||
@@ -416,9 +470,26 @@ 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 &&
|
||||
vendor !== VENDOR_COBALT &&
|
||||
vendor !== VENDOR_SONIOX &&
|
||||
vendor != VENDOR_CUSTOM && (
|
||||
<label htmlFor="use_for_tts" className="chk">
|
||||
@@ -432,18 +503,20 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
<div>Use for text-to-speech</div>
|
||||
</label>
|
||||
)}
|
||||
{vendor !== VENDOR_WELLSAID && vendor !== VENDOR_CUSTOM && (
|
||||
<label htmlFor="use_for_stt" className="chk">
|
||||
<input
|
||||
id="use_for_stt"
|
||||
name="use_for_stt"
|
||||
type="checkbox"
|
||||
onChange={(e) => setSttCheck(e.target.checked)}
|
||||
defaultChecked={sttCheck}
|
||||
/>
|
||||
<div>Use for speech-to-text</div>
|
||||
</label>
|
||||
)}
|
||||
{vendor !== VENDOR_WELLSAID &&
|
||||
vendor !== VENDOR_CUSTOM &&
|
||||
vendor !== VENDOR_ELEVENLABS && (
|
||||
<label htmlFor="use_for_stt" className="chk">
|
||||
<input
|
||||
id="use_for_stt"
|
||||
name="use_for_stt"
|
||||
type="checkbox"
|
||||
onChange={(e) => setSttCheck(e.target.checked)}
|
||||
defaultChecked={sttCheck}
|
||||
/>
|
||||
<div>Use for speech-to-text</div>
|
||||
</label>
|
||||
)}
|
||||
{vendor === VENDOR_CUSTOM && (
|
||||
<Fragment>
|
||||
<Checkzone
|
||||
@@ -511,6 +584,24 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
)}
|
||||
</fieldset>
|
||||
)}
|
||||
{vendor === VENDOR_COBALT && (
|
||||
<fieldset>
|
||||
<label htmlFor="cobalt_server_url">
|
||||
Server URI<span>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="cobalt_server_url"
|
||||
type="text"
|
||||
name="cobalt_server_url"
|
||||
placeholder="Required"
|
||||
required
|
||||
value={cobaltServerUri}
|
||||
onChange={(e) => {
|
||||
setCobaltServerUri(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
{vendor === VENDOR_CUSTOM && (
|
||||
<fieldset>
|
||||
<label htmlFor="custom_vendor_auth_token">
|
||||
@@ -695,9 +786,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
{(vendor === VENDOR_MICROSOFT ||
|
||||
vendor === VENDOR_WELLSAID ||
|
||||
{(vendor === VENDOR_WELLSAID ||
|
||||
vendor === VENDOR_DEEPGRAM ||
|
||||
vendor == VENDOR_ELEVENLABS ||
|
||||
vendor === VENDOR_SONIOX) && (
|
||||
<fieldset>
|
||||
<label htmlFor={`${vendor}_apikey`}>
|
||||
@@ -714,9 +805,24 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
{vendor == VENDOR_ELEVENLABS && (
|
||||
<fieldset>
|
||||
<label htmlFor={`${vendor}_apikey`}>Model</label>
|
||||
<Selector
|
||||
id={"audio_format"}
|
||||
name={"audio_format"}
|
||||
value={ttsModelId}
|
||||
options={ELEVENLABS_MODEL_OPTIONS}
|
||||
onChange={(e) => {
|
||||
setTtsModelId(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
{regions &&
|
||||
regions[vendor as keyof RegionVendors] &&
|
||||
vendor !== VENDOR_IBM && (
|
||||
vendor !== VENDOR_IBM &&
|
||||
vendor !== VENDOR_MICROSOFT && (
|
||||
<fieldset>
|
||||
<label htmlFor="region">
|
||||
Region<span>*</span>
|
||||
@@ -813,76 +919,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>{" "}
|
||||
</>
|
||||
);
|
||||
})
|
||||
@@ -108,7 +112,7 @@ export const SpeechServices = () => {
|
||||
</Icon>
|
||||
</Link>
|
||||
</section>
|
||||
<section className="filters filters--ender">
|
||||
<section className="filters filters--multi">
|
||||
<ScopedAccess user={user} scope={Scope.service_provider}>
|
||||
<AccountFilter
|
||||
account={[accountSid, setAccountSid]}
|
||||
@@ -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
|
||||
|
||||
@@ -88,7 +88,7 @@ export const Users = () => {
|
||||
</Icon>
|
||||
</Link>
|
||||
</section>
|
||||
<section className="filters filters--mix">
|
||||
<section className="filters filters--multi">
|
||||
<section>
|
||||
<SearchFilter
|
||||
placeholder="Filter users"
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -19,14 +19,45 @@
|
||||
grid-gap: ui-vars.$px02;
|
||||
}
|
||||
}
|
||||
|
||||
> :first-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
&--multi {
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
grid-gap: ui-vars.$px02;
|
||||
|
||||
> :first-child {
|
||||
margin-left: auto;
|
||||
@media (max-width: 1400px) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
|
||||
> * {
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
> * {
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
> * {
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
> * {
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ fieldset {
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
margin-top: ui-vars.$px02;
|
||||
|
||||
> div:last-child {
|
||||
|
||||
@@ -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);
|
||||
|
||||
20
src/vendor/index.tsx
vendored
20
src/vendor/index.tsx
vendored
@@ -8,6 +8,8 @@ import type {
|
||||
} from "./types";
|
||||
|
||||
export const LANG_EN_US = "en-US";
|
||||
export const ELEVENLABS_LANG_EN = "en";
|
||||
export const LANG_COBALT_EN_US = "en_US-8khz";
|
||||
export const LANG_EN_US_STANDARD_C = "en-US-Standard-C";
|
||||
export const VENDOR_GOOGLE = "google";
|
||||
export const VENDOR_AWS = "aws";
|
||||
@@ -19,6 +21,8 @@ export const VENDOR_IBM = "ibm";
|
||||
export const VENDOR_NVIDIA = "nvidia";
|
||||
export const VENDOR_SONIOX = "soniox";
|
||||
export const VENDOR_CUSTOM = "custom";
|
||||
export const VENDOR_COBALT = "cobalt";
|
||||
export const VENDOR_ELEVENLABS = "elevenlabs";
|
||||
|
||||
export const vendors: VendorOptions[] = [
|
||||
{
|
||||
@@ -61,7 +65,15 @@ export const vendors: VendorOptions[] = [
|
||||
name: "Custom",
|
||||
value: VENDOR_CUSTOM,
|
||||
},
|
||||
];
|
||||
{
|
||||
name: "Cobalt",
|
||||
value: VENDOR_COBALT,
|
||||
},
|
||||
{
|
||||
name: "ElevenLabs",
|
||||
value: VENDOR_ELEVENLABS,
|
||||
},
|
||||
].sort((a, b) => a.name.localeCompare(b.name)) as VendorOptions[];
|
||||
|
||||
export const useRegionVendors = () => {
|
||||
const [regions, setRegions] = useState<RegionVendors>();
|
||||
@@ -115,6 +127,7 @@ export const useSpeechVendors = () => {
|
||||
import("./speech-recognizer/ibm-speech-recognizer-lang"),
|
||||
import("./speech-recognizer/nvidia-speech-recognizer-lang"),
|
||||
import("./speech-recognizer/soniox-speech-recognizer-lang"),
|
||||
import("./speech-recognizer/cobalt-speech-recognizer-lang"),
|
||||
import("./speech-synthesis/aws-speech-synthesis-lang"),
|
||||
import("./speech-synthesis/google-speech-synthesis-lang"),
|
||||
import("./speech-synthesis/ms-speech-synthesis-lang"),
|
||||
@@ -122,6 +135,7 @@ export const useSpeechVendors = () => {
|
||||
import("./speech-synthesis/nuance-speech-synthesis-lang"),
|
||||
import("./speech-synthesis/ibm-speech-synthesis-lang"),
|
||||
import("./speech-synthesis/nvidia-speech-synthesis-lang"),
|
||||
import("./speech-synthesis/elevellabs-speech-synthesis-lang"),
|
||||
]).then(
|
||||
([
|
||||
{ default: awsRecognizer },
|
||||
@@ -132,6 +146,7 @@ export const useSpeechVendors = () => {
|
||||
{ default: ibmRecognizer },
|
||||
{ default: nvidiaRecognizer },
|
||||
{ default: sonioxRecognizer },
|
||||
{ default: cobaltRecognizer },
|
||||
{ default: awsSynthesis },
|
||||
{ default: googleSynthesis },
|
||||
{ default: msSynthesis },
|
||||
@@ -139,6 +154,7 @@ export const useSpeechVendors = () => {
|
||||
{ default: nuanceSynthesis },
|
||||
{ default: ibmSynthesis },
|
||||
{ default: nvidiaynthesis },
|
||||
{ default: elevenLabsSynthesis },
|
||||
]) => {
|
||||
if (!ignore) {
|
||||
setSpeech({
|
||||
@@ -150,6 +166,7 @@ export const useSpeechVendors = () => {
|
||||
nuance: nuanceSynthesis,
|
||||
ibm: ibmSynthesis,
|
||||
nvidia: nvidiaynthesis,
|
||||
elevenlabs: elevenLabsSynthesis,
|
||||
},
|
||||
recognizers: {
|
||||
aws: awsRecognizer,
|
||||
@@ -160,6 +177,7 @@ export const useSpeechVendors = () => {
|
||||
ibm: ibmRecognizer,
|
||||
nvidia: nvidiaRecognizer,
|
||||
soniox: sonioxRecognizer,
|
||||
cobalt: cobaltRecognizer,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
30
src/vendor/speech-recognizer/cobalt-speech-recognizer-lang.ts
vendored
Normal file
30
src/vendor/speech-recognizer/cobalt-speech-recognizer-lang.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { Language } from "../types";
|
||||
|
||||
export const languages: Language[] = [
|
||||
{
|
||||
name: "English US",
|
||||
code: "en_US-8khz",
|
||||
},
|
||||
{
|
||||
name: "English UK",
|
||||
code: "en_UK-8khz",
|
||||
},
|
||||
{
|
||||
name: "Spanish",
|
||||
code: "es_xx-8khz",
|
||||
},
|
||||
{
|
||||
name: "French",
|
||||
code: "fr_fr-8khz",
|
||||
},
|
||||
{
|
||||
name: "Russian",
|
||||
code: "ru_ru-8khz",
|
||||
},
|
||||
{
|
||||
name: "Portuguese",
|
||||
code: "pt_br-8khz",
|
||||
},
|
||||
];
|
||||
|
||||
export default languages;
|
||||
196
src/vendor/speech-synthesis/elevellabs-speech-synthesis-lang.ts
vendored
Normal file
196
src/vendor/speech-synthesis/elevellabs-speech-synthesis-lang.ts
vendored
Normal file
@@ -0,0 +1,196 @@
|
||||
import type { VoiceLanguage } from "../types";
|
||||
|
||||
export const languages: VoiceLanguage[] = [
|
||||
{
|
||||
code: "ar",
|
||||
name: "Arabic",
|
||||
voices: [
|
||||
{
|
||||
value: "pNInz6obpgDQGcFmaJgB",
|
||||
name: "Adam - american, deep, middle aged, male, narration",
|
||||
},
|
||||
{
|
||||
value: "ErXwobaYiN019PkySvjV",
|
||||
name: "Antoni - american, well-rounded, young, male, narration",
|
||||
},
|
||||
{
|
||||
value: "VR6AewLTigWG4xSOukaG",
|
||||
name: "Arnold - american, crisp, middle aged, male, narration",
|
||||
},
|
||||
{
|
||||
value: "EXAVITQu4vr4xnSDxMaL",
|
||||
name: "Bella - american, soft, young, female, narration",
|
||||
},
|
||||
{
|
||||
value: "N2lVS1w4EtoT3dr4eOWO",
|
||||
name: "Callum - american, hoarse, middle aged, male, video games",
|
||||
},
|
||||
{
|
||||
value: "IKne3meq5aSn9XLyUdCD",
|
||||
name: "Charlie - australian, casual, middle aged, male, conversational",
|
||||
},
|
||||
{
|
||||
value: "XB0fDUnXU5powFXDhCwa",
|
||||
name: "Charlotte - english-swedish, seductive, middle aged, female, video games",
|
||||
},
|
||||
{
|
||||
value: "2EiwWnXFnvU5JabPnv8n",
|
||||
name: "Clyde - american, war veteran, middle aged, male, video games",
|
||||
},
|
||||
{
|
||||
value: "onwK4e9ZLuTAKqWW03F9",
|
||||
name: "Daniel - british, deep, middle aged, male, news presenter",
|
||||
},
|
||||
{
|
||||
value: "CYw3kZ02Hs0563khs1Fj",
|
||||
name: "Dave - british-essex, conversational, young, male, video games",
|
||||
},
|
||||
{
|
||||
value: "AZnzlk1XvdvUeBnXmlld",
|
||||
name: "Domi - american, strong, young, female, narration",
|
||||
},
|
||||
{
|
||||
value: "ThT5KcBeYPX3keUQqHPh",
|
||||
name: "Dorothy - british, pleasant, young, female, children's stories",
|
||||
},
|
||||
{
|
||||
value: "MF3mGyEYCl7XYWbV9V6O",
|
||||
name: "Elli - american, emotional, young, female, narration",
|
||||
},
|
||||
{
|
||||
value: "LcfcDJNUP1GQjkzn1xUU",
|
||||
name: "Emily - american, calm, young, female, meditation",
|
||||
},
|
||||
{
|
||||
value: "g5CIjZEefAph4nQFvHAz",
|
||||
name: "Ethan - american, undefined, young, male, ASMR",
|
||||
},
|
||||
{
|
||||
value: "D38z5RcWu1voky8WS1ja",
|
||||
name: "Fin - irish, sailor, old, male, video games",
|
||||
},
|
||||
{
|
||||
value: "jsCqWAovK2LkecY7zXl4",
|
||||
name: "Freya - american, undefined, young, female, undefined",
|
||||
},
|
||||
{
|
||||
value: "jBpfuIE2acCO8z3wKNLl",
|
||||
name: "Gigi - american, childlish, young, female, animation",
|
||||
},
|
||||
{
|
||||
value: "zcAOhNBS3c14rBihAFp1",
|
||||
name: "Giovanni - english-italian, foreigner, young, male, audiobook",
|
||||
},
|
||||
{
|
||||
value: "z9fAnlkpzviPz146aGWa",
|
||||
name: "Glinda - american, witch, middle aged, female, video games",
|
||||
},
|
||||
{
|
||||
value: "oWAxZDx7w5VEj9dCyTzz",
|
||||
name: "Grace - american-southern, undefined, young, female, audiobook ",
|
||||
},
|
||||
{
|
||||
value: "SOYHLrjzK2X1ezoPC6cr",
|
||||
name: "Harry - american, anxious, young, male, video games",
|
||||
},
|
||||
{
|
||||
value: "ZQe5CZNOzWyzPSCn5a3c",
|
||||
name: "James - australian, calm , old, male, news",
|
||||
},
|
||||
{
|
||||
value: "bVMeCyTHy58xNoL34h3p",
|
||||
name: "Jeremy - american-irish, excited, young, male, narration",
|
||||
},
|
||||
{
|
||||
value: "t0jbNlBVZ17f02VDIeMI",
|
||||
name: "Jessie - american, raspy , old, male, video games",
|
||||
},
|
||||
{
|
||||
value: "Zlb1dXrM653N07WRdFW3",
|
||||
name: "Joseph - british, undefined, middle aged, male, news",
|
||||
},
|
||||
{
|
||||
value: "TxGEqnHWrfWFTfGW9XjX",
|
||||
name: "Josh - american, deep, young, male, narration",
|
||||
},
|
||||
{
|
||||
value: "TX3LPaxmHKxFdv7VOQHJ",
|
||||
name: "Liam - american, undefined, young, male, narration",
|
||||
},
|
||||
{
|
||||
value: "XrExE9yKIg1WjnnlVkGX",
|
||||
name: "Matilda - american, warm, young, female, audiobook",
|
||||
},
|
||||
{
|
||||
value: "Yko7PKHZNXotIFUBG7I9",
|
||||
name: "Matthew - british, undefined, middle aged, male, audiobook",
|
||||
},
|
||||
{
|
||||
value: "flq6f7yk4E4fJM5XTYuZ",
|
||||
name: "Michael - american, undefined, old, male, audiobook",
|
||||
},
|
||||
{
|
||||
value: "zrHiDhphv9ZnVXBqCLjz",
|
||||
name: "Mimi - english-swedish, childish, young, female, animation",
|
||||
},
|
||||
{
|
||||
value: "piTKgcLEGmPE4e6mEKli",
|
||||
name: "Nicole - american, whisper, young, female, audiobook",
|
||||
},
|
||||
{
|
||||
value: "ODq5zmih8GrVes37Dizd",
|
||||
name: "Patrick - american, shouty, middle aged, male, video games",
|
||||
},
|
||||
{
|
||||
value: "21m00Tcm4TlvDq8ikWAM",
|
||||
name: "Rachel - american, calm, young, female, narration",
|
||||
},
|
||||
{
|
||||
value: "wViXBPUzp2ZZixB1xQuM",
|
||||
name: "Ryan - american, soldier, middle aged, male, audiobook",
|
||||
},
|
||||
{
|
||||
value: "yoZ06aMxZJJ28mfd3POQ",
|
||||
name: "Sam - american, raspy, young, male, narration",
|
||||
},
|
||||
{
|
||||
value: "pMsXgVXv3BLzUgSXRplE",
|
||||
name: "Serena - american, pleasant, middle aged, female, interactive",
|
||||
},
|
||||
{
|
||||
value: "GBv7mTt0atIp3Br8iCZE",
|
||||
name: "Thomas - american, calm, young, male, meditation",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ code: "bg", name: "Bulgarian", voices: [] },
|
||||
{ code: "zh", name: "Chinese", voices: [] },
|
||||
{ code: "hr", name: "Croatian", voices: [] },
|
||||
{ code: "cs", name: "Czech", voices: [] },
|
||||
{ code: "da", name: "Danish", voices: [] },
|
||||
{ code: "nl", name: "Dutch", voices: [] },
|
||||
{ code: "en", name: "English", voices: [] },
|
||||
{ code: "fil", name: "Filipino", voices: [] },
|
||||
{ code: "fi", name: "Finnish", voices: [] },
|
||||
{ code: "fr", name: "French", voices: [] },
|
||||
{ code: "de", name: "German", voices: [] },
|
||||
{ code: "el", name: "Greek", voices: [] },
|
||||
{ code: "hi", name: "Hindi", voices: [] },
|
||||
{ code: "id", name: "Indonesian", voices: [] },
|
||||
{ code: "it", name: "Italian", voices: [] },
|
||||
{ code: "ja", name: "Japanese", voices: [] },
|
||||
{ code: "ko", name: "Korean", voices: [] },
|
||||
{ code: "ms", name: "Malay", voices: [] },
|
||||
{ code: "pl", name: "Polish", voices: [] },
|
||||
{ code: "pt", name: "Portuguese", voices: [] },
|
||||
{ code: "ro", name: "Romanian", voices: [] },
|
||||
{ code: "ru", name: "Russian", voices: [] },
|
||||
{ code: "sk", name: "Slovak", voices: [] },
|
||||
{ code: "es", name: "Spanish", voices: [] },
|
||||
{ code: "sv", name: "Swedish", voices: [] },
|
||||
{ code: "ta", name: "Tamil", voices: [] },
|
||||
{ code: "tr", name: "Turkish", voices: [] },
|
||||
{ code: "uk", name: "Ukrainian", voices: [] },
|
||||
];
|
||||
|
||||
export default languages;
|
||||
11
src/vendor/types.ts
vendored
11
src/vendor/types.ts
vendored
@@ -8,13 +8,20 @@ export type Vendor =
|
||||
| "IBM"
|
||||
| "Nvidia"
|
||||
| "Soniox"
|
||||
| "Custom";
|
||||
| "Cobalt"
|
||||
| "Custom"
|
||||
| "ElevenLabs";
|
||||
|
||||
export interface VendorOptions {
|
||||
name: Vendor;
|
||||
value: Lowercase<Vendor>;
|
||||
}
|
||||
|
||||
export interface LabelOptions {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Region {
|
||||
name: string;
|
||||
value: string;
|
||||
@@ -65,6 +72,7 @@ export interface RecognizerVendors {
|
||||
ibm: Language[];
|
||||
nvidia: Language[];
|
||||
soniox: Language[];
|
||||
cobalt: Language[];
|
||||
}
|
||||
|
||||
export interface SynthesisVendors {
|
||||
@@ -75,6 +83,7 @@ export interface SynthesisVendors {
|
||||
nuance: VoiceLanguage[];
|
||||
ibm: VoiceLanguage[];
|
||||
nvidia: VoiceLanguage[];
|
||||
elevenlabs: VoiceLanguage[];
|
||||
}
|
||||
|
||||
export interface MSRawSpeech {
|
||||
|
||||
Reference in New Issue
Block a user