Compare commits

...

33 Commits

Author SHA1 Message Date
Hoan Luu Huu
c33eb46ce0 soundhound speech credential support audio endpoint (#582)
* soubound speech credential support audio endpoint

* wip
2025-11-28 21:48:13 -05:00
Hoan Luu Huu
f003c158dc fix outbound call routing race condition show default lcr route set that user cannot delete (#577) 2025-11-18 07:52:28 -05:00
Sam Machin
b1ddaf230d require IP auth trunk to have either inbound or outbound carrier (#579)
also cleaned up wordig to be consistent `IP Trunk` not `Static IP Whitelist`
2025-11-10 10:29:45 -05:00
Hoan Luu Huu
0260b1ec8b Inbound and outbound sipgateway can be duplicated (#576) 2025-11-08 08:49:00 -05:00
Anton Voylenko
1c1f97f045 chore: bump node version (#575) 2025-11-04 19:33:41 -05:00
Hoan Luu Huu
e6c5a18c87 fixed reg trunk validation cannot move tab and focus to missing fields (#574)
* fixed reg trunk validation cannot move tab and focus to missing fields

* fixed reg trunk validation cannot move tab and focus to missing fields

* wip
2025-10-27 07:21:39 -04:00
Hoan Luu Huu
19742ab67e fixed cannot saved auth trunk (#573) 2025-10-24 07:22:36 -04:00
Hoan Luu Huu
53d0c0b510 Carrier change for trunk type (#564)
* support carrier credential authentication

* wip

* wip

* wip

* wip

* wip

* change trunk type to selector

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip
2025-10-21 06:49:25 -04:00
Hoan Luu Huu
7a0eb71bae support gladia stt (#572) 2025-10-20 04:47:55 -04:00
Hoan Luu Huu
6aae8d9930 support soundhound stt (#567)
* support houndify stt

* wip

* wip
2025-10-14 00:52:06 -04:00
Hoan Luu Huu
a70a1bf614 support elevenlabs different endpoint (#571)
* support elevenlabs different endpoint

* wip
2025-10-09 08:20:39 -04:00
Dave Horton
975a787f1e review Preview now that Flux is GA (#570) 2025-10-04 20:12:15 -04:00
Hoan Luu Huu
46e220f28b support deepgram flux (#569)
* support deepgram flux

* wip
2025-10-03 10:10:13 -04:00
Hoan Luu Huu
6836a99635 add special field for Carriers in env vars (#561)
* add special field for Carriers in env vars

* wip

* wip
2025-09-05 08:04:24 -04:00
Hoan Luu Huu
f7f4a2e7b1 cannot delete carrier because of undefined lcrs list (#563) 2025-09-01 08:12:31 -04:00
Hoan Luu Huu
f1f8a7d808 support resemble tts (#559)
* support resemble tts

* wip
2025-08-13 08:17:18 -04:00
sathish kumar pasham
9dd9cf867a 556 resolve security vulnerabilities by upgrading the vite library (#557) 2025-08-06 09:30:22 -04:00
Hoan Luu Huu
a372c09bc6 support deepgram EU-hosted STT (#555)
* support deepgram EU-hosted STT

* wip

* fix review comment

* wip

* wip
2025-08-04 07:24:33 -04:00
Hoan Luu Huu
031e5e923e support deepgram river (#547) 2025-07-29 13:54:22 -04:00
Hoan Luu Huu
e02904f7f3 Draw STT latency to recording player by using stt metrics from opentelemetry (#551)
* support showing stt latency from otel stt.latency_ms

* wip
2025-07-29 09:57:35 -04:00
Dave Horton
7eaf25d13f bump version 2025-07-15 11:42:26 -04:00
Hoan Luu Huu
6e4d663337 fixed deprecated api when migrating to sass 3 (#549) 2025-07-15 08:27:02 -04:00
sathish kumar pasham
c0a40dd784 resolve security vulnerabilities (#546) 2025-07-09 14:57:36 -04:00
Hoan Luu Huu
536bf0f471 support assemblyai v3 (#540)
* support assemblyai v3

* wip
2025-07-01 15:48:17 -04:00
Sam Machin
aaf1ede5c2 Update form.tsx (#545) 2025-07-01 07:57:05 -04:00
Hoan Luu Huu
24d646f705 support inworld tts (#537)
* support inworld tts

* wip
2025-06-27 07:13:51 -04:00
Hoan Luu Huu
c648afcb1a support mod cartesia transcribe (#536) 2025-06-17 20:53:45 +02:00
Hoan Luu Huu
4eca59d9bd fix regression bug: new app does not save tts voice by default (#535) 2025-06-06 14:50:34 +02:00
Hoan Luu Huu
4a293ae7da appEnvs should support enum dropdown (#532) 2025-06-02 07:41:18 -04:00
Hoan Luu Huu
03e52e3dc5 fixed Cannot delete Carrier, show message that there is link to LCR (#533)
* fixed Cannot delete Carrier, show message that there is link to LCR

* wip
2025-06-02 07:14:01 -04:00
Hoan Luu Huu
9ab592a898 fixed admin filter phone number by SP (#531) 2025-05-30 07:24:31 -04:00
Hoan Luu Huu
1723326890 fix app crash when create new speech credential (#530)
* fix app crash when create new speech credential

* fix app crash when create new speech credential
2025-05-29 08:31:41 -04:00
Hoan Luu Huu
504825d699 fix app envs does not take default value and filepicker is required even value is available (#529) 2025-05-28 19:58:21 -04:00
25 changed files with 3244 additions and 2098 deletions

2
.env
View File

@@ -1,4 +1,4 @@
#VITE_API_BASE_URL=http://127.0.0.1:3000/v1
# VITE_API_BASE_URL=http://127.0.0.1:3000/v1
#VITE_DEV_BASE_URL=http://127.0.0.1:3000/v1
## enables choosing units and lisenced account call limits

View File

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

3113
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
{
"name": "jambonz-webapp",
"description": "A simple provisioning web app for jambonz",
"version": "0.9.4",
"version": "0.9.5",
"license": "MIT",
"type": "module",
"engines": {
"node": ">=14.18"
"node": ">=18"
},
"contributors": [
{
@@ -41,7 +41,7 @@
"deploy": "npm i && npm run build && npm run pm2"
},
"dependencies": {
"@jambonz/ui-kit": "^0.0.21",
"@jambonz/ui-kit": "^0.0.22",
"@stripe/react-stripe-js": "^2.6.2",
"@stripe/stripe-js": "^3.2.0",
"dayjs": "^1.11.10",
@@ -71,16 +71,16 @@
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"express": "^4.19.2",
"express": "^5.1.0",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"nanoid": "^5.0.7",
"lint-staged": "^16.1.2",
"nanoid": "^5.1.5",
"prettier": "^3.2.5",
"sass": "^1.74.1",
"serve": "^14.2.1",
"sass": "^1.89.2",
"serve": "^14.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.4.4",
"vite": "^5.2.8"
"vite": "^6.4.1"
},
"lint-staged": {
"*.{ts,tsx}": "eslint --max-warnings=0",

View File

@@ -3,6 +3,7 @@ import type {
Currency,
ElevenLabsOptions,
GoogleCustomVoice,
InworldOptions,
LimitField,
LimitUnitOption,
PasswordSettings,
@@ -130,7 +131,7 @@ export const DEFAULT_WEBHOOK: WebHook = {
};
/** Default SIP/SMPP Gateways */
export const DEFAULT_SIP_GATEWAY: SipGateway = {
export const DEFAULT_SIP_INBOUND_GATEWAY: SipGateway = {
voip_carrier_sid: "",
ipv4: "",
port: 5060,
@@ -244,6 +245,14 @@ export const VERBIO_STT_MODELS = [
export const DEFAULT_VERBIO_MODEL = "V1";
// ASSEMBLYAI
export const ASSEMBLYAI_STT_VERSIONS = [
{ name: "V2", value: "v2" },
{ name: "V3", value: "v3" },
];
export const DEFAULT_ASSEMBLYAI_STT_VERSION = "v2";
export const ADDITIONAL_SPEECH_VENDORS: Lowercase<Vendor>[] = ["speechmatics"];
// Google Custom Voice reported usage options
@@ -277,6 +286,14 @@ export const DEFAULT_RIMELABS_OPTIONS: Partial<RimelabsOptions> = {
reduceLatency: true,
};
export const DEFAULT_INWORLD_OPTIONS: Partial<InworldOptions> = {
audioConfig: {
pitch: 0.0,
speakingRate: 1.0,
},
temperature: 0.8,
};
// PlayHT options
export const DEFAULT_PLAYHT_OPTIONS: Partial<PlayHTOptions> = {
quality: "medium",
@@ -331,6 +348,12 @@ export const DTMF_TYPE_SELECTION: SelectorOptions[] = [
{ name: "Tones", value: "tones" },
];
export const TRUNK_TYPE_SELECTION: SelectorOptions[] = [
{ name: "IP Trunk", value: "static_ip" },
{ name: "Auth Trunk", value: "auth" },
{ name: "Registration Trunk", value: "reg" },
];
/** Available webhook methods */
export const WEBHOOK_METHODS: WebhookOption[] = [
{
@@ -401,6 +424,11 @@ export const CurrencySymbol: Currency = {
usd: "$",
};
export const DEEPGRAM_STT_ENPOINT = [
{ name: "US (Default)", value: "" },
{ name: "EU-hosted", value: "api.eu.deepgram.com" },
];
/** User scope values values */
export const USER_ADMIN = "admin";
export const USER_SP = "service_provider";

View File

@@ -1,4 +1,10 @@
import type { Language, Model, Vendor, VoiceLanguage } from "src/vendor/types";
import type {
JambonzResourceOptions,
Language,
Model,
Vendor,
VoiceLanguage,
} from "src/vendor/types";
/** Simple types */
@@ -413,6 +419,7 @@ export interface SpeechCredential {
custom_stt_endpoint: null | string;
client_id: null | string;
client_secret: null | string;
client_key: null | string;
secret: null | string;
nuance_tts_uri: null | string;
nuance_stt_uri: null | string;
@@ -429,8 +436,10 @@ export interface SpeechCredential {
label: null | string;
cobalt_server_uri: null | string;
model_id: null | string;
stt_model_id: null | string;
voice_engine: null | string;
engine_version: null | string;
service_version: null | string;
model: null | string;
options: null | string;
deepgram_stt_uri: null | string;
@@ -438,6 +447,10 @@ export interface SpeechCredential {
deepgram_stt_use_tls: number;
speechmatics_stt_uri: null | string;
playht_tts_uri: null | string;
resemble_tts_uri: null | string;
resemble_tts_use_tls: number;
api_uri: null | string;
houndify_server_uri: null | string;
}
export interface Alert {
@@ -457,6 +470,8 @@ export interface CarrierRegisterStatus {
export type DtmfType = "rfc2833" | "tones" | "info";
export type TrunkType = "static_ip" | "auth" | "reg";
export interface Carrier {
voip_carrier_sid: string;
name: string;
@@ -485,6 +500,7 @@ export interface Carrier {
register_status: CarrierRegisterStatus;
dtmf_type: DtmfType;
outbound_sip_proxy: string | null;
trunk_type: TrunkType;
}
export interface PredefinedCarrier extends Carrier {
@@ -780,6 +796,16 @@ export interface RimelabsOptions {
reduceLatency: boolean;
}
export interface InworldOptions {
audioConfig: {
bitRate?: number;
sampleRateHertz?: number;
pitch?: number;
speakingRate?: number;
};
temperature?: number;
}
export type CartesiaEmotions =
| "anger:lowest"
| "anger:low"
@@ -810,6 +836,9 @@ export interface AppEnvProperty {
default?: string | number | boolean;
obscure?: boolean;
uiHint?: "input" | "textarea" | "filepicker";
enum?: string[];
jambonzResource?: "carriers";
jambonzResourceOptions?: JambonzResourceOptions[];
}
export interface AppEnv {

View File

@@ -25,7 +25,6 @@
}
select {
@include ui-mixins.m();
appearance: none;
padding: ui-vars.$px01 ui-vars.$px02;
border-radius: ui-vars.$px01;
@@ -33,6 +32,7 @@
background-color: ui-vars.$white;
width: 100%;
max-width: vars.$widthinput;
@include ui-mixins.m();
&:focus {
border-color: ui-vars.$dark;

View File

@@ -28,7 +28,6 @@
}
@mixin typeahead-input {
@include ui-mixins.m();
appearance: none;
padding: ui-vars.$px01 ui-vars.$px02;
border-radius: ui-vars.$px01;
@@ -37,6 +36,7 @@
max-width: vars.$widthtypeaheadinput;
transition: border-color 0.2s ease;
font-family: inherit;
@include ui-mixins.m();
&:focus {
border-color: ui-vars.$dark;
@@ -84,7 +84,6 @@
}
@mixin typeahead-dropdown {
@include ui-mixins.m();
position: absolute;
top: 100%;
left: 0;
@@ -93,6 +92,7 @@
border: 1px solid ui-vars.$dark;
max-height: 200px;
overflow-y: auto;
@include ui-mixins.m();
}
@mixin typeahead-option {
@@ -126,8 +126,8 @@
width: 100%;
input {
@include typeahead-input();
width: 100%;
@include typeahead-input();
}
span {
@@ -135,8 +135,8 @@
}
.typeahead-dropdown {
@include typeahead-dropdown();
z-index: 1000;
@include typeahead-dropdown();
}
.typeahead-option {
@@ -149,10 +149,10 @@
width: auto;
input {
@include typeahead-input();
height: 34px;
min-width: 370px;
font-size: var(--mxs-size);
@include typeahead-input();
}
span {
@@ -160,13 +160,13 @@
}
.typeahead-dropdown {
@include typeahead-dropdown();
width: 100%;
@include typeahead-dropdown();
}
.typeahead-option {
@include typeahead-option();
font-size: var(--mxs-size);
@include typeahead-option();
}
.pointerevents {

View File

@@ -25,6 +25,7 @@ import {
useServiceProviderData,
useApiData,
getAppEnvSchema,
getSPVoipCarriers,
} from "src/api";
import {
ROUTE_INTERNAL_ACCOUNTS,
@@ -53,7 +54,12 @@ import type {
AppEnv,
} from "src/api/types";
import { MSG_REQUIRED_FIELDS, MSG_WEBHOOK_FIELDS } from "src/constants";
import { hasLength, isUserAccountScope, useRedirect } from "src/utils";
import {
hasLength,
hasValue,
isUserAccountScope,
useRedirect,
} from "src/utils";
import { setAccountFilter, setLocation } from "src/store/localStore";
import SpeechProviderSelection from "./speech-selection";
import ObscureInput from "src/components/obscure-input";
@@ -583,6 +589,51 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
setFallbackSpeechRecognizerLabel(tmp);
};
const fetchAppEnvJambonzResources = async (appEnv: AppEnv) => {
if (appEnv) {
const promises = Object.entries(appEnv).map(async ([key, value]) => {
const { jambonzResource } = value;
switch (jambonzResource) {
case "carriers":
const carriers = await getSPVoipCarriers(
currentServiceProvider?.service_provider_sid || "",
{
page: 1,
page_size: 10000,
...(user?.account_sid && {
account_sid: user.account_sid,
}),
},
);
if (carriers.json.total) {
return {
key,
jambonzResourceOptions: carriers.json.data.map((carrier) => ({
name: carrier.name,
value: carrier.name,
})),
};
}
break;
default:
break;
}
return { key, jambonzResourceOptions: null };
});
const results = await Promise.all(promises);
// Merge the results back into appEnv
results.forEach(({ key, jambonzResourceOptions }) => {
if (jambonzResourceOptions) {
appEnv[key].jambonzResourceOptions = jambonzResourceOptions;
}
});
}
return appEnv;
};
useEffect(() => {
if (callWebhook && callWebhook.url) {
// Clear any existing timeout to prevent multiple requests
@@ -594,7 +645,26 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
appEnvTimeoutRef.current = setTimeout(() => {
getAppEnvSchema(callWebhook.url)
.then(({ json }) => {
setAppEnv(json);
// fetch app env jambonz_resource
fetchAppEnvJambonzResources(json).then((updatedEnv) => {
setAppEnv(updatedEnv);
const defaultEnvVars = Object.keys(updatedEnv).reduce(
(acc, key) => {
const value = updatedEnv[key];
if (value?.default) {
return { ...acc, [key]: value.default };
}
return acc;
},
{},
);
setEnvVars((prev) => ({
...defaultEnvVars,
...(prev || {}),
}));
});
// Default value
})
.catch((error) => {
setMessage(error.msg);
@@ -828,7 +898,9 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
? String(defaultValue)
: "",
onChange: (
e: React.ChangeEvent<HTMLInputElement>,
e: React.ChangeEvent<
HTMLInputElement | HTMLSelectElement
>,
) => {
// Convert to proper type based on schema
let newValue;
@@ -853,6 +925,15 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
type: isNumber ? "number" : "text",
};
const isDropdown =
(webhook.webhookEnv![key].type === "string" &&
(webhook.webhookEnv![key].enum?.length ||
0) > 0) ||
hasLength(
webhook.webhookEnv![key]
.jambonzResourceOptions,
);
const textAreaSpecificProps = {
rows: 6,
cols: 61,
@@ -863,6 +944,23 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
.obscure
? ObscureInput
: webhook.webhookEnv![key].uiHint || "input";
if (isDropdown) {
const options =
webhook.webhookEnv![key]
.jambonzResourceOptions ||
webhook.webhookEnv![key].enum!.map(
(option) => ({
name: option,
value: option,
}),
);
return (
<Selector
{...commonProps}
options={options}
/>
);
}
if (componentType === "filepicker") {
return (
<>
@@ -886,7 +984,8 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
}}
placeholder="Choose a file"
required={
webhook.webhookEnv![key].required
webhook.webhookEnv![key].required &&
!hasValue(envVars?.[key])
}
/>
{React.createElement("textarea", {

View File

@@ -35,6 +35,10 @@ import {
VENDOR_VOXIST,
VENDOR_RIMELABS,
VENDOR_OPENAI,
VENDOR_INWORLD,
VENDOR_DEEPGRAM_FLUX,
VENDOR_RESEMBLE,
VENDOR_HOUNDIFY,
} from "src/vendor";
import {
LabelOptions,
@@ -139,7 +143,12 @@ export const SpeechProviderSelection = ({
ttsEffectTimer.current = setTimeout(() => {
configSynthesis();
}, 200);
}, [synthVendor, synthLabel, serviceProviderSid]);
}, [
synthVendor,
synthLabel,
serviceProviderSid,
application_speech_synthesis_voice,
]);
// Get Recognizer languages and voices
useEffect(() => {
@@ -246,6 +255,15 @@ export const SpeechProviderSelection = ({
// Extract model
if (json.models && json.models.length) {
setSynthesisModelOptions(json.models);
if (
synthVendor === VENDOR_DEEPGRAM &&
(!application_speech_synthesis_voice ||
!json.models.some(
(m) => m.value === application_speech_synthesis_voice,
))
) {
setSynthVoice(json.models[0].value);
}
}
if (json.tts && json.tts.length) {
@@ -298,6 +316,15 @@ export const SpeechProviderSelection = ({
updateTtsVoice(newLang!.value, newLang!.voices[0].value);
return;
}
if (synthVendor === VENDOR_INWORLD) {
let newLang = json.tts.find((lang) => lang.value === "en");
// If the new language doesn't map then default to the first one
if (!newLang) {
newLang = json.tts[0];
}
updateTtsVoice(newLang!.value, 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);
@@ -337,7 +364,6 @@ export const SpeechProviderSelection = ({
const updateTtsVoice = (language: string, voice: string) => {
if (shouldUpdateTtsVoice.current) {
console.log("xhoaluu");
setSynthLang(language);
setSynthVoice(voice);
shouldUpdateTtsVoice.current = false;
@@ -345,6 +371,9 @@ export const SpeechProviderSelection = ({
};
const configRecognizer = () => {
if (recogVendor === VENDOR_DEEPGRAM_FLUX) {
return;
}
getSpeechSupportedLanguagesAndVoices(
serviceProviderSid,
recogVendor,
@@ -389,19 +418,6 @@ export const SpeechProviderSelection = ({
});
};
useEffect(() => {
if (
synthVendor === VENDOR_DEEPGRAM &&
synthesisModelOptions.length > 0 &&
!synthesisModelOptions.some(
(m) => m.value === application_speech_synthesis_voice,
)
) {
setSynthVoice(synthesisModelOptions[0].value);
} else {
setSynthVoice(application_speech_synthesis_voice || "");
}
}, [synthesisModelOptions, application_speech_synthesis_voice]);
return (
<>
<fieldset>
@@ -418,6 +434,8 @@ export const SpeechProviderSelection = ({
vendor.value !== VENDOR_SPEECHMATICS &&
vendor.value !== VENDOR_CUSTOM &&
vendor.value !== VENDOR_OPENAI &&
vendor.value !== VENDOR_DEEPGRAM_FLUX &&
vendor.value !== VENDOR_HOUNDIFY &&
vendor.value !== VENDOR_COBALT,
)}
onChange={(e) => {
@@ -572,6 +590,7 @@ export const SpeechProviderSelection = ({
vendor.value != VENDOR_WELLSAID &&
vendor.value != VENDOR_ELEVENLABS &&
vendor.value != VENDOR_WHISPER &&
vendor.value !== VENDOR_RESEMBLE &&
vendor.value !== VENDOR_CUSTOM,
)}
onChange={(e) => {
@@ -599,6 +618,7 @@ export const SpeechProviderSelection = ({
)}
{recogVendor &&
!recogVendor.toString().startsWith(VENDOR_CUSTOM) &&
recogVendor !== VENDOR_DEEPGRAM_FLUX &&
recogLang && (
<>
<label htmlFor="recognizer_lang">Language</label>

View File

@@ -2,11 +2,11 @@ import React, { useEffect, useState } from "react";
import { P } from "@jambonz/ui-kit";
import { Modal, ModalClose } from "src/components";
import { getFetch } from "src/api";
import { getFetch, getLcrRoutes, getLcrs } from "src/api";
import { API_PHONE_NUMBERS } from "src/api/constants";
import { formatPhoneNumber, hasLength } from "src/utils";
import { formatPhoneNumber, hasLength, hasValue } from "src/utils";
import type { Carrier, PhoneNumber } from "src/api/types";
import type { Carrier, Lcr, PhoneNumber } from "src/api/types";
type DeleteProps = {
carrier: Carrier;
@@ -20,28 +20,64 @@ export const DeleteCarrier = ({
handleSubmit,
}: DeleteProps) => {
const [phoneNumbers, setPhoneNumbers] = useState<PhoneNumber[]>();
const [lcrs, setLcrs] = useState<Lcr[]>();
useEffect(() => {
let ignore = false;
getFetch<PhoneNumber[]>(API_PHONE_NUMBERS).then(({ json }) => {
Promise.all([
getFetch<PhoneNumber[]>(API_PHONE_NUMBERS),
new Promise<Lcr[]>((resolve, reject) => {
getLcrs()
.then(({ json }) => {
Promise.all(
json.map((lcr: Lcr) =>
getLcrRoutes(lcr.lcr_sid!)
.then(({ json }) => {
if (
json.some((route) =>
route.lcr_carrier_set_entries?.some(
(entry) =>
entry.voip_carrier_sid === carrier.voip_carrier_sid,
),
)
) {
return lcr;
}
})
.catch((error) => reject(error)),
),
)
.then((lcrs) => {
resolve(lcrs as Lcr[]);
})
.catch((error) => reject(error));
})
.catch((error) => reject(error));
}),
]).then(([numbers, fetchedLcrs]) => {
if (!ignore) {
setPhoneNumbers(
json.filter(
numbers.json.filter(
(phone) => phone.voip_carrier_sid === carrier.voip_carrier_sid,
),
);
// Only set LCRs if they are not empty
setLcrs(fetchedLcrs.filter((p) => hasValue(p)));
}
});
return function cleanup() {
ignore = true;
};
}, []);
}, [carrier.voip_carrier_sid]);
const hasBlockingDependencies = hasLength(phoneNumbers) || hasLength(lcrs);
return (
<>
{phoneNumbers && !hasLength(phoneNumbers) && (
{phoneNumbers && lcrs && !hasBlockingDependencies && (
<Modal handleCancel={handleCancel} handleSubmit={handleSubmit}>
<P>
Are you sure you want to delete carrier{" "}
@@ -49,24 +85,49 @@ export const DeleteCarrier = ({
</P>
</Modal>
)}
{hasLength(phoneNumbers) && (
{hasBlockingDependencies && (
<ModalClose handleClose={handleCancel}>
<P>
In order to delete the carrier it cannot be in use by any{" "}
<span>Phone Numbers ({phoneNumbers.length})</span>.
{hasLength(phoneNumbers) && (
<span>Phone Numbers ({phoneNumbers.length})</span>
)}
{hasLength(phoneNumbers) && hasLength(lcrs) && " or "}
{hasLength(lcrs) && (
<span>Outbound call Routings ({lcrs.length})</span>
)}
.
</P>
<ul className="m">
<li>
<strong>Phone Numbers:</strong>
</li>
{phoneNumbers.map((phone) => {
return (
<li className="txt--teal" key={phone.phone_number_sid}>
{formatPhoneNumber(phone.number)}
</li>
);
})}
</ul>
{hasLength(phoneNumbers) && (
<ul className="m">
<li>
<strong>Phone Numbers:</strong>
</li>
{phoneNumbers.map((phone) => {
return (
<li className="txt--teal" key={phone.phone_number_sid}>
{formatPhoneNumber(phone.number)}
</li>
);
})}
</ul>
)}
{hasLength(lcrs) && (
<ul className="m">
<li>
<strong>Outbound Call Routing:</strong>
</li>
{lcrs.map((lcr) => {
return (
<li className="txt--teal" key={lcr.lcr_sid}>
{lcr.name || "Default route"}
</li>
);
})}
</ul>
)}
</ModalClose>
)}
</>

File diff suppressed because it is too large Load Diff

View File

@@ -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>();
@@ -82,7 +79,7 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
setLocation();
if (currentServiceProvider) {
setApiUrl(
`ServiceProviders/${currentServiceProvider.service_provider_sid}/VoipCarriers`,
`ServiceProviders/${currentServiceProvider.service_provider_sid}/VoipCarriers${accountSid ? `?account_sid=${accountSid}` : ""}`,
);
}
}, [user, currentServiceProvider, accountSid]);
@@ -92,16 +89,8 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
setAccountSid(user?.account_sid);
}
const carriersFiltered = carriers
? carriers.filter((carrier) =>
accountSid
? carrier.account_sid === accountSid
: carrier.account_sid === null,
)
: [];
const ret = carriersFiltered
? carriersFiltered.map((c: Carrier, i) => {
const ret = carriers
? carriers.map((c: Carrier, i) => {
if (i === 0) {
setDefaultCarrier(c.voip_carrier_sid);
}
@@ -123,45 +112,47 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
return ret;
}, [accountSid, carriers]);
if (lcrDataMap && lcrDataMap.data && lcrDataMap.data !== previouseLcr) {
setLcrName(lcrDataMap.data.name || "");
setIsActive(lcrDataMap.data.is_active);
setPreviousLcr(lcrDataMap.data);
}
useEffect(() => {
if (lcrDataMap && lcrDataMap.data && lcrDataMap.data !== previouseLcr) {
setLcrName(lcrDataMap.data.name || "");
setIsActive(lcrDataMap.data.is_active);
setPreviousLcr(lcrDataMap.data);
if (lcrDataMap.data.account_sid) {
setAccountSid(lcrDataMap.data.account_sid);
}
}
}, [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;

View File

@@ -83,6 +83,9 @@ export const PhoneNumbers = () => {
page_size: Number(perPageFilter),
...(accSid && { account_sid: accSid }),
...(filter && { filter }),
...(currentServiceProvider?.service_provider_sid && {
service_provider_sid: currentServiceProvider.service_provider_sid,
}),
})
.then(({ json }) => {
if (json) {

View File

@@ -4,7 +4,6 @@
.barGroup {
border-radius: ui-vars.$px01;
@include mixins.code();
text-align: left;
padding: ui-vars.$px03;
color: ui-vars.$pink;
@@ -13,6 +12,7 @@
margin-top: ui-vars.$px02;
overflow-x: auto;
overflow-y: scroll;
@include mixins.code();
@media (max-width: 600px) {
padding: 15px;
@@ -72,7 +72,6 @@
.spanDetailsWrapper {
border-radius: ui-vars.$px01;
@include mixins.code();
text-align: left;
padding: ui-vars.$px01;
background-color: ui-vars.$white;
@@ -82,6 +81,7 @@
max-width: ui-vars.$width-tablet-2;
max-height: 500px;
overflow-y: scroll;
@include mixins.code();
&__detailsWrapper {
height: 100%;

View File

@@ -161,6 +161,7 @@ export const Player = ({ call }: PlayerProps) => {
const drawSttRegionForSpan = (
s: JaegerSpan,
allSpans: JaegerSpan[],
startPoint: JaegerSpan,
channel = 0,
) => {
@@ -175,7 +176,36 @@ export const Player = ({ call }: PlayerProps) => {
const end =
(s.endTimeUnixNano - startPoint.startTimeUnixNano) / 1_000_000_000;
const endSpeechTime = getSilenceStartTime(start, end, channel);
const verbHookSpans = getSpansByNameRegex(allSpans, /verb:hook/);
const verbHookSpan = verbHookSpans.find(
(v) => v.parentSpanId === s.spanId,
);
let verbHookDurantion = 0;
let latency = 0;
if (verbHookSpan) {
verbHookDurantion =
(verbHookSpan.endTimeUnixNano - verbHookSpan.startTimeUnixNano) /
1_000_000_000;
}
const [sttLatencyMs] = getSpanAttributeByName(
s.attributes,
"stt.latency_ms",
);
let endSpeechTime = 0;
if (!sttLatencyMs) {
endSpeechTime = getSilenceStartTime(start, end, channel);
latency = Number(
(end - endSpeechTime - verbHookDurantion).toFixed(2),
);
} else {
endSpeechTime =
end -
Number(sttLatencyMs.value.stringValue) / 1_000 -
verbHookDurantion;
latency = Number(sttLatencyMs.value.stringValue) / 1_000;
}
const [sttResult] = getSpanAttributeByName(s.attributes, "stt.result");
let att: WaveSurferSttResult;
@@ -187,7 +217,7 @@ export const Player = ({ call }: PlayerProps) => {
transcript: data.alternatives[0].transcript,
confidence: data.alternatives[0].confidence,
language_code: data.language_code,
...(endSpeechTime > 0 && { latency: end - endSpeechTime }),
latency,
};
const [sttResolve] = getSpanAttributeByName(
@@ -206,7 +236,7 @@ export const Player = ({ call }: PlayerProps) => {
color: "rgba(255, 255, 0, 0.55)",
drag: false,
resize: false,
content: `${(end - endSpeechTime).toFixed(2)}s`,
content: `${latency}s`,
});
changeRegionMouseStyle(latencyRegion, channel);
@@ -353,7 +383,7 @@ export const Player = ({ call }: PlayerProps) => {
if (startPoint) {
const gatherSpans = getSpansByNameRegex(spans, /:gather{/);
gatherSpans.forEach((s) => {
drawSttRegionForSpan(s, startPoint);
drawSttRegionForSpan(s, spans, startPoint);
});
// Trasscription
@@ -363,6 +393,7 @@ export const Player = ({ call }: PlayerProps) => {
const channel = Number(cs.name.split(":")[1]);
drawSttRegionForSpan(
cs,
spans,
startPoint,
channel > 0 ? channel - 1 : channel,
);

View File

@@ -52,6 +52,11 @@ import {
VENDOR_CARTESIA,
VENDOR_VOXIST,
VENDOR_OPENAI,
VENDOR_INWORLD,
VENDOR_DEEPGRAM_FLUX,
VENDOR_RESEMBLE,
VENDOR_HOUNDIFY,
VENDOR_GLADIA,
} from "src/vendor";
import { MSG_REQUIRED_FIELDS } from "src/constants";
import {
@@ -80,9 +85,13 @@ import type {
import { setAccountFilter, setLocation } from "src/store/localStore";
import {
ADDITIONAL_SPEECH_VENDORS,
ASSEMBLYAI_STT_VERSIONS,
DEEPGRAM_STT_ENPOINT,
DEFAULT_ASSEMBLYAI_STT_VERSION,
DEFAULT_CARTESIA_OPTIONS,
DEFAULT_ELEVENLABS_OPTIONS,
DEFAULT_GOOGLE_CUSTOM_VOICE,
DEFAULT_INWORLD_OPTIONS,
DEFAULT_PLAYHT_OPTIONS,
DEFAULT_RIMELABS_OPTIONS,
DEFAULT_VERBIO_MODEL,
@@ -101,6 +110,13 @@ 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");
@@ -114,11 +130,13 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
);
const [region, setRegion] = useState("");
const [apiKey, setApiKey] = useState("");
const [apiUri, setApiUri] = useState("api.elevenlabs.io");
const [userId, setUserId] = useState("");
const [accessKeyId, setAccessKeyId] = useState("");
const [secretAccessKey, setSecretAccessKey] = useState("");
const [clientId, setClientId] = useState("");
const [secretKey, setSecretKey] = useState("");
const [clientKey, setClientKey] = useState("");
const [clientSecret, setClientSecret] = useState("");
const [googleServiceKey, setGoogleServiceKey] =
useState<GoogleServiceKey | null>(null);
@@ -129,6 +147,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
const [ttsModelId, setTtsModelId] = useState("");
const [sttModelId, setSttModelId] = useState("");
const [engineVersion, setEngineVersion] = useState(DEFAULT_VERBIO_MODEL);
const [serviceVersion, setServiceVersion] = useState(
DEFAULT_ASSEMBLYAI_STT_VERSION,
);
const [instanceId, setInstanceId] = useState("");
const [initialCheckCustomTts, setInitialCheckCustomTts] = useState(false);
const [initialCheckCustomStt, setInitialCheckCustomStt] = useState(false);
@@ -198,6 +219,13 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
const [tmpPlayhtTtsUri, setTmpPlayhtTtsUri] = useState("");
const [initialPlayhtOnpremCheck, setInitialPlayhtOnpremCheck] =
useState(false);
const [resembleTtsUri, setResembleTtsUri] = useState("");
const [tmpResembleTtsUri, setTmpResembleTtsUri] = useState("");
const [initialResembleOnpremCheck, setInitialResembleOnpremCheck] =
useState(false);
const [resembleTtsUseTls, setResembleTtsUseTls] = useState(false);
const [tmpResembleTtsUseTls, setTmpResembleTtsUseTls] = useState(false);
const [houndifyServerUri, setHoundifyServerUri] = useState("");
const handleFile = (file: File) => {
const handleError = () => {
setGoogleServiceKey(null);
@@ -233,6 +261,8 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
return DEFAULT_PLAYHT_OPTIONS;
case VENDOR_RIMELABS:
return DEFAULT_RIMELABS_OPTIONS;
case VENDOR_INWORLD:
return DEFAULT_INWORLD_OPTIONS;
case VENDOR_CARTESIA:
return DEFAULT_CARTESIA_OPTIONS;
}
@@ -249,6 +279,8 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
return "https://docs.play.ht/reference/api-generate-tts-audio-stream";
case VENDOR_RIMELABS:
return "https://rimelabs.mintlify.app/api-reference/endpoint/streaming-mp3#variable-parameters";
case VENDOR_INWORLD:
return "https://docs.inworld.ai/api-reference/ttsAPI/texttospeech/synthesize-speech-stream";
case VENDOR_CARTESIA:
return "https://docs.cartesia.ai/api-reference/tts/bytes";
}
@@ -260,7 +292,19 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
switch (vendor) {
case VENDOR_PLAYHT:
return "Voice Engine";
case VENDOR_DEEPGRAM:
return "Model ID";
case VENDOR_CARTESIA:
return "TTS Model ID";
default:
return "Model";
}
};
const getSTTModelLabelByVendor = (vendor: Lowercase<Vendor>) => {
switch (vendor) {
case VENDOR_CARTESIA:
return " STT Model ID";
case VENDOR_DEEPGRAM:
return "Model ID";
default:
@@ -397,6 +441,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
}),
...(vendor === VENDOR_CARTESIA && {
model_id: ttsModelId || null,
stt_model_id: sttModelId || null,
options: options || null,
}),
...(vendor === VENDOR_CUSTOM && {
@@ -414,16 +459,27 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
nuance_tts_uri: onPremNuanceTtsUrl || null,
nuance_stt_uri: onPremNuanceSttUrl || null,
}),
...(vendor === VENDOR_HOUNDIFY && {
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,
}),
...((vendor === VENDOR_ELEVENLABS ||
vendor === VENDOR_WHISPER ||
vendor === VENDOR_INWORLD ||
vendor === VENDOR_RIMELABS) && {
model_id: ttsModelId || null,
}),
...(vendor === VENDOR_ELEVENLABS && {
api_uri: apiUri || null,
}),
...((vendor === VENDOR_ELEVENLABS ||
vendor === VENDOR_PLAYHT ||
vendor === VENDOR_INWORLD ||
vendor === VENDOR_RIMELABS) && {
options: options || null,
}),
@@ -447,9 +503,20 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
...(vendor === VENDOR_VERBIO && {
engine_version: engineVersion,
}),
...(vendor === VENDOR_ASSEMBLYAI && {
service_version: serviceVersion || null,
}),
...(vendor === VENDOR_PLAYHT && {
playht_tts_uri: playhtTtsUri || null,
}),
...(vendor === VENDOR_RESEMBLE && {
resemble_tts_uri: resembleTtsUri || null,
resemble_tts_use_tls: resembleTtsUseTls ? 1 : 0,
}),
...(vendor === VENDOR_GLADIA && {
api_key: apiKey || null,
region: region || null,
}),
};
if (credential && credential.data) {
@@ -496,9 +563,13 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
vendor === VENDOR_ELEVENLABS ||
vendor === VENDOR_PLAYHT ||
vendor === VENDOR_RIMELABS ||
vendor === VENDOR_INWORLD ||
vendor === VENDOR_WHISPER ||
vendor === VENDOR_CARTESIA ||
vendor === VENDOR_OPENAI
vendor === VENDOR_OPENAI ||
vendor === VENDOR_RESEMBLE ||
vendor === VENDOR_DEEPGRAM_FLUX ||
vendor === VENDOR_GLADIA
? apiKey
: null,
}),
@@ -565,6 +636,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
vendor === VENDOR_WHISPER ||
vendor === VENDOR_PLAYHT ||
vendor === VENDOR_RIMELABS ||
vendor === VENDOR_INWORLD ||
vendor === VENDOR_CARTESIA ||
vendor === VENDOR_OPENAI ||
vendor === VENDOR_DEEPGRAM
@@ -599,7 +671,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
useEffect(() => {
const modelId = credential?.data?.model_id || "";
if (ttsModels.length > 0 && !ttsModels.some((m) => m.value === modelId)) {
setTtsModelId(sttModels[0].value);
setTtsModelId(ttsModels[0].value);
} else {
setTtsModelId(modelId);
}
@@ -651,6 +723,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
setApiKey(credential.data.api_key);
}
if (credential.data.api_uri) {
setApiUri(credential.data.api_uri);
}
if (credential.data.region) {
setRegion(credential.data.region);
}
@@ -662,6 +738,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
if (credential.data.client_id) {
setClientId(credential.data.client_id);
}
if (credential.data.client_key) {
setClientKey(credential.data.client_key);
}
if (credential.data.secret) {
setSecretKey(credential.data.secret);
@@ -758,10 +837,15 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
(vendor === VENDOR_OPENAI || vendor === VENDOR_DEEPGRAM)
) {
setSttModelId(credential.data.model_id);
} else if (credential.data.stt_model_id) {
setSttModelId(credential.data.stt_model_id);
}
if (credential?.data?.playht_tts_uri) {
setPlayhtTtsUri(credential.data.playht_tts_uri);
}
if (credential?.data?.resemble_tts_uri) {
setResembleTtsUri(credential.data.resemble_tts_uri);
}
}
if (credential?.data?.options) {
setOptions(credential.data.options);
@@ -776,9 +860,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
setUseCustomVoicesCheck(json.length > 0);
});
}
if (credential?.data?.deepgram_stt_uri) {
setDeepgramSttUri(credential.data.deepgram_stt_uri);
}
setDeepgramSttUri(credential?.data?.deepgram_stt_uri || "");
if (credential?.data?.deepgram_tts_uri) {
setDeepgramTtsUri(credential.data.deepgram_tts_uri);
}
@@ -787,12 +869,21 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
credential?.data?.deepgram_stt_use_tls > 0 ? true : false,
);
}
setInitialDeepgramOnpremCheck(hasValue(credential?.data?.deepgram_stt_uri));
setInitialDeepgramOnpremCheck(
hasValue(credential?.data?.deepgram_stt_uri) &&
!DEEPGRAM_STT_ENPOINT.map((e) => e.value).includes(
credential?.data?.deepgram_stt_uri,
),
);
if (credential?.data?.user_id) {
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);
}
@@ -817,6 +908,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
if (credential?.data?.engine_version) {
setEngineVersion(credential.data.engine_version);
}
if (credential?.data?.service_version) {
setServiceVersion(credential.data.service_version);
}
if (credential?.data?.speechmatics_stt_uri) {
setInitialSpeechMaticsOnpremCheck(
@@ -825,6 +919,15 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
setSpeechmaticsEndpoint(credential.data.speechmatics_stt_uri);
}
setInitialPlayhtOnpremCheck(hasValue(credential?.data?.playht_tts_uri));
setInitialResembleOnpremCheck(hasValue(credential?.data?.resemble_tts_uri));
if (credential?.data?.resemble_tts_use_tls) {
setResembleTtsUseTls(
credential?.data?.resemble_tts_use_tls > 0 ? true : false,
);
setTmpResembleTtsUseTls(
credential?.data?.resemble_tts_use_tls > 0 ? true : false,
);
}
}, [credential]);
const updateCustomVoices = (
@@ -888,6 +991,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
setVendor(e.target.value as Lowercase<Vendor>);
setRegion("");
setApiKey("");
setApiUri(
e.target.value === VENDOR_ELEVENLABS ? "api.elevenlabs.io" : "",
);
setGoogleServiceKey(null);
}}
disabled={credential ? true : false}
@@ -943,7 +1049,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
vendor !== VENDOR_COBALT &&
vendor !== VENDOR_SONIOX &&
vendor !== VENDOR_SPEECHMATICS &&
vendor !== VENDOR_DEEPGRAM_FLUX &&
vendor !== VENDOR_HOUNDIFY &&
vendor !== VENDOR_OPENAI &&
vendor !== VENDOR_GLADIA &&
vendor != VENDOR_CUSTOM && (
<label htmlFor="use_for_tts" className="chk">
<input
@@ -961,7 +1070,8 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
vendor !== VENDOR_WHISPER &&
vendor !== VENDOR_PLAYHT &&
vendor !== VENDOR_RIMELABS &&
vendor !== VENDOR_CARTESIA &&
vendor !== VENDOR_INWORLD &&
vendor !== VENDOR_RESEMBLE &&
vendor !== VENDOR_ELEVENLABS && (
<label htmlFor="use_for_stt" className="chk">
<input
@@ -1341,6 +1451,56 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
)}
</>
)}
{vendor === VENDOR_HOUNDIFY && (
<fieldset>
<label htmlFor="houndify_client_id">
Client ID
{!onPremNuanceSttCheck && !onPremNuanceTtsCheck && <span>*</span>}
</label>
<input
id="houndify_client_id"
required={!onPremNuanceSttCheck && !onPremNuanceTtsCheck}
type="text"
name="houndify_client_id"
placeholder="Client ID"
value={clientId}
onChange={(e) => setClientId(e.target.value)}
disabled={credential ? true : false}
/>
<label htmlFor="houndify_secret">
Client Key
{!onPremNuanceSttCheck && !onPremNuanceTtsCheck && <span>*</span>}
</label>
<Passwd
id="houndify_secret"
required={!onPremNuanceSttCheck && !onPremNuanceTtsCheck}
name="houndify_secret"
placeholder="Client Key"
value={clientKey ? getObscuredSecret(clientKey) : clientKey}
onChange={(e) => setClientKey(e.target.value)}
disabled={credential ? true : false}
/>
<label htmlFor="houndify_user_id">User ID</label>
<input
id="houndify_user_id"
type="text"
name="houndify_user_id"
placeholder="User ID"
value={userId}
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 && (
<>
<fieldset>
@@ -1497,6 +1657,22 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
</fieldset>
</>
)}
{vendor === VENDOR_ASSEMBLYAI && (
<fieldset>
<label htmlFor={`${vendor}_tts_model_id`}>
Service version<span>*</span>
</label>
<Selector
id={"assemblyai_service_version"}
name={"assemblyai_service_version"}
value={serviceVersion}
options={ASSEMBLYAI_STT_VERSIONS}
onChange={(e) => {
setServiceVersion(e.target.value);
}}
/>
</fieldset>
)}
{vendor === VENDOR_AWS && (
<fieldset>
<label htmlFor="vendor">
@@ -1685,16 +1861,98 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
</fieldset>
)}
{vendor === VENDOR_RESEMBLE && (
<fieldset>
<Checkzone
disabled={hasValue(credential)}
hidden
name="use_on-prem_resemble_container"
label="Use on-prem Resemble container"
initialCheck={initialResembleOnpremCheck}
handleChecked={(e) => {
setInitialResembleOnpremCheck(e.target.checked);
if (e.target.checked) {
if (tmpResembleTtsUri) {
setResembleTtsUri(tmpResembleTtsUri);
}
if (tmpResembleTtsUseTls) {
setResembleTtsUseTls(tmpResembleTtsUseTls);
}
} else {
setTmpResembleTtsUri(resembleTtsUri);
setResembleTtsUri("");
setTmpResembleTtsUseTls(resembleTtsUseTls);
setResembleTtsUseTls(false);
}
}}
>
<label htmlFor="resemble_uri_for_tts">
TTS Container URI<span>*</span>
</label>
<input
id="resemble_uri_for_tts"
required
type="text"
name="resemble_uri_for_tts"
placeholder=""
value={resembleTtsUri}
onChange={(e) => setResembleTtsUri(e.target.value)}
/>
<label htmlFor="resemble_stt_use_tls" className="chk">
<input
id="resemble_stt_use_tls"
name="resemble_stt_use_tls"
type="checkbox"
onChange={(e) => setResembleTtsUseTls(e.target.checked)}
defaultChecked={resembleTtsUseTls}
/>
<div>Use TLS</div>
</label>
</Checkzone>
</fieldset>
)}
{vendor === VENDOR_ELEVENLABS && (
<fieldset>
<label htmlFor="elevenlabs_api_uri">
Data residency<span>*</span>
</label>
<Selector
id="elevenlabs_api_uri"
name="elevenlabs_api_uri"
value={apiUri}
options={ELEVENLABS_API_URI_OPTIONS}
onChange={(e) => setApiUri(e.target.value)}
required
/>
<label htmlFor={`${vendor}_apikey`}>
API key<span>*</span>
</label>
<Passwd
id={`${vendor}_apikey`}
required
name={`${vendor}_apikey`}
placeholder="API key"
value={apiKey ? getObscuredSecret(apiKey) : apiKey}
onChange={(e) => setApiKey(e.target.value)}
disabled={credential ? true : false}
/>
</fieldset>
)}
{(vendor === VENDOR_WELLSAID ||
vendor === VENDOR_ASSEMBLYAI ||
vendor === VENDOR_VOXIST ||
vendor == VENDOR_ELEVENLABS ||
vendor === VENDOR_WHISPER ||
vendor === VENDOR_RIMELABS ||
vendor === VENDOR_INWORLD ||
vendor === VENDOR_SONIOX ||
vendor === VENDOR_CARTESIA ||
vendor === VENDOR_OPENAI ||
vendor === VENDOR_SPEECHMATICS) && (
vendor === VENDOR_DEEPGRAM_FLUX ||
vendor === VENDOR_RESEMBLE ||
vendor === VENDOR_SPEECHMATICS ||
vendor === VENDOR_GLADIA) && (
<fieldset>
<label htmlFor={`${vendor}_apikey`}>
API key<span>*</span>
@@ -1710,11 +1968,12 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
/>
</fieldset>
)}
{(vendor == VENDOR_ELEVENLABS ||
vendor == VENDOR_WHISPER ||
vendor === VENDOR_CARTESIA ||
{(vendor === VENDOR_ELEVENLABS ||
vendor === VENDOR_WHISPER ||
vendor === VENDOR_PLAYHT ||
vendor == VENDOR_RIMELABS) &&
vendor === VENDOR_RIMELABS ||
vendor === VENDOR_INWORLD ||
(ttsCheck && vendor === VENDOR_CARTESIA)) &&
ttsModels.length > 0 && (
<fieldset>
<label htmlFor={`${vendor}_tts_model_id`}>
@@ -1731,11 +1990,13 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
/>
</fieldset>
)}
{(vendor == VENDOR_OPENAI || vendor === VENDOR_DEEPGRAM) &&
{(vendor == VENDOR_OPENAI ||
vendor === VENDOR_DEEPGRAM ||
(sttCheck && vendor === VENDOR_CARTESIA)) &&
sttModels.length > 0 && (
<fieldset>
<label htmlFor={`${vendor}_stt_model_id`}>
{getModelLabelByVendor(vendor)}
{getSTTModelLabelByVendor(vendor)}
</label>
<Selector
id={"stt_model_id"}
@@ -1751,7 +2012,8 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
{(vendor === VENDOR_ELEVENLABS ||
vendor === VENDOR_PLAYHT ||
vendor === VENDOR_CARTESIA ||
vendor === VENDOR_RIMELABS) && (
vendor === VENDOR_RIMELABS ||
vendor === VENDOR_INWORLD) && (
<fieldset>
<Checkzone
hidden
@@ -1972,6 +2234,19 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
onChange={(e) => setApiKey(e.target.value)}
disabled={credential ? true : false}
/>
<label htmlFor={`${vendor}_deepgram_stt_enpoint`}>
Deepgram STT Endpoint<span>*</span>
</label>
<Selector
id={"deepgram_stt_enpoint"}
name={"deepgram_stt_enpoint"}
value={deepgramSttUri}
options={DEEPGRAM_STT_ENPOINT}
onChange={(e) => {
setDeepgramSttUri(e.target.value);
setDeepgramSttUseTls(hasValue(e.target.value));
}}
/>
</Checkzone>
<Checkzone
disabled={hasValue(credential)}

View File

@@ -7,12 +7,12 @@ input[type="text"],
input[type="email"],
input[type="number"],
input[type="password"] {
@include ui-mixins.m();
padding: ui-vars.$px01 ui-vars.$px02;
border-radius: ui-vars.$px01;
border: 2px solid ui-vars.$grey;
background-color: ui-vars.$white;
color: inherit;
@include ui-mixins.m();
&:focus {
border-color: ui-vars.$dark;
@@ -110,11 +110,11 @@ fieldset {
}
label {
@include ui-mixins.m();
@include ui-mixins.font-medium();
display: flex;
align-items: center;
flex-wrap: wrap;
@include ui-mixins.font-medium();
@include ui-mixins.m();
span {
color: ui-vars.$jambonz;
@@ -218,7 +218,8 @@ fieldset {
}
}
.gateway {
.gateway,
.gateway-inbound {
padding: ui-vars.$px02;
border-radius: ui-vars.$px01;
border: 2px solid ui-vars.$grey;
@@ -284,6 +285,18 @@ fieldset {
}
}
.gateway-inbound {
> div {
&:nth-child(1) {
grid-template-columns: [col] calc(70% - #{ui-vars.$px02 * 2}) [col] 30%;
@include mixins.small() {
grid-template-columns: [col] 100%;
}
}
}
}
.lcr {
@extend .gateway;

View File

@@ -21,10 +21,10 @@
}
&__row {
@include ui-mixins.m();
display: grid;
padding: ui-vars.$px03;
align-items: center;
@include ui-mixins.m();
@include mixins.small() {
padding: ui-vars.$px02;

View File

@@ -30,11 +30,11 @@
}
&__title {
@include ui-mixins.p();
display: flex;
align-items: center;
grid-gap: ui-vars.$px02;
color: ui-vars.$jambonz;
@include ui-mixins.p();
+ .item__meta {
@include mixins.small() {

View File

@@ -1,3 +1,4 @@
@use "sass:color";
@use "./forms";
// @use "./cards";
@use "./lists";
@@ -68,7 +69,7 @@ details {
&.ok {
color: ui-vars.$teal;
border: 2px solid ui-vars.$teal;
background-color: mix(ui-vars.$white, ui-vars.$teal, 95%);
background-color: color.mix(ui-vars.$white, ui-vars.$teal, 95%);
}
&.not-tested {
@@ -77,8 +78,8 @@ details {
}
summary {
@include ui-mixins.m();
cursor: pointer;
@include ui-mixins.m();
+ * {
margin-top: ui-vars.$px02;
@@ -160,7 +161,6 @@ details {
/** Used for recent-calls */
.pre-grid {
@include mixins.code();
display: grid;
grid-template-columns: auto 1fr;
grid-row-gap: ui-vars.$px01;
@@ -171,6 +171,7 @@ details {
background-color: ui-vars.$dark;
border-radius: ui-vars.$px01;
margin-top: ui-vars.$px02;
@include mixins.code();
}
.pre-grid-white {

28
src/vendor/index.tsx vendored
View File

@@ -12,6 +12,7 @@ export const VENDOR_MICROSOFT = "microsoft";
export const VENDOR_WELLSAID = "wellsaid";
export const VENDOR_NUANCE = "nuance";
export const VENDOR_DEEPGRAM = "deepgram";
export const VENDOR_DEEPGRAM_FLUX = "deepgramflux";
export const VENDOR_IBM = "ibm";
export const VENDOR_NVIDIA = "nvidia";
export const VENDOR_SONIOX = "soniox";
@@ -24,9 +25,13 @@ export const VENDOR_VOXIST = "voxist";
export const VENDOR_WHISPER = "whisper";
export const VENDOR_PLAYHT = "playht";
export const VENDOR_RIMELABS = "rimelabs";
export const VENDOR_INWORLD = "inworld";
export const VENDOR_VERBIO = "verbio";
export const VENDOR_CARTESIA = "cartesia";
export const VENDOR_OPENAI = "openai";
export const VENDOR_RESEMBLE = "resemble";
export const VENDOR_HOUNDIFY = "houndify";
export const VENDOR_GLADIA = "gladia";
export const vendors: VendorOptions[] = [
{
@@ -41,6 +46,10 @@ export const vendors: VendorOptions[] = [
name: "Deepgram",
value: VENDOR_DEEPGRAM,
},
{
name: "Deepgram Flux",
value: VENDOR_DEEPGRAM_FLUX,
},
{
name: "IBM",
value: VENDOR_IBM,
@@ -101,6 +110,10 @@ export const vendors: VendorOptions[] = [
name: "RimeLabs",
value: VENDOR_RIMELABS,
},
{
name: "Inworld",
value: VENDOR_INWORLD,
},
{
name: "Verbio",
value: VENDOR_VERBIO,
@@ -113,6 +126,18 @@ export const vendors: VendorOptions[] = [
name: "OpenAI",
value: VENDOR_OPENAI,
},
{
name: "Resemble",
value: VENDOR_RESEMBLE,
},
{
name: "SoundHound",
value: VENDOR_HOUNDIFY,
},
{
name: "Gladia",
value: VENDOR_GLADIA,
},
].sort((a, b) => a.name.localeCompare(b.name)) as VendorOptions[];
export const AWS_CREDENTIAL_ACCESS_KEY = "access_key";
@@ -145,12 +170,14 @@ export const useRegionVendors = () => {
import("./regions/ms-azure-regions"),
import("./regions/ibm-regions"),
import("./regions/speechmatics-regions"),
import("./regions/gladia-regions"),
]).then(
([
{ default: awsRegions },
{ default: msRegions },
{ default: ibmRegions },
{ default: speechmaticsRegions },
{ default: gladiaRegions },
]) => {
if (!ignore) {
setRegions({
@@ -158,6 +185,7 @@ export const useRegionVendors = () => {
microsoft: msRegions,
ibm: ibmRegions,
speechmatics: speechmaticsRegions,
gladia: gladiaRegions,
});
}
},

14
src/vendor/regions/gladia-regions.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
import type { Region } from "../types";
export const regions: Region[] = [
{
name: "US West",
value: "us-west",
},
{
name: "EU West",
value: "eu-west",
},
];
export default regions;

15
src/vendor/types.ts vendored
View File

@@ -5,6 +5,7 @@ export type Vendor =
| "WellSaid"
| "Nuance"
| "Deepgram"
| "DeepgramFlux"
| "IBM"
| "Nvidia"
| "Soniox"
@@ -17,9 +18,13 @@ export type Vendor =
| "whisper"
| "playht"
| "rimelabs"
| "inworld"
| "verbio"
| "openai"
| "Cartesia";
| "Cartesia"
| "Resemble"
| "Houndify"
| "gladia";
export interface VendorOptions {
name: Vendor;
@@ -31,6 +36,11 @@ export interface LabelOptions {
value: string;
}
export interface JambonzResourceOptions {
name: string;
value: string;
}
export interface Region {
name: string;
value: string;
@@ -76,6 +86,7 @@ export interface RegionVendors {
microsoft: Region[];
ibm: Region[];
speechmatics: Region[];
gladia: Region[];
}
export interface TtsModels {
@@ -96,6 +107,7 @@ export interface RecognizerVendors {
speechmatics: Language[];
cobalt: Language[];
assemblyai: Language[];
deepgramflux: Language[];
}
export interface SynthesisVendors {
@@ -112,6 +124,7 @@ export interface SynthesisVendors {
playht: VoiceLanguage[];
cartesia: VoiceLanguage[];
rimelabs: VoiceLanguage[];
inworld: VoiceLanguage[];
}
export interface MSRawSpeech {

View File

@@ -15,6 +15,15 @@ export default defineConfig(() => {
src: path.resolve(__dirname, "src"),
},
},
// Configure Sass to use the modern API
css: {
preprocessorOptions: {
scss: {
api: "modern-compiler",
},
},
},
};
return config;