mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-01-25 02:08:19 +00:00
Compare commits
69 Commits
v0.8.4
...
fix/speech
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6216d1db1 | ||
|
|
6dd445fdb4 | ||
|
|
440cf25935 | ||
|
|
97736654ef | ||
|
|
2a3fc07306 | ||
|
|
6e14207327 | ||
|
|
8d8d46e76e | ||
|
|
7f72d739cd | ||
|
|
c804d60664 | ||
|
|
df3fc8f2b7 | ||
|
|
65e5b511c3 | ||
|
|
dc519bdef9 | ||
|
|
af1ba3a15c | ||
|
|
67b7792d04 | ||
|
|
c5e7eb0d23 | ||
|
|
6ddcb82adc | ||
|
|
8b9c7ca9c0 | ||
|
|
353c7cfff8 | ||
|
|
7828dc3827 | ||
|
|
213267f682 | ||
|
|
cf056ae6f1 | ||
|
|
1c16d707ca | ||
|
|
2f2e58e180 | ||
|
|
eae674b992 | ||
|
|
aa7889a0d8 | ||
|
|
a892550b06 | ||
|
|
053f8e509f | ||
|
|
fc40695828 | ||
|
|
42af4f6243 | ||
|
|
7ec8065977 | ||
|
|
d8f05da6fd | ||
|
|
15c2b955ca | ||
|
|
87b3ca7e94 | ||
|
|
adafff7ec3 | ||
|
|
bc9a2464fd | ||
|
|
2a6f8c272c | ||
|
|
f031c47228 | ||
|
|
2e9b86c0c4 | ||
|
|
dd93bedd0e | ||
|
|
e2157ce50e | ||
|
|
a382f21f86 | ||
|
|
a20e1513bc | ||
|
|
af8c09587c | ||
|
|
3a19ff6840 | ||
|
|
729cefb06c | ||
|
|
26e3856603 | ||
|
|
f5302583b5 | ||
|
|
b5c27bb096 | ||
|
|
4a2c36ebba | ||
|
|
62234f9f64 | ||
|
|
9ddafee2cc | ||
|
|
24fc9d1bff | ||
|
|
08ab494cef | ||
|
|
75e7785061 | ||
|
|
72de9178a2 | ||
|
|
9741e5601f | ||
|
|
346ac66440 | ||
|
|
843d1eda1e | ||
|
|
27f02c2bb3 | ||
|
|
bb18556a6c | ||
|
|
393dd7374f | ||
|
|
4ad2154337 | ||
|
|
08d1293e34 | ||
|
|
4eb2281b9a | ||
|
|
61bd1f9bab | ||
|
|
16629ba508 | ||
|
|
63f8a82443 | ||
|
|
9ce1d83c8f | ||
|
|
961b7ecccb |
16
.env
16
.env
@@ -11,4 +11,18 @@ VITE_DEV_BASE_URL=http://127.0.0.1:3000/v1
|
||||
## disables Jaeger Tracing feature
|
||||
#VITE_APP_JAEGER_TRACING_DISABLED=true
|
||||
## enable record All Calls feature
|
||||
#VITE_APP_DISABLE_CALL_RECORDING=true
|
||||
#VITE_APP_DISABLE_CALL_RECORDING=true
|
||||
## enable Forgot password
|
||||
#VITE_APP_ENABLE_FORGOT_PASSWORD=true
|
||||
## enable hosted system
|
||||
#VITE_APP_ENABLE_HOSTED_SYSTEM=true
|
||||
## Google Client ID
|
||||
#VITE_APP_GOOGLE_CLIENT_ID=
|
||||
## Github Client ID
|
||||
#VITE_APP_GITHUB_CLIENT_ID=
|
||||
## Default jambonz service provider SID
|
||||
#VITE_APP_DEFAULT_SERVICE_PROVIDER_SID=
|
||||
## Base url for jambomz webapp
|
||||
#VITE_APP_BASE_URL="http://jambonz.one"
|
||||
## Strip publishable key
|
||||
#VITE_APP_STRIPE_PUBLISHABLE_KEY="pk_test_EChRaX9Tjk8csZZVSeoGqNvu00lsJzjaU1"
|
||||
@@ -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>
|
||||
|
||||
591
package-lock.json
generated
591
package-lock.json
generated
@@ -1,16 +1,18 @@
|
||||
{
|
||||
"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": {
|
||||
"@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",
|
||||
@@ -19,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",
|
||||
@@ -28,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",
|
||||
@@ -76,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",
|
||||
@@ -127,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"
|
||||
@@ -201,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"
|
||||
@@ -286,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"
|
||||
@@ -307,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"
|
||||
@@ -339,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": {
|
||||
@@ -424,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"
|
||||
@@ -521,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"
|
||||
},
|
||||
@@ -565,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": {
|
||||
@@ -611,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",
|
||||
@@ -629,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"
|
||||
},
|
||||
@@ -651,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",
|
||||
@@ -831,6 +895,24 @@
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stripe/react-stripe-js": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.1.1.tgz",
|
||||
"integrity": "sha512-aHm6cWOqmOyq3eKKlR42hgvrcZhzc9ijL+BeXJ5H3uaWUKKcooSsBys0tiKl0gLxYQHUjUCvrRSX8fYaPBmAKg==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@stripe/stripe-js": "^1.44.1",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stripe/stripe-js": {
|
||||
"version": "1.54.1",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.54.1.tgz",
|
||||
"integrity": "sha512-smEXPu1GKMcAj9g2luT16+oXfg2jAwyc68t2Dm5wdtYl3p8PqQaZEiI8tQmboaQAjgF8pIGma6byz1T1vgmpbA=="
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
@@ -880,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",
|
||||
@@ -994,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",
|
||||
@@ -2429,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": {
|
||||
@@ -5433,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": [
|
||||
{
|
||||
@@ -5443,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"
|
||||
},
|
||||
@@ -5456,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"
|
||||
},
|
||||
@@ -5568,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,
|
||||
@@ -5852,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,
|
||||
@@ -6014,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"
|
||||
}
|
||||
@@ -6494,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": {
|
||||
@@ -6754,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",
|
||||
@@ -6847,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",
|
||||
@@ -7027,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": {
|
||||
@@ -7065,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"
|
||||
@@ -7127,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": {
|
||||
@@ -7190,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"
|
||||
@@ -7205,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": {
|
||||
@@ -7228,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": {
|
||||
@@ -7297,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": {
|
||||
@@ -7350,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"
|
||||
},
|
||||
@@ -7387,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"
|
||||
}
|
||||
},
|
||||
@@ -7426,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",
|
||||
@@ -7444,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"
|
||||
},
|
||||
@@ -7459,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
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7596,6 +7761,19 @@
|
||||
"picomatch": "^2.2.2"
|
||||
}
|
||||
},
|
||||
"@stripe/react-stripe-js": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.1.1.tgz",
|
||||
"integrity": "sha512-aHm6cWOqmOyq3eKKlR42hgvrcZhzc9ijL+BeXJ5H3uaWUKKcooSsBys0tiKl0gLxYQHUjUCvrRSX8fYaPBmAKg==",
|
||||
"requires": {
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"@stripe/stripe-js": {
|
||||
"version": "1.54.1",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.54.1.tgz",
|
||||
"integrity": "sha512-smEXPu1GKMcAj9g2luT16+oXfg2jAwyc68t2Dm5wdtYl3p8PqQaZEiI8tQmboaQAjgF8pIGma6byz1T1vgmpbA=="
|
||||
},
|
||||
"@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
@@ -7645,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",
|
||||
@@ -7754,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",
|
||||
@@ -8651,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",
|
||||
@@ -10714,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
|
||||
}
|
||||
}
|
||||
@@ -10797,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
|
||||
@@ -10971,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,
|
||||
@@ -11071,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": {
|
||||
@@ -11407,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": {
|
||||
@@ -11573,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",
|
||||
@@ -11624,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",
|
||||
|
||||
@@ -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,7 +52,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",
|
||||
@@ -59,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",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type {
|
||||
Currency,
|
||||
ElevenLabsOptions,
|
||||
LimitField,
|
||||
LimitUnitOption,
|
||||
PasswordSettings,
|
||||
@@ -17,7 +19,13 @@ interface JambonzWindowObject {
|
||||
DISABLE_JAEGER_TRACING: string;
|
||||
DISABLE_CUSTOM_SPEECH: string;
|
||||
ENABLE_FORGOT_PASSWORD: string;
|
||||
ENABLE_HOSTED_SYSTEM: string;
|
||||
DISABLE_CALL_RECORDING: string;
|
||||
GITHUB_CLIENT_ID: string;
|
||||
GOOGLE_CLIENT_ID: string;
|
||||
BASE_URL: string;
|
||||
DEFAULT_SERVICE_PROVIDER_SID: string;
|
||||
STRIPE_PUBLISHABLE_KEY: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -41,7 +49,12 @@ export const DISABLE_CUSTOM_SPEECH: boolean =
|
||||
/** Enable Forgot Password */
|
||||
export const ENABLE_FORGOT_PASSWORD: boolean =
|
||||
window.JAMBONZ?.ENABLE_FORGOT_PASSWORD === "true" ||
|
||||
JSON.parse(import.meta.env.VITE_ENABLE_FORGOT_PASSWORD || "false");
|
||||
JSON.parse(import.meta.env.VITE_APP_ENABLE_FORGOT_PASSWORD || "false");
|
||||
|
||||
/** Enable Cloud version */
|
||||
export const ENABLE_HOSTED_SYSTEM: boolean =
|
||||
window.JAMBONZ?.ENABLE_HOSTED_SYSTEM === "true" ||
|
||||
JSON.parse(import.meta.env.VITE_APP_ENABLE_HOSTED_SYSTEM || "false");
|
||||
/** Disable Lcr */
|
||||
export const DISABLE_LCR: boolean =
|
||||
window.JAMBONZ?.DISABLE_LCR === "true" ||
|
||||
@@ -57,6 +70,23 @@ export const DISABLE_CALL_RECORDING: boolean =
|
||||
window.JAMBONZ?.DISABLE_CALL_RECORDING === "true" ||
|
||||
JSON.parse(import.meta.env.VITE_APP_DISABLE_CALL_RECORDING || "false");
|
||||
|
||||
export const DEFAULT_SERVICE_PROVIDER_SID: string =
|
||||
window.JAMBONZ?.DEFAULT_SERVICE_PROVIDER_SID ||
|
||||
import.meta.env.VITE_APP_DEFAULT_SERVICE_PROVIDER_SID;
|
||||
|
||||
export const GITHUB_CLIENT_ID: string =
|
||||
window.JAMBONZ?.GITHUB_CLIENT_ID || import.meta.env.VITE_APP_GITHUB_CLIENT_ID;
|
||||
|
||||
export const BASE_URL: string =
|
||||
window.JAMBONZ?.BASE_URL || import.meta.env.VITE_APP_BASE_URL;
|
||||
|
||||
export const GOOGLE_CLIENT_ID: string =
|
||||
window.JAMBONZ?.GOOGLE_CLIENT_ID || import.meta.env.VITE_APP_GOOGLE_CLIENT_ID;
|
||||
|
||||
export const STRIPE_PUBLISHABLE_KEY: string =
|
||||
window.JAMBONZ?.STRIPE_PUBLISHABLE_KEY ||
|
||||
import.meta.env.VITE_APP_STRIPE_PUBLISHABLE_KEY;
|
||||
|
||||
/** TCP Max Port */
|
||||
export const TCP_MAX_PORT = 65535;
|
||||
|
||||
@@ -131,6 +161,10 @@ 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",
|
||||
@@ -138,7 +172,19 @@ export const BUCKET_VENDOR_OPTIONS = [
|
||||
},
|
||||
{
|
||||
name: "AWS S3",
|
||||
value: "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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -152,6 +198,28 @@ 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" },
|
||||
];
|
||||
// Eleven Labs options
|
||||
export const DEFAULT_ELEVENLABS_OPTIONS: Partial<ElevenLabsOptions> = {
|
||||
optimize_streaming_latency: 3,
|
||||
voice_settings: {
|
||||
stability: 0.5,
|
||||
similarity_boost: 0.5,
|
||||
use_speaker_boost: true,
|
||||
},
|
||||
};
|
||||
/** Password Length options */
|
||||
|
||||
export const PASSWORD_MIN = 8;
|
||||
@@ -243,6 +311,16 @@ export const DEFAULT_PSWD_SETTINGS: PasswordSettings = {
|
||||
require_special_character: 0,
|
||||
};
|
||||
|
||||
export const PlanType = {
|
||||
PAID: "paid",
|
||||
TRIAL: "trial",
|
||||
FREE: "free",
|
||||
};
|
||||
|
||||
export const CurrencySymbol: Currency = {
|
||||
usd: "$",
|
||||
};
|
||||
|
||||
/** User scope values values */
|
||||
export const USER_ADMIN = "admin";
|
||||
export const USER_SP = "service_provider";
|
||||
@@ -257,6 +335,9 @@ export const CRED_NOT_TESTED = "not tested";
|
||||
export const CARRIER_REG_OK = "ok";
|
||||
export const CARRIER_REG_FAIL = "fail";
|
||||
|
||||
export const PRIVACY_POLICY = "https://jambonz.org/privacy";
|
||||
export const TERMS_OF_SERVICE = "https://jambonz.org/terms";
|
||||
|
||||
/** API base paths */
|
||||
export const API_LOGIN = `${API_BASE_URL}/login`;
|
||||
export const API_LOGOUT = `${API_BASE_URL}/logout`;
|
||||
@@ -279,3 +360,11 @@ export const API_LCR_ROUTES = `${API_BASE_URL}/LcrRoutes`;
|
||||
export const API_LCR_CARRIER_SET_ENTRIES = `${API_BASE_URL}/LcrCarrierSetEntries`;
|
||||
export const API_TTS_CACHE = `${API_BASE_URL}/TtsCache`;
|
||||
export const API_CLIENTS = `${API_BASE_URL}/Clients`;
|
||||
export const API_REGISTER = `${API_BASE_URL}/register`;
|
||||
export const API_ACTIVATION_CODE = `${API_BASE_URL}/ActivationCode`;
|
||||
export const API_AVAILABILITY = `${API_BASE_URL}/Availability`;
|
||||
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`;
|
||||
|
||||
166
src/api/index.ts
166
src/api/index.ts
@@ -26,6 +26,14 @@ import {
|
||||
API_LCRS,
|
||||
API_TTS_CACHE,
|
||||
API_CLIENTS,
|
||||
API_REGISTER,
|
||||
API_ACTIVATION_CODE,
|
||||
API_AVAILABILITY,
|
||||
API_PRICE,
|
||||
API_SUBSCRIPTIONS,
|
||||
API_CHANGE_PASSWORD,
|
||||
API_SIGNIN,
|
||||
API_GOOGLE_CUSTOM_VOICES,
|
||||
} from "./constants";
|
||||
import { ROUTE_LOGIN } from "src/router/routes";
|
||||
import {
|
||||
@@ -74,8 +82,20 @@ import type {
|
||||
BucketCredential,
|
||||
BucketCredentialTestResult,
|
||||
Client,
|
||||
RegisterRequest,
|
||||
RegisterResponse,
|
||||
ActivationCode,
|
||||
CurrentUserData,
|
||||
PriceInfo,
|
||||
Subscription,
|
||||
DeleteAccount,
|
||||
ChangePassword,
|
||||
SignIn,
|
||||
GoogleCustomVoice,
|
||||
GoogleCustomVoicesQuery,
|
||||
SpeechSupportedLanguagesAndVoices,
|
||||
} from "./types";
|
||||
import { StatusCodes } from "./types";
|
||||
import { Availability, StatusCodes } from "./types";
|
||||
import { JaegerRoot } from "./jaeger-types";
|
||||
|
||||
/** Wrap all requests to normalize response handling */
|
||||
@@ -163,7 +183,7 @@ const getAuthHeaders = () => {
|
||||
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -239,6 +259,17 @@ export const deleteFetch = <Type>(url: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteFetchWithPayload = <Type, Payload>(
|
||||
url: string,
|
||||
payload: Payload
|
||||
) => {
|
||||
return fetchTransport<Type>(url, {
|
||||
method: "DELETE",
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
};
|
||||
|
||||
/** All APIs need a wrapper utility that uses the FetchTransport */
|
||||
|
||||
export const postLogin = (payload: UserLoginPayload) => {
|
||||
@@ -397,6 +428,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);
|
||||
};
|
||||
@@ -413,6 +454,44 @@ export const postLcrCarrierSetEntry = (
|
||||
export const postClient = (payload: Partial<Client>) => {
|
||||
return postFetch<SidResponse, Partial<Client>>(API_CLIENTS, payload);
|
||||
};
|
||||
|
||||
export const postRegister = (payload: Partial<RegisterRequest>) => {
|
||||
return postFetch<RegisterResponse, Partial<RegisterRequest>>(
|
||||
API_REGISTER,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const postSipRealms = (accountSid: string, domain: string) => {
|
||||
return postFetch<EmptyResponse>(
|
||||
`${API_ACCOUNTS}/${accountSid}/SipRealms/${domain}`
|
||||
);
|
||||
};
|
||||
|
||||
export const postSubscriptions = (payload: Partial<Subscription>) => {
|
||||
return postFetch<Subscription, Partial<Subscription>>(
|
||||
API_SUBSCRIPTIONS,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const postChangepassword = (payload: Partial<ChangePassword>) => {
|
||||
return postFetch<EmptyResponse, Partial<ChangePassword>>(
|
||||
API_CHANGE_PASSWORD,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
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>) => {
|
||||
@@ -509,6 +588,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}`,
|
||||
@@ -532,6 +621,27 @@ export const putClient = (sid: string, payload: Partial<Client>) => {
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const putActivationCode = (
|
||||
code: string,
|
||||
payload: Partial<ActivationCode>
|
||||
) => {
|
||||
return putFetch<EmptyResponse, Partial<ActivationCode>>(
|
||||
`${API_ACTIVATION_CODE}/${code}`,
|
||||
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) => {
|
||||
@@ -546,8 +656,11 @@ export const deleteApiKey = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_API_KEYS}/${sid}`);
|
||||
};
|
||||
|
||||
export const deleteAccount = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_ACCOUNTS}/${sid}`);
|
||||
export const deleteAccount = (sid: string, payload: Partial<DeleteAccount>) => {
|
||||
return deleteFetchWithPayload<EmptyResponse, Partial<DeleteAccount>>(
|
||||
`${API_ACCOUNTS}/${sid}`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteApplication = (sid: string) => {
|
||||
@@ -614,6 +727,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) => {
|
||||
@@ -660,8 +781,25 @@ export const getClient = (sid: string) => {
|
||||
return getFetch<Client[]>(`${API_CLIENTS}/${sid}`);
|
||||
};
|
||||
|
||||
export const getAvailability = (domain: string) => {
|
||||
return getFetch<Availability>(
|
||||
`${API_AVAILABILITY}?type=subdomain&value=${domain}`
|
||||
);
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
return getFetch<CurrentUserData>(`${API_USERS}/me`);
|
||||
};
|
||||
|
||||
export const getRecentCalls = (sid: string, query: Partial<CallQuery>) => {
|
||||
const qryStr = getQuery<Partial<CallQuery>>(query);
|
||||
|
||||
@@ -729,6 +867,26 @@ export const getAlerts = (sid: string, query: Partial<PageQuery>) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getPrice = () => {
|
||||
return getFetch<PriceInfo[]>(API_PRICE);
|
||||
};
|
||||
|
||||
export const getSpeechSupportedLanguagesAndVoices = (
|
||||
sid: string | undefined,
|
||||
vendor: string,
|
||||
label: string
|
||||
) => {
|
||||
const userData = parseJwt(getToken());
|
||||
const apiUrl =
|
||||
(userData.scope === USER_ACCOUNT
|
||||
? `${API_ACCOUNTS}/${userData.account_sid}`
|
||||
: `${API_SERVICE_PROVIDERS}/${sid}`) +
|
||||
`/SpeechCredentials/speech/supportedLanguagesAndVoices?vendor=${vendor}${
|
||||
label ? `&label=${label}` : ""
|
||||
}`;
|
||||
return getFetch<SpeechSupportedLanguagesAndVoices>(apiUrl);
|
||||
};
|
||||
|
||||
/** Hooks for components to fetch data with refetch method */
|
||||
|
||||
/** :GET /{apiPath} -- this is generic for any fetch of data collections */
|
||||
|
||||
@@ -37,14 +37,26 @@ 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 WaveSurferTtsLatencyResult {
|
||||
vendor: string;
|
||||
latency: string;
|
||||
isCached: string;
|
||||
}
|
||||
|
||||
export interface WaveSurferGatherSpeechVerbHookLatencyResult {
|
||||
statusCode: number;
|
||||
latency: string;
|
||||
}
|
||||
|
||||
export interface WaveSurferDtmfResult {
|
||||
dtmf: string;
|
||||
duration: string;
|
||||
}
|
||||
|
||||
221
src/api/types.ts
221
src/api/types.ts
@@ -1,4 +1,4 @@
|
||||
import type { Vendor } from "src/vendor/types";
|
||||
import type { Language, Model, Vendor, VoiceLanguage } from "src/vendor/types";
|
||||
|
||||
/** Simple types */
|
||||
|
||||
@@ -148,6 +148,7 @@ export interface User {
|
||||
service_provider_name?: string | null;
|
||||
initial_password?: string;
|
||||
permissions?: UserPermissions[];
|
||||
provider?: null | string;
|
||||
}
|
||||
|
||||
export interface UserLogin {
|
||||
@@ -186,6 +187,8 @@ export interface UserJWT {
|
||||
|
||||
export interface CurrentUserData {
|
||||
user: User;
|
||||
account?: Account;
|
||||
subscription?: null | Subscription;
|
||||
}
|
||||
|
||||
export interface ServiceProvider {
|
||||
@@ -245,6 +248,7 @@ export interface Smpp {
|
||||
export interface Account {
|
||||
name: string;
|
||||
sip_realm: null | string;
|
||||
root_domain?: null | string;
|
||||
account_sid: string;
|
||||
webhook_secret: string;
|
||||
siprec_hook_sid: null | string;
|
||||
@@ -255,6 +259,38 @@ export interface Account {
|
||||
record_all_calls: number;
|
||||
record_format?: null | string;
|
||||
bucket_credential: null | BucketCredential;
|
||||
plan_type?: string;
|
||||
device_to_call_ratio?: number;
|
||||
trial_end_date?: null | string;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
price_id?: null | string;
|
||||
product_sid?: null | string;
|
||||
name?: string;
|
||||
quantity?: number;
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
action?: null | string;
|
||||
payment_method_id?: null | string;
|
||||
account_subscription_sid?: null | string;
|
||||
stripe_customer_id?: null | string;
|
||||
products?: null | Product[];
|
||||
start_date?: string;
|
||||
status?: string;
|
||||
client_secret?: null | string;
|
||||
last4?: null | string;
|
||||
exp_month?: null | string;
|
||||
exp_year?: null | string;
|
||||
card_type?: null | string;
|
||||
reason?: null | string;
|
||||
dry_run?: boolean;
|
||||
currency?: null | string;
|
||||
prorated_cost?: number;
|
||||
monthly_cost?: number;
|
||||
next_invoice_date?: null | string;
|
||||
}
|
||||
|
||||
export interface AwsTag {
|
||||
@@ -269,6 +305,9 @@ export interface BucketCredential {
|
||||
access_key_id?: null | string;
|
||||
secret_access_key?: null | string;
|
||||
tags?: null | AwsTag[];
|
||||
service_key?: null | string;
|
||||
connection_string?: null | string;
|
||||
endpoint?: null | string;
|
||||
}
|
||||
|
||||
export interface Application {
|
||||
@@ -282,9 +321,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 {
|
||||
@@ -324,6 +373,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;
|
||||
@@ -339,8 +396,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;
|
||||
@@ -355,6 +414,11 @@ 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;
|
||||
options: null | string;
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
@@ -408,7 +472,6 @@ export interface PredefinedCarrier extends Carrier {
|
||||
export interface Gateway {
|
||||
voip_carrier_sid: string;
|
||||
ipv4: string;
|
||||
port: number;
|
||||
netmask: number;
|
||||
inbound: number;
|
||||
outbound: number;
|
||||
@@ -418,12 +481,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 {
|
||||
@@ -440,7 +506,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[];
|
||||
}
|
||||
@@ -459,6 +525,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 {
|
||||
@@ -473,6 +542,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;
|
||||
@@ -503,3 +579,142 @@ export interface EmptyResponse {
|
||||
export interface TotalResponse {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
service_provider_sid: string;
|
||||
provider: string;
|
||||
oauth2_code?: string;
|
||||
oauth2_state?: string;
|
||||
oauth2_client_id?: string;
|
||||
oauth2_redirect_uri?: string;
|
||||
locationBeforeAuth?: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
email_activation_code?: string;
|
||||
inviteCode?: string;
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
jwt: string;
|
||||
user_sid: string;
|
||||
account_sid: string;
|
||||
root_domain: string;
|
||||
}
|
||||
|
||||
export interface ActivationCode {
|
||||
user_sid: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Availability {
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
export interface Invoice {
|
||||
total: number;
|
||||
currency: null | string;
|
||||
next_payment_attempt: null | string;
|
||||
}
|
||||
|
||||
export type Currency = {
|
||||
[key: string]: null | string;
|
||||
};
|
||||
|
||||
export interface Recurring {
|
||||
aggregate_usage: null | string;
|
||||
interval: null | string;
|
||||
interval_count: number;
|
||||
trial_period_days: null | string;
|
||||
usage_type: string;
|
||||
}
|
||||
|
||||
export interface Price {
|
||||
billing_scheme: string;
|
||||
currency: string;
|
||||
recurring: Recurring;
|
||||
stripe_price_id: null | string;
|
||||
tiers_mode: null | string;
|
||||
type: null | string;
|
||||
unit_amount: number;
|
||||
unit_amount_decimal: null | string;
|
||||
}
|
||||
|
||||
export interface PriceInfo {
|
||||
category: null | string;
|
||||
description: null | string;
|
||||
name: null | string;
|
||||
prices: Price[];
|
||||
product_sid: null | string;
|
||||
stripe_product_id: null | string;
|
||||
unit_label: null | string;
|
||||
}
|
||||
|
||||
export interface StripeCustomerId {
|
||||
stripe_customer_id: null | string;
|
||||
}
|
||||
|
||||
export interface Tier {
|
||||
up_to: number;
|
||||
flat_amount: number;
|
||||
unit_amount: number;
|
||||
}
|
||||
|
||||
export interface ServiceData {
|
||||
category: null | string;
|
||||
name: null | string;
|
||||
service: null | string;
|
||||
fees: number;
|
||||
feesLabel: null | string;
|
||||
cost: number;
|
||||
capacity: number;
|
||||
invalid: boolean;
|
||||
currency: null | string;
|
||||
min: number;
|
||||
max: number;
|
||||
dirty: boolean;
|
||||
visible: boolean;
|
||||
required: boolean;
|
||||
billing_scheme?: null | string;
|
||||
stripe_price_id?: null | string;
|
||||
unit_label?: null | string;
|
||||
product_sid?: null | string;
|
||||
stripe_product_id?: null | string;
|
||||
tiers?: Tier[];
|
||||
}
|
||||
|
||||
export interface DeleteAccount {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ChangePassword {
|
||||
old_password: null | string;
|
||||
new_password: null | string;
|
||||
}
|
||||
|
||||
export interface SignIn {
|
||||
link?: null | string;
|
||||
jwt?: null | string;
|
||||
account_sid?: null | string;
|
||||
}
|
||||
|
||||
export interface GetLanguagesAndVoices {
|
||||
vendor: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface SpeechSupportedLanguagesAndVoices {
|
||||
tts: VoiceLanguage[];
|
||||
stt: Language[];
|
||||
models: Model[];
|
||||
}
|
||||
|
||||
export interface ElevenLabsOptions {
|
||||
optimize_streaming_latency: number;
|
||||
voice_settings: Partial<{
|
||||
similarity_boost: number;
|
||||
stability: number;
|
||||
style: number;
|
||||
use_speaker_boost: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
48
src/components/domain-input/index.tsx
Normal file
48
src/components/domain-input/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
import { Icons } from "../icons";
|
||||
import "./styles.scss";
|
||||
|
||||
type DomainInputProbs = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
value: string;
|
||||
setValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
root_domain: string;
|
||||
placeholder?: string;
|
||||
is_valid: boolean;
|
||||
};
|
||||
|
||||
export const DomainInput = ({
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
setValue,
|
||||
root_domain,
|
||||
is_valid,
|
||||
placeholder,
|
||||
}: DomainInputProbs) => {
|
||||
return (
|
||||
<>
|
||||
<div className="clipboard clipboard-domain">
|
||||
<div className="input-container">
|
||||
<input
|
||||
id={id}
|
||||
name={name}
|
||||
type="text"
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
<div className={`input-icon txt--${is_valid ? "teal" : "red"}`}>
|
||||
{is_valid ? <Icons.CheckCircle /> : <Icons.XCircle />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="root-domain">
|
||||
<p>{root_domain}</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomainInput;
|
||||
55
src/components/domain-input/styles.scss
Normal file
55
src/components/domain-input/styles.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
@use "../../styles/vars";
|
||||
@use "../../styles/mixins";
|
||||
@use "@jambonz/ui-kit/src/styles/vars" as ui-vars;
|
||||
@use "@jambonz/ui-kit/src/styles/mixins" as ui-mixins;
|
||||
|
||||
.input-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.clipboard-domain {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
width: 100%;
|
||||
height: vars.$clipheight;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.internal form & {
|
||||
max-width: calc(#{vars.$widthinput} - #{vars.$clipheight});
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
right: 5%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.root-domain {
|
||||
height: vars.$clipheight;
|
||||
border-bottom-right-radius: ui-vars.$px01;
|
||||
border-top-right-radius: ui-vars.$px01;
|
||||
border: 2px solid ui-vars.$grey;
|
||||
border-left: 0;
|
||||
background-color: ui-vars.$pink;
|
||||
padding: ui-vars.$px01;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&[disabled] {
|
||||
@include mixins.disabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/components/editboard/index.tsx
Normal file
39
src/components/editboard/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Icons } from "src/components/icons";
|
||||
|
||||
type EditBoardProps = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
text: string;
|
||||
path: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export const EditBoard = ({
|
||||
text,
|
||||
id = "",
|
||||
name = "",
|
||||
path,
|
||||
title,
|
||||
}: EditBoardProps) => {
|
||||
const navigate = useNavigate();
|
||||
const handleClick = () => {
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="clipboard inpbtn">
|
||||
<input id={id} name={name} type="text" readOnly value={text} />
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
title={title ? title : "Edit"}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Icons.Edit />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -7,6 +7,7 @@ type CheckzoneProps = {
|
||||
id?: string;
|
||||
name: string;
|
||||
label: string;
|
||||
labelNode?: React.ReactNode;
|
||||
hidden?: boolean;
|
||||
children: React.ReactNode;
|
||||
initialCheck: boolean;
|
||||
@@ -22,6 +23,7 @@ export const Checkzone = forwardRef<CheckzoneRef, CheckzoneProps>(
|
||||
id,
|
||||
name,
|
||||
label,
|
||||
labelNode,
|
||||
hidden = false,
|
||||
children,
|
||||
initialCheck,
|
||||
@@ -47,21 +49,24 @@ export const Checkzone = forwardRef<CheckzoneRef, CheckzoneProps>(
|
||||
return (
|
||||
<div className={classesTop}>
|
||||
<label>
|
||||
<input
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
name={name}
|
||||
id={id || name}
|
||||
onChange={(e) => {
|
||||
setChecked(e.target.checked);
|
||||
<div className="label-container">
|
||||
<input
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
name={name}
|
||||
id={id || name}
|
||||
onChange={(e) => {
|
||||
setChecked(e.target.checked);
|
||||
|
||||
if (handleChecked) {
|
||||
handleChecked(e);
|
||||
}
|
||||
}}
|
||||
checked={checked}
|
||||
/>
|
||||
<div>{label}</div>
|
||||
if (handleChecked) {
|
||||
handleChecked(e);
|
||||
}
|
||||
}}
|
||||
checked={checked}
|
||||
/>
|
||||
{label && <div>{label}</div>}
|
||||
{labelNode && labelNode}
|
||||
</div>
|
||||
</label>
|
||||
{checked && <div className={classesIn}>{children}</div>}
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
width: 100%;
|
||||
max-width: vars.$widthinput;
|
||||
|
||||
> label {
|
||||
.label-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
> label {
|
||||
input {
|
||||
margin-top: ui-vars.$px01;
|
||||
margin-right: ui-vars.$px02;
|
||||
}
|
||||
}
|
||||
@@ -35,6 +38,10 @@
|
||||
margin-top: ui-vars.$px01;
|
||||
}
|
||||
|
||||
> a {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.active {
|
||||
cursor: auto;
|
||||
opacity: 1;
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -48,6 +48,9 @@ import {
|
||||
ChevronsRight,
|
||||
Download,
|
||||
Smartphone,
|
||||
Youtube,
|
||||
Mail,
|
||||
Tag,
|
||||
} from "react-feather";
|
||||
|
||||
import type { Icon } from "react-feather";
|
||||
@@ -106,4 +109,7 @@ export const Icons: IconMap = {
|
||||
ChevronsRight,
|
||||
Download,
|
||||
Smartphone,
|
||||
Youtube,
|
||||
Mail,
|
||||
Tag,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import ReactDOM from "react-dom";
|
||||
import { Button, ButtonGroup } from "@jambonz/ui-kit";
|
||||
|
||||
import "./styles.scss";
|
||||
import { Spinner } from "../spinner";
|
||||
|
||||
type ModalProps = {
|
||||
disabled?: boolean;
|
||||
@@ -69,6 +70,7 @@ export const ModalForm = ({
|
||||
}}
|
||||
>
|
||||
<div className="modal__stuff">{children}</div>
|
||||
|
||||
<ButtonGroup right>
|
||||
<Button
|
||||
small
|
||||
@@ -114,3 +116,37 @@ export const ModalClose = ({ children, handleClose }: CloseProps) => {
|
||||
portal
|
||||
);
|
||||
};
|
||||
|
||||
type LoaderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ModalLoader = ({ children }: LoaderProps) => {
|
||||
return ReactDOM.createPortal(
|
||||
<div className="modal" role="presentation">
|
||||
<div
|
||||
className="modal__box"
|
||||
role="presentation"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
className="modal__stuff"
|
||||
role="presentation"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
portal
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
export const TOAST_TIME = 3000;
|
||||
export const TOAST_TIME = 5000;
|
||||
export const SESS_FLASH_MSG = "SESS_FLASH_MSG";
|
||||
export const SESS_USER_SID = "SESS_USER_SID";
|
||||
export const SESS_OLD_PASSWORD = "SESS_OLD_PASSWORD";
|
||||
@@ -24,4 +24,3 @@ export const MSG_WEBHOOK_FIELDS = (
|
||||
<span>password</span> fields are required.
|
||||
</>
|
||||
);
|
||||
export const NOT_AVAILABLE_PREFIX = "NotAvalable";
|
||||
|
||||
@@ -18,7 +18,10 @@ import { Scope, UserData } from "src/store/types";
|
||||
import type { Icon } from "react-feather";
|
||||
import type { ACL } from "src/store/types";
|
||||
import { Lcr } from "src/api/types";
|
||||
import { DISABLE_LCR } from "src/api/constants";
|
||||
import {
|
||||
DISABLE_LCR,
|
||||
ENABLE_HOSTED_SYSTEM as ENABLE_HOSTED_SYSTEM,
|
||||
} from "src/api/constants";
|
||||
|
||||
export interface NaviItem {
|
||||
label: string;
|
||||
@@ -30,11 +33,17 @@ export interface NaviItem {
|
||||
}
|
||||
|
||||
export const naviTop: NaviItem[] = [
|
||||
{
|
||||
label: "Users",
|
||||
icon: Icons.UserCheck,
|
||||
route: () => ROUTE_INTERNAL_USERS,
|
||||
},
|
||||
// User is not allowed in hosted app
|
||||
...(!ENABLE_HOSTED_SYSTEM
|
||||
? [
|
||||
{
|
||||
label: "Users",
|
||||
icon: Icons.UserCheck,
|
||||
route: () => ROUTE_INTERNAL_USERS,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
{
|
||||
label: "Settings",
|
||||
icon: Icons.Settings,
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import React, { useEffect } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import { Icons } from "src/components";
|
||||
import { ROUTE_INTERNAL_USERS } from "src/router/routes";
|
||||
import {
|
||||
ROUTE_INTERNAL_USERS,
|
||||
ROUTE_REGISTER_SUB_DOMAIN,
|
||||
} from "src/router/routes";
|
||||
import { useApiData } from "src/api";
|
||||
import { useSelectState } from "src/store";
|
||||
|
||||
import type { CurrentUserData } from "src/api/types";
|
||||
|
||||
import "./styles.scss";
|
||||
import { ENABLE_HOSTED_SYSTEM } from "src/api/constants";
|
||||
import { setRootDomain } from "src/store/localStore";
|
||||
|
||||
export const UserMe = () => {
|
||||
const user = useSelectState("user");
|
||||
const [userData] = useApiData<CurrentUserData>("Users/me");
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
// If hosted platform is enabled, the account should have sip realm
|
||||
if (ENABLE_HOSTED_SYSTEM && userData && !userData.account?.sip_realm) {
|
||||
setRootDomain(userData?.account?.root_domain || "");
|
||||
navigate(ROUTE_REGISTER_SUB_DOMAIN);
|
||||
}
|
||||
}, [userData]);
|
||||
|
||||
return (
|
||||
<div className="user">
|
||||
|
||||
100
src/containers/internal/views/accounts/edit-sip-realm.tsx
Normal file
100
src/containers/internal/views/accounts/edit-sip-realm.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Button, ButtonGroup, H1, MS } from "@jambonz/ui-kit";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { getAvailability, postSipRealms, useApiData } from "src/api";
|
||||
import { CurrentUserData } from "src/api/types";
|
||||
import { Section } from "src/components";
|
||||
import DomainInput from "src/components/domain-input";
|
||||
import { Message } from "src/components/forms";
|
||||
import { ROUTE_INTERNAL_ACCOUNTS } from "src/router/routes";
|
||||
import { hasValue } from "src/utils";
|
||||
|
||||
export const EditSipRealm = () => {
|
||||
const [name, setName] = useState("");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const [userData] = useApiData<CurrentUserData>("Users/me");
|
||||
const typingTimeoutRef = useRef<number | null>(null);
|
||||
const [isValidDomain, setIsValidDomain] = useState(false);
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const rootDomain = userData?.account?.root_domain;
|
||||
const account_sid = userData?.account?.account_sid;
|
||||
|
||||
postSipRealms(account_sid || "", `${name}.${rootDomain}`)
|
||||
.then(() => {
|
||||
navigate(`${ROUTE_INTERNAL_ACCOUNTS}/${account_sid}/edit`);
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.msg);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typingTimeoutRef.current) {
|
||||
clearTimeout(typingTimeoutRef.current);
|
||||
}
|
||||
if (!name || name.length < 3) {
|
||||
setIsValidDomain(false);
|
||||
return;
|
||||
}
|
||||
setIsValidDomain(false);
|
||||
typingTimeoutRef.current = setTimeout(() => {
|
||||
getAvailability(`${name}.${userData?.account?.root_domain}`)
|
||||
.then(({ json }) =>
|
||||
setIsValidDomain(
|
||||
Boolean(json.available) && hasValue(name) && name.length != 0
|
||||
)
|
||||
)
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.msg);
|
||||
setIsValidDomain(false);
|
||||
});
|
||||
}, 500);
|
||||
}, [name]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Edit Sip Realm</H1>
|
||||
<Section slim>
|
||||
<form className="form form--internal" onSubmit={handleSubmit}>
|
||||
<fieldset>
|
||||
<MS>
|
||||
This is the domain name where your carrier will send calls, and
|
||||
where you can register devices to.
|
||||
</MS>
|
||||
{errorMessage && <Message message={errorMessage} />}
|
||||
<br />
|
||||
<DomainInput
|
||||
id="sip_realm"
|
||||
name="sip_realm"
|
||||
value={name}
|
||||
setValue={setName}
|
||||
placeholder="Your name here"
|
||||
root_domain={`.${userData?.account?.root_domain || ""}`}
|
||||
is_valid={isValidDomain}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<ButtonGroup left>
|
||||
<Button
|
||||
small
|
||||
subStyle="grey"
|
||||
as={Link}
|
||||
to={`${ROUTE_INTERNAL_ACCOUNTS}/${userData?.account?.account_sid}/edit`}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" small disabled={!isValidDomain}>
|
||||
Change Sip Realm
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditSipRealm;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { P, Button, ButtonGroup, MS, Icon } from "@jambonz/ui-kit";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { P, Button, ButtonGroup, MS, Icon, H1 } from "@jambonz/ui-kit";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import {
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
deleteAccountLimit,
|
||||
deleteAccountTtsCache,
|
||||
postAccountBucketCredentialTest,
|
||||
deleteAccount,
|
||||
} from "src/api";
|
||||
import { ClipBoard, Icons, Modal, Section, Tooltip } from "src/components";
|
||||
import {
|
||||
@@ -21,20 +22,28 @@ import {
|
||||
Message,
|
||||
ApplicationSelect,
|
||||
LocalLimits,
|
||||
FileUpload,
|
||||
} from "src/components/forms";
|
||||
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,
|
||||
CurrencySymbol,
|
||||
DEFAULT_WEBHOOK,
|
||||
DISABLE_CALL_RECORDING,
|
||||
ENABLE_HOSTED_SYSTEM,
|
||||
PlanType,
|
||||
USER_ACCOUNT,
|
||||
WEBHOOK_METHODS,
|
||||
} from "src/api/constants";
|
||||
import { MSG_REQUIRED_FIELDS, MSG_WEBHOOK_FIELDS } from "src/constants";
|
||||
|
||||
import type {
|
||||
import {
|
||||
WebHook,
|
||||
Account,
|
||||
Application,
|
||||
@@ -44,9 +53,19 @@ import type {
|
||||
TtsCache,
|
||||
BucketCredential,
|
||||
AwsTag,
|
||||
Invoice,
|
||||
CurrentUserData,
|
||||
Carrier,
|
||||
SpeechCredential,
|
||||
} from "src/api/types";
|
||||
import { hasLength, hasValue } from "src/utils";
|
||||
import { useRegionVendors } from "src/vendor";
|
||||
import { GoogleServiceKey } from "src/vendor/types";
|
||||
import { getObscuredGoogleServiceKey } from "../speech-services/utils";
|
||||
import dayjs from "dayjs";
|
||||
import { EditBoard } from "src/components/editboard";
|
||||
import { ModalLoader } from "src/components/modal";
|
||||
import { useAuth } from "src/router/auth";
|
||||
|
||||
type AccountFormProps = {
|
||||
apps?: Application[];
|
||||
@@ -61,14 +80,22 @@ export const AccountForm = ({
|
||||
account,
|
||||
ttsCache,
|
||||
}: AccountFormProps) => {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const user = useSelectState("user");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [accounts] = useApiData<Account[]>("Accounts");
|
||||
const [invoice] = useApiData<Invoice>("Invoices");
|
||||
const [userData] = useApiData<CurrentUserData>("Users/me");
|
||||
const [userCarriers] = useApiData<Carrier[]>(`VoipCarriers`);
|
||||
const [userSpeechs] = useApiData<SpeechCredential[]>(
|
||||
`/Accounts/${params.account_sid}/SpeechCredentials`
|
||||
);
|
||||
const [name, setName] = useState("");
|
||||
const [realm, setRealm] = useState("");
|
||||
const [appId, setAppId] = useState("");
|
||||
const [recId, setRecId] = useState("");
|
||||
const { signout } = useAuth();
|
||||
const [regHook, setRegHook] = useState<WebHook>(DEFAULT_WEBHOOK);
|
||||
const [queueHook, setQueueHook] = useState<WebHook>(DEFAULT_WEBHOOK);
|
||||
const [modal, setModal] = useState(false);
|
||||
@@ -81,14 +108,31 @@ export const AccountForm = ({
|
||||
const [initialCheckRecordAllCall, setInitialCheckRecordAllCall] =
|
||||
useState(false);
|
||||
const [bucketVendor, setBucketVendor] = useState("");
|
||||
const [tmpBucketVendor, setTmpBucketVendor] = useState("");
|
||||
const [recordFormat, setRecordFormat] = useState("mp3");
|
||||
const [bucketRegion, setBucketRegion] = useState("us-east-1");
|
||||
const [bucketName, setBucketName] = useState("");
|
||||
const [tmpBucketName, setTmpBucketName] = useState("");
|
||||
const [bucketAccessKeyId, setBucketAccessKeyId] = useState("");
|
||||
const [bucketSecretAccessKey, setBucketSecretAccessKey] = useState("");
|
||||
const [bucketCredentialChecked, setBucketCredentialChecked] = useState(false);
|
||||
const [bucketTags, setBucketTags] = useState<AwsTag[]>([]);
|
||||
const [bucketGoogleServiceKey, setBucketGoogleServiceKey] =
|
||||
useState<GoogleServiceKey | null>(null);
|
||||
const [tmpBucketGoogleServiceKey, setTmpBucketGoogleServiceKey] =
|
||||
useState<GoogleServiceKey | null>(null);
|
||||
const regions = useRegionVendors();
|
||||
const [subscriptionDescription, setSubscriptionDescription] = useState("");
|
||||
const [isDeleteAccount, setIsDeleteAccount] = useState(false);
|
||||
const [requiresPassword, setRequiresPassword] = useState(true);
|
||||
const [deleteAccountPasswd, setDeleteAccountPasswd] = useState("");
|
||||
const [deleteMessage, setDeleteMessage] = useState("");
|
||||
const [isDisableDeleteAccountButton, setIsDisableDeleteAccountButton] =
|
||||
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 = [
|
||||
@@ -124,6 +168,49 @@ export const AccountForm = ({
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isDeleteAccount &&
|
||||
deleteMessageRef.current &&
|
||||
deleteMessageRef.current !== document.activeElement
|
||||
) {
|
||||
deleteMessageRef.current.focus();
|
||||
}
|
||||
}, [isDeleteAccount]);
|
||||
|
||||
const handleDeleteAccount = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (deleteMessage !== "delete my account") {
|
||||
toastError(
|
||||
"You must type the delete message correctly in order to delete your account."
|
||||
);
|
||||
if (
|
||||
deleteMessageRef.current &&
|
||||
deleteMessageRef.current !== document.activeElement
|
||||
) {
|
||||
deleteMessageRef.current.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
setIsDisableDeleteAccountButton(true);
|
||||
setIsShowModalLoader(true);
|
||||
|
||||
deleteAccount(userData?.account?.account_sid || "", {
|
||||
password: deleteAccountPasswd,
|
||||
})
|
||||
.then(() => {
|
||||
signout();
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsDisableDeleteAccountButton(false);
|
||||
setIsShowModalLoader(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirm = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -134,15 +221,57 @@ export const AccountForm = ({
|
||||
setModal(false);
|
||||
};
|
||||
|
||||
const handleFile = (file: File) => {
|
||||
const handleError = () => {
|
||||
setBucketGoogleServiceKey(null);
|
||||
setTmpBucketGoogleServiceKey(null);
|
||||
toastError("Invalid service key file, could not parse as JSON.");
|
||||
};
|
||||
|
||||
file
|
||||
.text()
|
||||
.then((text) => {
|
||||
try {
|
||||
const json: GoogleServiceKey = JSON.parse(text);
|
||||
|
||||
if (json.private_key && json.client_email) {
|
||||
setBucketGoogleServiceKey(json);
|
||||
setTmpBucketGoogleServiceKey(json);
|
||||
} else {
|
||||
setBucketGoogleServiceKey(null);
|
||||
setTmpBucketGoogleServiceKey(null);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
handleError();
|
||||
});
|
||||
};
|
||||
|
||||
const handleTestBucketCredential = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!account || !account.data) return;
|
||||
const cred: BucketCredential = {
|
||||
vendor: bucketVendor,
|
||||
region: bucketRegion,
|
||||
name: bucketName,
|
||||
access_key_id: bucketAccessKeyId,
|
||||
secret_access_key: bucketSecretAccessKey,
|
||||
...(bucketVendor === BUCKET_VENDOR_AWS && {
|
||||
region: bucketRegion,
|
||||
access_key_id: bucketAccessKeyId,
|
||||
secret_access_key: bucketSecretAccessKey,
|
||||
}),
|
||||
...(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(
|
||||
@@ -248,7 +377,7 @@ export const AccountForm = ({
|
||||
if (account && account.data) {
|
||||
putAccount(account.data.account_sid, {
|
||||
name,
|
||||
sip_realm: realm || null,
|
||||
...(!ENABLE_HOSTED_SYSTEM && { sip_realm: realm || null }),
|
||||
webhook_secret: account.data.webhook_secret,
|
||||
siprec_hook_sid: recId || null,
|
||||
queue_event_hook: queueHook || account.data.queue_event_hook,
|
||||
@@ -256,7 +385,7 @@ export const AccountForm = ({
|
||||
device_calling_application_sid: appId || null,
|
||||
record_all_calls: recordAllCalls ? 1 : 0,
|
||||
record_format: recordFormat ? recordFormat : "mp3",
|
||||
...(bucketVendor === "aws_s3" && {
|
||||
...(bucketVendor === BUCKET_VENDOR_AWS && {
|
||||
bucket_credential: {
|
||||
vendor: bucketVendor || null,
|
||||
region: bucketRegion || "us-east-1",
|
||||
@@ -266,6 +395,31 @@ export const AccountForm = ({
|
||||
...(hasLength(bucketTags) && { tags: bucketTags }),
|
||||
},
|
||||
}),
|
||||
...(bucketVendor === BUCKET_VENDOR_GOOGLE && {
|
||||
bucket_credential: {
|
||||
vendor: bucketVendor || null,
|
||||
service_key: JSON.stringify(bucketGoogleServiceKey),
|
||||
name: bucketName || null,
|
||||
...(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: {
|
||||
@@ -347,11 +501,15 @@ export const AccountForm = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (account.data.bucket_credential?.vendor) {
|
||||
if (tmpBucketVendor) {
|
||||
setBucketVendor(tmpBucketVendor);
|
||||
} else if (account.data.bucket_credential?.vendor) {
|
||||
setBucketVendor(account.data.bucket_credential?.vendor);
|
||||
}
|
||||
|
||||
if (account.data.bucket_credential?.name) {
|
||||
if (tmpBucketName) {
|
||||
setBucketName(tmpBucketName);
|
||||
} else if (account.data.bucket_credential?.name) {
|
||||
setBucketName(account.data.bucket_credential?.name);
|
||||
}
|
||||
|
||||
@@ -366,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);
|
||||
}
|
||||
@@ -378,12 +544,88 @@ export const AccountForm = ({
|
||||
if (account.data.record_format) {
|
||||
setRecordFormat(account.data.record_format || "mp3");
|
||||
}
|
||||
if (tmpBucketGoogleServiceKey) {
|
||||
setBucketGoogleServiceKey(tmpBucketGoogleServiceKey);
|
||||
} else if (account.data.bucket_credential?.service_key) {
|
||||
setBucketGoogleServiceKey(
|
||||
JSON.parse(account.data.bucket_credential?.service_key)
|
||||
);
|
||||
}
|
||||
setInitialCheckRecordAllCall(
|
||||
hasValue(bucketVendor) && bucketVendor.length !== 0
|
||||
);
|
||||
}
|
||||
}, [account]);
|
||||
|
||||
if (ENABLE_HOSTED_SYSTEM) {
|
||||
useEffect(() => {
|
||||
if (userData && userData.user) {
|
||||
setRequiresPassword(userData.user.provider === "local");
|
||||
}
|
||||
if (userData && userData.account) {
|
||||
const pType = userData.account.plan_type;
|
||||
const { products } = userData.subscription || {};
|
||||
const registeredDeviceRecord = products
|
||||
? products.find((item) => item.name === "registered device") || {
|
||||
quantity: 0,
|
||||
}
|
||||
: { quantity: 0 };
|
||||
const callSessionRecord = products
|
||||
? products.find(
|
||||
(item) => item.name === "concurrent call session"
|
||||
) || { quantity: 0 }
|
||||
: { quantity: 0 };
|
||||
const quantity =
|
||||
(userData.account.device_to_call_ratio || 0) *
|
||||
(callSessionRecord.quantity || 0) +
|
||||
(registeredDeviceRecord.quantity || 0);
|
||||
const { trial_end_date, is_active } = userData.account || {};
|
||||
switch (pType) {
|
||||
case PlanType.TRIAL:
|
||||
setSubscriptionDescription(
|
||||
`You are currently on the Free plan (trial period). You are limited to ${
|
||||
callSessionRecord.quantity
|
||||
} simultaneous calls and ${quantity} registered devices.${
|
||||
trial_end_date
|
||||
? ` Your free trial will end on ${dayjs(
|
||||
trial_end_date
|
||||
).format("MMM DD, YYYY")}.`
|
||||
: ""
|
||||
}`
|
||||
);
|
||||
break;
|
||||
case PlanType.PAID:
|
||||
if (invoice) {
|
||||
setSubscriptionDescription(
|
||||
`Your paid subscription includes capacity for ${
|
||||
callSessionRecord.quantity
|
||||
} simultaneous calls, and ${quantity} registered devices. You are billed ${
|
||||
CurrencySymbol[invoice.currency || "usd"]
|
||||
}${(invoice.total || 0) / 100} on ${dayjs
|
||||
.unix(Number(invoice.next_payment_attempt))
|
||||
.format("MMM DD, YYYY")}.`
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
case PlanType.FREE:
|
||||
if (is_active) {
|
||||
setSubscriptionDescription(
|
||||
`You are currently on the Free plan (trial period expired). You are limited to ${callSessionRecord.quantity} simultaneous calls and ${quantity} registered devices`
|
||||
);
|
||||
} else {
|
||||
setSubscriptionDescription(
|
||||
"Your free trial has expired. Please upgrade your subscription to a paid plan to continue service"
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Make sure Account page is alway scroll to top to see subscription
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}, [userData, invoice]);
|
||||
}
|
||||
|
||||
const updateBucketTags = (
|
||||
index: number,
|
||||
key: string,
|
||||
@@ -406,6 +648,166 @@ export const AccountForm = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{ENABLE_HOSTED_SYSTEM && (
|
||||
<>
|
||||
<Section>
|
||||
<H1 className="h5">Your Subscription</H1>
|
||||
<P>{subscriptionDescription}</P>
|
||||
<br />
|
||||
|
||||
<div className="mast">
|
||||
<ButtonGroup left>
|
||||
<Button
|
||||
type="button"
|
||||
mainStyle="hollow"
|
||||
subStyle="grey"
|
||||
small
|
||||
onClick={() => setIsDeleteAccount(true)}
|
||||
>
|
||||
Delete Account
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup right>
|
||||
{userData?.account?.plan_type === PlanType.PAID ? (
|
||||
<>
|
||||
<Button
|
||||
small
|
||||
as={Link}
|
||||
to={`${ROUTE_INTERNAL_ACCOUNTS}/${user?.account_sid}/manage-payment`}
|
||||
>
|
||||
Manage Payment Info
|
||||
</Button>
|
||||
<Button
|
||||
small
|
||||
as={Link}
|
||||
to={`${ROUTE_INTERNAL_ACCOUNTS}/${user?.account_sid}/modify-subscription`}
|
||||
>
|
||||
Modify My Subscription
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
as={Link}
|
||||
to={`${ROUTE_INTERNAL_ACCOUNTS}/${user?.account_sid}/subscription`}
|
||||
>
|
||||
Upgrade to a Paid Subscription
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</Section>
|
||||
{isDeleteAccount && (
|
||||
<Section slim>
|
||||
<form
|
||||
className={`form form--internal ${
|
||||
!account?.data && account?.refetch ? "form--blur" : ""
|
||||
}`}
|
||||
onSubmit={handleDeleteAccount}
|
||||
>
|
||||
<fieldset>
|
||||
<H1 className="h4">Delete Account</H1>
|
||||
<P>
|
||||
<span>
|
||||
<strong>Warning!</strong>
|
||||
</span>{" "}
|
||||
This will permantly delete all of your data from our
|
||||
database. You will not be able to restore your account. You
|
||||
must {requiresPassword && "provide your password and"} type
|
||||
“delete my account” into the Delete Message field.
|
||||
</P>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
{requiresPassword && (
|
||||
<>
|
||||
<label htmlFor="password">
|
||||
Password<span>*</span>
|
||||
</label>
|
||||
<Passwd
|
||||
id="delete_account_password"
|
||||
name="delete_account_password"
|
||||
value={deleteAccountPasswd}
|
||||
placeholder="Password"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setDeleteAccountPasswd(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<label htmlFor="deleteMessage">
|
||||
Delete Message<span>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="deleteMessage"
|
||||
required
|
||||
type="text"
|
||||
name="deleteMessage"
|
||||
placeholder="Delete Message"
|
||||
value={deleteMessage}
|
||||
ref={deleteMessageRef}
|
||||
onChange={(e) => setDeleteMessage(e.target.value)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<ButtonGroup right>
|
||||
<Button
|
||||
subStyle="grey"
|
||||
type="button"
|
||||
onClick={() => setIsDeleteAccount(false)}
|
||||
small
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isDisableDeleteAccountButton}
|
||||
small
|
||||
>
|
||||
PERMANENTLY DELETE MY ACCOUNT
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Section>
|
||||
)}
|
||||
{(!userCarriers ||
|
||||
userCarriers.length === 0 ||
|
||||
!userSpeechs ||
|
||||
userSpeechs.length === 0) && (
|
||||
<Section>
|
||||
<H1 className="h5">Finish Account Setup</H1>
|
||||
<H1 className="h6">To do</H1>
|
||||
{(!userCarriers || userCarriers.length === 0) && (
|
||||
<>
|
||||
<br />
|
||||
<div>
|
||||
<span>
|
||||
<Icons.Edit />
|
||||
Add a <Link to="/internal/carriers">carrier</Link> to
|
||||
route calls
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{(!userSpeechs || userSpeechs.length === 0) && (
|
||||
<>
|
||||
<br />
|
||||
<div>
|
||||
<span>
|
||||
<Icons.Edit />
|
||||
Add <Link to="/internal/speech-services">
|
||||
speech
|
||||
</Link>{" "}
|
||||
credentials for text-to-speech and speech-to-text
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Section slim>
|
||||
<form className="form form--internal" onSubmit={handleSubmit}>
|
||||
<fieldset>
|
||||
@@ -435,22 +837,34 @@ export const AccountForm = ({
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<LocalLimits
|
||||
data={limits && limits.data}
|
||||
limits={[localLimits, setLocalLimits]}
|
||||
/>
|
||||
</fieldset>
|
||||
{!ENABLE_HOSTED_SYSTEM && (
|
||||
<fieldset>
|
||||
<LocalLimits
|
||||
data={limits && limits.data}
|
||||
limits={[localLimits, setLocalLimits]}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
<fieldset>
|
||||
<label htmlFor="sip_realm">SIP realm</label>
|
||||
<input
|
||||
id="sip_realm"
|
||||
type="text"
|
||||
name="sip_realm"
|
||||
placeholder="The domain name that SIP devices will register with"
|
||||
value={realm}
|
||||
onChange={(e) => setRealm(e.target.value)}
|
||||
/>
|
||||
{ENABLE_HOSTED_SYSTEM ? (
|
||||
<EditBoard
|
||||
id="sip_realm"
|
||||
name="sip_realm"
|
||||
text={realm}
|
||||
title="Change SIP Realm"
|
||||
path={`/internal/accounts/${user?.account_sid}/sip-realm/edit`}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
id="sip_realm"
|
||||
type="text"
|
||||
name="sip_realm"
|
||||
placeholder="The domain name that SIP devices will register with"
|
||||
value={realm}
|
||||
onChange={(e) => setRealm(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</fieldset>
|
||||
{account && account.data && (
|
||||
<fieldset>
|
||||
@@ -639,45 +1053,75 @@ export const AccountForm = ({
|
||||
options={BUCKET_VENDOR_OPTIONS}
|
||||
onChange={(e) => {
|
||||
setBucketVendor(e.target.value);
|
||||
setTmpBucketVendor(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{bucketVendor === "aws_s3" && (
|
||||
{bucketVendor === BUCKET_VENDOR_S3_COMPATIBLE && (
|
||||
<>
|
||||
<label htmlFor="bucket_name">
|
||||
Bucket Name<span>*</span>
|
||||
<label htmlFor="endpoint">
|
||||
Endpoint URI<span>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="bucket_name"
|
||||
id="endpoint"
|
||||
required
|
||||
type="text"
|
||||
name="bucket_name"
|
||||
placeholder="Bucket"
|
||||
value={bucketName}
|
||||
name="endpoint"
|
||||
placeholder="https://domain.com"
|
||||
value={endpoint}
|
||||
onChange={(e) => {
|
||||
setBucketName(e.target.value);
|
||||
setEndpoint(e.target.value);
|
||||
}}
|
||||
/>
|
||||
{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_name">
|
||||
{bucketVendor === BUCKET_VENDOR_AZURE
|
||||
? "Container"
|
||||
: "Bucket"}{" "}
|
||||
Name<span>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="bucket_name"
|
||||
required
|
||||
type="text"
|
||||
name="bucket_name"
|
||||
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_S3_COMPATIBLE) && (
|
||||
<>
|
||||
{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>
|
||||
@@ -705,86 +1149,147 @@ export const AccountForm = ({
|
||||
setBucketSecretAccessKey(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="aws_s3_tags">S3 Tags</label>
|
||||
{hasLength(bucketTags) &&
|
||||
bucketTags.map((b, i) => (
|
||||
<div key={`s3_tags_${i}`} className="bucket_tag">
|
||||
<div>
|
||||
<div>
|
||||
<input
|
||||
id={`bucket_tag_name_${i}`}
|
||||
name={`bucket_tag_name_${i}`}
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
required
|
||||
value={b.Key}
|
||||
onChange={(e) => {
|
||||
updateBucketTags(i, "Key", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
id={`bucket_tag_value_${i}`}
|
||||
name={`bucket_tag_value_${i}`}
|
||||
type="text"
|
||||
placeholder="Value"
|
||||
required
|
||||
value={b.Value}
|
||||
onChange={(e) => {
|
||||
updateBucketTags(
|
||||
i,
|
||||
"Value",
|
||||
e.target.value
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="btnty"
|
||||
title="Delete Aws Tag"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setBucketTags(
|
||||
bucketTags.filter((g2, i2) => i2 !== i)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Icon>
|
||||
<Icons.Trash2 />
|
||||
</Icon>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<ButtonGroup left>
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
onClick={addBucketTag}
|
||||
title="Add S3 Tags"
|
||||
>
|
||||
<Icon subStyle="teal">
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup left>
|
||||
<Button
|
||||
onClick={handleTestBucketCredential}
|
||||
small
|
||||
disabled={
|
||||
!bucketName ||
|
||||
!bucketAccessKeyId ||
|
||||
!bucketSecretAccessKey
|
||||
}
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</>
|
||||
)}
|
||||
{bucketVendor === BUCKET_VENDOR_GOOGLE && (
|
||||
<>
|
||||
<label htmlFor="google_service_key">
|
||||
Service key<span>*</span>
|
||||
<Tooltip text="Provide a JSON key for a Service Account with APIs enabled for Cloud Storage and Storage Transfer API">
|
||||
{" "}
|
||||
</Tooltip>
|
||||
</label>
|
||||
<FileUpload
|
||||
id="google_service_key"
|
||||
name="google_service_key"
|
||||
handleFile={handleFile}
|
||||
placeholder="Choose a file"
|
||||
required={!bucketGoogleServiceKey}
|
||||
/>
|
||||
{bucketGoogleServiceKey && (
|
||||
<pre>
|
||||
<code>
|
||||
{JSON.stringify(
|
||||
getObscuredGoogleServiceKey(
|
||||
bucketGoogleServiceKey
|
||||
),
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</code>
|
||||
</pre>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{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_S3_COMPATIBLE
|
||||
? "S3"
|
||||
: bucketVendor === BUCKET_VENDOR_GOOGLE
|
||||
? "Google Cloud Storage"
|
||||
: bucketVendor === BUCKET_VENDOR_AZURE
|
||||
? "Azure Cloud Storage"
|
||||
: ""}{" "}
|
||||
Tags
|
||||
</label>
|
||||
{hasLength(bucketTags) &&
|
||||
bucketTags.map((b, i) => (
|
||||
<div key={`s3_tags_${i}`} className="bucket_tag">
|
||||
<div>
|
||||
<div>
|
||||
<input
|
||||
id={`bucket_tag_name_${i}`}
|
||||
name={`bucket_tag_name_${i}`}
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
required
|
||||
value={b.Key}
|
||||
onChange={(e) => {
|
||||
updateBucketTags(i, "Key", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
id={`bucket_tag_value_${i}`}
|
||||
name={`bucket_tag_value_${i}`}
|
||||
type="text"
|
||||
placeholder="Value"
|
||||
required
|
||||
value={b.Value}
|
||||
onChange={(e) => {
|
||||
updateBucketTags(i, "Value", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="btnty"
|
||||
title="Delete Aws Tag"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setBucketTags(
|
||||
bucketTags.filter((g2, i2) => i2 !== i)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Icon>
|
||||
<Icons.Trash2 />
|
||||
</Icon>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<ButtonGroup left>
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
onClick={addBucketTag}
|
||||
title="Add S3 Tags"
|
||||
>
|
||||
<Icon subStyle="teal">
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup left>
|
||||
<Button
|
||||
onClick={handleTestBucketCredential}
|
||||
small
|
||||
disabled={
|
||||
!bucketName ||
|
||||
(bucketVendor === BUCKET_VENDOR_AWS &&
|
||||
(!bucketAccessKeyId || !bucketSecretAccessKey)) ||
|
||||
(bucketVendor === BUCKET_VENDOR_GOOGLE &&
|
||||
!bucketGoogleServiceKey) ||
|
||||
(bucketVendor === BUCKET_VENDOR_AZURE &&
|
||||
!azureConnectionString) ||
|
||||
(bucketVendor === BUCKET_VENDOR_S3_COMPATIBLE &&
|
||||
(!endpoint ||
|
||||
!bucketAccessKeyId ||
|
||||
!bucketSecretAccessKey))
|
||||
}
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<label htmlFor="record_all_call" className="chk">
|
||||
<input
|
||||
id="record_all_call"
|
||||
@@ -793,11 +1298,7 @@ export const AccountForm = ({
|
||||
onChange={(e) => setRecordAllCalls(e.target.checked)}
|
||||
checked={recordAllCalls}
|
||||
/>
|
||||
<div></div>
|
||||
<Tooltip
|
||||
text="You can also record calls only to specific applications"
|
||||
subStyle="info"
|
||||
>
|
||||
<Tooltip text="You can also record calls only to specific applications">
|
||||
Record all calls for this account
|
||||
</Tooltip>
|
||||
</label>
|
||||
@@ -813,14 +1314,17 @@ export const AccountForm = ({
|
||||
)}
|
||||
<fieldset>
|
||||
<ButtonGroup left>
|
||||
<Button
|
||||
small
|
||||
subStyle="grey"
|
||||
as={Link}
|
||||
to={ROUTE_INTERNAL_ACCOUNTS}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{user?.scope != USER_ACCOUNT && (
|
||||
<Button
|
||||
small
|
||||
subStyle="grey"
|
||||
as={Link}
|
||||
to={ROUTE_INTERNAL_ACCOUNTS}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button type="submit" small>
|
||||
Save
|
||||
</Button>
|
||||
@@ -844,6 +1348,14 @@ export const AccountForm = ({
|
||||
<P>Are you sure you want to clean TTS cache for this account?</P>
|
||||
</Modal>
|
||||
)}
|
||||
{isShowModalLoader && (
|
||||
<ModalLoader>
|
||||
<P>
|
||||
Your requested changes are being processed. Please do not leave the
|
||||
page or hit the back button until complete.
|
||||
</P>
|
||||
</ModalLoader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ export const Accounts = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteAccount(account.account_sid)
|
||||
deleteAccount(account.account_sid, {})
|
||||
.then(() => {
|
||||
refetch();
|
||||
setAccount(null);
|
||||
@@ -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]}
|
||||
|
||||
191
src/containers/internal/views/accounts/manage-payment-form.tsx
Normal file
191
src/containers/internal/views/accounts/manage-payment-form.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import { Button, ButtonGroup, H1, P } from "@jambonz/ui-kit";
|
||||
import {
|
||||
PaymentElement,
|
||||
useElements,
|
||||
useStripe,
|
||||
} from "@stripe/react-stripe-js";
|
||||
import React, { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { postSubscriptions, useApiData } from "src/api";
|
||||
import { CurrentUserData, Subscription } from "src/api/types";
|
||||
import { Section } from "src/components";
|
||||
import { ROUTE_INTERNAL_ACCOUNTS } from "src/router/routes";
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import { PaymentMethod } from "@stripe/stripe-js";
|
||||
import { ModalLoader } from "src/components/modal";
|
||||
|
||||
export const ManagePaymentForm = () => {
|
||||
const user = useSelectState("user");
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
const [userData] = useApiData<CurrentUserData>("Users/me");
|
||||
const [isChangePayment, setIsChangePayment] = useState(false);
|
||||
const [isSavingNewCard, setIsSavingNewCard] = useState(false);
|
||||
const [isShowModalLoader, setIsShowModalLoader] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const createSubscription = async (paymentMethod: PaymentMethod) => {
|
||||
const body: Subscription = {
|
||||
action: "update-payment-method",
|
||||
payment_method_id: paymentMethod.id,
|
||||
};
|
||||
|
||||
postSubscriptions(body)
|
||||
.then(({ json }) => {
|
||||
if (json.status === "success") {
|
||||
toastSuccess("Payment completed successfully");
|
||||
navigate(
|
||||
`${ROUTE_INTERNAL_ACCOUNTS}/${userData?.account?.account_sid}/edit`
|
||||
);
|
||||
} else if (json.status === "action required") {
|
||||
if (stripe) {
|
||||
const location = window.location;
|
||||
stripe
|
||||
.confirmPayment({
|
||||
clientSecret: json.client_secret || "",
|
||||
confirmParams: {
|
||||
return_url: `${location.protocol}//${location.host}${ROUTE_INTERNAL_ACCOUNTS}/${userData?.account?.account_sid}/edit`,
|
||||
},
|
||||
})
|
||||
.then((error) => {
|
||||
if (error) {
|
||||
toastError(error.error.message || "");
|
||||
return;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSavingNewCard(false);
|
||||
setIsShowModalLoader(false);
|
||||
});
|
||||
}
|
||||
} else if (json.status === "card error") {
|
||||
setIsSavingNewCard(false);
|
||||
setIsShowModalLoader(false);
|
||||
toastError(json.reason || "Something went wrong, please try again.");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg || "Something went wrong, please try again.");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSavingNewCard(false);
|
||||
setIsShowModalLoader(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveNewCard = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!stripe || !elements) {
|
||||
return;
|
||||
}
|
||||
|
||||
const card = elements.getElement(PaymentElement);
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
const { error: elementsError } = await elements.submit();
|
||||
if (elementsError) {
|
||||
toastError(elementsError.message || "");
|
||||
return;
|
||||
}
|
||||
const { error, paymentMethod } = await stripe.createPaymentMethod({
|
||||
element: card,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
toastError(error.message || "Something went wrong, please try again.");
|
||||
return;
|
||||
}
|
||||
setIsSavingNewCard(true);
|
||||
setIsShowModalLoader(true);
|
||||
createSubscription(paymentMethod);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Manage Payment Information</H1>
|
||||
{userData?.subscription && (
|
||||
<Section>
|
||||
<H1 className="h3">Current Payment Information</H1>
|
||||
<div className="item__details">
|
||||
<div className="pre-grid-white">
|
||||
<div>Card Type:</div>
|
||||
<div>{userData.subscription.card_type}</div>
|
||||
<div>Card Number:</div>
|
||||
<div>
|
||||
{userData.subscription.last4
|
||||
? `**** **** **** ${userData.subscription.last4}`
|
||||
: ""}
|
||||
</div>
|
||||
<div>Expiration:</div>
|
||||
<div>
|
||||
{userData.subscription.exp_year
|
||||
? `${userData.subscription.exp_month}/${userData.subscription.exp_year}`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonGroup right>
|
||||
<Button
|
||||
type="button"
|
||||
subStyle="grey"
|
||||
as={Link}
|
||||
to={`${ROUTE_INTERNAL_ACCOUNTS}/${user?.account_sid}/edit`}
|
||||
small
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => setIsChangePayment(true)} small>
|
||||
Change Payment Info
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Section>
|
||||
)}
|
||||
{isChangePayment && (
|
||||
<Section>
|
||||
<div className="grid--col4--users">
|
||||
<H1 className="h3">New Payment Information</H1>
|
||||
<div className="grid__row">
|
||||
<div></div>
|
||||
<div>
|
||||
<PaymentElement
|
||||
options={{
|
||||
paymentMethodOrder: ["card"],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonGroup right>
|
||||
<Button
|
||||
type="button"
|
||||
subStyle="grey"
|
||||
onClick={() => setIsChangePayment(false)}
|
||||
small
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSaveNewCard}
|
||||
disabled={!stripe || isSavingNewCard}
|
||||
small
|
||||
>
|
||||
Save New Card
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Section>
|
||||
)}
|
||||
{isShowModalLoader && (
|
||||
<ModalLoader>
|
||||
<P>
|
||||
Your requested changes are being processed. Please do not leave the
|
||||
page or hit the back button until complete.
|
||||
</P>
|
||||
</ModalLoader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManagePaymentForm;
|
||||
23
src/containers/internal/views/accounts/manage-payment.tsx
Normal file
23
src/containers/internal/views/accounts/manage-payment.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Elements } from "@stripe/react-stripe-js";
|
||||
import { stripePromise } from "./subscription";
|
||||
import ManagePaymentForm from "./manage-payment-form";
|
||||
import React from "react";
|
||||
|
||||
export const ManagePayment = () => {
|
||||
return (
|
||||
<>
|
||||
<Elements
|
||||
stripe={stripePromise}
|
||||
options={{
|
||||
mode: "setup",
|
||||
currency: "usd",
|
||||
paymentMethodCreation: "manual",
|
||||
}}
|
||||
>
|
||||
<ManagePaymentForm />
|
||||
</Elements>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManagePayment;
|
||||
633
src/containers/internal/views/accounts/subscription-form.tsx
Normal file
633
src/containers/internal/views/accounts/subscription-form.tsx
Normal file
@@ -0,0 +1,633 @@
|
||||
import { Button, ButtonGroup, H1, P } from "@jambonz/ui-kit";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { postSubscriptions, useApiData } from "src/api";
|
||||
import { CurrencySymbol } from "src/api/constants";
|
||||
import {
|
||||
CurrentUserData,
|
||||
PriceInfo,
|
||||
ServiceData,
|
||||
StripeCustomerId,
|
||||
Subscription,
|
||||
} from "src/api/types";
|
||||
import { Modal, Section } from "src/components";
|
||||
import { ROUTE_INTERNAL_ACCOUNTS } from "src/router/routes";
|
||||
import { hasValue } from "src/utils";
|
||||
import {
|
||||
PaymentElement,
|
||||
useElements,
|
||||
useStripe,
|
||||
} from "@stripe/react-stripe-js";
|
||||
import { PaymentMethod } from "@stripe/stripe-js";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
import { ModalLoader } from "src/components/modal";
|
||||
|
||||
const SubscriptionForm = () => {
|
||||
const [userData] = useApiData<CurrentUserData>("Users/me");
|
||||
const [priceInfo] = useApiData<PriceInfo[]>("/Prices");
|
||||
const [userStripeInfo] = useApiData<StripeCustomerId>("/StripeCustomerId");
|
||||
const [total, setTotal] = useState(0);
|
||||
const [cardErrorCase, setCardErrorCase] = useState(false);
|
||||
const [isReviewChanges, setIsReviewChanges] = useState(false);
|
||||
const [isReturnToFreePlan, setIsReturnToFreePlan] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const isModifySubscription = location.pathname.includes(
|
||||
"modify-subscription"
|
||||
);
|
||||
const [billingCharge, setBillingCharge] = useState<Subscription | null>(null);
|
||||
const [isShowModalLoader, setIsShowModalLoader] = useState(false);
|
||||
const [isDisableSubmitButton, setIsDisableSubmitButton] =
|
||||
useState(isModifySubscription);
|
||||
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
|
||||
const createSubscription = async (paymentMethod: PaymentMethod) => {
|
||||
let body: Subscription = {};
|
||||
|
||||
if (cardErrorCase) {
|
||||
body = {
|
||||
action: "update-payment-method",
|
||||
payment_method_id: paymentMethod.id,
|
||||
};
|
||||
} else {
|
||||
body = {
|
||||
action: "upgrade-to-paid",
|
||||
payment_method_id: paymentMethod.id,
|
||||
stripe_customer_id: userStripeInfo?.stripe_customer_id,
|
||||
products: serviceData.map((service) => ({
|
||||
price_id: service.stripe_price_id,
|
||||
product_sid: service.product_sid,
|
||||
quantity: service.capacity || 0,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
postSubscriptions(body)
|
||||
.then(({ json }) => {
|
||||
if (json.status === "success") {
|
||||
toastSuccess("Payment completed successfully");
|
||||
navigate(
|
||||
`${ROUTE_INTERNAL_ACCOUNTS}/${userData?.account?.account_sid}/edit`
|
||||
);
|
||||
} else if (json.status === "action required") {
|
||||
if (stripe) {
|
||||
const location = window.location;
|
||||
stripe
|
||||
.confirmPayment({
|
||||
clientSecret: json.client_secret || "",
|
||||
confirmParams: {
|
||||
return_url: `${location.protocol}//${location.host}${ROUTE_INTERNAL_ACCOUNTS}/${userData?.account?.account_sid}/edit`,
|
||||
},
|
||||
})
|
||||
.then((error) => {
|
||||
if (error) {
|
||||
toastError(error.error.message || "");
|
||||
return;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsDisableSubmitButton(false);
|
||||
setIsShowModalLoader(false);
|
||||
});
|
||||
}
|
||||
} else if (json.status === "card error") {
|
||||
setIsDisableSubmitButton(false);
|
||||
setIsShowModalLoader(false);
|
||||
setCardErrorCase(true);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsDisableSubmitButton(false);
|
||||
setIsShowModalLoader(false);
|
||||
toastError(error.msg || "Something went wrong, please try again.");
|
||||
});
|
||||
};
|
||||
|
||||
const retrieveBillingChanges = async () => {
|
||||
const updatedProducts = serviceData.map((product) => ({
|
||||
price_id: product.stripe_price_id,
|
||||
product_sid: product.product_sid,
|
||||
quantity: product.capacity || 0,
|
||||
}));
|
||||
|
||||
postSubscriptions({
|
||||
action: "update-quantities",
|
||||
dry_run: true,
|
||||
products: updatedProducts,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
setBillingCharge(json);
|
||||
setIsReviewChanges(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg || "Something went wrong, please try again.");
|
||||
setIsDisableSubmitButton(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!stripe || !elements) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDisableSubmitButton(true);
|
||||
if (isModifySubscription) {
|
||||
retrieveBillingChanges();
|
||||
return;
|
||||
}
|
||||
setIsShowModalLoader(true);
|
||||
const { error: elementsError } = await elements.submit();
|
||||
if (elementsError) {
|
||||
setIsDisableSubmitButton(false);
|
||||
setIsShowModalLoader(false);
|
||||
toastError(elementsError.message || "");
|
||||
return;
|
||||
}
|
||||
const card = elements.getElement(PaymentElement);
|
||||
if (!card) {
|
||||
setIsDisableSubmitButton(false);
|
||||
setIsShowModalLoader(false);
|
||||
return;
|
||||
}
|
||||
const { error, paymentMethod } = await stripe.createPaymentMethod({
|
||||
element: card,
|
||||
});
|
||||
if (error) {
|
||||
setIsDisableSubmitButton(false);
|
||||
setIsShowModalLoader(false);
|
||||
toastError(error.message || "");
|
||||
return;
|
||||
}
|
||||
|
||||
createSubscription(paymentMethod);
|
||||
};
|
||||
|
||||
const handleReturnToFreePlan = () => {
|
||||
setIsReturnToFreePlan(false);
|
||||
setIsShowModalLoader(true);
|
||||
const body: Subscription = {
|
||||
action: "downgrade-to-free",
|
||||
};
|
||||
|
||||
postSubscriptions(body)
|
||||
.then(() => {
|
||||
toastSuccess("Downgrade to free plan completed successfully");
|
||||
navigate(
|
||||
`${ROUTE_INTERNAL_ACCOUNTS}/${userData?.account?.account_sid}/edit`
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
})
|
||||
.finally(() => setIsShowModalLoader(false));
|
||||
};
|
||||
|
||||
const handleReviewChangeSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsShowModalLoader(true);
|
||||
|
||||
const updatedProducts = serviceData.map((product) => ({
|
||||
price_id: product.stripe_price_id,
|
||||
product_sid: product.product_sid,
|
||||
quantity: product.capacity,
|
||||
}));
|
||||
|
||||
postSubscriptions({
|
||||
action: "update-quantities",
|
||||
products: updatedProducts,
|
||||
})
|
||||
.then(() => {
|
||||
toastSuccess(
|
||||
"Your subscription capacity has been successfully modified."
|
||||
);
|
||||
navigate(
|
||||
`${ROUTE_INTERNAL_ACCOUNTS}/${userData?.account?.account_sid}/edit`
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toastError(
|
||||
`The additional capacity you that you requested could not be granted due to a failure processing payment.
|
||||
Please configure a valid credit card for your account and the upgrade will be automatically processed`
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsShowModalLoader(false);
|
||||
setIsDisableSubmitButton(false);
|
||||
});
|
||||
};
|
||||
// subscription categories
|
||||
const [serviceData, setServiceData] = useState<ServiceData[]>([
|
||||
{
|
||||
category: "voice_call_session",
|
||||
name: "concurrent call session",
|
||||
service: "Maximum concurrent call sessions",
|
||||
fees: 0,
|
||||
feesLabel: "",
|
||||
cost: 0,
|
||||
capacity: 0,
|
||||
invalid: false,
|
||||
currency: "usd",
|
||||
min: 5,
|
||||
max: 1000,
|
||||
dirty: false,
|
||||
visible: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
category: "device",
|
||||
name: "registered device",
|
||||
service: "Additional device registrations",
|
||||
fees: 0,
|
||||
feesLabel: "",
|
||||
cost: 0,
|
||||
capacity: 0,
|
||||
invalid: false,
|
||||
currency: "usd",
|
||||
min: 0,
|
||||
max: 200,
|
||||
dirty: false,
|
||||
visible: false,
|
||||
required: false,
|
||||
},
|
||||
]);
|
||||
const [originalServiceData, setOriginalServiceData] = useState<ServiceData[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
const initFeesAndCost = (priceData: PriceInfo[]) => {
|
||||
serviceData.forEach((service) => {
|
||||
const record = priceData.find(
|
||||
(item) => item.category === service.category
|
||||
);
|
||||
|
||||
if (record) {
|
||||
const price = record.prices.find(
|
||||
(item) => item.currency === service.currency
|
||||
);
|
||||
|
||||
if (price) {
|
||||
let fees = 0;
|
||||
switch (price.billing_scheme) {
|
||||
case "per_unit":
|
||||
fees = (price.unit_amount * 1) / 100;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
service.billing_scheme = price.billing_scheme;
|
||||
service.stripe_price_id = price.stripe_price_id;
|
||||
service.unit_label = record.unit_label;
|
||||
service.product_sid = record.product_sid;
|
||||
service.stripe_product_id = record.stripe_product_id;
|
||||
service.fees = fees;
|
||||
service.feesLabel = `${
|
||||
CurrencySymbol[service.currency || "usd"]
|
||||
}${fees} per ${
|
||||
record.unit_label?.slice(0, 3) === "per"
|
||||
? record.unit_label.slice(3)
|
||||
: record.unit_label
|
||||
}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setServiceData([...serviceData]);
|
||||
};
|
||||
|
||||
const getServicePrice = (
|
||||
service: ServiceData,
|
||||
capacity: number
|
||||
): [number, string, number] => {
|
||||
let fees = 0;
|
||||
let feesLabel = "";
|
||||
let cost = 0;
|
||||
const capacityNum = capacity;
|
||||
if (service.billing_scheme === "per_unit") {
|
||||
fees = service.fees;
|
||||
cost = fees * capacityNum;
|
||||
} else if (service.billing_scheme === "tiered") {
|
||||
const filteredTiers = service.tiers
|
||||
? service.tiers.filter(
|
||||
(item) => !item.up_to || item.up_to >= capacityNum
|
||||
)
|
||||
: [];
|
||||
if (filteredTiers.length) {
|
||||
const tier = filteredTiers[0];
|
||||
if (typeof tier.flat_amount === "number") {
|
||||
fees = tier.flat_amount / 100;
|
||||
cost = fees;
|
||||
} else {
|
||||
fees = tier.unit_amount / 100;
|
||||
cost = fees * capacityNum;
|
||||
}
|
||||
}
|
||||
}
|
||||
feesLabel = `${CurrencySymbol[service.currency || "usd"]}${fees} per ${
|
||||
service.unit_label && service.unit_label.slice(0, 3) === "per"
|
||||
? service.unit_label.slice(3)
|
||||
: service.unit_label
|
||||
}`;
|
||||
|
||||
return [fees, feesLabel, cost];
|
||||
};
|
||||
|
||||
const setProductsInfo = (data: CurrentUserData) => {
|
||||
const { products } = data.subscription || {};
|
||||
|
||||
const services = serviceData.map((service) => {
|
||||
const { quantity } = products
|
||||
? products.find((item) => item.name === service.name) || {}
|
||||
: { quantity: null };
|
||||
const [fees, feesLabel, cost] = getServicePrice(service, quantity || 0);
|
||||
return {
|
||||
...service,
|
||||
capacity: quantity || 0,
|
||||
invalid: false,
|
||||
fees,
|
||||
feesLabel,
|
||||
cost,
|
||||
visible: hasValue(quantity) && quantity > 0,
|
||||
};
|
||||
});
|
||||
|
||||
setServiceData(services);
|
||||
setOriginalServiceData([...services]);
|
||||
};
|
||||
|
||||
const updateServiceData = (
|
||||
index: number,
|
||||
key: string,
|
||||
value: typeof serviceData[number][keyof ServiceData]
|
||||
) => {
|
||||
setServiceData(
|
||||
serviceData.map((g, i) =>
|
||||
i === index
|
||||
? {
|
||||
...g,
|
||||
[key]: value,
|
||||
...(key === "capacity" && { cost: Number(value) * g.fees }),
|
||||
}
|
||||
: g
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (priceInfo) {
|
||||
initFeesAndCost(priceInfo);
|
||||
}
|
||||
|
||||
if (userData && priceInfo) {
|
||||
setProductsInfo(userData);
|
||||
}
|
||||
}, [priceInfo, userData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isModifySubscription && originalServiceData.length > 0) {
|
||||
setIsDisableSubmitButton(
|
||||
serviceData[0].capacity === originalServiceData[0].capacity &&
|
||||
serviceData[1].capacity === originalServiceData[1].capacity
|
||||
);
|
||||
}
|
||||
setTotal(serviceData.reduce((res, service) => res + service.cost || 0, 0));
|
||||
}, [serviceData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">
|
||||
{isModifySubscription
|
||||
? "Configure Your Subscription"
|
||||
: "Upgrade your Subscription"}
|
||||
</H1>
|
||||
{isShowModalLoader && (
|
||||
<ModalLoader>
|
||||
<P>
|
||||
Your requested changes are being processed. Please do not leave the
|
||||
page or hit the back button until complete.
|
||||
</P>
|
||||
</ModalLoader>
|
||||
)}
|
||||
{isReviewChanges && !isShowModalLoader && (
|
||||
<Modal
|
||||
handleCancel={() => {
|
||||
setIsReviewChanges(false);
|
||||
setIsDisableSubmitButton(false);
|
||||
}}
|
||||
handleSubmit={handleReviewChangeSubmit}
|
||||
>
|
||||
<H1 className="h4">Confirm Changes</H1>
|
||||
<P>
|
||||
By pressing{" "}
|
||||
<span>
|
||||
<strong>Confirm</strong>
|
||||
</span>{" "}
|
||||
below, your plan will be immediately adjusted to the following
|
||||
levels:
|
||||
</P>
|
||||
<ul className="m">
|
||||
<li>{`- ${serviceData[0].capacity} simultaneous calls`}</li>
|
||||
{userData?.account && userData?.account.device_to_call_ratio && (
|
||||
<li>{`- ${
|
||||
userData?.account.device_to_call_ratio *
|
||||
(serviceData[0].capacity + serviceData[1].capacity)
|
||||
} registered devices`}</li>
|
||||
)}
|
||||
</ul>
|
||||
<P>
|
||||
{(billingCharge?.prorated_cost || 0) > 0 &&
|
||||
`Your new monthly charge will be $${
|
||||
(billingCharge?.monthly_cost || 0) / 100
|
||||
}, and you will immediately be charged a one-time prorated amount of $${
|
||||
(billingCharge?.prorated_cost || 0) / 100
|
||||
} to cover the remainder of the current billing period.`}
|
||||
{billingCharge?.prorated_cost === 0 &&
|
||||
`Your monthly charge will be $${
|
||||
(billingCharge.monthly_cost || 0) / 100
|
||||
}.`}
|
||||
{(billingCharge?.prorated_cost || 0) < 0 &&
|
||||
`Your new monthly charge will be $${
|
||||
(billingCharge?.monthly_cost || 0) / 100
|
||||
}, and you will receive a credit of $${
|
||||
-(billingCharge?.prorated_cost || 0) / 100
|
||||
} on your next invoice to reflect changes made during the current billing period.`}
|
||||
</P>
|
||||
</Modal>
|
||||
)}
|
||||
{isReturnToFreePlan && !isShowModalLoader && (
|
||||
<Modal
|
||||
handleCancel={() => setIsReturnToFreePlan(false)}
|
||||
handleSubmit={handleReturnToFreePlan}
|
||||
>
|
||||
<H1 className="h4">Return to Free Plan</H1>
|
||||
<P>
|
||||
Returning to the free plan will reduce your capacity to a maximum of
|
||||
1 simultaneous call session and 1 registered device. Your current
|
||||
plan and capacity will continue through the rest of the billing
|
||||
cycle and your plan change will take effect at the beginning of the
|
||||
next billing cycle. Are you sure you want to continue?
|
||||
</P>
|
||||
</Modal>
|
||||
)}
|
||||
<Section slim>
|
||||
<form className="form form--internal" onSubmit={handleSubmit}>
|
||||
<div className="grid grid--col4--users">
|
||||
<div className="grid__row grid__th">
|
||||
<div>Service</div>
|
||||
<div>Capacity</div>
|
||||
<div>Price</div>
|
||||
<div>Cost</div>
|
||||
</div>
|
||||
|
||||
{serviceData &&
|
||||
serviceData
|
||||
.filter((service) => service.visible)
|
||||
.map((service, idx) => (
|
||||
<React.Fragment key={`subscription-${idx}`}>
|
||||
<div className="grid__row">
|
||||
<div>
|
||||
<label htmlFor={service.name || ""}>
|
||||
{service.service}
|
||||
<span>*</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
id="tech_prefix"
|
||||
name="tech_prefix"
|
||||
type="number"
|
||||
value={service.capacity}
|
||||
required
|
||||
min={service.min}
|
||||
max={service.max}
|
||||
onChange={(e) => {
|
||||
updateServiceData(
|
||||
idx,
|
||||
"capacity",
|
||||
e.target.value ? Number(e.target.value) : ""
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<em>{service.feesLabel}</em>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<P>
|
||||
<strong>
|
||||
{CurrencySymbol[service.currency || "usd"]}
|
||||
{service.cost}
|
||||
</strong>
|
||||
</P>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{serviceData[0].capacity !== 0 && !serviceData[1].visible && (
|
||||
<>
|
||||
<div className="grid__row">
|
||||
<label htmlFor="max_concurrent_call_sessons">
|
||||
{`With ${
|
||||
serviceData[0].capacity
|
||||
} call sessions you can register ${
|
||||
serviceData[0].capacity *
|
||||
(userData?.account?.device_to_call_ratio || 0)
|
||||
} concurrent devices`}
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
mainStyle="hollow"
|
||||
onClick={() =>
|
||||
setServiceData((prev) => {
|
||||
prev[1].visible = true;
|
||||
return [...prev];
|
||||
})
|
||||
}
|
||||
>
|
||||
Would you like to purchase additional device
|
||||
registrations?
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="grid__row">
|
||||
<div>
|
||||
<label htmlFor="total">Total Monthly Cost</label>
|
||||
</div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div>
|
||||
<P>
|
||||
<strong>
|
||||
{CurrencySymbol[serviceData[0].currency || "usd"]}
|
||||
{total}
|
||||
</strong>
|
||||
</P>
|
||||
</div>
|
||||
</div>
|
||||
{!isModifySubscription && (
|
||||
<fieldset>
|
||||
<label htmlFor="total">Payment Information</label>
|
||||
<div className="grid__row">
|
||||
<div></div>
|
||||
<div>
|
||||
<PaymentElement
|
||||
options={{
|
||||
paymentMethodOrder: ["card"],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
)}
|
||||
</div>
|
||||
<fieldset>
|
||||
<>
|
||||
<div className={isModifySubscription ? "mast" : ""}>
|
||||
{isModifySubscription && (
|
||||
<ButtonGroup right>
|
||||
<Button
|
||||
type="button"
|
||||
subStyle="grey"
|
||||
mainStyle="hollow"
|
||||
onClick={() => setIsReturnToFreePlan(true)}
|
||||
small
|
||||
>
|
||||
Return to free plan
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
|
||||
<ButtonGroup right>
|
||||
<Button
|
||||
subStyle="grey"
|
||||
as={Link}
|
||||
to={`${ROUTE_INTERNAL_ACCOUNTS}/${userData?.account?.account_sid}/edit`}
|
||||
small
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button type="submit" disabled={isDisableSubmitButton} small>
|
||||
{isModifySubscription
|
||||
? "Review Changes"
|
||||
: `Pay ${CurrencySymbol[serviceData[0].currency || "usd"]}
|
||||
${total} and Upgrade to Paid Plan`}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubscriptionForm;
|
||||
32
src/containers/internal/views/accounts/subscription.tsx
Normal file
32
src/containers/internal/views/accounts/subscription.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
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 = ENABLE_HOSTED_SYSTEM
|
||||
? loadStripe(STRIPE_PUBLISHABLE_KEY)
|
||||
: null;
|
||||
|
||||
export const Subscription = () => {
|
||||
return (
|
||||
<>
|
||||
<Elements
|
||||
stripe={stripePromise}
|
||||
options={{
|
||||
mode: "setup",
|
||||
currency: "usd",
|
||||
paymentMethodCreation: "manual",
|
||||
}}
|
||||
>
|
||||
<SubscriptionForm />
|
||||
</Elements>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Subscription;
|
||||
@@ -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,6 @@ 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 +37,8 @@ import {
|
||||
import type {
|
||||
RecognizerVendors,
|
||||
SynthesisVendors,
|
||||
Voice,
|
||||
VoiceLanguage,
|
||||
Language,
|
||||
VendorOptions,
|
||||
LabelOptions,
|
||||
} from "src/vendor/types";
|
||||
|
||||
import type {
|
||||
@@ -59,6 +52,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>;
|
||||
@@ -66,8 +60,8 @@ type ApplicationFormProps = {
|
||||
|
||||
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 +92,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 +197,7 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
const payload: Partial<Application> = {
|
||||
name: applicationName,
|
||||
app_json: applicationJson || null,
|
||||
call_hook: callWebhook || null,
|
||||
@@ -180,10 +206,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 +261,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 +273,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 +285,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) {
|
||||
@@ -245,6 +386,53 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
}
|
||||
}, [accountSid]);
|
||||
|
||||
useEffect(() => {
|
||||
let label: string;
|
||||
// Synthesis Label
|
||||
label = application?.data?.speech_synthesis_label || "";
|
||||
if (ttsLabelOptions && !ttsLabelOptions.find((l) => l.value === label)) {
|
||||
label = ttsLabelOptions.length ? ttsLabelOptions[0].value : "";
|
||||
}
|
||||
setSynthLabel(label);
|
||||
|
||||
// fallback Synthesis Label
|
||||
label = application?.data?.fallback_speech_synthesis_label || "";
|
||||
if (
|
||||
fallbackTtsLabelOptions &&
|
||||
!fallbackTtsLabelOptions.find((l) => l.value === label)
|
||||
) {
|
||||
label = fallbackTtsLabelOptions.length
|
||||
? fallbackTtsLabelOptions[0].value
|
||||
: "";
|
||||
}
|
||||
setFallbackSpeechSynthsisLabel(label);
|
||||
|
||||
// regconizer label
|
||||
label = application?.data?.speech_recognizer_label || "";
|
||||
if (sttLabelOptions && !sttLabelOptions.find((l) => l.value === label)) {
|
||||
label = sttLabelOptions.length ? sttLabelOptions[0].value : "";
|
||||
}
|
||||
setRecogLabel(label);
|
||||
|
||||
// fallback regconizer label
|
||||
label = application?.data?.fallback_speech_recognizer_label || "";
|
||||
if (
|
||||
fallbackSttLabelOptions &&
|
||||
!fallbackSttLabelOptions.find((l) => l.value === label)
|
||||
) {
|
||||
label = fallbackSttLabelOptions.length
|
||||
? fallbackSttLabelOptions[0].value
|
||||
: "";
|
||||
}
|
||||
setFallbackSpeechRecognizerLabel(label);
|
||||
}, [
|
||||
ttsLabelOptions,
|
||||
sttLabelOptions,
|
||||
fallbackTtsLabelOptions,
|
||||
fallbackSttLabelOptions,
|
||||
application,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocation();
|
||||
if (application && application.data) {
|
||||
@@ -321,12 +509,84 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
|
||||
if (application.data.speech_recognizer_language)
|
||||
setRecogLang(application.data.speech_recognizer_language);
|
||||
|
||||
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_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
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [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 +710,85 @@ 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}
|
||||
ttsVendor={[synthVendor, setSynthVendor]}
|
||||
ttsVendorOptions={ttsVendorOptions}
|
||||
ttsVoice={[synthVoice, setSynthVoice]}
|
||||
ttsLang={[synthLang, setSynthLang]}
|
||||
ttsLabelOptions={ttsLabelOptions}
|
||||
ttsLabel={[synthLabel, setSynthLabel]}
|
||||
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}
|
||||
ttsVendor={[
|
||||
fallbackSpeechSynthsisVendor,
|
||||
setFallbackSpeechSynthsisVendor,
|
||||
]}
|
||||
ttsVendorOptions={ttsVendorOptions}
|
||||
ttsVoice={[
|
||||
fallbackSpeechSynthsisVoice,
|
||||
setFallbackSpeechSynthsisVoice,
|
||||
]}
|
||||
ttsLang={[
|
||||
fallbackSpeechSynthsisLanguage,
|
||||
setFallbackSpeechSynthsisLanguage,
|
||||
]}
|
||||
ttsLabelOptions={fallbackTtsLabelOptions}
|
||||
ttsLabel={[
|
||||
fallbackSpeechSynthsisLabel,
|
||||
setFallbackSpeechSynthsisLabel,
|
||||
]}
|
||||
sttVendor={[
|
||||
fallbackSpeechRecognizerVendor,
|
||||
setFallbackSpeechRecognizerVendor,
|
||||
]}
|
||||
sttVendorOptions={sttVendorOptions}
|
||||
sttLang={[
|
||||
fallbackSpeechRecognizerLanguage,
|
||||
setFallbackSpeechRecognizerLanguage,
|
||||
]}
|
||||
sttLabelOptions={fallbackSttLabelOptions}
|
||||
sttLabel={[
|
||||
fallbackSpeechRecognizerLabel,
|
||||
setFallbackSpeechRecognizerLabel,
|
||||
]}
|
||||
/>
|
||||
{synthVendor &&
|
||||
!synthVendor.toString().startsWith(VENDOR_CUSTOM) &&
|
||||
synthLang && (
|
||||
<>
|
||||
<label htmlFor="synthesis_lang">Language</label>
|
||||
<Selector
|
||||
id="synthesis_lang"
|
||||
name="synthesis_lang"
|
||||
value={synthLang}
|
||||
options={synthesis[
|
||||
synthVendor as keyof SynthesisVendors
|
||||
].map((lang: VoiceLanguage) => ({
|
||||
name: lang.name,
|
||||
value: lang.code,
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
const language = e.target.value;
|
||||
setSynthLang(language);
|
||||
|
||||
/** When using Google and en-US, ensure "Standard-C" is used as default */
|
||||
if (
|
||||
synthVendor === VENDOR_GOOGLE &&
|
||||
language === LANG_EN_US
|
||||
) {
|
||||
setSynthVoice(LANG_EN_US_STANDARD_C);
|
||||
return;
|
||||
}
|
||||
|
||||
const newLang = synthesis[
|
||||
synthVendor as keyof SynthesisVendors
|
||||
].find((lang) => lang.code === language);
|
||||
|
||||
setSynthVoice(newLang!.voices[0].value);
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="synthesis_voice">Voice</label>
|
||||
<Selector
|
||||
id="synthesis_voice"
|
||||
name="synthesis_voice"
|
||||
value={synthVoice}
|
||||
options={
|
||||
synthesis[synthVendor as keyof SynthesisVendors]
|
||||
.filter(
|
||||
(lang: VoiceLanguage) => lang.code === synthLang
|
||||
)
|
||||
.flatMap((lang: VoiceLanguage) =>
|
||||
lang.voices.map((voice: Voice) => ({
|
||||
name: voice.name,
|
||||
value: voice.value,
|
||||
}))
|
||||
) as Voice[]
|
||||
}
|
||||
onChange={(e) => setSynthVoice(e.target.value)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{synthVendor.toString().startsWith(VENDOR_CUSTOM) && (
|
||||
<>
|
||||
<label htmlFor="custom_vendor_synthesis_lang">Language</label>
|
||||
<input
|
||||
id="custom_vendor_synthesis_lang"
|
||||
type="text"
|
||||
name="custom_vendor_synthesis_lang"
|
||||
placeholder="Required"
|
||||
required
|
||||
value={synthLang}
|
||||
onChange={(e) => {
|
||||
setSynthLang(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<label htmlFor="custom_vendor_synthesis_voice">Voice</label>
|
||||
<input
|
||||
id="custom_vendor_synthesis_voice"
|
||||
type="text"
|
||||
name="custom_vendor_synthesis_voice"
|
||||
placeholder="Required"
|
||||
required
|
||||
value={synthVoice}
|
||||
onChange={(e) => {
|
||||
setSynthVoice(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</fieldset>
|
||||
)}
|
||||
{recognizers && (
|
||||
<fieldset>
|
||||
<label htmlFor="recognizer_vendor">Speech recognizer vendor</label>
|
||||
<Selector
|
||||
id="recognizer_vendor"
|
||||
name="recognizer_vendor"
|
||||
value={recogVendor}
|
||||
options={softSttVendor.filter(
|
||||
(vendor) =>
|
||||
vendor.value != VENDOR_WELLSAID &&
|
||||
vendor.value !== VENDOR_CUSTOM
|
||||
)}
|
||||
onChange={(e) => {
|
||||
const vendor = e.target.value as keyof RecognizerVendors;
|
||||
setRecogVendor(vendor);
|
||||
|
||||
/**When vendor is custom, Language is input by user */
|
||||
if (vendor.toString() === VENDOR_CUSTOM) return;
|
||||
|
||||
/** Google and AWS have different language lists */
|
||||
/** If the new language doesn't map then default to "en-US" */
|
||||
const newLang = recognizers[vendor].find(
|
||||
(lang: Language) => lang.code === recogLang
|
||||
);
|
||||
|
||||
if (
|
||||
(vendor === VENDOR_GOOGLE || vendor === VENDOR_AWS) &&
|
||||
!newLang
|
||||
) {
|
||||
setRecogLang(LANG_EN_US);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{recogVendor &&
|
||||
!recogVendor.toString().startsWith(VENDOR_CUSTOM) &&
|
||||
recogLang && (
|
||||
<>
|
||||
<label htmlFor="recognizer_lang">Language</label>
|
||||
<Selector
|
||||
id="recognizer_lang"
|
||||
name="recognizer_lang"
|
||||
value={recogLang}
|
||||
options={recognizers[
|
||||
recogVendor as keyof RecognizerVendors
|
||||
].map((lang: Language) => ({
|
||||
name: lang.name,
|
||||
value: lang.code,
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
setRecogLang(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{recogVendor.toString().startsWith(VENDOR_CUSTOM) && (
|
||||
<>
|
||||
<label htmlFor="custom_vendor_recognizer_voice">Language</label>
|
||||
<input
|
||||
id="custom_vendor_recognizer_voice"
|
||||
type="text"
|
||||
name="custom_vendor_recognizer_voice"
|
||||
placeholder="Required"
|
||||
required
|
||||
value={recogLang}
|
||||
onChange={(e) => {
|
||||
setRecogLang(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</fieldset>
|
||||
)}
|
||||
<fieldset>
|
||||
<Button
|
||||
type="button"
|
||||
small
|
||||
onClick={swapPrimaryAndfalloverSpeech}
|
||||
>
|
||||
Swap primary and fallback
|
||||
</Button>
|
||||
</fieldset>
|
||||
</Checkzone>
|
||||
</fieldset>
|
||||
{(import.meta.env.INITIAL_APP_JSON_ENABLED === undefined ||
|
||||
import.meta.env.INITIAL_APP_JSON_ENABLED) && (
|
||||
<fieldset>
|
||||
|
||||
@@ -96,7 +96,7 @@ export const Applications = () => {
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
<section className="filters filters--spaced">
|
||||
<section className="filters filters--multi">
|
||||
<SearchFilter
|
||||
placeholder="Filter applications"
|
||||
filter={[filter, setFilter]}
|
||||
|
||||
577
src/containers/internal/views/applications/speech-selection.tsx
Normal file
577
src/containers/internal/views/applications/speech-selection.tsx
Normal file
@@ -0,0 +1,577 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
getGoogleCustomVoices,
|
||||
getSpeechSupportedLanguagesAndVoices,
|
||||
} from "src/api";
|
||||
import { USER_ADMIN } from "src/api/constants";
|
||||
import {
|
||||
SpeechCredential,
|
||||
SpeechSupportedLanguagesAndVoices,
|
||||
} from "src/api/types";
|
||||
import { Selector } from "src/components/forms";
|
||||
import { SelectorOption } from "src/components/forms/selector";
|
||||
import { toastError, 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,
|
||||
RecognizerVendors,
|
||||
SynthesisVendors,
|
||||
VendorOptions,
|
||||
} from "src/vendor/types";
|
||||
type SpeechProviderSelectionProbs = {
|
||||
accountSid: string;
|
||||
serviceProviderSid: string;
|
||||
credentials: SpeechCredential[] | 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>>];
|
||||
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,
|
||||
ttsVendor: [synthVendor, setSynthVendor],
|
||||
ttsVendorOptions,
|
||||
ttsVoice: [synthVoice, setSynthVoice],
|
||||
ttsLang: [synthLang, setSynthLang],
|
||||
ttsLabelOptions,
|
||||
ttsLabel: [synthLabel, setSynthLabel],
|
||||
sttVendor: [recogVendor, setRecogVendor],
|
||||
sttVendorOptions,
|
||||
sttLang: [recogLang, setRecogLang],
|
||||
sttLabelOptions,
|
||||
sttLabel: [recogLabel, setRecogLabel],
|
||||
}: SpeechProviderSelectionProbs) => {
|
||||
const user = useSelectState("user");
|
||||
const [
|
||||
synthesisSupportedLanguagesAndVoices,
|
||||
setSynthesisSupportedLanguagesAndVoices,
|
||||
] = useState<SpeechSupportedLanguagesAndVoices | null>();
|
||||
const [selectedCredential, setSelectedCredential] = useState<
|
||||
SpeechCredential | undefined
|
||||
>();
|
||||
const [synthesisVoiceOptions, setSynthesisVoiceOptions] = useState<
|
||||
SelectorOption[]
|
||||
>([]);
|
||||
const [synthesisLanguageOptions, setSynthesisLanguageOptions] = useState<
|
||||
SelectorOption[]
|
||||
>([]);
|
||||
const [synthesisModelOptions, setSynthesisModelOptions] = useState<
|
||||
SelectorOption[]
|
||||
>([]);
|
||||
const [
|
||||
synthesisGoogleCustomVoiceOptions,
|
||||
setSynthesisGoogleCustomVoiceOptions,
|
||||
] = useState<SelectorOption[]>([]);
|
||||
const [recogLanguageOptions, setRecogLanguageOptions] = useState<
|
||||
SelectorOption[]
|
||||
>([]);
|
||||
|
||||
const currentTtsVendor = useRef(synthVendor);
|
||||
const currentSttVendor = useRef(recogVendor);
|
||||
const shouldUpdateTtsVoice = useRef(false);
|
||||
const shouldUpdateSttLanguage = useRef(false);
|
||||
const ttsEffectTimer = useRef<number | null>(null);
|
||||
const sttEffectTimer = useRef<number | null>(null);
|
||||
|
||||
// Get Synthesis languages and voices
|
||||
useEffect(() => {
|
||||
if (
|
||||
!user ||
|
||||
!synthVendor ||
|
||||
(user?.scope === USER_ADMIN && !serviceProviderSid)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
currentTtsVendor.current = synthVendor;
|
||||
/** When Custom Vendor is used, user you have to input the lange and voice. */
|
||||
if (synthVendor.toString().startsWith(VENDOR_CUSTOM)) {
|
||||
setSynthVoice("");
|
||||
return;
|
||||
}
|
||||
// just execute last change
|
||||
if (ttsEffectTimer.current) {
|
||||
clearTimeout(ttsEffectTimer.current);
|
||||
}
|
||||
|
||||
ttsEffectTimer.current = setTimeout(() => {
|
||||
configSynthesis();
|
||||
}, 200);
|
||||
}, [synthVendor, synthLabel, serviceProviderSid]);
|
||||
|
||||
// Get Recognizer languages and voices
|
||||
useEffect(() => {
|
||||
/** When Custom Vendor is used, user you have to input the lange and voice. */
|
||||
if (recogVendor.toString().startsWith(VENDOR_CUSTOM)) {
|
||||
setRecogLang(LANG_EN_US);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!user ||
|
||||
!recogVendor ||
|
||||
(user?.scope === USER_ADMIN && !serviceProviderSid)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
currentSttVendor.current = recogVendor;
|
||||
// just execute last change
|
||||
if (sttEffectTimer.current) {
|
||||
clearTimeout(sttEffectTimer.current);
|
||||
}
|
||||
|
||||
sttEffectTimer.current = setTimeout(() => {
|
||||
configRecognizer();
|
||||
}, 200);
|
||||
}, [recogVendor, recogLabel, serviceProviderSid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (credentials) {
|
||||
setSelectedCredential(
|
||||
credentials.find(
|
||||
(c) => c.vendor === synthVendor && (c.label || "") === synthLabel
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [synthVendor, synthLabel, credentials]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!synthLabel && ttsLabelOptions?.length > 0) {
|
||||
setSynthLabel(ttsLabelOptions[0].value);
|
||||
}
|
||||
if (!recogLabel && sttLabelOptions?.length > 0) {
|
||||
setRecogLabel(sttLabelOptions[0].value);
|
||||
}
|
||||
}, [ttsLabelOptions, sttLabelOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (synthesisSupportedLanguagesAndVoices) {
|
||||
// Extract Voice
|
||||
const voicesOpts =
|
||||
synthesisSupportedLanguagesAndVoices.tts?.find((lang) => {
|
||||
if (synthVendor === VENDOR_ELEVENLABS && lang.voices.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return lang.value === synthLang;
|
||||
})?.voices || [];
|
||||
|
||||
if (synthVendor === VENDOR_GOOGLE && synthesisGoogleCustomVoiceOptions) {
|
||||
if (synthesisGoogleCustomVoiceOptions) {
|
||||
setSynthesisVoiceOptions([
|
||||
...synthesisGoogleCustomVoiceOptions,
|
||||
...voicesOpts,
|
||||
]);
|
||||
} else {
|
||||
setSynthesisVoiceOptions(voicesOpts);
|
||||
}
|
||||
if (synthesisGoogleCustomVoiceOptions.length > 0) {
|
||||
updateTtsVoice(synthesisGoogleCustomVoiceOptions[0].value);
|
||||
}
|
||||
} else {
|
||||
setSynthesisVoiceOptions(voicesOpts);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
synthLang,
|
||||
synthesisSupportedLanguagesAndVoices,
|
||||
synthesisGoogleCustomVoiceOptions,
|
||||
]);
|
||||
|
||||
const configSynthesis = () => {
|
||||
getSpeechSupportedLanguagesAndVoices(
|
||||
serviceProviderSid,
|
||||
synthVendor,
|
||||
synthLabel
|
||||
)
|
||||
.then(({ json }) => {
|
||||
// while fetching data, user might change the vendor
|
||||
if (currentTtsVendor.current !== synthVendor) {
|
||||
return;
|
||||
}
|
||||
setSynthesisSupportedLanguagesAndVoices(json);
|
||||
// Extract model
|
||||
if (json.models && json.models.length) {
|
||||
setSynthesisModelOptions(json.models);
|
||||
if (synthVendor === VENDOR_DEEPGRAM) {
|
||||
setSynthVoice(json.models[0].value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (json.tts && json.tts.length) {
|
||||
// Extract Language
|
||||
const langOpts = json.tts.map((lang) => ({
|
||||
name: lang.name,
|
||||
value: lang.value,
|
||||
}));
|
||||
setSynthesisLanguageOptions(langOpts);
|
||||
|
||||
// Default setting
|
||||
const googleLang = json.tts.find((lang) => lang.value === synthLang);
|
||||
if (
|
||||
synthVendor === VENDOR_GOOGLE &&
|
||||
(!googleLang ||
|
||||
!googleLang.voices.find((v) => v.value === synthVoice))
|
||||
) {
|
||||
setSynthLang(LANG_EN_US);
|
||||
updateTtsVoice(LANG_EN_US_STANDARD_C);
|
||||
return;
|
||||
}
|
||||
if (synthVendor === VENDOR_ELEVENLABS) {
|
||||
// Samve Voices applied to all languages
|
||||
// Voices are only available for the 1st language.
|
||||
setSynthLang(ELEVENLABS_LANG_EN);
|
||||
updateTtsVoice(json.tts[0].voices[0].value);
|
||||
return;
|
||||
}
|
||||
if (synthVendor === VENDOR_WHISPER) {
|
||||
const newLang = json.tts.find((lang) => lang.value === LANG_EN_US);
|
||||
setSynthLang(LANG_EN_US);
|
||||
updateTtsVoice(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 = json.tts.find((lang) => lang.value === synthLang);
|
||||
|
||||
if (newLang) {
|
||||
updateTtsVoice(newLang.voices[0].value);
|
||||
return;
|
||||
}
|
||||
|
||||
newLang = json.tts.find((lang) => lang.value === LANG_EN_US);
|
||||
|
||||
setSynthLang(LANG_EN_US);
|
||||
updateTtsVoice(newLang!.voices[0].value);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
|
||||
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 (currentTtsVendor.current !== VENDOR_GOOGLE) {
|
||||
return;
|
||||
}
|
||||
const customVOices = json.map((v) => ({
|
||||
name: `${v.name} (Custom)`,
|
||||
value: `custom_${v.google_custom_voice_sid}`,
|
||||
}));
|
||||
setSynthesisGoogleCustomVoiceOptions(customVOices);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateTtsVoice = (value: string) => {
|
||||
if (shouldUpdateTtsVoice.current) {
|
||||
setSynthVoice(value);
|
||||
shouldUpdateTtsVoice.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const configRecognizer = () => {
|
||||
getSpeechSupportedLanguagesAndVoices(
|
||||
serviceProviderSid,
|
||||
recogVendor,
|
||||
recogLabel
|
||||
)
|
||||
.then(({ json }) => {
|
||||
// while fetching data, the user might change the vendor
|
||||
if (currentSttVendor.current !== recogVendor) {
|
||||
return;
|
||||
}
|
||||
// Extract Language
|
||||
const langOpts = json.stt.map((lang) => ({
|
||||
name: lang.name,
|
||||
value: lang.value,
|
||||
}));
|
||||
setRecogLanguageOptions(langOpts);
|
||||
|
||||
/**When vendor is custom, Language is input by user */
|
||||
if (
|
||||
recogVendor.toString() === VENDOR_CUSTOM ||
|
||||
!shouldUpdateSttLanguage.current
|
||||
)
|
||||
return;
|
||||
shouldUpdateSttLanguage.current = false;
|
||||
/** Google and AWS have different language lists */
|
||||
/** If the new language doesn't map then default to "en-US" */
|
||||
const newLang = json.stt.find((lang) => lang.value === recogLang);
|
||||
|
||||
if (
|
||||
(recogVendor === VENDOR_GOOGLE || recogVendor === VENDOR_AWS) &&
|
||||
!newLang
|
||||
) {
|
||||
setRecogLang(LANG_EN_US);
|
||||
} else if (recogVendor === VENDOR_COBALT && !newLang) {
|
||||
setRecogLang(LANG_COBALT_EN_US);
|
||||
} else if (langOpts.length && !newLang) {
|
||||
setRecogLang(langOpts[0].value);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<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_ASSEMBLYAI &&
|
||||
vendor.value != VENDOR_SONIOX &&
|
||||
vendor.value !== VENDOR_CUSTOM &&
|
||||
vendor.value !== VENDOR_COBALT
|
||||
)}
|
||||
onChange={(e) => {
|
||||
const vendor = e.target.value as keyof SynthesisVendors;
|
||||
shouldUpdateTtsVoice.current = true;
|
||||
setSynthVendor(vendor);
|
||||
setSynthLabel("");
|
||||
setSynthesisLanguageOptions([]);
|
||||
setSynthesisVoiceOptions([]);
|
||||
}}
|
||||
/>
|
||||
{hasLength(ttsLabelOptions) && (
|
||||
<>
|
||||
<label htmlFor="synthesis_label">Label</label>
|
||||
<Selector
|
||||
id="systhesis_label"
|
||||
name="systhesis_label"
|
||||
value={synthLabel}
|
||||
options={ttsLabelOptions}
|
||||
onChange={(e) => {
|
||||
setSynthLabel(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{synthesisModelOptions && synthVendor === VENDOR_DEEPGRAM && (
|
||||
<>
|
||||
<label htmlFor="synthesis_lang">Model</label>
|
||||
<Selector
|
||||
id="synthesis_voice"
|
||||
name="synthesis_voice"
|
||||
value={synthVoice}
|
||||
options={synthesisModelOptions}
|
||||
onChange={(e) => setSynthVoice(e.target.value)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{synthVendor &&
|
||||
!synthVendor.toString().startsWith(VENDOR_CUSTOM) &&
|
||||
synthVendor !== VENDOR_DEEPGRAM &&
|
||||
synthLang && (
|
||||
<>
|
||||
<label htmlFor="synthesis_lang">Language</label>
|
||||
<Selector
|
||||
id="synthesis_lang"
|
||||
name="synthesis_lang"
|
||||
value={synthLang}
|
||||
options={synthesisLanguageOptions}
|
||||
onChange={(e) => {
|
||||
shouldUpdateTtsVoice.current = true;
|
||||
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 voices =
|
||||
synthesisSupportedLanguagesAndVoices?.tts.find(
|
||||
(lang) => lang.value === language
|
||||
)?.voices || [];
|
||||
if (
|
||||
synthVendor === VENDOR_GOOGLE &&
|
||||
synthesisGoogleCustomVoiceOptions &&
|
||||
synthesisGoogleCustomVoiceOptions.length
|
||||
) {
|
||||
setSynthesisVoiceOptions([
|
||||
...synthesisGoogleCustomVoiceOptions,
|
||||
...voices,
|
||||
]);
|
||||
} else {
|
||||
setSynthesisVoiceOptions(voices);
|
||||
}
|
||||
|
||||
setSynthVoice(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>
|
||||
<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;
|
||||
shouldUpdateSttLanguage.current = true;
|
||||
setRecogVendor(vendor);
|
||||
setRecogLabel("");
|
||||
|
||||
setRecogLanguageOptions([]);
|
||||
}}
|
||||
/>
|
||||
{hasLength(sttLabelOptions) && (
|
||||
<>
|
||||
<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={recogLanguageOptions}
|
||||
onChange={(e) => {
|
||||
setRecogLang(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{recogVendor.toString().startsWith(VENDOR_CUSTOM) && (
|
||||
<>
|
||||
<label htmlFor="custom_vendor_recognizer_voice">Language</label>
|
||||
<input
|
||||
id="custom_vendor_recognizer_voice"
|
||||
type="text"
|
||||
name="custom_vendor_recognizer_voice"
|
||||
placeholder="Required"
|
||||
required
|
||||
value={recogLang}
|
||||
onChange={(e) => {
|
||||
setRecogLang(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</fieldset>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpeechProviderSelection;
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
FQDN,
|
||||
FQDN_TOP_LEVEL,
|
||||
INVALID,
|
||||
IP,
|
||||
NETMASK_OPTIONS,
|
||||
SIP_GATEWAY_PROTOCOL_OPTIONS,
|
||||
TCP_MAX_PORT,
|
||||
@@ -47,6 +48,8 @@ import {
|
||||
hasLength,
|
||||
isValidPort,
|
||||
disableDefaultTrunkRouting,
|
||||
hasValue,
|
||||
isNotBlank,
|
||||
} from "src/utils";
|
||||
|
||||
import type {
|
||||
@@ -236,7 +239,20 @@ export const CarrierForm = ({
|
||||
value: typeof sipGateways[number][keyof SipGateway]
|
||||
) => {
|
||||
setSipGateways(
|
||||
sipGateways.map((g, i) => (i === index ? { ...g, [key]: value } : g))
|
||||
sipGateways.map((g, i) =>
|
||||
i === index
|
||||
? {
|
||||
...g,
|
||||
[key]: value,
|
||||
// If Change to ipv4 and port is null, change port to 5060
|
||||
...(key === "ipv4" &&
|
||||
value &&
|
||||
typeof value === "string" &&
|
||||
getIpValidationType(value) === IP &&
|
||||
g.port === null && { port: 5060 }),
|
||||
}
|
||||
: g
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
@@ -776,7 +802,7 @@ export const CarrierForm = ({
|
||||
Does your carrier require authentication on outbound calls?
|
||||
</MS>
|
||||
<label htmlFor="sip_username">
|
||||
Username {sipPass || sipRegister ? <span>*</span> : ""}
|
||||
Auth username {sipPass || sipRegister ? <span>*</span> : ""}
|
||||
</label>
|
||||
<input
|
||||
id="sip_username"
|
||||
@@ -831,7 +857,7 @@ export const CarrierForm = ({
|
||||
required={sipRegister}
|
||||
onChange={(e) => setSipRealm(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="from_user">SIP from user</label>
|
||||
<label htmlFor="from_user">Username</label>
|
||||
<input
|
||||
id="from_user"
|
||||
name="from_user"
|
||||
@@ -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
|
||||
|
||||
@@ -30,16 +30,24 @@ import {
|
||||
API_SIP_GATEWAY,
|
||||
API_SMPP_GATEWAY,
|
||||
CARRIER_REG_OK,
|
||||
ENABLE_HOSTED_SYSTEM,
|
||||
USER_ACCOUNT,
|
||||
} from "src/api/constants";
|
||||
import { DeleteCarrier } from "./delete";
|
||||
|
||||
import type { Account, Carrier, SipGateway, SmppGateway } from "src/api/types";
|
||||
import type {
|
||||
Account,
|
||||
Carrier,
|
||||
CurrentUserData,
|
||||
SipGateway,
|
||||
SmppGateway,
|
||||
} from "src/api/types";
|
||||
import { Scope } from "src/store/types";
|
||||
import { getAccountFilter, setLocation } from "src/store/localStore";
|
||||
|
||||
export const Carriers = () => {
|
||||
const user = useSelectState("user");
|
||||
const [userData] = useApiData<CurrentUserData>("Users/me");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [apiUrl, setApiUrl] = useState("");
|
||||
const [carrier, setCarrier] = useState<Carrier | null>(null);
|
||||
@@ -130,7 +138,16 @@ export const Carriers = () => {
|
||||
return (
|
||||
<>
|
||||
<section className="mast">
|
||||
<H1 className="h2">Carriers</H1>
|
||||
<div>
|
||||
<H1 className="h2">Carriers</H1>
|
||||
{ENABLE_HOSTED_SYSTEM && (
|
||||
<M>
|
||||
Have your carrier send calls to{" "}
|
||||
<span>{userData?.account?.sip_realm}</span>
|
||||
</M>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link to={`${ROUTE_INTERNAL_CARRIERS}/add`} title="Add a Carrier">
|
||||
{" "}
|
||||
<Icon>
|
||||
@@ -138,7 +155,7 @@ export const Carriers = () => {
|
||||
</Icon>
|
||||
</Link>
|
||||
</section>
|
||||
<section className="filters filters--spaced">
|
||||
<section className="filters filters--multi">
|
||||
<SearchFilter
|
||||
placeholder="Filter carriers"
|
||||
filter={[filter, setFilter]}
|
||||
|
||||
@@ -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 || ""}
|
||||
|
||||
@@ -5,7 +5,7 @@ import ClientsForm from "./form";
|
||||
export const ClientsAdd = () => {
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Add client</H1>
|
||||
<H1 className="h2">Add sip client</H1>
|
||||
<ClientsForm />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ClientsDelete = ({
|
||||
<>
|
||||
<Modal handleCancel={handleCancel} handleSubmit={handleSubmit}>
|
||||
<P>
|
||||
Are you sure you want to delete the client{" "}
|
||||
Are you sure you want to delete the sip client{" "}
|
||||
<strong>{client.username}</strong>?
|
||||
</P>
|
||||
</Modal>
|
||||
|
||||
@@ -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 }} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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,12 +37,22 @@ export const Clients = () => {
|
||||
return clients;
|
||||
}
|
||||
|
||||
setSelectedAccount(
|
||||
accountSid
|
||||
? accounts?.find((a: Account) => a.account_sid === accountSid)
|
||||
: null
|
||||
);
|
||||
|
||||
return clients
|
||||
? clients.filter((c) => {
|
||||
return accountSid ? c.account_sid === accountSid : true;
|
||||
return accountSid
|
||||
? c.account_sid === accountSid
|
||||
: accounts
|
||||
? accounts.map((a) => a.account_sid).includes(c.account_sid || "")
|
||||
: false;
|
||||
})
|
||||
: [];
|
||||
}, [accountSid, clients]);
|
||||
}, [accountSid, clients, accounts]);
|
||||
|
||||
const filteredClients = useFilteredResults(filter, tmpFilteredClients);
|
||||
|
||||
@@ -48,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);
|
||||
@@ -63,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 />
|
||||
@@ -72,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]}
|
||||
@@ -152,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 && (
|
||||
|
||||
@@ -122,6 +122,7 @@ export const Card = ({
|
||||
className={`lcr lcr--route lcr-card lcr-card-${
|
||||
isDragging ? "disappear" : "appear"
|
||||
}`}
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
handler-id={handlerId}
|
||||
>
|
||||
<div>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -76,7 +76,7 @@ export const DetailsItem = ({ call }: DetailsItemProps) => {
|
||||
<CallDetail call={transformRecentCall(call)} />
|
||||
</Tab>
|
||||
<Tab id="tracing" label="Tracing">
|
||||
<CallTracing call={call} />
|
||||
{open && <CallTracing call={call} />}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
@@ -48,8 +48,7 @@ export const RecentCalls = () => {
|
||||
const [dateFilter, setDateFilter] = useState("today");
|
||||
const [directionFilter, setDirectionFilter] = useState("io");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
const [fromFilter, setFromFilter] = useState("");
|
||||
const [toFilter, setToFilter] = useState("");
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const [perPageFilter, setPerPageFilter] = useState("25");
|
||||
@@ -67,8 +66,7 @@ export const RecentCalls = () => {
|
||||
: { days: Number(dateFilter) }),
|
||||
...(statusFilter !== "all" && { answered: statusFilter }),
|
||||
...(directionFilter !== "io" && { direction: directionFilter }),
|
||||
...(fromFilter && { from: fromFilter }),
|
||||
...(toFilter && { to: toFilter }),
|
||||
...(filter && { filter }),
|
||||
};
|
||||
|
||||
getRecentCalls(accountSid, payload)
|
||||
@@ -105,8 +103,7 @@ export const RecentCalls = () => {
|
||||
dateFilter,
|
||||
directionFilter,
|
||||
statusFilter,
|
||||
fromFilter,
|
||||
toFilter,
|
||||
filter,
|
||||
]);
|
||||
|
||||
/** Reset page number when filters change */
|
||||
@@ -150,13 +147,8 @@ export const RecentCalls = () => {
|
||||
options={statusSelection}
|
||||
/>
|
||||
<SearchFilter
|
||||
placeholder="Filter From"
|
||||
filter={[fromFilter, setFromFilter]}
|
||||
delay={1000}
|
||||
/>
|
||||
<SearchFilter
|
||||
placeholder="Filter To"
|
||||
filter={[toFilter, setToFilter]}
|
||||
placeholder="Filter"
|
||||
filter={[filter, setFilter]}
|
||||
delay={1000}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -3,24 +3,26 @@ 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,
|
||||
WaveSurferGatherSpeechVerbHookLatencyResult,
|
||||
WaveSurferSttResult,
|
||||
WaveSurferTtsLatencyResult,
|
||||
} from "src/api/jaeger-types";
|
||||
import {
|
||||
getSpanAttributeByName,
|
||||
getSpansByName,
|
||||
getSpansByNameRegex,
|
||||
getSpansFromJaegerRoot,
|
||||
} from "./utils";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
|
||||
type PlayerProps = {
|
||||
call: RecentCall;
|
||||
@@ -37,21 +39,34 @@ 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 [waveSurferTtsLatencyData, setWaveSurferTtsLatencyData] =
|
||||
useState<WaveSurferTtsLatencyResult | null>();
|
||||
|
||||
const [
|
||||
waveSurferGatherSpeechVerbHookLatencyData,
|
||||
setWaveSurferGatherSpeechVerbHookLatencyData,
|
||||
] = useState<WaveSurferGatherSpeechVerbHookLatencyResult | 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 +79,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 +121,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 +174,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)}s`,
|
||||
});
|
||||
|
||||
changeRegionMouseStyle(latencyRegion, channel);
|
||||
}
|
||||
} else {
|
||||
const [sttResolve] = getSpanAttributeByName(
|
||||
s.attributes,
|
||||
@@ -164,17 +233,121 @@ 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 drawTtsLatencyRegion = (s: JaegerSpan, startPoint: JaegerSpan) => {
|
||||
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;
|
||||
let end =
|
||||
(s.endTimeUnixNano - startPoint.startTimeUnixNano) / 1_000_000_000;
|
||||
|
||||
const [ttsVendor] = getSpanAttributeByName(s.attributes, "tts.vendor");
|
||||
const [ttsCache] = getSpanAttributeByName(s.attributes, "tts.cached");
|
||||
const [streamLatency] = getSpanAttributeByName(
|
||||
s.attributes,
|
||||
"time_to_first_byte_ms"
|
||||
);
|
||||
if (streamLatency && streamLatency.value.stringValue) {
|
||||
end = start + Number(streamLatency.value.stringValue) / 1_000;
|
||||
}
|
||||
if (ttsVendor && ttsCache && !Boolean(ttsCache.value.boolValue)) {
|
||||
const latencyRegion = waveSurferRegionsPluginRef.current.addRegion({
|
||||
id: s.spanId,
|
||||
start: start,
|
||||
end,
|
||||
color: "rgba(255, 155, 0, 0.55)",
|
||||
drag: false,
|
||||
resize: false,
|
||||
content: createMultiLineTextElement(`${(end - start).toFixed(2)}s`),
|
||||
});
|
||||
|
||||
changeRegionMouseStyle(latencyRegion, 1);
|
||||
|
||||
latencyRegion.on("click", () => {
|
||||
setWaveSurferTtsLatencyData({
|
||||
vendor: ttsVendor.value.stringValue,
|
||||
latency: `${(end - start).toFixed(2)}s`,
|
||||
isCached: String(ttsCache.value.boolValue),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const drawVerbHookDelayRegion = (s: JaegerSpan, startPoint: JaegerSpan) => {
|
||||
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;
|
||||
const end =
|
||||
(s.endTimeUnixNano - startPoint.startTimeUnixNano) / 1_000_000_000;
|
||||
const tmpEnd = end - start < 0.05 ? start + 0.05 : end;
|
||||
|
||||
const latencyRegion = waveSurferRegionsPluginRef.current.addRegion({
|
||||
id: s.spanId,
|
||||
start: start,
|
||||
end: tmpEnd,
|
||||
color: "rgba(255, 3, 180, 0.55)",
|
||||
drag: false,
|
||||
resize: false,
|
||||
content: createMultiLineTextElement(`${(end - start).toFixed(2)}s`),
|
||||
});
|
||||
const [statusCode] = getSpanAttributeByName(
|
||||
s.attributes,
|
||||
"http.statusCode"
|
||||
);
|
||||
changeRegionMouseStyle(latencyRegion, 0);
|
||||
latencyRegion.on("click", () => {
|
||||
setWaveSurferGatherSpeechVerbHookLatencyData({
|
||||
statusCode: statusCode ? Number(statusCode.value.doubleValue) : 404,
|
||||
latency: `${(end - start).toFixed(2)}s`,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function createMultiLineTextElement(text: string) {
|
||||
const div = document.createElement("div");
|
||||
div.style.paddingLeft = "10px";
|
||||
div.style.paddingTop = "15px";
|
||||
div.appendChild(document.createElement("br"));
|
||||
div.appendChild(document.createTextNode(text));
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
const buildWavesurferRegion = () => {
|
||||
if (jaegerRoot) {
|
||||
const spans = getSpansFromJaegerRoot(jaegerRoot);
|
||||
const [startPoint] = getSpansByName(spans, "background-listen:listen");
|
||||
const start = getSpansByNameRegex(spans, /background-record:listen/);
|
||||
const startPoint = start ? start[0] : null;
|
||||
// there should be only one startPoint for background listen
|
||||
if (startPoint) {
|
||||
const gatherSpans = getSpansByNameRegex(spans, /:gather{/);
|
||||
@@ -182,6 +355,7 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
drawSttRegionForSpan(s, startPoint);
|
||||
});
|
||||
|
||||
// Trasscription
|
||||
const transcribeSpans = getSpansByNameRegex(spans, /stt-listen:/);
|
||||
transcribeSpans.forEach((cs) => {
|
||||
// Channel start from 0
|
||||
@@ -192,17 +366,54 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
channel > 0 ? channel - 1 : channel
|
||||
);
|
||||
});
|
||||
|
||||
// DTMF
|
||||
const dtmfSpans = getSpansByNameRegex(spans, /dtmf:/);
|
||||
dtmfSpans.forEach((ds) => {
|
||||
drawDtmfRegionForSpan(ds, startPoint);
|
||||
});
|
||||
// TTS delay
|
||||
const ttsSpans = getSpansByNameRegex(spans, /tts-generation/);
|
||||
ttsSpans.forEach((tts) => {
|
||||
drawTtsLatencyRegion(tts, startPoint);
|
||||
});
|
||||
|
||||
// Gather verb hook delay
|
||||
const verbHookSpans = getSpansByNameRegex(spans, /verb:hook/);
|
||||
verbHookSpans
|
||||
.filter((s) => {
|
||||
const [httpBody] = getSpanAttributeByName(
|
||||
s.attributes,
|
||||
"http.body"
|
||||
);
|
||||
return (
|
||||
httpBody.value.stringValue.includes(
|
||||
'"reason":"speechDetected"'
|
||||
) ||
|
||||
httpBody.value.stringValue.includes('"reason":"dtmfDetected"')
|
||||
);
|
||||
})
|
||||
.forEach((s) => {
|
||||
drawVerbHookDelayRegion(s, startPoint);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 +446,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 +456,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 +528,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,19 +605,20 @@ 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";
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div>Overlay STT and DTMF events</div>
|
||||
<div>Show latencies</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 +626,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 +680,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 +694,7 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
<strong>Dtmf:</strong>
|
||||
</div>
|
||||
<div className="spanDetailsWrapper__details_body">
|
||||
{waveSufferDtmfData.dtmf}
|
||||
{waveSurferDtmfData.dtmf}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -457,13 +703,95 @@ export const Player = ({ call }: PlayerProps) => {
|
||||
<strong>Duration:</strong>
|
||||
</div>
|
||||
<div className="spanDetailsWrapper__details_body">
|
||||
{waveSufferDtmfData.duration}
|
||||
{waveSurferDtmfData.duration}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalClose>
|
||||
)}
|
||||
{waveSurferTtsLatencyData && (
|
||||
<ModalClose handleClose={() => setWaveSurferTtsLatencyData(null)}>
|
||||
<div className="spanDetailsWrapper__header">
|
||||
<P>
|
||||
<strong>Tts Latency</strong>
|
||||
</P>
|
||||
</div>
|
||||
<div className="spanDetailsWrapper">
|
||||
<div className="spanDetailsWrapper__detailsWrapper">
|
||||
<div className="spanDetailsWrapper__details">
|
||||
<div className="spanDetailsWrapper__details_header">
|
||||
<strong>Vendor:</strong>
|
||||
</div>
|
||||
<div className="spanDetailsWrapper__details_body">
|
||||
{waveSurferTtsLatencyData.vendor}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="spanDetailsWrapper__details">
|
||||
<div className="spanDetailsWrapper__details_header">
|
||||
<strong>Latency:</strong>
|
||||
</div>
|
||||
<div className="spanDetailsWrapper__details_body">
|
||||
{waveSurferTtsLatencyData.latency}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="spanDetailsWrapper__details">
|
||||
<div className="spanDetailsWrapper__details_header">
|
||||
<strong>From Cache:</strong>
|
||||
</div>
|
||||
<div className="spanDetailsWrapper__details_body">
|
||||
{waveSurferTtsLatencyData.isCached}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalClose>
|
||||
)}
|
||||
{waveSurferGatherSpeechVerbHookLatencyData && (
|
||||
<ModalClose
|
||||
handleClose={() => setWaveSurferGatherSpeechVerbHookLatencyData(null)}
|
||||
>
|
||||
<div className="spanDetailsWrapper__header">
|
||||
<P>
|
||||
<strong>Application Response Latency</strong>
|
||||
</P>
|
||||
</div>
|
||||
<div className="spanDetailsWrapper">
|
||||
<div className="spanDetailsWrapper__detailsWrapper">
|
||||
<div className="spanDetailsWrapper__details">
|
||||
<div className="spanDetailsWrapper__details_header">
|
||||
<strong>Status Code:</strong>
|
||||
</div>
|
||||
<div className="spanDetailsWrapper__details_body">
|
||||
{waveSurferGatherSpeechVerbHookLatencyData.statusCode}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="spanDetailsWrapper__details">
|
||||
<div className="spanDetailsWrapper__details_header">
|
||||
<strong>Latency:</strong>
|
||||
</div>
|
||||
<div className="spanDetailsWrapper__details_body">
|
||||
{waveSurferGatherSpeechVerbHookLatencyData.latency}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalClose>
|
||||
)}
|
||||
{deleteRecordUrl && (
|
||||
<Modal
|
||||
handleCancel={() => setDeleteRecordUrl("")}
|
||||
handleSubmit={handleDeleteRecordSubmit}
|
||||
>
|
||||
<P>
|
||||
Are you sure you want to delete the record for call{" "}
|
||||
<strong>{call_sid}</strong>?
|
||||
</P>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,7 +19,59 @@
|
||||
}
|
||||
}
|
||||
|
||||
.controll-btn-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
padding: 13px;
|
||||
|
||||
&__center {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
button:not(:last-child) {
|
||||
margin-right: ui-vars.$px01;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.ico {
|
||||
color: ui-vars.$white;
|
||||
@include mixins.icosize();
|
||||
}
|
||||
}
|
||||
|
||||
&__right {
|
||||
a:not(:last-child) {
|
||||
margin-right: ui-vars.$px01;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.ico {
|
||||
color: ui-vars.$white;
|
||||
@include mixins.icosize();
|
||||
}
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.media-container {
|
||||
overflow-x: auto;
|
||||
border: 1px solid black;
|
||||
border-radius: ui-vars.$px01;
|
||||
padding: 13px;
|
||||
@@ -31,18 +83,5 @@
|
||||
justify-content: center;
|
||||
grid-gap: ui-vars.$px01;
|
||||
margin-top: ui-vars.$px01;
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ico {
|
||||
color: ui-vars.$white;
|
||||
@include mixins.icosize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,11 @@ export const DeleteSpeechService = ({
|
||||
return (
|
||||
<Modal handleCancel={handleCancel} handleSubmit={handleSubmit}>
|
||||
<P>
|
||||
Are you sure you want to delete the <strong>{credential.vendor}</strong>{" "}
|
||||
Are you sure you want to delete the{" "}
|
||||
<strong>
|
||||
{credential.vendor}
|
||||
{credential.label ? ` (${credential.label})` : ""}
|
||||
</strong>{" "}
|
||||
speech service?
|
||||
</P>
|
||||
</Modal>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { MSG_SOMETHING_WRONG } from "src/constants";
|
||||
|
||||
import { ROUTE_LOGIN } from "src/router/routes";
|
||||
import { toastSuccess } from "src/store";
|
||||
|
||||
export const ForgotPassword = () => {
|
||||
const [message, setMessage] = useState("");
|
||||
@@ -22,6 +23,9 @@ export const ForgotPassword = () => {
|
||||
postForgotPassword({ email })
|
||||
.then((response) => {
|
||||
if (response.status === StatusCodes.NO_CONTENT) {
|
||||
toastSuccess(
|
||||
"A password reset email has been sent to your email. Please check your inbox (and, possibly, spam folder) and follow the instructions to reset your password."
|
||||
);
|
||||
navigate(ROUTE_LOGIN);
|
||||
} else {
|
||||
setMessage(MSG_SOMETHING_WRONG);
|
||||
|
||||
@@ -2,8 +2,8 @@ import React, { useEffect, useState } from "react";
|
||||
import { Button, H1 } from "@jambonz/ui-kit";
|
||||
import { useLocation, Navigate, Link } from "react-router-dom";
|
||||
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import { useAuth } from "src/router/auth";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
import { getToken, parseJwt, useAuth } from "src/router/auth";
|
||||
import {
|
||||
SESS_FLASH_MSG,
|
||||
SESS_OLD_PASSWORD,
|
||||
@@ -13,15 +13,26 @@ import { Passwd, Message } from "src/components/forms";
|
||||
import {
|
||||
ROUTE_INTERNAL_ACCOUNTS,
|
||||
ROUTE_CREATE_PASSWORD,
|
||||
ROUTE_INTERNAL_APPLICATIONS,
|
||||
ROUTE_FORGOT_PASSWORD,
|
||||
ROUTE_REGISTER,
|
||||
} from "src/router/routes";
|
||||
import { USER_ACCOUNT, ENABLE_FORGOT_PASSWORD } from "src/api/constants";
|
||||
import {
|
||||
USER_ACCOUNT,
|
||||
ENABLE_FORGOT_PASSWORD,
|
||||
ENABLE_HOSTED_SYSTEM,
|
||||
} from "src/api/constants";
|
||||
import { Icons } from "src/components";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { setLocationBeforeOauth, setOauthState } from "src/store/localStore";
|
||||
import { getGithubOauthUrl, getGoogleOauthUrl } from "./utils";
|
||||
import { UserData } from "src/store/types";
|
||||
|
||||
export const Login = () => {
|
||||
const state = uuid();
|
||||
setOauthState(state);
|
||||
setLocationBeforeOauth("/sign-in");
|
||||
const { signin, authorized } = useAuth();
|
||||
const location = useLocation();
|
||||
const user = useSelectState("user");
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [message, setMessage] = useState("");
|
||||
@@ -56,13 +67,13 @@ export const Login = () => {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const userData: UserData = parseJwt(getToken());
|
||||
return (
|
||||
<Navigate
|
||||
to={
|
||||
user?.scope !== USER_ACCOUNT
|
||||
userData?.scope !== USER_ACCOUNT
|
||||
? ROUTE_INTERNAL_ACCOUNTS
|
||||
: ROUTE_INTERNAL_APPLICATIONS
|
||||
: `${ROUTE_INTERNAL_ACCOUNTS}/${userData.account_sid}/edit`
|
||||
}
|
||||
state={{ from: location }}
|
||||
replace
|
||||
@@ -91,13 +102,36 @@ export const Login = () => {
|
||||
/>
|
||||
{message && <Message message={message} />}
|
||||
<Button type="submit">Log in</Button>
|
||||
{ENABLE_FORGOT_PASSWORD && (
|
||||
<div>
|
||||
<Link to={ROUTE_FORGOT_PASSWORD} title="Forgot Password">
|
||||
<p>Forgot Password</p>
|
||||
</Link>
|
||||
{(ENABLE_FORGOT_PASSWORD || ENABLE_HOSTED_SYSTEM) && (
|
||||
<div className={ENABLE_HOSTED_SYSTEM ? "mast" : ""}>
|
||||
{ENABLE_HOSTED_SYSTEM && (
|
||||
<Link to={ROUTE_REGISTER} title="Forgot Password">
|
||||
<p>Register</p>
|
||||
</Link>
|
||||
)}
|
||||
{ENABLE_FORGOT_PASSWORD && (
|
||||
<Link to={ROUTE_FORGOT_PASSWORD} title="Forgot Password">
|
||||
<p>Forgot Password</p>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{ENABLE_HOSTED_SYSTEM && (
|
||||
<>
|
||||
<a href={getGoogleOauthUrl(state)} className="btn btn--hollow">
|
||||
<div className="mast">
|
||||
<Icons.Youtube />
|
||||
<span>Sign In With Google</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href={getGithubOauthUrl(state)} className="btn btn--hollow">
|
||||
<div className="mast">
|
||||
<Icons.GitHub />
|
||||
<span>Sign In With Github</span>
|
||||
</div>
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
||||
95
src/containers/login/oauth-callback.tsx
Normal file
95
src/containers/login/oauth-callback.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { getMe, postRegister } from "src/api";
|
||||
import {
|
||||
DEFAULT_SERVICE_PROVIDER_SID,
|
||||
GITHUB_CLIENT_ID,
|
||||
GOOGLE_CLIENT_ID,
|
||||
BASE_URL,
|
||||
} from "src/api/constants";
|
||||
import { Spinner } from "src/components";
|
||||
import { setToken } from "src/router/auth";
|
||||
import {
|
||||
ROUTE_INTERNAL_ACCOUNTS,
|
||||
ROUTE_LOGIN,
|
||||
ROUTE_REGISTER,
|
||||
ROUTE_REGISTER_SUB_DOMAIN,
|
||||
} from "src/router/routes";
|
||||
import { toastError } from "src/store";
|
||||
import {
|
||||
getLocationBeforeOauth,
|
||||
getOauthState,
|
||||
removeLocationBeforeOauth,
|
||||
removeOauthState,
|
||||
setRootDomain,
|
||||
} from "src/store/localStore";
|
||||
|
||||
export const OauthCallback = () => {
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const code = queryParams.get("code");
|
||||
const newState = queryParams.get("state");
|
||||
const originalState = getOauthState();
|
||||
const previousLocation = getLocationBeforeOauth();
|
||||
const { provider } = useParams();
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
if (provider !== "github" && provider !== "google") {
|
||||
toastError(`${provider} is not a valid OAuth provider`);
|
||||
navigate(ROUTE_LOGIN);
|
||||
return;
|
||||
}
|
||||
if (!code || !originalState || !newState || newState !== originalState) {
|
||||
toastError("Invalid state");
|
||||
navigate(ROUTE_LOGIN);
|
||||
}
|
||||
|
||||
let oauth2_client_id;
|
||||
let oauth2_redirect_uri;
|
||||
|
||||
if (provider === "github") {
|
||||
oauth2_client_id = GITHUB_CLIENT_ID;
|
||||
oauth2_redirect_uri = `${BASE_URL}/oauth-callback/github`;
|
||||
} else if (provider === "google") {
|
||||
oauth2_client_id = GOOGLE_CLIENT_ID;
|
||||
oauth2_redirect_uri = `${BASE_URL}/oauth-callback/google`;
|
||||
}
|
||||
|
||||
removeOauthState();
|
||||
removeLocationBeforeOauth();
|
||||
|
||||
postRegister({
|
||||
service_provider_sid: DEFAULT_SERVICE_PROVIDER_SID,
|
||||
provider,
|
||||
oauth2_code: code || "",
|
||||
oauth2_state: originalState,
|
||||
oauth2_client_id,
|
||||
oauth2_redirect_uri,
|
||||
locationBeforeAuth: previousLocation,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
setToken(json.jwt);
|
||||
setRootDomain(json.root_domain);
|
||||
if (previousLocation === "/register") {
|
||||
navigate(ROUTE_REGISTER_SUB_DOMAIN);
|
||||
} else {
|
||||
getMe()
|
||||
.then(({ json: me }) => {
|
||||
if (!me.account?.sip_realm) {
|
||||
navigate(ROUTE_REGISTER_SUB_DOMAIN);
|
||||
} else {
|
||||
navigate(`${ROUTE_INTERNAL_ACCOUNTS}/${json.account_sid}/edit`);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
navigate(ROUTE_REGISTER);
|
||||
});
|
||||
}, []);
|
||||
return <Spinner />;
|
||||
};
|
||||
|
||||
export default OauthCallback;
|
||||
67
src/containers/login/register-email.tsx
Normal file
67
src/containers/login/register-email.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Button, H1 } from "@jambonz/ui-kit";
|
||||
import React, { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { postRegister } from "src/api";
|
||||
import { DEFAULT_SERVICE_PROVIDER_SID } from "src/api/constants";
|
||||
import { Passwd } from "src/components/forms";
|
||||
import { ROUTE_LOGIN, ROUTE_REGISTER_EMAIL_VERIFY } from "src/router/routes";
|
||||
import { generateActivationCode } from "./utils";
|
||||
import { setToken } from "src/router/auth";
|
||||
import { toastError } from "src/store";
|
||||
import { setRootDomain } from "src/store/localStore";
|
||||
|
||||
export const RegisterEmail = () => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const email_activation_code = generateActivationCode();
|
||||
postRegister({
|
||||
service_provider_sid: DEFAULT_SERVICE_PROVIDER_SID,
|
||||
provider: "local",
|
||||
email,
|
||||
password,
|
||||
email_activation_code,
|
||||
inviteCode: undefined, // reserved for inviteCode.
|
||||
})
|
||||
.then(({ json }) => {
|
||||
setToken(json.jwt);
|
||||
setRootDomain(json.root_domain);
|
||||
navigate(ROUTE_REGISTER_EMAIL_VERIFY);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Register</H1>
|
||||
|
||||
<form className="form form--login" onSubmit={handleSubmit}>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
name="email"
|
||||
placeholder="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<Passwd
|
||||
required
|
||||
name="password"
|
||||
value={password}
|
||||
placeholder="Password"
|
||||
setValue={setPassword}
|
||||
/>
|
||||
<Button type="submit">Continue →</Button>
|
||||
<Link to={ROUTE_LOGIN} title="Go back">
|
||||
<p>Go back</p>
|
||||
</Link>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterEmail;
|
||||
54
src/containers/login/register-verify-email.tsx
Normal file
54
src/containers/login/register-verify-email.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Button, H1, MS } from "@jambonz/ui-kit";
|
||||
import React, { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { putActivationCode } from "src/api";
|
||||
import { getToken, parseJwt } from "src/router/auth";
|
||||
import {
|
||||
ROUTE_REGISTER_EMAIL,
|
||||
ROUTE_REGISTER_SUB_DOMAIN,
|
||||
} from "src/router/routes";
|
||||
import { toastError } from "src/store";
|
||||
import { UserData } from "src/store/types";
|
||||
|
||||
export const EmailVerify = () => {
|
||||
const [code, setCode] = useState("");
|
||||
const userData: UserData = parseJwt(getToken());
|
||||
const navigate = useNavigate();
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
putActivationCode(code, {
|
||||
user_sid: userData.user_sid,
|
||||
type: "email",
|
||||
})
|
||||
.then(() => {
|
||||
navigate(ROUTE_REGISTER_SUB_DOMAIN);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Register</H1>
|
||||
|
||||
<form className="form form--login" onSubmit={handleSubmit}>
|
||||
<MS>Please enter the code we just sent to your email</MS>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
name="code"
|
||||
placeholder="Verification Code"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
/>
|
||||
<Button type="submit">Continue →</Button>
|
||||
<Link to={ROUTE_REGISTER_EMAIL} title="Go back">
|
||||
<p>Go back</p>
|
||||
</Link>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailVerify;
|
||||
67
src/containers/login/register.tsx
Normal file
67
src/containers/login/register.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from "react";
|
||||
import { getGithubOauthUrl, getGoogleOauthUrl } from "./utils";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { setLocationBeforeOauth, setOauthState } from "src/store/localStore";
|
||||
import { Icons } from "src/components";
|
||||
import { Button, H1 } from "@jambonz/ui-kit";
|
||||
import { PRIVACY_POLICY, TERMS_OF_SERVICE } from "src/api/constants";
|
||||
import { Checkzone } from "src/components/forms";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ROUTE_REGISTER_EMAIL } from "src/router/routes";
|
||||
|
||||
export const Register = () => {
|
||||
const state = uuid();
|
||||
setOauthState(state);
|
||||
setLocationBeforeOauth("/register");
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Register</H1>
|
||||
|
||||
<form className="form form--login" onSubmit={handleSubmit}>
|
||||
<Checkzone
|
||||
hidden
|
||||
name="is_accepted"
|
||||
label=""
|
||||
labelNode={
|
||||
<div>
|
||||
I accept the
|
||||
<a href={TERMS_OF_SERVICE}>
|
||||
<span> Terms of Service </span>
|
||||
</a>
|
||||
and have read the
|
||||
<a href={PRIVACY_POLICY}>
|
||||
<span> Privacy Policy</span>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
initialCheck={false}
|
||||
>
|
||||
<Button as={Link} to={ROUTE_REGISTER_EMAIL} mainStyle="hollow">
|
||||
<div className="mast">
|
||||
<Icons.Mail />
|
||||
<span>Sign Up With Email</span>
|
||||
</div>
|
||||
</Button>
|
||||
<a href={getGoogleOauthUrl(state)} className="btn btn--hollow">
|
||||
<div className="mast">
|
||||
<Icons.Youtube />
|
||||
<span>Sign Up With Google</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href={getGithubOauthUrl(state)} className="btn btn--hollow">
|
||||
<div className="mast">
|
||||
<Icons.GitHub />
|
||||
<span>Sign Up With Github</span>
|
||||
</div>
|
||||
</a>
|
||||
</Checkzone>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
97
src/containers/login/reset-password.tsx
Normal file
97
src/containers/login/reset-password.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Button, H1 } from "@jambonz/ui-kit";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { postChangepassword, postSignIn } from "src/api";
|
||||
import { Message, Passwd } from "src/components/forms";
|
||||
import { setToken } from "src/router/auth";
|
||||
import { ROUTE_LOGIN } from "src/router/routes";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
|
||||
export const ResetPassword = () => {
|
||||
const params = useParams();
|
||||
const resetId = params.id;
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmNewPassword, setConfirmNewPassword] = useState("");
|
||||
const [isDisableSubmitButton, setIsDisableSubmitButton] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setMessage("");
|
||||
if (newPassword !== confirmNewPassword) {
|
||||
setMessage(
|
||||
"The confirmation password does not match the new password. Please ensure both passwords are identical."
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 6) {
|
||||
setMessage("The password must be at least 7 characters long.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/[a-zA-Z]/.test(newPassword)) {
|
||||
setMessage("Password must contain a letter.");
|
||||
}
|
||||
|
||||
setIsDisableSubmitButton(true);
|
||||
postChangepassword({
|
||||
old_password: resetId,
|
||||
new_password: newPassword,
|
||||
})
|
||||
.then(() => {
|
||||
toastSuccess("New password was successfully set.");
|
||||
setToken("");
|
||||
navigate(ROUTE_LOGIN);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
postSignIn({
|
||||
link: resetId,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
setToken(json.jwt || "");
|
||||
})
|
||||
.catch((error) => toastError(error.msg));
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Reset Password</H1>
|
||||
<form className="form form--login" onSubmit={handleSubmit}>
|
||||
<label htmlFor="new_password">New password</label>
|
||||
<Passwd
|
||||
id="new_password"
|
||||
name="new_password"
|
||||
value={newPassword}
|
||||
placeholder="New password"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setNewPassword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<label htmlFor="confirm_new_password">Confirm new password</label>
|
||||
<Passwd
|
||||
id="confirm_new_password"
|
||||
name="confirm_new_password"
|
||||
value={confirmNewPassword}
|
||||
placeholder="Confirm new password"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setConfirmNewPassword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
{message && <Message message={message} />}
|
||||
<Button type="submit" disabled={isDisableSubmitButton}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPassword;
|
||||
83
src/containers/login/sub-domain.tsx
Normal file
83
src/containers/login/sub-domain.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Button, H1, MS } from "@jambonz/ui-kit";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { getAvailability, postSipRealms } from "src/api";
|
||||
import DomainInput from "src/components/domain-input";
|
||||
import { Message } from "src/components/forms";
|
||||
import { getToken, parseJwt } from "src/router/auth";
|
||||
import { ROUTE_INTERNAL_ACCOUNTS } from "src/router/routes";
|
||||
import { getRootDomain } from "src/store/localStore";
|
||||
import { UserData } from "src/store/types";
|
||||
import { hasValue } from "src/utils";
|
||||
|
||||
export const RegisterChooseSubdomain = () => {
|
||||
const [name, setName] = useState("");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [isValidDomain, setIsValidDomain] = useState(false);
|
||||
const rootDomain = getRootDomain();
|
||||
const userData: UserData = parseJwt(getToken());
|
||||
const navigate = useNavigate();
|
||||
const typingTimeoutRef = useRef<number | null>(null);
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrorMessage("");
|
||||
|
||||
postSipRealms(userData.account_sid || "", `${name}.${rootDomain}`)
|
||||
.then(() => {
|
||||
navigate(`${ROUTE_INTERNAL_ACCOUNTS}/${userData.account_sid}/edit`);
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.msg);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typingTimeoutRef.current) {
|
||||
clearTimeout(typingTimeoutRef.current);
|
||||
}
|
||||
if (!name || name.length < 3) {
|
||||
setIsValidDomain(false);
|
||||
return;
|
||||
}
|
||||
setIsValidDomain(false);
|
||||
typingTimeoutRef.current = setTimeout(() => {
|
||||
getAvailability(`${name}.${rootDomain}`)
|
||||
.then(({ json }) =>
|
||||
setIsValidDomain(
|
||||
Boolean(json.available) && hasValue(name) && name.length != 0
|
||||
)
|
||||
)
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.msg);
|
||||
setIsValidDomain(false);
|
||||
});
|
||||
}, 500);
|
||||
}, [name]);
|
||||
return (
|
||||
<>
|
||||
<H1 className="h2">Choose a subdomain</H1>
|
||||
|
||||
<form className="form form--login" onSubmit={handleSubmit}>
|
||||
{errorMessage && <Message message={errorMessage} />}
|
||||
<MS>
|
||||
This will be the FQDN where your carrier will send calls, and where
|
||||
you can register devices to. This can be changed at any time.
|
||||
</MS>
|
||||
<DomainInput
|
||||
id="subdomain"
|
||||
name="subdomain"
|
||||
value={name}
|
||||
setValue={setName}
|
||||
placeholder="Your name here"
|
||||
root_domain={rootDomain ? `.${rootDomain}` : ""}
|
||||
is_valid={isValidDomain}
|
||||
/>
|
||||
<Button type="submit" disabled={!isValidDomain}>
|
||||
Complete Registration →
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterChooseSubdomain;
|
||||
24
src/containers/login/utils.ts
Normal file
24
src/containers/login/utils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
GITHUB_CLIENT_ID,
|
||||
GOOGLE_CLIENT_ID,
|
||||
BASE_URL,
|
||||
} from "src/api/constants";
|
||||
|
||||
export const getGithubOauthUrl = (state: string) => {
|
||||
return `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&state=${state}&scope=user:email&allow_signup=false`;
|
||||
};
|
||||
|
||||
export const getGoogleOauthUrl = (state: string) => {
|
||||
return `https://accounts.google.com/o/oauth2/v2/auth?scope=email+profile&access_type=offline&include_granted_scopes=true&response_type=code&state=${state}&redirect_uri=${BASE_URL}/oauth-callback/google&client_id=${GOOGLE_CLIENT_ID}`;
|
||||
};
|
||||
|
||||
const length = 6;
|
||||
const digit = () => Math.floor(Math.random() * 10);
|
||||
|
||||
export function generateActivationCode() {
|
||||
let activationCode = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
activationCode += digit();
|
||||
}
|
||||
return activationCode;
|
||||
}
|
||||
@@ -4,13 +4,13 @@
|
||||
import React, { useContext } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { postLogin, postLogout } from "src/api";
|
||||
import { getMe, postLogin, postLogout } from "src/api";
|
||||
import { StatusCodes } from "src/api/types";
|
||||
import {
|
||||
ROUTE_CREATE_PASSWORD,
|
||||
ROUTE_INTERNAL_ACCOUNTS,
|
||||
ROUTE_INTERNAL_APPLICATIONS,
|
||||
ROUTE_LOGIN,
|
||||
ROUTE_REGISTER_SUB_DOMAIN,
|
||||
} from "./routes";
|
||||
import {
|
||||
SESS_OLD_PASSWORD,
|
||||
@@ -23,8 +23,13 @@ import {
|
||||
} from "src/constants";
|
||||
|
||||
import type { UserLogin } from "src/api/types";
|
||||
import { USER_ACCOUNT } from "src/api/constants";
|
||||
import { ENABLE_HOSTED_SYSTEM, USER_ACCOUNT } from "src/api/constants";
|
||||
import type { UserData } from "src/store/types";
|
||||
import { toastError } from "src/store";
|
||||
import {
|
||||
removeLocationBeforeOauth,
|
||||
removeOauthState,
|
||||
} from "src/store/localStore";
|
||||
|
||||
interface SignIn {
|
||||
(username: string, password: string): Promise<UserLogin>;
|
||||
@@ -103,7 +108,23 @@ export const useProvideAuth = (): AuthStateContext => {
|
||||
setToken(token);
|
||||
userData = parseJwt(token);
|
||||
|
||||
if (response.json.force_change) {
|
||||
if (ENABLE_HOSTED_SYSTEM) {
|
||||
getMe()
|
||||
.then(({ json }) => {
|
||||
if (!json.account?.sip_realm) {
|
||||
navigate(ROUTE_REGISTER_SUB_DOMAIN);
|
||||
} else {
|
||||
navigate(
|
||||
userData.scope !== USER_ACCOUNT
|
||||
? ROUTE_INTERNAL_ACCOUNTS
|
||||
: `${ROUTE_INTERNAL_ACCOUNTS}/${userData.account_sid}/edit`
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
} else if (response.json.force_change) {
|
||||
sessionStorage.setItem(SESS_USER_SID, response.json.user_sid);
|
||||
sessionStorage.setItem(SESS_OLD_PASSWORD, password);
|
||||
navigate(ROUTE_CREATE_PASSWORD);
|
||||
@@ -111,7 +132,7 @@ export const useProvideAuth = (): AuthStateContext => {
|
||||
navigate(
|
||||
userData.scope !== USER_ACCOUNT
|
||||
? ROUTE_INTERNAL_ACCOUNTS
|
||||
: ROUTE_INTERNAL_APPLICATIONS
|
||||
: `${ROUTE_INTERNAL_ACCOUNTS}/${userData.account_sid}/edit`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,19 +152,23 @@ export const useProvideAuth = (): AuthStateContext => {
|
||||
}
|
||||
|
||||
reject(MSG_SOMETHING_WRONG);
|
||||
})
|
||||
.finally(() => {
|
||||
removeOauthState();
|
||||
removeLocationBeforeOauth();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const signout = () => {
|
||||
window.location.href = ROUTE_LOGIN;
|
||||
return new Promise((resolve, reject) => {
|
||||
postLogout()
|
||||
.then((response) => {
|
||||
if (response.status === StatusCodes.OK) {
|
||||
if (response.status === StatusCodes.NO_CONTENT) {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
sessionStorage.setItem(SESS_FLASH_MSG, MSG_LOGGED_OUT);
|
||||
window.location.href = ROUTE_LOGIN;
|
||||
resolve(response.json);
|
||||
}
|
||||
})
|
||||
@@ -151,6 +176,7 @@ export const useProvideAuth = (): AuthStateContext => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
sessionStorage.setItem(SESS_FLASH_MSG, MSG_LOGGED_OUT);
|
||||
window.location.href = ROUTE_LOGIN;
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ import { useSelectState } from "src/store";
|
||||
import { Login, Layout as LoginLayout } from "src/containers/login";
|
||||
import { Layout as InternalLayout } from "src/containers/internal";
|
||||
import { NotFound } from "src/containers/notfound";
|
||||
import { ENABLE_FORGOT_PASSWORD } from "src/api/constants";
|
||||
import {
|
||||
ENABLE_HOSTED_SYSTEM,
|
||||
ENABLE_FORGOT_PASSWORD,
|
||||
} from "src/api/constants";
|
||||
|
||||
/** Login */
|
||||
import CreatePassword from "src/containers/login/create-password";
|
||||
@@ -45,6 +48,15 @@ import LcrsEdit from "src/containers/internal/views/least-cost-routing/edit";
|
||||
import Clients from "src/containers/internal/views/clients";
|
||||
import ClientsAdd from "src/containers/internal/views/clients/add";
|
||||
import ClientsEdit from "src/containers/internal/views/clients/edit";
|
||||
import OauthCallback from "src/containers/login/oauth-callback";
|
||||
import Register from "src/containers/login/register";
|
||||
import RegisterEmail from "src/containers/login/register-email";
|
||||
import EmailVerify from "src/containers/login/register-verify-email";
|
||||
import RegisterChooseSubdomain from "src/containers/login/sub-domain";
|
||||
import Subscription from "src/containers/internal/views/accounts/subscription";
|
||||
import ManagePayment from "src/containers/internal/views/accounts/manage-payment";
|
||||
import EditSipRealm from "src/containers/internal/views/accounts/edit-sip-realm";
|
||||
import ResetPassword from "src/containers/login/reset-password";
|
||||
|
||||
export const Router = () => {
|
||||
const toast = useSelectState("toast");
|
||||
@@ -65,9 +77,29 @@ export const Router = () => {
|
||||
}
|
||||
/>
|
||||
{ENABLE_FORGOT_PASSWORD && (
|
||||
<Route path="forgot-password" element={<ForgotPassword />} />
|
||||
<>
|
||||
<Route path="forgot-password" element={<ForgotPassword />} />
|
||||
<Route path="reset-password/:id" element={<ResetPassword />} />
|
||||
</>
|
||||
)}
|
||||
{ENABLE_HOSTED_SYSTEM && (
|
||||
<>
|
||||
<Route path="register" element={<Register />} />
|
||||
<Route path="register/email" element={<RegisterEmail />} />
|
||||
<Route
|
||||
path="register/email/verify-your-email"
|
||||
element={<EmailVerify />}
|
||||
/>
|
||||
<Route
|
||||
path="register/choose-a-subdomain"
|
||||
element={<RegisterChooseSubdomain />}
|
||||
/>
|
||||
<Route
|
||||
path="oauth-callback/:provider"
|
||||
element={<OauthCallback />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 404 page not found */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Route>
|
||||
@@ -90,6 +122,26 @@ export const Router = () => {
|
||||
path="accounts/:account_sid/edit"
|
||||
element={<AccountEdit />}
|
||||
/>
|
||||
{ENABLE_HOSTED_SYSTEM && (
|
||||
<>
|
||||
<Route
|
||||
path="accounts/:account_sid/subscription"
|
||||
element={<Subscription />}
|
||||
/>
|
||||
<Route
|
||||
path="accounts/:account_sid/manage-payment"
|
||||
element={<ManagePayment />}
|
||||
/>
|
||||
<Route
|
||||
path="accounts/:account_sid/modify-subscription"
|
||||
element={<Subscription />}
|
||||
/>
|
||||
<Route
|
||||
path="accounts/:account_sid/sip-realm/edit"
|
||||
element={<EditSipRealm />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Route path="applications" element={<Applications />} />
|
||||
<Route path="applications/add" element={<ApplicationAdd />} />
|
||||
<Route
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
export const ROUTE_LOGIN = "/";
|
||||
export const ROUTE_REGISTER = "/register";
|
||||
export const ROUTE_REGISTER_EMAIL = "/register/email";
|
||||
export const ROUTE_REGISTER_EMAIL_VERIFY = "/register/email/verify-your-email";
|
||||
export const ROUTE_REGISTER_SUB_DOMAIN = "/register/choose-a-subdomain";
|
||||
export const ROUTE_CREATE_PASSWORD = "/create-password";
|
||||
export const ROUTE_FORGOT_PASSWORD = "/forgot-password";
|
||||
export const ROUTE_INTERNAL_USERS = "/internal/users";
|
||||
|
||||
@@ -58,6 +58,48 @@ export const removeQueryFilter = () => {
|
||||
return localStorage.removeItem(storeQueryFilter);
|
||||
};
|
||||
|
||||
/**Oauth2 */
|
||||
const oauthStateKey = "oauth-state";
|
||||
export const getOauthState = () => {
|
||||
return localStorage.getItem(oauthStateKey) || "";
|
||||
};
|
||||
|
||||
export const setOauthState = (token: string) => {
|
||||
localStorage.setItem(oauthStateKey, token);
|
||||
};
|
||||
|
||||
export const removeOauthState = () => {
|
||||
return localStorage.removeItem(oauthStateKey);
|
||||
};
|
||||
|
||||
const locationBeforeOauthKey = "location-before-oauth";
|
||||
|
||||
export const getLocationBeforeOauth = () => {
|
||||
return localStorage.getItem(locationBeforeOauthKey) || "";
|
||||
};
|
||||
|
||||
export const setLocationBeforeOauth = (token: string) => {
|
||||
localStorage.setItem(locationBeforeOauthKey, token);
|
||||
};
|
||||
|
||||
export const removeLocationBeforeOauth = () => {
|
||||
return localStorage.removeItem(locationBeforeOauthKey);
|
||||
};
|
||||
|
||||
// Email register
|
||||
const rootDomainKey = "root-domain";
|
||||
export const setRootDomain = (domain: string) => {
|
||||
return localStorage.setItem(rootDomainKey, domain);
|
||||
};
|
||||
|
||||
export const getRootDomain = () => {
|
||||
return localStorage.getItem(rootDomainKey);
|
||||
};
|
||||
|
||||
export const removeRootDomain = () => {
|
||||
return localStorage.removeItem(rootDomainKey);
|
||||
};
|
||||
|
||||
/**
|
||||
* Methods to get/set the location from local storage
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,9 @@ fieldset {
|
||||
> button {
|
||||
width: 100%;
|
||||
}
|
||||
> a {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.msg {
|
||||
width: 100%;
|
||||
@@ -102,6 +105,10 @@ fieldset {
|
||||
}
|
||||
}
|
||||
|
||||
&--blur {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
label {
|
||||
@include ui-mixins.m();
|
||||
@include ui-mixins.font-medium();
|
||||
@@ -224,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 {
|
||||
@@ -291,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
padding-right: ui-vars.$px02;
|
||||
}
|
||||
}
|
||||
|
||||
&__empty {
|
||||
display: flex;
|
||||
}
|
||||
@@ -112,4 +111,18 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--col4--users {
|
||||
.grid__row {
|
||||
grid-template-columns: [col] 30% [col] 40% [col] 25% [col] 5%;
|
||||
grid-template-rows: [row] auto [row] auto [row] [row] auto;
|
||||
display: grid;
|
||||
justify-content: space-between;
|
||||
|
||||
> div:last-child {
|
||||
text-align: left;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +173,13 @@ details {
|
||||
margin-top: ui-vars.$px02;
|
||||
}
|
||||
|
||||
.pre-grid-white {
|
||||
@extend .pre-grid;
|
||||
background-color: ui-vars.$white;
|
||||
color: ui-vars.$dark;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.pcap {
|
||||
margin-top: ui-vars.$px02;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
108
src/vendor/index.tsx
vendored
108
src/vendor/index.tsx
vendored
@@ -1,13 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type {
|
||||
VendorOptions,
|
||||
SynthesisVendors,
|
||||
RecognizerVendors,
|
||||
RegionVendors,
|
||||
} from "./types";
|
||||
import type { VendorOptions, RegionVendors } 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 +16,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 +62,23 @@ 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 useRegionVendors = () => {
|
||||
const [regions, setRegions] = useState<RegionVendors>();
|
||||
@@ -96,80 +113,3 @@ export const useRegionVendors = () => {
|
||||
|
||||
return regions;
|
||||
};
|
||||
|
||||
export const useSpeechVendors = () => {
|
||||
const [speech, setSpeech] = useState<{
|
||||
synthesis?: SynthesisVendors;
|
||||
recognizers?: RecognizerVendors;
|
||||
}>({});
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
Promise.all([
|
||||
import("./speech-recognizer/aws-speech-recognizer-lang"),
|
||||
import("./speech-recognizer/google-speech-recognizer-lang"),
|
||||
import("./speech-recognizer/ms-speech-recognizer-lang"),
|
||||
import("./speech-recognizer/nuance-speech-recognizer-lang"),
|
||||
import("./speech-recognizer/deepgram-speech-recognizer-lang"),
|
||||
import("./speech-recognizer/ibm-speech-recognizer-lang"),
|
||||
import("./speech-recognizer/nvidia-speech-recognizer-lang"),
|
||||
import("./speech-recognizer/soniox-speech-recognizer-lang"),
|
||||
import("./speech-synthesis/aws-speech-synthesis-lang"),
|
||||
import("./speech-synthesis/google-speech-synthesis-lang"),
|
||||
import("./speech-synthesis/ms-speech-synthesis-lang"),
|
||||
import("./speech-synthesis/wellsaid-speech-synthesis-lang"),
|
||||
import("./speech-synthesis/nuance-speech-synthesis-lang"),
|
||||
import("./speech-synthesis/ibm-speech-synthesis-lang"),
|
||||
import("./speech-synthesis/nvidia-speech-synthesis-lang"),
|
||||
]).then(
|
||||
([
|
||||
{ default: awsRecognizer },
|
||||
{ default: googleRecognizer },
|
||||
{ default: msRecognizer },
|
||||
{ default: nuanceRecognizer },
|
||||
{ default: deepgramRecognizer },
|
||||
{ default: ibmRecognizer },
|
||||
{ default: nvidiaRecognizer },
|
||||
{ default: sonioxRecognizer },
|
||||
{ default: awsSynthesis },
|
||||
{ default: googleSynthesis },
|
||||
{ default: msSynthesis },
|
||||
{ default: wellsaidSynthesis },
|
||||
{ default: nuanceSynthesis },
|
||||
{ default: ibmSynthesis },
|
||||
{ default: nvidiaynthesis },
|
||||
]) => {
|
||||
if (!ignore) {
|
||||
setSpeech({
|
||||
synthesis: {
|
||||
aws: awsSynthesis,
|
||||
google: googleSynthesis,
|
||||
microsoft: msSynthesis,
|
||||
wellsaid: wellsaidSynthesis,
|
||||
nuance: nuanceSynthesis,
|
||||
ibm: ibmSynthesis,
|
||||
nvidia: nvidiaynthesis,
|
||||
},
|
||||
recognizers: {
|
||||
aws: awsRecognizer,
|
||||
google: googleRecognizer,
|
||||
microsoft: msRecognizer,
|
||||
nuance: nuanceRecognizer,
|
||||
deepgram: deepgramRecognizer,
|
||||
ibm: ibmRecognizer,
|
||||
nvidia: nvidiaRecognizer,
|
||||
soniox: sonioxRecognizer,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return function cleanup() {
|
||||
ignore = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return speech;
|
||||
};
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { Language } from "../types";
|
||||
|
||||
export const languages: Language[] = [
|
||||
{ name: "Australian English", code: "en-AU" },
|
||||
{ name: "British English", code: "en-GB" },
|
||||
{ name: "US English", code: "en-US" },
|
||||
{ name: "French", code: "fr-FR" },
|
||||
{ name: "Canadian French", code: "fr-CA" },
|
||||
{ name: "German", code: "de-DE" },
|
||||
{ name: "Italian", code: "it-IT" },
|
||||
{ name: "US Spanish", code: "es-US" },
|
||||
];
|
||||
|
||||
export default languages;
|
||||
@@ -1,126 +0,0 @@
|
||||
import type { Language } from "../types";
|
||||
|
||||
export const languages: Language[] = [
|
||||
{
|
||||
name: "Chinese - general",
|
||||
code: "zh",
|
||||
},
|
||||
{
|
||||
name: "Chinese (China)",
|
||||
code: "zh-CN",
|
||||
},
|
||||
{
|
||||
name: "Chinese (Taiwan)",
|
||||
code: "zh-TW",
|
||||
},
|
||||
{
|
||||
name: "Dutch - general",
|
||||
code: "nl",
|
||||
},
|
||||
{
|
||||
name: "English - general",
|
||||
code: "en",
|
||||
},
|
||||
{
|
||||
name: "English (Australia)",
|
||||
code: "en-AU",
|
||||
},
|
||||
{
|
||||
name: "English (United Kingdom)",
|
||||
code: "en-GB",
|
||||
},
|
||||
{
|
||||
name: "English (India)",
|
||||
code: "en-IN",
|
||||
},
|
||||
{
|
||||
name: "English (New Zealand)",
|
||||
code: "en-NZ",
|
||||
},
|
||||
{
|
||||
name: "English (United States)",
|
||||
code: "en-US",
|
||||
},
|
||||
{
|
||||
name: "French - general",
|
||||
code: "fr",
|
||||
},
|
||||
{
|
||||
name: "French (Canada)",
|
||||
code: "fr-CA",
|
||||
},
|
||||
{
|
||||
name: "German - general",
|
||||
code: "de",
|
||||
},
|
||||
{
|
||||
name: "Hindi - general",
|
||||
code: "hi",
|
||||
},
|
||||
{
|
||||
name: "Hindi (Roman Script)",
|
||||
code: "hi-Latin",
|
||||
},
|
||||
{
|
||||
name: "Indonesian - general",
|
||||
code: "in",
|
||||
},
|
||||
{
|
||||
name: "Italian - general",
|
||||
code: "it",
|
||||
},
|
||||
{
|
||||
name: "Japanese - general",
|
||||
code: "ja",
|
||||
},
|
||||
{
|
||||
name: "Korean - general",
|
||||
code: "ko",
|
||||
},
|
||||
{
|
||||
name: "Norwegian - general",
|
||||
code: "no",
|
||||
},
|
||||
{
|
||||
name: "Polish - general",
|
||||
code: "pl",
|
||||
},
|
||||
{
|
||||
name: "Portuguese - general",
|
||||
code: "pt",
|
||||
},
|
||||
{
|
||||
name: "Portuguese (Brazil)",
|
||||
code: "pt-BR",
|
||||
},
|
||||
{
|
||||
name: "Portuguese (Portugal)",
|
||||
code: "pt-PT",
|
||||
},
|
||||
{
|
||||
name: "Russian - general",
|
||||
code: "ru",
|
||||
},
|
||||
{
|
||||
name: "Spanish - general",
|
||||
code: "es",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Latin America)",
|
||||
code: "es-419",
|
||||
},
|
||||
{
|
||||
name: "Swedish - general",
|
||||
code: "sv",
|
||||
},
|
||||
{
|
||||
name: "Turkish - general",
|
||||
code: "tr",
|
||||
},
|
||||
{
|
||||
name: "Ukrainian - general",
|
||||
code: "uk",
|
||||
},
|
||||
];
|
||||
|
||||
export default languages;
|
||||
@@ -1,134 +0,0 @@
|
||||
import type { Language } from "../types";
|
||||
|
||||
export const languages: Language[] = [
|
||||
{ name: "Afrikaans (South Africa)", code: "af-ZA" },
|
||||
{ name: "Albanian (Albania)", code: "sq-AL" },
|
||||
{ name: "Amharic (Ethiopia)", code: "am-ET" },
|
||||
{ name: "Arabic (Algeria)", code: "ar-DZ" },
|
||||
{ name: "Arabic (Bahrain)", code: "ar-BH" },
|
||||
{ name: "Arabic (Egypt)", code: "ar-EG" },
|
||||
{ name: "Arabic (Iraq)", code: "ar-IQ" },
|
||||
{ name: "Arabic (Israel)", code: "ar-IL" },
|
||||
{ name: "Arabic (Jordan)", code: "ar-JO" },
|
||||
{ name: "Arabic (Kuwait)", code: "ar-KW" },
|
||||
{ name: "Arabic (Lebanon)", code: "ar-LB" },
|
||||
{ name: "Arabic (Morocco)", code: "ar-MA" },
|
||||
{ name: "Arabic (Oman)", code: "ar-OM" },
|
||||
{ name: "Arabic (Qatar)", code: "ar-QA" },
|
||||
{ name: "Arabic (Saudi Arabia)", code: "ar-SA" },
|
||||
{ name: "Arabic (State of Palestine)", code: "ar-PS" },
|
||||
{ name: "Arabic (Tunisia)", code: "ar-TN" },
|
||||
{ name: "Arabic (United Arab Emirates)", code: "ar-AE" },
|
||||
{ name: "Armenian (Armenia)", code: "hy-AM" },
|
||||
{ name: "Azerbaijani (Azerbaijan)", code: "az-AZ" },
|
||||
{ name: "Basque (Spain)", code: "eu-ES" },
|
||||
{ name: "Bengali (Bangladesh)", code: "bn-BD" },
|
||||
{ name: "Bengali (India)", code: "bn-IN" },
|
||||
{ name: "Bulgarian (Bulgaria)", code: "bg-BG" },
|
||||
{ name: "Burmese (Myanmar)", code: "my-MM" },
|
||||
{ name: "Catalan (Spain)", code: "ca-ES" },
|
||||
{ name: "Chinese, Cantonese (Traditional, Hong Kong)", code: "yue-Hant-HK" },
|
||||
{ name: "Chinese, Mandarin (Simplified, China)", code: "zh" },
|
||||
{ name: "Chinese, Mandarin (Simplified, Hong Kong)", code: "zh-HK" },
|
||||
{ name: "Chinese, Mandarin (Simplified, Taiwan)", code: "zh-TW" },
|
||||
{ name: "Croatian (Croatia)", code: "hr-HR" },
|
||||
{ name: "Czech (Czech Republic)", code: "cs-CZ" },
|
||||
{ name: "Danish (Denmark)", code: "da-DK" },
|
||||
{ name: "Dutch (Belgium)", code: "nl-BE" },
|
||||
{ name: "Dutch (Netherlands)", code: "nl-NL" },
|
||||
{ name: "English (Australia)", code: "en-AU" },
|
||||
{ name: "English (Canada)", code: "en-CA" },
|
||||
{ name: "English (Ghana)", code: "en-GH" },
|
||||
{ name: "English (India)", code: "en-IN" },
|
||||
{ name: "English (Ireland)", code: "en-IE" },
|
||||
{ name: "English (Kenya)", code: "en-KE" },
|
||||
{ name: "English (New Zealand)", code: "en-NZ" },
|
||||
{ name: "English (Nigeria)", code: "en-NG" },
|
||||
{ name: "English (Philippines)", code: "en-PH" },
|
||||
{ name: "English (Singapore)", code: "en-SG" },
|
||||
{ name: "English (South Africa)", code: "en-ZA" },
|
||||
{ name: "English (Tanzania)", code: "en-TZ" },
|
||||
{ name: "English (United Kingdom)", code: "en-GB" },
|
||||
{ name: "English (United States)", code: "en-US" },
|
||||
{ name: "Estonian (Estonia)", code: "et-EE" },
|
||||
{ name: "Filipino (Philippines)", code: "fil-PH" },
|
||||
{ name: "Finnish (Finland)", code: "fi-FI" },
|
||||
{ name: "French (Canada)", code: "fr-CA" },
|
||||
{ name: "French (France)", code: "fr-FR" },
|
||||
{ name: "Galician (Spain)", code: "gl-ES" },
|
||||
{ name: "Georgian (Georgia)", code: "ka-GE" },
|
||||
{ name: "German (Germany)", code: "de-DE" },
|
||||
{ name: "Greek (Greece)", code: "el-GR" },
|
||||
{ name: "Gujarati (India)", code: "gu-IN" },
|
||||
{ name: "Hebrew (Israel)", code: "he-IL" },
|
||||
{ name: "Hindi (India)", code: "hi-IN" },
|
||||
{ name: "Hungarian (Hungary)", code: "hu-HU" },
|
||||
{ name: "Icelandic (Iceland)", code: "is-IS" },
|
||||
{ name: "Indonesian (Indonesia)", code: "id-ID" },
|
||||
{ name: "Italian (Italy)", code: "it-IT" },
|
||||
{ name: "Japanese (Japan)", code: "ja-JP" },
|
||||
{ name: "Javanese (Indonesia)", code: "jv-ID" },
|
||||
{ name: "Kannada (India)", code: "kn-IN" },
|
||||
{ name: "Khmer (Cambodia)", code: "km-KH" },
|
||||
{ name: "Korean (South Korea)", code: "ko-KR" },
|
||||
{ name: "Lao (Laos)", code: "lo-LA" },
|
||||
{ name: "Latvian (Latvia)", code: "lv-LV" },
|
||||
{ name: "Lithuanian (Lithuania)", code: "lt-LT" },
|
||||
{ name: "Macedonian (North Macedonia)", code: "mk-MK" },
|
||||
{ name: "Malay (Malaysia)", code: "ms-MY" },
|
||||
{ name: "Malayalam (India)", code: "ml-IN" },
|
||||
{ name: "Marathi (India)", code: "mr-IN" },
|
||||
{ name: "Mongolian (Mongolia)", code: "mn-MN" },
|
||||
{ name: "Nepali (Nepal)", code: "ne-NP" },
|
||||
{ name: "Norwegian Bokmål (Norway)", code: "nb-NO" },
|
||||
{ name: "Persian (Iran)", code: "fa-IR" },
|
||||
{ name: "Polish (Poland)", code: "pl-PL" },
|
||||
{ name: "Portuguese (Brazil)", code: "pt-BR" },
|
||||
{ name: "Portuguese (Portugal)", code: "pt-PT" },
|
||||
{ name: "Punjabi (Gurmukhi, India)", code: "pa-guru-IN" },
|
||||
{ name: "Romanian (Romania)", code: "ro-RO" },
|
||||
{ name: "Russian (Russia)", code: "ru-RU" },
|
||||
{ name: "Serbian (Serbia)", code: "sr-RS" },
|
||||
{ name: "Sinhala (Sri Lanka)", code: "si-LK" },
|
||||
{ name: "Slovak (Slovakia)", code: "sk-SK" },
|
||||
{ name: "Slovenian (Slovenia)", code: "sl-SI" },
|
||||
{ name: "Spanish (Argentina)", code: "es-AR" },
|
||||
{ name: "Spanish (Bolivia)", code: "es-BO" },
|
||||
{ name: "Spanish (Chile)", code: "es-CL" },
|
||||
{ name: "Spanish (Colombia)", code: "es-CO" },
|
||||
{ name: "Spanish (Costa Rica)", code: "es-CR" },
|
||||
{ name: "Spanish (Dominican Republic)", code: "es-DO" },
|
||||
{ name: "Spanish (Ecuador)", code: "es-EC" },
|
||||
{ name: "Spanish (El Salvador)", code: "es-SV" },
|
||||
{ name: "Spanish (Guatemala)", code: "es-GT" },
|
||||
{ name: "Spanish (Honduras)", code: "es-HN" },
|
||||
{ name: "Spanish (Mexico)", code: "es-MX" },
|
||||
{ name: "Spanish (Nicaragua)", code: "es-NI" },
|
||||
{ name: "Spanish (Panama)", code: "es-PA" },
|
||||
{ name: "Spanish (Paraguay)", code: "es-PY" },
|
||||
{ name: "Spanish (Peru)", code: "es-PE" },
|
||||
{ name: "Spanish (Puerto Rico)", code: "es-PR" },
|
||||
{ name: "Spanish (Spain)", code: "es-ES" },
|
||||
{ name: "Spanish (United States)", code: "es-US" },
|
||||
{ name: "Spanish (Uruguay)", code: "es-UY" },
|
||||
{ name: "Spanish (Venezuela)", code: "es-VE" },
|
||||
{ name: "Sundanese (Indonesia)", code: "su-ID" },
|
||||
{ name: "Swahili (Kenya)", code: "sw-KE" },
|
||||
{ name: "Swahili (Tanzania)", code: "sw-TZ" },
|
||||
{ name: "Swedish (Sweden)", code: "sv-SE" },
|
||||
{ name: "Tamil (India)", code: "ta-IN" },
|
||||
{ name: "Tamil (Malaysia)", code: "ta-MY" },
|
||||
{ name: "Tamil (Singapore)", code: "ta-SG" },
|
||||
{ name: "Tamil (Sri Lanka)", code: "ta-LK" },
|
||||
{ name: "Telugu (India)", code: "te-IN" },
|
||||
{ name: "Thai (Thailand)", code: "th-TH" },
|
||||
{ name: "Turkish (Turkey)", code: "tr-TR" },
|
||||
{ name: "Ukrainian (Ukraine)", code: "uk-UA" },
|
||||
{ name: "Urdu (India)", code: "ur-IN" },
|
||||
{ name: "Urdu (Pakistan)", code: "ur-PK" },
|
||||
{ name: "Uzbek (Uzbekistan)", code: "uz-UZ" },
|
||||
{ name: "Vietnamese (Vietnam)", code: "vi-VN" },
|
||||
{ name: "Zulu (South Africa)", code: "zu-ZA" },
|
||||
];
|
||||
|
||||
export default languages;
|
||||
@@ -1,86 +0,0 @@
|
||||
import type { Language } from "../types";
|
||||
|
||||
export const languages: Language[] = [
|
||||
{
|
||||
name: "Arabic (Modern Standard)",
|
||||
code: "ar-MS_Telephony",
|
||||
},
|
||||
{
|
||||
name: "Chinese (Mandarin)",
|
||||
code: "zh-CN_Telephony",
|
||||
},
|
||||
{
|
||||
name: "Czech ",
|
||||
code: "cs-CZ_Telephony",
|
||||
},
|
||||
{
|
||||
name: "Dutch (Belgian)",
|
||||
code: "nl-BE_Telephony",
|
||||
},
|
||||
{
|
||||
name: "Dutch (Netherlands)",
|
||||
code: "nl-NL_Telephony",
|
||||
},
|
||||
{
|
||||
name: "English (all supported dialects)",
|
||||
code: "en-WW_Medical_Telephony",
|
||||
},
|
||||
{
|
||||
name: "English (Australian)",
|
||||
code: "en-AU_Telephony",
|
||||
},
|
||||
{
|
||||
name: "English (Indian)",
|
||||
code: "en-IN_Telephony",
|
||||
},
|
||||
{
|
||||
name: "English (United Kingdom)",
|
||||
code: "en-GB_Telephony",
|
||||
},
|
||||
{
|
||||
name: "English (United States)",
|
||||
code: "en-US_Telephony",
|
||||
},
|
||||
{
|
||||
name: "French (Canadian)",
|
||||
code: "fr-CA_Telephony",
|
||||
},
|
||||
{
|
||||
name: "French (France)",
|
||||
code: "fr-FR_Telephony",
|
||||
},
|
||||
{
|
||||
name: "German",
|
||||
code: "de-DE_Telephony",
|
||||
},
|
||||
{
|
||||
name: "Hindi (Indian)",
|
||||
code: "hi-IN_Telephony",
|
||||
},
|
||||
{
|
||||
name: "Italian",
|
||||
code: "it-IT_Telephony",
|
||||
},
|
||||
{
|
||||
name: "Korean",
|
||||
code: "ko-KR_Telephony",
|
||||
},
|
||||
{
|
||||
name: "Portuguese (Brazilian)",
|
||||
code: "pt-BR_Telephony",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Mexican)",
|
||||
code: "es-LA_Telephony",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Castilian)",
|
||||
code: "es-ES_Telephony",
|
||||
},
|
||||
{
|
||||
name: "Swedish ",
|
||||
code: "sv-SE_Telephony",
|
||||
},
|
||||
];
|
||||
|
||||
export default languages;
|
||||
@@ -1,494 +0,0 @@
|
||||
import type { Language } from "../types";
|
||||
|
||||
export const languages: Language[] = [
|
||||
{
|
||||
name: "Afrikaans (South Africa)",
|
||||
code: "af-ZA",
|
||||
},
|
||||
{
|
||||
name: "Amharic (Ethiopia)",
|
||||
code: "am-ET",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Algeria)",
|
||||
code: "ar-DZ",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Bahrain)",
|
||||
code: "ar-BH",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Egypt)",
|
||||
code: "ar-EG",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Iraq)",
|
||||
code: "ar-IQ",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Israel)",
|
||||
code: "ar-IL",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Jordan)",
|
||||
code: "ar-JO",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Kuwait)",
|
||||
code: "ar-KW",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Lebanon)",
|
||||
code: "ar-LB",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Libya)",
|
||||
code: "ar-LY",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Morocco)",
|
||||
code: "ar-MA",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Oman)",
|
||||
code: "ar-OM",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Qatar)",
|
||||
code: "ar-QA",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Saudi Arabia)",
|
||||
code: "ar-SA",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Palestinian Authority)",
|
||||
code: "ar-PS",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Syria)",
|
||||
code: "ar-SY",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Tunisia)",
|
||||
code: "ar-TN",
|
||||
},
|
||||
{
|
||||
name: "Arabic (United Arab Emirates)",
|
||||
code: "ar-AE",
|
||||
},
|
||||
{
|
||||
name: "Arabic (Yemen)",
|
||||
code: "ar-YE",
|
||||
},
|
||||
{
|
||||
name: "Bulgarian (Bulgaria)",
|
||||
code: "bg-BG",
|
||||
},
|
||||
{
|
||||
name: "Bengali (India)",
|
||||
code: "bn-IN",
|
||||
},
|
||||
{
|
||||
name: "Catalan (Spain)",
|
||||
code: "ca-ES",
|
||||
},
|
||||
{
|
||||
name: "Chinese (Cantonese, Traditional)",
|
||||
code: "zh-HK",
|
||||
},
|
||||
{
|
||||
name: "Chinese (Mandarin, Simplified)",
|
||||
code: "zh-CN",
|
||||
},
|
||||
{
|
||||
name: "Chinese (Taiwanese Mandarin)",
|
||||
code: "zh-TW",
|
||||
},
|
||||
{
|
||||
name: "Croatian (Croatia)",
|
||||
code: "hr-HR",
|
||||
},
|
||||
{
|
||||
name: "Czech (Czech)",
|
||||
code: "cs-CZ",
|
||||
},
|
||||
{
|
||||
name: "Danish (Denmark)",
|
||||
code: "da-DK",
|
||||
},
|
||||
{
|
||||
name: "Dutch (Netherlands)",
|
||||
code: "nl-NL",
|
||||
},
|
||||
{
|
||||
name: "Dutch (Belgium)",
|
||||
code: "nl-BE",
|
||||
},
|
||||
{
|
||||
name: "English (Australia)",
|
||||
code: "en-AU",
|
||||
},
|
||||
{
|
||||
name: "English (Canada)",
|
||||
code: "en-CA",
|
||||
},
|
||||
{
|
||||
name: "English (Ghana)",
|
||||
code: "en-GH",
|
||||
},
|
||||
{
|
||||
name: "English (Hong Kong)",
|
||||
code: "en-HK",
|
||||
},
|
||||
{
|
||||
name: "English (India)",
|
||||
code: "en-IN",
|
||||
},
|
||||
{
|
||||
name: "English (Ireland)",
|
||||
code: "en-IE",
|
||||
},
|
||||
{
|
||||
name: "English (Kenya)",
|
||||
code: "en-KE",
|
||||
},
|
||||
{
|
||||
name: "English (New Zealand)",
|
||||
code: "en-NZ",
|
||||
},
|
||||
{
|
||||
name: "English (Nigeria)",
|
||||
code: "en-NG",
|
||||
},
|
||||
{
|
||||
name: "English (Philippines)",
|
||||
code: "en-PH",
|
||||
},
|
||||
{
|
||||
name: "English (Singapore)",
|
||||
code: "en-SG",
|
||||
},
|
||||
{
|
||||
name: "English (South Africa)",
|
||||
code: "en-ZA",
|
||||
},
|
||||
{
|
||||
name: "English (Tanzania)",
|
||||
code: "en-TZ",
|
||||
},
|
||||
{
|
||||
name: "English (United Kingdom)",
|
||||
code: "en-GB",
|
||||
},
|
||||
{
|
||||
name: "English (United States)",
|
||||
code: "en-US",
|
||||
},
|
||||
{
|
||||
name: "Estonian(Estonia)",
|
||||
code: "et-EE",
|
||||
},
|
||||
{
|
||||
name: "Filipino (Philippines)",
|
||||
code: "fil-PH",
|
||||
},
|
||||
{
|
||||
name: "Finnish (Finland)",
|
||||
code: "fi-FI",
|
||||
},
|
||||
{
|
||||
name: "French (Belgium)",
|
||||
code: "fr-BE",
|
||||
},
|
||||
{
|
||||
name: "French (Canada)",
|
||||
code: "fr-CA",
|
||||
},
|
||||
{
|
||||
name: "French (France)",
|
||||
code: "fr-FR",
|
||||
},
|
||||
{
|
||||
name: "French (Switzerland)",
|
||||
code: "fr-CH",
|
||||
},
|
||||
{
|
||||
name: "German (Austria)",
|
||||
code: "de-AT",
|
||||
},
|
||||
{
|
||||
name: "German (Switzerland)",
|
||||
code: "de-CH",
|
||||
},
|
||||
{
|
||||
name: "German (Germany)",
|
||||
code: "de-DE",
|
||||
},
|
||||
{
|
||||
name: "Greek (Greece)",
|
||||
code: "el-GR",
|
||||
},
|
||||
{
|
||||
name: "Gujarati (Indian)",
|
||||
code: "gu-IN",
|
||||
},
|
||||
{
|
||||
name: "Hebrew (Israel)",
|
||||
code: "he-IL",
|
||||
},
|
||||
{
|
||||
name: "Hindi (India)",
|
||||
code: "hi-IN",
|
||||
},
|
||||
{
|
||||
name: "Hungarian (Hungary)",
|
||||
code: "hu-HU",
|
||||
},
|
||||
{
|
||||
name: "Indonesian (Indonesia)",
|
||||
code: "id-ID",
|
||||
},
|
||||
{
|
||||
name: "Icelandic (Iceland)",
|
||||
code: "is-IS",
|
||||
},
|
||||
{
|
||||
name: "Irish (Ireland)",
|
||||
code: "ga-IE",
|
||||
},
|
||||
{
|
||||
name: "Italian (Italy)",
|
||||
code: "it-IT",
|
||||
},
|
||||
{
|
||||
name: "Japanese (Japan)",
|
||||
code: "ja-JP",
|
||||
},
|
||||
{
|
||||
name: "Javanese (Indonesia)",
|
||||
code: "jv-ID",
|
||||
},
|
||||
{
|
||||
name: "Kannada (India)",
|
||||
code: "kn-IN",
|
||||
},
|
||||
{
|
||||
name: "Khmer (Cambodia)",
|
||||
code: "km-KH",
|
||||
},
|
||||
{
|
||||
name: "Korean (Korea)",
|
||||
code: "ko-KR",
|
||||
},
|
||||
{
|
||||
name: "Latvian (Latvia)",
|
||||
code: "lv-LV",
|
||||
},
|
||||
{
|
||||
name: "Lao (Laos)",
|
||||
code: "lo-LA",
|
||||
},
|
||||
{
|
||||
name: "Lithuanian (Lithuania)",
|
||||
code: "lt-LT",
|
||||
},
|
||||
{
|
||||
name: "Malay (Malaysia)",
|
||||
code: "ms-MY",
|
||||
},
|
||||
{
|
||||
name: "Macedonian (North Macedonia)",
|
||||
code: "mk-MK",
|
||||
},
|
||||
{
|
||||
name: "Maltese (Malta)",
|
||||
code: "mt-MT",
|
||||
},
|
||||
{
|
||||
name: "Marathi (India)",
|
||||
code: "mr-IN",
|
||||
},
|
||||
{
|
||||
name: "Burmese (Myanmar)",
|
||||
code: "my-MM",
|
||||
},
|
||||
{
|
||||
name: "Norwegian (Bokmål, Norway)",
|
||||
code: "nb-NO",
|
||||
},
|
||||
{
|
||||
name: "Persian (Iran)",
|
||||
code: "fa-IR",
|
||||
},
|
||||
{
|
||||
name: "Polish (Poland)",
|
||||
code: "pl-PL",
|
||||
},
|
||||
{
|
||||
name: "Portuguese (Brazil)",
|
||||
code: "pt-BR",
|
||||
},
|
||||
{
|
||||
name: "Portuguese (Portugal)",
|
||||
code: "pt-PT",
|
||||
},
|
||||
{
|
||||
name: "Romanian (Romania)",
|
||||
code: "ro-RO",
|
||||
},
|
||||
{
|
||||
name: "Russian (Russia)",
|
||||
code: "ru-RU",
|
||||
},
|
||||
{
|
||||
name: "Slovak (Slovakia)",
|
||||
code: "sk-SK",
|
||||
},
|
||||
{
|
||||
name: "Slovenian (Slovenia)",
|
||||
code: "sl-SI",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Argentina)",
|
||||
code: "es-AR",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Bolivia)",
|
||||
code: "es-BO",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Chile)",
|
||||
code: "es-CL",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Colombia)",
|
||||
code: "es-CO",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Costa Rica)",
|
||||
code: "es-CR",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Cuba)",
|
||||
code: "es-CU",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Dominican Republic)",
|
||||
code: "es-DO",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Ecuador)",
|
||||
code: "es-EC",
|
||||
},
|
||||
{
|
||||
name: "Spanish (El Salvador)",
|
||||
code: "es-SV",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Equatorial Guinea)",
|
||||
code: "es-GQ",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Guatemala)",
|
||||
code: "es-GT",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Honduras)",
|
||||
code: "es-HN",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Mexico)",
|
||||
code: "es-MX",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Nicaragua)",
|
||||
code: "es-NI",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Panama)",
|
||||
code: "es-PA",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Paraguay)",
|
||||
code: "es-PY",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Peru)",
|
||||
code: "es-PE",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Puerto Rico)",
|
||||
code: "es-PR",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Spain)",
|
||||
code: "es-ES",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Uruguay)",
|
||||
code: "es-UY",
|
||||
},
|
||||
{
|
||||
name: "Spanish (USA)",
|
||||
code: "es-US",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Venezuela)",
|
||||
code: "es-VE",
|
||||
},
|
||||
{
|
||||
name: "Swahili (Kenya)",
|
||||
code: "sw-KE",
|
||||
},
|
||||
{
|
||||
name: "Swahili (Tanzania)",
|
||||
code: "sw-TZ",
|
||||
},
|
||||
{
|
||||
name: "Sinhala (Sri Lanka)",
|
||||
code: "si-LK",
|
||||
},
|
||||
{
|
||||
name: "Swedish (Sweden)",
|
||||
code: "sv-SE",
|
||||
},
|
||||
{
|
||||
name: "Serbian (Serbia)",
|
||||
code: "sr-RS",
|
||||
},
|
||||
{
|
||||
name: "Tamil (India)",
|
||||
code: "ta-IN",
|
||||
},
|
||||
{
|
||||
name: "Telugu (India)",
|
||||
code: "te-IN",
|
||||
},
|
||||
{
|
||||
name: "Thai (Thailand)",
|
||||
code: "th-TH",
|
||||
},
|
||||
{
|
||||
name: "Turkish (Turkey)",
|
||||
code: "tr-TR",
|
||||
},
|
||||
{
|
||||
name: "Ukrainian (Ukraine)",
|
||||
code: "uk-UA",
|
||||
},
|
||||
{
|
||||
name: "Uzbek (Uzbekistan)",
|
||||
code: "uz-UZ",
|
||||
},
|
||||
{
|
||||
name: "Zulu (South Africa)",
|
||||
code: "zu-ZA",
|
||||
},
|
||||
{
|
||||
name: "Vietnamese (Vietnam)",
|
||||
code: "vi-VN",
|
||||
},
|
||||
];
|
||||
|
||||
export default languages;
|
||||
@@ -1,211 +0,0 @@
|
||||
import type { Language } from "../types";
|
||||
|
||||
export const languages: Language[] = [
|
||||
{
|
||||
name: "Arabic (Worldwide)",
|
||||
code: "ar-WW",
|
||||
codeMix: "ara-XWW",
|
||||
},
|
||||
{
|
||||
name: "Catalan (Spain)",
|
||||
code: "ca-ES",
|
||||
codeMix: "cat-ESP",
|
||||
},
|
||||
{
|
||||
name: "Croatian (Croatia)",
|
||||
code: "hr-HR",
|
||||
codeMix: "hrv-HRV",
|
||||
},
|
||||
{
|
||||
name: "Czech (Czech Republic)",
|
||||
code: "cs-CZ",
|
||||
codeMix: "ces-CZE",
|
||||
},
|
||||
{
|
||||
name: "Danish (Denmark)",
|
||||
code: "da-DK",
|
||||
codeMix: "dan-DNK",
|
||||
},
|
||||
{
|
||||
name: "Dutch (Netherlands)",
|
||||
code: "nl-NL",
|
||||
codeMix: "nld-NLD",
|
||||
},
|
||||
{
|
||||
name: "English (Australia)",
|
||||
code: "en-AU",
|
||||
codeMix: "eng-AUS",
|
||||
},
|
||||
{
|
||||
name: "English (United States)",
|
||||
code: "en-US",
|
||||
codeMix: "eng-USA",
|
||||
},
|
||||
{
|
||||
name: "English (India)",
|
||||
code: "en-IN",
|
||||
codeMix: "eng-IND",
|
||||
},
|
||||
{
|
||||
name: "English (United Kingdom)",
|
||||
code: "en-GB",
|
||||
codeMix: "eng-GBR",
|
||||
},
|
||||
{
|
||||
name: "Finnish (Finland)",
|
||||
code: "fi-FI",
|
||||
codeMix: "fin-FIN",
|
||||
},
|
||||
{
|
||||
name: "French (Canada)",
|
||||
code: "fr-CA",
|
||||
codeMix: "fra-CAN",
|
||||
},
|
||||
{
|
||||
name: "French (France)",
|
||||
code: "fr-FR",
|
||||
codeMix: "fra-FRA",
|
||||
},
|
||||
{
|
||||
name: "German (Germany)",
|
||||
code: "de-DE",
|
||||
codeMix: "deu-DEU",
|
||||
},
|
||||
{
|
||||
name: "Greek (Greece)",
|
||||
code: "el-GR",
|
||||
codeMix: "ell-GRC",
|
||||
},
|
||||
{
|
||||
name: "Hebrew (Israel)",
|
||||
code: "he-IL",
|
||||
codeMix: "heb-ISR",
|
||||
},
|
||||
{
|
||||
name: "Hindi (India)",
|
||||
code: "hi-IN",
|
||||
codeMix: "hin-IND",
|
||||
},
|
||||
{
|
||||
name: "Hungarian (Hungary)",
|
||||
code: "hu-HU",
|
||||
codeMix: "hun-HUN",
|
||||
},
|
||||
{
|
||||
name: "Indonesian (Indonesia)",
|
||||
code: "id-ID",
|
||||
codeMix: "ind-IDN",
|
||||
},
|
||||
{
|
||||
name: "Italian (Italy)",
|
||||
code: "it-IT",
|
||||
codeMix: "ita-ITA",
|
||||
},
|
||||
{
|
||||
name: "Japanese (Japan)",
|
||||
code: "ja-JP",
|
||||
codeMix: "jpn-JPN",
|
||||
},
|
||||
{
|
||||
name: "Korean (South Korea)",
|
||||
code: "ko-KR",
|
||||
codeMix: "kor-KOR",
|
||||
},
|
||||
{
|
||||
name: "Malay (Malaysia)",
|
||||
code: "ms-MY",
|
||||
codeMix: "zlm-MYS",
|
||||
},
|
||||
{
|
||||
name: "Norwegian (Norway)",
|
||||
code: "no-NO",
|
||||
codeMix: "nor-NOR",
|
||||
},
|
||||
{
|
||||
name: "Polish (Poland)",
|
||||
code: "pl-PL",
|
||||
codeMix: "pol-POL",
|
||||
},
|
||||
{
|
||||
name: "Portuguese (Brazil)",
|
||||
code: "pt-BR",
|
||||
codeMix: "por-BRA",
|
||||
},
|
||||
{
|
||||
name: "Portuguese (Portugal)",
|
||||
code: "pt-PT",
|
||||
codeMix: "por-PRT",
|
||||
},
|
||||
{
|
||||
name: "Romanian (Romania)",
|
||||
code: "ro-RO",
|
||||
codeMix: "ron-ROU",
|
||||
},
|
||||
{
|
||||
name: "Russian (Russia)",
|
||||
code: "ru-RU",
|
||||
codeMix: "rus-RUS",
|
||||
},
|
||||
{
|
||||
name: "Shanghainese (China)",
|
||||
code: "zh-WU",
|
||||
codeMix: "wuu-CHN",
|
||||
},
|
||||
{
|
||||
name: "Mandarin (China)",
|
||||
code: "zh-CN",
|
||||
codeMix: "cmn-CHN",
|
||||
},
|
||||
{
|
||||
name: "Slovak (Slovakia)",
|
||||
code: "sk-SK",
|
||||
codeMix: "slk-SVK",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Spain)",
|
||||
code: "es-ES",
|
||||
codeMix: "spa-ESP",
|
||||
},
|
||||
{
|
||||
name: "Spanish (Latin America)",
|
||||
code: "es-US",
|
||||
codeMix: "spa-XLA",
|
||||
},
|
||||
{
|
||||
name: "Swedish (Sweden)",
|
||||
code: "sv-SE",
|
||||
codeMix: "swe-SWE",
|
||||
},
|
||||
{
|
||||
name: "Thai (Thailand)",
|
||||
code: "th-TH",
|
||||
codeMix: "tha-THA",
|
||||
},
|
||||
{
|
||||
name: "Cantonese (Hong Kong)",
|
||||
code: "cn-HK",
|
||||
codeMix: "yue-CHS",
|
||||
},
|
||||
{
|
||||
name: "Mandarin (Taiwan)",
|
||||
code: "zh-TW",
|
||||
codeMix: "cmn-TWN",
|
||||
},
|
||||
{
|
||||
name: "Turkish (Turkey)",
|
||||
code: "tr-TR",
|
||||
codeMix: "tur-TUR",
|
||||
},
|
||||
{
|
||||
name: "Ukrainian (Ukraine)",
|
||||
code: "uk-UA",
|
||||
codeMix: "ukr-UKR",
|
||||
},
|
||||
{
|
||||
name: "Vietnamese (Vietnam)",
|
||||
code: "vi-VN",
|
||||
codeMix: "vie-VNM",
|
||||
},
|
||||
];
|
||||
|
||||
export default languages;
|
||||
@@ -1,62 +0,0 @@
|
||||
import type { Language } from "../types";
|
||||
|
||||
export const languages: Language[] = [
|
||||
{
|
||||
name: "Arabic",
|
||||
code: "ar-AR",
|
||||
},
|
||||
{
|
||||
name: "English",
|
||||
code: "en-US",
|
||||
},
|
||||
{
|
||||
name: "English - GB",
|
||||
code: "en-GB",
|
||||
},
|
||||
{
|
||||
name: "Spanish - US",
|
||||
code: "es-US",
|
||||
},
|
||||
{
|
||||
name: "Spanish",
|
||||
code: "es-ES",
|
||||
},
|
||||
{
|
||||
name: "German",
|
||||
code: "de-DE",
|
||||
},
|
||||
{
|
||||
name: "French",
|
||||
code: "fr-FR",
|
||||
},
|
||||
{
|
||||
name: "Hindi",
|
||||
code: "hi-IN",
|
||||
},
|
||||
{
|
||||
name: "Russian",
|
||||
code: "ru-RU",
|
||||
},
|
||||
{
|
||||
name: "Korean",
|
||||
code: "ko-KR",
|
||||
},
|
||||
{
|
||||
name: "Brazilian-Portuguese",
|
||||
code: "pt-BR",
|
||||
},
|
||||
{
|
||||
name: "Japanese",
|
||||
code: "ja-JP",
|
||||
},
|
||||
{
|
||||
name: "Italian",
|
||||
code: "it-IT",
|
||||
},
|
||||
{
|
||||
name: "Mandarin",
|
||||
code: "zh-CN",
|
||||
},
|
||||
];
|
||||
|
||||
export default languages;
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { Language } from "../types";
|
||||
|
||||
export const languages: Language[] = [
|
||||
{
|
||||
name: "English (United States)",
|
||||
code: "en-US",
|
||||
},
|
||||
];
|
||||
|
||||
export default languages;
|
||||
@@ -1,217 +0,0 @@
|
||||
import type { VoiceLanguage } from "../types";
|
||||
|
||||
export const languages: VoiceLanguage[] = [
|
||||
{
|
||||
code: "arb",
|
||||
name: "Arabic",
|
||||
voices: [{ value: "Zeina", name: "Zeina (Female)" }],
|
||||
},
|
||||
{
|
||||
code: "cmn-CN",
|
||||
name: "Chinese, Mandarin",
|
||||
voices: [{ value: "Zhiyu", name: "Zhiyu (Female)" }],
|
||||
},
|
||||
{
|
||||
code: "da-DK",
|
||||
name: "Danish",
|
||||
voices: [
|
||||
{ value: "Naja", name: "Naja (Female)" },
|
||||
{ value: "Mads", name: "Mads (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "nl-NL",
|
||||
name: "Dutch",
|
||||
voices: [
|
||||
{ value: "Lotte", name: "Lotte (Female)" },
|
||||
{ value: "Ruben", name: "Ruben (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-AU",
|
||||
name: "English (Australian)",
|
||||
voices: [
|
||||
{ value: "Nicole", name: "Nicole (Female)" },
|
||||
{ value: "Russell", name: "Russell (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-GB",
|
||||
name: "English (British)",
|
||||
voices: [
|
||||
{ value: "Amy", name: "Amy (Female)" },
|
||||
{ value: "Emma", name: "Emma (Female)" },
|
||||
{ value: "Brian", name: "Brian (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-IN",
|
||||
name: "English (Indian)",
|
||||
voices: [
|
||||
{ value: "Aditi", name: "Aditi (Female)" },
|
||||
{ value: "Raveena", name: "Raveena (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-US",
|
||||
name: "English (US)",
|
||||
voices: [
|
||||
{ value: "Joanna", name: "Joanna (Female)" },
|
||||
{ value: "Kendra", name: "Kendra (Female)" },
|
||||
{ value: "Kimberly", name: "Kimberly (Female)" },
|
||||
{ value: "Ivy", name: "Ivy (Female child)" },
|
||||
{ value: "Salli", name: "Salli (Female)" },
|
||||
{ value: "Joey", name: "Joey (Male)" },
|
||||
{ value: "Matthew", name: "Matthew (Male)" },
|
||||
{ value: "Justin", name: "Justin (Male child)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-GB-WLS",
|
||||
name: "English (Welsh)",
|
||||
voices: [{ value: "Geraint", name: "Geraint (Male)" }],
|
||||
},
|
||||
{
|
||||
code: "fr-FR",
|
||||
name: "French",
|
||||
voices: [
|
||||
{ value: "Celine", name: "Céline (Female)" },
|
||||
{ value: "Lea", name: "Léa (Female)" },
|
||||
{ value: "Mathieu", name: "Mathieu (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "fr-CA",
|
||||
name: "French (Canadian)",
|
||||
voices: [{ value: "Chantal", name: "Chantal (Female)" }],
|
||||
},
|
||||
{
|
||||
code: "de-DE",
|
||||
name: "German",
|
||||
voices: [
|
||||
{ value: "Marlene", name: "Marlene (Female)" },
|
||||
{ value: "Vicki", name: "Vicki (Female)" },
|
||||
{ value: "Hans", name: "Hans (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "hi-IN",
|
||||
name: "Hindi",
|
||||
voices: [{ value: "Aditi", name: "Aditi (Female)" }],
|
||||
},
|
||||
{
|
||||
code: "is-IS",
|
||||
name: "Icelandic",
|
||||
voices: [
|
||||
{ value: "Dora", name: "Dóra (Female)" },
|
||||
{ value: "Karl", name: "Karl (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "it-IT",
|
||||
name: "Italian",
|
||||
voices: [
|
||||
{ value: "Carla", name: "Carla (Female)" },
|
||||
{ value: "Bianca", name: "Bianca (Female)" },
|
||||
{ value: "Giorgio", name: "Giorgio (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "ja-JP",
|
||||
name: "Japanese",
|
||||
voices: [
|
||||
{ value: "Mizuki", name: "Mizuki (Female)" },
|
||||
{ value: "Takumi", name: "Takumi (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "ko-KR",
|
||||
name: "Korean",
|
||||
voices: [{ value: "Seoyeon", name: "Seoyeon (Female)" }],
|
||||
},
|
||||
{
|
||||
code: "nb-NO",
|
||||
name: "Norwegian",
|
||||
voices: [{ value: "Liv", name: "Liv (Female)" }],
|
||||
},
|
||||
{
|
||||
code: "pl-PL",
|
||||
name: "Polish",
|
||||
voices: [
|
||||
{ value: "Ewa", name: "Ewa (Female)" },
|
||||
{ value: "Maja", name: "Maja (Female)" },
|
||||
{ value: "Jacek", name: "Jacek (Male)" },
|
||||
{ value: "Jan", name: "Jan (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "pt-BR",
|
||||
name: "Portuguese (Brazilian)",
|
||||
voices: [
|
||||
{ value: "Camila", name: "Camila (Female)" },
|
||||
{ value: "Vitoria", name: "Vitória (Female)" },
|
||||
{ value: "Ricardo", name: "Ricardo (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "pt-PT",
|
||||
name: "Portuguese (European)",
|
||||
voices: [
|
||||
{ value: "Ines", name: "Inês (Female)" },
|
||||
{ value: "Cristiano", name: "Cristiano (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "ro-RO",
|
||||
name: "Romanian",
|
||||
voices: [{ value: "Carmen", name: "Carmen (Female)" }],
|
||||
},
|
||||
{
|
||||
code: "ru-RU",
|
||||
name: "Russian",
|
||||
voices: [
|
||||
{ value: "Tatyana", name: "Tatyana (Female)" },
|
||||
{ value: "Maxim", name: "Maxim (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "es-ES",
|
||||
name: "Spanish (European)",
|
||||
voices: [
|
||||
{ value: "Conchita", name: "Conchita (Female)" },
|
||||
{ value: "Lucia", name: "Lucia (Female)" },
|
||||
{ value: "Enrique", name: "Enrique (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "es-MX",
|
||||
name: "Spanish (Mexican)",
|
||||
voices: [{ value: "Mia", name: "Mia (Female)" }],
|
||||
},
|
||||
{
|
||||
code: "es-US",
|
||||
name: "Spanish (US)",
|
||||
voices: [
|
||||
{ value: "Lupe", name: "Lupe (Female)" },
|
||||
{ value: "Penelope", name: "Penélope (Female)" },
|
||||
{ value: "Miguel", name: "Miguel (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "sv-SE",
|
||||
name: "Swedish",
|
||||
voices: [{ value: "Astrid", name: "Astrid (Female)" }],
|
||||
},
|
||||
{
|
||||
code: "tr-TR",
|
||||
name: "Turkish",
|
||||
voices: [{ value: "Filiz", name: "Filiz (Female)" }],
|
||||
},
|
||||
{
|
||||
code: "cy-GB",
|
||||
name: "Welsh",
|
||||
voices: [{ value: "Gwyneth", name: "Gwyneth (Female)" }],
|
||||
},
|
||||
];
|
||||
|
||||
export default languages;
|
||||
@@ -1,489 +0,0 @@
|
||||
import type { VoiceLanguage } from "../types";
|
||||
|
||||
export const languages: VoiceLanguage[] = [
|
||||
{
|
||||
code: "ar-XA",
|
||||
name: "Arabic",
|
||||
voices: [
|
||||
{ value: "ar-XA-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "ar-XA-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "ar-XA-Standard-C", name: "Standard-C (Male)" },
|
||||
{ value: "ar-XA-Standard-D", name: "Standard-D (Female)" },
|
||||
{ value: "ar-XA-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "ar-XA-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "ar-XA-Wavenet-C", name: "Wavenet-C (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "cs-CZ",
|
||||
name: "Czech (Czech Republic)",
|
||||
voices: [
|
||||
{ value: "cs-CZ-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "cs-CZ-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "da-DK",
|
||||
name: "Danish (Denmark)",
|
||||
voices: [
|
||||
{ value: "da-DK-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "da-DK-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "da-DK-Neural2-D", name: "Neural2-D (Female)" },
|
||||
{ value: "da-DK-Neural2-F", name: "Neural2-F (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "nl-NL",
|
||||
name: "Dutch (Netherlands)",
|
||||
voices: [
|
||||
{ value: "nl-NL-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "nl-NL-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "nl-NL-Standard-C", name: "Standard-C (Male)" },
|
||||
{ value: "nl-NL-Standard-D", name: "Standard-D (Female)" },
|
||||
{ value: "nl-NL-Standard-E", name: "Standard-E (Female)" },
|
||||
{ value: "nl-NL-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "nl-NL-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "nl-NL-Wavenet-C", name: "Wavenet-C (Male)" },
|
||||
{ value: "nl-NL-Wavenet-D", name: "Wavenet-D (Female)" },
|
||||
{ value: "nl-NL-Wavenet-E", name: "Wavenet-E (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-AU",
|
||||
name: "English (Australia)",
|
||||
voices: [
|
||||
{ value: "en-AU-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "en-AU-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "en-AU-Standard-C", name: "Standard-C (Female)" },
|
||||
{ value: "en-AU-Standard-D", name: "Standard-D (Male)" },
|
||||
{ value: "en-AU-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "en-AU-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "en-AU-Wavenet-C", name: "Wavenet-C (Female)" },
|
||||
{ value: "en-AU-Wavenet-D", name: "Wavenet-D (Male)" },
|
||||
{ value: "en-AU-Neural2-A", name: "Neural2-A (Female)" },
|
||||
{ value: "en-AU-Neural2-B", name: "Neural2-B (Male)" },
|
||||
{ value: "en-AU-Neural2-C", name: "Neural2-C (Female)" },
|
||||
{ value: "en-AU-Neural2-D", name: "Neural2-D (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-IN",
|
||||
name: "English (India)",
|
||||
voices: [
|
||||
{ value: "en-IN-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "en-IN-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "en-IN-Standard-C", name: "Standard-C (Male)" },
|
||||
{ value: "en-IN-Standard-D", name: "Standard-D (Female)" },
|
||||
{ value: "en-IN-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "en-IN-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "en-IN-Wavenet-C", name: "Wavenet-C (Male)" },
|
||||
{ value: "en-IN-Wavenet-D", name: "Wavenet-D (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-GB",
|
||||
name: "English (UK)",
|
||||
voices: [
|
||||
{ value: "en-GB-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "en-GB-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "en-GB-Standard-C", name: "Standard-C (Female)" },
|
||||
{ value: "en-GB-Standard-D", name: "Standard-D (Male)" },
|
||||
{ value: "en-GB-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "en-GB-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "en-GB-Wavenet-C", name: "Wavenet-C (Female)" },
|
||||
{ value: "en-GB-Wavenet-D", name: "Wavenet-D (Male)" },
|
||||
{ value: "en-GB-Neural2-A", name: "Neural2-A (Female)" },
|
||||
{ value: "en-GB-Neural2-B", name: "Neural2-B (Male)" },
|
||||
{ value: "en-GB-Neural2-C", name: "Neural2-C (Female)" },
|
||||
{ value: "en-GB-Neural2-D", name: "Neural2-D (Male)" },
|
||||
{ value: "en-GB-Neural2-F", name: "Neural2-F (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-US",
|
||||
name: "English (US)",
|
||||
voices: [
|
||||
{ value: "en-US-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "en-US-Standard-C", name: "Standard-C (Female)" },
|
||||
{ value: "en-US-Standard-D", name: "Standard-D (Male)" },
|
||||
{ value: "en-US-Standard-E", name: "Standard-E (Female)" },
|
||||
{ value: "en-US-Wavenet-A", name: "Wavenet-A (Male)" },
|
||||
{ value: "en-US-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "en-US-Wavenet-C", name: "Wavenet-C (Female)" },
|
||||
{ value: "en-US-Wavenet-D", name: "Wavenet-D (Male)" },
|
||||
{ value: "en-US-Wavenet-E", name: "Wavenet-E (Female)" },
|
||||
{ value: "en-US-Wavenet-F", name: "Wavenet-F (Female)" },
|
||||
{ value: "en-US-Neural2-A", name: "Neural2-A (Male)" },
|
||||
{ value: "en-US-Neural2-C", name: "Neural2-C (Female)" },
|
||||
{ value: "en-US-Neural2-D", name: "Neural2-D (Male)" },
|
||||
{ value: "en-US-Neural2-E", name: "Neural2-E (Female)" },
|
||||
{ value: "en-US-Neural2-F", name: "Neural2-F (Female)" },
|
||||
{ value: "en-US-Neural2-G", name: "Neural2-G (Female)" },
|
||||
{ value: "en-US-Neural2-H", name: "Neural2-H (Female)" },
|
||||
{ value: "en-US-Neural2-I", name: "Neural2-I (Male)" },
|
||||
{ value: "en-US-Neural2-J", name: "Neural2-J (Male)" },
|
||||
{ value: "en-US-Studio-M", name: "Studio-M (Male)" },
|
||||
{ value: "en-US-Studio-O", name: "Studio-M (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "fil-PH",
|
||||
name: "Filipino (Philippines)",
|
||||
voices: [
|
||||
{ value: "fil-PH-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "fil-PH-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "fil-ph-Neural2-A", name: "Neural2-A (Female)" },
|
||||
{ value: "fil-ph-Neural2-D", name: "Neural2-A (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "fi-FI",
|
||||
name: "Finnish (Finland)",
|
||||
voices: [
|
||||
{ value: "fi-FI-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "fi-FI-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "fr-CA",
|
||||
name: "French (Canada)",
|
||||
voices: [
|
||||
{ value: "fr-CA-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "fr-CA-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "fr-CA-Standard-C", name: "Standard-C (Female)" },
|
||||
{ value: "fr-CA-Standard-D", name: "Standard-D (Male)" },
|
||||
{ value: "fr-CA-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "fr-CA-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "fr-CA-Wavenet-C", name: "Wavenet-C (Female)" },
|
||||
{ value: "fr-CA-Wavenet-D", name: "Wavenet-D (Male)" },
|
||||
{ value: "fr-CA-Neural2-A", name: "Neural2-A (Female)" },
|
||||
{ value: "fr-CA-Neural2-B", name: "Neural2-B (Male)" },
|
||||
{ value: "fr-CA-Neural2-C", name: "Neural2-C (Female)" },
|
||||
{ value: "fr-CA-Neural2-D", name: "Neural2-D (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "fr-FR",
|
||||
name: "French (France)",
|
||||
voices: [
|
||||
{ value: "fr-FR-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "fr-FR-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "fr-FR-Standard-C", name: "Standard-C (Female)" },
|
||||
{ value: "fr-FR-Standard-D", name: "Standard-D (Male)" },
|
||||
{ value: "fr-FR-Standard-E", name: "Standard-E (Female)" },
|
||||
{ value: "fr-FR-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "fr-FR-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "fr-FR-Wavenet-C", name: "Wavenet-C (Female)" },
|
||||
{ value: "fr-FR-Wavenet-D", name: "Wavenet-D (Male)" },
|
||||
{ value: "fr-FR-Wavenet-E", name: "Wavenet-E (Female)" },
|
||||
{ value: "fr-FR-Neural2-A", name: "Neural2-A (Female)" },
|
||||
{ value: "fr-FR-Neural2-B", name: "Neural2-B (Male)" },
|
||||
{ value: "fr-FR-Neural2-C", name: "Neural2-C (Female)" },
|
||||
{ value: "fr-FR-Neural2-D", name: "Neural2-D (Male)" },
|
||||
{ value: "fr-FR-Neural2-E", name: "Neural2-E (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "de-DE",
|
||||
name: "German (Germany)",
|
||||
voices: [
|
||||
{ value: "de-DE-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "de-DE-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "de-DE-Standard-E", name: "Standard-E (Male)" },
|
||||
{ value: "de-DE-Standard-F", name: "Standard-F (Female)" },
|
||||
{ value: "de-DE-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "de-DE-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "de-DE-Wavenet-C", name: "Wavenet-C (Female)" },
|
||||
{ value: "de-DE-Wavenet-D", name: "Wavenet-D (Male)" },
|
||||
{ value: "de-DE-Wavenet-E", name: "Wavenet-E (Male)" },
|
||||
{ value: "de-DE-Wavenet-F", name: "Wavenet-F (Female)" },
|
||||
{ value: "de-DE-Neural2-B", name: "Neural2-B (Male)" },
|
||||
{ value: "de-DE-Neural2-C", name: "Neural2-C (Female)" },
|
||||
{ value: "de-DE-Neural2-D", name: "Neural2-D (Male)" },
|
||||
{ value: "de-DE-Neural2-F", name: "Neural2-F (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "el-GR",
|
||||
name: "Greek (Greece)",
|
||||
voices: [
|
||||
{ value: "el-GR-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "el-GR-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "hi-IN",
|
||||
name: "Hindi (India)",
|
||||
voices: [
|
||||
{ value: "hi-IN-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "hi-IN-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "hi-IN-Standard-C", name: "Standard-C (Male)" },
|
||||
{ value: "hi-IN-Standard-D", name: "Standard-D (Female)" },
|
||||
{ value: "hi-IN-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "hi-IN-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "hi-IN-Wavenet-C", name: "Wavenet-C (Male)" },
|
||||
{ value: "hi-IN-Wavenet-D", name: "Wavenet-D (Female)" },
|
||||
{ value: "hi-IN-Neural2-A", name: "Neural2-A (Female)" },
|
||||
{ value: "hi-IN-Neural2-B", name: "Neural2-B (Male)" },
|
||||
{ value: "hi-IN-Neural2-C", name: "Neural2-C (Male)" },
|
||||
{ value: "hi-IN-Neural2-D", name: "Neural2-D (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "hu-HU",
|
||||
name: "Hungarian (Hungary)",
|
||||
voices: [
|
||||
{ value: "hu-HU-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "hu-HU-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "id-ID",
|
||||
name: "Indonesian (Indonesia)",
|
||||
voices: [
|
||||
{ value: "id-ID-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "id-ID-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "id-ID-Standard-C", name: "Standard-C (Male)" },
|
||||
{ value: "id-ID-Standard-D", name: "Standard-D (Female)" },
|
||||
{ value: "id-ID-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "id-ID-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "id-ID-Wavenet-C", name: "Wavenet-C (Male)" },
|
||||
{ value: "id-ID-Wavenet-D", name: "Wavenet-D (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "it-IT",
|
||||
name: "Italian (Italy)",
|
||||
voices: [
|
||||
{ value: "it-IT-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "it-IT-Standard-B", name: "Standard-B (Female)" },
|
||||
{ value: "it-IT-Standard-C", name: "Standard-C (Male)" },
|
||||
{ value: "it-IT-Standard-D", name: "Standard-D (Male)" },
|
||||
{ value: "it-IT-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "it-IT-Wavenet-B", name: "Wavenet-B (Female)" },
|
||||
{ value: "it-IT-Wavenet-C", name: "Wavenet-C (Male)" },
|
||||
{ value: "it-IT-Wavenet-D", name: "Wavenet-D (Male)" },
|
||||
{ value: "it-IT-Neural2-A", name: "Neural2-A (Female)" },
|
||||
{ value: "it-IT-Neural2-C", name: "Neural2-C (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "ja-JP",
|
||||
name: "Japanese (Japan)",
|
||||
voices: [
|
||||
{ value: "ja-JP-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "ja-JP-Standard-B", name: "Standard-B (Female)" },
|
||||
{ value: "ja-JP-Standard-C", name: "Standard-C (Male)" },
|
||||
{ value: "ja-JP-Standard-D", name: "Standard-D (Male)" },
|
||||
{ value: "ja-JP-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "ja-JP-Wavenet-B", name: "Wavenet-B (Female)" },
|
||||
{ value: "ja-JP-Wavenet-C", name: "Wavenet-C (Male)" },
|
||||
{ value: "ja-JP-Wavenet-D", name: "Wavenet-D (Male)" },
|
||||
{ value: "ja-JP-Neural2-B", name: "Neural2-B (Female)" },
|
||||
{ value: "ja-JP-Neural2-C", name: "Neural2-C (Male)" },
|
||||
{ value: "ja-JP-Neural2-D", name: "Neural2-D (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "ko-KR",
|
||||
name: "Korean (South Korea)",
|
||||
voices: [
|
||||
{ value: "ko-KR-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "ko-KR-Standard-B", name: "Standard-B (Female)" },
|
||||
{ value: "ko-KR-Standard-C", name: "Standard-C (Male)" },
|
||||
{ value: "ko-KR-Standard-D", name: "Standard-D (Male)" },
|
||||
{ value: "ko-KR-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "ko-KR-Wavenet-B", name: "Wavenet-B (Female)" },
|
||||
{ value: "ko-KR-Wavenet-C", name: "Wavenet-C (Male)" },
|
||||
{ value: "ko-KR-Wavenet-D", name: "Wavenet-D (Male)" },
|
||||
{ value: "ko-KR-Neural2-A", name: "Neural2-A (Female)" },
|
||||
{ value: "ko-KR-Neural2-B", name: "Neural2-B (Female)" },
|
||||
{ value: "ko-KR-Neural2-C", name: "Neural2-C (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "cmn-CN",
|
||||
name: "Mandarin Chinese",
|
||||
voices: [
|
||||
{ value: "cmn-CN-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "cmn-CN-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "cmn-CN-Standard-C", name: "Standard-C (Male)" },
|
||||
{ value: "cmn-CN-Standard-D", name: "Standard-D (Female)" },
|
||||
{ value: "cmn-CN-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "cmn-CN-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "cmn-CN-Wavenet-C", name: "Wavenet-C (Male)" },
|
||||
{ value: "cmn-CN-Wavenet-D", name: "Wavenet-D (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "cmn-TW",
|
||||
name: "Mandarin Chinese (Traditional)",
|
||||
voices: [
|
||||
{ value: "cmn-TW-Standard-A-Alpha", name: "Standard-A-Alpha (Female)" },
|
||||
{ value: "cmn-TW-Standard-B-Alpha", name: "Standard-B-Alpha (Male)" },
|
||||
{ value: "cmn-TW-Standard-C-Alpha", name: "Standard-C-Alpha (Male)" },
|
||||
{ value: "cmn-TW-Wavenet-A-Alpha", name: "Wavenet-A-Alpha (Female)" },
|
||||
{ value: "cmn-TW-Wavenet-B-Alpha", name: "Wavenet-B-Alpha (Male)" },
|
||||
{ value: "cmn-TW-Wavenet-C-Alpha", name: "Wavenet-C-Alpha (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "nb-NO",
|
||||
name: "Norwegian (Norway)",
|
||||
voices: [
|
||||
{ value: "nb-NO-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "nb-NO-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "nb-NO-Standard-C", name: "Standard-C (Female)" },
|
||||
{ value: "nb-NO-Standard-D", name: "Standard-D (Male)" },
|
||||
{ value: "nb-no-Standard-E", name: "Standard-E (Female)" },
|
||||
{ value: "nb-NO-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "nb-NO-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "nb-NO-Wavenet-C", name: "Wavenet-C (Female)" },
|
||||
{ value: "nb-NO-Wavenet-D", name: "Wavenet-D (Male)" },
|
||||
{ value: "nb-no-Wavenet-E", name: "Wavenet-E (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "pl-PL",
|
||||
name: "Polish (Poland)",
|
||||
voices: [
|
||||
{ value: "pl-PL-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "pl-PL-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "pl-PL-Standard-C", name: "Standard-C (Male)" },
|
||||
{ value: "pl-PL-Standard-D", name: "Standard-D (Female)" },
|
||||
{ value: "pl-PL-Standard-E", name: "Standard-E (Female)" },
|
||||
{ value: "pl-PL-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "pl-PL-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "pl-PL-Wavenet-C", name: "Wavenet-C (Male)" },
|
||||
{ value: "pl-PL-Wavenet-D", name: "Wavenet-D (Female)" },
|
||||
{ value: "pl-PL-Wavenet-E", name: "Wavenet-E (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "pt-BR",
|
||||
name: "Portuguese (Brazil)",
|
||||
voices: [
|
||||
{ value: "pt-BR-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "pt-BR-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "pt-BR-Neural2-A", name: "Neural2-A (Female)" },
|
||||
{ value: "pt-BR-Neural2-B", name: "Neural2-B (Male)" },
|
||||
{ value: "pt-BR-Neural2-C", name: "Neural2-C (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "pt-PT",
|
||||
name: "Portuguese (Portugal)",
|
||||
voices: [
|
||||
{ value: "pt-PT-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "pt-PT-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "pt-PT-Standard-C", name: "Standard-C (Male)" },
|
||||
{ value: "pt-PT-Standard-D", name: "Standard-D (Female)" },
|
||||
{ value: "pt-PT-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "pt-PT-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "pt-PT-Wavenet-C", name: "Wavenet-C (Male)" },
|
||||
{ value: "pt-PT-Wavenet-D", name: "Wavenet-D (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "ru-RU",
|
||||
name: "Russian (Russia)",
|
||||
voices: [
|
||||
{ value: "ru-RU-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "ru-RU-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "ru-RU-Standard-C", name: "Standard-C (Female)" },
|
||||
{ value: "ru-RU-Standard-D", name: "Standard-D (Male)" },
|
||||
{ value: "ru-RU-Standard-E", name: "Standard-E (Female)" },
|
||||
{ value: "ru-RU-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "ru-RU-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "ru-RU-Wavenet-C", name: "Wavenet-C (Female)" },
|
||||
{ value: "ru-RU-Wavenet-D", name: "Wavenet-D (Male)" },
|
||||
{ value: "ru-RU-Wavenet-E", name: "Wavenet-E (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "sk-SK",
|
||||
name: "Slovak (Slovakia)",
|
||||
voices: [
|
||||
{ value: "sk-SK-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "sk-SK-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "es-ES",
|
||||
name: "Spanish (Spain)",
|
||||
voices: [
|
||||
{ value: "es-ES-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "es-ES-Neural2-A", name: "Neural2-A (Female)" },
|
||||
{ value: "es-ES-Neural2-B", name: "Neural2-B (Male)" },
|
||||
{ value: "es-ES-Neural2-C", name: "Neural2-C (Female)" },
|
||||
{ value: "es-ES-Neural2-D", name: "Neural2-D (Female)" },
|
||||
{ value: "es-ES-Neural2-E", name: "Neural2-E (Female)" },
|
||||
{ value: "es-ES-Neural2-F", name: "Neural2-F (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "es-US",
|
||||
name: "Spanish (US)",
|
||||
voices: [
|
||||
{ value: "es-US-Neural2-A", name: "Neural2-A (Female)" },
|
||||
{ value: "es-US-Neural2-B", name: "Neural2-B (Male)" },
|
||||
{ value: "es-US-Neural2-C", name: "Neural2-C (Male)" },
|
||||
{ value: "es-US-Studio-B", name: "Studio-B (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "sv-SE",
|
||||
name: "Swedish (Sweden)",
|
||||
voices: [
|
||||
{ value: "sv-SE-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "sv-SE-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "tr-TR",
|
||||
name: "Turkish (Turkey)",
|
||||
voices: [
|
||||
{ value: "tr-TR-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "tr-TR-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "tr-TR-Standard-C", name: "Standard-C (Female)" },
|
||||
{ value: "tr-TR-Standard-D", name: "Standard-D (Female)" },
|
||||
{ value: "tr-TR-Standard-E", name: "Standard-E (Male)" },
|
||||
{ value: "tr-TR-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "tr-TR-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "tr-TR-Wavenet-C", name: "Wavenet-C (Female)" },
|
||||
{ value: "tr-TR-Wavenet-D", name: "Wavenet-D (Female)" },
|
||||
{ value: "tr-TR-Wavenet-E", name: "Wavenet-E (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "uk-UA",
|
||||
name: "Ukrainian (Ukraine)",
|
||||
voices: [
|
||||
{ value: "uk-UA-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "uk-UA-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "th-TH",
|
||||
name: "Thai (Thailand)",
|
||||
voices: [{ value: "th-TH-Neural2-C", name: "Neural2-C (Female)" }],
|
||||
},
|
||||
{
|
||||
code: "vi-VN",
|
||||
name: "Vietnamese (Vietnam)",
|
||||
voices: [
|
||||
{ value: "vi-VN-Standard-A", name: "Standard-A (Female)" },
|
||||
{ value: "vi-VN-Standard-B", name: "Standard-B (Male)" },
|
||||
{ value: "vi-VN-Standard-C", name: "Standard-C (Female)" },
|
||||
{ value: "vi-VN-Standard-D", name: "Standard-D (Male)" },
|
||||
{ value: "vi-VN-Wavenet-A", name: "Wavenet-A (Female)" },
|
||||
{ value: "vi-VN-Wavenet-B", name: "Wavenet-B (Male)" },
|
||||
{ value: "vi-VN-Wavenet-C", name: "Wavenet-C (Female)" },
|
||||
{ value: "vi-VN-Wavenet-D", name: "Wavenet-D (Male)" },
|
||||
{ value: "vi-VN-Neural2-A", name: "Neural2-A (Female)" },
|
||||
{ value: "vi-VN-Neural2-D", name: "Neural2-D (Male)" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default languages;
|
||||
@@ -1,171 +0,0 @@
|
||||
import type { VoiceLanguage } from "../types";
|
||||
|
||||
export const languages: VoiceLanguage[] = [
|
||||
{
|
||||
code: "de-DE",
|
||||
name: "German (Germany)",
|
||||
voices: [
|
||||
{ value: "de-DE_DieterVoice", name: "Dieter (Male): Standard German" },
|
||||
{
|
||||
value: "de-DE_DieterV2Voice",
|
||||
name: "Dieter 2 (Male): Standard German",
|
||||
},
|
||||
{
|
||||
value: "de-DE_DieterV3Voice",
|
||||
name: "Dieter 3 (Male): Standard German",
|
||||
},
|
||||
{ value: "de-DE_ErikaV3Voice", name: "Erika (Female): Standard German" },
|
||||
{ value: "de-DE_BirgitVoice", name: "Brigit (Female): Standard German" },
|
||||
{
|
||||
value: "de-DE_BirgitV2Voice",
|
||||
name: "Brigit 2 (Female): Standard German",
|
||||
},
|
||||
{
|
||||
value: "de-DE_BirgitV3Voice",
|
||||
name: "Brigit 3 (Female): Standard German",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-US",
|
||||
name: "English (US)",
|
||||
voices: [
|
||||
{
|
||||
value: "en-US_MichaelExpressive",
|
||||
name: "Michael (Male): American English - Expressive",
|
||||
},
|
||||
{ value: "en-US_MichaelVoice", name: "Michael (Male): American English" },
|
||||
{
|
||||
value: "en-US_MichaelV2Voice",
|
||||
name: "Michael 2 (Male): American English",
|
||||
},
|
||||
{
|
||||
value: "en-US_MichaelV3Voice",
|
||||
name: "Michael 3 (Male): American English",
|
||||
},
|
||||
{ value: "en-US_HenryV3Voice", name: "Henry (Male): American English" },
|
||||
{ value: "en-US_EmilyV3Voice", name: "Emily (Female): American English" },
|
||||
{
|
||||
value: "en-US_OliviaV3Voice",
|
||||
name: "Olivia (Female): American English",
|
||||
},
|
||||
{
|
||||
value: "en-US_AllisonExpressive",
|
||||
name: "Allison (Female): American English - Expressive",
|
||||
},
|
||||
{
|
||||
value: "en-US_AllisonVoice",
|
||||
name: "Allison (Female): American English",
|
||||
},
|
||||
{
|
||||
value: "en-US_AllisonV2Voice",
|
||||
name: "Allison 2 (Female): American English",
|
||||
},
|
||||
{
|
||||
value: "en-US_AllisonV3Voice",
|
||||
name: "Allison 3 (Female): American English",
|
||||
},
|
||||
{
|
||||
value: "en-US_LisaExpressive",
|
||||
name: "Lisa (Female): American English - Expressive",
|
||||
},
|
||||
{ value: "en-US_LisaVoice", name: "Lisa (Female): American English" },
|
||||
{ value: "en-US_LisaV2Voice", name: "Lisa 2 (Female): American English" },
|
||||
{ value: "en-US_LisaV3Voice", name: "Lisa 3 (Female): American English" },
|
||||
{ value: "en-US_KevinV3Voice", name: "Kevin (Male): American English" },
|
||||
{
|
||||
value: "en-US_EmmaExpressive",
|
||||
name: "Emma (Female): American English - Expressive",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-GB",
|
||||
name: "English (GB)",
|
||||
voices: [
|
||||
{ value: "en-GB_JamesV3Voice", name: "James (Male)" },
|
||||
{ value: "en-GB_KateVoice", name: "Kate (Female)" },
|
||||
{ value: "en-GB_KateV3Voice", name: "Kate 2 (Female)" },
|
||||
{ value: "en-GB_CharlotteV3Voice", name: "Charlotte (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "es-US",
|
||||
name: "Spanish (North America)",
|
||||
voices: [
|
||||
{
|
||||
value: "es-US_SofiaVoice",
|
||||
name: "Sofia (Female): North American Spanish",
|
||||
},
|
||||
{
|
||||
value: "es-US_SofiaV3Voice",
|
||||
name: "Sofia 2 (Female): North American Spanish",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "es-LA",
|
||||
name: "Spanish (Latin America)",
|
||||
voices: [
|
||||
{
|
||||
value: "es-LA_SofiaVoice",
|
||||
name: "Sofia (Female): Latin American Spanish",
|
||||
},
|
||||
{
|
||||
value: "es-LA_SofiaV3Voice",
|
||||
name: "Sofia 2 (Female): Latin American Spanish",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "es-ES",
|
||||
name: "Spanish (Castilian)",
|
||||
voices: [
|
||||
{ value: "es-ES_LauraVoice", name: "Laura (Female)" },
|
||||
{ value: "es-ES_LauraV3Voice", name: "Laura 2 (Female)" },
|
||||
{ value: "es-ES_EnriqueVoice", name: "Enrique (Male)" },
|
||||
{ value: "es-ES_EnriqueV3Voice", name: "Enrique 2 (Male)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "fr-FR",
|
||||
name: "French (FR)",
|
||||
voices: [
|
||||
{ value: "fr-FR_NicolasV3Voice", name: "Nicolas (Male)" },
|
||||
{ value: "fr-FR_ReneeVoice", name: "Renee (Female)" },
|
||||
{ value: "fr-FR_ReneeV3Voice", name: "Renee 2 (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "fr-CA",
|
||||
name: "French (CA)",
|
||||
voices: [{ value: "fr-CA_LouiseV3Voice", name: "Louise (Female)" }],
|
||||
},
|
||||
{
|
||||
code: "it-IT",
|
||||
name: "Italian",
|
||||
voices: [
|
||||
{ value: "it-IT_FrancescaVoice", name: "Francesca (Female)" },
|
||||
{ value: "it-IT_FrancescaV2Voice", name: "Francesca 2 (Female)" },
|
||||
{ value: "it-IT_FrancescaV3Voice", name: "Francesca 3 (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "pt-BR",
|
||||
name: "Portuguese (Brazil)",
|
||||
voices: [
|
||||
{ value: "pt-BR_IsabelaVoice", name: "Isabela (Female)" },
|
||||
{ value: "pt-BR_IsabelaV3Voice", name: "Isabela 2 (Female)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "ja-JP",
|
||||
name: "Japanese",
|
||||
voices: [
|
||||
{ value: "ja-JP_EmiVoice", name: "Emi (Female)" },
|
||||
{ value: "ja-JP_EmiV3Voice", name: "Emi 2 (Female)" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default languages;
|
||||
@@ -1,30 +0,0 @@
|
||||
import { rawData } from "./ms-speech-synthesis-raw";
|
||||
|
||||
import type { VoiceLanguage } from "../types";
|
||||
|
||||
export const languages: VoiceLanguage[] = [];
|
||||
|
||||
rawData.forEach((data) => {
|
||||
const lang = languages.find((l) => {
|
||||
return l.code === data.Locale;
|
||||
});
|
||||
|
||||
if (!lang) {
|
||||
languages.push({
|
||||
code: data.Locale,
|
||||
name: data.LocaleName,
|
||||
voices: rawData
|
||||
.filter((d) => {
|
||||
return d.Locale === data.Locale;
|
||||
})
|
||||
.map((d) => {
|
||||
return {
|
||||
value: d.ShortName,
|
||||
name: `${d.DisplayName} (${d.Gender})`,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default languages;
|
||||
4404
src/vendor/speech-synthesis/ms-speech-synthesis-raw.ts
vendored
4404
src/vendor/speech-synthesis/ms-speech-synthesis-raw.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,962 +0,0 @@
|
||||
import type { VoiceLanguage } from "../types";
|
||||
|
||||
export const languages: VoiceLanguage[] = [
|
||||
{
|
||||
code: "ar-WW",
|
||||
name: "Arabic (Worldwide)",
|
||||
voices: [
|
||||
{
|
||||
value: "Laila - standard",
|
||||
name: "Laila (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Tarik - standard",
|
||||
name: "Tarik (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Miriam - standard",
|
||||
name: "Miriam (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "eu-ES",
|
||||
name: "Basque (Spain)",
|
||||
voices: [{ value: "Miren", name: "Miren (standard)", model: "standard" }],
|
||||
},
|
||||
{
|
||||
code: "bn-IN",
|
||||
name: "Bengali (India)",
|
||||
voices: [
|
||||
{ value: "Paya - standard", name: "Paya (standard)", model: "standard" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "bho-IN",
|
||||
name: "Bhojpuri (India)",
|
||||
voices: [
|
||||
{ value: "Jaya - standard", name: "Jaya (standard)", model: "standard" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "bg-BG",
|
||||
name: "Bulgarian (Bulgaria)",
|
||||
voices: [
|
||||
{
|
||||
value: "Daria - standard",
|
||||
name: "Daria (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "yue-HK",
|
||||
name: "Cantonese (Hong Kong)",
|
||||
voices: [
|
||||
{
|
||||
value: "Sinji-Ml - standard",
|
||||
name: "Sinji-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "ca-ES",
|
||||
name: "Catalan (Spain)",
|
||||
voices: [
|
||||
{
|
||||
value: "Jordi - standard",
|
||||
name: "Jordi (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Montserrat - standard",
|
||||
name: "Montserrat (standard)",
|
||||
model: "enhanced",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "yue-HK",
|
||||
name: "Croatian (Croatia)",
|
||||
voices: [
|
||||
{ value: "Lana - standard", name: "Lana (standard)", model: "standard" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "cs-CZ",
|
||||
name: "Czech (Czech Republic)",
|
||||
voices: [
|
||||
{
|
||||
value: "Iveta - standard",
|
||||
name: "Iveta (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Zuzana - standard",
|
||||
name: "Zuzana (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Zuzana-ml - enhanced",
|
||||
name: "Zuzana (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "da-DK",
|
||||
name: "Danish (Denmark)",
|
||||
voices: [
|
||||
{
|
||||
value: "Magnus - standard",
|
||||
name: "Magnus (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{ value: "Sara - standard", name: "Sara (standard)", model: "standard" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "nl-BE",
|
||||
name: "Dutch (Belgium)",
|
||||
voices: [
|
||||
{
|
||||
value: "Ellen - standard",
|
||||
name: "Ellen (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "nl-NL",
|
||||
name: "Dutch (Belgium)",
|
||||
voices: [
|
||||
{
|
||||
value: "Claire-Ml - standard",
|
||||
name: "Claire-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Xander - standard",
|
||||
name: "Xander (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-AU",
|
||||
name: "English (Australia)",
|
||||
voices: [
|
||||
{
|
||||
value: "Karen - standard",
|
||||
name: "Karen (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{ value: "Lee - standard", name: "Lee (standard)", model: "standard" },
|
||||
{
|
||||
value: "Matilda - enhanced",
|
||||
name: "Matilda (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-IN",
|
||||
name: "English (India)",
|
||||
voices: [
|
||||
{
|
||||
value: "Isha-Ml - enhanced",
|
||||
name: "Isha-Ml (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
{
|
||||
value: "Rishi - standard",
|
||||
name: "Rishi (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Rishi-Ml - standard",
|
||||
name: "Rishi-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Sangeeta - standard",
|
||||
name: "Sangeeta (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Veena - standard",
|
||||
name: "Veena (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-IE",
|
||||
name: "English (Ireland)",
|
||||
voices: [
|
||||
{
|
||||
value: "Moira - standard",
|
||||
name: "Moira (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-SC",
|
||||
name: "English (Scotland)",
|
||||
voices: [
|
||||
{
|
||||
value: "Fiona - standard",
|
||||
name: "Fiona (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-ZA",
|
||||
name: "English (South Africa)",
|
||||
voices: [
|
||||
{
|
||||
value: "Tessa - standard",
|
||||
name: "Tessa (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-GB",
|
||||
name: "English (United Kingdom)",
|
||||
voices: [
|
||||
{
|
||||
value: "Daniel - standard",
|
||||
name: "Daniel (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{ value: "Kate - standard", name: "Kate (standard)", model: "standard" },
|
||||
{
|
||||
value: "Malcolm - standard",
|
||||
name: "Malcolm (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Oliver - standard",
|
||||
name: "Oliver (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Serena - enhanced",
|
||||
name: "Serena (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
{
|
||||
value: "Simon - standard",
|
||||
name: "Simon (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Stephanie - standard",
|
||||
name: "Stephanie (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "en-US",
|
||||
name: "English (United States)",
|
||||
voices: [
|
||||
{
|
||||
value: "Allison - standard",
|
||||
name: "Allison (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Ava-Ml - enhanced",
|
||||
name: "Ava-Ml (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
{
|
||||
value: "Chloe - standard",
|
||||
name: "Chloe (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{ value: "Evan - enhanced", name: "Evan (enhanced)", model: "enhanced" },
|
||||
{
|
||||
value: "Nathan - enhanced",
|
||||
name: "Nathan (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
{
|
||||
value: "Evelyn - standard",
|
||||
name: "Evelyn (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Nolan - standard",
|
||||
name: "Nolan (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Samantha - standard",
|
||||
name: "Samantha (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Susan - standard",
|
||||
name: "Susan (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{ value: "Tom - standard", name: "Tom (standard)", model: "standard" },
|
||||
{
|
||||
value: "Zoe-Ml - enhanced",
|
||||
name: "Zoe-Ml (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "fi-FI",
|
||||
name: "Finnish (Finland)",
|
||||
voices: [
|
||||
{ value: "Onni - standard", name: "Onni (standard)", model: "standard" },
|
||||
{ value: "Satu - standard", name: "Satu (standard)", model: "standard" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "fr-BE",
|
||||
name: "French (Belgium)",
|
||||
voices: [
|
||||
{ value: "Aude - standard", name: "Aude (standard)", model: "standard" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "fr-CA",
|
||||
name: "French (Canada)",
|
||||
voices: [
|
||||
{
|
||||
value: "Amelie-Ml - enhanced",
|
||||
name: "Amelie-Ml (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
{
|
||||
value: "Chantal - standard",
|
||||
name: "Chantal (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Nicolas - standard",
|
||||
name: "Nicolas (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "fr-FR",
|
||||
name: "French (France)",
|
||||
voices: [
|
||||
{
|
||||
value: "Audrey-Ml - enhanced",
|
||||
name: "Audrey-Ml (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
{
|
||||
value: "Aurelie - standard",
|
||||
name: "Aurelie (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Thomas - standard",
|
||||
name: "Thomas (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "gl-ES",
|
||||
name: "Galician (Spain)",
|
||||
voices: [
|
||||
{
|
||||
value: "Carmela - standard",
|
||||
name: "Carmela (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "de-DE",
|
||||
name: "German (Germany)",
|
||||
voices: [
|
||||
{
|
||||
value: "Anna-Ml - enhanced",
|
||||
name: "Anna-Ml (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
{
|
||||
value: "Markus - standard",
|
||||
name: "Markus (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Petra-Ml - enhanced",
|
||||
name: "Petra-Ml (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
{
|
||||
value: "Viktor - standard",
|
||||
name: "Viktor (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Yannick - standard",
|
||||
name: "Yannick (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "el-GR",
|
||||
name: "Greek (Greece)",
|
||||
voices: [
|
||||
{
|
||||
value: "Melina - standard",
|
||||
name: "Melina (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Nikos - standard",
|
||||
name: "Nikos (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "he-IL",
|
||||
name: "Hebrew (Israel)",
|
||||
voices: [
|
||||
{
|
||||
value: "Carmit - standard",
|
||||
name: "Carmit (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "hi-IN",
|
||||
name: "Hindi (India)",
|
||||
voices: [
|
||||
{
|
||||
value: "Kiyara-Ml - enhanced",
|
||||
name: "Kiyara-Ml (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
{
|
||||
value: "Lekha - standard",
|
||||
name: "Lekha (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{ value: "neel - standard", name: "Neel (standard)", model: "standard" },
|
||||
{
|
||||
value: "Neel-Ml - standard",
|
||||
name: "Neel-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "hu-HU",
|
||||
name: "Hungarian (Hungary)",
|
||||
voices: [
|
||||
{
|
||||
value: "Mariska - standard",
|
||||
name: "Mariska (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "id-ID",
|
||||
name: "Indonesian (Indonesia)",
|
||||
voices: [
|
||||
{
|
||||
value: "Damayanti - standard",
|
||||
name: "Damayanti (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "it-IT",
|
||||
name: "Italian (Italy)",
|
||||
voices: [
|
||||
{ value: "Emma - enhanced", name: "Emma (enhanced)", model: "enhanced" },
|
||||
{
|
||||
value: "Federica-Ml - standard",
|
||||
name: "Federica-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{ value: "Luca - standard", name: "Luca (standard)", model: "standard" },
|
||||
{
|
||||
value: "Neel-Ml - standard",
|
||||
name: "Neel-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Paola - standard",
|
||||
name: "Paola (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "ja-JP",
|
||||
name: "Japanese (Japan)",
|
||||
voices: [
|
||||
{
|
||||
value: "Ayane - standard",
|
||||
name: "Ayane (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Daisuke - standard",
|
||||
name: "Daisuke (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Ichiro - standard",
|
||||
name: "Ichiro (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Koharu - standard",
|
||||
name: "Koharu (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Kyoko - standard",
|
||||
name: "Kyoko (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Mizuki - standard",
|
||||
name: "Mizuki (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Otoya - standard",
|
||||
name: "Otoya (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Sakura - standard",
|
||||
name: "Sakura (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Seiji - standard",
|
||||
name: "Seiji (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "kn-IN",
|
||||
name: "Kannada (India)",
|
||||
voices: [
|
||||
{
|
||||
value: "Alpana - standard",
|
||||
name: "Alpana (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "ko-KR",
|
||||
name: "Korean (South Korea)",
|
||||
voices: [
|
||||
{ value: "Jina - enhanced", name: "Jina (enhanced)", model: "enhanced" },
|
||||
{ value: "Sora - standard", name: "Sora (standard)", model: "standard" },
|
||||
{ value: "Yuna - standard", name: "Yuna (standard)", model: "standard" },
|
||||
{
|
||||
value: "Yuna-Ml - enhanced",
|
||||
name: "Yuna-Ml (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "zlm-MY",
|
||||
name: "Malay (Malaysia)",
|
||||
voices: [
|
||||
{
|
||||
value: "Amira - standard",
|
||||
name: "Amira (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "zh-CN",
|
||||
name: "Mandarin (China)",
|
||||
voices: [
|
||||
{
|
||||
value: "Lili-Ml - enhanced",
|
||||
name: "Lili-Ml (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
{
|
||||
value: "Binbin-Ml - standard",
|
||||
name: "Binbin-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Lilian-Ml - standard",
|
||||
name: "Lilian-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Lisheng-Ml - standard",
|
||||
name: "Lisheng-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Tiantian-Ml - standard",
|
||||
name: "Tiantian-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Tingting-Ml - standard",
|
||||
name: "Tingting-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "cmn-TW",
|
||||
name: "Mandarin (Taiwan)",
|
||||
voices: [
|
||||
{
|
||||
value: "Meijia-Ml - standard",
|
||||
name: "Meijia-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "mr-IN",
|
||||
name: "Marathi (India)",
|
||||
voices: [
|
||||
{
|
||||
value: "Ananya - standard",
|
||||
name: "Ananya (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "nb-NO",
|
||||
name: "Norwegian Bokmål (Norway)",
|
||||
voices: [
|
||||
{
|
||||
value: "Henrik - standard",
|
||||
name: "Henrik (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{ value: "Nora - standard", name: "Nora (standard)", model: "standard" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "pl-PL",
|
||||
name: "Polish (Poland)",
|
||||
voices: [
|
||||
{ value: "Ewa - enhanced", name: "Ewa (enhanced)", model: "enhanced" },
|
||||
{
|
||||
value: "Krzysztof - standard",
|
||||
name: "Krzysztof (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Zosia - standard",
|
||||
name: "Zosia (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "pt-BR",
|
||||
name: "Portuguese (Brazil)",
|
||||
voices: [
|
||||
{
|
||||
value: "luciana - enhanced",
|
||||
name: "Luciana (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
{
|
||||
value: "Fernanda - standard",
|
||||
name: "Fernanda (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Felipe - standard",
|
||||
name: "Felipe (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "pt-PT",
|
||||
name: "Portuguese (Portugal)",
|
||||
voices: [
|
||||
{
|
||||
value: "Catarina - standard",
|
||||
name: "Catarina (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Joana - standard",
|
||||
name: "Joana (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Joaquim - standard",
|
||||
name: "Joaquim (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "ro-RO",
|
||||
name: "Romanian (Romania)",
|
||||
voices: [
|
||||
{
|
||||
value: "Ioana - standard",
|
||||
name: "Ioana (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "ru-RU",
|
||||
name: "Russian (Russia)",
|
||||
voices: [
|
||||
{
|
||||
value: "Katya - standard",
|
||||
name: "Katya (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Katya-Ml - standard",
|
||||
name: "Katya-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Milena - standard",
|
||||
name: "Milena (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{ value: "yuri - standard", name: "Yuri (standard)", model: "standard" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "sk-SK",
|
||||
name: "Slovak (Slovakia)",
|
||||
voices: [
|
||||
{
|
||||
value: "Laura - standard",
|
||||
name: "Laura (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "es-AR",
|
||||
name: "Spanish (Argentina)",
|
||||
voices: [
|
||||
{
|
||||
value: "Diego - standard",
|
||||
name: "Diego (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Isabela - standard",
|
||||
name: "Isabela (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "es-CL",
|
||||
name: "Spanish (Chile)",
|
||||
voices: [
|
||||
{
|
||||
value: "Francisca - standard",
|
||||
name: "Francisca (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "es-CO",
|
||||
name: "Spanish (Colombia)",
|
||||
voices: [
|
||||
{
|
||||
value: "Carlos - standard",
|
||||
name: "Carlos (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Soledad - standard",
|
||||
name: "Soledad (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Ximena - standard",
|
||||
name: "Ximena (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "es-MX",
|
||||
name: "Spanish (Mexico)",
|
||||
voices: [
|
||||
{
|
||||
value: "Angelica - standard",
|
||||
name: "Angelica (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Javier - standard",
|
||||
name: "Javier (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{ value: "Juan - standard", name: "Juan (standard)", model: "standard" },
|
||||
{
|
||||
value: "Paulina-Ml - enhanced",
|
||||
name: "Paulina-Ml (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "es-ES",
|
||||
name: "Spanish (Spain)",
|
||||
voices: [
|
||||
{
|
||||
value: "Jorge - standard",
|
||||
name: "Jorge (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Marisol-Ml - standard",
|
||||
name: "Marisol-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Monica-Ml - standard",
|
||||
name: "Monica-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "sv-SE",
|
||||
name: "Swedish (Sweden)",
|
||||
voices: [
|
||||
{ value: "Alva - standard", name: "Alva (standard)", model: "standard" },
|
||||
{
|
||||
value: "Klara - standard",
|
||||
name: "Klara (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Oskar - standard",
|
||||
name: "Oskar (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "ta-IN",
|
||||
name: "Tamil (India)",
|
||||
voices: [
|
||||
{ value: "Vani - standard", name: "Vani (standard)", model: "standard" },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "te-IN",
|
||||
name: "Telugu (India)",
|
||||
voices: [
|
||||
{
|
||||
value: "Geeta - standard",
|
||||
name: "Geeta (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "th-TH",
|
||||
name: "Thai (Thailand)",
|
||||
voices: [
|
||||
{
|
||||
value: "Kanya - enhanced",
|
||||
name: "Kanya (enhanced)",
|
||||
model: "enhanced",
|
||||
},
|
||||
{
|
||||
value: "Narisa - standard",
|
||||
name: "Narisa (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "tr-TR",
|
||||
name: "Turkish (Turkey)",
|
||||
voices: [
|
||||
{
|
||||
value: "Cem-Ml - standard",
|
||||
name: "Cem-Ml (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
{
|
||||
value: "Yelda - standard",
|
||||
name: "Yelda (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "uk-UA",
|
||||
name: "Ukrainian (Ukraine)",
|
||||
voices: [
|
||||
{
|
||||
value: "Lesya - standard",
|
||||
name: "Lesya (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "va-ES",
|
||||
name: "Valencian (Spain)",
|
||||
voices: [
|
||||
{
|
||||
value: "Empar - standard",
|
||||
name: "Empar (standard)",
|
||||
model: "standard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "vi-VN",
|
||||
name: "Vietnamese (Vietnam)",
|
||||
voices: [
|
||||
{ value: "Linh - standard", name: "Linh (standard)", model: "standard" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default languages;
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { VoiceLanguage } from "../types";
|
||||
|
||||
export const languages: VoiceLanguage[] = [
|
||||
{
|
||||
code: "en-US",
|
||||
name: "English",
|
||||
voices: [
|
||||
{
|
||||
value: "English-US.Female-1",
|
||||
name: "Female",
|
||||
},
|
||||
{
|
||||
value: "English-US.Male-1",
|
||||
name: "Male",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default languages;
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { VoiceLanguage } from "../types";
|
||||
|
||||
export const languages: VoiceLanguage[] = [
|
||||
{
|
||||
code: "en-US",
|
||||
name: "English (US)",
|
||||
voices: [
|
||||
{ value: "3", name: "Alana B." },
|
||||
{ value: "4", name: "Ramona J." },
|
||||
{ value: "5", name: "Ramona J. (promo)" },
|
||||
{ value: "7", name: "Wade C." },
|
||||
{ value: "8", name: "Sofia H." },
|
||||
{ value: "9", name: "David D." },
|
||||
{ value: "11", name: "Isabel V." },
|
||||
{ value: "12", name: "Ava H." },
|
||||
{ value: "13", name: "Jeremy G." },
|
||||
{ value: "14", name: "Nicole L." },
|
||||
{ value: "15", name: "Paige L." },
|
||||
{ value: "16", name: "Tobin A." },
|
||||
{ value: "17", name: "Kai M." },
|
||||
{ value: "18", name: "Tristan F." },
|
||||
{ value: "19", name: "Patrick K." },
|
||||
{ value: "20", name: "Soifia H. (promo)" },
|
||||
{ value: "21", name: "Damian P. (promo)" },
|
||||
{ value: "22", name: "Jodi P. (promo)" },
|
||||
{ value: "23", name: "Lee M. (promo)" },
|
||||
{ value: "24", name: "Selene R. (promo)" },
|
||||
{ value: "26", name: "Wade C. (promo)" },
|
||||
{ value: "27", name: "Joe F." },
|
||||
{ value: "28", name: "Joe F. (promo)" },
|
||||
{ value: "29", name: "Garry J. (character)" },
|
||||
{ value: "33", name: "Jude D." },
|
||||
{ value: "34", name: "Eric S. (promo)" },
|
||||
{ value: "35", name: "Chase J." },
|
||||
{ value: "37", name: "Steve B. (promo)" },
|
||||
{ value: "38", name: "Bella B. (promo)" },
|
||||
{ value: "39", name: "Tilda C. (promo)" },
|
||||
{ value: "41", name: "Paul B. (promo)" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default languages;
|
||||
30
src/vendor/types.ts
vendored
30
src/vendor/types.ts
vendored
@@ -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;
|
||||
@@ -28,7 +42,7 @@ export interface Voice {
|
||||
|
||||
export interface Language {
|
||||
name: string;
|
||||
code: string;
|
||||
value: string;
|
||||
codeMix?: string;
|
||||
}
|
||||
|
||||
@@ -56,6 +70,12 @@ export interface RegionVendors {
|
||||
ibm: Region[];
|
||||
}
|
||||
|
||||
export interface TtsModels {
|
||||
elevenlabs: Model[];
|
||||
whisper: Model[];
|
||||
deepgram: Model[];
|
||||
}
|
||||
|
||||
export interface RecognizerVendors {
|
||||
aws: Language[];
|
||||
google: Language[];
|
||||
@@ -65,6 +85,8 @@ export interface RecognizerVendors {
|
||||
ibm: Language[];
|
||||
nvidia: Language[];
|
||||
soniox: Language[];
|
||||
cobalt: Language[];
|
||||
assemblyai: Language[];
|
||||
}
|
||||
|
||||
export interface SynthesisVendors {
|
||||
@@ -75,6 +97,9 @@ export interface SynthesisVendors {
|
||||
nuance: VoiceLanguage[];
|
||||
ibm: VoiceLanguage[];
|
||||
nvidia: VoiceLanguage[];
|
||||
elevenlabs: VoiceLanguage[];
|
||||
whisper: VoiceLanguage[];
|
||||
deepgram: VoiceLanguage[];
|
||||
}
|
||||
|
||||
export interface MSRawSpeech {
|
||||
@@ -91,5 +116,6 @@ export interface MSRawSpeech {
|
||||
StyleList?: string[];
|
||||
RolePlayList?: string[];
|
||||
WordPerMinute?: Record<string, unknown>;
|
||||
WordsPerMinute?: string;
|
||||
SecondaryLocaleList?: string[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user