Compare commits

..

35 Commits

Author SHA1 Message Date
Quan HL
99c431ea4e fix carrier registration status 2023-11-24 06:51:45 +07:00
Hoan Luu Huu
42af4f6243 fix elevenlabs set voice (#353) 2023-11-21 08:27:47 -06:00
Hoan Luu Huu
7ec8065977 fix lcr create update generate too much request (#351)
* fix lcr create update generate too much request

* wip
2023-11-14 08:07:29 -05:00
Dave Horton
d8f05da6fd fix regression bug: not sending aws_region (#348) 2023-11-09 16:02:42 -05:00
Dave Horton
15c2b955ca 0.8.5 2023-11-09 12:37:59 -05:00
Hoan Luu Huu
87b3ca7e94 Support whisper TTS (#346)
* support tts whisper

* support tts whisper

* wip

* wip

* fix wrong language and voice
2023-11-09 09:50:38 -05:00
Hoan Luu Huu
adafff7ec3 disable update client username/password (#345)
* disable update client username/password

* fix application stt vendor for elevenlabs
2023-11-08 12:13:08 -05:00
Dave Horton
bc9a2464fd Fine tune speech latency (#344)
* divide speech into 10ms segments when evaluating peaks for speech latency

* minor

* minor

* minor change for clarity
2023-11-07 14:40:44 -05:00
Dave Horton
2a6f8c272c assemblyai is stt only (#343) 2023-11-06 12:34:26 -05:00
Dave Horton
f031c47228 changes to enable/disable direct calling from chrome extension (#342)
* changes to enable/disable direct calling from chrome extension

* wip

* wip

* wip

---------

Co-authored-by: Quan HL <quan.luuhoang8@gmail.com>
2023-11-06 09:34:33 -05:00
Hoan Luu Huu
2e9b86c0c4 feat calculate speech recognition latency (#341)
* feat calculate speech recognition latency

* fix review comments

* wip

* wip

* wip

* wip

* wip
2023-11-06 09:31:24 -05:00
Hoan Luu Huu
dd93bedd0e Feat/assemblyai (#340)
* feat assembly ai

* feat assembly ai
2023-11-01 08:03:24 -04:00
Hoan Luu Huu
e2157ce50e feat google custom voice (#338)
* feat google custom voice

* google custom voice

* wip

* wip

* wip
2023-10-30 20:28:34 -04:00
Dave Horton
a382f21f86 #335 - allow top level fqdn for outbound gateway (#336) 2023-10-21 11:22:18 +02:00
Hoan Luu Huu
a20e1513bc remove warning message if there is no device call application (#334) 2023-10-20 13:18:01 +02:00
Hoan Luu Huu
af8c09587c fix wrong css for filter on each component (#333) 2023-10-20 08:16:00 +02:00
Hoan Luu Huu
3a19ff6840 upgrade wavesuffer to 7.3.4 (#329)
* upgrade wavesuffer to 7.3.4

* fix typo issue for wavesurfer

* fix

* fix
2023-10-18 19:54:59 +02:00
Hoan Luu Huu
729cefb06c fix css for recent calls in small screen (#331) 2023-10-16 12:51:49 +02:00
Hoan Luu Huu
26e3856603 add elevenlabs (#330)
* add elevenlabs

* wip

* wip

* fix review comments

* fetch voice and language for eleven labs

* fix revciew comment

* fix revciew comment

* fix revciew comment

* fixed review comment
2023-10-16 10:21:19 +02:00
Hoan Luu Huu
f5302583b5 fix cobalt default language (#326) 2023-09-27 07:50:11 -04:00
Hoan Luu Huu
b5c27bb096 add cobalt stt (#324)
* add cobalt stt

* update languages for cobalt

* update languages for cobalt
2023-09-26 08:42:34 -04:00
Hoan Luu Huu
4a2c36ebba allow sip port is null (#323)
* allow sip port is null

* fix port placeholder when protocol is tls or tls/srtp
2023-09-18 20:15:53 -04:00
Hoan Luu Huu
62234f9f64 pad crypto sip gateway (#322) 2023-09-18 07:59:07 -04:00
Anton Voylenko
9ddafee2cc feat: support s3 compatible storage (#318)
* feat: support s3 compatible storage

* reorder vendor list
2023-09-12 12:28:03 -04:00
Hoan Luu Huu
24fc9d1bff feat: custom Microsoft need to have input text for tts voice (#316)
* feat: custom Microsoft need to have input text for tts voice

* fix
2023-09-08 08:15:44 -04:00
Hoan Luu Huu
08ab494cef feat azure fromhost (#302)
* feat azure fromhost

* wip

* wip

* wip

* wip

* fix review comment

* fix review comment

* wip

* wip
2023-08-30 21:07:03 -04:00
Hoan Luu Huu
75e7785061 fix choose speech dedential by label (#315) 2023-08-30 09:22:39 -04:00
Hoan Luu Huu
72de9178a2 support delete record file (#313)
* support delete record file

* fix
2023-08-23 19:58:59 -04:00
Hoan Luu Huu
9741e5601f feat fallback speech vendor (#310)
* feat fallback speech vendor

* fix

* fix

* fix

* wip

* wip
2023-08-22 08:08:22 -04:00
Hoan Luu Huu
346ac66440 support azure storage (#312) 2023-08-22 07:50:38 -04:00
Hoan Luu Huu
843d1eda1e feat support multiple speech credential with different labels but same vendor (#305)
* feat support multiple speech credential with different labels but same vendor

* fix

* fix review comment

* fix review comment

* fix label tooltip

* fixed
2023-08-15 09:00:00 -04:00
Hoan Luu Huu
27f02c2bb3 update preview description (#301) 2023-08-04 09:51:01 -04:00
Hoan Luu Huu
bb18556a6c allow only card type in stripe PaymentElement (#299) 2023-08-04 06:57:10 -04:00
Hoan Luu Huu
393dd7374f fix clients make user confuse (#298)
* fix clients make user confuse

* fix
2023-08-03 21:13:04 -04:00
Hoan Luu Huu
4ad2154337 fix choose/edit sub domain (#295)
* fix choose/edit sub domain

* add carrier instructions to send call to sip-realm
2023-08-02 07:42:33 -04:00
51 changed files with 3235 additions and 1004 deletions

View File

@@ -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>

558
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "jambonz-webapp",
"version": "0.8.4",
"version": "0.8.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "jambonz-webapp",
"version": "0.8.4",
"version": "0.8.5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -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",
@@ -78,17 +77,89 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz",
"integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==",
"version": "7.22.13",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
"integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
"dev": true,
"dependencies": {
"@babel/highlight": "^7.22.5"
"@babel/highlight": "^7.22.13",
"chalk": "^2.4.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/code-frame/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/@babel/code-frame/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/code-frame/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/code-frame/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/compat-data": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz",
@@ -129,12 +200,12 @@
}
},
"node_modules/@babel/generator": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz",
"integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==",
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz",
"integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==",
"dev": true,
"dependencies": {
"@babel/types": "^7.22.5",
"@babel/types": "^7.23.3",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -203,22 +274,22 @@
"dev": true
},
"node_modules/@babel/helper-environment-visitor": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz",
"integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-function-name": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz",
"integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"dependencies": {
"@babel/template": "^7.22.5",
"@babel/types": "^7.22.5"
"@babel/template": "^7.22.15",
"@babel/types": "^7.23.0"
},
"engines": {
"node": ">=6.9.0"
@@ -288,9 +359,9 @@
}
},
"node_modules/@babel/helper-split-export-declaration": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz",
"integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==",
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
"integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
"dev": true,
"dependencies": {
"@babel/types": "^7.22.5"
@@ -309,9 +380,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz",
"integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -341,13 +412,13 @@
}
},
"node_modules/@babel/highlight": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz",
"integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
"integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.5",
"chalk": "^2.0.0",
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"engines": {
@@ -426,9 +497,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz",
"integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==",
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz",
"integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -523,33 +594,33 @@
}
},
"node_modules/@babel/template": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
"integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==",
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.22.5",
"@babel/parser": "^7.22.5",
"@babel/types": "^7.22.5"
"@babel/code-frame": "^7.22.13",
"@babel/parser": "^7.22.15",
"@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz",
"integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==",
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz",
"integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.22.5",
"@babel/generator": "^7.22.5",
"@babel/helper-environment-visitor": "^7.22.5",
"@babel/helper-function-name": "^7.22.5",
"@babel/code-frame": "^7.22.13",
"@babel/generator": "^7.23.3",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.5",
"@babel/parser": "^7.22.5",
"@babel/types": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.23.3",
"@babel/types": "^7.23.3",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -567,13 +638,13 @@
}
},
"node_modules/@babel/types": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz",
"integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==",
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz",
"integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
@@ -613,9 +684,9 @@
}
},
"node_modules/@cypress/request": {
"version": "2.88.10",
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz",
"integrity": "sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==",
"version": "2.88.12",
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz",
"integrity": "sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==",
"dev": true,
"dependencies": {
"aws-sign2": "~0.7.0",
@@ -631,9 +702,9 @@
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"qs": "~6.10.3",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.5.0",
"tough-cookie": "^4.1.3",
"tunnel-agent": "^0.6.0",
"uuid": "^8.3.2"
},
@@ -653,15 +724,6 @@
"node": ">= 0.6"
}
},
"node_modules/@cypress/request/node_modules/qs": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
"dev": true,
"engines": {
"node": ">=0.6"
}
},
"node_modules/@cypress/xvfb": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
@@ -900,12 +962,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 +1070,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",
@@ -2449,9 +2496,9 @@
"license": "MIT"
},
"node_modules/cypress": {
"version": "10.8.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.8.0.tgz",
"integrity": "sha512-QVse0dnLm018hgti2enKMVZR9qbIO488YGX06nH5j3Dg1isL38DwrBtyrax02CANU6y8F4EJUuyW6HJKw1jsFA==",
"version": "10.11.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.11.0.tgz",
"integrity": "sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@@ -5453,7 +5500,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.14",
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true,
"funding": [
{
@@ -5463,11 +5512,14 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.4",
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
@@ -5476,10 +5528,16 @@
}
},
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -5588,6 +5646,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"dev": true
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"dev": true,
@@ -5872,6 +5936,12 @@
"node": ">=0.10.0"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true
},
"node_modules/resolve": {
"version": "1.22.1",
"dev": true,
@@ -6034,9 +6104,10 @@
}
},
"node_modules/semver": {
"version": "6.3.0",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
@@ -6514,16 +6585,27 @@
}
},
"node_modules/tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
"dev": true,
"dependencies": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=0.8"
"node": ">=6"
}
},
"node_modules/tough-cookie/node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"dev": true,
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/ts-node": {
@@ -6774,6 +6856,16 @@
"punycode": "^2.1.0"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"dev": true,
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -6867,9 +6959,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",
@@ -7047,12 +7139,71 @@
}
},
"@babel/code-frame": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz",
"integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==",
"version": "7.22.13",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
"integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
"dev": true,
"requires": {
"@babel/highlight": "^7.22.5"
"@babel/highlight": "^7.22.13",
"chalk": "^2.4.2"
},
"dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"@babel/compat-data": {
@@ -7085,12 +7236,12 @@
}
},
"@babel/generator": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz",
"integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==",
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz",
"integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==",
"dev": true,
"requires": {
"@babel/types": "^7.22.5",
"@babel/types": "^7.23.3",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -7147,19 +7298,19 @@
}
},
"@babel/helper-environment-visitor": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz",
"integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true
},
"@babel/helper-function-name": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz",
"integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"requires": {
"@babel/template": "^7.22.5",
"@babel/types": "^7.22.5"
"@babel/template": "^7.22.15",
"@babel/types": "^7.23.0"
}
},
"@babel/helper-hoist-variables": {
@@ -7210,9 +7361,9 @@
}
},
"@babel/helper-split-export-declaration": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz",
"integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==",
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
"integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
"dev": true,
"requires": {
"@babel/types": "^7.22.5"
@@ -7225,9 +7376,9 @@
"dev": true
},
"@babel/helper-validator-identifier": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz",
"integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true
},
"@babel/helper-validator-option": {
@@ -7248,13 +7399,13 @@
}
},
"@babel/highlight": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz",
"integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
"integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.22.5",
"chalk": "^2.0.0",
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"dependencies": {
@@ -7317,9 +7468,9 @@
}
},
"@babel/parser": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz",
"integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==",
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz",
"integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==",
"dev": true
},
"@babel/plugin-syntax-jsx": {
@@ -7370,30 +7521,30 @@
}
},
"@babel/template": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
"integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==",
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.22.5",
"@babel/parser": "^7.22.5",
"@babel/types": "^7.22.5"
"@babel/code-frame": "^7.22.13",
"@babel/parser": "^7.22.15",
"@babel/types": "^7.22.15"
}
},
"@babel/traverse": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz",
"integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==",
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz",
"integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.22.5",
"@babel/generator": "^7.22.5",
"@babel/helper-environment-visitor": "^7.22.5",
"@babel/helper-function-name": "^7.22.5",
"@babel/code-frame": "^7.22.13",
"@babel/generator": "^7.23.3",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.5",
"@babel/parser": "^7.22.5",
"@babel/types": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.23.3",
"@babel/types": "^7.23.3",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -7407,13 +7558,13 @@
}
},
"@babel/types": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz",
"integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==",
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz",
"integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==",
"dev": true,
"requires": {
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
}
},
@@ -7446,9 +7597,9 @@
}
},
"@cypress/request": {
"version": "2.88.10",
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz",
"integrity": "sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==",
"version": "2.88.12",
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz",
"integrity": "sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==",
"dev": true,
"requires": {
"aws-sign2": "~0.7.0",
@@ -7464,9 +7615,9 @@
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"qs": "~6.10.3",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.5.0",
"tough-cookie": "^4.1.3",
"tunnel-agent": "^0.6.0",
"uuid": "^8.3.2"
},
@@ -7479,12 +7630,6 @@
"requires": {
"mime-db": "1.52.0"
}
},
"qs": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
"dev": true
}
}
},
@@ -7678,12 +7823,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 +7926,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",
@@ -8684,9 +8814,9 @@
"devOptional": true
},
"cypress": {
"version": "10.8.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.8.0.tgz",
"integrity": "sha512-QVse0dnLm018hgti2enKMVZR9qbIO488YGX06nH5j3Dg1isL38DwrBtyrax02CANU6y8F4EJUuyW6HJKw1jsFA==",
"version": "10.11.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.11.0.tgz",
"integrity": "sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA==",
"dev": true,
"requires": {
"@cypress/request": "^2.88.10",
@@ -10747,18 +10877,20 @@
"dev": true
},
"postcss": {
"version": "8.4.14",
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true,
"requires": {
"nanoid": "^3.3.4",
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"dependencies": {
"nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true
}
}
@@ -10830,6 +10962,12 @@
"side-channel": "^1.0.4"
}
},
"querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"dev": true
},
"queue-microtask": {
"version": "1.2.3",
"dev": true
@@ -11004,6 +11142,12 @@
"version": "2.0.2",
"dev": true
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true
},
"resolve": {
"version": "1.22.1",
"dev": true,
@@ -11104,7 +11248,9 @@
}
},
"semver": {
"version": "6.3.0",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true
},
"send": {
@@ -11440,13 +11586,23 @@
"dev": true
},
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
"dev": true,
"requires": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"dependencies": {
"universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"dev": true
}
}
},
"ts-node": {
@@ -11606,6 +11762,16 @@
"punycode": "^2.1.0"
}
},
"url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"dev": true,
"requires": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -11657,9 +11823,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",

View File

@@ -1,7 +1,7 @@
{
"name": "jambonz-webapp",
"description": "A simple provisioning web app for jambonz",
"version": "0.8.4",
"version": "0.8.5",
"license": "MIT",
"type": "module",
"engines": {
@@ -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",

View File

@@ -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,19 @@ export const AUDIO_FORMAT_OPTIONS = [
value: "wav",
},
];
export const DEFAULT_ELEVENLABS_MODEL = "eleven_multilingual_v2";
export const DEFAULT_WHISPER_MODEL = "tts-1";
// Google Custom Voice reported usage options
export const DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE = "REALTIME";
export const GOOGLE_CUSTOM_VOICES_REPORTED_USAGE = [
{ name: "REPORTED_USAGE_UNSPECIFIED", value: "REPORTED_USAGE_UNSPECIFIED" },
{ name: "REALTIME", value: "REALTIME" },
{ name: "OFFLINE", value: "OFFLINE" },
];
/** Password Length options */
export const PASSWORD_MIN = 8;
@@ -334,3 +357,4 @@ export const API_PRICE = `${API_BASE_URL}/Prices`;
export const API_SUBSCRIPTIONS = `${API_BASE_URL}/Subscriptions`;
export const API_CHANGE_PASSWORD = `${API_BASE_URL}/change-password`;
export const API_SIGNIN = `${API_BASE_URL}/signin`;
export const API_GOOGLE_CUSTOM_VOICES = `${API_BASE_URL}/GoogleCustomVoices`;

View File

@@ -33,6 +33,7 @@ import {
API_SUBSCRIPTIONS,
API_CHANGE_PASSWORD,
API_SIGNIN,
API_GOOGLE_CUSTOM_VOICES,
} from "./constants";
import { ROUTE_LOGIN } from "src/router/routes";
import {
@@ -90,6 +91,12 @@ import type {
DeleteAccount,
ChangePassword,
SignIn,
GetVoices,
LanguageOption,
VoiceOption,
GetLanguages,
GoogleCustomVoice,
GoogleCustomVoicesQuery,
} from "./types";
import { Availability, StatusCodes } from "./types";
import { JaegerRoot } from "./jaeger-types";
@@ -329,6 +336,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,
@@ -424,6 +457,16 @@ export const postLcr = (payload: Partial<Lcr>) => {
return postFetch<SidResponse, Partial<Lcr>>(API_LCRS, payload);
};
export const postLcrCreateRoutes = (
sid: string,
payload: Partial<LcrRoute[]>
) => {
return postFetch<EmptyResponse, Partial<LcrRoute[]>>(
`${API_LCRS}/${sid}/Routes`,
payload
);
};
export const postLcrRoute = (payload: Partial<LcrRoute>) => {
return postFetch<SidResponse, Partial<LcrRoute>>(API_LCR_ROUTES, payload);
};
@@ -471,6 +514,13 @@ export const postChangepassword = (payload: Partial<ChangePassword>) => {
export const postSignIn = (payload: Partial<SignIn>) => {
return postFetch<SignIn, Partial<SignIn>>(API_SIGNIN, payload);
};
export const postGoogleCustomVoice = (payload: Partial<GoogleCustomVoice>) => {
return postFetch<SidResponse, Partial<GoogleCustomVoice>>(
API_GOOGLE_CUSTOM_VOICES,
payload
);
};
/** Named wrappers for `putFetch` */
export const putUser = (sid: string, payload: Partial<UserUpdatePayload>) => {
@@ -567,6 +617,16 @@ export const putLcr = (sid: string, payload: Partial<Lcr>) => {
return putFetch<EmptyResponse, Partial<Lcr>>(`${API_LCRS}/${sid}`, payload);
};
export const putLcrUpdateRoutes = (
sid: string,
payload: Partial<LcrRoute[]>
) => {
return putFetch<EmptyResponse, Partial<LcrRoute[]>>(
`${API_LCRS}/${sid}/Routes`,
payload
);
};
export const putLcrRoutes = (sid: string, payload: Partial<LcrRoute>) => {
return putFetch<EmptyResponse, Partial<LcrRoute>>(
`${API_LCR_ROUTES}/${sid}`,
@@ -600,6 +660,17 @@ export const putActivationCode = (
payload
);
};
export const putGoogleCustomVoice = (
sid: string,
payload: Partial<GoogleCustomVoice>
) => {
return putFetch<EmptyResponse, Partial<GoogleCustomVoice>>(
`${API_GOOGLE_CUSTOM_VOICES}/${sid}`,
payload
);
};
/** Named wrappers for `deleteFetch` */
export const deleteUser = (sid: string) => {
@@ -685,6 +756,14 @@ 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);
};
export const deleteGoogleCustomVoice = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_GOOGLE_CUSTOM_VOICES}/${sid}`);
};
/** Named wrappers for `getFetch` */
export const getUser = (sid: string) => {
@@ -737,6 +816,13 @@ export const getAvailability = (domain: string) => {
);
};
export const getGoogleCustomVoices = (
query: Partial<GoogleCustomVoicesQuery>
) => {
const qryStr = getQuery<Partial<GoogleCustomVoicesQuery>>(query);
return getFetch<GoogleCustomVoice[]>(`${API_GOOGLE_CUSTOM_VOICES}?${qryStr}`);
};
/** Wrappers for APIs that can have a mock dev server response */
export const getMe = () => {

View File

@@ -37,14 +37,15 @@ export interface JaegerAttribute {
value: JaegerValue;
}
export interface WaveSufferSttResult {
export interface WaveSurferSttResult {
vendor: string;
transcript: string;
confidence: number;
language_code: string;
latency?: number;
}
export interface WaveSufferDtmfResult {
export interface WaveSurferDtmfResult {
dtmf: string;
duration: string;
}

View File

@@ -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 {
@@ -360,6 +372,14 @@ export interface RecentCall {
recording_url?: string;
}
export interface GoogleCustomVoice {
google_custom_voice_sid?: string;
speech_credential_sid?: string;
name: string;
reported_usage: string;
model: string;
}
export interface SpeechCredential {
speech_credential_sid: string;
service_provider_sid: null | string;
@@ -375,8 +395,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 +413,10 @@ 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;
model: null | string;
}
export interface Alert {
@@ -444,7 +470,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 +479,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 {
@@ -476,7 +504,7 @@ export interface LcrRoute {
lcr_route_sid?: null | string;
lcr_sid: null | string;
regex: null | string;
desciption?: null | string;
description?: null | string;
priority: number;
lcr_carrier_set_entries?: LcrCarrierSetEntry[];
}
@@ -495,6 +523,9 @@ export interface Client {
username: null | string;
password?: null | string;
is_active: boolean;
allow_direct_app_calling: boolean;
allow_direct_queue_calling: boolean;
allow_direct_user_calling: boolean;
}
export interface PageQuery {
@@ -509,6 +540,13 @@ export interface CallQuery extends PageQuery {
answered?: string;
}
export interface GoogleCustomVoicesQuery {
speech_credential_sid?: string;
label?: string;
account_sid?: string;
service_provider_sid: string;
}
export interface PagedResponse<Type> {
page_size: number;
total: number;
@@ -657,3 +695,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;
}

View File

@@ -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>
))}

View File

@@ -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,
};

View File

@@ -24,4 +24,3 @@ export const MSG_WEBHOOK_FIELDS = (
<span>password</span> fields are required.
</>
);
export const NOT_AVAILABLE_PREFIX = "NotAvalable";

View File

@@ -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);
}
@@ -656,7 +693,9 @@ export const AccountForm = ({
{isDeleteAccount && (
<Section slim>
<form
className="form form--internal"
className={`form form--internal ${
!account?.data && account?.refetch ? "form--blur" : ""
}`}
onSubmit={handleDeleteAccount}
>
<fieldset>
@@ -1012,43 +1051,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 +1175,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 +1272,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

View File

@@ -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]}

View File

@@ -148,7 +148,11 @@ export const ManagePaymentForm = () => {
<div className="grid__row">
<div></div>
<div>
<PaymentElement />
<PaymentElement
options={{
paymentMethodOrder: ["card"],
}}
/>
</div>
</div>
</div>

View File

@@ -577,7 +577,11 @@ const SubscriptionForm = () => {
<div className="grid__row">
<div></div>
<div>
<PaymentElement />
<PaymentElement
options={{
paymentMethodOrder: ["card"],
}}
/>
</div>
</div>
</fieldset>

View File

@@ -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 (

View File

@@ -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>;
@@ -68,6 +63,7 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
const navigate = useNavigate();
const { synthesis, recognizers } = useSpeechVendors();
const user = useSelectState("user");
const currentServiceProvider = useSelectState("currentServiceProvider");
const [accounts] = useServiceProviderData<Account[]>("Accounts");
const [applications] = useApiData<Application[]>("Applications");
const [applicationName, setApplicationName] = useState("");
@@ -98,10 +94,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 +199,7 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
}
}
const payload = {
const payload: Partial<Application> = {
name: applicationName,
app_json: applicationJson || null,
call_hook: callWebhook || null,
@@ -180,10 +208,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 +263,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 +275,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 +287,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,12 +464,98 @@ 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}>
<form
className={`form form--internal ${
!application?.data && application?.refetch ? "form--blur" : ""
}`}
onSubmit={handleSubmit}
>
<fieldset>
<MS>{MSG_REQUIRED_FIELDS}</MS>
</fieldset>
@@ -450,216 +679,89 @@ 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
serviceProviderSid={
currentServiceProvider?.service_provider_sid || ""
}
accountSid={accountSid}
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
serviceProviderSid={
currentServiceProvider?.service_provider_sid || ""
}
accountSid={accountSid}
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>

View File

@@ -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]}

View File

@@ -0,0 +1,469 @@
import React, { useEffect, useRef, useState } from "react";
import {
getGoogleCustomVoices,
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_ASSEMBLYAI,
VENDOR_ELEVENLABS,
VENDOR_GOOGLE,
VENDOR_MICROSOFT,
VENDOR_SONIOX,
VENDOR_WELLSAID,
VENDOR_WHISPER,
} from "src/vendor";
import {
LabelOptions,
Language,
RecognizerVendors,
SynthesisVendors,
VendorOptions,
Voice,
VoiceLanguage,
} from "src/vendor/types";
type SpeechProviderSelectionProbs = {
accountSid: string;
serviceProviderSid: string;
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 = ({
accountSid,
serviceProviderSid,
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");
const currentVendor = useRef(synthVendor);
useEffect(() => {
currentVendor.current = synthVendor;
if (!synthesis) {
return;
}
const voiceOpts = synthesis[synthVendor as keyof SynthesisVendors]
.filter((lang: VoiceLanguage) => {
// ELEVENLABS has same voice for all lange, take voices from the 1st language
// Only first language has voices, the rest has empty voices
if (synthVendor === VENDOR_ELEVENLABS && lang.voices.length > 0) {
return true;
}
return lang.code === synthLang;
})
.flatMap((lang: VoiceLanguage) =>
lang.voices.map((voice: Voice) => ({
name: voice.name,
value: voice.value,
}))
) as Voice[];
setSynthesisVoiceOptions(voiceOpts);
const langOpts = synthesis[synthVendor as keyof SynthesisVendors].map(
(lang: VoiceLanguage) => ({
name: lang.name,
value: lang.code,
})
);
setSynthesisLanguageOptions(langOpts);
if (synthVendor === VENDOR_ELEVENLABS) {
postSpeechServiceVoices(
currentServiceProvider
? currentServiceProvider.service_provider_sid
: "",
{
vendor: synthVendor,
label: synthLabel,
}
).then(({ json }) => {
// If after successfully fetching data, vendor is still good, then apply value
if (currentVendor.current !== VENDOR_ELEVENLABS) {
return;
}
if (json.length > 0) {
setSynthesisVoiceOptions(json);
}
});
postSpeechServiceLanguages(
currentServiceProvider
? currentServiceProvider.service_provider_sid
: "",
{
vendor: synthVendor,
label: synthLabel,
}
).then(({ json }) => {
if (json.length > 0) {
setSynthesisLanguageOptions(json);
}
});
} else if (synthVendor === VENDOR_GOOGLE) {
getGoogleCustomVoices({
...(synthLabel && { label: synthLabel }),
account_sid: accountSid,
service_provider_sid: serviceProviderSid,
}).then(({ json }) => {
// If after successfully fetching data, vendor is still good, then apply value
if (currentVendor.current !== VENDOR_GOOGLE) {
return;
}
const customVOices = json.map((v) => ({
name: `${v.name} (Custom)`,
value: `custom_${v.google_custom_voice_sid}`,
}));
const options = synthesis[synthVendor as keyof SynthesisVendors]
.filter((lang: VoiceLanguage) => {
return lang.code === synthLang;
})
.flatMap((lang: VoiceLanguage) =>
lang.voices.map((voice: Voice) => ({
name: voice.name,
value: voice.value,
}))
) as Voice[];
setSynthesisVoiceOptions([...customVOices, ...options]);
if (customVOices.length > 0) {
setSynthVoice(customVOices[0].value);
}
});
}
}, [synthVendor, synthesis, synthLabel, accountSid, serviceProviderSid]);
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_ASSEMBLYAI &&
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) {
// Samve Voices applied to all languages
// Voices are only available for the 1st language.
setSynthLang(ELEVENLABS_LANG_EN);
setSynthVoice(synthesis[vendor][0].voices[0].value);
return;
}
if (vendor === VENDOR_WHISPER) {
const newLang = synthesis[vendor].find(
(lang) => lang.code === LANG_EN_US
);
setSynthLang(LANG_EN_US);
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_ELEVENLABS &&
vendor.value != VENDOR_WHISPER &&
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;

View File

@@ -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
)
);
};
@@ -320,10 +336,13 @@ export const CarrierForm = ({
const gateway = sipGateways[i];
const type = getIpValidationType(gateway.ipv4);
/** DH: unclear why we had this restriction, removing for now
if (type === FQDN_TOP_LEVEL) {
refSipIp.current[i].focus();
return "When using an FQDN, you must use a subdomain (e.g. sip.example.com).";
} else if (type === FQDN && (!gateway.outbound || gateway.inbound)) {
*/
if (type === FQDN && (!gateway.outbound || gateway.inbound)) {
refSipIp.current[i].focus();
return "A fully qualified domain name may only be used for outbound calls.";
} else if (type === INVALID) {
@@ -409,7 +428,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 */
@@ -618,7 +639,12 @@ export const CarrierForm = ({
return (
<Section slim>
<form className="form form--internal" onSubmit={handleSubmit}>
<form
className={`form form--internal ${
!carrier?.data && carrier?.refetch ? "form--blur" : ""
}`}
onSubmit={handleSubmit}
>
<fieldset>
<MS>{MSG_REQUIRED_FIELDS}</MS>
</fieldset>
@@ -962,13 +988,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 +1098,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

View File

@@ -155,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]}

View File

@@ -10,6 +10,13 @@ type CarrierProps = {
};
export const RegisterStatus = ({ carrier }: CarrierProps) => {
const getReason = () => {
return carrier.register_status.reason
? typeof carrier.register_status.reason === "string"
? carrier.register_status.reason
: "Not Started"
: "Not Started";
};
const renderStatus = () => {
return (
<div
@@ -20,7 +27,7 @@ export const RegisterStatus = ({ carrier }: CarrierProps) => {
: "jam"
: "jean"
}`}
title={carrier.register_status.reason || "Not Started"}
title={getReason()}
>
{carrier.register_status.status === CARRIER_REG_OK ? (
<Icons.CheckCircle />
@@ -40,8 +47,7 @@ export const RegisterStatus = ({ carrier }: CarrierProps) => {
<details className={carrier.register_status.status || "not-tested"}>
<summary>{renderStatus()}</summary>
<MS>
<strong>Reason:</strong>{" "}
{carrier.register_status.reason || "Not Started"}
<strong>Reason:</strong> {getReason()}
</MS>
<PcapButton
accountSid={carrier.account_sid || ""}

View File

@@ -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 />
</>
);

View File

@@ -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>

View File

@@ -1,13 +1,15 @@
import { H1 } from "@jambonz/ui-kit";
import React, { useEffect } from "react";
import { useParams } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom";
import { useApiData } from "src/api";
import { Client } from "src/api/types";
import { toastError } from "src/store";
import ClientsForm from "./form";
import { ROUTE_INTERNAL_CLIENTS } from "src/router/routes";
export const ClientsEdit = () => {
const params = useParams();
const navigate = useNavigate();
const [data, refetch, error] = useApiData<Client>(
`Clients/${params.client_sid}`
);
@@ -16,12 +18,13 @@ export const ClientsEdit = () => {
useEffect(() => {
if (error) {
toastError(error.msg);
navigate(ROUTE_INTERNAL_CLIENTS);
}
}, [error]);
return (
<>
<H1 className="h2">Edit client</H1>
<H1 className="h2">Edit sip client</H1>
<ClientsForm client={{ data, refetch, error }} />
</>
);

View File

@@ -9,7 +9,7 @@ import {
} from "src/api";
import { USER_ACCOUNT } from "src/api/constants";
import { Account, Client, UseApiDataMap } from "src/api/types";
import { Section } from "src/components";
import { Section, Tooltip } from "src/components";
import { AccountSelect, Message, Passwd } from "src/components/forms";
import { MSG_REQUIRED_FIELDS } from "src/constants";
import { ROUTE_INTERNAL_CLIENTS } from "src/router/routes";
@@ -30,7 +30,12 @@ export const ClientsForm = ({ client }: ClientsFormProps) => {
const [accountSid, setAccountSid] = useState("");
const [password, setPassword] = useState("");
const [username, setUsername] = useState("");
const [isActive, setIsActive] = useState(true);
const [isActive, setIsActive] = useState(
client ? client.data?.is_active : true
);
const [allowDirectAppCalling, setAllowDirectAppCalling] = useState(true);
const [allowDirectQueueCalling, setAllowDirectQueueCalling] = useState(true);
const [allowDirectUserCalling, setAllowDirectUserCalling] = useState(true);
const [modal, setModal] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const handleSubmit = (e: React.FormEvent) => {
@@ -42,6 +47,9 @@ export const ClientsForm = ({ client }: ClientsFormProps) => {
username: username,
password: password,
is_active: isActive,
allow_direct_app_calling: allowDirectAppCalling,
allow_direct_queue_calling: allowDirectQueueCalling,
allow_direct_user_calling: allowDirectUserCalling,
})
.then(() => {
toastSuccess("Client created successfully");
@@ -52,10 +60,10 @@ export const ClientsForm = ({ client }: ClientsFormProps) => {
});
} else {
putClient(client.data?.client_sid || "", {
account_sid: accountSid,
username: username,
...(password && { password: password }),
is_active: isActive,
allow_direct_app_calling: allowDirectAppCalling,
allow_direct_queue_calling: allowDirectQueueCalling,
allow_direct_user_calling: allowDirectUserCalling,
})
.then(() => {
toastSuccess("Client updated successfully");
@@ -99,6 +107,9 @@ export const ClientsForm = ({ client }: ClientsFormProps) => {
}
setIsActive(client.data.is_active);
setAllowDirectAppCalling(client.data.allow_direct_app_calling);
setAllowDirectQueueCalling(client.data.allow_direct_queue_calling);
setAllowDirectUserCalling(client.data.allow_direct_user_calling);
}
}, [client]);
@@ -107,8 +118,6 @@ export const ClientsForm = ({ client }: ClientsFormProps) => {
if (!accountSid || !accounts || !acc) return;
if (!acc?.sip_realm) {
setErrorMessage(`Sip realm is not set for the account.`);
} else if (!acc?.device_calling_application_sid) {
setErrorMessage(`Device calling application is not set for the account.`);
} else {
setErrorMessage("");
}
@@ -116,7 +125,12 @@ export const ClientsForm = ({ client }: ClientsFormProps) => {
return (
<>
<Section slim>
<form className="form form--internal" onSubmit={handleSubmit}>
<form
className={`form form--internal ${
!client?.data && client?.refetch ? "form--blur" : ""
}`}
onSubmit={handleSubmit}
>
<fieldset>
<MS>{MSG_REQUIRED_FIELDS}</MS>
{errorMessage && <Message message={errorMessage} />}
@@ -134,22 +148,12 @@ export const ClientsForm = ({ client }: ClientsFormProps) => {
placeholder="user name"
value={username}
required={true}
disabled={hasValue(client)}
autoComplete="off"
onChange={(e) => setUsername(e.target.value)}
/>
</div>
</div>
<label htmlFor="is_active" className="chk">
<input
id="is_active"
name="is_active"
type="checkbox"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
/>
<div>Active</div>
</label>
</fieldset>
<fieldset>
<label htmlFor="password">
Password{!hasValue(client) && <span>*</span>}
</label>
@@ -160,14 +164,67 @@ export const ClientsForm = ({ client }: ClientsFormProps) => {
value={password}
placeholder="Password"
setValue={setPassword}
disabled={hasValue(client)}
autoComplete="off"
/>
</fieldset>
<fieldset>
<label htmlFor="is_active" className="chk">
<input
id="is_active"
name="is_active"
type="checkbox"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
/>
<div>Active</div>
</label>
<label htmlFor="allow_direct_app_calling" className="chk">
<input
id="allow_direct_app_calling"
name="allow_direct_app_calling"
type="checkbox"
checked={allowDirectAppCalling}
onChange={(e) => setAllowDirectAppCalling(e.target.checked)}
/>
<div>Allow direct calling to applications</div>
<Tooltip text="Allow user to call applications without configuring an application for sip device calls.">
{" "}
</Tooltip>
</label>
<label htmlFor="allow_direct_queue_calling" className="chk">
<input
id="allow_direct_queue_calling"
name="allow_direct_queue_calling"
type="checkbox"
checked={allowDirectQueueCalling}
onChange={(e) => setAllowDirectQueueCalling(e.target.checked)}
/>
<div>Allow direct calling to queues</div>
<Tooltip text="Allow user to take calls from queues without configuring an application for sip device calls.">
{" "}
</Tooltip>
</label>
<label htmlFor="allow_direct_user_calling" className="chk">
<input
id="allow_direct_user_calling"
name="allow_direct_user_calling"
type="checkbox"
checked={allowDirectUserCalling}
onChange={(e) => setAllowDirectUserCalling(e.target.checked)}
/>
<div>Allow direct calling to other users</div>
<Tooltip text="Allow user to call other users without configuring an application for sip device calls.">
{" "}
</Tooltip>
</label>
</fieldset>
{user?.scope !== USER_ACCOUNT && (
<fieldset>
<AccountSelect
accounts={accounts}
account={[accountSid, setAccountSid]}
label="Used by"
label="Belongs to"
required={true}
defaultOption={false}
disabled={hasValue(client)}

View File

@@ -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 && (

View File

@@ -6,7 +6,6 @@ import update from "immutability-helper";
import { deleteLcrRoute } from "src/api";
import { toastError, toastSuccess } from "src/store";
import { SelectorOption } from "src/components/forms/selector";
import { NOT_AVAILABLE_PREFIX } from "src/constants";
type ContainerProps = {
lcrRoute: [LcrRoute[], React.Dispatch<React.SetStateAction<LcrRoute[]>>];
@@ -61,11 +60,7 @@ export const Container = ({
};
const handleRouteDelete = (r: LcrRoute | undefined, index: number) => {
if (
r &&
r.lcr_route_sid &&
!r.lcr_route_sid.startsWith(NOT_AVAILABLE_PREFIX)
) {
if (r && r.lcr_route_sid) {
deleteLcrRoute(r.lcr_route_sid)
.then(() => {
toastSuccess("Least cost routing rule successfully deleted");
@@ -82,7 +77,7 @@ export const Container = ({
{hasLength(lcrRoutes) &&
lcrRoutes.map((lr, i) => (
<Card
key={lr.lcr_route_sid}
key={lr.lcr_route_sid || i}
lr={lr}
index={i}
moveCard={moveCard}

View File

@@ -8,37 +8,33 @@ import {
useDispatch,
useSelectState,
} from "src/store";
import { MSG_REQUIRED_FIELDS, NOT_AVAILABLE_PREFIX } from "src/constants";
import { MSG_REQUIRED_FIELDS } from "src/constants";
import { setLocation } from "src/store/localStore";
import { AccountSelect, Message, Selector } from "src/components/forms";
import type {
Account,
Carrier,
Lcr,
LcrCarrierSetEntry,
LcrRoute,
UseApiDataMap,
} from "src/api/types";
import { ROUTE_INTERNAL_LEST_COST_ROUTING } from "src/router/routes";
import {
deleteLcr,
postLcrCarrierSetEntry,
putLcrCarrierSetEntries,
putLcrRoutes,
putLcr,
postLcrCreateRoutes,
putLcrUpdateRoutes,
useApiData,
useServiceProviderData,
getLcrRoute,
} from "src/api";
import { USER_ACCOUNT, USER_ADMIN } from "src/api/constants";
import { postLcr } from "src/api";
import { postLcrRoute } from "src/api";
import DeleteLcr from "./delete";
import { Scope } from "src/store/types";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import Container from "./container";
import { v4 } from "uuid";
import { hasValue } from "src/utils";
type LcrFormProps = {
lcrDataMap?: UseApiDataMap<Lcr>;
@@ -47,7 +43,7 @@ type LcrFormProps = {
export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
const LCR_ROUTE_TEMPLATE: LcrRoute = {
lcr_route_sid: `${NOT_AVAILABLE_PREFIX}${v4()}`,
lcr_route_sid: "",
regex: "",
lcr_sid: "",
priority: 0,
@@ -68,7 +64,7 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
const [defaultLcrCarrier, setDefaultLcrCarrier] = useState("");
const [defaultLcrCarrierSetEntrySid, setDefaultLcrCarrierSetEntrySid] =
useState<string | null>();
const [defaultLcrRouteSid, setDefaultLcrRouteSid] = useState("");
const [defaultLcrRoute, setDefaultLcrRoute] = useState<LcrRoute | null>(null);
const [defaultCarrier, setDefaultCarrier] = useState("");
const [apiUrl, setApiUrl] = useState("");
const [accountSid, setAccountSid] = useState("");
@@ -136,44 +132,53 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
setPreviousLcr(lcrDataMap.data);
}
if (
lcrRouteDataMap &&
lcrRouteDataMap.data &&
lcrRouteDataMap.data !== previousLcrRoutes
) {
setPreviousLcrRoutes(lcrRouteDataMap.data);
// Find default carrier
lcrRouteDataMap.data.forEach((lr) => {
lr.lcr_carrier_set_entries?.forEach((entry) => {
if (
entry.lcr_carrier_set_entry_sid ===
lcrDataMap?.data?.default_carrier_set_entry_sid
) {
setDefaultLcrCarrier(entry.voip_carrier_sid || defaultCarrier);
setDefaultLcrCarrierSetEntrySid(
entry.lcr_carrier_set_entry_sid || null
);
setDefaultLcrRouteSid(entry.lcr_route_sid || "");
}
});
});
}
useMemo(() => {
let default_lcr_route_sid = "";
if (
lcrRouteDataMap &&
lcrRouteDataMap.data &&
lcrRouteDataMap.data !== previousLcrRoutes
) {
setPreviousLcrRoutes(lcrRouteDataMap.data);
// Find default carrier
lcrRouteDataMap.data.forEach((lr) => {
lr.lcr_carrier_set_entries?.forEach((entry) => {
if (
entry.lcr_carrier_set_entry_sid ===
lcrDataMap?.data?.default_carrier_set_entry_sid
) {
setDefaultLcrCarrier(entry.voip_carrier_sid || defaultCarrier);
setDefaultLcrCarrierSetEntrySid(
entry.lcr_carrier_set_entry_sid || null
);
default_lcr_route_sid = entry.lcr_route_sid || "";
setDefaultLcrRoute(lr);
}
});
});
}
if (lcrRouteDataMap && lcrRouteDataMap.data)
setLcrRoutes(
lcrRouteDataMap.data.filter(
(route) => route.lcr_route_sid !== defaultLcrRouteSid
(route) => route.lcr_route_sid !== default_lcr_route_sid
)
);
}, [defaultLcrRouteSid]);
}, [lcrRouteDataMap?.data]);
const addLcrRoutes = () => {
const newLcrRoute = LCR_ROUTE_TEMPLATE;
const ls = [
...lcrRoutes,
{
...LCR_ROUTE_TEMPLATE,
...newLcrRoute,
priority: lcrRoutes.length,
lcr_carrier_set_entries: newLcrRoute.lcr_carrier_set_entries?.map(
(r) => ({
...r,
voip_carrier_sid: defaultCarrier || carrierSelectorOptions[0].value,
})
),
},
];
setLcrRoutes(ls);
@@ -194,11 +199,48 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
const lcrPayload: Lcr = getLcrPayload();
postLcr(lcrPayload)
.then(({ json }) => {
Promise.all(
lcrRoutes.map((route, i) => handleLcrRoutePost(json.sid, route, i))
)
const lcrsPayload = lcrRoutes.map((l, i) => ({
...l,
lcr_carrier_set_entries: l.lcr_carrier_set_entries?.map((e) => ({
...e,
voip_carrier_sid:
e.voip_carrier_sid ||
defaultCarrier ||
carrierSelectorOptions[0]?.value,
})),
lcr_sid: json.sid,
priority: i,
}));
lcrsPayload.push({
lcr_sid: json.sid,
regex: ".*",
description: "System Default Route",
priority: 9999,
lcr_carrier_set_entries: [
{
lcr_route_sid: "",
voip_carrier_sid:
defaultLcrCarrier || carrierSelectorOptions[0]?.value,
priority: 0,
},
],
});
postLcrCreateRoutes(json.sid, lcrsPayload)
.then(() => {
handleLcrDefaultCarrierPost(json.sid);
if (lcrDataMap) {
toastSuccess("Least cost routing successfully updated");
} else {
toastSuccess("Least cost routing successfully created");
if (user?.access === Scope.admin) {
navigate(ROUTE_INTERNAL_LEST_COST_ROUTING);
} else {
navigate(
`${ROUTE_INTERNAL_LEST_COST_ROUTING}/${json.sid}/edit`
);
}
// Update global state
dispatch({ type: "lcr" });
}
})
.catch(({ msg }) => {
toastError(msg);
@@ -209,209 +251,38 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
});
};
const handleLcrDefaultCarrierPost = (lcr_sid: string) => {
const defaultRoute = {
lcr_sid: lcr_sid,
regex: ".*",
desciption: "System Default Route",
priority: 9999,
lcr_carrier_set_entries: [
{
lcr_route_sid: "",
voip_carrier_sid: defaultLcrCarrier,
priority: 0,
},
],
};
handleLcrRoutePost(lcr_sid, defaultRoute, 9999).then((lcr_route_sid) => {
// There is small hack here to wait the data commited in bd for lcr entries
new Promise(async (r) => setTimeout(() => r(0), 300)).then(() => {
getLcrRoute(lcr_route_sid).then(({ json }) => {
if (json.lcr_carrier_set_entries?.length) {
const lcr_carrier_set_entry_sid =
json.lcr_carrier_set_entries[0].lcr_carrier_set_entry_sid;
putLcr(lcr_sid, {
default_carrier_set_entry_sid: lcr_carrier_set_entry_sid,
})
.then(() => {
if (lcrDataMap) {
toastSuccess("Least cost routing successfully updated");
} else {
toastSuccess("Least cost routing successfully created");
if (user?.access === Scope.admin) {
navigate(ROUTE_INTERNAL_LEST_COST_ROUTING);
} else {
navigate(
`${ROUTE_INTERNAL_LEST_COST_ROUTING}/${lcr_sid}/edit`
);
}
// Update global state
dispatch({ type: "lcr" });
}
})
.catch((error) => {
toastError(error);
});
}
});
});
});
};
const handleLcrRoutePost = (
lcr_sid: string,
route: LcrRoute,
priority: number
): Promise<string> => {
return new Promise(async (resolve, reject) => {
const lcrRoutePayload: LcrRoute = {
lcr_sid,
regex: route.regex,
priority,
};
postLcrRoute(lcrRoutePayload)
.then(({ json }) => {
if (route.lcr_carrier_set_entries) {
Promise.all(
route.lcr_carrier_set_entries.map((entry) => {
handleLcrCarrierSetEntryPost(json.sid, entry);
})
)
.then(() => {
resolve(json.sid);
})
.catch((error) => {
reject(error);
});
}
})
.catch((error) => {
reject(error);
});
});
};
const handleLcrCarrierSetEntryPost = (
lcr_route_sid: string,
entry: LcrCarrierSetEntry
): Promise<string> => {
const lcrCarrierSetEntryPayload: LcrCarrierSetEntry = {
...entry,
voip_carrier_sid: entry.voip_carrier_sid || defaultCarrier,
lcr_route_sid,
};
return new Promise<string>(async (r, e) => {
postLcrCarrierSetEntry(lcrCarrierSetEntryPayload)
.then(({ json }) => r(json.sid))
.catch((error) => e(error));
});
};
const handleLcrPut = () => {
if (lcrDataMap && lcrDataMap.data && lcrDataMap.data.lcr_sid) {
// update LCR
const lcrPayload: Lcr = getLcrPayload();
putLcr(lcrDataMap.data.lcr_sid, lcrPayload).then(() => {
Promise.all(
lcrRoutes.map((route, i) => {
if (
route.lcr_route_sid &&
!route.lcr_route_sid?.startsWith(NOT_AVAILABLE_PREFIX)
) {
handleLcrRoutePut(
lcrDataMap.data?.lcr_sid || "",
route.lcr_route_sid,
route,
i
);
} else {
handleLcrRoutePost(lcrDataMap.data?.lcr_sid || "", route, i);
}
})
)
putLcrUpdateRoutes(lcrDataMap.data?.lcr_sid || "", [
...lcrRoutes.map((r, i) => ({
...r,
priority: i,
})),
...(hasValue(defaultLcrRoute)
? [
{
...defaultLcrRoute,
lcr_carrier_set_entries:
defaultLcrRoute.lcr_carrier_set_entries?.map((r) => ({
...r,
voip_carrier_sid:
defaultLcrCarrier ||
r.voip_carrier_sid ||
carrierSelectorOptions[0].value,
})),
},
]
: []),
])
.then(() => {
if (defaultLcrCarrierSetEntrySid) {
const defaultEntry: LcrCarrierSetEntry = {
lcr_route_sid: defaultLcrRouteSid,
voip_carrier_sid: defaultLcrCarrier,
priority: 0,
};
handleLcrCarrierEntryPut(
defaultLcrRouteSid,
defaultLcrCarrierSetEntrySid,
defaultEntry
).then(() => {
toastSuccess("Least cost routing rule successfully updated");
});
}
toastSuccess("Least cost routing rule successfully updated");
})
.catch((error) => toastError(error));
});
}
const handleLcrRoutePut = (
lcr_sid: string,
lcr_route_sid: string,
route: LcrRoute,
priority: number
): Promise<string> => {
return new Promise(async (resolve, reject) => {
const lcrRoutePayload: LcrRoute = {
lcr_sid,
regex: route.regex,
priority,
};
putLcrRoutes(lcr_route_sid, lcrRoutePayload).then(() => {
if (
route.lcr_carrier_set_entries &&
route.lcr_carrier_set_entries.length > 0
) {
Promise.all(
route.lcr_carrier_set_entries.map((entry) => {
if (entry.lcr_carrier_set_entry_sid) {
return handleLcrCarrierEntryPut(
entry.lcr_route_sid || lcr_route_sid,
entry.lcr_carrier_set_entry_sid,
entry
);
} else {
return handleLcrCarrierSetEntryPost(lcr_route_sid, entry);
}
})
)
.then(() => {
resolve("Least cost routing rule successfully updated");
})
.catch((error) => {
reject(error);
});
}
});
});
};
const handleLcrCarrierEntryPut = (
lcr_route_sid: string,
lcr_carrier_set_entry_sid: string,
entry: LcrCarrierSetEntry
): Promise<string> => {
const lcrCarrierSetEntryPayload: LcrCarrierSetEntry = {
lcr_route_sid,
workload: entry.workload,
voip_carrier_sid: entry.voip_carrier_sid || defaultCarrier,
priority: entry.priority,
};
return new Promise<string>(async (r, e) => {
putLcrCarrierSetEntries(
lcr_carrier_set_entry_sid,
lcrCarrierSetEntryPayload
)
.then(() => r("success"))
.catch((error) => e(error));
});
};
};
const handleSubmit = (e: React.FormEvent) => {
@@ -449,7 +320,12 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
return (
<>
<Section slim>
<form className="form form--internal" onSubmit={handleSubmit}>
<form
className={`form form--internal ${
!lcrDataMap?.data && lcrDataMap?.refetch ? "form--blur" : ""
}`}
onSubmit={handleSubmit}
>
<fieldset>
<MS>{MSG_REQUIRED_FIELDS}</MS>
{errorMessage && <Message message={errorMessage} />}

View File

@@ -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

View File

@@ -120,7 +120,12 @@ export const MsTeamsTenantForm = ({
return (
<Section slim>
<form className="form form--internal" onSubmit={handleSubmit}>
<form
className={`form form--internal ${
!msTeamsTenant?.data && msTeamsTenant?.refetch ? "form--blur" : ""
}`}
onSubmit={handleSubmit}
>
<fieldset>
<MS>{MSG_REQUIRED_FIELDS}</MS>
</fieldset>

View File

@@ -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]}

View File

@@ -141,7 +141,12 @@ export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
return (
<>
<Section slim>
<form className="form form--internal" onSubmit={handleSubmit}>
<form
className={`form form--internal ${
!phoneNumber?.data && phoneNumber?.refetch ? "form--blur" : ""
}`}
onSubmit={handleSubmit}
>
<fieldset>
<MS>{MSG_REQUIRED_FIELDS}</MS>
</fieldset>

View File

@@ -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]}

View File

@@ -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);
});
}
}
@@ -107,13 +112,52 @@ export const Player = ({ call }: PlayerProps) => {
});
};
const PEAKS_WINDOW = 5; // require 30 ms of speech energy over threshold to trigger
const PEAK_THRESHOLD = 0.03;
const getSilenceStartTime = (
start: number,
end: number,
channel: number
): number => {
if (waveSurferRef.current) {
const duration = waveSurferRef.current.getDecodedData()?.duration;
if (duration && duration > 0) {
const maxLength = Math.round(duration * 8000) / 10; // evaluate speech energy every 10 ms
const peaks = waveSurferRef.current.exportPeaks({ maxLength });
if (peaks && peaks.length > channel) {
if (duration && duration > 0) {
const data = peaks[channel];
const startPeak = Math.ceil((start * data.length) / duration);
const endPeak = Math.ceil((end * data.length) / duration);
let count = 0;
for (let i = endPeak; i > startPeak; i--)
if (Math.abs(data[i]) > PEAK_THRESHOLD) {
count++;
if (count === PEAKS_WINDOW) {
return (
((i + PEAKS_WINDOW) * duration) / data.length + 0.02 // add 20 ms adjustment
);
}
} else {
count = 0;
}
}
}
}
}
return -1;
};
const drawSttRegionForSpan = (
s: JaegerSpan,
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,26 +165,42 @@ export const Player = ({ call }: PlayerProps) => {
const end =
(s.endTimeUnixNano - startPoint.startTimeUnixNano) / 1_000_000_000;
const region = waveSufferRef.current.addRegion({
id: s.spanId,
start,
end,
color: "rgba(255, 0, 0, 0.15)",
drag: false,
loop: false,
resize: false,
});
changeRegionMouseStyle(region, channel);
const endSpeechTime = getSilenceStartTime(start, end, channel);
const [sttResult] = getSpanAttributeByName(s.attributes, "stt.result");
let att: WaveSufferSttResult;
let att: WaveSurferSttResult;
if (sttResult) {
const data = JSON.parse(sttResult.value.stringValue);
att = {
vendor: data.vendor.name,
transcript: data.alternatives[0].transcript,
confidence: data.alternatives[0].confidence,
language_code: data.language_code,
...(endSpeechTime > 0 && { latency: end - endSpeechTime }),
};
const [sttResolve] = getSpanAttributeByName(
s.attributes,
"stt.resolve"
);
if (
endSpeechTime > 0 &&
sttResolve &&
sttResolve.value.stringValue === "speech"
) {
const latencyRegion = waveSurferRegionsPluginRef.current.addRegion({
id: s.spanId + "latency",
start: endSpeechTime,
end,
color: "rgba(255, 255, 0, 0.55)",
drag: false,
resize: false,
content: `${(end - endSpeechTime).toFixed(2)} sec`,
});
changeRegionMouseStyle(latencyRegion, channel);
}
} else {
const [sttResolve] = getSpanAttributeByName(
s.attributes,
@@ -164,14 +224,25 @@ export const Player = ({ call }: PlayerProps) => {
}
}
const region = waveSurferRegionsPluginRef.current.addRegion({
id: s.spanId,
start,
end,
color: "rgba(255, 0, 0, 0.15)",
drag: false,
resize: false,
});
changeRegionMouseStyle(region, channel);
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 +272,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 +319,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 +329,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 +401,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 +478,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 +490,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 +499,53 @@ 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>
)}
{waveSurferRegionData.latency && (
<div className="spanDetailsWrapper__details">
<div className="spanDetailsWrapper__details_header">
<strong>Latency:</strong>
</div>
<div className="spanDetailsWrapper__details_body">
{waveSurferRegionData.latency.toFixed(2)} seconds
</div>
</div>
)}
@@ -434,8 +553,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 +567,7 @@ export const Player = ({ call }: PlayerProps) => {
<strong>Dtmf:</strong>
</div>
<div className="spanDetailsWrapper__details_body">
{waveSufferDtmfData.dtmf}
{waveSurferDtmfData.dtmf}
</div>
</div>
@@ -457,13 +576,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>
)}
</>
);
};

View File

@@ -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();
}
}
}

View File

@@ -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>

View File

@@ -1,19 +1,24 @@
import React, { Fragment, useEffect, useState } from "react";
import { Button, ButtonGroup, MS } from "@jambonz/ui-kit";
import { Button, ButtonGroup, Icon, MS, MXS } 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 { Icons, Section, Tooltip } from "src/components";
import {
FileUpload,
Selector,
Passwd,
AccountSelect,
Checkzone,
Message,
} from "src/components/forms";
import { toastError, toastSuccess, useSelectState } from "src/store";
import {
deleteGoogleCustomVoice,
getGoogleCustomVoices,
postGoogleCustomVoice,
postSpeechService,
putGoogleCustomVoice,
putSpeechService,
useServiceProviderData,
} from "src/api";
@@ -30,20 +35,41 @@ import {
VENDOR_NVIDIA,
VENDOR_SONIOX,
VENDOR_CUSTOM,
VENDOR_COBALT,
VENDOR_ELEVENLABS,
VENDOR_ASSEMBLYAI,
VENDOR_WHISPER,
useTtsModels,
} from "src/vendor";
import { MSG_REQUIRED_FIELDS } from "src/constants";
import {
checkSelectOptions,
getObscuredSecret,
isUserAccountScope,
isNotBlank,
hasLength,
} from "src/utils";
import { getObscuredGoogleServiceKey } from "./utils";
import { CredentialStatus } from "./status";
import type { RegionVendors, GoogleServiceKey, Vendor } from "src/vendor/types";
import type { Account, SpeechCredential, UseApiDataMap } from "src/api/types";
import type {
RegionVendors,
GoogleServiceKey,
Vendor,
TtsModels,
} from "src/vendor/types";
import type {
Account,
GoogleCustomVoice,
SpeechCredential,
UseApiDataMap,
} from "src/api/types";
import { setAccountFilter, setLocation } from "src/store/localStore";
import { DISABLE_CUSTOM_SPEECH } from "src/api/constants";
import {
DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
DISABLE_CUSTOM_SPEECH,
GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
} from "src/api/constants";
type SpeechServiceFormProps = {
credential?: UseApiDataMap<SpeechCredential>;
@@ -75,11 +101,20 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
const [sttApiKey, setSttApiKey] = useState("");
const [ttsRegion, setTtsRegion] = useState("");
const [ttsApiKey, setTtsApiKey] = useState("");
const [ttsModelId, setTtsModelId] = useState("");
const [instanceId, setInstanceId] = useState("");
const [initialCheckCustomTts, setInitialCheckCustomTts] = useState(false);
const [initialCheckCustomStt, setInitialCheckCustomStt] = useState(false);
const [initialCheckOnpremAzureService, setInitialCheckOnpremAzureService] =
useState(false);
const [useCustomTts, setUseCustomTts] = useState(false);
const [useCustomStt, setUseCustomStt] = useState(false);
const [customTtsEndpointUrl, setCustomTtsEndpointUrl] = useState("");
const [tmpCustomTtsEndpointUrl, setTmpCustomTtsEndpointUrl] = useState("");
const [customTtsEndpoint, setCustomTtsEndpoint] = useState("");
const [tmpCustomTtsEndpoint, setTmpCustomTtsEndpoint] = useState("");
const [customSttEndpointUrl, setCustomSttEndpointUrl] = useState("");
const [tmpCustomSttEndpointUrl, setTmpCustomSttEndpointUrl] = useState("");
const [customSttEndpoint, setCustomSttEndpoint] = useState("");
const [tmpCustomSttEndpoint, setTmpCustomSttEndpoint] = useState("");
const [rivaServerUri, setRivaServerUri] = useState("");
@@ -99,6 +134,12 @@ 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 [useCustomVoicesCheck, setUseCustomVoicesCheck] = useState(false);
const [customVoices, setCustomVoices] = useState<GoogleCustomVoice[]>([]);
const [customVoicesMessage, setCustomVoicesMessage] = useState("");
const ttsModels = useTtsModels();
const handleFile = (file: File) => {
const handleError = () => {
@@ -126,6 +167,62 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
});
};
const handlePutGoogleCustomVoices = () => {
if (!credential || !credential.data) {
return;
}
if (useCustomVoicesCheck) {
Promise.all(
customVoices.map((v) => {
if (v.google_custom_voice_sid) {
const sid = v.google_custom_voice_sid;
delete v.google_custom_voice_sid;
return putGoogleCustomVoice(sid, v);
} else {
return postGoogleCustomVoice({
...v,
speech_credential_sid: credential.data?.speech_credential_sid,
});
}
})
)
.then(() => {
toastSuccess("Speech credential updated successfully");
credential.refetch();
navigate(
`${ROUTE_INTERNAL_SPEECH}/${credential?.data?.speech_credential_sid}/edit`
);
})
.catch((error) => {
toastError(error.msg);
});
} else if (useCustomVoicesCheck && customVoices.length > 0) {
Promise.all(
customVoices.map((v) => {
if (v.google_custom_voice_sid) {
return deleteGoogleCustomVoice(v.google_custom_voice_sid);
}
})
)
.then(() => {
toastSuccess("Speech credential updated successfully");
credential.refetch();
navigate(
`${ROUTE_INTERNAL_SPEECH}/${credential?.data?.speech_credential_sid}/edit`
);
})
.catch((error) => {
toastError(error.msg);
});
} else {
toastSuccess("Speech credential updated successfully");
credential.refetch();
navigate(
`${ROUTE_INTERNAL_SPEECH}/${credential.data.speech_credential_sid}/edit`
);
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
@@ -143,14 +240,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 +279,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 || vendor === VENDOR_WHISPER) && {
model_id: ttsModelId || null,
}),
};
if (credential && credential.data) {
@@ -189,11 +297,15 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
)
.then(() => {
if (credential && credential.data) {
toastSuccess("Speech credential updated successfully");
credential.refetch();
navigate(
`${ROUTE_INTERNAL_SPEECH}/${credential.data.speech_credential_sid}/edit`
);
if (credential.data.vendor === VENDOR_GOOGLE) {
handlePutGoogleCustomVoices();
} else {
toastSuccess("Speech credential updated successfully");
credential.refetch();
navigate(
`${ROUTE_INTERNAL_SPEECH}/${credential.data.speech_credential_sid}/edit`
);
}
}
})
.catch((error) => {
@@ -206,19 +318,39 @@ 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_ASSEMBLYAI ||
vendor === VENDOR_SONIOX ||
vendor === VENDOR_ELEVENLABS ||
vendor === VENDOR_WHISPER
? apiKey
: null,
}),
riva_server_uri: vendor == VENDOR_NVIDIA ? rivaServerUri : null,
})
.then(() => {
toastSuccess("Speech credential created successfully");
navigate(ROUTE_INTERNAL_SPEECH);
setAccountFilter(accountSid);
.then(({ json }) => {
if (vendor === VENDOR_GOOGLE && useCustomVoicesCheck) {
Promise.all(
customVoices.map((v) =>
postGoogleCustomVoice({
...v,
speech_credential_sid: json.sid,
})
)
).then(() => {
toastSuccess("Speech credential created successfully");
navigate(ROUTE_INTERNAL_SPEECH);
setAccountFilter(accountSid);
});
} else {
toastSuccess("Speech credential created successfully");
navigate(ROUTE_INTERNAL_SPEECH);
setAccountFilter(accountSid);
}
})
.catch((error) => {
toastError(error.msg);
@@ -330,13 +462,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,12 +492,52 @@ 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);
}
}
if (credential?.data?.vendor === VENDOR_GOOGLE) {
// let try to check if there is custom voices
getGoogleCustomVoices({
speech_credential_sid: credential.data.speech_credential_sid,
}).then(({ json }) => {
setCustomVoices(json);
setUseCustomVoicesCheck(json.length > 0);
});
}
}, [credential]);
const updateCustomVoices = (
index: number,
key: string,
value: typeof customVoices[number][keyof GoogleCustomVoice]
) => {
setCustomVoices((prev) =>
prev.map((g, i) =>
i === index
? {
...g,
[key]: value,
}
: g
)
);
};
return (
<Section slim>
<form className="form form--internal" onSubmit={handleSubmit}>
<form
className={`form form--internal ${
!credential?.data && credential?.refetch ? "form--blur" : ""
}`}
onSubmit={handleSubmit}
>
<fieldset>
<MS>{MSG_REQUIRED_FIELDS}</MS>
</fieldset>
@@ -385,6 +570,15 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
setRegion("");
setApiKey("");
setGoogleServiceKey(null);
if (
ttsModels &&
(e.target.value === VENDOR_ELEVENLABS ||
e.target.value === VENDOR_WHISPER)
) {
setTtsModelId(
ttsModels[e.target.value as keyof TtsModels][0].value
);
}
}}
disabled={credential ? true : false}
required
@@ -416,9 +610,27 @@ 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_ASSEMBLYAI &&
vendor !== VENDOR_COBALT &&
vendor !== VENDOR_SONIOX &&
vendor != VENDOR_CUSTOM && (
<label htmlFor="use_for_tts" className="chk">
@@ -432,18 +644,21 @@ 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_WHISPER &&
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 +726,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">
@@ -556,6 +789,166 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
</pre>
</fieldset>
)}
{ttsCheck && vendor === VENDOR_GOOGLE && (
<fieldset>
<label htmlFor="use_custom_voice" className="chk">
<input
id="use_custom_voice"
name="use_custom_voice"
type="checkbox"
onChange={(e) => {
if (customVoices.length === 0) {
setCustomVoices([
{
name: "",
reported_usage:
DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
model: "",
},
]);
}
setUseCustomVoicesCheck(e.target.checked);
}}
checked={useCustomVoicesCheck}
/>
<div>Use custom voices</div>
</label>
{useCustomVoicesCheck && (
<fieldset>
<label htmlFor="sip_gateways">Custom Voices</label>
<MXS>
<em>At least one Custom voice is required.</em>
</MXS>
{customVoicesMessage && (
<Message message={customVoicesMessage} />
)}
{hasLength(customVoices) &&
customVoices.map((v, i) => (
<div key={`custom_voice_${i}`} className="customVoice">
<div>
<div>
<label htmlFor="custom_voice_name">
Name / Reported Usage
</label>
</div>
</div>
<div>
<div>
<input
id={`sip_ip_${i}`}
name={`sip_ip_${i}`}
type="text"
placeholder="Assigned Name"
required
value={v.name}
onChange={(e) => {
updateCustomVoices(i, "name", e.target.value);
}}
/>
</div>
<div>
<Selector
id={"google_custom_voices_reported_usage"}
name={"google_custom_voices_reported_usage"}
value={v.reported_usage}
options={GOOGLE_CUSTOM_VOICES_REPORTED_USAGE}
onChange={(e) => {
updateCustomVoices(
i,
"reported_usage",
e.target.value
);
}}
/>
</div>
</div>
<div>
<div>
<label htmlFor="custom_voice_name">Model</label>
</div>
</div>
<div>
<div>
<input
id={`sip_ip_${i}`}
name={`sip_ip_${i}`}
type="text"
placeholder="Model"
required
value={v.model}
style={{ maxWidth: "100%" }}
onChange={(e) => {
updateCustomVoices(
i,
"model",
e.target.value
);
}}
/>
</div>
</div>
<button
className="btnty"
title="Delete custom voice"
type="button"
onClick={() => {
setCustomVoicesMessage("");
if (customVoices.length === 1) {
setCustomVoicesMessage(
"You must provide at least one custom voice."
);
return;
}
if (v.google_custom_voice_sid) {
deleteGoogleCustomVoice(
v.google_custom_voice_sid
).finally(() => {
credential?.refetch();
});
}
setCustomVoices((prev) =>
prev.filter((_, idx) => idx !== i)
);
}}
>
<Icon>
<Icons.Trash2 />
</Icon>
</button>
</div>
))}
<ButtonGroup left>
<button
className="btnty"
type="button"
title="Add Voice"
onClick={() => {
setCustomVoicesMessage("");
setCustomVoices((prev) => [
...prev,
{
name: "",
reported_usage:
DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
model: "",
},
]);
}}
>
<Icon subStyle="teal">
<Icons.Plus />
</Icon>
</button>
</ButtonGroup>
</fieldset>
)}
</fieldset>
)}
</>
)}
{vendor === VENDOR_NUANCE && (
@@ -695,9 +1088,11 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
/>
</fieldset>
)}
{(vendor === VENDOR_MICROSOFT ||
vendor === VENDOR_WELLSAID ||
{(vendor === VENDOR_WELLSAID ||
vendor === VENDOR_DEEPGRAM ||
vendor === VENDOR_ASSEMBLYAI ||
vendor == VENDOR_ELEVENLABS ||
vendor === VENDOR_WHISPER ||
vendor === VENDOR_SONIOX) && (
<fieldset>
<label htmlFor={`${vendor}_apikey`}>
@@ -714,9 +1109,25 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
/>
</fieldset>
)}
{(vendor == VENDOR_ELEVENLABS || vendor == VENDOR_WHISPER) &&
ttsModels && (
<fieldset>
<label htmlFor={`${vendor}_tts_model_id`}>Model</label>
<Selector
id={"tts_model_id"}
name={"tts_model_id"}
value={ttsModelId}
options={ttsModels[vendor as keyof TtsModels]}
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 +1224,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>
)}

View File

@@ -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

View File

@@ -183,7 +183,12 @@ export const UserForm = ({ user }: UserFormProps) => {
return (
<>
<Section slim>
<form className="form form--internal" onSubmit={handleSubmit}>
<form
className={`form form--internal ${
!user?.data && user?.refetch ? "form--blur" : ""
}`}
onSubmit={handleSubmit}
>
<fieldset>
<MS>{MSG_REQUIRED_FIELDS}</MS>
</fieldset>

View File

@@ -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"

View File

@@ -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;
}
}
}

View File

@@ -105,6 +105,10 @@ fieldset {
}
}
&--blur {
pointer-events: none;
}
label {
@include ui-mixins.m();
@include ui-mixins.font-medium();
@@ -227,7 +231,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 {
@@ -294,3 +298,53 @@ fieldset {
.bucket_tag {
@extend .lcr;
}
.customVoice {
padding: ui-vars.$px02;
border-radius: ui-vars.$px01;
border: 2px solid ui-vars.$grey;
max-width: ui-vars.$width-mobile;
position: relative;
> div {
display: grid;
grid-gap: ui-vars.$px02;
align-items: center;
&:nth-child(1) {
grid-template-columns: [col] 100%;
}
&:nth-child(2) {
grid-template-columns: [col] calc(40% - #{ui-vars.$px02 * 2}) [col] 60%;
margin-top: ui-vars.$px02;
}
&:nth-child(3) {
grid-template-columns: [col] 100%;
margin-top: ui-vars.$px02;
}
&:nth-child(4) {
grid-template-columns: [col] 100%;
margin-top: ui-vars.$px02;
}
}
> button {
position: absolute;
right: 0;
bottom: 50%;
transform: translate3d(50%, 50%, 0);
@include mixins.small() {
top: auto;
bottom: auto;
transform: none;
position: relative;
margin-top: ui-vars.$px02;
display: flex;
margin-left: auto;
}
}
}

View File

@@ -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);

61
src/vendor/index.tsx vendored
View File

@@ -5,9 +5,12 @@ import type {
SynthesisVendors,
RecognizerVendors,
RegionVendors,
TtsModels,
} 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 +22,10 @@ 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 VENDOR_ASSEMBLYAI = "assemblyai";
export const VENDOR_WHISPER = "whisper";
export const vendors: VendorOptions[] = [
{
@@ -61,7 +68,47 @@ export const vendors: VendorOptions[] = [
name: "Custom",
value: VENDOR_CUSTOM,
},
];
{
name: "Cobalt",
value: VENDOR_COBALT,
},
{
name: "ElevenLabs",
value: VENDOR_ELEVENLABS,
},
{
name: "AssemblyAI",
value: VENDOR_ASSEMBLYAI,
},
{
name: "Whisper",
value: VENDOR_WHISPER,
},
].sort((a, b) => a.name.localeCompare(b.name)) as VendorOptions[];
export const useTtsModels = () => {
const [models, setModels] = useState<TtsModels>();
useEffect(() => {
let ignore = false;
Promise.all([
import("./speech-synthsis-models/elevenlabs-models"),
import("./speech-synthsis-models/whisper-models"),
]).then(([{ default: elevenlabs }, { default: whisper }]) => {
if (!ignore) {
setModels({
elevenlabs,
whisper,
});
}
});
return function cleanup() {
ignore = true;
};
}, []);
return models;
};
export const useRegionVendors = () => {
const [regions, setRegions] = useState<RegionVendors>();
@@ -115,6 +162,8 @@ 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-recognizer/assemblyai-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 +171,8 @@ 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"),
import("./speech-synthesis/whisper-speech-synthesis-lang"),
]).then(
([
{ default: awsRecognizer },
@@ -132,6 +183,8 @@ export const useSpeechVendors = () => {
{ default: ibmRecognizer },
{ default: nvidiaRecognizer },
{ default: sonioxRecognizer },
{ default: cobaltRecognizer },
{ default: assemblyaiRecognizer },
{ default: awsSynthesis },
{ default: googleSynthesis },
{ default: msSynthesis },
@@ -139,6 +192,8 @@ export const useSpeechVendors = () => {
{ default: nuanceSynthesis },
{ default: ibmSynthesis },
{ default: nvidiaynthesis },
{ default: elevenLabsSynthesis },
{ default: whisperSynthesis },
]) => {
if (!ignore) {
setSpeech({
@@ -150,6 +205,8 @@ export const useSpeechVendors = () => {
nuance: nuanceSynthesis,
ibm: ibmSynthesis,
nvidia: nvidiaynthesis,
elevenlabs: elevenLabsSynthesis,
whisper: whisperSynthesis,
},
recognizers: {
aws: awsRecognizer,
@@ -160,6 +217,8 @@ export const useSpeechVendors = () => {
ibm: ibmRecognizer,
nvidia: nvidiaRecognizer,
soniox: sonioxRecognizer,
cobalt: cobaltRecognizer,
assemblyai: assemblyaiRecognizer,
},
});
}

View File

@@ -0,0 +1,26 @@
import type { Language } from "../types";
export const languages: Language[] = [
{ name: "Global English", code: "en" },
{ name: "Australian English", code: "en_au" },
{ name: "British English", code: "en_uk" },
{ name: "US English", code: "en_us" },
{ name: "Spanish", code: "es" },
{ name: "French", code: "fr" },
{ name: "German", code: "de" },
{ name: "Italian", code: "it" },
{ name: "Portuguese", code: "pt" },
{ name: "Dutch", code: "nl" },
{ name: "Hindi", code: "hi" },
{ name: "Japanese", code: "ja" },
{ name: "Chinese", code: "zh" },
{ name: "Finnish", code: "fi" },
{ name: "Korean", code: "ko" },
{ name: "Polish", code: "pl" },
{ name: "Russian", code: "ru" },
{ name: "Turkish", code: "tr" },
{ name: "Ukrainian", code: "uk" },
{ name: "Vietnamese", code: "vi" },
];
export default languages;

View 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;

View 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;

View File

@@ -0,0 +1,18 @@
import type { VoiceLanguage } from "../types";
export const languages: VoiceLanguage[] = [
{
code: "en-US",
name: "English",
voices: [
{ value: "alloy", name: "Alloy" },
{ value: "echo", name: "Echo" },
{ value: "fable", name: "Fable" },
{ value: "onyx", name: "Onyx" },
{ value: "nova", name: "Nova" },
{ value: "shimmer", name: "Shimmer" },
],
},
];
export default languages;

View File

@@ -0,0 +1,9 @@
import type { Model } from "../types";
export const models: Model[] = [
{ name: "Multilingual v2", value: "eleven_multilingual_v2" },
{ name: "Multilingual v1", value: "eleven_multilingual_v1" },
{ name: "English v1", value: "eleven_monolingual_v1" },
];
export default models;

View File

@@ -0,0 +1,8 @@
import type { Model } from "../types";
export const models: Model[] = [
{ name: "TTS-1", value: "tts-1" },
{ name: "TTS-1-HD", value: "tts-1-hd" },
];
export default models;

25
src/vendor/types.ts vendored
View File

@@ -8,18 +8,32 @@ export type Vendor =
| "IBM"
| "Nvidia"
| "Soniox"
| "Custom";
| "Cobalt"
| "Custom"
| "ElevenLabs"
| "assemblyai"
| "whisper";
export interface VendorOptions {
name: Vendor;
value: Lowercase<Vendor>;
}
export interface LabelOptions {
name: string;
value: string;
}
export interface Region {
name: string;
value: string;
}
export interface Model {
name: string;
value: string;
}
export interface Voice {
name: string;
value: string;
@@ -56,6 +70,11 @@ export interface RegionVendors {
ibm: Region[];
}
export interface TtsModels {
elevenlabs: Model[];
whisper: Model[];
}
export interface RecognizerVendors {
aws: Language[];
google: Language[];
@@ -65,6 +84,8 @@ export interface RecognizerVendors {
ibm: Language[];
nvidia: Language[];
soniox: Language[];
cobalt: Language[];
assemblyai: Language[];
}
export interface SynthesisVendors {
@@ -75,6 +96,8 @@ export interface SynthesisVendors {
nuance: VoiceLanguage[];
ibm: VoiceLanguage[];
nvidia: VoiceLanguage[];
elevenlabs: VoiceLanguage[];
whisper: VoiceLanguage[];
}
export interface MSRawSpeech {