@iblai/iblai-js 2.7.1 → 2.7.3

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.
@@ -3,7 +3,7 @@ import * as React from 'react';
3
3
  import React__default, { useState, useEffect, forwardRef, createElement as createElement$3, useLayoutEffect, createContext, useContext, useMemo, useReducer, useRef, useImperativeHandle, useCallback, isValidElement, Children, PureComponent, cloneElement, Component as Component$1, useId as useId$2, useDebugValue, lazy, Suspense } from 'react';
4
4
  import * as dataLayer from '@iblai/data-layer';
5
5
  import { useGetUserMetadataQuery, useGetUserMetadataEdxQuery, useUpdateUserMetadataMutation, useUpdateUserMetadataEdxMutation, useUploadProfileImageMutation, useResetPasswordMutation, useSelfRetireMutation, useCreateUserInstitutionMutation, useGetUserInstitutionsQuery, useCreateUserEducationMutation, useUpdateUserEducationMutation, useDeleteUserEducationMutation, useGetUserEducationQuery, useCreateUserCompanyMutation, useGetUserCompaniesQuery, useCreateUserExperienceMutation, useUpdateUserExperienceMutation, useDeleteUserExperienceMutation, useGetUserExperienceQuery, useGetUserResumeQuery, useCreateUserResumeMutation, useGetMySubscriptionsQuery, useGetItemSubscriptionQuery, useCancelSubscriptionMutation, useGetLearnerDetailsQuery, useGetUserEnrolledCoursesQuery, useGetUserEnrolledProgramsQuery, useGetUserCatalogPathwaysQuery, useGetUsersAsAssertionsQuery, useGetUserEarnedSkillsQuery, useGetUserReportedSkillsQuery, useGetUserDesiredSkillsQuery, useCreateGlobalMemoryMutation, useUpdateGlobalMemoryMutation, useUpdateMentorMemoryMutation, useGetMemsearchStatusQuery, useGetUserMemorySettingsQuery, useUpdateUserMemorySettingsMutation, useGetGlobalMemoriesQuery, useGetAllMentorMemoriesQuery, useDeleteGlobalMemoryMutation, useDeleteMentorMemoryMutation, useGetAiSearchMentorsQuery, useStarMentorMutation, useUnstarMentorMutation, useGetUserChatHistoryReportsQuery, USER_CHAT_HISTORY_REPORT_ACTIVE_STATES, useCreateUserChatHistoryReportMutation, useLazyGetDownloadReportFromURLQuery, useGetUserChatHistoryReportStatusQuery, userChatHistoryApiSlice, USER_CHAT_HISTORY_QUERY_KEYS, useGetUserChatHistoryQuery, useGetUserChatHistoryFilterQuery, useGetMentorsQuery, useGetUserChatHistorySessionQuery, useInviteUserMutation, usePlatformInvitationsQuery, useCreateCatalogInvitationCourseBulkMutation, useGetCatalogInvitationsCourseQuery, useLazyPlatformUsersQuery, useLazyPlatformUserGroupsQuery, useGetPersonnalizedSearchQuery, useCreateCatalogInvitationProgramBulkMutation, useGetCatalogInvitationsProgramQuery, useUpdateUserRoleMutation, useUpdateUserStatusMutation, useUpdatePlatformUserRoleWithPoliciesMutation, usePlatformUsersQuery, useGetAccessibleUsersQuery, isPoliciesResponse, platformApiSlice, featureTags, useLazyGetRbacTeamsAccessListQuery, useCreateRbacTeamsAccessMutation, useGetRbacGroupsQuery, usePlatformUserGroupsQuery, useCreateRbacGroupMutation, useUpdateRbacGroupMutation, useDeleteRbacGroupMutation, useCreatePlatformUserGroupMutation, useUpdatePlatformUserGroupMutation, useDeletePlatformUserGroupMutation, useGetRbacGroupDetailsQuery, useGetPlatformUserGroupDetailsQuery, useGetRbacPermissionsMutation, useGetRbacRolesQuery, useCreateRbacRoleMutation, useUpdateRbacRoleMutation, useDeleteRbacRoleMutation, useGetRbacRoleDetailsQuery, useGetRbacActionsDefinitionQuery, useGetRbacResourceDiscoveryQuery, useGetRbacResourceListQuery, useGetRbacPoliciesQuery, useCreateRbacPolicyMutation, useUpdateRbacPolicyMutation, useDeleteRbacPolicyMutation, useGetRbacPolicyDetailsQuery, useGetWatchedGroupsQuery, useCreateWatchedGroupMutation, useUpdateWatchedGroupMutation, useDeleteWatchedGroupMutation, useAddWatchedUserMutation, useRemoveWatchedUserMutation, useAddWatcherMutation, useUpdateWatcherMutation, useDeleteWatcherMutation, useGetWatchedUsersQuery, useGetWatchersQuery, WATCHER_NOTIFICATION_EVENTS, WATCHER_NOTIFICATION_EVENT_LABELS, useGetApplicationFormsQuery, useGetApplicationFormDetailQuery, useCreateApplicationFormMutation, useUpdateApplicationFormMutation, useGetApplicationAdminDetailQuery, useInviteApplicationCandidateMutation, useLazyGetApplicationAttachmentFileQuery, useCreateApplicationWaiverMutation, useDeleteApplicationWaiverMutation, useSubmitApplicationOverrideMutation, useUpdateApplicationPlacementMutation, useDeleteApplicationCourseAssignmentMutation, useCreateApplicationNoteMutation, useUpdateApplicationNoteMutation, useDeleteApplicationNoteMutation, useTransitionApplicationMutation, useLinkApplicationCandidateMutation, useCreateApplicationPlacementsMutation, useGetCustomMentorsQuery, useCreateApplicationCourseAssignmentsMutation, useGetApplicationBlocksQuery, useDeleteApplicationBlockMutation, useGetApplicationsPipelineStatsQuery, useGetApplicationsPipelineQuery, useGetTenantMetadataQuery, useUpdateTenantMetadataMutation, useLazyGetTenantMetadataQuery, useDeleteApiKeyMutation, useGetApiKeyQuery, useGetApiKeyFieldPermissionsQuery, useUpdateApiKeyMutation, useGetApiKeysQuery, unwrapApiTokenList, useCreateApiKeyMutation, useCreateLLMCredentialMutation, useGetCredentialsSchemaQuery, useGetMaskedLLMCredentialsQuery, useGetLlmsQuery, useDeleteIntegrationCredentialMutation, useDeleteCredentialMutation, useCreateIntegrationCredentialMutation, useGetIntegrationCredentialsSchemaQuery, useGetMaskedIntegrationCredentialsQuery, useGetAccountBillingInfoQuery, useUpdateAutoRechargeInfoMutation, useTriggerAutoRechargeMutation, useGetAgentSpendCapQuery, useGetTenantSpendCapQuery, useListAgentUserSpendCapsQuery, useUpsertAgentSpendCapMutation, useDeleteAgentSpendCapMutation, useUpsertTenantSpendCapMutation, useDeleteTenantSpendCapMutation, useUpsertUserSpendCapMutation, useDeleteUserSpendCapMutation, useGetFinancialStatsQuery, useListAgentSpendCapsQuery, useGetCreditTransactionsQuery, useCreateStripeCustomerPortalMutation, useSetPlatformConfigurationsMutation, useGetPlatformConfigurationsQuery, useUpdatePlatformMembershipMutation, useGetPlatformMembershipQuery, useGetCustomDomainsQuery, useCreateCustomDomainMutation, useDeleteCustomDomainMutation, useGetStudentMentorCreationStatusQuery, useSetStudentMentorCreationStatusMutation, coreApiSlice, recommendationPromptTypeEnum, useGetRecommendedPromptsListQuery, useCreateRecommendedPromptMutation, useUpdateRecommendedPromptMutation, useDeleteRecommendedPromptMutation, useLazyGetPublicPlatformImageAssetFileUrlQuery, useCreatePlatformImageAssetMutation, useGetProviderConfigQuery, useCreateProviderConfigMutation, useDeleteProviderConfigMutation, useGetExternalMappingQuery, useGetCredentialsListQuery, useCreateExternalMappingMutation, useDeleteExternalMappingMutation, useGetMemsearchConfigQuery, useUpdateMemsearchConfigMutation, useGetStripeConnectStatusQuery, useStartStripeConnectOnboardingMutation, useLazyGetStripeConnectDashboardQuery, useListPaywallsQuery, useListPricesQuery, useCreatePriceMutation, useUpdatePriceMutation, useDeletePriceMutation, useGetPaywallConfigQuery, useEnablePaywallMutation, useUpdatePaywallMutation, useLazyGetCourseMetaDataQuery, useLazyGetCourseCompletionOutlinesQuery, useLazyGetCourseEligibilityQuery, useLazyGetEdxSSOTokenQuery, useCreateCourseEnrollmentMutation, useCreateStripeCheckoutSessionMutation, useLazyGetCourseProgressQuery, useLazyGetCourseCompletionQuery, useUpdateExamAttemptMutation, useStartExamMutation, useLazyGetExamInfoQuery, useGetAgentSkillResourcesQuery, useCreateAgentSkillResourceMutation, useUpdateAgentSkillResourceMutation, useUploadAgentSkillResourceAssetMutation, useDeleteAgentSkillResourceMutation, useGetMentorSkillAssignmentsQuery, useGetAgentSkillsQuery, useCreateMentorSkillAssignmentMutation, useUpdateMentorSkillAssignmentMutation, useDeleteMentorSkillAssignmentMutation, useCreateAgentSkillMutation, useUpdateAgentSkillMutation, useDeleteAgentSkillMutation, filterSlashSkills, useGetMentorSkillAssignmentsInfiniteQuery, MENTOR_SKILL_ASSIGNMENTS_PAGE_SIZE, useUpdateUserProjectMutation, useGetTrainingDocumentsQuery, useGetVectorDocumentsQuery, useGetMentorMemoriesListQuery, useGetMemoryCategoriesAdminQuery, useCreateMentorMemoryMutation, useLazyGetConnectedServiceAuthUrlQuery, useCreateUserProjectMutation, useDeleteUserProjectMutation, useGetUserProjectDetailsQuery, useEditTrainingDocumentMutation, useAddTrainingDocumentMutation, useLazyGetCredentialsQuery, useGetMentorSettingsQuery, useEditMentorMutation, useUploadLightLogoMutation, useUploadDarkLogoMutation, useUpdatePlatformInfoMutation, LOGO_ENDPOINTS, useCreateEvalDatasetMutation, useAddEvalDatasetItemsMutation, useUploadEvalDatasetCsvMutation, useDeleteEvalDatasetItemMutation, useListEvalDatasetItemsQuery, evalTotalPages, useListEvalDatasetsQuery, useGetUserProjectsQuery, useCreateSessionIdMutation, useGetMentorCategoriesQuery, useGetTenantChatPrivacyConfigQuery as useGetTenantChatPrivacyConfigQuery$1, useGetCallConfigurationsQuery, useCreateCallConfigurationMutation, useUpdateCallConfigurationMutation, chatPrivacyApiSlice, useDeleteMentorMutation, useForkMentorMutation, useGetToolsQuery, useLazyGetMCPServersQuery, useOauthFindMutation, useLazyStartOAuthFlowQuery, useCreateMCPServerMutation, usePartialUpdateMCPServerMutation, useCreateMCPServerConnectionMutation, usePatchMCPServerConnectionMutation, useGetConnectedServicesQuery, useGetMCPServersQuery, useGetMCPServerConnectionsQuery, useUpdateMCPServerMutation, useDeleteMCPServerMutation, useEditMentorJsonMutation, useDisconnectServiceMutation, useGetPromptCategoriesQuery, PRIVACY_ACTIONS, PRIVACY_ENTITY_TYPES, useGetVoicesQuery, useGetVoiceQuery, isBaseAgentMentor, useGetGradeResultQuery, useOverrideGradeResultMutation, useGetChatHistoryFilterQuery, useListGradeResultsQuery, useGetMentorGraderConfigurationQuery, useListGraderCriteriaQuery, useCreateMentorGraderConfigurationMutation, useUpdateMentorGraderConfigurationMutation, useCreateGraderCriterionMutation, useUpdateGraderCriterionMutation, useDeleteGraderCriterionMutation, useGetSpendCapStatusQuery, useGetMentorPublicSettingsQuery, useGetChatHistoryQuery, useGetMentorSummariesQuery, useGetConversationMemoriesQuery, useGetSupportTicketsQuery, useGetSupportTicketMessagesQuery, useCreateSupportTicketMessageMutation, usePatchSupportTicketMutation, useCloseSupportTicketMutation, useCreateLtiMentorMutation, useUpdateLtiMentorMutation, useGetLtiMentorsQuery, useDeleteLtiMentorMutation, useCreateLtiKeyMutation, useUpdateLtiKeyMutation, useDeleteLtiKeyMutation, useGetLtiKeysQuery, useCreateLtiToolMutation, useUpdateLtiToolMutation, useGetLtiToolsQuery, useGetPlatformInfoQuery, useCreatePromptMutation, useGetPromptsSearchQuery, useUpdatePromptMutation, useCreateRedirectTokenMutation, useGetShareableLinkQuery, useCreateShareableLinkMutation, useUpdateShareableLinkMutation, useCreateDisclaimerMutation, useUpdateDisclaimerMutation, useGetDisclaimersQuery, useUpdateRbacMentorAccessMutation, useGetRbacMentorAccessListQuery, useStartEvalRunMutation, useEvaluateEvalRunMutation, useGetEvalRunQuery, useDeleteEvalScoreMutation, useListEvalRunJudgesQuery, useCreateEvalScoreMutation, useDeleteEvalRunMutation, useExportEvalRunMutation, useListEvalRunsQuery, useLazyGetEvalRunQuery, evaluationsApiSlice, EVALUATIONS_TAGS, useGetPersonnalizedMentorsQuery, useCreateCallCredentialsMutation, useGetGuidedPromptsQuery, useUpdateChatSessionSharedMutation, useUpdateMessageFeedbackMutation, useLazyGetPromptsSearchQuery, useLazyGetGuidedPromptsQuery, useGetPublicMentorsQuery, useGetPeriodicAgentsQuery, useGetPeriodicAgentLogsListQuery, useCreatePeriodicAgentMutation, useDeletePeriodicAgentMutation, useDeletePromptMutation, useDeleteTrainingDocumentMutation, useGetTrainingDocumentRetrainScheduleQuery, useCreateTrainingDocumentRetrainScheduleMutation, useCreateMemoryCategoryMutation, useUpdateMemoryCategoryMutation, useDeleteMemoryCategoryMutation, useUpdateArtifactMutation, useLazyGetArtifactVersionQuery, useLazyListArtifactVersionsQuery, useSetCurrentVersionMutation, useListArtifactVersionsQuery, useLazyGetArtifactQuery, useLazyListArtifactsQuery, useEditSessionMutation } from '@iblai/data-layer';
6
- import { getInitials, useTenantMetadata, useCurrentTenant, selectRbacPermissions, isPureWatcher, WithPermissions, canWriteField, useStripeUpgrade, openExternalUrl, CHAT_AREA_SIZE, isAlphaNumeric32, checkRbacPermission, selectNumberOfActiveChatMessages, useUsername, selectStreaming, isLoggedIn, TOOLS, hasRemoteAiConfig, isSafariBrowser, useShowAttachment, useShowVoiceCall, useShowVoiceRecorder, useMentorSettings, selectShowingSharedChat, useShowFreeTrialDialog, useEmbedMode, chatInputSliceSelectors, useResponsive, useAccessingPublicRoute, useVisitingTenant, useModelFileUploadCapabilities, useChatFileUpload, useVoiceChat, selectAttachedFiles, MENTOR_CHAT_DOCUMENTS_EXTENSIONS, removeFile, chatInputSliceActions, chatActions, WithFormPermissions, useTenantContext, TimeTracker, advancedTabsProperties, defaultSessionIds, pickWritableFields, ANONYMOUS_USERNAME as ANONYMOUS_USERNAME$3, redirectToAuthSpaJoinTenant, selectActiveTab, useAxdToken, useWelcomeMessage, markdownToPlainText, useCachedSessionId, use402ErrorCheck, selectTokenEnabled, selectToken, useServiceWorker, useUserAgreement, useAdvancedChat, sendMessageToParentWebsite, useMentorTools, useFileDragDrop, eventBus, RemoteEvents, selectEnableChatActionsPopup, advancedTabs, isInIframe, getAuthSpaJoinUrl, addMessage, clearFiles, MENTOR_VISIBILITY } from '@iblai/web-utils';
6
+ import { removeAuthItem, setAuthItem, getInitials, useTenantMetadata, useCurrentTenant, selectRbacPermissions, isPureWatcher, WithPermissions, canWriteField, useStripeUpgrade, openExternalUrl, CHAT_AREA_SIZE, isAlphaNumeric32, checkRbacPermission, selectNumberOfActiveChatMessages, useUsername, selectStreaming, isLoggedIn, TOOLS, hasRemoteAiConfig, isSafariBrowser, useShowAttachment, useShowVoiceCall, useShowVoiceRecorder, useMentorSettings, selectShowingSharedChat, useShowFreeTrialDialog, useEmbedMode, chatInputSliceSelectors, useResponsive, useAccessingPublicRoute, useVisitingTenant, useModelFileUploadCapabilities, useChatFileUpload, useVoiceChat, selectAttachedFiles, MENTOR_CHAT_DOCUMENTS_EXTENSIONS, removeFile, chatInputSliceActions, chatActions, WithFormPermissions, useTenantContext, TimeTracker, advancedTabsProperties, defaultSessionIds, pickWritableFields, ANONYMOUS_USERNAME as ANONYMOUS_USERNAME$3, redirectToAuthSpaJoinTenant, selectActiveTab, useAxdToken, useWelcomeMessage, markdownToPlainText, useCachedSessionId, use402ErrorCheck, selectTokenEnabled, selectToken, useServiceWorker, useUserAgreement, useAdvancedChat, sendMessageToParentWebsite, useMentorTools, useFileDragDrop, eventBus, RemoteEvents, selectEnableChatActionsPopup, advancedTabs, isInIframe, getAuthSpaJoinUrl, addMessage, clearFiles, MENTOR_VISIBILITY } from '@iblai/web-utils';
7
7
  import { useDispatch, useSelector } from 'react-redux';
8
8
  import { toast } from 'sonner';
9
9
  import { useSearchParams, useParams, useRouter, usePathname } from 'next/navigation';
@@ -101,17 +101,21 @@ const initializeLocalStorageWithObject = async (data, localStorageKeys) => {
101
101
  deleteCookie('ibl_tenant');
102
102
  // Clear visiting_tenant to prevent stale tenant data from causing redirect issues
103
103
  // This is important during tenant switches where old visiting_tenant could redirect back to previous tenant
104
- localStorage.removeItem('visiting_tenant');
104
+ // Route through authStorage: under NEXT_PUBLIC_IBL_PER_TAB_AUTH this installs
105
+ // the session into sessionStorage (this tab's source of truth) AND mirrors the
106
+ // most-recent login into localStorage (the seed); with the flag off it is
107
+ // exactly today's localStorage behavior.
108
+ removeAuthItem('visiting_tenant');
105
109
  // Clear current_tenant to ensure a fresh state on tenant switch
106
110
  // The new current_tenant will be set by TenantProvider after redirect
107
- localStorage.removeItem('current_tenant');
108
- localStorage.removeItem('tenants');
111
+ removeAuthItem('current_tenant');
112
+ removeAuthItem('tenants');
109
113
  Object.entries(data).forEach(([key, value]) => {
110
- localStorage.setItem(key, value);
114
+ setAuthItem(key, value);
111
115
  });
112
116
  // Ensure tenant key is explicitly set from parsedData to prevent stale values
113
117
  if (data.tenant) {
114
- localStorage.setItem('tenant', data.tenant);
118
+ setAuthItem('tenant', data.tenant);
115
119
  }
116
120
  // Sync to cookies for cross-SPA synchronization
117
121
  // This sets the NEW auth data in cookies after we've cleared the stale ones
@@ -99502,18 +99506,47 @@ var llmTabLabels$3 = {
99502
99506
  title: "LLM Configuration",
99503
99507
  description: "Configure the language model settings for your agent."
99504
99508
  },
99509
+ infoBox: "This is the brain behind your agent — the AI model that powers every reply. Bigger models reason better on tricky questions; lighter ones are faster and cost less. Not sure? The default is a solid all-rounder.",
99505
99510
  search: {
99506
99511
  placeholder: "Search Providers"
99507
99512
  },
99513
+ providerLogoAlt: "{providerName} logo",
99508
99514
  toasts: {
99509
99515
  updateSuccess: "LLM updated successfully",
99510
99516
  updateError: "Failed to update LLM"
99511
99517
  },
99512
99518
  providerModal: {
99513
99519
  title: "LLM Selection",
99514
- description: "Select one of the models provided by this provider.",
99520
+ description: "Select one of the agents provided by {providerName}",
99515
99521
  searchPlaceholder: "Search",
99516
- helpText: "Choose your preferred LLM from the available provider to tailor your experience."
99522
+ helpText: "Choose your preferred LLM from the available provider to tailor your experience.",
99523
+ providerIconAlt: "{providerName} icon",
99524
+ tooLargeTitle: "This model may be too large for your system",
99525
+ tooLargeDescription: "{modelName} needs about {modelSize}. It may run very slowly or fail to load on this machine. Download anyway?",
99526
+ cancel: "Cancel",
99527
+ downloadAnyway: "Download anyway",
99528
+ alreadyDownloadingTitle: "A model is already downloading",
99529
+ unnamedModel: "A model",
99530
+ alreadyDownloadingDescription: "{modelName} is being downloaded. Only one on-device model downloads at a time — wait for it to finish, or click {modelName} in the list to cancel it.",
99531
+ gotIt: "Got it",
99532
+ announceDownloaded: "{modelName} downloaded",
99533
+ announceCancelled: "Download cancelled",
99534
+ announceFailed: "Download failed",
99535
+ announceStarted: "Started downloading {modelName}",
99536
+ localModel: {
99537
+ onDevice: "On-device",
99538
+ starting: "Starting…",
99539
+ cancel: "Cancel",
99540
+ inUse: "In use",
99541
+ downloadFailedRetry: "Download failed — retry",
99542
+ ariaDownload: "Download {modelName}, {modelSize}, on-device model",
99543
+ ariaStarting: "Starting download of {modelName}",
99544
+ ariaDownloading: "Cancel download of {modelName}, {percent} percent",
99545
+ ariaInstalled: "Use {modelName}, on-device model",
99546
+ ariaSelected: "Selected {modelName}, on-device model, in use",
99547
+ ariaError: "Retry download of {modelName}",
99548
+ ariaErrorWithReason: "Retry download of {modelName}, {reason}"
99549
+ }
99517
99550
  }
99518
99551
  };
99519
99552
  var llmsTabCreateLlmModal$3 = {
@@ -101834,9 +101867,6 @@ var tabsHumanSupportTab$3 = {
101834
101867
  ticketListLabel: "Support ticket list",
101835
101868
  ticketDetailLabel: "Support ticket details"
101836
101869
  };
101837
- var tabsLlmTab$3 = {
101838
- providerLogoAlt: "{name} logo"
101839
- };
101840
101870
  var tabsPoliciestab$3 = {
101841
101871
  searchPoliciesPlaceholder: "Search policies",
101842
101872
  searchPoliciesLabel: "Search policies",
@@ -103745,7 +103775,6 @@ var en = {
103745
103775
  tabsGroupstab: tabsGroupstab$3,
103746
103776
  tabsHistoryTab: tabsHistoryTab$3,
103747
103777
  tabsHumanSupportTab: tabsHumanSupportTab$3,
103748
- tabsLlmTab: tabsLlmTab$3,
103749
103778
  tabsPoliciestab: tabsPoliciestab$3,
103750
103779
  tabsPrivacyTab: tabsPrivacyTab$3,
103751
103780
  tabsPromptsTab: tabsPromptsTab$3,
@@ -106640,18 +106669,47 @@ var llmTabLabels$2 = {
106640
106669
  title: "Configuration LLM",
106641
106670
  description: "Configurez les paramètres du modèle de langage pour votre agent."
106642
106671
  },
106672
+ infoBox: "C'est le cerveau de votre agent — le modèle d'IA qui alimente chaque réponse. Les modèles plus grands raisonnent mieux sur les questions complexes ; les plus légers sont plus rapides et moins coûteux. Pas sûr ? Le modèle par défaut est un excellent choix polyvalent.",
106643
106673
  search: {
106644
106674
  placeholder: "Rechercher des fournisseurs"
106645
106675
  },
106676
+ providerLogoAlt: "Logo {providerName}",
106646
106677
  toasts: {
106647
106678
  updateSuccess: "LLM mis à jour avec succès",
106648
106679
  updateError: "Échec de la mise à jour du LLM"
106649
106680
  },
106650
106681
  providerModal: {
106651
106682
  title: "Sélection du LLM",
106652
- description: "Sélectionnez l'un des modèles fournis par ce fournisseur.",
106683
+ description: "Sélectionnez l'un des agents proposés par {providerName}",
106653
106684
  searchPlaceholder: "Rechercher",
106654
- helpText: "Choisissez votre LLM préféré parmi les fournisseurs disponibles pour personnaliser votre expérience."
106685
+ helpText: "Choisissez votre LLM préféré parmi les fournisseurs disponibles pour personnaliser votre expérience.",
106686
+ providerIconAlt: "icône {providerName}",
106687
+ tooLargeTitle: "Ce modèle est peut-être trop volumineux pour votre système",
106688
+ tooLargeDescription: "{modelName} nécessite environ {modelSize}. Il peut être très lent ou ne pas se charger sur cette machine. Télécharger quand même ?",
106689
+ cancel: "Annuler",
106690
+ downloadAnyway: "Télécharger quand même",
106691
+ alreadyDownloadingTitle: "Un modèle est déjà en cours de téléchargement",
106692
+ unnamedModel: "Un modèle",
106693
+ alreadyDownloadingDescription: "{modelName} est en cours de téléchargement. Un seul modèle sur l'appareil est téléchargé à la fois : attendez la fin ou cliquez sur {modelName} dans la liste pour l'annuler.",
106694
+ gotIt: "Compris",
106695
+ announceDownloaded: "{modelName} téléchargé",
106696
+ announceCancelled: "Téléchargement annulé",
106697
+ announceFailed: "Échec du téléchargement",
106698
+ announceStarted: "Téléchargement de {modelName} démarré",
106699
+ localModel: {
106700
+ onDevice: "Sur l'appareil",
106701
+ starting: "Démarrage…",
106702
+ cancel: "Annuler",
106703
+ inUse: "Utilisé",
106704
+ downloadFailedRetry: "Échec du téléchargement — réessayer",
106705
+ ariaDownload: "Télécharger {modelName}, {modelSize}, modèle sur l'appareil",
106706
+ ariaStarting: "Démarrage du téléchargement de {modelName}",
106707
+ ariaDownloading: "Annuler le téléchargement de {modelName}, {percent} pour cent",
106708
+ ariaInstalled: "Utiliser {modelName}, modèle sur l'appareil",
106709
+ ariaSelected: "{modelName} sélectionné, modèle sur l'appareil, utilisé",
106710
+ ariaError: "Réessayer le téléchargement de {modelName}",
106711
+ ariaErrorWithReason: "Réessayer le téléchargement de {modelName}, {reason}"
106712
+ }
106655
106713
  }
106656
106714
  };
106657
106715
  var llmsTabCreateLlmModal$2 = {
@@ -108972,9 +109030,6 @@ var tabsHumanSupportTab$2 = {
108972
109030
  ticketListLabel: "Liste des tickets de support",
108973
109031
  ticketDetailLabel: "Détails du ticket de support"
108974
109032
  };
108975
- var tabsLlmTab$2 = {
108976
- providerLogoAlt: "Logo {name}"
108977
- };
108978
109033
  var tabsPoliciestab$2 = {
108979
109034
  searchPoliciesPlaceholder: "Rechercher des politiques",
108980
109035
  searchPoliciesLabel: "Rechercher des politiques",
@@ -110718,7 +110773,6 @@ var fr = {
110718
110773
  tabsGroupstab: tabsGroupstab$2,
110719
110774
  tabsHistoryTab: tabsHistoryTab$2,
110720
110775
  tabsHumanSupportTab: tabsHumanSupportTab$2,
110721
- tabsLlmTab: tabsLlmTab$2,
110722
110776
  tabsPoliciestab: tabsPoliciestab$2,
110723
110777
  tabsPrivacyTab: tabsPrivacyTab$2,
110724
110778
  tabsPromptsTab: tabsPromptsTab$2,
@@ -113612,18 +113666,47 @@ var llmTabLabels$1 = {
113612
113666
  title: "Configuración de LLM",
113613
113667
  description: "Configure los ajustes del modelo de lenguaje para su agente."
113614
113668
  },
113669
+ infoBox: "Este es el cerebro de tu agente: el modelo de IA que impulsa cada respuesta. Los modelos más grandes razonan mejor las preguntas complejas; los más ligeros son más rápidos y cuestan menos. ¿No estás seguro? El predeterminado es una opción versátil y confiable.",
113615
113670
  search: {
113616
113671
  placeholder: "Buscar proveedores"
113617
113672
  },
113673
+ providerLogoAlt: "Logo de {providerName}",
113618
113674
  toasts: {
113619
113675
  updateSuccess: "LLM actualizado correctamente",
113620
113676
  updateError: "Error al actualizar el LLM"
113621
113677
  },
113622
113678
  providerModal: {
113623
113679
  title: "Selección de LLM",
113624
- description: "Seleccione uno de los modelos proporcionados por este proveedor.",
113680
+ description: "Seleccione uno de los agentes proporcionados por {providerName}",
113625
113681
  searchPlaceholder: "Buscar",
113626
- helpText: "Elija su LLM preferido entre los proveedores disponibles para personalizar su experiencia."
113682
+ helpText: "Elija su LLM preferido entre los proveedores disponibles para personalizar su experiencia.",
113683
+ providerIconAlt: "ícono de {providerName}",
113684
+ tooLargeTitle: "Este modelo puede ser demasiado grande para su sistema",
113685
+ tooLargeDescription: "{modelName} necesita aproximadamente {modelSize}. Puede funcionar muy lentamente o no cargarse en este equipo. ¿Descargar de todos modos?",
113686
+ cancel: "Cancelar",
113687
+ downloadAnyway: "Descargar de todos modos",
113688
+ alreadyDownloadingTitle: "Ya se está descargando un modelo",
113689
+ unnamedModel: "Un modelo",
113690
+ alreadyDownloadingDescription: "{modelName} se está descargando. Solo se descarga un modelo en el dispositivo a la vez: espere a que termine o haga clic en {modelName} en la lista para cancelarlo.",
113691
+ gotIt: "Entendido",
113692
+ announceDownloaded: "{modelName} descargado",
113693
+ announceCancelled: "Descarga cancelada",
113694
+ announceFailed: "Error en la descarga",
113695
+ announceStarted: "Descarga de {modelName} iniciada",
113696
+ localModel: {
113697
+ onDevice: "En el dispositivo",
113698
+ starting: "Iniciando…",
113699
+ cancel: "Cancelar",
113700
+ inUse: "En uso",
113701
+ downloadFailedRetry: "Error en la descarga: reintentar",
113702
+ ariaDownload: "Descargar {modelName}, {modelSize}, modelo en el dispositivo",
113703
+ ariaStarting: "Iniciando la descarga de {modelName}",
113704
+ ariaDownloading: "Cancelar la descarga de {modelName}, {percent} por ciento",
113705
+ ariaInstalled: "Usar {modelName}, modelo en el dispositivo",
113706
+ ariaSelected: "{modelName} seleccionado, modelo en el dispositivo, en uso",
113707
+ ariaError: "Reintentar la descarga de {modelName}",
113708
+ ariaErrorWithReason: "Reintentar la descarga de {modelName}, {reason}"
113709
+ }
113627
113710
  }
113628
113711
  };
113629
113712
  var llmsTabCreateLlmModal$1 = {
@@ -115944,9 +116027,6 @@ var tabsHumanSupportTab$1 = {
115944
116027
  ticketListLabel: "Lista de tickets de soporte",
115945
116028
  ticketDetailLabel: "Detalles del ticket de soporte"
115946
116029
  };
115947
- var tabsLlmTab$1 = {
115948
- providerLogoAlt: "Logo de {name}"
115949
- };
115950
116030
  var tabsPoliciestab$1 = {
115951
116031
  searchPoliciesPlaceholder: "Buscar políticas",
115952
116032
  searchPoliciesLabel: "Buscar políticas",
@@ -117690,7 +117770,6 @@ var es = {
117690
117770
  tabsGroupstab: tabsGroupstab$1,
117691
117771
  tabsHistoryTab: tabsHistoryTab$1,
117692
117772
  tabsHumanSupportTab: tabsHumanSupportTab$1,
117693
- tabsLlmTab: tabsLlmTab$1,
117694
117773
  tabsPoliciestab: tabsPoliciestab$1,
117695
117774
  tabsPrivacyTab: tabsPrivacyTab$1,
117696
117775
  tabsPromptsTab: tabsPromptsTab$1,
@@ -120584,18 +120663,47 @@ var llmTabLabels = {
120584
120663
  title: "LLM 配置",
120585
120664
  description: "为您的智能体配置语言模型设置。"
120586
120665
  },
120666
+ infoBox: "这是您智能体的大脑——为每次回复提供动力的 AI 模型。更大的模型在处理复杂问题时推理能力更强;更轻量的模型则更快、成本更低。不确定?默认模型是可靠的全能之选。",
120587
120667
  search: {
120588
120668
  placeholder: "搜索提供商"
120589
120669
  },
120670
+ providerLogoAlt: "{providerName} 徽标",
120590
120671
  toasts: {
120591
120672
  updateSuccess: "LLM 更新成功",
120592
120673
  updateError: "LLM 更新失败"
120593
120674
  },
120594
120675
  providerModal: {
120595
120676
  title: "LLM 选择",
120596
- description: "从此提供商中选择一个模型。",
120677
+ description: "从 {providerName} 提供的代理中选择一个",
120597
120678
  searchPlaceholder: "搜索",
120598
- helpText: "从可用提供商中选择您偏好的 LLM,以定制您的使用体验。"
120679
+ helpText: "从可用提供商中选择您偏好的 LLM,以定制您的使用体验。",
120680
+ providerIconAlt: "{providerName} 图标",
120681
+ tooLargeTitle: "该模型可能对您的系统而言过大",
120682
+ tooLargeDescription: "{modelName} 约需 {modelSize}。它在此设备上可能运行非常缓慢或无法加载。仍要下载吗?",
120683
+ cancel: "取消",
120684
+ downloadAnyway: "仍要下载",
120685
+ alreadyDownloadingTitle: "已有模型正在下载",
120686
+ unnamedModel: "某个模型",
120687
+ alreadyDownloadingDescription: "{modelName} 正在下载。一次只能下载一个设备端模型——请等待其完成,或点击列表中的 {modelName} 以取消下载。",
120688
+ gotIt: "知道了",
120689
+ announceDownloaded: "{modelName} 已下载",
120690
+ announceCancelled: "下载已取消",
120691
+ announceFailed: "下载失败",
120692
+ announceStarted: "已开始下载 {modelName}",
120693
+ localModel: {
120694
+ onDevice: "设备端",
120695
+ starting: "正在启动…",
120696
+ cancel: "取消",
120697
+ inUse: "使用中",
120698
+ downloadFailedRetry: "下载失败——重试",
120699
+ ariaDownload: "下载 {modelName},{modelSize},设备端模型",
120700
+ ariaStarting: "正在开始下载 {modelName}",
120701
+ ariaDownloading: "取消下载 {modelName},{percent}%",
120702
+ ariaInstalled: "使用 {modelName},设备端模型",
120703
+ ariaSelected: "已选择 {modelName},设备端模型,使用中",
120704
+ ariaError: "重试下载 {modelName}",
120705
+ ariaErrorWithReason: "重试下载 {modelName},{reason}"
120706
+ }
120599
120707
  }
120600
120708
  };
120601
120709
  var llmsTabCreateLlmModal = {
@@ -122916,9 +123024,6 @@ var tabsHumanSupportTab = {
122916
123024
  ticketListLabel: "支持工单列表",
122917
123025
  ticketDetailLabel: "支持工单详情"
122918
123026
  };
122919
- var tabsLlmTab = {
122920
- providerLogoAlt: "{name} 标志"
122921
- };
122922
123027
  var tabsPoliciestab = {
122923
123028
  searchPoliciesPlaceholder: "搜索策略",
122924
123029
  searchPoliciesLabel: "搜索策略",
@@ -124662,7 +124767,6 @@ var zh = {
124662
124767
  tabsGroupstab: tabsGroupstab,
124663
124768
  tabsHistoryTab: tabsHistoryTab,
124664
124769
  tabsHumanSupportTab: tabsHumanSupportTab,
124665
- tabsLlmTab: tabsLlmTab,
124666
124770
  tabsPoliciestab: tabsPoliciestab,
124667
124771
  tabsPrivacyTab: tabsPrivacyTab,
124668
124772
  tabsPromptsTab: tabsPromptsTab,
@@ -130883,6 +130987,7 @@ Progress.displayName = Root$9.displayName;
130883
130987
 
130884
130988
  const LOCAL_LLM_ENABLED_KEY = 'ibl_local_llm_enabled';
130885
130989
  const LOCAL_LLM_MODEL_KEY = 'ibl_local_llm_model';
130990
+ const LOCAL_LLM_TOOL_SUPPORT_KEY = 'ibl_local_llm_tool_support';
130886
130991
  /**
130887
130992
  * Curated catalog of small, on-device-friendly models from the Ollama library
130888
130993
  * (https://ollama.com/library). The model manager itself (Ollama) is not a
@@ -130940,6 +131045,12 @@ const LOCAL_MODELS = [
130940
131045
  tool_support: true,
130941
131046
  },
130942
131047
  ];
131048
+ /**
131049
+ * Fraction of the machine's memory above which a model is treated as "too large"
131050
+ * and a confirmation is required before downloading. TESTING VALUE — set low (1%)
131051
+ * so the warning is easy to trigger; raise toward real capacity (e.g. 0.8) later.
131052
+ */
131053
+ const MODEL_SIZE_WARN_FRACTION = 0.01;
130943
131054
  /** Multipliers for the size units used in the catalog (binary / 1024-based). */
130944
131055
  const SIZE_UNIT_BYTES = {
130945
131056
  B: 1,
@@ -130961,6 +131072,31 @@ function parseModelSizeBytes(size) {
130961
131072
  return null;
130962
131073
  return value * ((_a = SIZE_UNIT_BYTES[match[2].toUpperCase()]) !== null && _a !== void 0 ? _a : 1);
130963
131074
  }
131075
+ /**
131076
+ * The memory a model can use on this machine: the larger of system RAM and GPU
131077
+ * VRAM (a model runs in one or the other). Returns 0 when memory is unknown.
131078
+ */
131079
+ function usableMemoryBytes(systemMemory) {
131080
+ if (!systemMemory)
131081
+ return 0;
131082
+ return Math.max(systemMemory.ram_total, systemMemory.vram_total);
131083
+ }
131084
+ /**
131085
+ * Whether `model` is large enough relative to this machine's memory to warrant a
131086
+ * "might not run" confirmation. False when memory or the model size is unknown,
131087
+ * so the download simply proceeds rather than warning on incomplete data.
131088
+ */
131089
+ function modelExceedsCapacity(model, systemMemory) {
131090
+ const capacity = usableMemoryBytes(systemMemory);
131091
+ const modelBytes = parseModelSizeBytes(model.size);
131092
+ if (modelBytes == null)
131093
+ return false;
131094
+ // Unknown memory (capacity 0) → don't warn; the download simply proceeds
131095
+ // rather than blocking on incomplete data.
131096
+ if (capacity <= 0)
131097
+ return false;
131098
+ return modelBytes > capacity * MODEL_SIZE_WARN_FRACTION;
131099
+ }
130964
131100
  /**
130965
131101
  * Default minimum model size (GB) that can use the Cowork
130966
131102
  * (GhostOS MCP/tools). Smaller models don't support tools/MCP well, so the
@@ -131008,6 +131144,33 @@ function getLocalLLMModel() {
131008
131144
  return null;
131009
131145
  return localStorage.getItem(LOCAL_LLM_MODEL_KEY);
131010
131146
  }
131147
+ /**
131148
+ * Set the model the user has chosen to chat with (Ollama pull id).
131149
+ */
131150
+ function setLocalLLMModel(modelId) {
131151
+ if (typeof window === 'undefined')
131152
+ return;
131153
+ localStorage.setItem(LOCAL_LLM_MODEL_KEY, modelId);
131154
+ }
131155
+ /**
131156
+ * Whether the model the user selected supports tools / function calling
131157
+ * (persisted alongside the model id). The host (e.g. ghost-os) reads this to
131158
+ * route chat to the MCP/tool proxy (:8000) vs plain Ollama (:11434). Defaults to
131159
+ * false when unknown. See {@link LocalModel.tool_support}.
131160
+ */
131161
+ function getLocalLLMToolSupport() {
131162
+ if (typeof window === 'undefined')
131163
+ return false;
131164
+ return localStorage.getItem(LOCAL_LLM_TOOL_SUPPORT_KEY) === 'true';
131165
+ }
131166
+ /**
131167
+ * Persist whether the selected model supports tools (set when a model is chosen).
131168
+ */
131169
+ function setLocalLLMToolSupport(toolSupport) {
131170
+ if (typeof window === 'undefined')
131171
+ return;
131172
+ localStorage.setItem(LOCAL_LLM_TOOL_SUPPORT_KEY, String(toolSupport));
131173
+ }
131011
131174
  function LocalLLMTab({ isAvailable, state, ollamaStatus, isUsingFoundry = false, foundryModels = [], selectedFoundryModel = null, foundryStatus, onInstallOllama, onStopManager, onInstallFoundry, onCheckStatus, onSelectFoundryModel, }) {
131012
131175
  var _a, _b, _c, _d, _e;
131013
131176
  const t = useT();
@@ -225279,62 +225442,215 @@ function CreateApiModal$1({ isOpen, onClose, tenantKey, username }) {
225279
225442
  : t('apiTabLabels.createModal.submit') })) }) })] })] }) }), apiKey && jsx(ApiKeyModal$1, { isOpen: true, onClose: () => setApiKey(null), apiKey: apiKey })] }));
225280
225443
  }
225281
225444
 
225445
+ /**
225446
+ * Small pure utilities colocated with the LLM tab.
225447
+ * Ported from standalone's `@/lib/utils` and `@/lib/error`.
225448
+ */
225449
+ // Canonical provider name for any label — a backend key ("azure_openai"), a
225450
+ // human label ("Microsoft"), or a local model's provider ("Meta"). Folds every
225451
+ // alias onto one identity so the same provider from different sources compares
225452
+ // equal. See getProviderName.
225453
+ const PROVIDER_NAME_BY_ALIAS = {
225454
+ microsoft: 'azure_openai',
225455
+ azureopenai: 'azure_openai',
225456
+ openai: 'openai',
225457
+ google: 'google',
225458
+ gemini: 'google',
225459
+ meta: 'llama',
225460
+ metallama: 'llama',
225461
+ llama: 'llama',
225462
+ mistral: 'mistral',
225463
+ mistralai: 'mistral',
225464
+ nvidia: 'nvidia',
225465
+ iblchatnvidia: 'nvidia',
225466
+ deepseek: 'deepseek',
225467
+ anthropic: 'anthropic',
225468
+ iblchatanthropic: 'anthropic',
225469
+ claude: 'anthropic',
225470
+ groq: 'groq',
225471
+ perplexity: 'perplexity',
225472
+ xai: 'xai',
225473
+ bedrock: 'bedrock',
225474
+ amazonbedrock: 'bedrock',
225475
+ amazon: 'bedrock',
225476
+ iblchatbedrock: 'bedrock',
225477
+ alibaba: 'alibaba',
225478
+ qwen: 'alibaba',
225479
+ ibm: 'ibm',
225480
+ granite: 'ibm',
225481
+ // ibl.ai's own hosted provider. The backend key is `iblai` (NameEnum.IBLAI);
225482
+ // the extra aliases cover the `IBLChat<Vendor>` house spelling and the
225483
+ // dotted/spaced brand forms, which all normalize to one of these.
225484
+ iblai: 'iblai',
225485
+ ibl: 'iblai',
225486
+ iblchatibl: 'iblai',
225487
+ iblchatiblai: 'iblai',
225488
+ };
225489
+ /**
225490
+ * Canonical provider name for any label, case- and punctuation-insensitive.
225491
+ * Folds backend keys and human/display labels onto one identity — e.g.
225492
+ * `getProviderName('Microsoft') === 'azure_openai'`,
225493
+ * `getProviderName('Meta') === 'llama'`, `getProviderName('Google') === 'google'`.
225494
+ * Unknown providers return their normalized (lowercased, alphanumeric-only) form.
225495
+ *
225496
+ * This is the single source of truth for provider identity, which matters as
225497
+ * soon as on-device models enter the picture: the local catalog labels its
225498
+ * providers the human way ("Meta", "Microsoft", "Alibaba") while the backend LLM
225499
+ * list uses wire keys ("llama", "azure_openai"). Comparing the two raw would
225500
+ * render a duplicate card for the same vendor.
225501
+ */
225502
+ function getProviderName(llmProvider) {
225503
+ var _a;
225504
+ const normalized = (llmProvider !== null && llmProvider !== void 0 ? llmProvider : '').toLowerCase().replace(/[^a-z0-9]/g, '');
225505
+ return (_a = PROVIDER_NAME_BY_ALIAS[normalized]) !== null && _a !== void 0 ? _a : normalized;
225506
+ }
225507
+ /**
225508
+ * Whether two Ollama model refs name the same model, comparing by base name and
225509
+ * ignoring a trailing `:tag` (so "llama3.2" ≡ "llama3.2:latest", and
225510
+ * "granite4.1:8b" ≡ "granite4.1"). The download state and Ollama's /api/tags may
225511
+ * carry a tag suffix the catalog id doesn't (or vice-versa); an exact `===` then
225512
+ * fails to recognize a row's own in-flight download — the row shows "Download"
225513
+ * with no progress while the pull runs. Base-name matching is safe here because
225514
+ * every catalog model has a unique base (llama3.2, qwen3, mistral, granite4.1,
225515
+ * gpt-oss, gemma4), so it can never light up the wrong row.
225516
+ */
225517
+ function sameModel(a, b) {
225518
+ const base = (ref) => (ref !== null && ref !== void 0 ? ref : '').toLowerCase().split(':')[0];
225519
+ const ba = base(a);
225520
+ return ba.length > 0 && ba === base(b);
225521
+ }
225522
+ /**
225523
+ * Whether a catalog model id (e.g. "granite4.1:8b") is among the installed
225524
+ * Ollama tags (e.g. "granite4.1:latest").
225525
+ *
225526
+ * Deliberately NOT the profile tab's `isModelInstalled`, which matches an exact
225527
+ * tag or the `<id>:` prefix: that is one-directional and misses a catalog id
225528
+ * that itself carries a tag ("granite4.1:8b" against an installed
225529
+ * "granite4.1:latest"). The picker needs the symmetric base-name comparison of
225530
+ * {@link sameModel} so a pulled model is recognized whichever side wears the tag.
225531
+ */
225532
+ function isLocalModelInstalled(modelId, installedTags) {
225533
+ return !!(installedTags === null || installedTags === void 0 ? void 0 : installedTags.some((tag) => sameModel(tag, modelId)));
225534
+ }
225535
+ function canSwitchProvider(providers, name) {
225536
+ const provider = providers.find((p) => p.name === name);
225537
+ return !!(provider === null || provider === void 0 ? void 0 : provider.chat_models) && provider.chat_models.length > 0;
225538
+ }
225539
+ /**
225540
+ * Whether we hold a usable LLM key for this provider — its own credentials, or
225541
+ * the tenant's main keys when it is allowed to borrow them. An *absent*
225542
+ * `main_has_credentials` means the backend did not report on the main keys and
225543
+ * is treated as available (matching standalone's `@/lib/utils` and
225544
+ * `claw-sandbox`'s copy); only an explicit `false` rules the main keys out.
225545
+ */
225546
+ function canSwitchLLm(llm) {
225547
+ if (llm === null || llm === void 0 ? void 0 : llm.has_credentials) {
225548
+ return true;
225549
+ }
225550
+ return ((llm === null || llm === void 0 ? void 0 : llm.can_use_main_keys) &&
225551
+ ((llm === null || llm === void 0 ? void 0 : llm.main_has_credentials) === true || (llm === null || llm === void 0 ? void 0 : llm.main_has_credentials) === undefined));
225552
+ }
225553
+ /**
225554
+ * Compares two raw provider names by the label the user actually sees on the
225555
+ * card, not by the backend key — the two diverge often enough to matter
225556
+ * (`bedrock` renders as "Amazon", `azure_openai` as "Microsoft", `iblai` as
225557
+ * "ibl.ai"). Comparison is case-insensitive (`sensitivity: 'base'`) so casing
225558
+ * never produces a surprising order. `getDetails` is the host-provided resolver
225559
+ * the grid renders with, so the sort always matches what is on screen; unknown
225560
+ * providers fall back to their raw name as the display name.
225561
+ */
225562
+ function compareLLMProvidersByDisplayName(a, b, getDetails) {
225563
+ return getDetails(a).name.localeCompare(getDetails(b).name, undefined, {
225564
+ sensitivity: 'base',
225565
+ });
225566
+ }
225567
+ /**
225568
+ * Whether the user can reach *any* model of this provider.
225569
+ *
225570
+ * This is exactly the provider-level half of the per-model `isDisabled` rule in
225571
+ * `llm-tab/llm-provider-modal.tsx`: a model row there is disabled when
225572
+ * `!canSwitchLLm(provider) || !canSwitchProvider(llms, provider.name)`, and both
225573
+ * of those depend only on the provider — so when either fails, *every* model row
225574
+ * of that provider is disabled. `canSwitchProvider` is just "has at least one
225575
+ * `chat_models` entry", which is checked inline here to keep the helper pure and
225576
+ * free of the surrounding provider list.
225577
+ *
225578
+ * `canSwitchLLm` can return `undefined` (its `can_use_main_keys` branch), hence
225579
+ * the explicit `Boolean(...)`.
225580
+ */
225581
+ function canAccessProvider(provider) {
225582
+ var _a, _b;
225583
+ return Boolean(canSwitchLLm(provider !== null && provider !== void 0 ? provider : {})) && ((_b = (_a = provider === null || provider === void 0 ? void 0 : provider.chat_models) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0;
225584
+ }
225585
+ /**
225586
+ * Orders LLM provider cards as two alphabetical groups: providers the user can
225587
+ * actually use (see {@link canAccessProvider}) first, then the ones they can't —
225588
+ * each group sorted alphabetically by display name.
225589
+ *
225590
+ * The grouping predicate is deliberately the same one the grid grays cards with,
225591
+ * so the two always line up: group 1 renders normally, group 2 renders grayed.
225592
+ * Grouping on credentials alone would let a provider that has a key but ships no
225593
+ * chat models sort into the "usable" group while rendering grayed.
225594
+ *
225595
+ * Pure and non-mutating: RTK Query results are frozen, so the input is copied
225596
+ * before sorting. `Array.prototype.sort` is stable, so providers that tie on
225597
+ * both group and display name keep their original relative order.
225598
+ */
225599
+ function sortLLMProvidersByCredentials(providers, getDetails) {
225600
+ return [...providers].sort((a, b) => {
225601
+ const aUsable = canAccessProvider(a);
225602
+ const bUsable = canAccessProvider(b);
225603
+ if (aUsable !== bUsable)
225604
+ return aUsable ? -1 : 1;
225605
+ return compareLLMProvidersByDisplayName(a.name, b.name, getDetails);
225606
+ });
225607
+ }
225608
+ function extractErrorMessage$1(error, fallback) {
225609
+ var _a, _b;
225610
+ const apiError = error;
225611
+ return ((_a = apiError === null || apiError === void 0 ? void 0 : apiError.data) === null || _a === void 0 ? void 0 : _a.error) || ((_b = apiError === null || apiError === void 0 ? void 0 : apiError.error) === null || _b === void 0 ? void 0 : _b.error) || fallback;
225612
+ }
225613
+
225614
+ // Logo + display name keyed by canonical provider name (see getProviderName).
225615
+ const PROVIDER_DETAILS_BY_NAME = {
225616
+ groq: { logo: '/llm-groq-provider.png', name: 'Groq' },
225617
+ nvidia: { logo: '/llm-nvidia-provider.webp', name: 'NVIDIA' },
225618
+ azure_openai: { logo: '/llm-microsoft-provider.png', name: 'Microsoft' },
225619
+ openai: { logo: '/llm-openai-provider-2.svg', name: 'OpenAI' },
225620
+ mistral: { logo: '/llm-mistral-provider.jpeg', name: 'Mistral' },
225621
+ google: { logo: '/llm-google-provider.svg', name: 'Google' },
225622
+ llama: { logo: '/llm-llama-provider.jpeg', name: 'Meta' },
225623
+ anthropic: { logo: '/llm-claude-provider.png', name: 'Anthropic' },
225624
+ perplexity: { logo: '/llm-perplexity-provider.webp', name: 'Perplexity' },
225625
+ deepseek: { logo: '/llm-deepseek-provider.png', name: 'DeepSeek' },
225626
+ xai: { logo: '/llm-xai-provider.jpg', name: 'xAI' },
225627
+ bedrock: { logo: '/llm-amazon-provider.png', name: 'Amazon' },
225628
+ alibaba: { logo: '/llm-alibaba-provider.png', name: 'Alibaba' },
225629
+ ibm: { logo: '/llm-ibm-provider.png', name: 'IBM' },
225630
+ iblai: { logo: '/llm-iblai-provider.png', name: 'ibl.ai' },
225631
+ };
225282
225632
  /**
225283
225633
  * Returns the canonical logo path and display name for an LLM provider.
225634
+ *
225635
+ * Resolution goes through {@link getProviderName}, so a backend wire key
225636
+ * (`azure_openai`), a human label (`Microsoft`) and an on-device catalog's
225637
+ * provider string (`Meta`, `Alibaba`) all land on the same entry — the provider
225638
+ * grid merges cloud and on-device providers into one list, so the two spellings
225639
+ * have to agree.
225640
+ *
225284
225641
  * `logo` is a static-root path (e.g. `/llm-amazon-provider.png`); the package
225285
225642
  * ships the canonical assets under `@iblai/web-containers/public/` — copy them
225286
225643
  * into your app's static-root (`public/` for Next.js) so the paths resolve.
225287
225644
  */
225288
225645
  function getLLMProviderDetails(llmProvider, llmName) {
225289
- switch (llmProvider) {
225290
- case 'groq':
225291
- return { logo: '/llm-groq-provider.png', name: 'Groq' };
225292
- case 'azure_openai':
225293
- return { logo: '/llm-microsoft-provider.png', name: 'Microsoft' };
225294
- case 'openai':
225295
- if (llmName)
225296
- return { logo: '/llm-openai-provider.jpg', name: 'OpenAI' };
225297
- return { logo: '/llm-openai-provider-2.svg', name: 'OpenAI' };
225298
- case 'mistral':
225299
- return { logo: '/llm-mistral-provider.jpeg', name: 'Mistral' };
225300
- case 'google':
225301
- if (llmName)
225302
- return { logo: '/llm-gemini-provider.png', name: 'Google' };
225303
- return { logo: '/llm-google-provider.svg', name: 'Google' };
225304
- case 'llama':
225305
- return { logo: '/llm-llama-provider.jpeg', name: 'Meta' };
225306
- case 'perplexity':
225307
- return { logo: '/llm-perplexity-provider.webp', name: 'Perplexity' };
225308
- case 'deepseek':
225309
- return { logo: '/llm-deepseek-provider.png', name: 'DeepSeek' };
225310
- case 'xai':
225311
- return { logo: '/llm-xai-provider.jpg', name: 'xAI' };
225312
- case 'IBLChatAnthropic':
225313
- case 'anthropic':
225314
- return { logo: '/llm-claude-provider.png', name: 'Anthropic' };
225315
- case 'IBLChatNvidia':
225316
- case 'nvidia':
225317
- return { logo: '/llm-nvidia-provider.webp', name: 'NVIDIA' };
225318
- case 'bedrock':
225319
- case 'amazon-bedrock':
225320
- case 'amazon_bedrock':
225321
- case 'IBLChatBedrock':
225322
- return { logo: '/llm-amazon-provider.png', name: 'Amazon' };
225323
- // ibl.ai's own hosted provider. The backend key is `iblai` (NameEnum.IBLAI);
225324
- // the other cases cover the `IBLChat<Vendor>` house spelling and the
225325
- // dotted/dashed/underscored brand forms seen in labels and older payloads.
225326
- case 'iblai':
225327
- case 'IBLAI':
225328
- case 'ibl.ai':
225329
- case 'ibl_ai':
225330
- case 'ibl-ai':
225331
- case 'ibl':
225332
- case 'IBLChatIBL':
225333
- case 'IBLChatIBLAI':
225334
- return { logo: '/llm-iblai-provider.png', name: 'ibl.ai' };
225335
- default:
225336
- return { logo: '/llm-generic-provider.png', name: llmProvider };
225337
- }
225646
+ var _a;
225647
+ const name = getProviderName(llmProvider);
225648
+ // OpenAI and Google use a model-specific logo when a concrete model is named.
225649
+ if (name === 'openai' && llmName)
225650
+ return { logo: '/llm-openai-provider.jpg', name: 'OpenAI' };
225651
+ if (name === 'google' && llmName)
225652
+ return { logo: '/llm-gemini-provider.png', name: 'Google' };
225653
+ return (_a = PROVIDER_DETAILS_BY_NAME[name]) !== null && _a !== void 0 ? _a : { logo: '/llm-generic-provider.png', name: llmProvider };
225338
225654
  }
225339
225655
 
225340
225656
  // Dynamic schema based on selected provider
@@ -262271,7 +262587,7 @@ const resourceTypes = [
262271
262587
  * rate limit.
262272
262588
  */
262273
262589
  const SPEND_CAP_EXCEEDED_ERROR_CODE = 'spend_cap_exceeded';
262274
- const extractErrorMessage$1 = (error, fallback) => {
262590
+ const extractErrorMessage = (error, fallback) => {
262275
262591
  var _a, _b, _c;
262276
262592
  const apiError = error;
262277
262593
  if (((_a = apiError === null || apiError === void 0 ? void 0 : apiError.data) === null || _a === void 0 ? void 0 : _a.error_code) === SPEND_CAP_EXCEEDED_ERROR_CODE) {
@@ -262350,7 +262666,7 @@ function GithubFileUploadModal() {
262350
262666
  }
262351
262667
  catch (error) {
262352
262668
  console.error(JSON.stringify(error));
262353
- const errorMessage = extractErrorMessage$1(error, t('resourceModalGithubFileUploadModal.errorAddingDocument'));
262669
+ const errorMessage = extractErrorMessage(error, t('resourceModalGithubFileUploadModal.errorAddingDocument'));
262354
262670
  toast.error(errorMessage);
262355
262671
  console.error(JSON.stringify({ tenant: tenantKey, error }));
262356
262672
  }
@@ -262417,7 +262733,7 @@ function LocalFileUploadModal({ resource, maxFileSizeMB = DEFAULT_MAX_FILE_SIZE_
262417
262733
  }
262418
262734
  catch (error) {
262419
262735
  console.error(JSON.stringify(error));
262420
- const errorMessage = extractErrorMessage$1(error, t('resourceModalLocalFileUploadModal.errorAddingDocument'));
262736
+ const errorMessage = extractErrorMessage(error, t('resourceModalLocalFileUploadModal.errorAddingDocument'));
262421
262737
  toast.error(errorMessage);
262422
262738
  console.error(JSON.stringify({ tenant: tenantKey, error }));
262423
262739
  }
@@ -262520,7 +262836,7 @@ function UrlUploadModal({ resource }) {
262520
262836
  }
262521
262837
  catch (error) {
262522
262838
  console.error(JSON.stringify(error));
262523
- const errorMessage = extractErrorMessage$1(error, t('resourceModalUrlUploadModal.errorAddingTrainingDocument'));
262839
+ const errorMessage = extractErrorMessage(error, t('resourceModalUrlUploadModal.errorAddingTrainingDocument'));
262524
262840
  toast.error(errorMessage);
262525
262841
  console.error(JSON.stringify({ tenant: tenantKey, error }));
262526
262842
  }
@@ -262608,7 +262924,7 @@ function useWebsiteCrawlerResource(resource) {
262608
262924
  }
262609
262925
  catch (error) {
262610
262926
  console.error(JSON.stringify(error));
262611
- const errorMessage = extractErrorMessage$1(error, 'Error submitting web crawl data');
262927
+ const errorMessage = extractErrorMessage(error, 'Error submitting web crawl data');
262612
262928
  toast.error(errorMessage);
262613
262929
  console.error(JSON.stringify({ tenant: tenantKey, error }));
262614
262930
  }
@@ -262798,7 +263114,7 @@ const useGoogleDrivePicker = ({ tenantKey, mentorId }) => {
262798
263114
  }
262799
263115
  catch (error) {
262800
263116
  console.error(JSON.stringify(error));
262801
- const errorMessage = extractErrorMessage$1(error, 'Error adding training document');
263117
+ const errorMessage = extractErrorMessage(error, 'Error adding training document');
262802
263118
  toast.error(errorMessage);
262803
263119
  console.error(JSON.stringify({ tenant: tenantKey, error }));
262804
263120
  }
@@ -263103,7 +263419,7 @@ const useDropboxPicker = ({ tenantKey, mentorId, cancel = () => { }, linkType =
263103
263419
  }
263104
263420
  catch (error) {
263105
263421
  console.error(JSON.stringify(error));
263106
- const errorMessage = extractErrorMessage$1(error, 'Error adding training document');
263422
+ const errorMessage = extractErrorMessage(error, 'Error adding training document');
263107
263423
  toast.error(errorMessage);
263108
263424
  console.error(JSON.stringify({ tenant: tenantKey, error }));
263109
263425
  }
@@ -263328,7 +263644,7 @@ const useOneDrivePicker = ({ tenantKey, mentorId }) => {
263328
263644
  }
263329
263645
  catch (error) {
263330
263646
  console.error(JSON.stringify(error));
263331
- const errorMessage = extractErrorMessage$1(error, 'Error adding training document');
263647
+ const errorMessage = extractErrorMessage(error, 'Error adding training document');
263332
263648
  toast.error(errorMessage);
263333
263649
  console.error(JSON.stringify({ tenant: tenantKey, error }));
263334
263650
  }
@@ -270320,86 +270636,51 @@ var copyMentorModal = /*#__PURE__*/Object.freeze({
270320
270636
  });
270321
270637
 
270322
270638
  /**
270323
- * Small pure utilities colocated with the LLM tab.
270324
- * Ported from standalone's `@/lib/utils` and `@/lib/error`.
270325
- */
270326
- function canSwitchProvider(providers, name) {
270327
- const provider = providers.find((p) => p.name === name);
270328
- return !!(provider === null || provider === void 0 ? void 0 : provider.chat_models) && provider.chat_models.length > 0;
270329
- }
270330
- /**
270331
- * Whether we hold a usable LLM key for this provider — its own credentials, or
270332
- * the tenant's main keys when it is allowed to borrow them. An *absent*
270333
- * `main_has_credentials` means the backend did not report on the main keys and
270334
- * is treated as available (matching standalone's `@/lib/utils` and
270335
- * `claw-sandbox`'s copy); only an explicit `false` rules the main keys out.
270336
- */
270337
- function canSwitchLLm(llm) {
270338
- if (llm === null || llm === void 0 ? void 0 : llm.has_credentials) {
270339
- return true;
270340
- }
270341
- return ((llm === null || llm === void 0 ? void 0 : llm.can_use_main_keys) &&
270342
- ((llm === null || llm === void 0 ? void 0 : llm.main_has_credentials) === true || (llm === null || llm === void 0 ? void 0 : llm.main_has_credentials) === undefined));
270343
- }
270344
- /**
270345
- * Compares two raw provider names by the label the user actually sees on the
270346
- * card, not by the backend key — the two diverge often enough to matter
270347
- * (`bedrock` renders as "Amazon", `azure_openai` as "Microsoft", `iblai` as
270348
- * "ibl.ai"). Comparison is case-insensitive (`sensitivity: 'base'`) so casing
270349
- * never produces a surprising order. `getDetails` is the host-provided resolver
270350
- * the grid renders with, so the sort always matches what is on screen; unknown
270351
- * providers fall back to their raw name as the display name.
270352
- */
270353
- function compareLLMProvidersByDisplayName(a, b, getDetails) {
270354
- return getDetails(a).name.localeCompare(getDetails(b).name, undefined, {
270355
- sensitivity: 'base',
270356
- });
270357
- }
270358
- /**
270359
- * Whether the user can reach *any* model of this provider.
270360
- *
270361
- * This is exactly the provider-level half of the per-model `isDisabled` rule in
270362
- * `llm-tab/llm-provider-modal.tsx`: a model row there is disabled when
270363
- * `!canSwitchLLm(provider) || !canSwitchProvider(llms, provider.name)`, and both
270364
- * of those depend only on the provider — so when either fails, *every* model row
270365
- * of that provider is disabled. `canSwitchProvider` is just "has at least one
270366
- * `chat_models` entry", which is checked inline here to keep the helper pure and
270367
- * free of the surrounding provider list.
270368
- *
270369
- * `canSwitchLLm` can return `undefined` (its `can_use_main_keys` branch), hence
270370
- * the explicit `Boolean(...)`.
270639
+ * Window event any surface dispatches after changing the device-global
270640
+ * on-device selection — a model pick in the LLM tab's provider modal, or the
270641
+ * Local Models master toggle in Profile → Advanced. `storage` events only fire
270642
+ * cross-tab, so this covers the same-tab case (e.g. a nav-bar badge that has to
270643
+ * update the instant a model is picked in a modal above it).
270371
270644
  */
270372
- function canAccessProvider(provider) {
270373
- var _a, _b;
270374
- return Boolean(canSwitchLLm(provider !== null && provider !== void 0 ? provider : {})) && ((_b = (_a = provider === null || provider === void 0 ? void 0 : provider.chat_models) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0;
270375
- }
270645
+ const LOCAL_LLM_CHANGED_EVENT = 'ibl:local-llm-changed';
270376
270646
  /**
270377
- * Orders LLM provider cards as two alphabetical groups: providers the user can
270378
- * actually use (see {@link canAccessProvider}) first, then the ones they can't
270379
- * each group sorted alphabetically by display name.
270380
- *
270381
- * The grouping predicate is deliberately the same one the grid grays cards with,
270382
- * so the two always line up: group 1 renders normally, group 2 renders grayed.
270383
- * Grouping on credentials alone would let a provider that has a key but ships no
270384
- * chat models sort into the "usable" group while rendering grayed.
270385
- *
270386
- * Pure and non-mutating: RTK Query results are frozen, so the input is copied
270387
- * before sorting. `Array.prototype.sort` is stable, so providers that tie on
270388
- * both group and display name keep their original relative order.
270647
+ * Reactive view of the on-device model selection (`ibl_local_llm_enabled` +
270648
+ * `ibl_local_llm_model` in localStorage). Reads on mount so it is
270649
+ * SSR/hydration-safe (nothing resolves until the client mounts) — and re-reads
270650
+ * on cross-tab `storage` events and same-tab {@link LOCAL_LLM_CHANGED_EVENT}
270651
+ * events.
270389
270652
  */
270390
- function sortLLMProvidersByCredentials(providers, getDetails) {
270391
- return [...providers].sort((a, b) => {
270392
- const aUsable = canAccessProvider(a);
270393
- const bUsable = canAccessProvider(b);
270394
- if (aUsable !== bUsable)
270395
- return aUsable ? -1 : 1;
270396
- return compareLLMProvidersByDisplayName(a.name, b.name, getDetails);
270653
+ function useSelectedLocalModel() {
270654
+ const [sel, setSel] = useState({
270655
+ isLocal: false,
270656
+ model: null,
270657
+ modelId: null,
270397
270658
  });
270398
- }
270399
- function extractErrorMessage(error, fallback) {
270400
- var _a, _b;
270401
- const apiError = error;
270402
- return ((_a = apiError === null || apiError === void 0 ? void 0 : apiError.data) === null || _a === void 0 ? void 0 : _a.error) || ((_b = apiError === null || apiError === void 0 ? void 0 : apiError.error) === null || _b === void 0 ? void 0 : _b.error) || fallback;
270659
+ useEffect(() => {
270660
+ const read = () => {
270661
+ var _a;
270662
+ const enabled = isLocalLLMEnabled();
270663
+ const id = getLocalLLMModel();
270664
+ const model = id ? ((_a = LOCAL_MODELS.find((m) => m.id === id)) !== null && _a !== void 0 ? _a : null) : null;
270665
+ // Self-heal the derived tool-support cache. Local chat routes on
270666
+ // `ibl_local_llm_tool_support`, but an older selection may have left it
270667
+ // stale/false — which makes streaming reject a genuinely tool-capable
270668
+ // model. The catalog is the source of truth; reconcile here. Write only on
270669
+ // a real mismatch so it can't loop.
270670
+ if (enabled && model && getLocalLLMToolSupport() !== model.tool_support) {
270671
+ setLocalLLMToolSupport(model.tool_support);
270672
+ }
270673
+ setSel({ isLocal: enabled && !!id, model, modelId: id });
270674
+ };
270675
+ read();
270676
+ window.addEventListener('storage', read);
270677
+ window.addEventListener(LOCAL_LLM_CHANGED_EVENT, read);
270678
+ return () => {
270679
+ window.removeEventListener('storage', read);
270680
+ window.removeEventListener(LOCAL_LLM_CHANGED_EVENT, read);
270681
+ };
270682
+ }, []);
270683
+ return sel;
270403
270684
  }
270404
270685
 
270405
270686
  const AGENT_LLM_TAB_LABELS = {
@@ -270407,18 +270688,47 @@ const AGENT_LLM_TAB_LABELS = {
270407
270688
  title: 'LLM Configuration',
270408
270689
  description: 'Configure the language model settings for your agent.',
270409
270690
  },
270691
+ infoBox: 'This is the brain behind your agent — the AI model that powers every reply. Bigger models reason better on tricky questions; lighter ones are faster and cost less. Not sure? The default is a solid all-rounder.',
270410
270692
  search: {
270411
270693
  placeholder: 'Search Providers',
270412
270694
  },
270695
+ providerLogoAlt: (providerName) => `${providerName} logo`,
270413
270696
  toasts: {
270414
270697
  updateSuccess: 'LLM updated successfully',
270415
270698
  updateError: 'Failed to update LLM',
270416
270699
  },
270417
270700
  providerModal: {
270418
270701
  title: 'LLM Selection',
270419
- description: 'Select one of the models provided by this provider.',
270702
+ description: (providerName) => `Select one of the agents provided by ${providerName}`,
270420
270703
  searchPlaceholder: 'Search',
270421
270704
  helpText: 'Choose your preferred LLM from the available provider to tailor your experience.',
270705
+ providerIconAlt: (providerName) => `${providerName} icon`,
270706
+ tooLargeTitle: 'This model may be too large for your system',
270707
+ tooLargeDescription: (modelName, modelSize) => `${modelName} needs about ${modelSize}. It may run very slowly or fail to load on this machine. Download anyway?`,
270708
+ cancel: 'Cancel',
270709
+ downloadAnyway: 'Download anyway',
270710
+ alreadyDownloadingTitle: 'A model is already downloading',
270711
+ unnamedModel: 'A model',
270712
+ alreadyDownloadingDescription: (modelName) => `${modelName} is being downloaded. Only one on-device model downloads at a time — wait for it to finish, or click ${modelName} in the list to cancel it.`,
270713
+ gotIt: 'Got it',
270714
+ announceDownloaded: (modelName) => `${modelName} downloaded`,
270715
+ announceCancelled: 'Download cancelled',
270716
+ announceFailed: 'Download failed',
270717
+ announceStarted: (modelName) => `Started downloading ${modelName}`,
270718
+ localModel: {
270719
+ onDevice: 'On-device',
270720
+ starting: 'Starting…',
270721
+ cancel: 'Cancel',
270722
+ inUse: 'In use',
270723
+ downloadFailedRetry: 'Download failed — retry',
270724
+ ariaDownload: (modelName, modelSize) => `Download ${modelName}, ${modelSize}, on-device model`,
270725
+ ariaStarting: (modelName) => `Starting download of ${modelName}`,
270726
+ ariaDownloading: (modelName, percent) => `Cancel download of ${modelName}, ${percent} percent`,
270727
+ ariaInstalled: (modelName) => `Use ${modelName}, on-device model`,
270728
+ ariaSelected: (modelName) => `Selected ${modelName}, on-device model, in use`,
270729
+ ariaError: (modelName) => `Retry download of ${modelName}`,
270730
+ ariaErrorWithReason: (modelName, reason) => `Retry download of ${modelName}, ${reason}`,
270731
+ },
270422
270732
  },
270423
270733
  };
270424
270734
  function buildLLMTabLabels(t) {
@@ -270427,18 +270737,47 @@ function buildLLMTabLabels(t) {
270427
270737
  title: t('llmTabLabels.header.title'),
270428
270738
  description: t('llmTabLabels.header.description'),
270429
270739
  },
270740
+ infoBox: t('llmTabLabels.infoBox'),
270430
270741
  search: {
270431
270742
  placeholder: t('llmTabLabels.search.placeholder'),
270432
270743
  },
270744
+ providerLogoAlt: (providerName) => t('llmTabLabels.providerLogoAlt', { providerName }),
270433
270745
  toasts: {
270434
270746
  updateSuccess: t('llmTabLabels.toasts.updateSuccess'),
270435
270747
  updateError: t('llmTabLabels.toasts.updateError'),
270436
270748
  },
270437
270749
  providerModal: {
270438
270750
  title: t('llmTabLabels.providerModal.title'),
270439
- description: t('llmTabLabels.providerModal.description'),
270751
+ description: (providerName) => t('llmTabLabels.providerModal.description', { providerName }),
270440
270752
  searchPlaceholder: t('llmTabLabels.providerModal.searchPlaceholder'),
270441
270753
  helpText: t('llmTabLabels.providerModal.helpText'),
270754
+ providerIconAlt: (providerName) => t('llmTabLabels.providerModal.providerIconAlt', { providerName }),
270755
+ tooLargeTitle: t('llmTabLabels.providerModal.tooLargeTitle'),
270756
+ tooLargeDescription: (modelName, modelSize) => t('llmTabLabels.providerModal.tooLargeDescription', { modelName, modelSize }),
270757
+ cancel: t('llmTabLabels.providerModal.cancel'),
270758
+ downloadAnyway: t('llmTabLabels.providerModal.downloadAnyway'),
270759
+ alreadyDownloadingTitle: t('llmTabLabels.providerModal.alreadyDownloadingTitle'),
270760
+ unnamedModel: t('llmTabLabels.providerModal.unnamedModel'),
270761
+ alreadyDownloadingDescription: (modelName) => t('llmTabLabels.providerModal.alreadyDownloadingDescription', { modelName }),
270762
+ gotIt: t('llmTabLabels.providerModal.gotIt'),
270763
+ announceDownloaded: (modelName) => t('llmTabLabels.providerModal.announceDownloaded', { modelName }),
270764
+ announceCancelled: t('llmTabLabels.providerModal.announceCancelled'),
270765
+ announceFailed: t('llmTabLabels.providerModal.announceFailed'),
270766
+ announceStarted: (modelName) => t('llmTabLabels.providerModal.announceStarted', { modelName }),
270767
+ localModel: {
270768
+ onDevice: t('llmTabLabels.providerModal.localModel.onDevice'),
270769
+ starting: t('llmTabLabels.providerModal.localModel.starting'),
270770
+ cancel: t('llmTabLabels.providerModal.localModel.cancel'),
270771
+ inUse: t('llmTabLabels.providerModal.localModel.inUse'),
270772
+ downloadFailedRetry: t('llmTabLabels.providerModal.localModel.downloadFailedRetry'),
270773
+ ariaDownload: (modelName, modelSize) => t('llmTabLabels.providerModal.localModel.ariaDownload', { modelName, modelSize }),
270774
+ ariaStarting: (modelName) => t('llmTabLabels.providerModal.localModel.ariaStarting', { modelName }),
270775
+ ariaDownloading: (modelName, percent) => t('llmTabLabels.providerModal.localModel.ariaDownloading', { modelName, percent }),
270776
+ ariaInstalled: (modelName) => t('llmTabLabels.providerModal.localModel.ariaInstalled', { modelName }),
270777
+ ariaSelected: (modelName) => t('llmTabLabels.providerModal.localModel.ariaSelected', { modelName }),
270778
+ ariaError: (modelName) => t('llmTabLabels.providerModal.localModel.ariaError', { modelName }),
270779
+ ariaErrorWithReason: (modelName, reason) => t('llmTabLabels.providerModal.localModel.ariaErrorWithReason', { modelName, reason }),
270780
+ },
270442
270781
  },
270443
270782
  };
270444
270783
  }
@@ -270450,38 +270789,287 @@ function resolveLLMTabLabels(override, t) {
270450
270789
  return deepMerge(base, override);
270451
270790
  }
270452
270791
 
270453
- function LLMProviderModal({ isOpen, onClose, onSelect, llmProvider, isSelecting, mentorSettings, llms, getLLMProviderDetails, labels = AGENT_LLM_TAB_LABELS.providerModal, }) {
270792
+ /**
270793
+ * A local (on-device) model presented in the same list as cloud models. It uses
270794
+ * the same provider logo as the cloud rows; the model name gets its own line and
270795
+ * the on-device badge, size, and download/status affordance sit on a subtitle
270796
+ * line beneath it. One click toggles download ⇄ cancel; once installed a click
270797
+ * selects it.
270798
+ */
270799
+ function LocalModelRow({ name, size, logo, status, progress, disabled = false, disabledReason, errorMessage, onActivate, labels = AGENT_LLM_TAB_LABELS.providerModal.localModel, }) {
270800
+ const selected = status === 'selected';
270801
+ const downloading = status === 'downloading' || status === 'starting';
270802
+ // A resting row (nothing downloading) can still be disabled when another
270803
+ // model is pulling; a row that is itself downloading stays clickable (cancel).
270804
+ const isBusyDisabled = disabled && !downloading && !selected;
270805
+ const inert = isBusyDisabled || selected;
270806
+ const pct = Math.round(Math.max(0, Math.min(100, progress)));
270807
+ const ariaLabel = (() => {
270808
+ switch (status) {
270809
+ case 'not-installed':
270810
+ return labels.ariaDownload(name, size);
270811
+ case 'starting':
270812
+ return labels.ariaStarting(name);
270813
+ case 'downloading':
270814
+ return labels.ariaDownloading(name, pct);
270815
+ case 'installed':
270816
+ return labels.ariaInstalled(name);
270817
+ case 'selected':
270818
+ return labels.ariaSelected(name);
270819
+ case 'error':
270820
+ return errorMessage
270821
+ ? labels.ariaErrorWithReason(name, errorMessage)
270822
+ : labels.ariaError(name);
270823
+ }
270824
+ })();
270825
+ return (jsxs("button", { type: "button", "data-testid": "llm-local-model-row", "data-model": name, "data-status": status, disabled: inert, "aria-label": ariaLabel, title: isBusyDisabled ? disabledReason : ariaLabel, onClick: () => {
270826
+ if (!inert)
270827
+ onActivate();
270828
+ }, className: cn(
270829
+ // `isolate` makes the button its own stacking context so the absolute
270830
+ // progress fill (z-0) layers above the button's background and below the
270831
+ // content (z-10) — without it the fill can render behind the background.
270832
+ 'group relative isolate flex items-center gap-3 overflow-hidden rounded-lg border p-4 text-left transition-colors', selected
270833
+ ? 'cursor-default border-blue-500 bg-blue-50'
270834
+ : isBusyDisabled
270835
+ ? 'cursor-not-allowed border-gray-200 opacity-60'
270836
+ : 'cursor-pointer border-gray-200 hover:border-blue-500 hover:bg-blue-50'), children: [downloading && (jsx("span", { "aria-hidden": "true", "data-testid": "llm-local-model-progress", className: "pointer-events-none absolute inset-y-0 left-0 z-0 bg-blue-500/40 motion-safe:transition-[width] motion-safe:duration-300",
270837
+ // Minimum width so the bar is visible the moment a download starts
270838
+ // (progress 0 / "starting"), then tracks real progress.
270839
+ style: { width: `${status === 'starting' ? 6 : Math.max(pct, 6)}%` } })), jsx("span", { className: "relative z-10 flex h-8 w-8 flex-shrink-0 items-center justify-center overflow-hidden rounded-lg", children: jsx(Image$4, { src: logo, alt: "", "aria-hidden": "true", width: 32, height: 32, className: cn('h-full w-full object-contain', {
270840
+ grayscale: isBusyDisabled,
270841
+ }), loading: "lazy" }) }), jsxs("span", { className: "relative z-10 flex min-w-0 flex-1 flex-col gap-1.5", children: [jsx("span", { className: "truncate text-sm font-medium text-[#646464] lowercase", children: name }), jsxs("span", { className: "flex min-w-0 items-center gap-1.5 text-sm text-gray-500", children: [jsx("span", { className: "flex-shrink-0 rounded bg-gray-100 px-2 py-0.5 text-sm font-medium text-gray-500", children: labels.onDevice }), status === 'not-installed' && (jsxs(Fragment$1, { children: [jsx("span", { "aria-hidden": "true", children: "\u00B7" }), jsx("span", { children: size }), jsx(Download, { className: "h-4 w-4", "aria-hidden": "true" })] })), status === 'starting' && (jsxs(Fragment$1, { children: [jsx("span", { "aria-hidden": "true", children: "\u00B7" }), jsx("span", { children: labels.starting })] })), status === 'downloading' && (jsxs(Fragment$1, { children: [jsx("span", { "aria-hidden": "true", children: "\u00B7" }), jsxs("span", { className: "tabular-nums group-hover:hidden group-focus-visible:hidden", children: [pct, "%"] }), jsxs("span", { className: "hidden items-center gap-1 text-blue-600 group-hover:flex group-focus-visible:flex", children: [jsx(X$2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), labels.cancel] })] })), status === 'selected' && (jsxs("span", { className: "flex items-center gap-1 text-blue-600", children: [jsx(Check, { className: "h-4 w-4", "aria-hidden": "true" }), labels.inUse] })), status === 'error' && (jsxs(Fragment$1, { children: [jsx("span", { "aria-hidden": "true", children: "\u00B7" }), jsx("span", { className: "truncate text-red-500", children: errorMessage || labels.downloadFailedRetry }), jsx(RotateCcw, { className: "h-4 w-4 flex-shrink-0 text-red-500", "aria-hidden": "true" })] }))] })] })] }));
270842
+ }
270843
+
270844
+ function LLMProviderModal({ isOpen, onClose, onSelect, llmProvider, isSelecting, mentorSettings, llms, getLLMProviderDetails: getLLMProviderDetails$1 = getLLMProviderDetails, labels = AGENT_LLM_TAB_LABELS.providerModal, }) {
270845
+ var _a, _b;
270454
270846
  const [searchQuery, setSearchQuery] = React__default.useState('');
270847
+ // On-device model download state (Tauri desktop only; `isAvailable` is false
270848
+ // on web, which hides every local row below).
270849
+ const { isAvailable, state: downloadState, ollamaStatus, systemMemory, startDownload, cancelDownload, } = useModelDownload();
270850
+ // Mirror the device-global local selection so picks re-render immediately.
270851
+ const [localSel, setLocalSel] = React__default.useState(() => ({
270852
+ enabled: isLocalLLMEnabled(),
270853
+ model: getLocalLLMModel(),
270854
+ }));
270855
+ // Model awaiting the "may be too large" confirmation before its download
270856
+ // starts; null when no confirmation is pending.
270857
+ const [pendingModel, setPendingModel] = React__default.useState(null);
270858
+ // Name of the model currently downloading, shown in an info dialog when the
270859
+ // user clicks a DIFFERENT model (one on-device download at a time). Null when
270860
+ // no such notice is showing.
270861
+ const [busyDownloadName, setBusyDownloadName] = React__default.useState(null);
270862
+ // The model THIS picker asked to download. Modal-local (not the shared, churny
270863
+ // download state) so it survives the hook's once-per-load reset / remounts /
270864
+ // reloads that wipe `downloadState.activeModel` — the pull's progress events
270865
+ // carry no model id, so once `activeModel` is lost nothing restores it and the
270866
+ // row can't recognize its own in-flight download (bar advances, row still says
270867
+ // "Download"). Cleared only on a terminal status, so the reset's transient
270868
+ // `idle` doesn't drop it mid-pull.
270869
+ const [requestedModel, setRequestedModel] = React__default.useState(null);
270870
+ React__default.useEffect(() => {
270871
+ if (downloadState.status === 'completed' ||
270872
+ downloadState.status === 'cancelled' ||
270873
+ downloadState.status === 'error') {
270874
+ setRequestedModel(null);
270875
+ }
270876
+ }, [downloadState.status]);
270877
+ // Which on-device model is downloading right now: prefer this picker's own
270878
+ // request, fall back to the shared state's id when present.
270879
+ const downloadingModel = requestedModel !== null && requestedModel !== void 0 ? requestedModel : downloadState.activeModel;
270880
+ // The last on-device model whose download reported `completed`. Held sticky so
270881
+ // its row shows "installed" straight through the gap between the terminal
270882
+ // `completed` event and the /api/tags refresh landing — otherwise the completed
270883
+ // handler's checkStatus churns downloadState.status (completed → checking →
270884
+ // idle) first and the row flickers back to a "Download" button.
270885
+ const [lastCompleted, setLastCompleted] = React__default.useState(null);
270886
+ React__default.useEffect(() => {
270887
+ if (downloadState.status === 'completed' && downloadingModel) {
270888
+ setLastCompleted(downloadingModel);
270889
+ }
270890
+ }, [downloadState.status, downloadingModel]);
270455
270891
  const filteredLLMs = React__default.useMemo(() => {
270456
- return llmProvider === null || llmProvider === void 0 ? void 0 : llmProvider.chat_models.filter((llm) => llm.llm_name.toLowerCase().includes(searchQuery.toLowerCase()));
270892
+ var _a, _b;
270893
+ // Guard `chat_models` (not just `llmProvider`): a provider can come back from
270894
+ // the API without a models array, and an undefined `.filter` here throws in
270895
+ // render and unmounts the whole dialog. Coalesce so the `.map` consumer below
270896
+ // always gets an array.
270897
+ return ((_b = (_a = llmProvider === null || llmProvider === void 0 ? void 0 : llmProvider.chat_models) === null || _a === void 0 ? void 0 : _a.filter((llm) => {
270898
+ var _a;
270899
+ const query = searchQuery.toLowerCase();
270900
+ // Match either label: rows are shown by `display_name`, so searching
270901
+ // what you can read has to work, but the raw key stays searchable for
270902
+ // anyone who knows it (e.g. "gpt-4.1" or "amazon.nova").
270903
+ return (llm.llm_name.toLowerCase().includes(query) ||
270904
+ ((_a = llm.display_name) !== null && _a !== void 0 ? _a : '').toLowerCase().includes(query));
270905
+ })) !== null && _b !== void 0 ? _b : []);
270457
270906
  }, [searchQuery, llmProvider]);
270907
+ // Local models belonging to THIS provider (merged into the same list).
270908
+ const localModels = React__default.useMemo(() => {
270909
+ if (!isAvailable)
270910
+ return [];
270911
+ const key = getProviderName(llmProvider.name);
270912
+ return LOCAL_MODELS.filter((m) => getProviderName(m.provider) === key &&
270913
+ m.name.toLowerCase().includes(searchQuery.toLowerCase()));
270914
+ }, [isAvailable, llmProvider.name, searchQuery]);
270458
270915
  const switchLLMAllowed = canSwitchLLm(llmProvider);
270459
270916
  const switchProviderAllowed = canSwitchProvider(llms, llmProvider.name);
270460
- return (jsx(Dialog, { open: isOpen, onOpenChange: onClose, children: jsxs(DialogContent, { className: "max-w-4xl space-y-6 p-4", children: [jsx(DialogDescription, { className: "sr-only", children: labels.description }), jsx(DialogHeader, { children: jsx(DialogTitle, { className: "ibl-dialog-title", children: labels.title }) }), jsx("p", { className: "text-sm text-gray-600", children: labels.helpText }), 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, { placeholder: labels.searchPlaceholder, className: "py-6 pl-9", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), autoComplete: "off", disabled: isSelecting })] }), jsx("div", { className: "grid max-h-[60vh] grid-cols-1 gap-4 overflow-y-auto pr-2 sm:grid-cols-2 lg:grid-cols-3", children: filteredLLMs.map((llm) => {
270461
- const isActive = (mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_name) === llm.llm_name &&
270462
- (mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_provider) === llmProvider.name;
270463
- const providerDetails = getLLMProviderDetails(llmProvider.name, llm.llm_name);
270464
- const isDisabled = !switchLLMAllowed || isSelecting || isActive || !switchProviderAllowed;
270465
- return (jsxs("button", { disabled: isDisabled, onClick: () => {
270466
- onSelect(llmProvider.name, llm.llm_name);
270467
- }, className: cn('flex cursor-pointer items-center gap-3 rounded-lg border border-gray-200 p-4 transition-colors', {
270468
- // Matches the provider grid's inactive treatment: tinted
270469
- // card, faded logo, full-strength label.
270470
- 'cursor-not-allowed border-gray-100 bg-gray-50': isDisabled && !isActive,
270471
- 'hover:border-blue-500 hover:bg-blue-50': !isDisabled,
270472
- 'cursor-not-allowed border-blue-500 bg-blue-50': isActive,
270473
- }), children: [jsx("span", { className: "flex h-8 w-8 items-center justify-center overflow-hidden rounded-lg", children: jsx(Image$4, { src: providerDetails.logo, alt: `${providerDetails.name} icon`, className: cn('h-full w-full object-contain', {
270474
- // `grayscale` alone is a no-op on already-black marks,
270475
- // so fade them as well same rule as the provider grid.
270476
- 'opacity-40 grayscale': isDisabled && !isActive,
270477
- }), width: 32, height: 32, loading: "lazy" }) }), jsx("span", { className: "text-left text-sm font-medium text-gray-900", children: llm.llm_name })] }, llm.llm_name));
270478
- }) })] }) }));
270479
- }
270480
-
270481
- function AgentLLMTab({ showConfigurationHeader = true, getLLMProviderDetails, labels: labelsOverride, }) {
270917
+ // Only an actual pull counts as "busy". A status refresh ('checking') must NOT
270918
+ // otherwise a stale `activeModel` makes a row flash a progress bar every time
270919
+ // status is re-checked.
270920
+ const busyDownloading = downloadState.status === 'downloading';
270921
+ const statusFor = (m) => {
270922
+ if (localSel.enabled && localSel.model === m.id)
270923
+ return 'selected';
270924
+ const active = sameModel(downloadingModel, m.id);
270925
+ if (active && downloadState.status === 'error')
270926
+ return 'error';
270927
+ if (active && busyDownloading)
270928
+ return downloadState.progress > 0 ? 'downloading' : 'starting';
270929
+ // `lastCompleted` bridges the just-finished → tags-refreshed gap so the row
270930
+ // stays "installed" instead of flickering back to "Download".
270931
+ if (sameModel(lastCompleted, m.id) ||
270932
+ isLocalModelInstalled(m.id, ollamaStatus === null || ollamaStatus === void 0 ? void 0 : ollamaStatus.installed_models))
270933
+ return 'installed';
270934
+ return 'not-installed';
270935
+ };
270936
+ // A cloud model is "in use" when it's the mentor's selected LLM and local mode
270937
+ // is off.
270938
+ const isCloudActive = (llm) => (mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_name) === llm.llm_name &&
270939
+ (mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_provider) === llmProvider.name &&
270940
+ !localSel.enabled;
270941
+ // Order the WHOLE list — cloud + local — available (usable now) first,
270942
+ // unavailable last, with the in-use model leading. "Available" for a cloud
270943
+ // model means the provider is switchable (has credentials + models); for a
270944
+ // local model it's the install state. So an installed on-device model outranks
270945
+ // cloud models whose provider has no credentials. Ranked by state, not the
270946
+ // transient download status, so rows don't jump mid-pull; a stable sort keeps
270947
+ // the original (catalog) order within each rank.
270948
+ const cloudRank = (llm) => isCloudActive(llm) ? 0 : switchLLMAllowed && switchProviderAllowed ? 1 : 2;
270949
+ const localRank = (m) => {
270950
+ const s = statusFor(m);
270951
+ return s === 'selected' ? 0 : s === 'installed' ? 1 : 2;
270952
+ };
270953
+ const sortedModels = [
270954
+ ...filteredLLMs.map((llm) => ({ kind: 'cloud', llm, rank: cloudRank(llm) })),
270955
+ ...localModels.map((model) => ({ kind: 'local', model, rank: localRank(model) })),
270956
+ ].sort((a, b) => a.rank - b.rank);
270957
+ // Preserve the "model may be too large" prompt before a download starts. Opens
270958
+ // the confirmation AlertDialog below (layered on top of the picker), rather
270959
+ // than a toast. `modelExceedsCapacity` compares the model's size to this
270960
+ // machine's RAM/VRAM; when memory is unknown it returns false, so the download
270961
+ // simply proceeds rather than blocking on incomplete data.
270962
+ const startLocalDownload = (m) => {
270963
+ if (modelExceedsCapacity(m, systemMemory)) {
270964
+ setPendingModel(m);
270965
+ return;
270966
+ }
270967
+ setRequestedModel(m.id);
270968
+ startDownload(m.id);
270969
+ };
270970
+ const activateLocal = (m, status) => {
270971
+ var _a;
270972
+ switch (status) {
270973
+ case 'not-installed':
270974
+ case 'error':
270975
+ // Only one on-device model downloads at a time. If ANY pull is already
270976
+ // running, don't start another — starting concurrent pulls floods every
270977
+ // useModelDownload instance with progress events and can hang the app.
270978
+ // Block even when the running model is unknown (activeModel wiped); the
270979
+ // user cancels by clicking the downloading row itself. Selecting an
270980
+ // already-installed model is unaffected (it starts no pull).
270981
+ if (busyDownloading && !sameModel(downloadingModel, m.id)) {
270982
+ const dl = LOCAL_MODELS.find((x) => sameModel(x.id, downloadingModel));
270983
+ setBusyDownloadName((_a = dl === null || dl === void 0 ? void 0 : dl.name) !== null && _a !== void 0 ? _a : labels.unnamedModel);
270984
+ return;
270985
+ }
270986
+ startLocalDownload(m);
270987
+ break;
270988
+ case 'starting':
270989
+ case 'downloading':
270990
+ cancelDownload();
270991
+ break;
270992
+ case 'installed':
270993
+ // Use this on-device model — device-global, mutually exclusive with cloud.
270994
+ setLocalLLMModel(m.id);
270995
+ setLocalLLMEnabled(true);
270996
+ // Persist tool-calling support so local chat routes to the MCP/tool
270997
+ // bridge. Without this the flag stays at its default (false) and
270998
+ // streaming chat rejects the model with "tool_support=false" even though
270999
+ // the catalog marks it tool-capable.
271000
+ setLocalLLMToolSupport(m.tool_support);
271001
+ setLocalSel({ enabled: true, model: m.id });
271002
+ // Notify same-tab listeners (e.g. a nav-bar on-device badge).
271003
+ window.dispatchEvent(new Event(LOCAL_LLM_CHANGED_EVENT));
271004
+ break;
271005
+ }
271006
+ };
271007
+ const selectCloud = (llmName) => {
271008
+ // Picking a cloud model turns local mode off so routing uses the cloud LLM.
271009
+ if (localSel.enabled) {
271010
+ setLocalLLMEnabled(false);
271011
+ setLocalSel((prev) => ({ ...prev, enabled: false }));
271012
+ window.dispatchEvent(new Event(LOCAL_LLM_CHANGED_EVENT));
271013
+ }
271014
+ onSelect(llmProvider.name, llmName);
271015
+ };
271016
+ // Announce download lifecycle changes once (not every %) for screen readers.
271017
+ const activeLocal = LOCAL_MODELS.find((m) => sameModel(m.id, downloadingModel));
271018
+ const liveMessage = !activeLocal
271019
+ ? ''
271020
+ : downloadState.status === 'completed'
271021
+ ? labels.announceDownloaded(activeLocal.name)
271022
+ : downloadState.status === 'cancelled'
271023
+ ? labels.announceCancelled
271024
+ : downloadState.status === 'error'
271025
+ ? labels.announceFailed
271026
+ : downloadState.status === 'checking' ||
271027
+ (downloadState.status === 'downloading' && downloadState.progress === 0)
271028
+ ? labels.announceStarted(activeLocal.name)
271029
+ : '';
271030
+ return (jsx(Dialog, { open: isOpen, onOpenChange: onClose, children: jsxs(DialogContent, { className: "max-w-4xl space-y-6 p-4", children: [jsx(DialogDescription, { className: "sr-only", children: labels.description(llmProvider.name) }), jsx(DialogHeader, { children: jsx(DialogTitle, { className: "ibl-dialog-title", children: labels.title }) }), jsx("p", { className: "text-sm text-gray-600", children: labels.helpText }), 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, { placeholder: labels.searchPlaceholder, className: "py-6 pl-9", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), autoComplete: "off", disabled: isSelecting })] }), jsx("div", { className: "sr-only", "aria-live": "polite", role: "status", children: liveMessage }), jsx("div", { className: "grid max-h-[60vh] grid-cols-1 gap-4 overflow-y-auto pr-2 sm:grid-cols-2 lg:grid-cols-3", children: sortedModels.map((item) => {
271031
+ if (item.kind === 'cloud') {
271032
+ const llm = item.llm;
271033
+ const isActive = isCloudActive(llm);
271034
+ const providerDetails = getLLMProviderDetails$1(llmProvider.name, llm.llm_name);
271035
+ const isDisabled = !switchLLMAllowed || isSelecting || isActive || !switchProviderAllowed;
271036
+ return (jsxs("button", { "data-model": llm.llm_name, disabled: isDisabled, onClick: () => {
271037
+ selectCloud(llm.llm_name);
271038
+ }, className: cn('flex cursor-pointer items-center gap-3 rounded-lg border border-gray-200 p-4 transition-colors', {
271039
+ // Matches the provider grid's inactive treatment: tinted
271040
+ // card, faded logo, full-strength label.
271041
+ 'cursor-not-allowed border-gray-100 bg-gray-50': isDisabled && !isActive,
271042
+ 'hover:border-blue-500 hover:bg-blue-50': !isDisabled,
271043
+ 'cursor-not-allowed border-blue-500 bg-blue-50': isActive,
271044
+ }), children: [jsx("span", { className: "flex h-8 w-8 items-center justify-center overflow-hidden rounded-lg", children: jsx(Image$4, { src: providerDetails.logo, alt: labels.providerIconAlt(providerDetails.name), className: cn('h-full w-full object-contain', {
271045
+ // `grayscale` alone is a no-op on already-black marks,
271046
+ // so fade them as well — same rule as the provider grid.
271047
+ 'opacity-40 grayscale': isDisabled && !isActive,
271048
+ }), width: 32, height: 32, loading: "lazy" }) }), jsx("span", { className: "text-left text-sm font-medium text-gray-900", children: llm.display_name || llm.llm_name })] }, `cloud-${llm.llm_name}`));
271049
+ }
271050
+ const m = item.model;
271051
+ const status = statusFor(m);
271052
+ return (jsx(LocalModelRow, { name: m.name, size: m.size, logo: getLLMProviderDetails$1(llmProvider.name, m.name).logo, status: status, progress: sameModel(downloadingModel, m.id) ? downloadState.progress : 0, onActivate: () => activateLocal(m, status), labels: labels.localModel }, `local-${m.id}`));
271053
+ }) }), jsx(AlertDialog, { open: !!pendingModel, onOpenChange: (open) => {
271054
+ if (!open)
271055
+ setPendingModel(null);
271056
+ }, children: jsxs(AlertDialogContent, { children: [jsxs(AlertDialogHeader, { children: [jsx(AlertDialogTitle, { children: labels.tooLargeTitle }), jsx(AlertDialogDescription, { children: labels.tooLargeDescription((_a = pendingModel === null || pendingModel === void 0 ? void 0 : pendingModel.name) !== null && _a !== void 0 ? _a : '', (_b = pendingModel === null || pendingModel === void 0 ? void 0 : pendingModel.size) !== null && _b !== void 0 ? _b : '') })] }), jsxs(AlertDialogFooter, { children: [jsx(AlertDialogCancel, { onClick: () => setPendingModel(null), children: labels.cancel }), jsx(AlertDialogAction, { onClick: () => {
271057
+ if (pendingModel) {
271058
+ setRequestedModel(pendingModel.id);
271059
+ startDownload(pendingModel.id);
271060
+ }
271061
+ setPendingModel(null);
271062
+ }, children: labels.downloadAnyway })] })] }) }), jsx(AlertDialog, { open: !!busyDownloadName, onOpenChange: (open) => {
271063
+ if (!open)
271064
+ setBusyDownloadName(null);
271065
+ }, children: jsxs(AlertDialogContent, { children: [jsxs(AlertDialogHeader, { children: [jsx(AlertDialogTitle, { children: labels.alreadyDownloadingTitle }), jsx(AlertDialogDescription, { children: labels.alreadyDownloadingDescription(busyDownloadName !== null && busyDownloadName !== void 0 ? busyDownloadName : '') })] }), jsx(AlertDialogFooter, { children: jsx(AlertDialogAction, { onClick: () => setBusyDownloadName(null), children: labels.gotIt }) })] }) })] }) }));
271066
+ }
271067
+
271068
+ function AgentLLMTab({ showConfigurationHeader = true, mentorId: mentorIdOverride, getLLMProviderDetails: getLLMProviderDetails$1 = getLLMProviderDetails, labels: labelsOverride, }) {
270482
271069
  var _a;
270483
271070
  const t = useT();
270484
- const { tenantKey, mentorId, username, enableRBAC } = useAgentSettings();
271071
+ const { tenantKey, mentorId: contextMentorId, username, enableRBAC } = useAgentSettings();
271072
+ const mentorId = mentorIdOverride || contextMentorId;
270485
271073
  const labels = React__default.useMemo(() => resolveLLMTabLabels(labelsOverride, t), [labelsOverride, t]);
270486
271074
  const [searchQuery, setSearchQuery] = React__default.useState('');
270487
271075
  const [selectedLLMProvider, setSelectedLLMProvider] = React__default.useState(null);
@@ -270499,13 +271087,44 @@ function AgentLLMTab({ showConfigurationHeader = true, getLLMProviderDetails, la
270499
271087
  }, {
270500
271088
  skip: !tenantKey || !username,
270501
271089
  });
271090
+ // Providers that only offer on-device (local) models — surfaced as grid cards
271091
+ // (Tauri desktop only) so their downloadable models are reachable even when the
271092
+ // backend LLM list has no matching cloud provider.
271093
+ const localOnlyProviders = React__default.useMemo(() => {
271094
+ if (!isTauriApp$1())
271095
+ return [];
271096
+ const cloudKeys = new Set((llmProviders !== null && llmProviders !== void 0 ? llmProviders : []).map((p) => getProviderName(p.name)));
271097
+ const seen = new Set();
271098
+ const result = [];
271099
+ for (const model of LOCAL_MODELS) {
271100
+ const key = getProviderName(model.provider);
271101
+ if (cloudKeys.has(key) || seen.has(key))
271102
+ continue;
271103
+ seen.add(key);
271104
+ result.push({ key, provider: model.provider });
271105
+ }
271106
+ // Alphabetical among themselves; the whole local-only block still renders
271107
+ // after the API providers.
271108
+ return result.sort((a, b) => compareLLMProvidersByDisplayName(a.provider, b.provider, getLLMProviderDetails$1));
271109
+ }, [llmProviders, getLLMProviderDetails$1]);
270502
271110
  // Two alphabetical groups: providers we have a usable LLM key for first, then
270503
271111
  // the ones we don't. Ordering is independent of the search filter below, so it
270504
271112
  // holds for whatever the filter leaves.
270505
- const sortedLLMProviders = React__default.useMemo(() => sortLLMProvidersByCredentials(llmProviders !== null && llmProviders !== void 0 ? llmProviders : [], getLLMProviderDetails), [llmProviders, getLLMProviderDetails]);
271113
+ const sortedLLMProviders = React__default.useMemo(() => sortLLMProvidersByCredentials(llmProviders !== null && llmProviders !== void 0 ? llmProviders : [], getLLMProviderDetails$1), [llmProviders, getLLMProviderDetails$1]);
270506
271114
  const [editMentor, { isLoading: isEditingMentor }] = useEditMentorMutation();
270507
271115
  const isDisabled = isMentorSettingsLoading || isLoadingLLMProviders || isEditingMentor;
270508
271116
  const isLoading = isMentorSettingsLoading || isLoadingLLMProviders;
271117
+ // The highlighted (selected) provider card must reflect the model chat
271118
+ // actually uses. When on-device mode is on, that's the selected local model's
271119
+ // provider (e.g. Llama 3.2 → Meta) — NOT the agent's stale cloud
271120
+ // `llm_provider` (which stays e.g. OpenAI). Falls back to the cloud provider
271121
+ // when local mode is off. Reactive so it updates the instant a model is picked.
271122
+ const selectedLocal = useSelectedLocalModel();
271123
+ const activeProviderKey = selectedLocal.isLocal && selectedLocal.model
271124
+ ? getProviderName(selectedLocal.model.provider)
271125
+ : (mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_provider)
271126
+ ? getProviderName(mentorSettings.llm_provider)
271127
+ : '';
270509
271128
  async function updateMentorLLM(llmProvider, llmName) {
270510
271129
  try {
270511
271130
  await editMentor({
@@ -270522,57 +271141,70 @@ function AgentLLMTab({ showConfigurationHeader = true, getLLMProviderDetails, la
270522
271141
  }
270523
271142
  catch (error) {
270524
271143
  console.error(JSON.stringify(error));
270525
- const errorMessage = extractErrorMessage(error, labels.toasts.updateError);
271144
+ const errorMessage = extractErrorMessage$1(error, labels.toasts.updateError);
270526
271145
  toast.error(errorMessage);
270527
271146
  console.error(JSON.stringify({ tenant: tenantKey, error }));
270528
271147
  }
270529
271148
  }
270530
- return (jsxs(Fragment$1, { children: [showConfigurationHeader && (jsx("div", { className: "flex hidden 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-700", children: labels.header.description })] }) })), jsx("div", { className: cn('flex-1 space-y-4 pt-3 pb-3 lg:pt-4 lg:pb-4', showConfigurationHeader ? 'px-3 lg:px-4' : 'px-0 lg:px-0'), style: {
271149
+ return (jsxs(Fragment$1, { children: [showConfigurationHeader && (jsx("div", { className: "flex hidden 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-700", children: labels.header.description })] }) })), jsxs("div", { className: cn('flex-1 space-y-4 pt-3 pb-3 lg:pt-4 lg:pb-4', showConfigurationHeader ? 'px-3 lg:px-4' : 'px-0 lg:px-0'), style: {
270531
271150
  overflowY: 'auto',
270532
271151
  overflowX: 'hidden',
270533
- }, children: jsxs("div", { className: "space-y-6", 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-500" }), jsx(Input, { type: "search", placeholder: labels.search.placeholder, className: "w-full py-6 pl-9 md:w-1/2", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), disabled: isDisabled })] }), isLoading ? (jsx("div", { className: "flex items-center justify-center py-10", children: jsx(Spinner, {}) })) : (jsx(WithFormPermissions, { name: "llm_provider",
270534
- // @ts-ignore
270535
- permissions: (_a = mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.permissions) === null || _a === void 0 ? void 0 : _a.field, enableRBAC: enableRBAC, children: ({ disabled }) => (jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: sortedLLMProviders
270536
- .filter((model) => model.name.toLowerCase().includes(searchQuery.toLowerCase()))
270537
- .map((model) => {
270538
- const providerDetails = getLLMProviderDetails(model.name);
270539
- const isActive = (mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_provider) === model.name;
270540
- // The generated LLM response type doesn't declare the
270541
- // credential flags the backend actually sends, so read them
270542
- // through the access-shaped view the helper expects.
270543
- const isGrayed = !canAccessProvider(model) && !isActive;
270544
- return (jsxs("div", { "data-testid": "llm-provider-card", "data-provider": model.name, "data-disabled": isGrayed ? 'true' : 'false', className: cn('flex cursor-pointer items-center gap-3 rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-shadow hover:shadow-md', {
270545
- 'border-blue-500': isActive,
270546
- // Tint the whole card too: desaturating a logo that
270547
- // is already black changes nothing, so the card
270548
- // itself has to carry some of the inactive signal.
270549
- 'border-gray-100 bg-gray-50': isGrayed,
270550
- }), onClick: () => {
270551
- if (isDisabled || disabled)
270552
- return;
270553
- setSelectedLLMProvider({
270554
- id: model.id,
270555
- name: model.name,
270556
- logo: model.logo,
270557
- description: model.description,
270558
- chat_models: model.chat_models,
270559
- // @ts-ignore - has_credentials property does not exist on LLMResponse type
270560
- has_credentials: model === null || model === void 0 ? void 0 : model.has_credentials,
270561
- // @ts-ignore - main_has_credentials property does not exist on LLMResponse type
270562
- main_has_credentials: model === null || model === void 0 ? void 0 : model.main_has_credentials,
270563
- // @ts-ignore - can_use_main_keys property does not exist on LLMResponse type
270564
- can_use_main_keys: model === null || model === void 0 ? void 0 : model.can_use_main_keys,
270565
- });
270566
- }, children: [jsx("div", { className: "h-8 w-8 flex-shrink-0", children: jsx(Image$4, { src: providerDetails.logo, alt: t('tabsLlmTab.providerLogoAlt', { name: providerDetails.name }), className: cn('h-full w-full object-contain', {
270567
- // `grayscale` only desaturates, so already-black
270568
- // marks (OpenAI, xAI) looked untouched. Fading is
270569
- // what actually reads as inactive at any hue.
270570
- 'opacity-40 grayscale': isGrayed,
270571
- }), width: 32, height: 32, loading: "lazy" }) }), jsx("span", { className: "text-sm font-medium text-gray-900", children: providerDetails.name })] }, model.id));
270572
- }) })) })), selectedLLMProvider && mentorSettings && (jsx(LLMProviderModal, { isOpen: true, onClose: () => setSelectedLLMProvider(null), onSelect: updateMentorLLM, llmProvider: selectedLLMProvider, isSelecting: isEditingMentor, mentorSettings: {
270573
- llm_name: mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_name,
270574
- llm_provider: mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_provider,
270575
- }, llms: (llmProviders !== null && llmProviders !== void 0 ? llmProviders : []), getLLMProviderDetails: getLLMProviderDetails, labels: labels.providerModal }))] }) })] }));
271152
+ }, children: [jsx("div", { className: "rounded-md border border-gray-200 bg-gray-50 p-3 text-xs text-gray-600", "data-testid": "llm-info-box", children: labels.infoBox }), jsxs("div", { className: "space-y-6", 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-500" }), jsx(Input, { type: "search", placeholder: labels.search.placeholder, className: "w-full py-6 pl-9 md:w-1/2", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), disabled: isDisabled })] }), isLoading ? (jsx("div", { className: "flex items-center justify-center py-10", children: jsx(Spinner, {}) })) : (jsx(WithFormPermissions, { name: "llm_provider",
271153
+ // @ts-ignore
271154
+ permissions: (_a = mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.permissions) === null || _a === void 0 ? void 0 : _a.field, enableRBAC: enableRBAC, children: ({ disabled }) => (jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: [sortedLLMProviders
271155
+ .filter((model) => model.name.toLowerCase().includes(searchQuery.toLowerCase()))
271156
+ .map((model) => {
271157
+ const providerDetails = getLLMProviderDetails$1(model.name);
271158
+ const isActive = !!activeProviderKey && getProviderName(model.name) === activeProviderKey;
271159
+ // The generated LLM response type doesn't declare the
271160
+ // credential flags the backend actually sends, so read them
271161
+ // through the access-shaped view the helper expects.
271162
+ const isGrayed = !canAccessProvider(model) && !isActive;
271163
+ return (jsxs("div", { "data-testid": "llm-provider-card", "data-provider": model.name, "data-disabled": isGrayed ? 'true' : 'false', className: cn('flex cursor-pointer items-center gap-3 rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-shadow hover:shadow-md', {
271164
+ 'border-blue-500': isActive,
271165
+ // Tint the whole card too: desaturating a logo that
271166
+ // is already black changes nothing, so the card
271167
+ // itself has to carry some of the inactive signal.
271168
+ 'border-gray-100 bg-gray-50': isGrayed,
271169
+ }), onClick: () => {
271170
+ if (isDisabled || disabled)
271171
+ return;
271172
+ setSelectedLLMProvider({
271173
+ id: model.id,
271174
+ name: model.name,
271175
+ logo: model.logo,
271176
+ description: model.description,
271177
+ chat_models: model.chat_models,
271178
+ // @ts-ignore - has_credentials property does not exist on LLMResponse type
271179
+ has_credentials: model === null || model === void 0 ? void 0 : model.has_credentials,
271180
+ // @ts-ignore - main_has_credentials property does not exist on LLMResponse type
271181
+ main_has_credentials: model === null || model === void 0 ? void 0 : model.main_has_credentials,
271182
+ // @ts-ignore - can_use_main_keys property does not exist on LLMResponse type
271183
+ can_use_main_keys: model === null || model === void 0 ? void 0 : model.can_use_main_keys,
271184
+ });
271185
+ }, children: [jsx("div", { className: "h-8 w-8 flex-shrink-0", children: jsx(Image$4, { src: providerDetails.logo, alt: labels.providerLogoAlt(providerDetails.name), className: cn('h-full w-full object-contain', {
271186
+ // `grayscale` only desaturates, so already-black
271187
+ // marks (OpenAI, xAI) looked untouched. Fading is
271188
+ // what actually reads as inactive at any hue.
271189
+ 'opacity-40 grayscale': isGrayed,
271190
+ }), width: 32, height: 32, loading: "lazy" }) }), jsx("span", { className: "text-sm font-medium text-gray-900", children: providerDetails.name })] }, model.id));
271191
+ }), localOnlyProviders.map((lp) => {
271192
+ const details = getLLMProviderDetails$1(lp.provider);
271193
+ return (jsxs("div", { "data-testid": "llm-provider-card", "data-provider": lp.provider, "data-disabled": "false", className: cn('flex cursor-pointer items-center gap-3 rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-shadow hover:shadow-md', { 'border-blue-500': lp.key === activeProviderKey }), onClick: () => {
271194
+ if (isDisabled || disabled)
271195
+ return;
271196
+ setSelectedLLMProvider({
271197
+ id: -1,
271198
+ name: lp.provider,
271199
+ logo: details.logo,
271200
+ description: null,
271201
+ chat_models: [],
271202
+ });
271203
+ }, children: [jsx("div", { className: "h-8 w-8 flex-shrink-0", children: jsx(Image$4, { src: details.logo, alt: labels.providerLogoAlt(details.name), className: "h-full w-full object-contain", width: 32, height: 32, loading: "lazy" }) }), jsx("span", { className: "text-sm font-medium text-gray-900", children: details.name })] }, `local-${lp.key}`));
271204
+ })] })) })), selectedLLMProvider && mentorSettings && (jsx(LLMProviderModal, { isOpen: true, onClose: () => setSelectedLLMProvider(null), onSelect: updateMentorLLM, llmProvider: selectedLLMProvider, isSelecting: isEditingMentor, mentorSettings: {
271205
+ llm_name: mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_name,
271206
+ llm_provider: mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_provider,
271207
+ }, llms: (llmProviders !== null && llmProviders !== void 0 ? llmProviders : []), getLLMProviderDetails: getLLMProviderDetails$1, labels: labels.providerModal }))] })] })] }));
270576
271208
  }
270577
271209
 
270578
271210
  function useTools({ labels }) {
@@ -337443,5 +338075,5 @@ var index_es = /*#__PURE__*/Object.freeze({
337443
338075
  vectorsRatio: vectorsRatio
337444
338076
  });
337445
338077
 
337446
- export { AGENT_ACCESS_TAB_LABELS, AGENT_ANALYTICS_TAB_LABELS, AGENT_API_TAB_LABELS, AGENT_DATASETS_TAB_LABELS, AGENT_DISCLAIMERS_TAB_LABELS, AGENT_EMBED_TAB_LABELS, AGENT_EVALUATION_TAB_LABELS, AGENT_GRADER_TAB_LABELS, AGENT_HISTORY_TAB_LABELS, AGENT_HUMAN_SUPPORT_TAB_LABELS, AGENT_LLM_TAB_LABELS, AGENT_LTI_TAB_LABELS, AGENT_MEMORY_TAB_LABELS, AGENT_PICKER_LABELS, AGENT_PRIVACY_TAB_LABELS, AGENT_PROMPTS_TAB_LABELS, AGENT_SAFETY_TAB_LABELS, AGENT_SCREENSHARE_TAB_LABELS, AGENT_SEARCH_LABELS, AGENT_SETTINGS_TAB_LABELS, AGENT_SKILLS_TAB_LABELS, AGENT_SPEND_CAPS_TAB_LABELS, AGENT_TASKS_TAB_LABELS, AGENT_TOOLS_TAB_LABELS, AGENT_VOICE_TAB_LABELS, Account, AgentAccessTab, AgentAnalyticsTab, AgentApiTab, AgentCard, AgentDatasetsTab, AgentDisclaimersTab, AgentEmbedTab, AgentEmptyState, AgentEvaluationTab, AgentGraderTab, AgentHistoryTab, AgentHumanSupportTab, AgentLLMTab, AgentLtiTab, AgentMemoryTab, AgentPrivacyTab, AgentPromptsTab, AgentSafetyTab, AgentScreenShareTab, AgentSearch, AgentSearchFilters, AgentSearchInput, AgentSettingsProvider, AgentSettingsTab, AgentSkillsTab, AgentSpendCapsTab, AgentTasksTab, AgentToolsTab, AgentVoiceTab, AppSidebar, CATALOGS, Chat, ClientErrorPage, ConversationStarters$1 as ConversationStarters, CopyButton, CopyMentorModal, CourseBox, CourseContentLayout, CourseContentTabPage, CreatePathwayModal, CredentialDetailModal, CredentialMiniBox, DEFAULT_ANALYTICS_HUB_TABS, DEFAULT_LOCALE, DeleteMentorModal, EMPTY_PROGRAM_SETTINGS_FORM, EdxIframe, ErrorPage, GRADING_TOOL_NAME, GreetingMethod, McpTab, MediaBox, MentorSelectionGrid, NavBar, OrganizationTab, PLATFORM_SIDEBAR_FLYOUT_ITEM_COLOR, PLATFORM_SIDEBAR_FLYOUT_TITLE_COLOR, PLATFORM_SIDEBAR_NAV_ACTIVE_BG_OPEN, PLATFORM_SIDEBAR_NAV_MUTED, PathwayDetailModal, PlatformAccountSheet, PlatformSidebar, PlatformSidebarCollapsedLabelFlyout, PlatformSidebarCollapsedNavFlyout, PlatformSidebarFooter, PlatformSidebarNavDivider, PlatformSidebarNavSection, PlatformSidebarSubNavItem, ProfileInfoCards, ProfileTabs, ProgramDetailModal, ProjectsSidebarDropdown, SPEND_CAPS_SUB_TABS, SUPPORTED_LOCALES, SidebarInset, SidebarProvider, SidebarTrigger, SpendCapUsage, SsoLogin, StarButton, UserProfileDropdown, UserProfileModal, WebContainersI18nProvider, formatDateString, getCatalog, getLLMProviderDetails, initializeLocalStorageWithObject, isValidLocale, resolveAgentPickerLabels, resolveAgentSearchLabels, resolveSettingsTabLabels, useAgentSearch, useAgentSearchWithPagination, useAgentSettings, useAgentStar, useGetChatDetails, usePlatformSidebarFooterActions, usePlatformSidebarNavCallback, useSidebar, useT, useTimeTracker, useWebContainersI18n, useWebContainersLocale };
338078
+ export { AGENT_ACCESS_TAB_LABELS, AGENT_ANALYTICS_TAB_LABELS, AGENT_API_TAB_LABELS, AGENT_DATASETS_TAB_LABELS, AGENT_DISCLAIMERS_TAB_LABELS, AGENT_EMBED_TAB_LABELS, AGENT_EVALUATION_TAB_LABELS, AGENT_GRADER_TAB_LABELS, AGENT_HISTORY_TAB_LABELS, AGENT_HUMAN_SUPPORT_TAB_LABELS, AGENT_LLM_TAB_LABELS, AGENT_LTI_TAB_LABELS, AGENT_MEMORY_TAB_LABELS, AGENT_PICKER_LABELS, AGENT_PRIVACY_TAB_LABELS, AGENT_PROMPTS_TAB_LABELS, AGENT_SAFETY_TAB_LABELS, AGENT_SCREENSHARE_TAB_LABELS, AGENT_SEARCH_LABELS, AGENT_SETTINGS_TAB_LABELS, AGENT_SKILLS_TAB_LABELS, AGENT_SPEND_CAPS_TAB_LABELS, AGENT_TASKS_TAB_LABELS, AGENT_TOOLS_TAB_LABELS, AGENT_VOICE_TAB_LABELS, Account, AgentAccessTab, AgentAnalyticsTab, AgentApiTab, AgentCard, AgentDatasetsTab, AgentDisclaimersTab, AgentEmbedTab, AgentEmptyState, AgentEvaluationTab, AgentGraderTab, AgentHistoryTab, AgentHumanSupportTab, AgentLLMTab, AgentLtiTab, AgentMemoryTab, AgentPrivacyTab, AgentPromptsTab, AgentSafetyTab, AgentScreenShareTab, AgentSearch, AgentSearchFilters, AgentSearchInput, AgentSettingsProvider, AgentSettingsTab, AgentSkillsTab, AgentSpendCapsTab, AgentTasksTab, AgentToolsTab, AgentVoiceTab, AppSidebar, CATALOGS, Chat, ClientErrorPage, ConversationStarters$1 as ConversationStarters, CopyButton, CopyMentorModal, CourseBox, CourseContentLayout, CourseContentTabPage, CreatePathwayModal, CredentialDetailModal, CredentialMiniBox, DEFAULT_ANALYTICS_HUB_TABS, DEFAULT_LOCALE, DeleteMentorModal, EMPTY_PROGRAM_SETTINGS_FORM, EdxIframe, ErrorPage, GRADING_TOOL_NAME, GreetingMethod, LLMProviderModal, LocalModelRow, McpTab, MediaBox, MentorSelectionGrid, NavBar, OrganizationTab, PLATFORM_SIDEBAR_FLYOUT_ITEM_COLOR, PLATFORM_SIDEBAR_FLYOUT_TITLE_COLOR, PLATFORM_SIDEBAR_NAV_ACTIVE_BG_OPEN, PLATFORM_SIDEBAR_NAV_MUTED, PathwayDetailModal, PlatformAccountSheet, PlatformSidebar, PlatformSidebarCollapsedLabelFlyout, PlatformSidebarCollapsedNavFlyout, PlatformSidebarFooter, PlatformSidebarNavDivider, PlatformSidebarNavSection, PlatformSidebarSubNavItem, ProfileInfoCards, ProfileTabs, ProgramDetailModal, ProjectsSidebarDropdown, SPEND_CAPS_SUB_TABS, SUPPORTED_LOCALES, SidebarInset, SidebarProvider, SidebarTrigger, SpendCapUsage, SsoLogin, StarButton, UserProfileDropdown, UserProfileModal, WebContainersI18nProvider, formatDateString, getCatalog, getLLMProviderDetails, getProviderName, initializeLocalStorageWithObject, isValidLocale, resolveAgentPickerLabels, resolveAgentSearchLabels, resolveLLMTabLabels, resolveSettingsTabLabels, useAgentSearch, useAgentSearchWithPagination, useAgentSettings, useAgentStar, useGetChatDetails, usePlatformSidebarFooterActions, usePlatformSidebarNavCallback, useSidebar, useT, useTimeTracker, useWebContainersI18n, useWebContainersLocale };
337447
338079
  //# sourceMappingURL=index.esm.js.map