@iblai/iblai-js 2.5.5 → 2.5.7

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.
@@ -65343,24 +65343,29 @@ var voiceTabLabels$3 = {
65343
65343
  title: "Agent Voice",
65344
65344
  description: "The voice your agent uses to read out replies in chat.",
65345
65345
  providerLabel: "Voice source",
65346
- providerTooltip: "Browser uses the listener's own device to speak. OpenAI and Google use a custom voice you pick below.",
65346
+ providerTooltip: "Browser and ibl.ai speak on the listener's own device. OpenAI and Google use a custom voice you pick below.",
65347
65347
  providerOptions: {
65348
65348
  browser: {
65349
65349
  label: "Browser",
65350
65350
  description: "Speak through the user's own device. No setup needed."
65351
65351
  },
65352
+ google: {
65353
+ label: "Google",
65354
+ description: "High-quality voices from Google."
65355
+ },
65356
+ iblai: {
65357
+ label: "ibl.ai",
65358
+ description: "High-quality voices from ibl.ai"
65359
+ },
65352
65360
  openai: {
65353
65361
  label: "OpenAI",
65354
65362
  description: "High-quality voices from OpenAI."
65355
- },
65356
- google: {
65357
- label: "Google",
65358
- description: "Natural-sounding voices from Google."
65359
65363
  }
65360
65364
  },
65361
65365
  voicePickerLabel: {
65362
65366
  openai: "OpenAI voice",
65363
- google: "Google voice"
65367
+ google: "Google voice",
65368
+ iblai: "ibl.ai voice"
65364
65369
  },
65365
65370
  searchPlaceholder: "Search voices…",
65366
65371
  emptyState: "No voices match your search.",
@@ -72387,24 +72392,29 @@ var voiceTabLabels$2 = {
72387
72392
  title: "Voix de l'agent",
72388
72393
  description: "La voix utilisée par votre agent pour lire les réponses dans le chat.",
72389
72394
  providerLabel: "Source de la voix",
72390
- providerTooltip: "Le navigateur utilise l'appareil de l'auditeur pour parler. OpenAI et Google utilisent une voix personnalisée que vous choisissez ci-dessous.",
72395
+ 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
72396
  providerOptions: {
72392
72397
  browser: {
72393
72398
  label: "Navigateur",
72394
72399
  description: "Parle via l'appareil de l'utilisateur. Aucune configuration requise."
72395
72400
  },
72401
+ google: {
72402
+ label: "Google",
72403
+ description: "Voix de haute qualité de Google."
72404
+ },
72405
+ iblai: {
72406
+ label: "ibl.ai",
72407
+ description: "Voix de haute qualité d'ibl.ai"
72408
+ },
72396
72409
  openai: {
72397
72410
  label: "OpenAI",
72398
72411
  description: "Voix de haute qualité d'OpenAI."
72399
- },
72400
- google: {
72401
- label: "Google",
72402
- description: "Voix naturelles de Google."
72403
72412
  }
72404
72413
  },
72405
72414
  voicePickerLabel: {
72406
72415
  openai: "Voix OpenAI",
72407
- google: "Voix Google"
72416
+ google: "Voix Google",
72417
+ iblai: "Voix ibl.ai"
72408
72418
  },
72409
72419
  searchPlaceholder: "Rechercher des voix…",
72410
72420
  emptyState: "Aucune voix ne correspond à votre recherche.",
@@ -79265,24 +79275,29 @@ var voiceTabLabels$1 = {
79265
79275
  title: "Voz del agente",
79266
79276
  description: "La voz que usa tu agente para leer las respuestas en el chat.",
79267
79277
  providerLabel: "Fuente de voz",
79268
- providerTooltip: "El navegador usa el dispositivo del oyente para hablar. OpenAI y Google usan una voz personalizada que eliges a continuación.",
79278
+ 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
79279
  providerOptions: {
79270
79280
  browser: {
79271
79281
  label: "Navegador",
79272
79282
  description: "Habla a través del dispositivo del usuario. No requiere configuración."
79273
79283
  },
79284
+ google: {
79285
+ label: "Google",
79286
+ description: "Voces de alta calidad de Google."
79287
+ },
79288
+ iblai: {
79289
+ label: "ibl.ai",
79290
+ description: "Voces de alta calidad de ibl.ai"
79291
+ },
79274
79292
  openai: {
79275
79293
  label: "OpenAI",
79276
79294
  description: "Voces de alta calidad de OpenAI."
79277
- },
79278
- google: {
79279
- label: "Google",
79280
- description: "Voces naturales de Google."
79281
79295
  }
79282
79296
  },
79283
79297
  voicePickerLabel: {
79284
79298
  openai: "Voz de OpenAI",
79285
- google: "Voz de Google"
79299
+ google: "Voz de Google",
79300
+ iblai: "Voz de ibl.ai"
79286
79301
  },
79287
79302
  searchPlaceholder: "Buscar voces…",
79288
79303
  emptyState: "Ninguna voz coincide con tu búsqueda.",
@@ -86143,24 +86158,29 @@ var voiceTabLabels = {
86143
86158
  title: "智能体声音",
86144
86159
  description: "您的智能体在聊天中朗读回复时使用的声音。",
86145
86160
  providerLabel: "声音来源",
86146
- providerTooltip: "浏览器使用听者自己的设备发声。OpenAI 和 Google 使用您在下方选择的自定义声音。",
86161
+ providerTooltip: "浏览器和 ibl.ai 在听者自己的设备上发声。OpenAI 和 Google 使用您在下方选择的自定义声音。",
86147
86162
  providerOptions: {
86148
86163
  browser: {
86149
86164
  label: "浏览器",
86150
86165
  description: "通过用户自己的设备发声,无需任何设置。"
86151
86166
  },
86167
+ google: {
86168
+ label: "Google",
86169
+ description: "来自 Google 的高质量声音。"
86170
+ },
86171
+ iblai: {
86172
+ label: "ibl.ai",
86173
+ description: "来自 ibl.ai 的高质量声音"
86174
+ },
86152
86175
  openai: {
86153
86176
  label: "OpenAI",
86154
86177
  description: "来自 OpenAI 的高质量声音。"
86155
- },
86156
- google: {
86157
- label: "Google",
86158
- description: "来自 Google 的自然声音。"
86159
86178
  }
86160
86179
  },
86161
86180
  voicePickerLabel: {
86162
86181
  openai: "OpenAI 声音",
86163
- google: "Google 声音"
86182
+ google: "Google 声音",
86183
+ iblai: "ibl.ai 语音"
86164
86184
  },
86165
86185
  searchPlaceholder: "搜索声音…",
86166
86186
  emptyState: "没有与您搜索匹配的声音。",
@@ -192428,7 +192448,7 @@ const defaultTabs = [
192428
192448
  value: 'monetization',
192429
192449
  },*/
192430
192450
  ];
192431
- const className = 'inline-flex items-center justify-center whitespace-nowrap rounded-[5px] px-2 sm:px-2.5 pt-1 pb-0.5 sm:text-sm text-gray-500 ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-blue-600 data-[state=active]:font-semibold text-sm font-medium data-[state=active]:bg-[#f5f7fb] data-[state=active]:shadow-none cursor-pointer';
192451
+ const className = 'inline-flex h-7 shrink-0 items-center justify-center whitespace-nowrap rounded-[5px] px-2 sm:px-2.5 py-0 text-sm text-gray-500 ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-blue-600 data-[state=active]:font-semibold font-medium data-[state=active]:bg-[#f5f7fb] data-[state=active]:shadow-none cursor-pointer';
192432
192452
  function AnalyticsLayout({ children, currentPath, basePath, onTabChange, tabs = defaultTabs, activeTabClassName = '', beforeDataReports, excludeTabs = [], showPicker = false, tenantKey, scopedMentor, onScopedMentorChange, }) {
192433
192453
  var _a, _b;
192434
192454
  const { currentTenant } = useCurrentTenant();
@@ -192521,12 +192541,12 @@ function AnalyticsLayout({ children, currentPath, basePath, onTabChange, tabs =
192521
192541
  return;
192522
192542
  onTabChange(tabValue);
192523
192543
  };
192524
- return (jsx(AnalyticsScopeProvider, { value: scopeValue, children: jsxs("div", { className: "flex flex-col flex-1 min-h-0 h-full overflow-hidden overscroll-none bg-[#f5f7fb]", children: [jsx("div", { className: "p-6 pb-0 pt-4 xs:pt-0", children: jsx("div", { className: "flex items-center justify-between mb-4 xs:mb-2", children: jsxs("div", { className: "flex items-center justify-between w-full overflow-x-auto", children: [jsxs("div", { className: "flex items-center gap-3", children: [showPicker && (jsx("div", { className: "shrink-0", children: jsx(AnalyticsAgentPicker, { tenantKey: (_b = tenantKey !== null && tenantKey !== void 0 ? tenantKey : currentTenant === null || currentTenant === void 0 ? void 0 : currentTenant.key) !== null && _b !== void 0 ? _b : '', value: activeScopedMentor, onChange: handleScopedMentorChange }) })), jsx(Tabs, { className: "bg-[#f5f7fb]", value: activeTab, children: jsx(TabsList, { className: "bg-[#f5f7fb]", children: tabs
192544
+ return (jsx(AnalyticsScopeProvider, { value: scopeValue, children: jsxs("div", { className: "flex flex-col flex-1 min-h-0 h-full overflow-hidden overscroll-none bg-[#f5f7fb]", children: [jsx("div", { className: "p-6 pb-0 pt-4", children: jsxs("div", { className: "mb-4 flex flex-col gap-2 sm:mb-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3", children: [jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-3", children: [showPicker && (jsx("div", { className: "shrink-0", children: jsx(AnalyticsAgentPicker, { tenantKey: (_b = tenantKey !== null && tenantKey !== void 0 ? tenantKey : currentTenant === null || currentTenant === void 0 ? void 0 : currentTenant.key) !== null && _b !== void 0 ? _b : '', value: activeScopedMentor, onChange: handleScopedMentorChange }) })), jsx("div", { className: "min-w-0 flex-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", children: jsx(Tabs, { className: "bg-[#f5f7fb]", value: activeTab, children: jsx(TabsList, { className: "w-max bg-[#f5f7fb]", children: tabs
192525
192545
  .filter((tab) => !excludeTabs.includes(tab.value) && canAccessTab(tab.value))
192526
192546
  .map((tab) => {
192527
192547
  const isActive = activeTab === tab.value;
192528
192548
  return (jsx(TabsTrigger, { value: tab.value, className: cn(className, isActive && activeTabClassName), onClick: () => handleTabClick(tab.value), children: tab.label }, tab.value));
192529
- }) }) })] }), jsxs("div", { className: "flex items-center gap-4", children: [beforeDataReports, canAccessTab('reports') && (jsx(Tabs, { className: "bg-[#f5f7fb]", value: activeTab, children: jsx(TabsList, { className: "bg-[#f5f7fb]", children: jsx(TabsTrigger, { value: "reports", className: cn(className, activeTab === 'reports' && activeTabClassName), onClick: () => handleTabClick('reports'), children: "Data Reports" }) }) }))] })] }) }) }), jsx("div", { className: "flex-1 overflow-auto overscroll-contain scrollbar-none px-6 pb-6 pt-0", children: isActiveTabAllowed ? children : null })] }) }));
192549
+ }) }) }) })] }), jsxs("div", { className: "flex w-full shrink-0 items-center justify-end gap-3 sm:w-auto", children: [beforeDataReports, canAccessTab('reports') && (jsx(Tabs, { className: "bg-[#f5f7fb]", value: activeTab, children: jsx(TabsList, { className: "bg-[#f5f7fb]", children: jsx(TabsTrigger, { value: "reports", className: cn(className, activeTab === 'reports' && activeTabClassName), onClick: () => handleTabClick('reports'), children: "Data Reports" }) }) }))] })] }) }), jsx("div", { className: "flex-1 overflow-auto overscroll-contain scrollbar-none px-6 pb-6 pt-0", children: isActiveTabAllowed ? children : null })] }) }));
192530
192550
  }
192531
192551
 
192532
192552
  const accessTimeData = [
@@ -229627,7 +229647,7 @@ function GroupsFilterDropdown({ groups, selectedGroupIds, onSelectionChange, isL
229627
229647
  ? ((_a = groups.find((g) => g.id === selectedGroupIds[0])) === null || _a === void 0 ? void 0 : _a.name) ||
229628
229648
  t('analyticsGroupsFilterDropdown.oneGroup')
229629
229649
  : t('analyticsGroupsFilterDropdown.multipleGroups', { count: selectedCount });
229630
- return (jsxs(Popover, { open: open, onOpenChange: setOpen, children: [jsx(PopoverTrigger, { asChild: true, children: jsxs(Button$1, { variant: "outline", role: "combobox", "aria-expanded": open, className: cn('min-w-[160px] justify-between text-sm font-medium h-[32px] border-gray-200 bg-white hover:bg-gray-50 -mt-5', selectedCount > 0 && 'border-blue-300 bg-blue-50 hover:bg-blue-100'), disabled: isLoading, children: [jsx("span", { className: "truncate", children: isLoading ? t('analyticsGroupsFilterDropdown.loading') : displayText }), jsxs("div", { className: "flex items-center gap-1 ml-2", children: [selectedCount > 0 && (jsx(X$1, { className: "h-3 w-3 text-gray-400 hover:text-gray-600", onClick: handleClearAll })), jsx(ChevronDown, { className: "h-4 w-4 text-gray-400" })] })] }) }), jsx(PopoverContent, { className: "w-[220px] p-0", align: "start", children: jsx("div", { className: "max-h-[300px] overflow-auto", children: groups.length === 0 ? (jsx("div", { className: "p-3 text-sm text-gray-500 text-center", children: t('analyticsGroupsFilterDropdown.noGroupsAvailable') })) : (jsx("div", { className: "p-1", children: groups.map((group) => {
229650
+ return (jsxs(Popover, { open: open, onOpenChange: setOpen, children: [jsx(PopoverTrigger, { asChild: true, children: jsxs(Button$1, { variant: "outline", role: "combobox", "aria-expanded": open, className: cn('h-9 max-w-full justify-between border-gray-200 bg-white text-sm font-medium hover:bg-gray-50 sm:min-w-[160px]', selectedCount > 0 && 'border-blue-300 bg-blue-50 hover:bg-blue-100'), disabled: isLoading, children: [jsx("span", { className: "truncate", children: isLoading ? t('analyticsGroupsFilterDropdown.loading') : displayText }), jsxs("div", { className: "flex items-center gap-1 ml-2", children: [selectedCount > 0 && (jsx(X$1, { className: "h-3 w-3 text-gray-400 hover:text-gray-600", onClick: handleClearAll })), jsx(ChevronDown, { className: "h-4 w-4 text-gray-400" })] })] }) }), jsx(PopoverContent, { className: "w-[220px] p-0", align: "start", children: jsx("div", { className: "max-h-[300px] overflow-auto", children: groups.length === 0 ? (jsx("div", { className: "p-3 text-sm text-gray-500 text-center", children: t('analyticsGroupsFilterDropdown.noGroupsAvailable') })) : (jsx("div", { className: "p-1", children: groups.map((group) => {
229631
229651
  const isSelected = selectedGroupIds.includes(group.id);
229632
229652
  return (jsxs("div", { className: cn('flex items-center gap-2 px-2 py-1.5 cursor-pointer rounded hover:bg-gray-100', isSelected && 'bg-blue-50 hover:bg-blue-100'), onClick: () => handleToggleGroup(group.id), children: [jsx(Checkbox, { checked: isSelected, className: "pointer-events-none" }), jsx("span", { className: "text-sm truncate flex-1", children: group.name }), isSelected && jsx(Check, { className: "h-4 w-4 text-blue-600" })] }, group.id));
229633
229653
  }) })) }) })] }));
@@ -102168,24 +102168,29 @@ var voiceTabLabels$3 = {
102168
102168
  title: "Agent Voice",
102169
102169
  description: "The voice your agent uses to read out replies in chat.",
102170
102170
  providerLabel: "Voice source",
102171
- providerTooltip: "Browser uses the listener's own device to speak. OpenAI and Google use a custom voice you pick below.",
102171
+ providerTooltip: "Browser and ibl.ai speak on the listener's own device. OpenAI and Google use a custom voice you pick below.",
102172
102172
  providerOptions: {
102173
102173
  browser: {
102174
102174
  label: "Browser",
102175
102175
  description: "Speak through the user's own device. No setup needed."
102176
102176
  },
102177
+ google: {
102178
+ label: "Google",
102179
+ description: "High-quality voices from Google."
102180
+ },
102181
+ iblai: {
102182
+ label: "ibl.ai",
102183
+ description: "High-quality voices from ibl.ai"
102184
+ },
102177
102185
  openai: {
102178
102186
  label: "OpenAI",
102179
102187
  description: "High-quality voices from OpenAI."
102180
- },
102181
- google: {
102182
- label: "Google",
102183
- description: "Natural-sounding voices from Google."
102184
102188
  }
102185
102189
  },
102186
102190
  voicePickerLabel: {
102187
102191
  openai: "OpenAI voice",
102188
- google: "Google voice"
102192
+ google: "Google voice",
102193
+ iblai: "ibl.ai voice"
102189
102194
  },
102190
102195
  searchPlaceholder: "Search voices…",
102191
102196
  emptyState: "No voices match your search.",
@@ -109212,24 +109217,29 @@ var voiceTabLabels$2 = {
109212
109217
  title: "Voix de l'agent",
109213
109218
  description: "La voix utilisée par votre agent pour lire les réponses dans le chat.",
109214
109219
  providerLabel: "Source de la voix",
109215
- providerTooltip: "Le navigateur utilise l'appareil de l'auditeur pour parler. OpenAI et Google utilisent une voix personnalisée que vous choisissez ci-dessous.",
109220
+ 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
109221
  providerOptions: {
109217
109222
  browser: {
109218
109223
  label: "Navigateur",
109219
109224
  description: "Parle via l'appareil de l'utilisateur. Aucune configuration requise."
109220
109225
  },
109226
+ google: {
109227
+ label: "Google",
109228
+ description: "Voix de haute qualité de Google."
109229
+ },
109230
+ iblai: {
109231
+ label: "ibl.ai",
109232
+ description: "Voix de haute qualité d'ibl.ai"
109233
+ },
109221
109234
  openai: {
109222
109235
  label: "OpenAI",
109223
109236
  description: "Voix de haute qualité d'OpenAI."
109224
- },
109225
- google: {
109226
- label: "Google",
109227
- description: "Voix naturelles de Google."
109228
109237
  }
109229
109238
  },
109230
109239
  voicePickerLabel: {
109231
109240
  openai: "Voix OpenAI",
109232
- google: "Voix Google"
109241
+ google: "Voix Google",
109242
+ iblai: "Voix ibl.ai"
109233
109243
  },
109234
109244
  searchPlaceholder: "Rechercher des voix…",
109235
109245
  emptyState: "Aucune voix ne correspond à votre recherche.",
@@ -116090,24 +116100,29 @@ var voiceTabLabels$1 = {
116090
116100
  title: "Voz del agente",
116091
116101
  description: "La voz que usa tu agente para leer las respuestas en el chat.",
116092
116102
  providerLabel: "Fuente de voz",
116093
- providerTooltip: "El navegador usa el dispositivo del oyente para hablar. OpenAI y Google usan una voz personalizada que eliges a continuación.",
116103
+ 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
116104
  providerOptions: {
116095
116105
  browser: {
116096
116106
  label: "Navegador",
116097
116107
  description: "Habla a través del dispositivo del usuario. No requiere configuración."
116098
116108
  },
116109
+ google: {
116110
+ label: "Google",
116111
+ description: "Voces de alta calidad de Google."
116112
+ },
116113
+ iblai: {
116114
+ label: "ibl.ai",
116115
+ description: "Voces de alta calidad de ibl.ai"
116116
+ },
116099
116117
  openai: {
116100
116118
  label: "OpenAI",
116101
116119
  description: "Voces de alta calidad de OpenAI."
116102
- },
116103
- google: {
116104
- label: "Google",
116105
- description: "Voces naturales de Google."
116106
116120
  }
116107
116121
  },
116108
116122
  voicePickerLabel: {
116109
116123
  openai: "Voz de OpenAI",
116110
- google: "Voz de Google"
116124
+ google: "Voz de Google",
116125
+ iblai: "Voz de ibl.ai"
116111
116126
  },
116112
116127
  searchPlaceholder: "Buscar voces…",
116113
116128
  emptyState: "Ninguna voz coincide con tu búsqueda.",
@@ -122968,24 +122983,29 @@ var voiceTabLabels = {
122968
122983
  title: "智能体声音",
122969
122984
  description: "您的智能体在聊天中朗读回复时使用的声音。",
122970
122985
  providerLabel: "声音来源",
122971
- providerTooltip: "浏览器使用听者自己的设备发声。OpenAI 和 Google 使用您在下方选择的自定义声音。",
122986
+ providerTooltip: "浏览器和 ibl.ai 在听者自己的设备上发声。OpenAI 和 Google 使用您在下方选择的自定义声音。",
122972
122987
  providerOptions: {
122973
122988
  browser: {
122974
122989
  label: "浏览器",
122975
122990
  description: "通过用户自己的设备发声,无需任何设置。"
122976
122991
  },
122992
+ google: {
122993
+ label: "Google",
122994
+ description: "来自 Google 的高质量声音。"
122995
+ },
122996
+ iblai: {
122997
+ label: "ibl.ai",
122998
+ description: "来自 ibl.ai 的高质量声音"
122999
+ },
122977
123000
  openai: {
122978
123001
  label: "OpenAI",
122979
123002
  description: "来自 OpenAI 的高质量声音。"
122980
- },
122981
- google: {
122982
- label: "Google",
122983
- description: "来自 Google 的自然声音。"
122984
123003
  }
122985
123004
  },
122986
123005
  voicePickerLabel: {
122987
123006
  openai: "OpenAI 声音",
122988
- google: "Google 声音"
123007
+ google: "Google 声音",
123008
+ iblai: "ibl.ai 语音"
122989
123009
  },
122990
123010
  searchPlaceholder: "搜索声音…",
122991
123011
  emptyState: "没有与您搜索匹配的声音。",
@@ -272019,23 +272039,28 @@ const AGENT_VOICE_TAB_LABELS = {
272019
272039
  title: 'Agent Voice',
272020
272040
  description: 'The voice your agent uses to read out replies in chat.',
272021
272041
  providerLabel: 'Voice source',
272022
- providerTooltip: "Browser uses the listener's own device to speak. OpenAI and Google use a custom voice you pick below.",
272042
+ providerTooltip: "Browser and ibl.ai speak on the listener's own device. OpenAI and Google use a custom voice you pick below.",
272023
272043
  providerOptions: {
272024
272044
  browser: {
272025
272045
  label: 'Browser',
272026
272046
  description: "Speak through the user's own device. No setup needed.",
272027
272047
  },
272048
+ google: {
272049
+ label: 'Google',
272050
+ description: 'High-quality voices from Google.',
272051
+ },
272052
+ iblai: {
272053
+ label: 'ibl.ai',
272054
+ description: 'High-quality voices from ibl.ai',
272055
+ },
272028
272056
  openai: {
272029
272057
  label: 'OpenAI',
272030
272058
  description: 'High-quality voices from OpenAI.',
272031
272059
  },
272032
- google: {
272033
- label: 'Google',
272034
- description: 'Natural-sounding voices from Google.',
272035
- },
272036
272060
  },
272037
272061
  voicePickerLabel: {
272038
272062
  openai: 'OpenAI voice',
272063
+ iblai: 'ibl.ai voice',
272039
272064
  google: 'Google voice',
272040
272065
  },
272041
272066
  searchPlaceholder: 'Search voices…',
@@ -272144,18 +272169,23 @@ function buildVoiceTabLabels(t) {
272144
272169
  label: t('voiceTabLabels.mentorVoice.providerOptions.browser.label'),
272145
272170
  description: t('voiceTabLabels.mentorVoice.providerOptions.browser.description'),
272146
272171
  },
272147
- openai: {
272148
- label: t('voiceTabLabels.mentorVoice.providerOptions.openai.label'),
272149
- description: t('voiceTabLabels.mentorVoice.providerOptions.openai.description'),
272150
- },
272151
272172
  google: {
272152
272173
  label: t('voiceTabLabels.mentorVoice.providerOptions.google.label'),
272153
272174
  description: t('voiceTabLabels.mentorVoice.providerOptions.google.description'),
272154
272175
  },
272176
+ iblai: {
272177
+ label: t('voiceTabLabels.mentorVoice.providerOptions.iblai.label'),
272178
+ description: t('voiceTabLabels.mentorVoice.providerOptions.iblai.description'),
272179
+ },
272180
+ openai: {
272181
+ label: t('voiceTabLabels.mentorVoice.providerOptions.openai.label'),
272182
+ description: t('voiceTabLabels.mentorVoice.providerOptions.openai.description'),
272183
+ },
272155
272184
  },
272156
272185
  voicePickerLabel: {
272157
272186
  openai: t('voiceTabLabels.mentorVoice.voicePickerLabel.openai'),
272158
272187
  google: t('voiceTabLabels.mentorVoice.voicePickerLabel.google'),
272188
+ iblai: t('voiceTabLabels.mentorVoice.voicePickerLabel.iblai'),
272159
272189
  },
272160
272190
  searchPlaceholder: t('voiceTabLabels.mentorVoice.searchPlaceholder'),
272161
272191
  emptyState: t('voiceTabLabels.mentorVoice.emptyState'),
@@ -272322,6 +272352,46 @@ function toTitleCase(name) {
272322
272352
  .map((word) => (word.length > 0 ? word[0].toUpperCase() + word.slice(1).toLowerCase() : word))
272323
272353
  .join(' ');
272324
272354
  }
272355
+ /**
272356
+ * On-device catalogue rows are stored under the model's own voice id -- two
272357
+ * letters of accent+gender, an underscore, then the voice's name
272358
+ * (`af_heart`, `bm_george`, `ef_dora`). Only the name is shown.
272359
+ *
272360
+ * Derived rather than mapped: the catalogue carries 50+ voices across several
272361
+ * languages and grows whenever the model does, so a hand-written table would
272362
+ * silently fall back to raw ids for everything it had not caught up with --
272363
+ * which is exactly what it did.
272364
+ */
272365
+ const KOKORO_VOICE_ID = /^[a-z]{2}_([a-z]+)$/;
272366
+ /**
272367
+ * What to show for a voice. Only the on-device provider needs unpacking;
272368
+ * every other catalogue already stores something human-readable, so it just
272369
+ * gets its casing normalised.
272370
+ *
272371
+ * A name that does not match the id shape falls through to the raw value, so
272372
+ * an unexpected row still renders instead of showing blank.
272373
+ */
272374
+ function voiceDisplayName(voice) {
272375
+ var _a;
272376
+ const name = (_a = voice.name) !== null && _a !== void 0 ? _a : '';
272377
+ if (isIblaiVoice(voice)) {
272378
+ const match = KOKORO_VOICE_ID.exec(name);
272379
+ if (match)
272380
+ return toTitleCase(match[1]);
272381
+ }
272382
+ return toTitleCase(name);
272383
+ }
272384
+ /**
272385
+ * Whether a catalogue row is an on-device (Kokoro) voice.
272386
+ *
272387
+ * The cast is deliberate: `VoiceProviderEnum` in the generated API client
272388
+ * predates this provider, so a direct comparison is a type error even though
272389
+ * the backend already returns `"iblai"` -- the same lag that leaves
272390
+ * `voice_provider` and `iblai_voice` off the generated mentor settings type.
272391
+ */
272392
+ function isIblaiVoice(voice) {
272393
+ return voice.provider === 'iblai';
272394
+ }
272325
272395
 
272326
272396
  const PAGE_SIZE$2 = 20;
272327
272397
  function VoicePicker({ org, userId, provider, selectedVoiceId, onSelect, disabled, labels, }) {
@@ -272367,10 +272437,36 @@ function VoicePicker({ org, userId, provider, selectedVoiceId, onSelect, disable
272367
272437
  !voices.some((v) => v.id === pinnedVoice.id);
272368
272438
  const { playingId, isLoading: isPreviewLoading, play, stop } = useAudioPreview();
272369
272439
  React__default.useEffect(() => () => stop(), [provider, stop]);
272370
- 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", { className: "rounded-md bg-blue-50/40 p-2", "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", {
272371
- // Padding (`p-1`) prevents the selected border from being
272372
- // clipped by `overflow-y-auto` on the first/last rows.
272373
- className: "grid max-h-[360px] grid-cols-1 gap-2 overflow-y-auto p-1 sm:grid-cols-2", 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" })] })] })] }))] }))] }));
272440
+ /**
272441
+ * Width the scrollbar takes out of the list's content box.
272442
+ *
272443
+ * Measured rather than assumed: it is ~15px on Windows and most Linux
272444
+ * desktops but 0 wherever the platform draws overlay scrollbars (macOS by
272445
+ * default), so a hardcoded value would misalign the list on half the
272446
+ * machines that see it. Used below to pull the list's right edge out by
272447
+ * exactly that much, so the cards line up with the search box above them
272448
+ * and the scrollbar sits in the dialog's own padding.
272449
+ */
272450
+ const listRef = React__default.useRef(null);
272451
+ const [scrollbarWidth, setScrollbarWidth] = React__default.useState(0);
272452
+ React__default.useLayoutEffect(() => {
272453
+ const el = listRef.current;
272454
+ if (!el)
272455
+ return;
272456
+ const measure = () => setScrollbarWidth(el.offsetWidth - el.clientWidth);
272457
+ measure();
272458
+ const observer = new ResizeObserver(measure);
272459
+ observer.observe(el);
272460
+ return () => observer.disconnect();
272461
+ }, [voices.length]);
272462
+ 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,
272463
+ // Vertical padding keeps the selected card's border from being
272464
+ // clipped by `overflow-y-auto` on the first and last rows. There is
272465
+ // deliberately no horizontal padding: the cards are meant to line
272466
+ // up with the search box, and the negative margin below moves the
272467
+ // scrollbar out of that alignment rather than letting it eat into
272468
+ // the last column.
272469
+ 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
272470
  }
272375
272471
  /**
272376
272472
  * A single voice row. Rendered as a `<div role="radio">` so the inner
@@ -272381,19 +272477,24 @@ function VoicePicker({ org, userId, provider, selectedVoiceId, onSelect, disable
272381
272477
  * clipped by the parent scroll container's `overflow-y-auto`.
272382
272478
  */
272383
272479
  function VoiceRow({ voice, isSelected, isPlaying, showPlayLoader, disabled, onSelect, play, labels, }) {
272384
- return (jsxs("div", { role: "radio", tabIndex: disabled ? -1 : 0, "aria-checked": isSelected, "aria-disabled": disabled || undefined, "aria-label": toTitleCase(voice.name), "data-voice-id": voice.id, "data-selected": isSelected || undefined, onClick: () => !disabled && onSelect(voice), onKeyDown: (e) => {
272480
+ // Whether a second line renders under the name. It decides the cross-axis
272481
+ // alignment: with a description the 36px badge should sit level with the
272482
+ // first line (`items-start`), but on a single-line card that pushes the
272483
+ // badge 4px below the name and preview button, which reads as crooked.
272484
+ const hasMeta = !isIblaiVoice(voice) && Boolean(voice.language || voice.description);
272485
+ 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
272486
  if (disabled)
272386
272487
  return;
272387
272488
  if (e.key === ' ' || e.key === 'Enter') {
272388
272489
  e.preventDefault();
272389
272490
  onSelect(voice);
272390
272491
  }
272391
- }, className: cn('flex cursor-pointer items-start gap-3 rounded-lg border-2 p-3 text-left transition-all hover:shadow-sm focus-visible:outline-blue-500', isSelected
272492
+ }, 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
272493
  ? '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: toTitleCase(voice.name) }), 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) => {
272394
- e.stopPropagation();
272395
- play(voice.id, voice.audio_url);
272396
- }, disabled: disabled, "aria-label": labels.previewAria(toTitleCase(voice.name), 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" })) }))] }), (voice.language || voice.description) && (jsx("p", { className: "mt-1 line-clamp-2 text-xs text-gray-500", children: [voice.language, voice.description].filter(Boolean).join(' · ') }))] })] }));
272494
+ : '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) => {
272495
+ e.stopPropagation();
272496
+ play(voice.id, voice.audio_url);
272497
+ }, 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
272498
  }
272398
272499
 
272399
272500
  /**
@@ -272433,8 +272534,10 @@ function VoicePickerTrigger({ org, userId, provider, selectedVoice, onSelect, di
272433
272534
  if (isOpen)
272434
272535
  stop();
272435
272536
  }, [isOpen, stop]);
272436
- const displayName = selectedVoice ? toTitleCase(selectedVoice.name) : null;
272437
- const language = (selectedVoice === null || selectedVoice === void 0 ? void 0 : selectedVoice.language) || null;
272537
+ const displayName = selectedVoice ? voiceDisplayName(selectedVoice) : null;
272538
+ // See the note in voice-picker.tsx: the on-device rows all carry the same
272539
+ // locale, so showing it adds nothing.
272540
+ const language = selectedVoice && isIblaiVoice(selectedVoice) ? null : (selectedVoice === null || selectedVoice === void 0 ? void 0 : selectedVoice.language) || null;
272438
272541
  const audioUrl = (selectedVoice === null || selectedVoice === void 0 ? void 0 : selectedVoice.audio_url) || '';
272439
272542
  const isPlaying = selectedVoice != null && playingId === selectedVoice.id;
272440
272543
  const showPlayLoader = isPlaying && isPreviewLoading;
@@ -272738,6 +272841,29 @@ function PromptCard$1({ label, placeholder, value, disabled, renderContent, onEd
272738
272841
  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
272842
  }
272740
272843
 
272844
+ /**
272845
+ * Providers whose voice comes from the server-side voices catalogue, and so
272846
+ * need a voice picker plus a voice id on save. Only `browser` is excluded: it
272847
+ * speaks through whatever voice the listener's own OS provides, which we
272848
+ * neither choose nor know.
272849
+ *
272850
+ * `iblai` synthesises on-device but still picks from the catalogue, because
272851
+ * the backend stores `iblai_voice` as a foreign key into the same table --
272852
+ * sending it a voice name is rejected with "A valid integer is required".
272853
+ */
272854
+ const CATALOGUE_VOICE_PROVIDERS = ['openai', 'google', 'iblai'];
272855
+ /**
272856
+ * Voice used when a mentor has never had one chosen.
272857
+ *
272858
+ * Must stay in step with the consuming app's own fallback (mentorai's
272859
+ * `DEFAULT_VOICE` in `lib/tts/config.ts`): the agent speaks that voice whether
272860
+ * or not anything is saved, so showing an empty picker would tell the admin
272861
+ * "nothing selected" while the listener hears Heart.
272862
+ */
272863
+ const DEFAULT_IBLAI_VOICE = 'af_heart';
272864
+ function usesVoiceCatalogue(provider) {
272865
+ return CATALOGUE_VOICE_PROVIDERS.includes(provider);
272866
+ }
272741
272867
  const SUB_TAB_VALUES = {
272742
272868
  voice: 'voice',
272743
272869
  callConfig: 'call-config',
@@ -272764,7 +272890,7 @@ function readVoiceName(value) {
272764
272890
  return null;
272765
272891
  }
272766
272892
  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;
272893
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
272768
272894
  const ctx = useAgentSettingsOptional();
272769
272895
  const tenantKey = (_a = tenantKeyProp !== null && tenantKeyProp !== void 0 ? tenantKeyProp : ctx === null || ctx === void 0 ? void 0 : ctx.tenantKey) !== null && _a !== void 0 ? _a : '';
272770
272896
  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 +272912,10 @@ function AgentVoiceTab({ labels: labelsOverride, defaultSubTab = 'voice', render
272786
272912
  // (with audio_url for the play button) and send the id on save.
272787
272913
  const savedOpenaiName = (_f = readVoiceName(mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.openai_voice)) !== null && _f !== void 0 ? _f : null;
272788
272914
  const savedGoogleName = (_g = readVoiceName(mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.google_voice)) !== null && _g !== void 0 ? _g : null;
272915
+ // Falls back to the default rather than null so the trigger shows the voice
272916
+ // that will actually be used. Nothing is written to the mentor by this: an
272917
+ // unsaved default and an explicitly-chosen Heart behave identically.
272918
+ const savedIblaiName = (_h = readVoiceName(mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.iblai_voice)) !== null && _h !== void 0 ? _h : DEFAULT_IBLAI_VOICE;
272789
272919
  const { voice: resolvedOpenaiVoice } = useResolveVoiceByName({
272790
272920
  org: tenantKey,
272791
272921
  userId: username,
@@ -272798,28 +272928,37 @@ function AgentVoiceTab({ labels: labelsOverride, defaultSubTab = 'voice', render
272798
272928
  provider: 'google',
272799
272929
  name: savedGoogleName,
272800
272930
  });
272931
+ const { voice: resolvedIblaiVoice } = useResolveVoiceByName({
272932
+ org: tenantKey,
272933
+ userId: username,
272934
+ provider: 'iblai',
272935
+ name: savedIblaiName,
272936
+ });
272801
272937
  const savedInstructions = typeof (mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.voice_instructions) === 'string' ? mentorSettings.voice_instructions : '';
272802
272938
  // Picked voices override the resolved server values once the user
272803
272939
  // makes a choice. Null means "use what came from the server".
272804
272940
  const [provider, setProvider] = React__default.useState(initialProvider);
272805
272941
  const [pickedOpenaiVoice, setPickedOpenaiVoice] = React__default.useState(null);
272806
272942
  const [pickedGoogleVoice, setPickedGoogleVoice] = React__default.useState(null);
272943
+ const [pickedIblaiVoice, setPickedIblaiVoice] = React__default.useState(null);
272807
272944
  const [instructions, setInstructions] = React__default.useState(savedInstructions);
272808
272945
  const [selectedPrompt, setSelectedPrompt] = React__default.useState(null);
272809
272946
  React__default.useEffect(() => {
272810
272947
  setProvider(initialProvider);
272811
272948
  setPickedOpenaiVoice(null);
272812
272949
  setPickedGoogleVoice(null);
272950
+ setPickedIblaiVoice(null);
272813
272951
  setInstructions(savedInstructions);
272814
272952
  // eslint-disable-next-line react-hooks/exhaustive-deps
272815
272953
  }, [mentorId, rawMentorSettings]);
272816
- const effectiveOpenaiVoice = (_h = pickedOpenaiVoice !== null && pickedOpenaiVoice !== void 0 ? pickedOpenaiVoice : resolvedOpenaiVoice) !== null && _h !== void 0 ? _h : null;
272817
- const effectiveGoogleVoice = (_j = pickedGoogleVoice !== null && pickedGoogleVoice !== void 0 ? pickedGoogleVoice : resolvedGoogleVoice) !== null && _j !== void 0 ? _j : null;
272954
+ const effectiveOpenaiVoice = (_j = pickedOpenaiVoice !== null && pickedOpenaiVoice !== void 0 ? pickedOpenaiVoice : resolvedOpenaiVoice) !== null && _j !== void 0 ? _j : null;
272955
+ const effectiveGoogleVoice = (_k = pickedGoogleVoice !== null && pickedGoogleVoice !== void 0 ? pickedGoogleVoice : resolvedGoogleVoice) !== null && _k !== void 0 ? _k : null;
272956
+ const effectiveIblaiVoice = (_l = pickedIblaiVoice !== null && pickedIblaiVoice !== void 0 ? pickedIblaiVoice : resolvedIblaiVoice) !== null && _l !== void 0 ? _l : null;
272818
272957
  const [editMentor, { isLoading: isSavingMentor }] = useEditMentorMutation();
272819
272958
  // "Enable voice calls" master toggle. Optimistic local mirror of the
272820
272959
  // server value so the switch responds instantly; re-synced whenever the
272821
272960
  // settings response changes.
272822
- const serverVoiceCallEnabled = (_k = mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.show_voice_call) !== null && _k !== void 0 ? _k : true;
272961
+ const serverVoiceCallEnabled = (_m = mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.show_voice_call) !== null && _m !== void 0 ? _m : true;
272823
272962
  const [voiceCallEnabled, setVoiceCallEnabled] = React__default.useState(serverVoiceCallEnabled);
272824
272963
  const [isTogglingVoiceCall, setIsTogglingVoiceCall] = React__default.useState(false);
272825
272964
  React__default.useEffect(() => {
@@ -272855,33 +272994,48 @@ function AgentVoiceTab({ labels: labelsOverride, defaultSubTab = 'voice', render
272855
272994
  const isDirty = provider !== initialProvider ||
272856
272995
  pickedOpenaiVoice != null ||
272857
272996
  pickedGoogleVoice != null ||
272997
+ pickedIblaiVoice != null ||
272858
272998
  instructionsDirty;
272859
272999
  /**
272860
- * Provider chips. We reuse the OpenAI / Google logos already shipped in
272861
- * the host's static-root (see `getLLMProviderDetails` for the canonical
272862
- * paths). Browser falls back to a Lucide globe there's no vendor logo
272863
- * for "device speech engine".
273000
+ * Provider chips, ordered by lowercasing each label and then sorting
273001
+ * alphabetically, so a newly added provider slots itself in without a
273002
+ * hand-edit. We reuse the vendor logos already shipped in the host's
273003
+ * static-root (see `getLLMProviderDetails` for the canonical paths).
273004
+ * Browser falls back to a Lucide globe — there's no vendor logo for
273005
+ * "device speech engine".
272864
273006
  */
272865
- const providerOptions = [
272866
- {
272867
- value: 'browser',
272868
- label: labels.mentorVoice.providerOptions.browser.label,
272869
- description: labels.mentorVoice.providerOptions.browser.description,
272870
- logo: { type: 'icon', Icon: Globe },
272871
- },
272872
- {
272873
- value: 'openai',
272874
- label: labels.mentorVoice.providerOptions.openai.label,
272875
- description: labels.mentorVoice.providerOptions.openai.description,
272876
- logo: { type: 'image', src: '/llm-openai-provider-2.svg', alt: 'OpenAI' },
272877
- },
272878
- {
272879
- value: 'google',
272880
- label: labels.mentorVoice.providerOptions.google.label,
272881
- description: labels.mentorVoice.providerOptions.google.description,
272882
- logo: { type: 'image', src: '/llm-google-provider.svg', alt: 'Google' },
272883
- },
272884
- ];
273007
+ const providerOptions = React__default.useMemo(() => {
273008
+ const options = [
273009
+ {
273010
+ value: 'browser',
273011
+ label: labels.mentorVoice.providerOptions.browser.label,
273012
+ description: labels.mentorVoice.providerOptions.browser.description,
273013
+ logo: { type: 'icon', Icon: Globe },
273014
+ },
273015
+ {
273016
+ value: 'google',
273017
+ label: labels.mentorVoice.providerOptions.google.label,
273018
+ description: labels.mentorVoice.providerOptions.google.description,
273019
+ logo: { type: 'image', src: '/llm-google-provider.svg', alt: 'Google' },
273020
+ },
273021
+ {
273022
+ value: 'iblai',
273023
+ label: labels.mentorVoice.providerOptions.iblai.label,
273024
+ description: labels.mentorVoice.providerOptions.iblai.description,
273025
+ logo: { type: 'image', src: '/llm-iblai-provider.png', alt: 'ibl.ai' },
273026
+ },
273027
+ {
273028
+ value: 'openai',
273029
+ label: labels.mentorVoice.providerOptions.openai.label,
273030
+ description: labels.mentorVoice.providerOptions.openai.description,
273031
+ logo: { type: 'image', src: '/llm-openai-provider-2.svg', alt: 'OpenAI' },
273032
+ },
273033
+ ];
273034
+ // Lowercase first, then compare. A case-sensitive sort would rank every
273035
+ // capitalised label ahead of the lowercase "ibl.ai" and push it past
273036
+ // OpenAI.
273037
+ return [...options].sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase()));
273038
+ }, [labels.mentorVoice.providerOptions]);
272885
273039
  const handleSaveVoice = async () => {
272886
273040
  try {
272887
273041
  const formData = { voice_provider: provider };
@@ -272894,6 +273048,9 @@ function AgentVoiceTab({ labels: labelsOverride, defaultSubTab = 'voice', render
272894
273048
  if (provider === 'google' && pickedGoogleVoice) {
272895
273049
  formData.google_voice = pickedGoogleVoice.id;
272896
273050
  }
273051
+ if (provider === 'iblai' && pickedIblaiVoice) {
273052
+ formData.iblai_voice = pickedIblaiVoice.id;
273053
+ }
272897
273054
  // Same "absent = keep" contract: only send voice_instructions when
272898
273055
  // the user actually edited it. Clearing sends "" (never null) —
272899
273056
  // that's what resets the stored value server-side.
@@ -272914,16 +273071,21 @@ function AgentVoiceTab({ labels: labelsOverride, defaultSubTab = 'voice', render
272914
273071
  toast.error(labels.toasts.voiceError);
272915
273072
  }
272916
273073
  };
273074
+ // Only `browser` has no voice to show -- it uses the listener's OS voice.
272917
273075
  const selectedVoice = provider === 'openai'
272918
273076
  ? effectiveOpenaiVoice
272919
273077
  : provider === 'google'
272920
273078
  ? effectiveGoogleVoice
272921
- : null;
273079
+ : provider === 'iblai'
273080
+ ? effectiveIblaiVoice
273081
+ : null;
272922
273082
  const onSelectVoice = (voice) => {
272923
273083
  if (provider === 'openai')
272924
273084
  setPickedOpenaiVoice(voice);
272925
273085
  else if (provider === 'google')
272926
273086
  setPickedGoogleVoice(voice);
273087
+ else if (provider === 'iblai')
273088
+ setPickedIblaiVoice(voice);
272927
273089
  };
272928
273090
  // Mirrors `AgentScreenShareTab`: hosts can render prompt content as
272929
273091
  // Markdown via `renderPromptContent`; plain text otherwise.
@@ -272934,19 +273096,19 @@ function AgentVoiceTab({ labels: labelsOverride, defaultSubTab = 'voice', render
272934
273096
  };
272935
273097
  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
273098
  // @ts-expect-error permissions.field is opaque
272937
- permissions: (_l = mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.permissions) === null || _l === void 0 ? void 0 : _l.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-3", role: "radiogroup", "aria-label": labels.mentorVoice.providerLabel, children: providerOptions.map((option) => {
273099
+ 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
273100
  const isSelected = provider === option.value;
272939
273101
  const isDisabledOption = isDisabled || disabled;
272940
273102
  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
273103
  ? 'border-blue-500 bg-blue-50/40'
272942
273104
  : '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 === 'openai' || provider === 'google') && (jsx(WithFormPermissions, { name: provider === 'openai' ? 'openai_voice' : 'google_voice',
273105
+ }) })] })) }), usesVoiceCatalogue(provider) && (jsx(WithFormPermissions, { name: `${provider}_voice`,
272944
273106
  // @ts-expect-error permissions.field is opaque
272945
- permissions: (_m = mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.permissions) === null || _m === void 0 ? void 0 : _m.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', {
273107
+ 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
273108
  voiceType: labels.mentorVoice.voicePickerLabel[provider],
272947
273109
  }), modalDescription: t('tabsVoiceTab.selectVoiceModalDescription'), testId: "mentor-voice-trigger" })] })) })), (provider === 'openai' || provider === 'google') && (jsx(WithFormPermissions, { name: "voice_instructions",
272948
273110
  // @ts-expect-error permissions.field is opaque
272949
- permissions: (_o = mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.permissions) === null || _o === void 0 ? void 0 : _o.field, enableRBAC: enableRBAC, children: ({ disabled }) => {
273111
+ permissions: (_q = mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.permissions) === null || _q === void 0 ? void 0 : _q.field, enableRBAC: enableRBAC, children: ({ disabled }) => {
272950
273112
  const isFieldDisabled = isDisabled || disabled;
272951
273113
  const presetKeys = ['warm', 'calm', 'energetic'];
272952
273114
  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 +273121,7 @@ function AgentVoiceTab({ labels: labelsOverride, defaultSubTab = 'voice', render
272959
273121
  }) }), 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
273122
  } })), 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
273123
  ? 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: (_p = mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.call_configuration) !== null && _p !== void 0 ? _p : null, renderPromptContent: renderPromptContent, toastLabels: {
273124
+ : 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
273125
  saved: labels.toasts.callConfigSaved,
272964
273126
  error: labels.toasts.callConfigError,
272965
273127
  } }))] })] }), selectedPrompt && (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iblai/iblai-js",
3
- "version": "2.5.5",
3
+ "version": "2.5.7",
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": {
@@ -76,10 +76,10 @@
76
76
  "axios": "1.13.6",
77
77
  "dotenv": "16.6.1",
78
78
  "winston": "3.19.0",
79
- "@iblai/mcp": "1.10.4",
79
+ "@iblai/mcp": "1.10.5",
80
80
  "@iblai/data-layer": "1.12.106",
81
- "@iblai/web-containers": "1.18.5",
82
- "@iblai/web-utils": "2.1.16"
81
+ "@iblai/web-containers": "1.18.7",
82
+ "@iblai/web-utils": "2.1.17"
83
83
  },
84
84
  "peerDependencies": {
85
85
  "@iblai/iblai-api": "4.166.0-ai",