Compare commits

...

12 Commits

Author SHA1 Message Date
Sam Machin
94181873f3 Disable password managers on some password forms (#503)
* Update index.tsx

add the data-lpignore attribute to the password component and remove redundant {} on autoComplete

* Update index.tsx

* add other password managers and disable ignore on main login
2025-04-11 08:19:46 -04:00
rammohan-y
3437d7f3d7 Feat/371 view only user checkbox (#478)
* feat/371 added read-only checkbox

* wip

* wip-2

* renamed is_read_only to is_view_only
2025-04-01 09:29:26 -04:00
rammohan-y
38128b1531 fixed error when selecting yesterday option from date filter, also added more options in the filter (#500) 2025-04-01 09:21:03 -04:00
Hoan Luu Huu
f9e4c241f3 support openAi stt (#496)
* support openAi stt

* wip

* wip

* add back model selection to openai
2025-03-28 10:15:27 -04:00
Hoan Luu Huu
c4be87353c fix: application synthsizer configuraiton is shown different than data stored in db when view application. (#493) 2025-03-17 07:24:51 -04:00
Hoan Luu Huu
9c9699ea69 improvements when entering application call hook (#489) 2025-03-10 09:26:27 -04:00
Hoan Luu Huu
e48fce08d4 support showing call cloudwatch logs (#490)
* support showing call cloudwatch logs

* wip

* fix review comments

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip
2025-03-10 08:35:18 -04:00
Hoan Luu Huu
e36b031c76 filter carrier based on account_sid when creating phone number (#492) 2025-03-06 07:41:50 -05:00
Hoan Luu Huu
bf87e4fb80 Feat/gh 482 (#488)
* remove messaging hook from application

* remove messaging hook from application
2025-02-26 19:02:54 -05:00
Hoan Luu Huu
b8140ba0d6 Support voip carrier sip proxy (#484)
* Support voip carrier sip proxy

* fixed review comment
2025-02-17 09:47:02 -05:00
Hoan Luu Huu
9fd847015e rime labs support new voices (#483) 2025-02-07 07:22:45 -05:00
Hoan Luu Huu
c237b7e7f2 support voxist stt (#480) 2025-02-05 08:33:12 -05:00
22 changed files with 547 additions and 130 deletions

6
.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
@@ -27,4 +27,6 @@ VITE_API_BASE_URL=http://127.0.0.1:3000/v1
## Strip publishable key
#VITE_APP_STRIPE_PUBLISHABLE_KEY="pk_test_EChRaX9Tjk8csZZVSeoGqNvu00lsJzjaU1"
## ignore some specific speech vendors, defined by ADDITIONAL_SPEECH_VENDORS constant
# VITE_APP_DISABLE_ADDITIONAL_SPEECH_VENDORS=true
# VITE_APP_DISABLE_ADDITIONAL_SPEECH_VENDORS=true
## AWS region for enabling Recent Call Feature server logs
#VITE_APP_AWS_REGION=us-west-2

50
package-lock.json generated
View File

@@ -2561,30 +2561,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
@@ -3238,6 +3214,31 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/cypress/node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/cypress/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -7205,6 +7206,7 @@
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}

View File

@@ -1,4 +1,3 @@
import { hasValue } from "src/utils";
import type {
CartesiaOptions,
Currency,
@@ -33,6 +32,7 @@ interface JambonzWindowObject {
DEFAULT_SERVICE_PROVIDER_SID: string;
STRIPE_PUBLISHABLE_KEY: string;
DISABLE_ADDITIONAL_SPEECH_VENDORS: string;
AWS_REGION: string;
}
declare global {
@@ -44,9 +44,10 @@ declare global {
/** https://vitejs.dev/guide/env-and-mode.html#env-files */
const CONFIGURED_API_BASE_URL =
window.JAMBONZ?.API_BASE_URL || import.meta.env.VITE_API_BASE_URL;
export const API_BASE_URL = hasValue(CONFIGURED_API_BASE_URL)
? CONFIGURED_API_BASE_URL
: `${window.location.protocol}//${window.location.hostname}/api/v1`;
export const API_BASE_URL =
CONFIGURED_API_BASE_URL && CONFIGURED_API_BASE_URL.length !== 0
? CONFIGURED_API_BASE_URL
: `${window.location.protocol}//${window.location.hostname}/api/v1`;
/** Serves mock API responses from a local dev API server */
export const DEV_BASE_URL = import.meta.env.VITE_DEV_BASE_URL;
@@ -87,6 +88,9 @@ export const DISABLE_ADDITIONAL_SPEECH_VENDORS: boolean =
import.meta.env.VITE_APP_DISABLE_ADDITIONAL_SPEECH_VENDORS || "false",
);
export const AWS_REGION: string =
window.JAMBONZ?.AWS_REGION || import.meta.env.VITE_APP_AWS_REGION;
export const DEFAULT_SERVICE_PROVIDER_SID: string =
window.JAMBONZ?.DEFAULT_SERVICE_PROVIDER_SID ||
import.meta.env.VITE_APP_DEFAULT_SERVICE_PROVIDER_SID;

View File

@@ -823,17 +823,19 @@ export const getRecentCalls = (sid: string, query: Partial<CallQuery>) => {
const qryStr = getQuery<Partial<CallQuery>>(query);
return getFetch<PagedResponse<RecentCall>>(
import.meta.env.DEV
? `${DEV_BASE_URL}/Accounts/${sid}/RecentCalls?${qryStr}`
: `${API_ACCOUNTS}/${sid}/RecentCalls?${qryStr}`,
`${API_ACCOUNTS}/${sid}/RecentCalls?${qryStr}`,
);
};
export const getRecentCall = (sid: string, sipCallId: string) => {
return getFetch<TotalResponse>(
import.meta.env.DEV
? `${DEV_BASE_URL}/Accounts/${sid}/RecentCalls/${sipCallId}`
: `${API_ACCOUNTS}/${sid}/RecentCalls/${sipCallId}`,
`${API_ACCOUNTS}/${sid}/RecentCalls/${sipCallId}`,
);
};
export const getRecentCallLog = (sid: string, callSid: string) => {
return getFetch<string[]>(
`${API_ACCOUNTS}/${sid}/RecentCalls/${callSid}/logs`,
);
};

View File

@@ -143,6 +143,7 @@ export interface User {
name: string;
email: string;
is_active: boolean;
is_view_only: boolean;
force_change: boolean;
account_sid: string | null;
account_name?: string | null;
@@ -174,6 +175,7 @@ export interface UserUpdatePayload {
name: string;
force_change: boolean;
is_active: boolean;
is_view_only: boolean;
service_provider_sid: string | null;
account_sid: string | null;
}
@@ -318,7 +320,6 @@ export interface Application {
app_json: null | string;
call_hook: null | WebHook;
account_sid: null | string;
messaging_hook: null | WebHook;
application_sid: string;
call_status_hook: null | WebHook;
speech_synthesis_voice: null | string;
@@ -481,6 +482,7 @@ export interface Carrier {
smpp_enquire_link_interval: number;
register_status: CarrierRegisterStatus;
dtmf_type: DtmfType;
outbound_sip_proxy: string | null;
}
export interface PredefinedCarrier extends Carrier {
@@ -728,6 +730,7 @@ export interface SpeechSupportedLanguagesAndVoices {
tts: VoiceLanguage[];
stt: Language[];
models: Model[];
sttModels: Model[];
}
export interface ElevenLabsOptions {

View File

@@ -8,6 +8,8 @@ type PasswdProps = JSX.IntrinsicElements["input"] & {
locked?: boolean;
/** This is optional in case an onChange override is necessary... */
setValue?: React.Dispatch<React.SetStateAction<string>>;
/** Whether to ignore password managers */
ignorePasswordManager?: boolean;
};
type PasswdRef = HTMLInputElement;
@@ -22,16 +24,27 @@ export const Passwd = forwardRef<PasswdRef, PasswdProps>(
setValue,
placeholder,
locked = false,
ignorePasswordManager = true,
...restProps
}: PasswdProps,
ref,
) => {
const [reveal, setReveal] = useState(false);
// Create object with conditional password manager attributes
const passwordManagerAttributes = ignorePasswordManager
? {
"data-lpignore": "true",
"data-1p-ignore": "",
"data-form-type": "other",
"data-bwignore": "",
}
: {};
return (
<div className="passwd">
<input
autoComplete={"off"}
autoComplete="off"
ref={ref}
type={reveal ? "text" : "password"}
name={name}
@@ -43,6 +56,7 @@ export const Passwd = forwardRef<PasswdRef, PasswdProps>(
}
}}
{...restProps}
{...passwordManagerAttributes}
/>
{!locked && (
<button

View File

@@ -47,7 +47,12 @@ export const Alerts = () => {
count: Number(perPageFilter),
...(dateFilter === "today"
? { start: dayjs().startOf("date").toISOString() }
: { days: Number(dateFilter) }),
: dateFilter === "yesterday"
? {
start: dayjs().subtract(1, "day").startOf("day").toISOString(),
end: dayjs().subtract(1, "day").endOf("day").toISOString(),
}
: { days: Number(dateFilter) }),
};
getAlerts(accountSid, payload)
@@ -103,7 +108,7 @@ export const Alerts = () => {
id="date_filter"
label="Date"
filter={[dateFilter, setDateFilter]}
options={DATE_SELECTION.slice(0, 2)}
options={DATE_SELECTION}
/>
</section>
<Section {...(hasLength(alerts) && { slim: true })}>

View File

@@ -77,11 +77,6 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
const [tmpStatusWebhook, setTmpStatusWebhook] =
useState<WebHook>(DEFAULT_WEBHOOK);
const [initialStatusWebhook, setInitialStatusWebhook] = useState(false);
const [messageWebhook, setMessageWebhook] =
useState<WebHook>(DEFAULT_WEBHOOK);
const [tmpMessageWebhook, setTmpMessageWebhook] =
useState<WebHook>(DEFAULT_WEBHOOK);
const [initialMessageWebhook, setInitialMessageWebhook] = useState(false);
const [synthVendor, setSynthVendor] =
useState<keyof SynthesisVendors>(VENDOR_GOOGLE);
const [synthLang, setSynthLang] = useState(LANG_EN_US);
@@ -150,16 +145,6 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
initialCheck: initialStatusWebhook,
required: true,
},
{
label: "Messaging",
prefix: "message_webhook",
stateVal: messageWebhook,
tmpStateVal: tmpMessageWebhook,
stateSet: setMessageWebhook,
tmpStateSet: setTmpMessageWebhook,
initialCheck: initialMessageWebhook,
required: false,
},
];
useRedirect<Account>(
@@ -202,7 +187,6 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
app_json: applicationJson || null,
call_hook: callWebhook || null,
account_sid: accountSid || null,
messaging_hook: messageWebhook || null,
call_status_hook: statusWebhook || null,
speech_synthesis_vendor: synthVendor || null,
speech_synthesis_language: synthLang || null,
@@ -471,24 +455,9 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
else setInitialStatusWebhook(false);
}
if (application.data.messaging_hook) {
setMessageWebhook(application.data.messaging_hook);
setTmpMessageWebhook(application.data.messaging_hook);
if (
application.data.messaging_hook.username ||
application.data.messaging_hook.password
)
setInitialMessageWebhook(true);
else setInitialMessageWebhook(false);
}
if (application.data.account_sid)
setAccountSid(application.data.account_sid);
if (application.data.messaging_hook)
setMessageWebhook(application.data.messaging_hook);
if (application.data.speech_synthesis_vendor)
setSynthVendor(
application.data.speech_synthesis_vendor as keyof SynthesisVendors,
@@ -640,7 +609,29 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
webhook.stateSet({
...webhook.stateVal,
url: e.target.value,
...(e.target.value.startsWith("ws") && {
method: "GET",
}),
});
if (
e.target.value.startsWith("ws") &&
webhook.prefix === "call_webhook"
) {
const statusWebhook = webhooks.find(
(w) => w.prefix === "status_webhook",
);
if (
statusWebhook &&
((statusWebhook.stateVal?.url || "").length === 0 ||
statusWebhook.stateVal?.url.startsWith("ws"))
) {
statusWebhook.stateSet({
...statusWebhook.stateVal,
url: e.target.value,
method: "GET",
});
}
}
}}
/>
</div>
@@ -656,6 +647,7 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
method: e.target.value as WebhookMethod,
});
}}
disabled={webhook.stateVal?.url.startsWith("ws")}
options={WEBHOOK_METHODS}
/>
</div>

View File

@@ -31,6 +31,9 @@ import {
VENDOR_SPEECHMATICS,
VENDOR_PLAYHT,
VENDOR_CARTESIA,
VENDOR_VOXIST,
VENDOR_RIMELABS,
VENDOR_OPENAI,
} from "src/vendor";
import {
LabelOptions,
@@ -199,7 +202,7 @@ export const SpeechProviderSelection = ({
setSynthesisVoiceOptions(voicesOpts);
}
if (synthesisGoogleCustomVoiceOptions.length > 0) {
updateTtsVoice(synthesisGoogleCustomVoiceOptions[0].value);
updateTtsVoice(synthLang, synthesisGoogleCustomVoiceOptions[0].value);
}
}
// PlayHT3.0 all voices are listed under english language, all voices can be used for multiple languages
@@ -260,35 +263,35 @@ export const SpeechProviderSelection = ({
(!googleLang ||
!googleLang.voices.find((v) => v.value === synthVoice))
) {
setSynthLang(LANG_EN_US);
updateTtsVoice(LANG_EN_US_STANDARD_C);
updateTtsVoice(LANG_EN_US, 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);
updateTtsVoice(ELEVENLABS_LANG_EN, 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);
updateTtsVoice(LANG_EN_US, newLang!.voices[0].value);
return;
}
if (synthVendor === VENDOR_PLAYHT) {
const newLang = json.tts.find(
(lang) => lang.value === LANG_EN_US || lang.value === "english",
);
setSynthLang(newLang!.value);
updateTtsVoice(newLang!.voices[0].value);
updateTtsVoice(newLang!.value, newLang!.voices[0].value);
return;
}
if (synthVendor === VENDOR_CARTESIA) {
const newLang = json.tts.find((lang) => lang.value === "en");
setSynthLang(newLang!.value);
updateTtsVoice(newLang!.voices[0].value);
updateTtsVoice(newLang!.value, newLang!.voices[0].value);
return;
}
if (synthVendor === VENDOR_RIMELABS) {
const newLang = json.tts.find((lang) => lang.value === "eng");
updateTtsVoice(newLang!.value, newLang!.voices[0].value);
return;
}
/** Google and AWS have different language lists */
@@ -296,14 +299,13 @@ export const SpeechProviderSelection = ({
let newLang = json.tts.find((lang) => lang.value === synthLang);
if (newLang) {
updateTtsVoice(newLang.voices[0].value);
updateTtsVoice(synthLang, newLang.voices[0].value);
return;
}
newLang = json.tts.find((lang) => lang.value === LANG_EN_US);
setSynthLang(LANG_EN_US);
updateTtsVoice(newLang!.voices[0].value);
updateTtsVoice(LANG_EN_US, newLang!.voices[0].value);
}
})
.catch((error) => {
@@ -329,9 +331,10 @@ export const SpeechProviderSelection = ({
}
};
const updateTtsVoice = (value: string) => {
const updateTtsVoice = (language: string, voice: string) => {
if (shouldUpdateTtsVoice.current) {
setSynthVoice(value);
setSynthLang(language);
setSynthVoice(voice);
shouldUpdateTtsVoice.current = false;
}
};
@@ -391,9 +394,11 @@ export const SpeechProviderSelection = ({
options={ttsVendorOptions.filter(
(vendor) =>
vendor.value !== VENDOR_ASSEMBLYAI &&
vendor.value !== VENDOR_VOXIST &&
vendor.value !== VENDOR_SONIOX &&
vendor.value !== VENDOR_SPEECHMATICS &&
vendor.value !== VENDOR_CUSTOM &&
vendor.value !== VENDOR_OPENAI &&
vendor.value !== VENDOR_COBALT,
)}
onChange={(e) => {

View File

@@ -119,6 +119,9 @@ export const CarrierForm = ({
const [diversion, setDiversion] = useState("");
const [initialDiversion, setInitialDiversion] = useState(false);
const [initialSipProxy, setInitialSipProxy] = useState(false);
const [outboundSipProxy, setOutboundSipProxy] = useState("");
const [smppSystemId, setSmppSystemId] = useState("");
const [smppPass, setSmppPass] = useState("");
const [smppInboundSystemId, setSmppInboundSystemId] = useState("");
@@ -142,6 +145,44 @@ export const CarrierForm = ({
const [smppInboundMessage, setSmppInboundMessage] = useState("");
const [smppOutboundMessage, setSmppOutboundMessage] = useState("");
const validateOutboundSipGateway = (gateway: string): boolean => {
/** validate outbound sip gateway that can be
* ip address
dns name
sip(s):ip address
sip(s):dns name
full sip uri
full sips uri
*/
// firstly checkig it's including sip or sips
if (
gateway.includes(":") &&
!gateway.includes("sip:") &&
!gateway.includes("sips:")
) {
return false;
}
if (gateway.includes("sip:") || gateway.includes("sips:")) {
const sipGateway = gateway.trim().split(":");
if (sipGateway.length === 2) {
const sipGatewayType = getIpValidationType(sipGateway[1]);
if (sipGatewayType === INVALID) {
return false;
}
} else {
return false;
}
}
// check IP address or domain name
else {
const sipGatewayType = getIpValidationType(gateway);
if (sipGatewayType === INVALID) {
return false;
}
}
return true;
};
const setCarrierStates = (obj: Carrier) => {
if (obj) {
setIsActive(obj.is_active);
@@ -207,6 +248,13 @@ export const CarrierForm = ({
setInitialDiversion(false);
}
if (obj.outbound_sip_proxy) {
setOutboundSipProxy(obj.outbound_sip_proxy);
setInitialSipProxy(true);
} else {
setInitialSipProxy(false);
}
if (obj.smpp_system_id) {
setSmppSystemId(obj.smpp_system_id);
}
@@ -508,6 +556,14 @@ export const CarrierForm = ({
}
}
if (
isNotBlank(outboundSipProxy) &&
!validateOutboundSipGateway(outboundSipProxy)
) {
toastError("Please provide a valid SIP Proxy domain or IP address.");
return;
}
if (currentServiceProvider) {
const carrierPayload: Partial<Carrier> = {
name: carrierName.trim(),
@@ -531,6 +587,7 @@ export const CarrierForm = ({
smpp_inbound_system_id: smppInboundSystemId.trim() || null,
smpp_inbound_password: smppInboundPass.trim() || null,
dtmf_type: dtmfType,
outbound_sip_proxy: outboundSipProxy.trim().replaceAll(" ", "") || null,
};
if (carrier && carrier.data) {
@@ -971,6 +1028,33 @@ export const CarrierForm = ({
/>
</Checkzone>
</fieldset>
<fieldset>
<Checkzone
hidden
name="outbound_sip_proxy"
label="Outbound SIP Proxy"
initialCheck={initialSipProxy}
handleChecked={(e) => {
if (!e.target.checked) {
setOutboundSipProxy("");
}
}}
>
<MS>
Send all calls to this carrier through an outbound proxy
</MS>
<input
id="outbound_sip_proxy"
name="outbound_sip_proxy"
type="text"
value={outboundSipProxy}
placeholder="Outbound Sip Proxy"
onChange={(e) => {
setOutboundSipProxy(e.target.value);
}}
/>
</Checkzone>
</fieldset>
<fieldset>
<label htmlFor="sip_gateways">
SIP gateways<span>*</span>

View File

@@ -41,12 +41,13 @@ export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
const [accounts] = useServiceProviderData<Account[]>("Accounts");
const [applications] = useServiceProviderData<Application[]>("Applications");
const [phoneNumbers] = useServiceProviderData<PhoneNumber[]>("PhoneNumbers");
const [carriers] = useServiceProviderData<Carrier[]>("VoipCarriers");
const [voipCarriers] = useServiceProviderData<Carrier[]>("VoipCarriers");
const [phoneNumberNum, setPhoneNumberNum] = useState("");
const [accountSid, setAccountSid] = useState("");
const [sipTrunkSid, setSipTrunkSid] = useState("");
const [applicationSid, setApplicationSid] = useState("");
const [message, setMessage] = useState("");
const [carriers, setCarriers] = useState<Carrier[]>(voipCarriers || []);
useRedirect<Account>(
accounts,
@@ -55,7 +56,7 @@ export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
);
useRedirect<Carrier>(
carriers,
voipCarriers,
ROUTE_INTERNAL_CARRIERS,
"You must create a SIP trunk before you can create a phone number.",
);
@@ -138,6 +139,20 @@ export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
}
}, [carriers, sipTrunkSid]);
// Filter carriers based on account_sid
useEffect(() => {
if (voipCarriers) {
setCarriers(
voipCarriers?.filter(
(carrier) =>
!accountSid ||
(carrier.is_active &&
(!carrier.account_sid || carrier.account_sid === accountSid)),
),
);
}
}, [accountSid, voipCarriers]);
return (
<>
<Section slim>
@@ -165,6 +180,12 @@ export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
disabled={phoneNumber ? true : false}
></input>
</fieldset>
<fieldset>
<AccountSelect
accounts={accounts}
account={[accountSid, setAccountSid]}
/>
</fieldset>
<fieldset>
<label htmlFor="sip_trunk">
Carrier <span>*</span>
@@ -188,12 +209,7 @@ export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
disabled={phoneNumber ? true : false}
/>
</fieldset>
<fieldset>
<AccountSelect
accounts={accounts}
account={[accountSid, setAccountSid]}
/>
</fieldset>
<fieldset>
<ApplicationSelect
defaultOption="Choose application"

View File

@@ -0,0 +1,131 @@
import dayjs from "dayjs";
import React, { useEffect, useState } from "react";
import { getRecentCallLog } from "src/api";
import { RecentCall } from "src/api/types";
import { Icons, Spinner } from "src/components";
import { toastError, toastSuccess } from "src/store";
import { hasValue } from "src/utils";
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
type CallSystemLogsProps = {
call: RecentCall;
};
// Helper function to format logs
const formatLog = (log: string): string => {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parsedLog = JSON.parse(log) as any;
const l = {
...parsedLog,
time: dayjs(parsedLog.time).utc().format("YYYY-MM-DD HH:mm:ssZ"),
};
return JSON.stringify(l, null, 2);
} catch {
return log;
}
};
export default function CallSystemLogs({ call }: CallSystemLogsProps) {
const [logs, setLogs] = useState<string[] | null>();
const [loading, setLoading] = useState(false);
const [count, setCount] = useState(0);
useEffect(() => {}, [call]);
const getLogs = () => {
setLoading(true);
setCount((prev) => prev + 1);
if (call && call.account_sid && call.call_sid) {
getRecentCallLog(call.account_sid, call.call_sid)
.then(({ json }) => {
setLogs(json);
})
.catch((err) => {
if (err.status === 404) {
toastError("There is no log for this call");
} else {
toastError(err.msg);
}
})
.finally(() => {
setLoading(false);
});
}
};
const copyToClipboard = () => {
if (!logs) {
return;
}
const textToCopy = logs.map(formatLog).join("\n\n");
navigator.clipboard
.writeText(textToCopy)
.then(() => toastSuccess("Logs copied to clipboard"))
.catch(() => toastError("Failed to copy logs"));
};
const downloadLogs = () => {
if (!logs) {
return;
}
const textToDownload = logs.map(formatLog).join("\n\n");
const blob = new Blob([textToDownload], { type: "text/plain" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `${call.call_sid}.log`;
a.click();
URL.revokeObjectURL(a.href);
};
return (
<>
<>
<div className="log-container">
<div className="log-buttons">
<button
onClick={getLogs}
className="log-retrieve-button"
title="Retrieve Logs"
disabled={loading}
>
<div style={{ display: "flex", gap: "5px" }}>
Retrieve Logs
{loading && <Spinner small />}
</div>
</button>
{hasValue(logs) && logs.length !== 0 && (
<>
<button
onClick={copyToClipboard}
className="log-button"
title="Copy to clipboard"
>
<Icons.Clipboard />
</button>
<button
onClick={downloadLogs}
className="log-button"
title="Download logs"
>
<Icons.Download />
</button>
</>
)}
</div>
<pre className="log-content">
{hasValue(logs) && logs.length !== 0
? logs?.map((log, index) => (
<div key={index}>{formatLog(log)}</div>
))
: count !== 0 && logs === null
? "No logs found"
: ""}
</pre>
</div>
</>
</>
);
}

View File

@@ -8,9 +8,10 @@ import type { RecentCall } from "src/api/types";
import { Tabs, Tab } from "@jambonz/ui-kit";
import CallDetail from "./call-detail";
import CallTracing from "./call-tracing";
import { DISABLE_JAEGER_TRACING } from "src/api/constants";
import { AWS_REGION, DISABLE_JAEGER_TRACING } from "src/api/constants";
import { Player } from "./player";
import "./styles.scss";
import CallSystemLogs from "./call-system-logs";
type DetailsItemProps = {
call: RecentCall;
@@ -78,6 +79,13 @@ export const DetailsItem = ({ call }: DetailsItemProps) => {
<Tab id="tracing" label="Tracing">
{open && <CallTracing call={call} />}
</Tab>
{hasValue(AWS_REGION) ? (
<Tab id="logs" label="Logs">
{open && <CallSystemLogs call={call} />}
</Tab>
) : (
<></>
)}
</Tabs>
)}
{open && (

View File

@@ -85,3 +85,82 @@
margin-top: ui-vars.$px01;
}
}
/* CallSystemLogs.css */
/* Styles for the log container */
.log-container {
border-radius: 8px;
position: relative;
background: #1a1a1a; /* Dark background for the container (optional, if you want the entire container dark) */
color: #ffffff; /* Ensure text is visible on dark background */
}
/* Styles for the log buttons container (optional, if you want to style it separately) */
.log-buttons {
position: absolute;
top: 10px;
right: 25px;
display: flex;
gap: 12px;
}
/* Styles for the log content (pre element) */
.log-content {
margin-top: 16px;
background: #1a1a1a; /* Darker background for the log content */
overflow: auto;
min-height: 250px;
max-height: 800px;
font-family: monospace;
font-size: 14px;
line-height: 1.5;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); /* Slightly darker shadow for contrast */
color: #e0e0e0; /* Light gray text for visibility on dark background */
}
/* Optional: Style for individual log entries (divs within pre) */
.log-content div {
margin-bottom: 10px;
}
/* Styles for log buttons */
.log-button {
padding: 8px;
cursor: pointer;
border: none;
border-radius: 50%;
background: #fff3f6; /* Light gray background for buttons, unchanged */
color: #da1c5c;
transition: transform 0.1s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.log-retrieve-button {
@extend .log-button;
border-radius: 8px;
width: auto;
height: 20px;
}
/* Hover state for buttons */
.log-button:hover {
background: #d5d5d5;
transform: scale(1.05);
}
.log-fetch-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%; /* Adjust based on your layout */
gap: 10px;
padding: 20px;
}

View File

@@ -50,6 +50,8 @@ import {
VENDOR_VERBIO,
VENDOR_SPEECHMATICS,
VENDOR_CARTESIA,
VENDOR_VOXIST,
VENDOR_OPENAI,
} from "src/vendor";
import { MSG_REQUIRED_FIELDS } from "src/constants";
import {
@@ -123,6 +125,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
const [ttsRegion, setTtsRegion] = useState("");
const [ttsApiKey, setTtsApiKey] = useState("");
const [ttsModelId, setTtsModelId] = useState("");
const [sttModelId, setSttModelId] = useState("");
const [engineVersion, setEngineVersion] = useState(DEFAULT_VERBIO_MODEL);
const [instanceId, setInstanceId] = useState("");
const [initialCheckCustomTts, setInitialCheckCustomTts] = useState(false);
@@ -166,6 +169,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
const [customVoices, setCustomVoices] = useState<GoogleCustomVoice[]>([]);
const [customVoicesMessage, setCustomVoicesMessage] = useState("");
const [ttsModels, setTtsModels] = useState<Model[]>([]);
const [sttModels, setSttModels] = useState<Model[]>([]);
const [optionsInitialChecked, setOptionsInitialChecked] = useState(false);
const [options, setOptions] = useState("");
const [tmpOptions, setTmpOptions] = useState("");
@@ -247,6 +251,17 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
return "";
};
const getModelLabelByVendor = (vendor: Lowercase<Vendor>) => {
switch (vendor) {
case VENDOR_PLAYHT:
return "Voice Engine";
case VENDOR_CARTESIA:
return "Model ID";
default:
return "Model";
}
};
const handlePutGoogleCustomVoices = () => {
if (!credential || !credential.data) {
return;
@@ -410,6 +425,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
ttsModelId && {
voice_engine: ttsModelId,
}),
...(vendor === VENDOR_OPENAI &&
sttModelId && {
model_id: sttModelId,
}),
...(vendor === VENDOR_DEEPGRAM && {
deepgram_stt_uri: deepgramSttUri || null,
deepgram_tts_uri: deepgramTtsUri || null,
@@ -461,13 +480,15 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
vendor === VENDOR_WELLSAID ||
vendor === VENDOR_DEEPGRAM ||
vendor === VENDOR_ASSEMBLYAI ||
vendor === VENDOR_VOXIST ||
vendor === VENDOR_SONIOX ||
vendor === VENDOR_SPEECHMATICS ||
vendor === VENDOR_ELEVENLABS ||
vendor === VENDOR_PLAYHT ||
vendor === VENDOR_RIMELABS ||
vendor === VENDOR_WHISPER ||
vendor === VENDOR_CARTESIA
vendor === VENDOR_CARTESIA ||
vendor === VENDOR_OPENAI
? apiKey
: null,
}),
@@ -534,7 +555,8 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
vendor === VENDOR_WHISPER ||
vendor === VENDOR_PLAYHT ||
vendor === VENDOR_RIMELABS ||
vendor === VENDOR_CARTESIA
vendor === VENDOR_CARTESIA ||
vendor === VENDOR_OPENAI
) {
getSpeechSupportedLanguagesAndVoices(
currentServiceProvider?.service_provider_sid,
@@ -551,6 +573,15 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
setTtsModelId(json.models[0].value);
}
}
if (json.sttModels) {
setSttModels(json.sttModels);
if (
json.sttModels.length > 0 &&
!json.sttModels.some((m) => m.value === sttModelId)
) {
setSttModelId(json.sttModels[0].value);
}
}
});
} else {
setTtsModels([]);
@@ -705,6 +736,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
if (credential.data.model_id) {
setTtsModelId(credential.data.model_id);
}
if (credential.data.model_id && vendor === VENDOR_OPENAI) {
setSttModelId(credential.data.model_id);
}
}
if (credential?.data?.options) {
setOptions(credential.data.options);
@@ -881,9 +915,11 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
{vendor && (
<fieldset>
{vendor !== VENDOR_ASSEMBLYAI &&
vendor !== VENDOR_VOXIST &&
vendor !== VENDOR_COBALT &&
vendor !== VENDOR_SONIOX &&
vendor !== VENDOR_SPEECHMATICS &&
vendor !== VENDOR_OPENAI &&
vendor != VENDOR_CUSTOM && (
<label htmlFor="use_for_tts" className="chk">
<input
@@ -1512,12 +1548,14 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
{(vendor === VENDOR_WELLSAID ||
vendor === VENDOR_ASSEMBLYAI ||
vendor === VENDOR_VOXIST ||
vendor == VENDOR_ELEVENLABS ||
vendor === VENDOR_WHISPER ||
vendor === VENDOR_PLAYHT ||
vendor === VENDOR_RIMELABS ||
vendor === VENDOR_SONIOX ||
vendor === VENDOR_CARTESIA ||
vendor === VENDOR_OPENAI ||
vendor === VENDOR_SPEECHMATICS) && (
<fieldset>
{vendor === VENDOR_PLAYHT && (
@@ -1553,40 +1591,16 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
/>
</fieldset>
)}
{vendor === VENDOR_PLAYHT && ttsModels.length > 0 && (
<fieldset>
<label htmlFor={`${vendor}_tts_model_id`}>Voice engine</label>
<Selector
id={"tts_model_id"}
name={"tts_model_id"}
value={ttsModelId}
options={ttsModels}
onChange={(e) => {
setTtsModelId(e.target.value);
}}
/>
</fieldset>
)}
{vendor === VENDOR_CARTESIA && ttsModels.length > 0 && (
<fieldset>
<label htmlFor={`${vendor}_tts_model_id`}>Model Id</label>
<Selector
id={"tts_model_id"}
name={"tts_model_id"}
value={ttsModelId}
options={ttsModels}
onChange={(e) => {
setTtsModelId(e.target.value);
}}
/>
</fieldset>
)}
{(vendor == VENDOR_ELEVENLABS ||
vendor == VENDOR_WHISPER ||
vendor === VENDOR_CARTESIA ||
vendor === VENDOR_PLAYHT ||
vendor == VENDOR_RIMELABS) &&
ttsModels.length > 0 && (
<fieldset>
<label htmlFor={`${vendor}_tts_model_id`}>Model</label>
<label htmlFor={`${vendor}_tts_model_id`}>
{getModelLabelByVendor(vendor)}
</label>
<Selector
id={"tts_model_id"}
name={"tts_model_id"}
@@ -1598,6 +1612,22 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
/>
</fieldset>
)}
{vendor == VENDOR_OPENAI && sttModels.length > 0 && (
<fieldset>
<label htmlFor={`${vendor}_stt_model_id`}>
{getModelLabelByVendor(vendor)}
</label>
<Selector
id={"stt_model_id"}
name={"stt_model_id"}
value={sttModelId}
options={sttModels}
onChange={(e) => {
setSttModelId(e.target.value);
}}
/>
</fieldset>
)}
{(vendor === VENDOR_ELEVENLABS ||
vendor === VENDOR_PLAYHT ||
vendor === VENDOR_CARTESIA ||

View File

@@ -61,6 +61,7 @@ export const UserForm = ({ user }: UserFormProps) => {
const [forceChange, setForceChange] = useState(true);
const [modal, setModal] = useState(false);
const [accountSid, setAccountSid] = useState("");
const [isViewOnly, setIsViewOnly] = useState(false);
const handleCancel = () => {
setModal(false);
@@ -115,6 +116,7 @@ export const UserForm = ({ user }: UserFormProps) => {
initial_password: initialPassword,
force_change: forceChange,
is_active: isActive,
is_view_only: isViewOnly,
service_provider_sid:
scope === USER_ADMIN && currentUser?.scope === USER_ADMIN
? null
@@ -145,6 +147,7 @@ export const UserForm = ({ user }: UserFormProps) => {
initial_password: initialPassword || null,
force_change: forceChange,
is_active: isActive,
is_view_only: isViewOnly,
service_provider_sid:
scope === USER_ADMIN && currentUser?.scope === USER_ADMIN
? null
@@ -172,6 +175,7 @@ export const UserForm = ({ user }: UserFormProps) => {
setName(user.data.name);
setForceChange(!!user.data.force_change);
setIsActive(!!user.data.is_active);
setIsViewOnly(!!user.data.is_view_only);
setEmail(user.data.email);
setScope(getUserScope(user.data));
if (user.data.account_sid) {
@@ -253,6 +257,16 @@ export const UserForm = ({ user }: UserFormProps) => {
/>
<div>User is active</div>
</label>
<label htmlFor="is_view_only" className="chk">
<input
id="is_view_only"
name="is_view_only"
type="checkbox"
checked={isViewOnly}
onChange={(e) => setIsViewOnly(e.target.checked)}
/>
<div>View-only User</div>
</label>
</fieldset>
)}
<fieldset>
@@ -283,6 +297,20 @@ export const UserForm = ({ user }: UserFormProps) => {
onChange={(e) => setEmail(e.target.value)}
/>
</fieldset>
{!user && (
<fieldset>
<label htmlFor="is_view_only" className="chk">
<input
id="is_view_only"
name="is_view_only"
type="checkbox"
checked={isViewOnly}
onChange={(e) => setIsViewOnly(e.target.checked)}
/>
<div>View-only User</div>
</label>
</fieldset>
)}
<fieldset>
<label htmlFor="initial_password">
Temporary password

View File

@@ -99,6 +99,7 @@ export const Login = () => {
value={password}
placeholder="Password"
setValue={setPassword}
ignorePasswordManager={false}
/>
{message && <Message message={message} />}
<Button type="submit">Log in</Button>

View File

@@ -86,7 +86,6 @@ export const parseJwt = (token: string) => {
})
.join(""),
);
return JSON.parse(jsonPayload);
};
@@ -107,7 +106,6 @@ export const useProvideAuth = (): AuthStateContext => {
token = response.json.token;
setToken(token);
userData = parseJwt(token);
if (ENABLE_HOSTED_SYSTEM) {
getMe()
.then(({ json }) => {

View File

@@ -44,7 +44,6 @@ const reducer: React.Reducer<State, Action<keyof State>> = (state, action) => {
return serviceProvidersAction(state, action);
case "currentServiceProvider":
return currentServiceProviderAction(state, action);
default:
throw new Error();
}

View File

@@ -26,6 +26,7 @@ export enum Scope {
export interface UserData extends UserJWT {
access: Scope;
read_only_feature: boolean;
}
export interface State {

10
src/vendor/index.tsx vendored
View File

@@ -20,11 +20,13 @@ export const VENDOR_CUSTOM = "custom";
export const VENDOR_COBALT = "cobalt";
export const VENDOR_ELEVENLABS = "elevenlabs";
export const VENDOR_ASSEMBLYAI = "assemblyai";
export const VENDOR_VOXIST = "voxist";
export const VENDOR_WHISPER = "whisper";
export const VENDOR_PLAYHT = "playht";
export const VENDOR_RIMELABS = "rimelabs";
export const VENDOR_VERBIO = "verbio";
export const VENDOR_CARTESIA = "cartesia";
export const VENDOR_OPENAI = "openai";
export const vendors: VendorOptions[] = [
{
@@ -83,6 +85,10 @@ export const vendors: VendorOptions[] = [
name: "AssemblyAI",
value: VENDOR_ASSEMBLYAI,
},
{
name: "Voxist",
value: VENDOR_VOXIST,
},
{
name: "Whisper",
value: VENDOR_WHISPER,
@@ -103,6 +109,10 @@ export const vendors: VendorOptions[] = [
name: "Cartesia",
value: VENDOR_CARTESIA,
},
{
name: "OpenAI",
value: VENDOR_OPENAI,
},
].sort((a, b) => a.name.localeCompare(b.name)) as VendorOptions[];
export const AWS_CREDENTIAL_ACCESS_KEY = "access_key";

3
src/vendor/types.ts vendored
View File

@@ -13,10 +13,12 @@ export type Vendor =
| "Custom"
| "ElevenLabs"
| "assemblyai"
| "voxist"
| "whisper"
| "playht"
| "rimelabs"
| "verbio"
| "openai"
| "Cartesia";
export interface VendorOptions {
@@ -109,6 +111,7 @@ export interface SynthesisVendors {
deepgram: VoiceLanguage[];
playht: VoiceLanguage[];
cartesia: VoiceLanguage[];
rimelabs: VoiceLanguage[];
}
export interface MSRawSpeech {