mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-01-25 02:08:19 +00:00
Compare commits
8 Commits
fix/reg_tr
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fc6b1ae40 | ||
|
|
c9fcdb08eb | ||
|
|
c33eb46ce0 | ||
|
|
f003c158dc | ||
|
|
b1ddaf230d | ||
|
|
0260b1ec8b | ||
|
|
1c1f97f045 | ||
|
|
e6c5a18c87 |
4
.env
4
.env
@@ -31,4 +31,6 @@
|
||||
## AWS region for enabling Recent Call Feature server logs
|
||||
#VITE_APP_AWS_REGION=us-west-2
|
||||
## enable lazy loading for phone numbers (improves performance when managing large quantities)
|
||||
# VITE_APP_ENABLE_PHONE_NUMBER_LAZY_LOAD=true
|
||||
# VITE_APP_ENABLE_PHONE_NUMBER_LAZY_LOAD=true
|
||||
# hides controlls to add Carrier and Phone number from non Admin/SP Users (also need to set flag on API server to block API calls)
|
||||
#VITE_ADMIN_CARRIER=1
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:18.15-alpine3.16 as builder
|
||||
FROM node:20-alpine AS builder
|
||||
RUN apk update && apk add --no-cache python3 make g++
|
||||
COPY . /opt/app
|
||||
WORKDIR /opt/app/
|
||||
@@ -6,7 +6,7 @@ RUN npm install
|
||||
RUN npm run build
|
||||
RUN npm prune
|
||||
|
||||
FROM node:18.14.1-alpine as webapp
|
||||
FROM node:20-alpine AS webapp
|
||||
RUN apk add curl
|
||||
WORKDIR /opt/app
|
||||
COPY . /opt/app
|
||||
|
||||
@@ -18,7 +18,7 @@ DISABLE_CALL_RECORDING=${DISABLE_CALL_RECORDING:-false}
|
||||
|
||||
# Serialize window global to provide the API URL to static frontend dist
|
||||
# This is declared and utilized in the web app: src/api/constants.ts
|
||||
SCRIPT_TAG="<script>window.JAMBONZ = {API_BASE_URL: \"${API_BASE_URL}\",DISABLE_LCR: \"${DISABLE_LCR}\",DISABLE_JAEGER_TRACING: \"${DISABLE_JAEGER_TRACING}\",DISABLE_CUSTOM_SPEECH: \"${DISABLE_CUSTOM_SPEECH}\",ENABLE_FORGOT_PASSWORD: \"${ENABLE_FORGOT_PASSWORD}\",DISABLE_CALL_RECORDING: \"${DISABLE_CALL_RECORDING}\"};</script>"
|
||||
SCRIPT_TAG="<script>window.JAMBONZ = {API_BASE_URL: \"${API_BASE_URL}\",DISABLE_LCR: \"${DISABLE_LCR}\",DISABLE_JAEGER_TRACING: \"${DISABLE_JAEGER_TRACING}\",DISABLE_CUSTOM_SPEECH: \"${DISABLE_CUSTOM_SPEECH}\",ENABLE_FORGOT_PASSWORD: \"${ENABLE_FORGOT_PASSWORD}\",DISABLE_CALL_RECORDING: \"${DISABLE_CALL_RECORDING}\",ADMIN_CARRIER: \"${ADMIN_CARRIER}\"};</script>"
|
||||
sed -i -e "\@</head>@i\ $SCRIPT_TAG" ./dist/index.html
|
||||
|
||||
# Start the frontend web app static server
|
||||
|
||||
86
package-lock.json
generated
86
package-lock.json
generated
@@ -46,13 +46,13 @@
|
||||
"nanoid": "^5.1.5",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.89.2",
|
||||
"serve": "^14.2.4",
|
||||
"serve": "^14.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.4",
|
||||
"vite": "^6.0.1"
|
||||
"vite": "^6.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -2196,19 +2196,6 @@
|
||||
"integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.11.3",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||
@@ -3265,6 +3252,7 @@
|
||||
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": ">= 1.43.0 < 2"
|
||||
},
|
||||
@@ -3273,37 +3261,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/compression": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
|
||||
"integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
||||
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.5",
|
||||
"bytes": "3.0.0",
|
||||
"compressible": "~2.0.16",
|
||||
"bytes": "3.1.2",
|
||||
"compressible": "~2.0.18",
|
||||
"debug": "2.6.9",
|
||||
"on-headers": "~1.0.2",
|
||||
"safe-buffer": "5.1.2",
|
||||
"negotiator": "~0.6.4",
|
||||
"on-headers": "~1.1.0",
|
||||
"safe-buffer": "5.2.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/compression/node_modules/bytes": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
||||
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/compression/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
@@ -3312,13 +3293,8 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/compression/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
@@ -6911,10 +6887,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -7071,10 +7048,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
|
||||
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -8047,10 +8025,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/serve": {
|
||||
"version": "14.2.4",
|
||||
"resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz",
|
||||
"integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==",
|
||||
"version": "14.2.5",
|
||||
"resolved": "https://registry.npmjs.org/serve/-/serve-14.2.5.tgz",
|
||||
"integrity": "sha512-Qn/qMkzCcMFVPb60E/hQy+iRLpiU8PamOfOSYoAHmmF+fFFmpPpqa6Oci2iWYpTdOUM3VF+TINud7CfbQnsZbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zeit/schemas": "2.36.0",
|
||||
"ajv": "8.12.0",
|
||||
@@ -8059,7 +8038,7 @@
|
||||
"chalk": "5.0.1",
|
||||
"chalk-template": "0.4.0",
|
||||
"clipboardy": "3.0.0",
|
||||
"compression": "1.7.4",
|
||||
"compression": "1.8.1",
|
||||
"is-port-reachable": "4.0.0",
|
||||
"serve-handler": "6.1.6",
|
||||
"update-check": "1.5.4"
|
||||
@@ -9104,10 +9083,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
"node": ">=18"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
@@ -77,10 +77,10 @@
|
||||
"nanoid": "^5.1.5",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.89.2",
|
||||
"serve": "^14.2.4",
|
||||
"serve": "^14.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.4",
|
||||
"vite": "^6.0.1"
|
||||
"vite": "^6.4.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": "eslint --max-warnings=0",
|
||||
|
||||
@@ -35,6 +35,7 @@ interface JambonzWindowObject {
|
||||
DISABLE_ADDITIONAL_SPEECH_VENDORS: string;
|
||||
AWS_REGION: string;
|
||||
ENABLE_PHONE_NUMBER_LAZY_LOAD: string;
|
||||
ADMIN_CARRIER: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -110,6 +111,8 @@ export const STRIPE_PUBLISHABLE_KEY: string =
|
||||
window.JAMBONZ?.STRIPE_PUBLISHABLE_KEY ||
|
||||
import.meta.env.VITE_APP_STRIPE_PUBLISHABLE_KEY;
|
||||
|
||||
export const ADMIN_CARRIER: string =
|
||||
window.JAMBONZ?.ADMIN_CARRIER || import.meta.env.VITE_ADMIN_CARRIER || "0";
|
||||
/** TCP Max Port */
|
||||
export const TCP_MAX_PORT = 65535;
|
||||
|
||||
@@ -429,6 +432,13 @@ export const DEEPGRAM_STT_ENPOINT = [
|
||||
{ name: "EU-hosted", value: "api.eu.deepgram.com" },
|
||||
];
|
||||
|
||||
// ElevenLabs API URI options
|
||||
export const ELEVENLABS_API_URI_OPTIONS = [
|
||||
{ name: "US", value: "api.elevenlabs.io" },
|
||||
{ name: "EU", value: "api.eu.residency.elevenlabs.io" },
|
||||
{ name: "IN", value: "api.in.residency.elevenlabs.io" },
|
||||
];
|
||||
|
||||
/** User scope values values */
|
||||
export const USER_ADMIN = "admin";
|
||||
export const USER_SP = "service_provider";
|
||||
|
||||
@@ -450,6 +450,7 @@ export interface SpeechCredential {
|
||||
resemble_tts_uri: null | string;
|
||||
resemble_tts_use_tls: number;
|
||||
api_uri: null | string;
|
||||
houndify_server_uri: null | string;
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
|
||||
@@ -463,24 +463,18 @@ export const CarrierForm = ({
|
||||
};
|
||||
|
||||
const getSipValidation = () => {
|
||||
if (sipInboundGateways.length === 0 && sipOutboundGateways.length === 0) {
|
||||
if (trunkType === "static_ip") {
|
||||
setActiveTab("inbound");
|
||||
return "Static IP Whitelist trunk type requires at least one inbound gateway.";
|
||||
} else if (trunkType === "reg") {
|
||||
setActiveTab("outbound");
|
||||
return "Registration trunk type requires at least one outbound gateway.";
|
||||
}
|
||||
}
|
||||
|
||||
if (trunkType === "static_ip" && sipInboundGateways.length < 1) {
|
||||
setActiveTab("inbound");
|
||||
return "Static IP Whitelist trunk type requires at least one inbound gateway.";
|
||||
if (
|
||||
trunkType === "static_ip" &&
|
||||
sipInboundGateways.length === 0 &&
|
||||
sipOutboundGateways.length === 0
|
||||
) {
|
||||
setActiveTab("general");
|
||||
return "IP Trunk type requires at least one inbound or outbound gateway.";
|
||||
}
|
||||
|
||||
if (trunkType === "reg" && sipOutboundGateways.length < 1) {
|
||||
setActiveTab("outbound");
|
||||
return "Registration trunk type requires at least one outbound gateway.";
|
||||
return "Registration Trunk type requires at least one outbound gateway.";
|
||||
}
|
||||
|
||||
// Validate Auth Trunk credentials
|
||||
@@ -577,26 +571,6 @@ export const CarrierForm = ({
|
||||
return "Each SIP gateway must have a unique IP address.";
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicates between inbound and outbound gateways
|
||||
for (let i = 0; i < sipInboundGateways.length; i++) {
|
||||
const inboundGateway = sipInboundGateways[i];
|
||||
const dupeInOutbound = sipOutboundGateways.find((g) => {
|
||||
return (
|
||||
inboundGateway.ipv4 &&
|
||||
g.ipv4 === inboundGateway.ipv4 &&
|
||||
g.port === inboundGateway.port
|
||||
);
|
||||
});
|
||||
|
||||
if (dupeInOutbound) {
|
||||
if (refSipInboundIp.current[i]) {
|
||||
refSipInboundIp.current[i].focus();
|
||||
}
|
||||
setActiveTab("inbound");
|
||||
return "Each SIP gateway must have a unique IP address.";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getSmppValidation = () => {
|
||||
@@ -721,9 +695,11 @@ export const CarrierForm = ({
|
||||
if (sipGatewayValidation) {
|
||||
if (
|
||||
sipGatewayValidation ===
|
||||
"Static IP Whitelist trunk type requires at least one inbound gateway."
|
||||
"IP Trunk type requires at least one inbound or outbound gateway."
|
||||
) {
|
||||
setSipInboundMessage(sipGatewayValidation);
|
||||
setSipOutboundMessage(sipGatewayValidation);
|
||||
toastError(sipGatewayValidation);
|
||||
} else if (
|
||||
sipGatewayValidation ===
|
||||
"Auth Trunk requires both username and password credentials."
|
||||
@@ -731,7 +707,7 @@ export const CarrierForm = ({
|
||||
setSipInboundMessage(sipGatewayValidation);
|
||||
} else if (
|
||||
sipGatewayValidation ===
|
||||
"Registration trunk type requires at least one outbound gateway."
|
||||
"Registration Trunk type requires at least one outbound gateway."
|
||||
) {
|
||||
setSipOutboundMessage(sipGatewayValidation);
|
||||
} else {
|
||||
|
||||
@@ -31,6 +31,9 @@ import {
|
||||
ENABLE_HOSTED_SYSTEM,
|
||||
PER_PAGE_SELECTION,
|
||||
USER_ACCOUNT,
|
||||
ADMIN_CARRIER,
|
||||
USER_ADMIN,
|
||||
USER_SP,
|
||||
} from "src/api/constants";
|
||||
import { DeleteCarrier } from "./delete";
|
||||
|
||||
@@ -202,13 +205,16 @@ export const Carriers = () => {
|
||||
</M>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link to={`${ROUTE_INTERNAL_CARRIERS}/add`} title="Add a Carrier">
|
||||
{" "}
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</Link>
|
||||
{((ADMIN_CARRIER === "1" &&
|
||||
(user?.scope === USER_ADMIN || user?.scope === USER_SP)) ||
|
||||
ADMIN_CARRIER === "0") && (
|
||||
<Link to={`${ROUTE_INTERNAL_CARRIERS}/add`} title="Add a Carrier">
|
||||
{" "}
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
<section className="filters filters--multi">
|
||||
<SearchFilter
|
||||
@@ -325,11 +331,15 @@ export const Carriers = () => {
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
<Section clean>
|
||||
<Button small as={Link} to={`${ROUTE_INTERNAL_CARRIERS}/add`}>
|
||||
Add carrier
|
||||
</Button>
|
||||
</Section>
|
||||
{((ADMIN_CARRIER === "1" &&
|
||||
(user?.scope === USER_ADMIN || user?.scope === USER_SP)) ||
|
||||
ADMIN_CARRIER === "0") && (
|
||||
<Section clean>
|
||||
<Button small as={Link} to={`${ROUTE_INTERNAL_CARRIERS}/add`}>
|
||||
Add carrier
|
||||
</Button>
|
||||
</Section>
|
||||
)}
|
||||
<footer>
|
||||
<ButtonGroup>
|
||||
<MS>
|
||||
|
||||
@@ -67,9 +67,6 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const [lcrRoutes, setLcrRoutes] = useState<LcrRoute[]>([LCR_ROUTE_TEMPLATE]);
|
||||
const [previousLcrRoutes, setPreviousLcrRoutes] = useState<LcrRoute[]>([
|
||||
LCR_ROUTE_TEMPLATE,
|
||||
]);
|
||||
const [previouseLcr, setPreviousLcr] = useState<Lcr | null>();
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [lcrForDelete, setLcrForDelete] = useState<Lcr | null>();
|
||||
@@ -127,38 +124,35 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
|
||||
}, [lcrDataMap?.data, previouseLcr]);
|
||||
|
||||
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
|
||||
) {
|
||||
// Only process when both lcrDataMap and lcrRouteDataMap are available
|
||||
if (lcrRouteDataMap && lcrRouteDataMap.data && lcrDataMap?.data) {
|
||||
const defaultCarrierSetEntrySid =
|
||||
lcrDataMap.data.default_carrier_set_entry_sid;
|
||||
|
||||
// Find and store default route information
|
||||
lcrRouteDataMap.data.forEach((route) => {
|
||||
route.lcr_carrier_set_entries?.forEach((entry) => {
|
||||
if (entry.lcr_carrier_set_entry_sid === defaultCarrierSetEntrySid) {
|
||||
setDefaultLcrCarrier(entry.voip_carrier_sid || defaultCarrier);
|
||||
setDefaultLcrCarrierSetEntrySid(
|
||||
entry.lcr_carrier_set_entry_sid || null,
|
||||
);
|
||||
default_lcr_route_sid = entry.lcr_route_sid || "";
|
||||
setDefaultLcrRoute(lr);
|
||||
setDefaultLcrRoute(route);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (lcrRouteDataMap && lcrRouteDataMap.data)
|
||||
setLcrRoutes(
|
||||
lcrRouteDataMap.data.filter(
|
||||
(route) => route.lcr_route_sid !== default_lcr_route_sid,
|
||||
),
|
||||
);
|
||||
}, [lcrRouteDataMap?.data]);
|
||||
// Filter out routes that contain the default carrier set entry
|
||||
const filteredRoutes = lcrRouteDataMap.data.filter((route) => {
|
||||
return !route.lcr_carrier_set_entries?.some(
|
||||
(entry) =>
|
||||
entry.lcr_carrier_set_entry_sid === defaultCarrierSetEntrySid,
|
||||
);
|
||||
});
|
||||
|
||||
setLcrRoutes(filteredRoutes);
|
||||
}
|
||||
}, [lcrRouteDataMap?.data, lcrDataMap?.data]);
|
||||
|
||||
const addLcrRoutes = () => {
|
||||
const newLcrRoute = LCR_ROUTE_TEMPLATE;
|
||||
|
||||
@@ -28,7 +28,13 @@ import { hasLength, hasValue, formatPhoneNumber } from "src/utils";
|
||||
import { DeletePhoneNumber } from "./delete";
|
||||
|
||||
import type { Account, PhoneNumber, Carrier, Application } from "src/api/types";
|
||||
import { PER_PAGE_SELECTION, USER_ACCOUNT } from "src/api/constants";
|
||||
import {
|
||||
PER_PAGE_SELECTION,
|
||||
USER_ACCOUNT,
|
||||
USER_ADMIN,
|
||||
ADMIN_CARRIER,
|
||||
USER_SP,
|
||||
} from "src/api/constants";
|
||||
import { ScopedAccess } from "src/components/scoped-access";
|
||||
import { Scope } from "src/store/types";
|
||||
import { getAccountFilter, setLocation } from "src/store/localStore";
|
||||
@@ -185,16 +191,20 @@ export const PhoneNumbers = () => {
|
||||
<>
|
||||
<section className="mast">
|
||||
<H1 className="h2">Phone numbers</H1>
|
||||
{hasLength(accounts) && hasLength(carriers) && (
|
||||
<Link
|
||||
to={`${ROUTE_INTERNAL_PHONE_NUMBERS}/add`}
|
||||
title="Add a phone number"
|
||||
>
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</Link>
|
||||
)}
|
||||
{hasLength(accounts) &&
|
||||
hasLength(carriers) &&
|
||||
((ADMIN_CARRIER === "1" &&
|
||||
(user?.scope === USER_ADMIN || user?.scope === USER_SP)) ||
|
||||
ADMIN_CARRIER === "0") && (
|
||||
<Link
|
||||
to={`${ROUTE_INTERNAL_PHONE_NUMBERS}/add`}
|
||||
title="Add a phone number"
|
||||
>
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
<section className="filters filters--multi">
|
||||
<SearchFilter
|
||||
@@ -368,14 +378,19 @@ export const PhoneNumbers = () => {
|
||||
>
|
||||
<Icons.Edit3 />
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
title="Delete phone number"
|
||||
onClick={() => setPhoneNumber(phoneNumber)}
|
||||
className="btnty"
|
||||
>
|
||||
<Icons.Trash />
|
||||
</button>
|
||||
{((ADMIN_CARRIER === "1" &&
|
||||
(user?.scope === USER_ADMIN ||
|
||||
user?.scope === USER_SP)) ||
|
||||
ADMIN_CARRIER === "0") && (
|
||||
<button
|
||||
type="button"
|
||||
title="Delete phone number"
|
||||
onClick={() => setPhoneNumber(phoneNumber)}
|
||||
className="btnty"
|
||||
>
|
||||
<Icons.Trash />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -404,13 +419,17 @@ export const PhoneNumbers = () => {
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
<Section clean>
|
||||
{hasLength(accounts) && hasLength(carriers) && (
|
||||
<Button small as={Link} to={`${ROUTE_INTERNAL_PHONE_NUMBERS}/add`}>
|
||||
Add phone number
|
||||
</Button>
|
||||
)}
|
||||
</Section>
|
||||
{((ADMIN_CARRIER === "1" &&
|
||||
(user?.scope === USER_ADMIN || user?.scope === USER_SP)) ||
|
||||
ADMIN_CARRIER === "0") && (
|
||||
<Section clean>
|
||||
{hasLength(accounts) && hasLength(carriers) && (
|
||||
<Button small as={Link} to={`${ROUTE_INTERNAL_PHONE_NUMBERS}/add`}>
|
||||
Add phone number
|
||||
</Button>
|
||||
)}
|
||||
</Section>
|
||||
)}
|
||||
<footer>
|
||||
<ButtonGroup>
|
||||
<MS>
|
||||
|
||||
@@ -97,6 +97,7 @@ import {
|
||||
DEFAULT_VERBIO_MODEL,
|
||||
DISABLE_ADDITIONAL_SPEECH_VENDORS,
|
||||
DISABLE_CUSTOM_SPEECH,
|
||||
ELEVENLABS_API_URI_OPTIONS,
|
||||
GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
|
||||
VERBIO_STT_MODELS,
|
||||
} from "src/api/constants";
|
||||
@@ -110,13 +111,6 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
const { toastError, toastSuccess } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const user = useSelectState("user");
|
||||
|
||||
// ElevenLabs API URI options
|
||||
const ELEVENLABS_API_URI_OPTIONS = [
|
||||
{ name: "US", value: "api.elevenlabs.io" },
|
||||
{ name: "EU", value: "api.eu.residency.elevenlabs.io" },
|
||||
{ name: "IN", value: "api.in.residency.elevenlabs.io" },
|
||||
];
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const regions = useRegionVendors();
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
@@ -225,6 +219,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
useState(false);
|
||||
const [resembleTtsUseTls, setResembleTtsUseTls] = useState(false);
|
||||
const [tmpResembleTtsUseTls, setTmpResembleTtsUseTls] = useState(false);
|
||||
const [houndifyServerUri, setHoundifyServerUri] = useState("");
|
||||
const handleFile = (file: File) => {
|
||||
const handleError = () => {
|
||||
setGoogleServiceKey(null);
|
||||
@@ -417,6 +412,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
...(vendor === VENDOR_AWS && {
|
||||
aws_region: region || null,
|
||||
}),
|
||||
...(vendor === VENDOR_GOOGLE && {
|
||||
model_id: ttsModelId || null,
|
||||
}),
|
||||
...(vendor === VENDOR_MICROSOFT && {
|
||||
region: region || null,
|
||||
use_custom_tts:
|
||||
@@ -462,6 +460,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
client_id: clientId || null,
|
||||
client_key: clientKey || null,
|
||||
user_id: userId || null,
|
||||
houndify_server_uri: houndifyServerUri || null,
|
||||
}),
|
||||
...(vendor === VENDOR_COBALT && {
|
||||
cobalt_server_uri: cobaltServerUri || null,
|
||||
@@ -850,6 +849,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
setOptionsInitialChecked(true);
|
||||
}
|
||||
if (credential?.data?.vendor === VENDOR_GOOGLE) {
|
||||
// Load model_id for Gemini TTS
|
||||
if (credential.data.model_id) {
|
||||
setTtsModelId(credential.data.model_id);
|
||||
}
|
||||
// let try to check if there is custom voices
|
||||
getGoogleCustomVoices({
|
||||
speech_credential_sid: credential.data.speech_credential_sid,
|
||||
@@ -878,6 +881,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
setUserId(credential.data.user_id);
|
||||
}
|
||||
|
||||
if (credential?.data?.houndify_server_uri) {
|
||||
setHoundifyServerUri(credential.data.houndify_server_uri);
|
||||
}
|
||||
|
||||
if (credential?.data?.voice_engine) {
|
||||
setTtsModelId(credential.data.voice_engine);
|
||||
}
|
||||
@@ -1230,218 +1237,245 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
</fieldset>
|
||||
)}
|
||||
{ttsCheck && vendor === VENDOR_GOOGLE && (
|
||||
<fieldset>
|
||||
<label htmlFor="use_custom_voice" className="chk">
|
||||
<>
|
||||
<fieldset>
|
||||
<label htmlFor="google_tts_model_id">
|
||||
Model ID
|
||||
<Tooltip text="Provide a model ID to enable Gemini TTS (e.g., gemini-2.5-flash-tts). Leave empty to use standard Google TTS.">
|
||||
{" "}
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
id="use_custom_voice"
|
||||
name="use_custom_voice"
|
||||
type="checkbox"
|
||||
onChange={(e) => {
|
||||
if (e.target.checked && customVoices.length === 0) {
|
||||
setCustomVoices([DEFAULT_GOOGLE_CUSTOM_VOICE]);
|
||||
}
|
||||
setUseCustomVoicesCheck(e.target.checked);
|
||||
}}
|
||||
checked={useCustomVoicesCheck}
|
||||
id="google_tts_model_id"
|
||||
name="google_tts_model_id"
|
||||
type="text"
|
||||
placeholder="e.g., gemini-2.5-flash-tts"
|
||||
value={ttsModelId}
|
||||
onChange={(e) => setTtsModelId(e.target.value)}
|
||||
/>
|
||||
<div>Use custom voices</div>
|
||||
</label>
|
||||
{useCustomVoicesCheck && (
|
||||
<fieldset>
|
||||
<label htmlFor="sip_gateways">Custom Voices</label>
|
||||
<MXS>
|
||||
<em>At least one Custom voice is required.</em>
|
||||
</MXS>
|
||||
{customVoicesMessage && (
|
||||
<Message message={customVoicesMessage} />
|
||||
)}
|
||||
{hasLength(customVoices) &&
|
||||
customVoices.map((v, i) => (
|
||||
<div key={`custom_voice_${i}`} className="customVoice">
|
||||
<div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label htmlFor="use_custom_voice" className="chk">
|
||||
<input
|
||||
id="use_custom_voice"
|
||||
name="use_custom_voice"
|
||||
type="checkbox"
|
||||
onChange={(e) => {
|
||||
if (e.target.checked && customVoices.length === 0) {
|
||||
setCustomVoices([DEFAULT_GOOGLE_CUSTOM_VOICE]);
|
||||
}
|
||||
setUseCustomVoicesCheck(e.target.checked);
|
||||
}}
|
||||
checked={useCustomVoicesCheck}
|
||||
/>
|
||||
<div>Use custom voices</div>
|
||||
</label>
|
||||
{useCustomVoicesCheck && (
|
||||
<fieldset>
|
||||
<label htmlFor="sip_gateways">Custom Voices</label>
|
||||
<MXS>
|
||||
<em>At least one Custom voice is required.</em>
|
||||
</MXS>
|
||||
{customVoicesMessage && (
|
||||
<Message message={customVoicesMessage} />
|
||||
)}
|
||||
{hasLength(customVoices) &&
|
||||
customVoices.map((v, i) => (
|
||||
<div
|
||||
key={`custom_voice_${i}`}
|
||||
className="customVoice"
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="custom_voice_name">
|
||||
Name
|
||||
{!v.use_voice_cloning_key
|
||||
? " / Reported Usage"
|
||||
: ""}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<input
|
||||
id={`sip_ip_${i}`}
|
||||
name={`sip_ip_${i}`}
|
||||
type="text"
|
||||
placeholder="Assigned Name"
|
||||
required
|
||||
value={v.name}
|
||||
onChange={(e) => {
|
||||
updateCustomVoices(i, "name", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!v.use_voice_cloning_key && (
|
||||
<div>
|
||||
<Selector
|
||||
id={"google_custom_voices_reported_usage"}
|
||||
name={"google_custom_voices_reported_usage"}
|
||||
value={v.reported_usage}
|
||||
options={GOOGLE_CUSTOM_VOICES_REPORTED_USAGE}
|
||||
<label htmlFor="custom_voice_name">
|
||||
Name
|
||||
{!v.use_voice_cloning_key
|
||||
? " / Reported Usage"
|
||||
: ""}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<input
|
||||
id={`sip_ip_${i}`}
|
||||
name={`sip_ip_${i}`}
|
||||
type="text"
|
||||
placeholder="Assigned Name"
|
||||
required
|
||||
value={v.name}
|
||||
onChange={(e) => {
|
||||
updateCustomVoices(
|
||||
i,
|
||||
"reported_usage",
|
||||
"name",
|
||||
e.target.value,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<label
|
||||
htmlFor={`use_voice_cloning_key_${i}`}
|
||||
className="chk"
|
||||
>
|
||||
<input
|
||||
id={`use_voice_cloning_key_${i}`}
|
||||
name={`use_voice_cloning_key_${i}`}
|
||||
type="checkbox"
|
||||
onChange={(e) => {
|
||||
updateCustomVoices(
|
||||
i,
|
||||
"use_voice_cloning_key",
|
||||
e.target.checked ? 1 : 0,
|
||||
);
|
||||
}}
|
||||
checked={v.use_voice_cloning_key ? true : false}
|
||||
/>
|
||||
<div>Use voice cloning key</div>
|
||||
</label>
|
||||
|
||||
{!v.use_voice_cloning_key && (
|
||||
<>
|
||||
<div>
|
||||
{!v.use_voice_cloning_key && (
|
||||
<div>
|
||||
<label htmlFor="custom_voice_name">
|
||||
Model
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<input
|
||||
id={`sip_ip_${i}`}
|
||||
name={`sip_ip_${i}`}
|
||||
type="text"
|
||||
placeholder="Model"
|
||||
required
|
||||
value={v.model}
|
||||
style={{ maxWidth: "100%" }}
|
||||
<Selector
|
||||
id={"google_custom_voices_reported_usage"}
|
||||
name={"google_custom_voices_reported_usage"}
|
||||
value={v.reported_usage}
|
||||
options={
|
||||
GOOGLE_CUSTOM_VOICES_REPORTED_USAGE
|
||||
}
|
||||
onChange={(e) => {
|
||||
updateCustomVoices(
|
||||
i,
|
||||
"model",
|
||||
"reported_usage",
|
||||
e.target.value,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{v.use_voice_cloning_key === 1 && (
|
||||
<>
|
||||
<div>
|
||||
<label
|
||||
htmlFor={`use_voice_cloning_key_${i}`}
|
||||
className="chk"
|
||||
>
|
||||
<input
|
||||
id={`use_voice_cloning_key_${i}`}
|
||||
name={`use_voice_cloning_key_${i}`}
|
||||
type="checkbox"
|
||||
onChange={(e) => {
|
||||
updateCustomVoices(
|
||||
i,
|
||||
"use_voice_cloning_key",
|
||||
e.target.checked ? 1 : 0,
|
||||
);
|
||||
}}
|
||||
checked={v.use_voice_cloning_key ? true : false}
|
||||
/>
|
||||
<div>Use voice cloning key</div>
|
||||
</label>
|
||||
|
||||
{!v.use_voice_cloning_key && (
|
||||
<>
|
||||
<div>
|
||||
{hasValue(v.voice_cloning_key) && (
|
||||
<pre>
|
||||
<code>{v.voice_cloning_key}</code>
|
||||
</pre>
|
||||
)}
|
||||
<div>
|
||||
<label htmlFor="custom_voice_name">
|
||||
Model
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FileUpload
|
||||
id={`google_voice_cloning_key_${i}`}
|
||||
name={`google_voice_cloning_key_${i}`}
|
||||
handleFile={(file) => {
|
||||
updateCustomVoices(
|
||||
i,
|
||||
"voice_cloning_key_file",
|
||||
file,
|
||||
);
|
||||
file.text().then((text) => {
|
||||
<div>
|
||||
<input
|
||||
id={`sip_ip_${i}`}
|
||||
name={`sip_ip_${i}`}
|
||||
type="text"
|
||||
placeholder="Model"
|
||||
required
|
||||
value={v.model}
|
||||
style={{ maxWidth: "100%" }}
|
||||
onChange={(e) => {
|
||||
updateCustomVoices(
|
||||
i,
|
||||
"voice_cloning_key",
|
||||
text.substring(0, 100) + "...",
|
||||
"model",
|
||||
e.target.value,
|
||||
);
|
||||
});
|
||||
}}
|
||||
required={!v.voice_cloning_key}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="btnty"
|
||||
title="Delete custom voice"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCustomVoicesMessage("");
|
||||
if (customVoices.length === 1) {
|
||||
setCustomVoicesMessage(
|
||||
"You must provide at least one custom voice.",
|
||||
{v.use_voice_cloning_key === 1 && (
|
||||
<>
|
||||
<div>
|
||||
<div>
|
||||
{hasValue(v.voice_cloning_key) && (
|
||||
<pre>
|
||||
<code>{v.voice_cloning_key}</code>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<FileUpload
|
||||
id={`google_voice_cloning_key_${i}`}
|
||||
name={`google_voice_cloning_key_${i}`}
|
||||
handleFile={(file) => {
|
||||
updateCustomVoices(
|
||||
i,
|
||||
"voice_cloning_key_file",
|
||||
file,
|
||||
);
|
||||
file.text().then((text) => {
|
||||
updateCustomVoices(
|
||||
i,
|
||||
"voice_cloning_key",
|
||||
text.substring(0, 100) + "...",
|
||||
);
|
||||
});
|
||||
}}
|
||||
required={!v.voice_cloning_key}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="btnty"
|
||||
title="Delete custom voice"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCustomVoicesMessage("");
|
||||
if (customVoices.length === 1) {
|
||||
setCustomVoicesMessage(
|
||||
"You must provide at least one custom voice.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (v.google_custom_voice_sid) {
|
||||
deleteGoogleCustomVoice(
|
||||
v.google_custom_voice_sid,
|
||||
).finally(() => {
|
||||
credential?.refetch();
|
||||
});
|
||||
}
|
||||
setCustomVoices((prev) =>
|
||||
prev.filter((_, idx) => idx !== i),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (v.google_custom_voice_sid) {
|
||||
deleteGoogleCustomVoice(
|
||||
v.google_custom_voice_sid,
|
||||
).finally(() => {
|
||||
credential?.refetch();
|
||||
});
|
||||
}
|
||||
setCustomVoices((prev) =>
|
||||
prev.filter((_, idx) => idx !== i),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Icon>
|
||||
<Icons.Trash2 />
|
||||
</Icon>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<ButtonGroup left>
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
title="Add Voice"
|
||||
onClick={() => {
|
||||
setCustomVoicesMessage("");
|
||||
setCustomVoices((prev) => [
|
||||
...prev,
|
||||
DEFAULT_GOOGLE_CUSTOM_VOICE,
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<Icon subStyle="teal">
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
</fieldset>
|
||||
)}
|
||||
</fieldset>
|
||||
}}
|
||||
>
|
||||
<Icon>
|
||||
<Icons.Trash2 />
|
||||
</Icon>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<ButtonGroup left>
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
title="Add Voice"
|
||||
onClick={() => {
|
||||
setCustomVoicesMessage("");
|
||||
setCustomVoices((prev) => [
|
||||
...prev,
|
||||
DEFAULT_GOOGLE_CUSTOM_VOICE,
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<Icon subStyle="teal">
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
</fieldset>
|
||||
)}
|
||||
</fieldset>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -1484,6 +1518,15 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
onChange={(e) => setUserId(e.target.value)}
|
||||
disabled={credential ? true : false}
|
||||
/>
|
||||
<label htmlFor="houndify_server_uri">Audio Endpoint</label>
|
||||
<input
|
||||
id="houndify_server_uri"
|
||||
type="text"
|
||||
name="houndify_server_uri"
|
||||
placeholder="Audio Endpoint (optional)"
|
||||
value={houndifyServerUri}
|
||||
onChange={(e) => setHoundifyServerUri(e.target.value)}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
{vendor === VENDOR_NUANCE && (
|
||||
|
||||
Reference in New Issue
Block a user