@iblai/iblai-js 2.5.6 → 2.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -65112,6 +65112,7 @@ var tabsUserstab$3 = {
|
|
|
65112
65112
|
columnStatus: "Status",
|
|
65113
65113
|
roleAdmin: "Admin",
|
|
65114
65114
|
roleUser: "User",
|
|
65115
|
+
selectRoleAriaLabel: "Select role for {name}",
|
|
65115
65116
|
selectPoliciesAriaLabel: "Select policies for {name}",
|
|
65116
65117
|
allPoliciesSelected: "All ({count}) selected",
|
|
65117
65118
|
nPoliciesSelected: "{count} selected",
|
|
@@ -65343,24 +65344,29 @@ var voiceTabLabels$3 = {
|
|
|
65343
65344
|
title: "Agent Voice",
|
|
65344
65345
|
description: "The voice your agent uses to read out replies in chat.",
|
|
65345
65346
|
providerLabel: "Voice source",
|
|
65346
|
-
providerTooltip: "Browser
|
|
65347
|
+
providerTooltip: "Browser and ibl.ai speak on the listener's own device. OpenAI and Google use a custom voice you pick below.",
|
|
65347
65348
|
providerOptions: {
|
|
65348
65349
|
browser: {
|
|
65349
65350
|
label: "Browser",
|
|
65350
65351
|
description: "Speak through the user's own device. No setup needed."
|
|
65351
65352
|
},
|
|
65353
|
+
google: {
|
|
65354
|
+
label: "Google",
|
|
65355
|
+
description: "High-quality voices from Google."
|
|
65356
|
+
},
|
|
65357
|
+
iblai: {
|
|
65358
|
+
label: "ibl.ai",
|
|
65359
|
+
description: "High-quality voices from ibl.ai"
|
|
65360
|
+
},
|
|
65352
65361
|
openai: {
|
|
65353
65362
|
label: "OpenAI",
|
|
65354
65363
|
description: "High-quality voices from OpenAI."
|
|
65355
|
-
},
|
|
65356
|
-
google: {
|
|
65357
|
-
label: "Google",
|
|
65358
|
-
description: "Natural-sounding voices from Google."
|
|
65359
65364
|
}
|
|
65360
65365
|
},
|
|
65361
65366
|
voicePickerLabel: {
|
|
65362
65367
|
openai: "OpenAI voice",
|
|
65363
|
-
google: "Google voice"
|
|
65368
|
+
google: "Google voice",
|
|
65369
|
+
iblai: "ibl.ai voice"
|
|
65364
65370
|
},
|
|
65365
65371
|
searchPlaceholder: "Search voices…",
|
|
65366
65372
|
emptyState: "No voices match your search.",
|
|
@@ -72156,6 +72162,7 @@ var tabsUserstab$2 = {
|
|
|
72156
72162
|
columnStatus: "Statut",
|
|
72157
72163
|
roleAdmin: "Admin",
|
|
72158
72164
|
roleUser: "Utilisateur",
|
|
72165
|
+
selectRoleAriaLabel: "Sélectionner le rôle pour {name}",
|
|
72159
72166
|
selectPoliciesAriaLabel: "Sélectionner les politiques pour {name}",
|
|
72160
72167
|
allPoliciesSelected: "Tout ({count}) sélectionné",
|
|
72161
72168
|
nPoliciesSelected: "{count} sélectionné(s)",
|
|
@@ -72387,24 +72394,29 @@ var voiceTabLabels$2 = {
|
|
|
72387
72394
|
title: "Voix de l'agent",
|
|
72388
72395
|
description: "La voix utilisée par votre agent pour lire les réponses dans le chat.",
|
|
72389
72396
|
providerLabel: "Source de la voix",
|
|
72390
|
-
providerTooltip: "Le navigateur
|
|
72397
|
+
providerTooltip: "Le navigateur et ibl.ai parlent sur l'appareil de l'auditeur. OpenAI et Google utilisent une voix personnalisée que vous choisissez ci-dessous.",
|
|
72391
72398
|
providerOptions: {
|
|
72392
72399
|
browser: {
|
|
72393
72400
|
label: "Navigateur",
|
|
72394
72401
|
description: "Parle via l'appareil de l'utilisateur. Aucune configuration requise."
|
|
72395
72402
|
},
|
|
72403
|
+
google: {
|
|
72404
|
+
label: "Google",
|
|
72405
|
+
description: "Voix de haute qualité de Google."
|
|
72406
|
+
},
|
|
72407
|
+
iblai: {
|
|
72408
|
+
label: "ibl.ai",
|
|
72409
|
+
description: "Voix de haute qualité d'ibl.ai"
|
|
72410
|
+
},
|
|
72396
72411
|
openai: {
|
|
72397
72412
|
label: "OpenAI",
|
|
72398
72413
|
description: "Voix de haute qualité d'OpenAI."
|
|
72399
|
-
},
|
|
72400
|
-
google: {
|
|
72401
|
-
label: "Google",
|
|
72402
|
-
description: "Voix naturelles de Google."
|
|
72403
72414
|
}
|
|
72404
72415
|
},
|
|
72405
72416
|
voicePickerLabel: {
|
|
72406
72417
|
openai: "Voix OpenAI",
|
|
72407
|
-
google: "Voix Google"
|
|
72418
|
+
google: "Voix Google",
|
|
72419
|
+
iblai: "Voix ibl.ai"
|
|
72408
72420
|
},
|
|
72409
72421
|
searchPlaceholder: "Rechercher des voix…",
|
|
72410
72422
|
emptyState: "Aucune voix ne correspond à votre recherche.",
|
|
@@ -79034,6 +79046,7 @@ var tabsUserstab$1 = {
|
|
|
79034
79046
|
columnStatus: "Estado",
|
|
79035
79047
|
roleAdmin: "Admin",
|
|
79036
79048
|
roleUser: "Usuario",
|
|
79049
|
+
selectRoleAriaLabel: "Seleccionar rol para {name}",
|
|
79037
79050
|
selectPoliciesAriaLabel: "Seleccionar políticas para {name}",
|
|
79038
79051
|
allPoliciesSelected: "Todas ({count}) seleccionadas",
|
|
79039
79052
|
nPoliciesSelected: "{count} seleccionada(s)",
|
|
@@ -79265,24 +79278,29 @@ var voiceTabLabels$1 = {
|
|
|
79265
79278
|
title: "Voz del agente",
|
|
79266
79279
|
description: "La voz que usa tu agente para leer las respuestas en el chat.",
|
|
79267
79280
|
providerLabel: "Fuente de voz",
|
|
79268
|
-
providerTooltip: "El navegador
|
|
79281
|
+
providerTooltip: "El navegador e ibl.ai hablan en el dispositivo del oyente. OpenAI y Google usan una voz personalizada que eliges a continuación.",
|
|
79269
79282
|
providerOptions: {
|
|
79270
79283
|
browser: {
|
|
79271
79284
|
label: "Navegador",
|
|
79272
79285
|
description: "Habla a través del dispositivo del usuario. No requiere configuración."
|
|
79273
79286
|
},
|
|
79287
|
+
google: {
|
|
79288
|
+
label: "Google",
|
|
79289
|
+
description: "Voces de alta calidad de Google."
|
|
79290
|
+
},
|
|
79291
|
+
iblai: {
|
|
79292
|
+
label: "ibl.ai",
|
|
79293
|
+
description: "Voces de alta calidad de ibl.ai"
|
|
79294
|
+
},
|
|
79274
79295
|
openai: {
|
|
79275
79296
|
label: "OpenAI",
|
|
79276
79297
|
description: "Voces de alta calidad de OpenAI."
|
|
79277
|
-
},
|
|
79278
|
-
google: {
|
|
79279
|
-
label: "Google",
|
|
79280
|
-
description: "Voces naturales de Google."
|
|
79281
79298
|
}
|
|
79282
79299
|
},
|
|
79283
79300
|
voicePickerLabel: {
|
|
79284
79301
|
openai: "Voz de OpenAI",
|
|
79285
|
-
google: "Voz de Google"
|
|
79302
|
+
google: "Voz de Google",
|
|
79303
|
+
iblai: "Voz de ibl.ai"
|
|
79286
79304
|
},
|
|
79287
79305
|
searchPlaceholder: "Buscar voces…",
|
|
79288
79306
|
emptyState: "Ninguna voz coincide con tu búsqueda.",
|
|
@@ -85912,6 +85930,7 @@ var tabsUserstab = {
|
|
|
85912
85930
|
columnStatus: "状态",
|
|
85913
85931
|
roleAdmin: "管理员",
|
|
85914
85932
|
roleUser: "用户",
|
|
85933
|
+
selectRoleAriaLabel: "为 {name} 选择角色",
|
|
85915
85934
|
selectPoliciesAriaLabel: "为 {name} 选择策略",
|
|
85916
85935
|
allPoliciesSelected: "全部({count})已选",
|
|
85917
85936
|
nPoliciesSelected: "已选 {count} 个",
|
|
@@ -86143,24 +86162,29 @@ var voiceTabLabels = {
|
|
|
86143
86162
|
title: "智能体声音",
|
|
86144
86163
|
description: "您的智能体在聊天中朗读回复时使用的声音。",
|
|
86145
86164
|
providerLabel: "声音来源",
|
|
86146
|
-
providerTooltip: "
|
|
86165
|
+
providerTooltip: "浏览器和 ibl.ai 在听者自己的设备上发声。OpenAI 和 Google 使用您在下方选择的自定义声音。",
|
|
86147
86166
|
providerOptions: {
|
|
86148
86167
|
browser: {
|
|
86149
86168
|
label: "浏览器",
|
|
86150
86169
|
description: "通过用户自己的设备发声,无需任何设置。"
|
|
86151
86170
|
},
|
|
86171
|
+
google: {
|
|
86172
|
+
label: "Google",
|
|
86173
|
+
description: "来自 Google 的高质量声音。"
|
|
86174
|
+
},
|
|
86175
|
+
iblai: {
|
|
86176
|
+
label: "ibl.ai",
|
|
86177
|
+
description: "来自 ibl.ai 的高质量声音"
|
|
86178
|
+
},
|
|
86152
86179
|
openai: {
|
|
86153
86180
|
label: "OpenAI",
|
|
86154
86181
|
description: "来自 OpenAI 的高质量声音。"
|
|
86155
|
-
},
|
|
86156
|
-
google: {
|
|
86157
|
-
label: "Google",
|
|
86158
|
-
description: "来自 Google 的自然声音。"
|
|
86159
86182
|
}
|
|
86160
86183
|
},
|
|
86161
86184
|
voicePickerLabel: {
|
|
86162
86185
|
openai: "OpenAI 声音",
|
|
86163
|
-
google: "Google 声音"
|
|
86186
|
+
google: "Google 声音",
|
|
86187
|
+
iblai: "ibl.ai 语音"
|
|
86164
86188
|
},
|
|
86165
86189
|
searchPlaceholder: "搜索声音…",
|
|
86166
86190
|
emptyState: "没有与您搜索匹配的声音。",
|
|
@@ -147875,6 +147899,7 @@ function UsersTab({ tenant, onInviteClick, currentSpa, showGradebookTab = false,
|
|
|
147875
147899
|
const [updateUserStatus] = useUpdateUserStatusMutation();
|
|
147876
147900
|
const [updatePlatformUserRoleWithPolicies] = useUpdatePlatformUserRoleWithPoliciesMutation();
|
|
147877
147901
|
const [updatingUserIds, setUpdatingUserIds] = useState(new Set());
|
|
147902
|
+
const [pendingRoles, setPendingRoles] = useState({});
|
|
147878
147903
|
const [openPolicyPopover, setOpenPolicyPopover] = useState(null);
|
|
147879
147904
|
const [selectedUser, setSelectedUser] = useState(null);
|
|
147880
147905
|
const { currentTenant } = useCurrentTenant();
|
|
@@ -147927,18 +147952,19 @@ function UsersTab({ tenant, onInviteClick, currentSpa, showGradebookTab = false,
|
|
|
147927
147952
|
if (!isWatcher)
|
|
147928
147953
|
await refetchPlatformUsers();
|
|
147929
147954
|
};
|
|
147930
|
-
const handleUserRoleChange = async (user) => {
|
|
147955
|
+
const handleUserRoleChange = async (user, nextRole) => {
|
|
147931
147956
|
const userId = user === null || user === void 0 ? void 0 : user.user_id;
|
|
147932
147957
|
if (!userId)
|
|
147933
147958
|
return;
|
|
147934
147959
|
// Add to updating state
|
|
147935
147960
|
setUpdatingUserIds((prev) => new Set(prev).add(userId));
|
|
147961
|
+
setPendingRoles((prev) => ({ ...prev, [userId]: nextRole }));
|
|
147936
147962
|
try {
|
|
147937
147963
|
await updateUserRole({
|
|
147938
147964
|
username: user === null || user === void 0 ? void 0 : user.username,
|
|
147939
147965
|
org: user === null || user === void 0 ? void 0 : user.platform_org,
|
|
147940
147966
|
role: 'org-instructor',
|
|
147941
|
-
active:
|
|
147967
|
+
active: nextRole === 'admin',
|
|
147942
147968
|
}).unwrap();
|
|
147943
147969
|
// Only clear policies if role change succeeded
|
|
147944
147970
|
try {
|
|
@@ -147960,6 +147986,7 @@ function UsersTab({ tenant, onInviteClick, currentSpa, showGradebookTab = false,
|
|
|
147960
147986
|
toast.success(t('tabsUserstab.roleUpdatedSuccess'));
|
|
147961
147987
|
// Manually invalidate the platformUsers query since it's in a different API slice
|
|
147962
147988
|
dispatch(platformApiSlice.util.invalidateTags([featureTags.PLATFORM_USERS]));
|
|
147989
|
+
await refetchPlatformUsers();
|
|
147963
147990
|
}
|
|
147964
147991
|
catch (error) {
|
|
147965
147992
|
toast.error(t('tabsUserstab.roleUpdateFailed'));
|
|
@@ -147970,6 +147997,10 @@ function UsersTab({ tenant, onInviteClick, currentSpa, showGradebookTab = false,
|
|
|
147970
147997
|
newSet.delete(userId);
|
|
147971
147998
|
return newSet;
|
|
147972
147999
|
});
|
|
148000
|
+
setPendingRoles((prev) => {
|
|
148001
|
+
const { [userId]: _removed, ...rest } = prev;
|
|
148002
|
+
return rest;
|
|
148003
|
+
});
|
|
147973
148004
|
}
|
|
147974
148005
|
};
|
|
147975
148006
|
const handleUserStatusChange = async (user, newStatus) => {
|
|
@@ -148070,6 +148101,7 @@ function UsersTab({ tenant, onInviteClick, currentSpa, showGradebookTab = false,
|
|
|
148070
148101
|
const totalCount = (_b = (isWatcher ? accessibleUsers === null || accessibleUsers === void 0 ? void 0 : accessibleUsers.count : users === null || users === void 0 ? void 0 : users.count)) !== null && _b !== void 0 ? _b : 0;
|
|
148071
148102
|
const totalPages = Math.ceil(totalCount / PAGE_SIZE$5);
|
|
148072
148103
|
return (jsxs("div", { className: "mt-4", children: [jsxs("div", { className: "flex justify-between items-center gap-1", children: [jsxs("div", { className: "relative mb-6 max-w-sm", children: [jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" }), jsx(Input, { type: "text", value: searchQuery, onChange: (event) => setQuerySearch(event.target.value), placeholder: t('tabsUserstab.searchPlaceholder'), className: "pl-10 pr-10 focus:ring-blue-500 focus:border-blue-500 h-[35px]", "aria-label": t('tabsUserstab.searchAriaLabel') }), searchQuery.length > 0 && (searchQuery !== debouncedSearchQuery || isLoading) && (jsx(LoaderCircle, { "data-testid": "search-spinner", className: "absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 animate-spin", "aria-hidden": "true" }))] }), !isWatcher && (jsx("div", { className: "mb-6", children: jsx(Button$1, { className: "bg-gradient-to-r from-[#2563EB] to-[#93C5FD] hover:opacity-90 text-white h-[35px]", onClick: onInviteClick, children: t('tabsUserstab.inviteButton') }) }))] }), isLoading ? (jsx("div", { className: "flex justify-center items-center h-64", children: jsx(Spinner, {}) })) : (jsxs(Fragment$1, { children: [jsx("div", { className: "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-x-auto", children: jsxs("table", { className: "w-full text-sm", children: [jsx("thead", { className: "bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600", children: jsxs("tr", { children: [jsx("th", { className: `px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400 ${isWatcher ? 'w-full' : 'w-[41%]'}`, children: t('tabsUserstab.columnEmail') }), !isWatcher && (jsxs(Fragment$1, { children: [jsx("th", { className: "px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400 w-[16%]", children: t('tabsUserstab.columnRole') }), jsx("th", { className: "px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400 w-[25%]", children: t('tabsUserstab.columnPolicies') }), jsx("th", { className: "px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400 w-[18%]", children: t('tabsUserstab.columnStatus') })] }))] }) }), jsxs("tbody", { className: "divide-y divide-gray-200 dark:divide-gray-600", children: [rows.map((row) => {
|
|
148104
|
+
var _a;
|
|
148073
148105
|
// Only set on the admin path — see {@link UserRow}.
|
|
148074
148106
|
const user = row.managed;
|
|
148075
148107
|
return (jsxs("tr", { onClick: () => setSelectedUser(row), className: "cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors", children: [jsx("td", { "aria-label": 'user-management-email-column', className: "px-6 py-4 text-gray-900 dark:text-gray-100 text-sm max-w-0", children: jsx("button", { type: "button", onClick: (e) => {
|
|
@@ -148077,7 +148109,9 @@ function UsersTab({ tenant, onInviteClick, currentSpa, showGradebookTab = false,
|
|
|
148077
148109
|
setSelectedUser(row);
|
|
148078
148110
|
}, className: "block max-w-full truncate text-left hover:text-blue-600 hover:underline focus:outline-none focus:text-blue-600", "aria-label": t('tabsUserstab.viewProfileAriaLabel', {
|
|
148079
148111
|
name: row.name || row.email,
|
|
148080
|
-
}), children: row.email }) }), user && (jsxs(Fragment$1, { children: [jsx("td", { className: "px-6 py-4", onClick: (e) => e.stopPropagation(), children: jsxs(Select$1, { value:
|
|
148112
|
+
}), children: row.email }) }), user && (jsxs(Fragment$1, { children: [jsx("td", { className: "px-6 py-4", onClick: (e) => e.stopPropagation(), children: jsxs(Select$1, { value: (_a = pendingRoles[user.user_id]) !== null && _a !== void 0 ? _a : (user.is_admin ? 'admin' : 'student'), onValueChange: (nextRole) => handleUserRoleChange(user, nextRole), disabled: updatingUserIds.has(user === null || user === void 0 ? void 0 : user.user_id), children: [jsxs(SelectTrigger, { className: "w-full text-sm", "aria-label": t('tabsUserstab.selectRoleAriaLabel', {
|
|
148113
|
+
name: user.name,
|
|
148114
|
+
}), children: [jsx(SelectValue, {}), updatingUserIds.has(user === null || user === void 0 ? void 0 : user.user_id) && (jsx(LoaderCircle, { "data-testid": "role-update-spinner", className: "h-4 w-4 animate-spin", "aria-hidden": "true" }))] }), jsxs(SelectContent, { className: "text-sm", children: [jsx(SelectItem, { value: "admin", children: t('tabsUserstab.roleAdmin') }), jsx(SelectItem, { value: "student", children: t('tabsUserstab.roleUser') })] })] }) }), jsx("td", { className: "px-6 py-4", onClick: (e) => e.stopPropagation(), children: jsxs(Popover, { modal: true, open: openPolicyPopover === user.user_id, onOpenChange: (open) => setOpenPolicyPopover(open ? user.user_id : null), children: [jsx(PopoverTrigger, { asChild: true, children: jsxs(Button$1, { variant: "outline", role: "combobox", "aria-expanded": openPolicyPopover === user.user_id, "aria-label": t('tabsUserstab.selectPoliciesAriaLabel', {
|
|
148081
148115
|
name: user.name,
|
|
148082
148116
|
}), className: "w-full justify-between text-sm h-9 font-normal", disabled: updatingUserIds.has(user === null || user === void 0 ? void 0 : user.user_id), children: [jsx("span", { className: "truncate", children: user.is_admin
|
|
148083
148117
|
? t('tabsUserstab.allPoliciesSelected', {
|
|
@@ -101937,6 +101937,7 @@ var tabsUserstab$3 = {
|
|
|
101937
101937
|
columnStatus: "Status",
|
|
101938
101938
|
roleAdmin: "Admin",
|
|
101939
101939
|
roleUser: "User",
|
|
101940
|
+
selectRoleAriaLabel: "Select role for {name}",
|
|
101940
101941
|
selectPoliciesAriaLabel: "Select policies for {name}",
|
|
101941
101942
|
allPoliciesSelected: "All ({count}) selected",
|
|
101942
101943
|
nPoliciesSelected: "{count} selected",
|
|
@@ -102168,24 +102169,29 @@ var voiceTabLabels$3 = {
|
|
|
102168
102169
|
title: "Agent Voice",
|
|
102169
102170
|
description: "The voice your agent uses to read out replies in chat.",
|
|
102170
102171
|
providerLabel: "Voice source",
|
|
102171
|
-
providerTooltip: "Browser
|
|
102172
|
+
providerTooltip: "Browser and ibl.ai speak on the listener's own device. OpenAI and Google use a custom voice you pick below.",
|
|
102172
102173
|
providerOptions: {
|
|
102173
102174
|
browser: {
|
|
102174
102175
|
label: "Browser",
|
|
102175
102176
|
description: "Speak through the user's own device. No setup needed."
|
|
102176
102177
|
},
|
|
102178
|
+
google: {
|
|
102179
|
+
label: "Google",
|
|
102180
|
+
description: "High-quality voices from Google."
|
|
102181
|
+
},
|
|
102182
|
+
iblai: {
|
|
102183
|
+
label: "ibl.ai",
|
|
102184
|
+
description: "High-quality voices from ibl.ai"
|
|
102185
|
+
},
|
|
102177
102186
|
openai: {
|
|
102178
102187
|
label: "OpenAI",
|
|
102179
102188
|
description: "High-quality voices from OpenAI."
|
|
102180
|
-
},
|
|
102181
|
-
google: {
|
|
102182
|
-
label: "Google",
|
|
102183
|
-
description: "Natural-sounding voices from Google."
|
|
102184
102189
|
}
|
|
102185
102190
|
},
|
|
102186
102191
|
voicePickerLabel: {
|
|
102187
102192
|
openai: "OpenAI voice",
|
|
102188
|
-
google: "Google voice"
|
|
102193
|
+
google: "Google voice",
|
|
102194
|
+
iblai: "ibl.ai voice"
|
|
102189
102195
|
},
|
|
102190
102196
|
searchPlaceholder: "Search voices…",
|
|
102191
102197
|
emptyState: "No voices match your search.",
|
|
@@ -108981,6 +108987,7 @@ var tabsUserstab$2 = {
|
|
|
108981
108987
|
columnStatus: "Statut",
|
|
108982
108988
|
roleAdmin: "Admin",
|
|
108983
108989
|
roleUser: "Utilisateur",
|
|
108990
|
+
selectRoleAriaLabel: "Sélectionner le rôle pour {name}",
|
|
108984
108991
|
selectPoliciesAriaLabel: "Sélectionner les politiques pour {name}",
|
|
108985
108992
|
allPoliciesSelected: "Tout ({count}) sélectionné",
|
|
108986
108993
|
nPoliciesSelected: "{count} sélectionné(s)",
|
|
@@ -109212,24 +109219,29 @@ var voiceTabLabels$2 = {
|
|
|
109212
109219
|
title: "Voix de l'agent",
|
|
109213
109220
|
description: "La voix utilisée par votre agent pour lire les réponses dans le chat.",
|
|
109214
109221
|
providerLabel: "Source de la voix",
|
|
109215
|
-
providerTooltip: "Le navigateur
|
|
109222
|
+
providerTooltip: "Le navigateur et ibl.ai parlent sur l'appareil de l'auditeur. OpenAI et Google utilisent une voix personnalisée que vous choisissez ci-dessous.",
|
|
109216
109223
|
providerOptions: {
|
|
109217
109224
|
browser: {
|
|
109218
109225
|
label: "Navigateur",
|
|
109219
109226
|
description: "Parle via l'appareil de l'utilisateur. Aucune configuration requise."
|
|
109220
109227
|
},
|
|
109228
|
+
google: {
|
|
109229
|
+
label: "Google",
|
|
109230
|
+
description: "Voix de haute qualité de Google."
|
|
109231
|
+
},
|
|
109232
|
+
iblai: {
|
|
109233
|
+
label: "ibl.ai",
|
|
109234
|
+
description: "Voix de haute qualité d'ibl.ai"
|
|
109235
|
+
},
|
|
109221
109236
|
openai: {
|
|
109222
109237
|
label: "OpenAI",
|
|
109223
109238
|
description: "Voix de haute qualité d'OpenAI."
|
|
109224
|
-
},
|
|
109225
|
-
google: {
|
|
109226
|
-
label: "Google",
|
|
109227
|
-
description: "Voix naturelles de Google."
|
|
109228
109239
|
}
|
|
109229
109240
|
},
|
|
109230
109241
|
voicePickerLabel: {
|
|
109231
109242
|
openai: "Voix OpenAI",
|
|
109232
|
-
google: "Voix Google"
|
|
109243
|
+
google: "Voix Google",
|
|
109244
|
+
iblai: "Voix ibl.ai"
|
|
109233
109245
|
},
|
|
109234
109246
|
searchPlaceholder: "Rechercher des voix…",
|
|
109235
109247
|
emptyState: "Aucune voix ne correspond à votre recherche.",
|
|
@@ -115859,6 +115871,7 @@ var tabsUserstab$1 = {
|
|
|
115859
115871
|
columnStatus: "Estado",
|
|
115860
115872
|
roleAdmin: "Admin",
|
|
115861
115873
|
roleUser: "Usuario",
|
|
115874
|
+
selectRoleAriaLabel: "Seleccionar rol para {name}",
|
|
115862
115875
|
selectPoliciesAriaLabel: "Seleccionar políticas para {name}",
|
|
115863
115876
|
allPoliciesSelected: "Todas ({count}) seleccionadas",
|
|
115864
115877
|
nPoliciesSelected: "{count} seleccionada(s)",
|
|
@@ -116090,24 +116103,29 @@ var voiceTabLabels$1 = {
|
|
|
116090
116103
|
title: "Voz del agente",
|
|
116091
116104
|
description: "La voz que usa tu agente para leer las respuestas en el chat.",
|
|
116092
116105
|
providerLabel: "Fuente de voz",
|
|
116093
|
-
providerTooltip: "El navegador
|
|
116106
|
+
providerTooltip: "El navegador e ibl.ai hablan en el dispositivo del oyente. OpenAI y Google usan una voz personalizada que eliges a continuación.",
|
|
116094
116107
|
providerOptions: {
|
|
116095
116108
|
browser: {
|
|
116096
116109
|
label: "Navegador",
|
|
116097
116110
|
description: "Habla a través del dispositivo del usuario. No requiere configuración."
|
|
116098
116111
|
},
|
|
116112
|
+
google: {
|
|
116113
|
+
label: "Google",
|
|
116114
|
+
description: "Voces de alta calidad de Google."
|
|
116115
|
+
},
|
|
116116
|
+
iblai: {
|
|
116117
|
+
label: "ibl.ai",
|
|
116118
|
+
description: "Voces de alta calidad de ibl.ai"
|
|
116119
|
+
},
|
|
116099
116120
|
openai: {
|
|
116100
116121
|
label: "OpenAI",
|
|
116101
116122
|
description: "Voces de alta calidad de OpenAI."
|
|
116102
|
-
},
|
|
116103
|
-
google: {
|
|
116104
|
-
label: "Google",
|
|
116105
|
-
description: "Voces naturales de Google."
|
|
116106
116123
|
}
|
|
116107
116124
|
},
|
|
116108
116125
|
voicePickerLabel: {
|
|
116109
116126
|
openai: "Voz de OpenAI",
|
|
116110
|
-
google: "Voz de Google"
|
|
116127
|
+
google: "Voz de Google",
|
|
116128
|
+
iblai: "Voz de ibl.ai"
|
|
116111
116129
|
},
|
|
116112
116130
|
searchPlaceholder: "Buscar voces…",
|
|
116113
116131
|
emptyState: "Ninguna voz coincide con tu búsqueda.",
|
|
@@ -122737,6 +122755,7 @@ var tabsUserstab = {
|
|
|
122737
122755
|
columnStatus: "状态",
|
|
122738
122756
|
roleAdmin: "管理员",
|
|
122739
122757
|
roleUser: "用户",
|
|
122758
|
+
selectRoleAriaLabel: "为 {name} 选择角色",
|
|
122740
122759
|
selectPoliciesAriaLabel: "为 {name} 选择策略",
|
|
122741
122760
|
allPoliciesSelected: "全部({count})已选",
|
|
122742
122761
|
nPoliciesSelected: "已选 {count} 个",
|
|
@@ -122968,24 +122987,29 @@ var voiceTabLabels = {
|
|
|
122968
122987
|
title: "智能体声音",
|
|
122969
122988
|
description: "您的智能体在聊天中朗读回复时使用的声音。",
|
|
122970
122989
|
providerLabel: "声音来源",
|
|
122971
|
-
providerTooltip: "
|
|
122990
|
+
providerTooltip: "浏览器和 ibl.ai 在听者自己的设备上发声。OpenAI 和 Google 使用您在下方选择的自定义声音。",
|
|
122972
122991
|
providerOptions: {
|
|
122973
122992
|
browser: {
|
|
122974
122993
|
label: "浏览器",
|
|
122975
122994
|
description: "通过用户自己的设备发声,无需任何设置。"
|
|
122976
122995
|
},
|
|
122996
|
+
google: {
|
|
122997
|
+
label: "Google",
|
|
122998
|
+
description: "来自 Google 的高质量声音。"
|
|
122999
|
+
},
|
|
123000
|
+
iblai: {
|
|
123001
|
+
label: "ibl.ai",
|
|
123002
|
+
description: "来自 ibl.ai 的高质量声音"
|
|
123003
|
+
},
|
|
122977
123004
|
openai: {
|
|
122978
123005
|
label: "OpenAI",
|
|
122979
123006
|
description: "来自 OpenAI 的高质量声音。"
|
|
122980
|
-
},
|
|
122981
|
-
google: {
|
|
122982
|
-
label: "Google",
|
|
122983
|
-
description: "来自 Google 的自然声音。"
|
|
122984
123007
|
}
|
|
122985
123008
|
},
|
|
122986
123009
|
voicePickerLabel: {
|
|
122987
123010
|
openai: "OpenAI 声音",
|
|
122988
|
-
google: "Google 声音"
|
|
123011
|
+
google: "Google 声音",
|
|
123012
|
+
iblai: "ibl.ai 语音"
|
|
122989
123013
|
},
|
|
122990
123014
|
searchPlaceholder: "搜索声音…",
|
|
122991
123015
|
emptyState: "没有与您搜索匹配的声音。",
|
|
@@ -180988,6 +181012,7 @@ function UsersTab({ tenant, onInviteClick, currentSpa, showGradebookTab = false,
|
|
|
180988
181012
|
const [updateUserStatus] = useUpdateUserStatusMutation();
|
|
180989
181013
|
const [updatePlatformUserRoleWithPolicies] = useUpdatePlatformUserRoleWithPoliciesMutation();
|
|
180990
181014
|
const [updatingUserIds, setUpdatingUserIds] = useState(new Set());
|
|
181015
|
+
const [pendingRoles, setPendingRoles] = useState({});
|
|
180991
181016
|
const [openPolicyPopover, setOpenPolicyPopover] = useState(null);
|
|
180992
181017
|
const [selectedUser, setSelectedUser] = useState(null);
|
|
180993
181018
|
const { currentTenant } = useCurrentTenant();
|
|
@@ -181040,18 +181065,19 @@ function UsersTab({ tenant, onInviteClick, currentSpa, showGradebookTab = false,
|
|
|
181040
181065
|
if (!isWatcher)
|
|
181041
181066
|
await refetchPlatformUsers();
|
|
181042
181067
|
};
|
|
181043
|
-
const handleUserRoleChange = async (user) => {
|
|
181068
|
+
const handleUserRoleChange = async (user, nextRole) => {
|
|
181044
181069
|
const userId = user === null || user === void 0 ? void 0 : user.user_id;
|
|
181045
181070
|
if (!userId)
|
|
181046
181071
|
return;
|
|
181047
181072
|
// Add to updating state
|
|
181048
181073
|
setUpdatingUserIds((prev) => new Set(prev).add(userId));
|
|
181074
|
+
setPendingRoles((prev) => ({ ...prev, [userId]: nextRole }));
|
|
181049
181075
|
try {
|
|
181050
181076
|
await updateUserRole({
|
|
181051
181077
|
username: user === null || user === void 0 ? void 0 : user.username,
|
|
181052
181078
|
org: user === null || user === void 0 ? void 0 : user.platform_org,
|
|
181053
181079
|
role: 'org-instructor',
|
|
181054
|
-
active:
|
|
181080
|
+
active: nextRole === 'admin',
|
|
181055
181081
|
}).unwrap();
|
|
181056
181082
|
// Only clear policies if role change succeeded
|
|
181057
181083
|
try {
|
|
@@ -181073,6 +181099,7 @@ function UsersTab({ tenant, onInviteClick, currentSpa, showGradebookTab = false,
|
|
|
181073
181099
|
toast.success(t('tabsUserstab.roleUpdatedSuccess'));
|
|
181074
181100
|
// Manually invalidate the platformUsers query since it's in a different API slice
|
|
181075
181101
|
dispatch(platformApiSlice.util.invalidateTags([featureTags.PLATFORM_USERS]));
|
|
181102
|
+
await refetchPlatformUsers();
|
|
181076
181103
|
}
|
|
181077
181104
|
catch (error) {
|
|
181078
181105
|
toast.error(t('tabsUserstab.roleUpdateFailed'));
|
|
@@ -181083,6 +181110,10 @@ function UsersTab({ tenant, onInviteClick, currentSpa, showGradebookTab = false,
|
|
|
181083
181110
|
newSet.delete(userId);
|
|
181084
181111
|
return newSet;
|
|
181085
181112
|
});
|
|
181113
|
+
setPendingRoles((prev) => {
|
|
181114
|
+
const { [userId]: _removed, ...rest } = prev;
|
|
181115
|
+
return rest;
|
|
181116
|
+
});
|
|
181086
181117
|
}
|
|
181087
181118
|
};
|
|
181088
181119
|
const handleUserStatusChange = async (user, newStatus) => {
|
|
@@ -181183,6 +181214,7 @@ function UsersTab({ tenant, onInviteClick, currentSpa, showGradebookTab = false,
|
|
|
181183
181214
|
const totalCount = (_b = (isWatcher ? accessibleUsers === null || accessibleUsers === void 0 ? void 0 : accessibleUsers.count : users === null || users === void 0 ? void 0 : users.count)) !== null && _b !== void 0 ? _b : 0;
|
|
181184
181215
|
const totalPages = Math.ceil(totalCount / PAGE_SIZE$d);
|
|
181185
181216
|
return (jsxs("div", { className: "mt-4", children: [jsxs("div", { className: "flex justify-between items-center gap-1", children: [jsxs("div", { className: "relative mb-6 max-w-sm", children: [jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" }), jsx(Input, { type: "text", value: searchQuery, onChange: (event) => setQuerySearch(event.target.value), placeholder: t('tabsUserstab.searchPlaceholder'), className: "pl-10 pr-10 focus:ring-blue-500 focus:border-blue-500 h-[35px]", "aria-label": t('tabsUserstab.searchAriaLabel') }), searchQuery.length > 0 && (searchQuery !== debouncedSearchQuery || isLoading) && (jsx(LoaderCircle, { "data-testid": "search-spinner", className: "absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 animate-spin", "aria-hidden": "true" }))] }), !isWatcher && (jsx("div", { className: "mb-6", children: jsx(Button$1, { className: "bg-gradient-to-r from-[#2563EB] to-[#93C5FD] hover:opacity-90 text-white h-[35px]", onClick: onInviteClick, children: t('tabsUserstab.inviteButton') }) }))] }), isLoading ? (jsx("div", { className: "flex justify-center items-center h-64", children: jsx(Spinner, {}) })) : (jsxs(Fragment$1, { children: [jsx("div", { className: "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-x-auto", children: jsxs("table", { className: "w-full text-sm", children: [jsx("thead", { className: "bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600", children: jsxs("tr", { children: [jsx("th", { className: `px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400 ${isWatcher ? 'w-full' : 'w-[41%]'}`, children: t('tabsUserstab.columnEmail') }), !isWatcher && (jsxs(Fragment$1, { children: [jsx("th", { className: "px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400 w-[16%]", children: t('tabsUserstab.columnRole') }), jsx("th", { className: "px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400 w-[25%]", children: t('tabsUserstab.columnPolicies') }), jsx("th", { className: "px-6 py-3 text-left font-medium text-gray-500 dark:text-gray-400 w-[18%]", children: t('tabsUserstab.columnStatus') })] }))] }) }), jsxs("tbody", { className: "divide-y divide-gray-200 dark:divide-gray-600", children: [rows.map((row) => {
|
|
181217
|
+
var _a;
|
|
181186
181218
|
// Only set on the admin path — see {@link UserRow}.
|
|
181187
181219
|
const user = row.managed;
|
|
181188
181220
|
return (jsxs("tr", { onClick: () => setSelectedUser(row), className: "cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors", children: [jsx("td", { "aria-label": 'user-management-email-column', className: "px-6 py-4 text-gray-900 dark:text-gray-100 text-sm max-w-0", children: jsx("button", { type: "button", onClick: (e) => {
|
|
@@ -181190,7 +181222,9 @@ function UsersTab({ tenant, onInviteClick, currentSpa, showGradebookTab = false,
|
|
|
181190
181222
|
setSelectedUser(row);
|
|
181191
181223
|
}, className: "block max-w-full truncate text-left hover:text-blue-600 hover:underline focus:outline-none focus:text-blue-600", "aria-label": t('tabsUserstab.viewProfileAriaLabel', {
|
|
181192
181224
|
name: row.name || row.email,
|
|
181193
|
-
}), children: row.email }) }), user && (jsxs(Fragment$1, { children: [jsx("td", { className: "px-6 py-4", onClick: (e) => e.stopPropagation(), children: jsxs(Select$1, { value:
|
|
181225
|
+
}), children: row.email }) }), user && (jsxs(Fragment$1, { children: [jsx("td", { className: "px-6 py-4", onClick: (e) => e.stopPropagation(), children: jsxs(Select$1, { value: (_a = pendingRoles[user.user_id]) !== null && _a !== void 0 ? _a : (user.is_admin ? 'admin' : 'student'), onValueChange: (nextRole) => handleUserRoleChange(user, nextRole), disabled: updatingUserIds.has(user === null || user === void 0 ? void 0 : user.user_id), children: [jsxs(SelectTrigger, { className: "w-full text-sm", "aria-label": t('tabsUserstab.selectRoleAriaLabel', {
|
|
181226
|
+
name: user.name,
|
|
181227
|
+
}), children: [jsx(SelectValue, {}), updatingUserIds.has(user === null || user === void 0 ? void 0 : user.user_id) && (jsx(LoaderCircle, { "data-testid": "role-update-spinner", className: "h-4 w-4 animate-spin", "aria-hidden": "true" }))] }), jsxs(SelectContent, { className: "text-sm", children: [jsx(SelectItem, { value: "admin", children: t('tabsUserstab.roleAdmin') }), jsx(SelectItem, { value: "student", children: t('tabsUserstab.roleUser') })] })] }) }), jsx("td", { className: "px-6 py-4", onClick: (e) => e.stopPropagation(), children: jsxs(Popover, { modal: true, open: openPolicyPopover === user.user_id, onOpenChange: (open) => setOpenPolicyPopover(open ? user.user_id : null), children: [jsx(PopoverTrigger, { asChild: true, children: jsxs(Button$1, { variant: "outline", role: "combobox", "aria-expanded": openPolicyPopover === user.user_id, "aria-label": t('tabsUserstab.selectPoliciesAriaLabel', {
|
|
181194
181228
|
name: user.name,
|
|
181195
181229
|
}), className: "w-full justify-between text-sm h-9 font-normal", disabled: updatingUserIds.has(user === null || user === void 0 ? void 0 : user.user_id), children: [jsx("span", { className: "truncate", children: user.is_admin
|
|
181196
181230
|
? t('tabsUserstab.allPoliciesSelected', {
|
|
@@ -272019,23 +272053,28 @@ const AGENT_VOICE_TAB_LABELS = {
|
|
|
272019
272053
|
title: 'Agent Voice',
|
|
272020
272054
|
description: 'The voice your agent uses to read out replies in chat.',
|
|
272021
272055
|
providerLabel: 'Voice source',
|
|
272022
|
-
providerTooltip: "Browser
|
|
272056
|
+
providerTooltip: "Browser and ibl.ai speak on the listener's own device. OpenAI and Google use a custom voice you pick below.",
|
|
272023
272057
|
providerOptions: {
|
|
272024
272058
|
browser: {
|
|
272025
272059
|
label: 'Browser',
|
|
272026
272060
|
description: "Speak through the user's own device. No setup needed.",
|
|
272027
272061
|
},
|
|
272062
|
+
google: {
|
|
272063
|
+
label: 'Google',
|
|
272064
|
+
description: 'High-quality voices from Google.',
|
|
272065
|
+
},
|
|
272066
|
+
iblai: {
|
|
272067
|
+
label: 'ibl.ai',
|
|
272068
|
+
description: 'High-quality voices from ibl.ai',
|
|
272069
|
+
},
|
|
272028
272070
|
openai: {
|
|
272029
272071
|
label: 'OpenAI',
|
|
272030
272072
|
description: 'High-quality voices from OpenAI.',
|
|
272031
272073
|
},
|
|
272032
|
-
google: {
|
|
272033
|
-
label: 'Google',
|
|
272034
|
-
description: 'Natural-sounding voices from Google.',
|
|
272035
|
-
},
|
|
272036
272074
|
},
|
|
272037
272075
|
voicePickerLabel: {
|
|
272038
272076
|
openai: 'OpenAI voice',
|
|
272077
|
+
iblai: 'ibl.ai voice',
|
|
272039
272078
|
google: 'Google voice',
|
|
272040
272079
|
},
|
|
272041
272080
|
searchPlaceholder: 'Search voices…',
|
|
@@ -272144,18 +272183,23 @@ function buildVoiceTabLabels(t) {
|
|
|
272144
272183
|
label: t('voiceTabLabels.mentorVoice.providerOptions.browser.label'),
|
|
272145
272184
|
description: t('voiceTabLabels.mentorVoice.providerOptions.browser.description'),
|
|
272146
272185
|
},
|
|
272147
|
-
openai: {
|
|
272148
|
-
label: t('voiceTabLabels.mentorVoice.providerOptions.openai.label'),
|
|
272149
|
-
description: t('voiceTabLabels.mentorVoice.providerOptions.openai.description'),
|
|
272150
|
-
},
|
|
272151
272186
|
google: {
|
|
272152
272187
|
label: t('voiceTabLabels.mentorVoice.providerOptions.google.label'),
|
|
272153
272188
|
description: t('voiceTabLabels.mentorVoice.providerOptions.google.description'),
|
|
272154
272189
|
},
|
|
272190
|
+
iblai: {
|
|
272191
|
+
label: t('voiceTabLabels.mentorVoice.providerOptions.iblai.label'),
|
|
272192
|
+
description: t('voiceTabLabels.mentorVoice.providerOptions.iblai.description'),
|
|
272193
|
+
},
|
|
272194
|
+
openai: {
|
|
272195
|
+
label: t('voiceTabLabels.mentorVoice.providerOptions.openai.label'),
|
|
272196
|
+
description: t('voiceTabLabels.mentorVoice.providerOptions.openai.description'),
|
|
272197
|
+
},
|
|
272155
272198
|
},
|
|
272156
272199
|
voicePickerLabel: {
|
|
272157
272200
|
openai: t('voiceTabLabels.mentorVoice.voicePickerLabel.openai'),
|
|
272158
272201
|
google: t('voiceTabLabels.mentorVoice.voicePickerLabel.google'),
|
|
272202
|
+
iblai: t('voiceTabLabels.mentorVoice.voicePickerLabel.iblai'),
|
|
272159
272203
|
},
|
|
272160
272204
|
searchPlaceholder: t('voiceTabLabels.mentorVoice.searchPlaceholder'),
|
|
272161
272205
|
emptyState: t('voiceTabLabels.mentorVoice.emptyState'),
|
|
@@ -272322,6 +272366,46 @@ function toTitleCase(name) {
|
|
|
272322
272366
|
.map((word) => (word.length > 0 ? word[0].toUpperCase() + word.slice(1).toLowerCase() : word))
|
|
272323
272367
|
.join(' ');
|
|
272324
272368
|
}
|
|
272369
|
+
/**
|
|
272370
|
+
* On-device catalogue rows are stored under the model's own voice id -- two
|
|
272371
|
+
* letters of accent+gender, an underscore, then the voice's name
|
|
272372
|
+
* (`af_heart`, `bm_george`, `ef_dora`). Only the name is shown.
|
|
272373
|
+
*
|
|
272374
|
+
* Derived rather than mapped: the catalogue carries 50+ voices across several
|
|
272375
|
+
* languages and grows whenever the model does, so a hand-written table would
|
|
272376
|
+
* silently fall back to raw ids for everything it had not caught up with --
|
|
272377
|
+
* which is exactly what it did.
|
|
272378
|
+
*/
|
|
272379
|
+
const KOKORO_VOICE_ID = /^[a-z]{2}_([a-z]+)$/;
|
|
272380
|
+
/**
|
|
272381
|
+
* What to show for a voice. Only the on-device provider needs unpacking;
|
|
272382
|
+
* every other catalogue already stores something human-readable, so it just
|
|
272383
|
+
* gets its casing normalised.
|
|
272384
|
+
*
|
|
272385
|
+
* A name that does not match the id shape falls through to the raw value, so
|
|
272386
|
+
* an unexpected row still renders instead of showing blank.
|
|
272387
|
+
*/
|
|
272388
|
+
function voiceDisplayName(voice) {
|
|
272389
|
+
var _a;
|
|
272390
|
+
const name = (_a = voice.name) !== null && _a !== void 0 ? _a : '';
|
|
272391
|
+
if (isIblaiVoice(voice)) {
|
|
272392
|
+
const match = KOKORO_VOICE_ID.exec(name);
|
|
272393
|
+
if (match)
|
|
272394
|
+
return toTitleCase(match[1]);
|
|
272395
|
+
}
|
|
272396
|
+
return toTitleCase(name);
|
|
272397
|
+
}
|
|
272398
|
+
/**
|
|
272399
|
+
* Whether a catalogue row is an on-device (Kokoro) voice.
|
|
272400
|
+
*
|
|
272401
|
+
* The cast is deliberate: `VoiceProviderEnum` in the generated API client
|
|
272402
|
+
* predates this provider, so a direct comparison is a type error even though
|
|
272403
|
+
* the backend already returns `"iblai"` -- the same lag that leaves
|
|
272404
|
+
* `voice_provider` and `iblai_voice` off the generated mentor settings type.
|
|
272405
|
+
*/
|
|
272406
|
+
function isIblaiVoice(voice) {
|
|
272407
|
+
return voice.provider === 'iblai';
|
|
272408
|
+
}
|
|
272325
272409
|
|
|
272326
272410
|
const PAGE_SIZE$2 = 20;
|
|
272327
272411
|
function VoicePicker({ org, userId, provider, selectedVoiceId, onSelect, disabled, labels, }) {
|
|
@@ -272367,10 +272451,36 @@ function VoicePicker({ org, userId, provider, selectedVoiceId, onSelect, disable
|
|
|
272367
272451
|
!voices.some((v) => v.id === pinnedVoice.id);
|
|
272368
272452
|
const { playingId, isLoading: isPreviewLoading, play, stop } = useAudioPreview();
|
|
272369
272453
|
React__default.useEffect(() => () => stop(), [provider, stop]);
|
|
272370
|
-
|
|
272371
|
-
|
|
272372
|
-
|
|
272373
|
-
|
|
272454
|
+
/**
|
|
272455
|
+
* Width the scrollbar takes out of the list's content box.
|
|
272456
|
+
*
|
|
272457
|
+
* Measured rather than assumed: it is ~15px on Windows and most Linux
|
|
272458
|
+
* desktops but 0 wherever the platform draws overlay scrollbars (macOS by
|
|
272459
|
+
* default), so a hardcoded value would misalign the list on half the
|
|
272460
|
+
* machines that see it. Used below to pull the list's right edge out by
|
|
272461
|
+
* exactly that much, so the cards line up with the search box above them
|
|
272462
|
+
* and the scrollbar sits in the dialog's own padding.
|
|
272463
|
+
*/
|
|
272464
|
+
const listRef = React__default.useRef(null);
|
|
272465
|
+
const [scrollbarWidth, setScrollbarWidth] = React__default.useState(0);
|
|
272466
|
+
React__default.useLayoutEffect(() => {
|
|
272467
|
+
const el = listRef.current;
|
|
272468
|
+
if (!el)
|
|
272469
|
+
return;
|
|
272470
|
+
const measure = () => setScrollbarWidth(el.offsetWidth - el.clientWidth);
|
|
272471
|
+
measure();
|
|
272472
|
+
const observer = new ResizeObserver(measure);
|
|
272473
|
+
observer.observe(el);
|
|
272474
|
+
return () => observer.disconnect();
|
|
272475
|
+
}, [voices.length]);
|
|
272476
|
+
return (jsxs("div", { className: "space-y-3", children: [jsxs("div", { className: "relative", children: [jsx(Search, { className: "absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" }), jsx(Input, { type: "search", placeholder: labels.searchPlaceholder, className: "w-full pl-9", value: search, onChange: (e) => setSearch(e.target.value), disabled: disabled, "aria-label": labels.searchPlaceholder, "data-testid": "voice-picker-search" })] }), isError && jsx("p", { className: "text-sm text-red-500", children: labels.errorState }), isFetching ? (jsx("div", { className: "flex items-center justify-center py-8", children: jsx(Spinner, {}) })) : voices.length === 0 && !showPinned ? (jsx("div", { className: "rounded-md border border-dashed border-gray-200 py-8 text-center text-sm text-gray-500", children: labels.emptyState })) : (jsxs(Fragment$1, { children: [showPinned && pinnedVoice && (jsx("div", { "data-testid": "pinned-voice-row", children: jsx(VoiceRow, { voice: pinnedVoice, isSelected: true, isPlaying: playingId === pinnedVoice.id, showPlayLoader: playingId === pinnedVoice.id && isPreviewLoading, disabled: !!disabled, onSelect: onSelect, play: play, labels: labels }) })), jsx("div", { ref: listRef,
|
|
272477
|
+
// Vertical padding keeps the selected card's border from being
|
|
272478
|
+
// clipped by `overflow-y-auto` on the first and last rows. There is
|
|
272479
|
+
// deliberately no horizontal padding: the cards are meant to line
|
|
272480
|
+
// up with the search box, and the negative margin below moves the
|
|
272481
|
+
// scrollbar out of that alignment rather than letting it eat into
|
|
272482
|
+
// the last column.
|
|
272483
|
+
className: "grid max-h-[360px] grid-cols-1 gap-2 overflow-y-auto py-1 sm:grid-cols-2", style: { marginRight: -scrollbarWidth }, role: "radiogroup", "aria-label": t('voiceTabVoicePicker.availableVoicesAriaLabel'), children: voices.map((voice) => (jsx(VoiceRow, { voice: voice, isSelected: selectedVoiceId === voice.id, isPlaying: playingId === voice.id, showPlayLoader: playingId === voice.id && isPreviewLoading, disabled: !!disabled, onSelect: onSelect, play: play, labels: labels }, voice.id))) }), totalPages > 1 && (jsxs("div", { className: "flex items-center justify-between gap-2 border-t border-gray-100 pt-3 text-xs text-gray-600", "data-testid": "voice-pagination", children: [jsxs("span", { "aria-live": "polite", children: [t('voiceTabVoicePicker.paginationPage'), " ", jsx("strong", { children: page }), ' ', t('voiceTabVoicePicker.paginationOf'), " ", jsx("strong", { children: totalPages }), jsx("span", { className: "ml-1 text-gray-400", children: t('voiceTabVoicePicker.paginationVoiceCount', { total }) })] }), jsxs("div", { className: "flex items-center gap-2", children: [jsxs(Button$1, { type: "button", variant: "outline", size: "sm", disabled: disabled || page <= 1, onClick: () => setPage((p) => Math.max(1, p - 1)), "aria-label": t('voiceTabVoicePicker.prevPageAriaLabel'), "data-testid": "voice-prev-page", children: [jsx(ChevronLeft, { className: "h-4 w-4" }), t('voiceTabVoicePicker.prevButton')] }), jsxs(Button$1, { type: "button", variant: "outline", size: "sm", disabled: disabled || page >= totalPages, onClick: () => setPage((p) => Math.min(totalPages, p + 1)), "aria-label": t('voiceTabVoicePicker.nextPageAriaLabel'), "data-testid": "voice-next-page", children: [t('voiceTabVoicePicker.nextButton'), jsx(ChevronRight, { className: "h-4 w-4" })] })] })] }))] }))] }));
|
|
272374
272484
|
}
|
|
272375
272485
|
/**
|
|
272376
272486
|
* A single voice row. Rendered as a `<div role="radio">` so the inner
|
|
@@ -272381,19 +272491,24 @@ function VoicePicker({ org, userId, provider, selectedVoiceId, onSelect, disable
|
|
|
272381
272491
|
* clipped by the parent scroll container's `overflow-y-auto`.
|
|
272382
272492
|
*/
|
|
272383
272493
|
function VoiceRow({ voice, isSelected, isPlaying, showPlayLoader, disabled, onSelect, play, labels, }) {
|
|
272384
|
-
|
|
272494
|
+
// Whether a second line renders under the name. It decides the cross-axis
|
|
272495
|
+
// alignment: with a description the 36px badge should sit level with the
|
|
272496
|
+
// first line (`items-start`), but on a single-line card that pushes the
|
|
272497
|
+
// badge 4px below the name and preview button, which reads as crooked.
|
|
272498
|
+
const hasMeta = !isIblaiVoice(voice) && Boolean(voice.language || voice.description);
|
|
272499
|
+
return (jsxs("div", { role: "radio", tabIndex: disabled ? -1 : 0, "aria-checked": isSelected, "aria-disabled": disabled || undefined, "aria-label": voiceDisplayName(voice), "data-voice-id": voice.id, "data-selected": isSelected || undefined, onClick: () => !disabled && onSelect(voice), onKeyDown: (e) => {
|
|
272385
272500
|
if (disabled)
|
|
272386
272501
|
return;
|
|
272387
272502
|
if (e.key === ' ' || e.key === 'Enter') {
|
|
272388
272503
|
e.preventDefault();
|
|
272389
272504
|
onSelect(voice);
|
|
272390
272505
|
}
|
|
272391
|
-
}, className: cn('flex cursor-pointer
|
|
272506
|
+
}, className: cn('flex cursor-pointer gap-3 rounded-lg border-2 p-3 text-left transition-all hover:shadow-sm focus-visible:outline-blue-500', hasMeta ? 'items-start' : 'items-center', isSelected
|
|
272392
272507
|
? 'border-blue-500 bg-blue-50/40'
|
|
272393
|
-
: 'border-gray-200 bg-white hover:border-gray-300', disabled && 'cursor-not-allowed opacity-60'), children: [jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-blue-50 text-blue-600", children: jsx(Volume2, { className: "h-4 w-4", "aria-hidden": "true" }) }), jsxs("div", { className: "min-w-0 flex-1", children: [jsxs("div", { className: "flex items-center justify-between gap-2", children: [jsx("span", { className: "truncate text-sm font-medium text-gray-900", children:
|
|
272394
|
-
|
|
272395
|
-
|
|
272396
|
-
|
|
272508
|
+
: 'border-gray-200 bg-white hover:border-gray-300', disabled && 'cursor-not-allowed opacity-60'), children: [jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-blue-50 text-blue-600", children: jsx(Volume2, { className: "h-4 w-4", "aria-hidden": "true" }) }), jsxs("div", { className: "min-w-0 flex-1", children: [jsxs("div", { className: "flex items-center justify-between gap-2", children: [jsx("span", { className: "truncate text-sm font-medium text-gray-900", children: voiceDisplayName(voice) }), jsx("span", { className: "flex h-7 w-7 flex-shrink-0 items-center justify-center", children: voice.audio_url && (jsx(Button$1, { type: "button", variant: "ghost", size: "icon", className: "h-7 w-7 flex-shrink-0 text-gray-500 hover:text-blue-600", onClick: (e) => {
|
|
272509
|
+
e.stopPropagation();
|
|
272510
|
+
play(voice.id, voice.audio_url);
|
|
272511
|
+
}, disabled: disabled, "aria-label": labels.previewAria(voiceDisplayName(voice), isPlaying ? 'stop' : 'play'), children: showPlayLoader ? (jsx(LoaderCircle, { className: "h-4 w-4 animate-spin" })) : isPlaying ? (jsx(Pause, { className: "h-4 w-4" })) : (jsx(Play, { className: "h-4 w-4" })) })) })] }), hasMeta && (jsx("p", { className: "mt-1 line-clamp-2 text-xs text-gray-500", children: [voice.language, voice.description].filter(Boolean).join(' · ') }))] })] }));
|
|
272397
272512
|
}
|
|
272398
272513
|
|
|
272399
272514
|
/**
|
|
@@ -272433,8 +272548,10 @@ function VoicePickerTrigger({ org, userId, provider, selectedVoice, onSelect, di
|
|
|
272433
272548
|
if (isOpen)
|
|
272434
272549
|
stop();
|
|
272435
272550
|
}, [isOpen, stop]);
|
|
272436
|
-
const displayName = selectedVoice ?
|
|
272437
|
-
|
|
272551
|
+
const displayName = selectedVoice ? voiceDisplayName(selectedVoice) : null;
|
|
272552
|
+
// See the note in voice-picker.tsx: the on-device rows all carry the same
|
|
272553
|
+
// locale, so showing it adds nothing.
|
|
272554
|
+
const language = selectedVoice && isIblaiVoice(selectedVoice) ? null : (selectedVoice === null || selectedVoice === void 0 ? void 0 : selectedVoice.language) || null;
|
|
272438
272555
|
const audioUrl = (selectedVoice === null || selectedVoice === void 0 ? void 0 : selectedVoice.audio_url) || '';
|
|
272439
272556
|
const isPlaying = selectedVoice != null && playingId === selectedVoice.id;
|
|
272440
272557
|
const showPlayLoader = isPlaying && isPreviewLoading;
|
|
@@ -272738,6 +272855,29 @@ function PromptCard$1({ label, placeholder, value, disabled, renderContent, onEd
|
|
|
272738
272855
|
return (jsxs("div", { className: "overflow-hidden rounded-lg bg-gray-50", "data-testid": testId, children: [jsx("div", { className: "flex items-center justify-between border-b border-gray-200 p-[1.12rem]", children: jsxs("div", { className: "flex items-center gap-2", children: [jsx("h3", { className: "text-sm font-medium text-gray-900", children: label }), headerExtra] }) }), jsxs("div", { className: "flex items-start gap-2 p-4", children: [jsx("div", { className: "mt-1 flex-shrink-0", "aria-hidden": "true", children: "\uD83D\uDCDD" }), jsx("div", { className: "flex h-[180px] flex-1 flex-col", children: jsx("div", { className: "mb-4 flex-grow overflow-y-auto", tabIndex: 0, role: "region", "aria-label": `${label} content`, children: isEmpty ? (jsx("p", { className: "text-sm text-gray-400 italic", children: placeholder })) : (renderContent(value)) }) })] }), jsxs("div", { className: "flex w-full gap-2 px-4 pb-4", children: [jsxs(Button$1, { variant: "outline", size: "sm", className: "h-8 flex-1 py-5", disabled: disabled, onClick: onEdit, "aria-label": `Edit ${label}`, children: [jsx(SquarePen, { className: "mr-2 h-4 w-4" }), "Edit"] }), jsx(CopyButton, { text: value, disabled: disabled })] })] }));
|
|
272739
272856
|
}
|
|
272740
272857
|
|
|
272858
|
+
/**
|
|
272859
|
+
* Providers whose voice comes from the server-side voices catalogue, and so
|
|
272860
|
+
* need a voice picker plus a voice id on save. Only `browser` is excluded: it
|
|
272861
|
+
* speaks through whatever voice the listener's own OS provides, which we
|
|
272862
|
+
* neither choose nor know.
|
|
272863
|
+
*
|
|
272864
|
+
* `iblai` synthesises on-device but still picks from the catalogue, because
|
|
272865
|
+
* the backend stores `iblai_voice` as a foreign key into the same table --
|
|
272866
|
+
* sending it a voice name is rejected with "A valid integer is required".
|
|
272867
|
+
*/
|
|
272868
|
+
const CATALOGUE_VOICE_PROVIDERS = ['openai', 'google', 'iblai'];
|
|
272869
|
+
/**
|
|
272870
|
+
* Voice used when a mentor has never had one chosen.
|
|
272871
|
+
*
|
|
272872
|
+
* Must stay in step with the consuming app's own fallback (mentorai's
|
|
272873
|
+
* `DEFAULT_VOICE` in `lib/tts/config.ts`): the agent speaks that voice whether
|
|
272874
|
+
* or not anything is saved, so showing an empty picker would tell the admin
|
|
272875
|
+
* "nothing selected" while the listener hears Heart.
|
|
272876
|
+
*/
|
|
272877
|
+
const DEFAULT_IBLAI_VOICE = 'af_heart';
|
|
272878
|
+
function usesVoiceCatalogue(provider) {
|
|
272879
|
+
return CATALOGUE_VOICE_PROVIDERS.includes(provider);
|
|
272880
|
+
}
|
|
272741
272881
|
const SUB_TAB_VALUES = {
|
|
272742
272882
|
voice: 'voice',
|
|
272743
272883
|
callConfig: 'call-config',
|
|
@@ -272764,7 +272904,7 @@ function readVoiceName(value) {
|
|
|
272764
272904
|
return null;
|
|
272765
272905
|
}
|
|
272766
272906
|
function AgentVoiceTab({ labels: labelsOverride, defaultSubTab = 'voice', renderPromptContent, tenantKey: tenantKeyProp, mentorId: mentorIdProp, username: usernameProp, enableRBAC: enableRBACProp, }) {
|
|
272767
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
272907
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
|
|
272768
272908
|
const ctx = useAgentSettingsOptional();
|
|
272769
272909
|
const tenantKey = (_a = tenantKeyProp !== null && tenantKeyProp !== void 0 ? tenantKeyProp : ctx === null || ctx === void 0 ? void 0 : ctx.tenantKey) !== null && _a !== void 0 ? _a : '';
|
|
272770
272910
|
const mentorId = (_b = mentorIdProp !== null && mentorIdProp !== void 0 ? mentorIdProp : ctx === null || ctx === void 0 ? void 0 : ctx.mentorId) !== null && _b !== void 0 ? _b : '';
|
|
@@ -272786,6 +272926,10 @@ function AgentVoiceTab({ labels: labelsOverride, defaultSubTab = 'voice', render
|
|
|
272786
272926
|
// (with audio_url for the play button) and send the id on save.
|
|
272787
272927
|
const savedOpenaiName = (_f = readVoiceName(mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.openai_voice)) !== null && _f !== void 0 ? _f : null;
|
|
272788
272928
|
const savedGoogleName = (_g = readVoiceName(mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.google_voice)) !== null && _g !== void 0 ? _g : null;
|
|
272929
|
+
// Falls back to the default rather than null so the trigger shows the voice
|
|
272930
|
+
// that will actually be used. Nothing is written to the mentor by this: an
|
|
272931
|
+
// unsaved default and an explicitly-chosen Heart behave identically.
|
|
272932
|
+
const savedIblaiName = (_h = readVoiceName(mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.iblai_voice)) !== null && _h !== void 0 ? _h : DEFAULT_IBLAI_VOICE;
|
|
272789
272933
|
const { voice: resolvedOpenaiVoice } = useResolveVoiceByName({
|
|
272790
272934
|
org: tenantKey,
|
|
272791
272935
|
userId: username,
|
|
@@ -272798,28 +272942,37 @@ function AgentVoiceTab({ labels: labelsOverride, defaultSubTab = 'voice', render
|
|
|
272798
272942
|
provider: 'google',
|
|
272799
272943
|
name: savedGoogleName,
|
|
272800
272944
|
});
|
|
272945
|
+
const { voice: resolvedIblaiVoice } = useResolveVoiceByName({
|
|
272946
|
+
org: tenantKey,
|
|
272947
|
+
userId: username,
|
|
272948
|
+
provider: 'iblai',
|
|
272949
|
+
name: savedIblaiName,
|
|
272950
|
+
});
|
|
272801
272951
|
const savedInstructions = typeof (mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.voice_instructions) === 'string' ? mentorSettings.voice_instructions : '';
|
|
272802
272952
|
// Picked voices override the resolved server values once the user
|
|
272803
272953
|
// makes a choice. Null means "use what came from the server".
|
|
272804
272954
|
const [provider, setProvider] = React__default.useState(initialProvider);
|
|
272805
272955
|
const [pickedOpenaiVoice, setPickedOpenaiVoice] = React__default.useState(null);
|
|
272806
272956
|
const [pickedGoogleVoice, setPickedGoogleVoice] = React__default.useState(null);
|
|
272957
|
+
const [pickedIblaiVoice, setPickedIblaiVoice] = React__default.useState(null);
|
|
272807
272958
|
const [instructions, setInstructions] = React__default.useState(savedInstructions);
|
|
272808
272959
|
const [selectedPrompt, setSelectedPrompt] = React__default.useState(null);
|
|
272809
272960
|
React__default.useEffect(() => {
|
|
272810
272961
|
setProvider(initialProvider);
|
|
272811
272962
|
setPickedOpenaiVoice(null);
|
|
272812
272963
|
setPickedGoogleVoice(null);
|
|
272964
|
+
setPickedIblaiVoice(null);
|
|
272813
272965
|
setInstructions(savedInstructions);
|
|
272814
272966
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
272815
272967
|
}, [mentorId, rawMentorSettings]);
|
|
272816
|
-
const effectiveOpenaiVoice = (
|
|
272817
|
-
const effectiveGoogleVoice = (
|
|
272968
|
+
const effectiveOpenaiVoice = (_j = pickedOpenaiVoice !== null && pickedOpenaiVoice !== void 0 ? pickedOpenaiVoice : resolvedOpenaiVoice) !== null && _j !== void 0 ? _j : null;
|
|
272969
|
+
const effectiveGoogleVoice = (_k = pickedGoogleVoice !== null && pickedGoogleVoice !== void 0 ? pickedGoogleVoice : resolvedGoogleVoice) !== null && _k !== void 0 ? _k : null;
|
|
272970
|
+
const effectiveIblaiVoice = (_l = pickedIblaiVoice !== null && pickedIblaiVoice !== void 0 ? pickedIblaiVoice : resolvedIblaiVoice) !== null && _l !== void 0 ? _l : null;
|
|
272818
272971
|
const [editMentor, { isLoading: isSavingMentor }] = useEditMentorMutation();
|
|
272819
272972
|
// "Enable voice calls" master toggle. Optimistic local mirror of the
|
|
272820
272973
|
// server value so the switch responds instantly; re-synced whenever the
|
|
272821
272974
|
// settings response changes.
|
|
272822
|
-
const serverVoiceCallEnabled = (
|
|
272975
|
+
const serverVoiceCallEnabled = (_m = mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.show_voice_call) !== null && _m !== void 0 ? _m : true;
|
|
272823
272976
|
const [voiceCallEnabled, setVoiceCallEnabled] = React__default.useState(serverVoiceCallEnabled);
|
|
272824
272977
|
const [isTogglingVoiceCall, setIsTogglingVoiceCall] = React__default.useState(false);
|
|
272825
272978
|
React__default.useEffect(() => {
|
|
@@ -272855,33 +273008,48 @@ function AgentVoiceTab({ labels: labelsOverride, defaultSubTab = 'voice', render
|
|
|
272855
273008
|
const isDirty = provider !== initialProvider ||
|
|
272856
273009
|
pickedOpenaiVoice != null ||
|
|
272857
273010
|
pickedGoogleVoice != null ||
|
|
273011
|
+
pickedIblaiVoice != null ||
|
|
272858
273012
|
instructionsDirty;
|
|
272859
273013
|
/**
|
|
272860
|
-
* Provider chips
|
|
272861
|
-
*
|
|
272862
|
-
*
|
|
272863
|
-
* for
|
|
273014
|
+
* Provider chips, ordered by lowercasing each label and then sorting
|
|
273015
|
+
* alphabetically, so a newly added provider slots itself in without a
|
|
273016
|
+
* hand-edit. We reuse the vendor logos already shipped in the host's
|
|
273017
|
+
* static-root (see `getLLMProviderDetails` for the canonical paths).
|
|
273018
|
+
* Browser falls back to a Lucide globe — there's no vendor logo for
|
|
273019
|
+
* "device speech engine".
|
|
272864
273020
|
*/
|
|
272865
|
-
const providerOptions =
|
|
272866
|
-
|
|
272867
|
-
|
|
272868
|
-
|
|
272869
|
-
|
|
272870
|
-
|
|
272871
|
-
|
|
272872
|
-
|
|
272873
|
-
|
|
272874
|
-
|
|
272875
|
-
|
|
272876
|
-
|
|
272877
|
-
|
|
272878
|
-
|
|
272879
|
-
|
|
272880
|
-
|
|
272881
|
-
|
|
272882
|
-
|
|
272883
|
-
|
|
272884
|
-
|
|
273021
|
+
const providerOptions = React__default.useMemo(() => {
|
|
273022
|
+
const options = [
|
|
273023
|
+
{
|
|
273024
|
+
value: 'browser',
|
|
273025
|
+
label: labels.mentorVoice.providerOptions.browser.label,
|
|
273026
|
+
description: labels.mentorVoice.providerOptions.browser.description,
|
|
273027
|
+
logo: { type: 'icon', Icon: Globe },
|
|
273028
|
+
},
|
|
273029
|
+
{
|
|
273030
|
+
value: 'google',
|
|
273031
|
+
label: labels.mentorVoice.providerOptions.google.label,
|
|
273032
|
+
description: labels.mentorVoice.providerOptions.google.description,
|
|
273033
|
+
logo: { type: 'image', src: '/llm-google-provider.svg', alt: 'Google' },
|
|
273034
|
+
},
|
|
273035
|
+
{
|
|
273036
|
+
value: 'iblai',
|
|
273037
|
+
label: labels.mentorVoice.providerOptions.iblai.label,
|
|
273038
|
+
description: labels.mentorVoice.providerOptions.iblai.description,
|
|
273039
|
+
logo: { type: 'image', src: '/llm-iblai-provider.png', alt: 'ibl.ai' },
|
|
273040
|
+
},
|
|
273041
|
+
{
|
|
273042
|
+
value: 'openai',
|
|
273043
|
+
label: labels.mentorVoice.providerOptions.openai.label,
|
|
273044
|
+
description: labels.mentorVoice.providerOptions.openai.description,
|
|
273045
|
+
logo: { type: 'image', src: '/llm-openai-provider-2.svg', alt: 'OpenAI' },
|
|
273046
|
+
},
|
|
273047
|
+
];
|
|
273048
|
+
// Lowercase first, then compare. A case-sensitive sort would rank every
|
|
273049
|
+
// capitalised label ahead of the lowercase "ibl.ai" and push it past
|
|
273050
|
+
// OpenAI.
|
|
273051
|
+
return [...options].sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase()));
|
|
273052
|
+
}, [labels.mentorVoice.providerOptions]);
|
|
272885
273053
|
const handleSaveVoice = async () => {
|
|
272886
273054
|
try {
|
|
272887
273055
|
const formData = { voice_provider: provider };
|
|
@@ -272894,6 +273062,9 @@ function AgentVoiceTab({ labels: labelsOverride, defaultSubTab = 'voice', render
|
|
|
272894
273062
|
if (provider === 'google' && pickedGoogleVoice) {
|
|
272895
273063
|
formData.google_voice = pickedGoogleVoice.id;
|
|
272896
273064
|
}
|
|
273065
|
+
if (provider === 'iblai' && pickedIblaiVoice) {
|
|
273066
|
+
formData.iblai_voice = pickedIblaiVoice.id;
|
|
273067
|
+
}
|
|
272897
273068
|
// Same "absent = keep" contract: only send voice_instructions when
|
|
272898
273069
|
// the user actually edited it. Clearing sends "" (never null) —
|
|
272899
273070
|
// that's what resets the stored value server-side.
|
|
@@ -272914,16 +273085,21 @@ function AgentVoiceTab({ labels: labelsOverride, defaultSubTab = 'voice', render
|
|
|
272914
273085
|
toast.error(labels.toasts.voiceError);
|
|
272915
273086
|
}
|
|
272916
273087
|
};
|
|
273088
|
+
// Only `browser` has no voice to show -- it uses the listener's OS voice.
|
|
272917
273089
|
const selectedVoice = provider === 'openai'
|
|
272918
273090
|
? effectiveOpenaiVoice
|
|
272919
273091
|
: provider === 'google'
|
|
272920
273092
|
? effectiveGoogleVoice
|
|
272921
|
-
:
|
|
273093
|
+
: provider === 'iblai'
|
|
273094
|
+
? effectiveIblaiVoice
|
|
273095
|
+
: null;
|
|
272922
273096
|
const onSelectVoice = (voice) => {
|
|
272923
273097
|
if (provider === 'openai')
|
|
272924
273098
|
setPickedOpenaiVoice(voice);
|
|
272925
273099
|
else if (provider === 'google')
|
|
272926
273100
|
setPickedGoogleVoice(voice);
|
|
273101
|
+
else if (provider === 'iblai')
|
|
273102
|
+
setPickedIblaiVoice(voice);
|
|
272927
273103
|
};
|
|
272928
273104
|
// Mirrors `AgentScreenShareTab`: hosts can render prompt content as
|
|
272929
273105
|
// Markdown via `renderPromptContent`; plain text otherwise.
|
|
@@ -272934,19 +273110,19 @@ function AgentVoiceTab({ labels: labelsOverride, defaultSubTab = 'voice', render
|
|
|
272934
273110
|
};
|
|
272935
273111
|
return (jsxs(Fragment$1, { children: [jsx("div", { className: "flex h-[73px] flex-shrink-0 items-center border-b border-gray-200 bg-white p-4 lg:block", children: jsxs("div", { children: [jsx("h3", { className: "mb-1 text-base font-medium text-gray-900", children: labels.header.title }), jsx("p", { className: "text-xs text-gray-600", children: labels.header.description })] }) }), jsx("div", { className: "flex-1 space-y-4 p-3 lg:p-4", style: { overflowY: 'auto', overflowX: 'hidden' }, "data-testid": "voice-tab-body", children: isMentorSettingsLoading ? (jsx("div", { className: "flex items-center justify-center py-12", "data-testid": "voice-tab-loading", children: jsx(Spinner, {}) })) : (jsxs(CapabilityGate, { title: labels.capability.title, description: labels.capability.description, offHint: labels.capability.offHint, enabled: voiceCallEnabled, onToggle: handleToggleVoiceCall, disabled: isDisabled || isTogglingVoiceCall, ariaLabel: labels.capability.title, toggleTestId: "voice-capability-toggle", children: [jsxs(Tabs, { defaultValue: SUB_TAB_VALUES[defaultSubTab], className: "w-full", children: [jsxs(TabsList, { "aria-label": labels.header.title, "data-testid": "voice-sub-tabs", children: [jsx(TabsTrigger, { value: SUB_TAB_VALUES.voice, "data-testid": "voice-sub-tab-voice", children: labels.subTabs.voice }), jsx(TabsTrigger, { value: SUB_TAB_VALUES.callConfig, "data-testid": "voice-sub-tab-call-config", children: labels.subTabs.callConfig })] }), jsxs(TabsContent, { value: SUB_TAB_VALUES.voice, className: "space-y-4", children: [jsx("p", { className: "text-xs text-gray-600", children: labels.mentorVoice.description }), jsx(WithFormPermissions, { name: "voice_provider",
|
|
272936
273112
|
// @ts-expect-error permissions.field is opaque
|
|
272937
|
-
permissions: (
|
|
273113
|
+
permissions: (_o = mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.permissions) === null || _o === void 0 ? void 0 : _o.field, enableRBAC: enableRBAC, children: ({ disabled }) => (jsxs("div", { className: "space-y-2", children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx(Label$1, { className: "text-sm font-medium text-[#646464]", children: labels.mentorVoice.providerLabel }), jsx(FieldTooltip, { text: labels.mentorVoice.providerTooltip })] }), jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4", role: "radiogroup", "aria-label": labels.mentorVoice.providerLabel, children: providerOptions.map((option) => {
|
|
272938
273114
|
const isSelected = provider === option.value;
|
|
272939
273115
|
const isDisabledOption = isDisabled || disabled;
|
|
272940
273116
|
return (jsxs("button", { type: "button", role: "radio", "aria-checked": isSelected, "aria-label": option.label, disabled: isDisabledOption, "data-testid": `voice-provider-${option.value}`, "data-voice-provider": option.value, onClick: () => setProvider(option.value), className: cn('flex flex-col items-start gap-2 rounded-lg border-2 p-3 text-left transition-all hover:shadow-sm', isSelected
|
|
272941
273117
|
? 'border-blue-500 bg-blue-50/40'
|
|
272942
273118
|
: 'border-gray-200 bg-white hover:border-gray-300', isDisabledOption && 'cursor-not-allowed opacity-60'), children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "flex h-6 w-6 flex-shrink-0 items-center justify-center", children: option.logo.type === 'image' ? (jsx(Image$4, { src: option.logo.src, alt: option.logo.alt, width: 24, height: 24, className: "h-6 w-6 object-contain" })) : (jsx(option.logo.Icon, { className: "h-5 w-5 text-blue-600", "aria-hidden": "true" })) }), jsx("span", { className: "text-sm font-medium text-gray-900", children: option.label })] }), jsx("p", { className: "text-xs text-gray-500", children: option.description })] }, option.value));
|
|
272943
|
-
}) })] })) }), (provider
|
|
273119
|
+
}) })] })) }), usesVoiceCatalogue(provider) && (jsx(WithFormPermissions, { name: `${provider}_voice`,
|
|
272944
273120
|
// @ts-expect-error permissions.field is opaque
|
|
272945
|
-
permissions: (
|
|
273121
|
+
permissions: (_p = mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.permissions) === null || _p === void 0 ? void 0 : _p.field, enableRBAC: enableRBAC, children: ({ disabled }) => (jsxs("div", { className: "space-y-2", children: [jsx(Label$1, { className: "text-sm font-medium text-[#646464]", children: t('tabsVoiceTab.voiceLabel') }), jsx(VoicePickerTrigger, { org: tenantKey, userId: username, provider: provider, selectedVoice: selectedVoice, onSelect: onSelectVoice, disabled: isDisabled || disabled, labels: labels.mentorVoice, modalTitle: t('tabsVoiceTab.selectVoiceModalTitle', {
|
|
272946
273122
|
voiceType: labels.mentorVoice.voicePickerLabel[provider],
|
|
272947
273123
|
}), modalDescription: t('tabsVoiceTab.selectVoiceModalDescription'), testId: "mentor-voice-trigger" })] })) })), (provider === 'openai' || provider === 'google') && (jsx(WithFormPermissions, { name: "voice_instructions",
|
|
272948
273124
|
// @ts-expect-error permissions.field is opaque
|
|
272949
|
-
permissions: (
|
|
273125
|
+
permissions: (_q = mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.permissions) === null || _q === void 0 ? void 0 : _q.field, enableRBAC: enableRBAC, children: ({ disabled }) => {
|
|
272950
273126
|
const isFieldDisabled = isDisabled || disabled;
|
|
272951
273127
|
const presetKeys = ['warm', 'calm', 'energetic'];
|
|
272952
273128
|
return (jsxs("div", { className: "space-y-2", children: [jsx(PromptCard$1, { label: labels.mentorVoice.instructions.label, placeholder: labels.mentorVoice.instructions.placeholder, value: instructions, disabled: isFieldDisabled, renderContent: renderInstructionsContent, testId: "voice-instructions-card", headerExtra: jsx(FieldTooltip, { text: labels.mentorVoice.instructions.tooltip }), onEdit: () => setSelectedPrompt({
|
|
@@ -272959,7 +273135,7 @@ function AgentVoiceTab({ labels: labelsOverride, defaultSubTab = 'voice', render
|
|
|
272959
273135
|
}) }), jsxs("div", { className: "flex items-start justify-between gap-3", children: [jsx("p", { id: "voice-instructions-help", className: "text-xs text-gray-500", children: labels.mentorVoice.instructions.helpText }), jsxs("span", { id: "voice-instructions-counter", "aria-live": "polite", "data-testid": "voice-instructions-counter", className: cn('flex-shrink-0 text-xs text-gray-400', instructionsOverCap && 'text-red-500'), children: [instructions.length, "/", VOICE_INSTRUCTIONS_MAX] })] }), jsxs("div", { className: "flex flex-wrap items-center gap-2", role: "group", "aria-label": labels.mentorVoice.instructions.presetsLabel, children: [jsxs("span", { className: "text-xs text-gray-500", children: [labels.mentorVoice.instructions.presetsLabel, ":"] }), presetKeys.map((key) => (jsx("button", { type: "button", disabled: isFieldDisabled, "data-testid": `voice-instructions-preset-${key}`, onClick: () => setInstructions(labels.mentorVoice.instructions.presets[key].text), className: cn('inline-flex items-center rounded-full border border-gray-300 bg-white px-3 py-1 text-xs text-gray-700 transition-colors hover:bg-gray-50', isFieldDisabled && 'cursor-not-allowed opacity-50'), children: labels.mentorVoice.instructions.presets[key].label }, key)))] })] }));
|
|
272960
273136
|
} })), jsx("div", { className: "flex items-center justify-end border-t border-gray-100 pt-3", children: jsx(Button$1, { type: "button", onClick: handleSaveVoice, disabled: isDisabled || !isDirty || instructionsOverCap, "data-testid": "voice-save-button", className: "bg-gradient-to-r from-[#2563EB] to-[#93C5FD] text-white hover:opacity-90", children: isSavingMentor
|
|
272961
273137
|
? labels.mentorVoice.savingButton
|
|
272962
|
-
: labels.mentorVoice.saveButton }) })] }), jsxs(TabsContent, { value: SUB_TAB_VALUES.callConfig, className: "space-y-4", children: [jsx("p", { className: "text-xs text-gray-600", children: labels.callConfig.description }), mentorId && username && tenantKey && (jsx(CallConfigSection, { org: tenantKey, userId: username, mentorId: mentorId, isDisabled: isDisabled, labels: labels.callConfig, voicePickerLabels: labels.mentorVoice, initialConfig: (
|
|
273138
|
+
: labels.mentorVoice.saveButton }) })] }), jsxs(TabsContent, { value: SUB_TAB_VALUES.callConfig, className: "space-y-4", children: [jsx("p", { className: "text-xs text-gray-600", children: labels.callConfig.description }), mentorId && username && tenantKey && (jsx(CallConfigSection, { org: tenantKey, userId: username, mentorId: mentorId, isDisabled: isDisabled, labels: labels.callConfig, voicePickerLabels: labels.mentorVoice, initialConfig: (_r = mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.call_configuration) !== null && _r !== void 0 ? _r : null, renderPromptContent: renderPromptContent, toastLabels: {
|
|
272963
273139
|
saved: labels.toasts.callConfigSaved,
|
|
272964
273140
|
error: labels.toasts.callConfigError,
|
|
272965
273141
|
} }))] })] }), selectedPrompt && (
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iblai/iblai-js",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.8",
|
|
4
4
|
"description": "Unified JavaScript SDK for IBL.ai — re-exports data-layer, web-containers, and web-utils under a single package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -77,8 +77,8 @@
|
|
|
77
77
|
"dotenv": "16.6.1",
|
|
78
78
|
"winston": "3.19.0",
|
|
79
79
|
"@iblai/data-layer": "1.12.106",
|
|
80
|
-
"@iblai/
|
|
81
|
-
"@iblai/
|
|
80
|
+
"@iblai/mcp": "1.10.5",
|
|
81
|
+
"@iblai/web-containers": "1.18.8",
|
|
82
82
|
"@iblai/web-utils": "2.1.17"
|
|
83
83
|
},
|
|
84
84
|
"peerDependencies": {
|