@iblai/iblai-js 2.7.0 → 2.7.2

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.
@@ -99502,18 +99502,47 @@ var llmTabLabels$3 = {
99502
99502
  title: "LLM Configuration",
99503
99503
  description: "Configure the language model settings for your agent."
99504
99504
  },
99505
+ 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
99506
  search: {
99506
99507
  placeholder: "Search Providers"
99507
99508
  },
99509
+ providerLogoAlt: "{providerName} logo",
99508
99510
  toasts: {
99509
99511
  updateSuccess: "LLM updated successfully",
99510
99512
  updateError: "Failed to update LLM"
99511
99513
  },
99512
99514
  providerModal: {
99513
99515
  title: "LLM Selection",
99514
- description: "Select one of the models provided by this provider.",
99516
+ description: "Select one of the agents provided by {providerName}",
99515
99517
  searchPlaceholder: "Search",
99516
- helpText: "Choose your preferred LLM from the available provider to tailor your experience."
99518
+ helpText: "Choose your preferred LLM from the available provider to tailor your experience.",
99519
+ providerIconAlt: "{providerName} icon",
99520
+ tooLargeTitle: "This model may be too large for your system",
99521
+ tooLargeDescription: "{modelName} needs about {modelSize}. It may run very slowly or fail to load on this machine. Download anyway?",
99522
+ cancel: "Cancel",
99523
+ downloadAnyway: "Download anyway",
99524
+ alreadyDownloadingTitle: "A model is already downloading",
99525
+ unnamedModel: "A model",
99526
+ 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.",
99527
+ gotIt: "Got it",
99528
+ announceDownloaded: "{modelName} downloaded",
99529
+ announceCancelled: "Download cancelled",
99530
+ announceFailed: "Download failed",
99531
+ announceStarted: "Started downloading {modelName}",
99532
+ localModel: {
99533
+ onDevice: "On-device",
99534
+ starting: "Starting…",
99535
+ cancel: "Cancel",
99536
+ inUse: "In use",
99537
+ downloadFailedRetry: "Download failed — retry",
99538
+ ariaDownload: "Download {modelName}, {modelSize}, on-device model",
99539
+ ariaStarting: "Starting download of {modelName}",
99540
+ ariaDownloading: "Cancel download of {modelName}, {percent} percent",
99541
+ ariaInstalled: "Use {modelName}, on-device model",
99542
+ ariaSelected: "Selected {modelName}, on-device model, in use",
99543
+ ariaError: "Retry download of {modelName}",
99544
+ ariaErrorWithReason: "Retry download of {modelName}, {reason}"
99545
+ }
99517
99546
  }
99518
99547
  };
99519
99548
  var llmsTabCreateLlmModal$3 = {
@@ -101834,9 +101863,6 @@ var tabsHumanSupportTab$3 = {
101834
101863
  ticketListLabel: "Support ticket list",
101835
101864
  ticketDetailLabel: "Support ticket details"
101836
101865
  };
101837
- var tabsLlmTab$3 = {
101838
- providerLogoAlt: "{name} logo"
101839
- };
101840
101866
  var tabsPoliciestab$3 = {
101841
101867
  searchPoliciesPlaceholder: "Search policies",
101842
101868
  searchPoliciesLabel: "Search policies",
@@ -103745,7 +103771,6 @@ var en = {
103745
103771
  tabsGroupstab: tabsGroupstab$3,
103746
103772
  tabsHistoryTab: tabsHistoryTab$3,
103747
103773
  tabsHumanSupportTab: tabsHumanSupportTab$3,
103748
- tabsLlmTab: tabsLlmTab$3,
103749
103774
  tabsPoliciestab: tabsPoliciestab$3,
103750
103775
  tabsPrivacyTab: tabsPrivacyTab$3,
103751
103776
  tabsPromptsTab: tabsPromptsTab$3,
@@ -106640,18 +106665,47 @@ var llmTabLabels$2 = {
106640
106665
  title: "Configuration LLM",
106641
106666
  description: "Configurez les paramètres du modèle de langage pour votre agent."
106642
106667
  },
106668
+ 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
106669
  search: {
106644
106670
  placeholder: "Rechercher des fournisseurs"
106645
106671
  },
106672
+ providerLogoAlt: "Logo {providerName}",
106646
106673
  toasts: {
106647
106674
  updateSuccess: "LLM mis à jour avec succès",
106648
106675
  updateError: "Échec de la mise à jour du LLM"
106649
106676
  },
106650
106677
  providerModal: {
106651
106678
  title: "Sélection du LLM",
106652
- description: "Sélectionnez l'un des modèles fournis par ce fournisseur.",
106679
+ description: "Sélectionnez l'un des agents proposés par {providerName}",
106653
106680
  searchPlaceholder: "Rechercher",
106654
- helpText: "Choisissez votre LLM préféré parmi les fournisseurs disponibles pour personnaliser votre expérience."
106681
+ helpText: "Choisissez votre LLM préféré parmi les fournisseurs disponibles pour personnaliser votre expérience.",
106682
+ providerIconAlt: "icône {providerName}",
106683
+ tooLargeTitle: "Ce modèle est peut-être trop volumineux pour votre système",
106684
+ 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 ?",
106685
+ cancel: "Annuler",
106686
+ downloadAnyway: "Télécharger quand même",
106687
+ alreadyDownloadingTitle: "Un modèle est déjà en cours de téléchargement",
106688
+ unnamedModel: "Un modèle",
106689
+ 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.",
106690
+ gotIt: "Compris",
106691
+ announceDownloaded: "{modelName} téléchargé",
106692
+ announceCancelled: "Téléchargement annulé",
106693
+ announceFailed: "Échec du téléchargement",
106694
+ announceStarted: "Téléchargement de {modelName} démarré",
106695
+ localModel: {
106696
+ onDevice: "Sur l'appareil",
106697
+ starting: "Démarrage…",
106698
+ cancel: "Annuler",
106699
+ inUse: "Utilisé",
106700
+ downloadFailedRetry: "Échec du téléchargement — réessayer",
106701
+ ariaDownload: "Télécharger {modelName}, {modelSize}, modèle sur l'appareil",
106702
+ ariaStarting: "Démarrage du téléchargement de {modelName}",
106703
+ ariaDownloading: "Annuler le téléchargement de {modelName}, {percent} pour cent",
106704
+ ariaInstalled: "Utiliser {modelName}, modèle sur l'appareil",
106705
+ ariaSelected: "{modelName} sélectionné, modèle sur l'appareil, utilisé",
106706
+ ariaError: "Réessayer le téléchargement de {modelName}",
106707
+ ariaErrorWithReason: "Réessayer le téléchargement de {modelName}, {reason}"
106708
+ }
106655
106709
  }
106656
106710
  };
106657
106711
  var llmsTabCreateLlmModal$2 = {
@@ -108972,9 +109026,6 @@ var tabsHumanSupportTab$2 = {
108972
109026
  ticketListLabel: "Liste des tickets de support",
108973
109027
  ticketDetailLabel: "Détails du ticket de support"
108974
109028
  };
108975
- var tabsLlmTab$2 = {
108976
- providerLogoAlt: "Logo {name}"
108977
- };
108978
109029
  var tabsPoliciestab$2 = {
108979
109030
  searchPoliciesPlaceholder: "Rechercher des politiques",
108980
109031
  searchPoliciesLabel: "Rechercher des politiques",
@@ -110718,7 +110769,6 @@ var fr = {
110718
110769
  tabsGroupstab: tabsGroupstab$2,
110719
110770
  tabsHistoryTab: tabsHistoryTab$2,
110720
110771
  tabsHumanSupportTab: tabsHumanSupportTab$2,
110721
- tabsLlmTab: tabsLlmTab$2,
110722
110772
  tabsPoliciestab: tabsPoliciestab$2,
110723
110773
  tabsPrivacyTab: tabsPrivacyTab$2,
110724
110774
  tabsPromptsTab: tabsPromptsTab$2,
@@ -113612,18 +113662,47 @@ var llmTabLabels$1 = {
113612
113662
  title: "Configuración de LLM",
113613
113663
  description: "Configure los ajustes del modelo de lenguaje para su agente."
113614
113664
  },
113665
+ 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
113666
  search: {
113616
113667
  placeholder: "Buscar proveedores"
113617
113668
  },
113669
+ providerLogoAlt: "Logo de {providerName}",
113618
113670
  toasts: {
113619
113671
  updateSuccess: "LLM actualizado correctamente",
113620
113672
  updateError: "Error al actualizar el LLM"
113621
113673
  },
113622
113674
  providerModal: {
113623
113675
  title: "Selección de LLM",
113624
- description: "Seleccione uno de los modelos proporcionados por este proveedor.",
113676
+ description: "Seleccione uno de los agentes proporcionados por {providerName}",
113625
113677
  searchPlaceholder: "Buscar",
113626
- helpText: "Elija su LLM preferido entre los proveedores disponibles para personalizar su experiencia."
113678
+ helpText: "Elija su LLM preferido entre los proveedores disponibles para personalizar su experiencia.",
113679
+ providerIconAlt: "ícono de {providerName}",
113680
+ tooLargeTitle: "Este modelo puede ser demasiado grande para su sistema",
113681
+ tooLargeDescription: "{modelName} necesita aproximadamente {modelSize}. Puede funcionar muy lentamente o no cargarse en este equipo. ¿Descargar de todos modos?",
113682
+ cancel: "Cancelar",
113683
+ downloadAnyway: "Descargar de todos modos",
113684
+ alreadyDownloadingTitle: "Ya se está descargando un modelo",
113685
+ unnamedModel: "Un modelo",
113686
+ 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.",
113687
+ gotIt: "Entendido",
113688
+ announceDownloaded: "{modelName} descargado",
113689
+ announceCancelled: "Descarga cancelada",
113690
+ announceFailed: "Error en la descarga",
113691
+ announceStarted: "Descarga de {modelName} iniciada",
113692
+ localModel: {
113693
+ onDevice: "En el dispositivo",
113694
+ starting: "Iniciando…",
113695
+ cancel: "Cancelar",
113696
+ inUse: "En uso",
113697
+ downloadFailedRetry: "Error en la descarga: reintentar",
113698
+ ariaDownload: "Descargar {modelName}, {modelSize}, modelo en el dispositivo",
113699
+ ariaStarting: "Iniciando la descarga de {modelName}",
113700
+ ariaDownloading: "Cancelar la descarga de {modelName}, {percent} por ciento",
113701
+ ariaInstalled: "Usar {modelName}, modelo en el dispositivo",
113702
+ ariaSelected: "{modelName} seleccionado, modelo en el dispositivo, en uso",
113703
+ ariaError: "Reintentar la descarga de {modelName}",
113704
+ ariaErrorWithReason: "Reintentar la descarga de {modelName}, {reason}"
113705
+ }
113627
113706
  }
113628
113707
  };
113629
113708
  var llmsTabCreateLlmModal$1 = {
@@ -115944,9 +116023,6 @@ var tabsHumanSupportTab$1 = {
115944
116023
  ticketListLabel: "Lista de tickets de soporte",
115945
116024
  ticketDetailLabel: "Detalles del ticket de soporte"
115946
116025
  };
115947
- var tabsLlmTab$1 = {
115948
- providerLogoAlt: "Logo de {name}"
115949
- };
115950
116026
  var tabsPoliciestab$1 = {
115951
116027
  searchPoliciesPlaceholder: "Buscar políticas",
115952
116028
  searchPoliciesLabel: "Buscar políticas",
@@ -117690,7 +117766,6 @@ var es = {
117690
117766
  tabsGroupstab: tabsGroupstab$1,
117691
117767
  tabsHistoryTab: tabsHistoryTab$1,
117692
117768
  tabsHumanSupportTab: tabsHumanSupportTab$1,
117693
- tabsLlmTab: tabsLlmTab$1,
117694
117769
  tabsPoliciestab: tabsPoliciestab$1,
117695
117770
  tabsPrivacyTab: tabsPrivacyTab$1,
117696
117771
  tabsPromptsTab: tabsPromptsTab$1,
@@ -120584,18 +120659,47 @@ var llmTabLabels = {
120584
120659
  title: "LLM 配置",
120585
120660
  description: "为您的智能体配置语言模型设置。"
120586
120661
  },
120662
+ infoBox: "这是您智能体的大脑——为每次回复提供动力的 AI 模型。更大的模型在处理复杂问题时推理能力更强;更轻量的模型则更快、成本更低。不确定?默认模型是可靠的全能之选。",
120587
120663
  search: {
120588
120664
  placeholder: "搜索提供商"
120589
120665
  },
120666
+ providerLogoAlt: "{providerName} 徽标",
120590
120667
  toasts: {
120591
120668
  updateSuccess: "LLM 更新成功",
120592
120669
  updateError: "LLM 更新失败"
120593
120670
  },
120594
120671
  providerModal: {
120595
120672
  title: "LLM 选择",
120596
- description: "从此提供商中选择一个模型。",
120673
+ description: "从 {providerName} 提供的代理中选择一个",
120597
120674
  searchPlaceholder: "搜索",
120598
- helpText: "从可用提供商中选择您偏好的 LLM,以定制您的使用体验。"
120675
+ helpText: "从可用提供商中选择您偏好的 LLM,以定制您的使用体验。",
120676
+ providerIconAlt: "{providerName} 图标",
120677
+ tooLargeTitle: "该模型可能对您的系统而言过大",
120678
+ tooLargeDescription: "{modelName} 约需 {modelSize}。它在此设备上可能运行非常缓慢或无法加载。仍要下载吗?",
120679
+ cancel: "取消",
120680
+ downloadAnyway: "仍要下载",
120681
+ alreadyDownloadingTitle: "已有模型正在下载",
120682
+ unnamedModel: "某个模型",
120683
+ alreadyDownloadingDescription: "{modelName} 正在下载。一次只能下载一个设备端模型——请等待其完成,或点击列表中的 {modelName} 以取消下载。",
120684
+ gotIt: "知道了",
120685
+ announceDownloaded: "{modelName} 已下载",
120686
+ announceCancelled: "下载已取消",
120687
+ announceFailed: "下载失败",
120688
+ announceStarted: "已开始下载 {modelName}",
120689
+ localModel: {
120690
+ onDevice: "设备端",
120691
+ starting: "正在启动…",
120692
+ cancel: "取消",
120693
+ inUse: "使用中",
120694
+ downloadFailedRetry: "下载失败——重试",
120695
+ ariaDownload: "下载 {modelName},{modelSize},设备端模型",
120696
+ ariaStarting: "正在开始下载 {modelName}",
120697
+ ariaDownloading: "取消下载 {modelName},{percent}%",
120698
+ ariaInstalled: "使用 {modelName},设备端模型",
120699
+ ariaSelected: "已选择 {modelName},设备端模型,使用中",
120700
+ ariaError: "重试下载 {modelName}",
120701
+ ariaErrorWithReason: "重试下载 {modelName},{reason}"
120702
+ }
120599
120703
  }
120600
120704
  };
120601
120705
  var llmsTabCreateLlmModal = {
@@ -122916,9 +123020,6 @@ var tabsHumanSupportTab = {
122916
123020
  ticketListLabel: "支持工单列表",
122917
123021
  ticketDetailLabel: "支持工单详情"
122918
123022
  };
122919
- var tabsLlmTab = {
122920
- providerLogoAlt: "{name} 标志"
122921
- };
122922
123023
  var tabsPoliciestab = {
122923
123024
  searchPoliciesPlaceholder: "搜索策略",
122924
123025
  searchPoliciesLabel: "搜索策略",
@@ -124662,7 +124763,6 @@ var zh = {
124662
124763
  tabsGroupstab: tabsGroupstab,
124663
124764
  tabsHistoryTab: tabsHistoryTab,
124664
124765
  tabsHumanSupportTab: tabsHumanSupportTab,
124665
- tabsLlmTab: tabsLlmTab,
124666
124766
  tabsPoliciestab: tabsPoliciestab,
124667
124767
  tabsPrivacyTab: tabsPrivacyTab,
124668
124768
  tabsPromptsTab: tabsPromptsTab,
@@ -130883,6 +130983,7 @@ Progress.displayName = Root$9.displayName;
130883
130983
 
130884
130984
  const LOCAL_LLM_ENABLED_KEY = 'ibl_local_llm_enabled';
130885
130985
  const LOCAL_LLM_MODEL_KEY = 'ibl_local_llm_model';
130986
+ const LOCAL_LLM_TOOL_SUPPORT_KEY = 'ibl_local_llm_tool_support';
130886
130987
  /**
130887
130988
  * Curated catalog of small, on-device-friendly models from the Ollama library
130888
130989
  * (https://ollama.com/library). The model manager itself (Ollama) is not a
@@ -130940,6 +131041,12 @@ const LOCAL_MODELS = [
130940
131041
  tool_support: true,
130941
131042
  },
130942
131043
  ];
131044
+ /**
131045
+ * Fraction of the machine's memory above which a model is treated as "too large"
131046
+ * and a confirmation is required before downloading. TESTING VALUE — set low (1%)
131047
+ * so the warning is easy to trigger; raise toward real capacity (e.g. 0.8) later.
131048
+ */
131049
+ const MODEL_SIZE_WARN_FRACTION = 0.01;
130943
131050
  /** Multipliers for the size units used in the catalog (binary / 1024-based). */
130944
131051
  const SIZE_UNIT_BYTES = {
130945
131052
  B: 1,
@@ -130961,6 +131068,31 @@ function parseModelSizeBytes(size) {
130961
131068
  return null;
130962
131069
  return value * ((_a = SIZE_UNIT_BYTES[match[2].toUpperCase()]) !== null && _a !== void 0 ? _a : 1);
130963
131070
  }
131071
+ /**
131072
+ * The memory a model can use on this machine: the larger of system RAM and GPU
131073
+ * VRAM (a model runs in one or the other). Returns 0 when memory is unknown.
131074
+ */
131075
+ function usableMemoryBytes(systemMemory) {
131076
+ if (!systemMemory)
131077
+ return 0;
131078
+ return Math.max(systemMemory.ram_total, systemMemory.vram_total);
131079
+ }
131080
+ /**
131081
+ * Whether `model` is large enough relative to this machine's memory to warrant a
131082
+ * "might not run" confirmation. False when memory or the model size is unknown,
131083
+ * so the download simply proceeds rather than warning on incomplete data.
131084
+ */
131085
+ function modelExceedsCapacity(model, systemMemory) {
131086
+ const capacity = usableMemoryBytes(systemMemory);
131087
+ const modelBytes = parseModelSizeBytes(model.size);
131088
+ if (modelBytes == null)
131089
+ return false;
131090
+ // Unknown memory (capacity 0) → don't warn; the download simply proceeds
131091
+ // rather than blocking on incomplete data.
131092
+ if (capacity <= 0)
131093
+ return false;
131094
+ return modelBytes > capacity * MODEL_SIZE_WARN_FRACTION;
131095
+ }
130964
131096
  /**
130965
131097
  * Default minimum model size (GB) that can use the Cowork
130966
131098
  * (GhostOS MCP/tools). Smaller models don't support tools/MCP well, so the
@@ -131008,6 +131140,33 @@ function getLocalLLMModel() {
131008
131140
  return null;
131009
131141
  return localStorage.getItem(LOCAL_LLM_MODEL_KEY);
131010
131142
  }
131143
+ /**
131144
+ * Set the model the user has chosen to chat with (Ollama pull id).
131145
+ */
131146
+ function setLocalLLMModel(modelId) {
131147
+ if (typeof window === 'undefined')
131148
+ return;
131149
+ localStorage.setItem(LOCAL_LLM_MODEL_KEY, modelId);
131150
+ }
131151
+ /**
131152
+ * Whether the model the user selected supports tools / function calling
131153
+ * (persisted alongside the model id). The host (e.g. ghost-os) reads this to
131154
+ * route chat to the MCP/tool proxy (:8000) vs plain Ollama (:11434). Defaults to
131155
+ * false when unknown. See {@link LocalModel.tool_support}.
131156
+ */
131157
+ function getLocalLLMToolSupport() {
131158
+ if (typeof window === 'undefined')
131159
+ return false;
131160
+ return localStorage.getItem(LOCAL_LLM_TOOL_SUPPORT_KEY) === 'true';
131161
+ }
131162
+ /**
131163
+ * Persist whether the selected model supports tools (set when a model is chosen).
131164
+ */
131165
+ function setLocalLLMToolSupport(toolSupport) {
131166
+ if (typeof window === 'undefined')
131167
+ return;
131168
+ localStorage.setItem(LOCAL_LLM_TOOL_SUPPORT_KEY, String(toolSupport));
131169
+ }
131011
131170
  function LocalLLMTab({ isAvailable, state, ollamaStatus, isUsingFoundry = false, foundryModels = [], selectedFoundryModel = null, foundryStatus, onInstallOllama, onStopManager, onInstallFoundry, onCheckStatus, onSelectFoundryModel, }) {
131012
131171
  var _a, _b, _c, _d, _e;
131013
131172
  const t = useT();
@@ -225279,62 +225438,215 @@ function CreateApiModal$1({ isOpen, onClose, tenantKey, username }) {
225279
225438
  : t('apiTabLabels.createModal.submit') })) }) })] })] }) }), apiKey && jsx(ApiKeyModal$1, { isOpen: true, onClose: () => setApiKey(null), apiKey: apiKey })] }));
225280
225439
  }
225281
225440
 
225441
+ /**
225442
+ * Small pure utilities colocated with the LLM tab.
225443
+ * Ported from standalone's `@/lib/utils` and `@/lib/error`.
225444
+ */
225445
+ // Canonical provider name for any label — a backend key ("azure_openai"), a
225446
+ // human label ("Microsoft"), or a local model's provider ("Meta"). Folds every
225447
+ // alias onto one identity so the same provider from different sources compares
225448
+ // equal. See getProviderName.
225449
+ const PROVIDER_NAME_BY_ALIAS = {
225450
+ microsoft: 'azure_openai',
225451
+ azureopenai: 'azure_openai',
225452
+ openai: 'openai',
225453
+ google: 'google',
225454
+ gemini: 'google',
225455
+ meta: 'llama',
225456
+ metallama: 'llama',
225457
+ llama: 'llama',
225458
+ mistral: 'mistral',
225459
+ mistralai: 'mistral',
225460
+ nvidia: 'nvidia',
225461
+ iblchatnvidia: 'nvidia',
225462
+ deepseek: 'deepseek',
225463
+ anthropic: 'anthropic',
225464
+ iblchatanthropic: 'anthropic',
225465
+ claude: 'anthropic',
225466
+ groq: 'groq',
225467
+ perplexity: 'perplexity',
225468
+ xai: 'xai',
225469
+ bedrock: 'bedrock',
225470
+ amazonbedrock: 'bedrock',
225471
+ amazon: 'bedrock',
225472
+ iblchatbedrock: 'bedrock',
225473
+ alibaba: 'alibaba',
225474
+ qwen: 'alibaba',
225475
+ ibm: 'ibm',
225476
+ granite: 'ibm',
225477
+ // ibl.ai's own hosted provider. The backend key is `iblai` (NameEnum.IBLAI);
225478
+ // the extra aliases cover the `IBLChat<Vendor>` house spelling and the
225479
+ // dotted/spaced brand forms, which all normalize to one of these.
225480
+ iblai: 'iblai',
225481
+ ibl: 'iblai',
225482
+ iblchatibl: 'iblai',
225483
+ iblchatiblai: 'iblai',
225484
+ };
225485
+ /**
225486
+ * Canonical provider name for any label, case- and punctuation-insensitive.
225487
+ * Folds backend keys and human/display labels onto one identity — e.g.
225488
+ * `getProviderName('Microsoft') === 'azure_openai'`,
225489
+ * `getProviderName('Meta') === 'llama'`, `getProviderName('Google') === 'google'`.
225490
+ * Unknown providers return their normalized (lowercased, alphanumeric-only) form.
225491
+ *
225492
+ * This is the single source of truth for provider identity, which matters as
225493
+ * soon as on-device models enter the picture: the local catalog labels its
225494
+ * providers the human way ("Meta", "Microsoft", "Alibaba") while the backend LLM
225495
+ * list uses wire keys ("llama", "azure_openai"). Comparing the two raw would
225496
+ * render a duplicate card for the same vendor.
225497
+ */
225498
+ function getProviderName(llmProvider) {
225499
+ var _a;
225500
+ const normalized = (llmProvider !== null && llmProvider !== void 0 ? llmProvider : '').toLowerCase().replace(/[^a-z0-9]/g, '');
225501
+ return (_a = PROVIDER_NAME_BY_ALIAS[normalized]) !== null && _a !== void 0 ? _a : normalized;
225502
+ }
225503
+ /**
225504
+ * Whether two Ollama model refs name the same model, comparing by base name and
225505
+ * ignoring a trailing `:tag` (so "llama3.2" ≡ "llama3.2:latest", and
225506
+ * "granite4.1:8b" ≡ "granite4.1"). The download state and Ollama's /api/tags may
225507
+ * carry a tag suffix the catalog id doesn't (or vice-versa); an exact `===` then
225508
+ * fails to recognize a row's own in-flight download — the row shows "Download"
225509
+ * with no progress while the pull runs. Base-name matching is safe here because
225510
+ * every catalog model has a unique base (llama3.2, qwen3, mistral, granite4.1,
225511
+ * gpt-oss, gemma4), so it can never light up the wrong row.
225512
+ */
225513
+ function sameModel(a, b) {
225514
+ const base = (ref) => (ref !== null && ref !== void 0 ? ref : '').toLowerCase().split(':')[0];
225515
+ const ba = base(a);
225516
+ return ba.length > 0 && ba === base(b);
225517
+ }
225518
+ /**
225519
+ * Whether a catalog model id (e.g. "granite4.1:8b") is among the installed
225520
+ * Ollama tags (e.g. "granite4.1:latest").
225521
+ *
225522
+ * Deliberately NOT the profile tab's `isModelInstalled`, which matches an exact
225523
+ * tag or the `<id>:` prefix: that is one-directional and misses a catalog id
225524
+ * that itself carries a tag ("granite4.1:8b" against an installed
225525
+ * "granite4.1:latest"). The picker needs the symmetric base-name comparison of
225526
+ * {@link sameModel} so a pulled model is recognized whichever side wears the tag.
225527
+ */
225528
+ function isLocalModelInstalled(modelId, installedTags) {
225529
+ return !!(installedTags === null || installedTags === void 0 ? void 0 : installedTags.some((tag) => sameModel(tag, modelId)));
225530
+ }
225531
+ function canSwitchProvider(providers, name) {
225532
+ const provider = providers.find((p) => p.name === name);
225533
+ return !!(provider === null || provider === void 0 ? void 0 : provider.chat_models) && provider.chat_models.length > 0;
225534
+ }
225535
+ /**
225536
+ * Whether we hold a usable LLM key for this provider — its own credentials, or
225537
+ * the tenant's main keys when it is allowed to borrow them. An *absent*
225538
+ * `main_has_credentials` means the backend did not report on the main keys and
225539
+ * is treated as available (matching standalone's `@/lib/utils` and
225540
+ * `claw-sandbox`'s copy); only an explicit `false` rules the main keys out.
225541
+ */
225542
+ function canSwitchLLm(llm) {
225543
+ if (llm === null || llm === void 0 ? void 0 : llm.has_credentials) {
225544
+ return true;
225545
+ }
225546
+ return ((llm === null || llm === void 0 ? void 0 : llm.can_use_main_keys) &&
225547
+ ((llm === null || llm === void 0 ? void 0 : llm.main_has_credentials) === true || (llm === null || llm === void 0 ? void 0 : llm.main_has_credentials) === undefined));
225548
+ }
225549
+ /**
225550
+ * Compares two raw provider names by the label the user actually sees on the
225551
+ * card, not by the backend key — the two diverge often enough to matter
225552
+ * (`bedrock` renders as "Amazon", `azure_openai` as "Microsoft", `iblai` as
225553
+ * "ibl.ai"). Comparison is case-insensitive (`sensitivity: 'base'`) so casing
225554
+ * never produces a surprising order. `getDetails` is the host-provided resolver
225555
+ * the grid renders with, so the sort always matches what is on screen; unknown
225556
+ * providers fall back to their raw name as the display name.
225557
+ */
225558
+ function compareLLMProvidersByDisplayName(a, b, getDetails) {
225559
+ return getDetails(a).name.localeCompare(getDetails(b).name, undefined, {
225560
+ sensitivity: 'base',
225561
+ });
225562
+ }
225563
+ /**
225564
+ * Whether the user can reach *any* model of this provider.
225565
+ *
225566
+ * This is exactly the provider-level half of the per-model `isDisabled` rule in
225567
+ * `llm-tab/llm-provider-modal.tsx`: a model row there is disabled when
225568
+ * `!canSwitchLLm(provider) || !canSwitchProvider(llms, provider.name)`, and both
225569
+ * of those depend only on the provider — so when either fails, *every* model row
225570
+ * of that provider is disabled. `canSwitchProvider` is just "has at least one
225571
+ * `chat_models` entry", which is checked inline here to keep the helper pure and
225572
+ * free of the surrounding provider list.
225573
+ *
225574
+ * `canSwitchLLm` can return `undefined` (its `can_use_main_keys` branch), hence
225575
+ * the explicit `Boolean(...)`.
225576
+ */
225577
+ function canAccessProvider(provider) {
225578
+ var _a, _b;
225579
+ 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;
225580
+ }
225581
+ /**
225582
+ * Orders LLM provider cards as two alphabetical groups: providers the user can
225583
+ * actually use (see {@link canAccessProvider}) first, then the ones they can't —
225584
+ * each group sorted alphabetically by display name.
225585
+ *
225586
+ * The grouping predicate is deliberately the same one the grid grays cards with,
225587
+ * so the two always line up: group 1 renders normally, group 2 renders grayed.
225588
+ * Grouping on credentials alone would let a provider that has a key but ships no
225589
+ * chat models sort into the "usable" group while rendering grayed.
225590
+ *
225591
+ * Pure and non-mutating: RTK Query results are frozen, so the input is copied
225592
+ * before sorting. `Array.prototype.sort` is stable, so providers that tie on
225593
+ * both group and display name keep their original relative order.
225594
+ */
225595
+ function sortLLMProvidersByCredentials(providers, getDetails) {
225596
+ return [...providers].sort((a, b) => {
225597
+ const aUsable = canAccessProvider(a);
225598
+ const bUsable = canAccessProvider(b);
225599
+ if (aUsable !== bUsable)
225600
+ return aUsable ? -1 : 1;
225601
+ return compareLLMProvidersByDisplayName(a.name, b.name, getDetails);
225602
+ });
225603
+ }
225604
+ function extractErrorMessage$1(error, fallback) {
225605
+ var _a, _b;
225606
+ const apiError = error;
225607
+ 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;
225608
+ }
225609
+
225610
+ // Logo + display name keyed by canonical provider name (see getProviderName).
225611
+ const PROVIDER_DETAILS_BY_NAME = {
225612
+ groq: { logo: '/llm-groq-provider.png', name: 'Groq' },
225613
+ nvidia: { logo: '/llm-nvidia-provider.webp', name: 'NVIDIA' },
225614
+ azure_openai: { logo: '/llm-microsoft-provider.png', name: 'Microsoft' },
225615
+ openai: { logo: '/llm-openai-provider-2.svg', name: 'OpenAI' },
225616
+ mistral: { logo: '/llm-mistral-provider.jpeg', name: 'Mistral' },
225617
+ google: { logo: '/llm-google-provider.svg', name: 'Google' },
225618
+ llama: { logo: '/llm-llama-provider.jpeg', name: 'Meta' },
225619
+ anthropic: { logo: '/llm-claude-provider.png', name: 'Anthropic' },
225620
+ perplexity: { logo: '/llm-perplexity-provider.webp', name: 'Perplexity' },
225621
+ deepseek: { logo: '/llm-deepseek-provider.png', name: 'DeepSeek' },
225622
+ xai: { logo: '/llm-xai-provider.jpg', name: 'xAI' },
225623
+ bedrock: { logo: '/llm-amazon-provider.png', name: 'Amazon' },
225624
+ alibaba: { logo: '/llm-alibaba-provider.png', name: 'Alibaba' },
225625
+ ibm: { logo: '/llm-ibm-provider.png', name: 'IBM' },
225626
+ iblai: { logo: '/llm-iblai-provider.png', name: 'ibl.ai' },
225627
+ };
225282
225628
  /**
225283
225629
  * Returns the canonical logo path and display name for an LLM provider.
225630
+ *
225631
+ * Resolution goes through {@link getProviderName}, so a backend wire key
225632
+ * (`azure_openai`), a human label (`Microsoft`) and an on-device catalog's
225633
+ * provider string (`Meta`, `Alibaba`) all land on the same entry — the provider
225634
+ * grid merges cloud and on-device providers into one list, so the two spellings
225635
+ * have to agree.
225636
+ *
225284
225637
  * `logo` is a static-root path (e.g. `/llm-amazon-provider.png`); the package
225285
225638
  * ships the canonical assets under `@iblai/web-containers/public/` — copy them
225286
225639
  * into your app's static-root (`public/` for Next.js) so the paths resolve.
225287
225640
  */
225288
225641
  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
- }
225642
+ var _a;
225643
+ const name = getProviderName(llmProvider);
225644
+ // OpenAI and Google use a model-specific logo when a concrete model is named.
225645
+ if (name === 'openai' && llmName)
225646
+ return { logo: '/llm-openai-provider.jpg', name: 'OpenAI' };
225647
+ if (name === 'google' && llmName)
225648
+ return { logo: '/llm-gemini-provider.png', name: 'Google' };
225649
+ return (_a = PROVIDER_DETAILS_BY_NAME[name]) !== null && _a !== void 0 ? _a : { logo: '/llm-generic-provider.png', name: llmProvider };
225338
225650
  }
225339
225651
 
225340
225652
  // Dynamic schema based on selected provider
@@ -262271,7 +262583,7 @@ const resourceTypes = [
262271
262583
  * rate limit.
262272
262584
  */
262273
262585
  const SPEND_CAP_EXCEEDED_ERROR_CODE = 'spend_cap_exceeded';
262274
- const extractErrorMessage$1 = (error, fallback) => {
262586
+ const extractErrorMessage = (error, fallback) => {
262275
262587
  var _a, _b, _c;
262276
262588
  const apiError = error;
262277
262589
  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 +262662,7 @@ function GithubFileUploadModal() {
262350
262662
  }
262351
262663
  catch (error) {
262352
262664
  console.error(JSON.stringify(error));
262353
- const errorMessage = extractErrorMessage$1(error, t('resourceModalGithubFileUploadModal.errorAddingDocument'));
262665
+ const errorMessage = extractErrorMessage(error, t('resourceModalGithubFileUploadModal.errorAddingDocument'));
262354
262666
  toast.error(errorMessage);
262355
262667
  console.error(JSON.stringify({ tenant: tenantKey, error }));
262356
262668
  }
@@ -262417,7 +262729,7 @@ function LocalFileUploadModal({ resource, maxFileSizeMB = DEFAULT_MAX_FILE_SIZE_
262417
262729
  }
262418
262730
  catch (error) {
262419
262731
  console.error(JSON.stringify(error));
262420
- const errorMessage = extractErrorMessage$1(error, t('resourceModalLocalFileUploadModal.errorAddingDocument'));
262732
+ const errorMessage = extractErrorMessage(error, t('resourceModalLocalFileUploadModal.errorAddingDocument'));
262421
262733
  toast.error(errorMessage);
262422
262734
  console.error(JSON.stringify({ tenant: tenantKey, error }));
262423
262735
  }
@@ -262520,7 +262832,7 @@ function UrlUploadModal({ resource }) {
262520
262832
  }
262521
262833
  catch (error) {
262522
262834
  console.error(JSON.stringify(error));
262523
- const errorMessage = extractErrorMessage$1(error, t('resourceModalUrlUploadModal.errorAddingTrainingDocument'));
262835
+ const errorMessage = extractErrorMessage(error, t('resourceModalUrlUploadModal.errorAddingTrainingDocument'));
262524
262836
  toast.error(errorMessage);
262525
262837
  console.error(JSON.stringify({ tenant: tenantKey, error }));
262526
262838
  }
@@ -262608,7 +262920,7 @@ function useWebsiteCrawlerResource(resource) {
262608
262920
  }
262609
262921
  catch (error) {
262610
262922
  console.error(JSON.stringify(error));
262611
- const errorMessage = extractErrorMessage$1(error, 'Error submitting web crawl data');
262923
+ const errorMessage = extractErrorMessage(error, 'Error submitting web crawl data');
262612
262924
  toast.error(errorMessage);
262613
262925
  console.error(JSON.stringify({ tenant: tenantKey, error }));
262614
262926
  }
@@ -262798,7 +263110,7 @@ const useGoogleDrivePicker = ({ tenantKey, mentorId }) => {
262798
263110
  }
262799
263111
  catch (error) {
262800
263112
  console.error(JSON.stringify(error));
262801
- const errorMessage = extractErrorMessage$1(error, 'Error adding training document');
263113
+ const errorMessage = extractErrorMessage(error, 'Error adding training document');
262802
263114
  toast.error(errorMessage);
262803
263115
  console.error(JSON.stringify({ tenant: tenantKey, error }));
262804
263116
  }
@@ -263103,7 +263415,7 @@ const useDropboxPicker = ({ tenantKey, mentorId, cancel = () => { }, linkType =
263103
263415
  }
263104
263416
  catch (error) {
263105
263417
  console.error(JSON.stringify(error));
263106
- const errorMessage = extractErrorMessage$1(error, 'Error adding training document');
263418
+ const errorMessage = extractErrorMessage(error, 'Error adding training document');
263107
263419
  toast.error(errorMessage);
263108
263420
  console.error(JSON.stringify({ tenant: tenantKey, error }));
263109
263421
  }
@@ -263328,7 +263640,7 @@ const useOneDrivePicker = ({ tenantKey, mentorId }) => {
263328
263640
  }
263329
263641
  catch (error) {
263330
263642
  console.error(JSON.stringify(error));
263331
- const errorMessage = extractErrorMessage$1(error, 'Error adding training document');
263643
+ const errorMessage = extractErrorMessage(error, 'Error adding training document');
263332
263644
  toast.error(errorMessage);
263333
263645
  console.error(JSON.stringify({ tenant: tenantKey, error }));
263334
263646
  }
@@ -270320,86 +270632,51 @@ var copyMentorModal = /*#__PURE__*/Object.freeze({
270320
270632
  });
270321
270633
 
270322
270634
  /**
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(...)`.
270635
+ * Window event any surface dispatches after changing the device-global
270636
+ * on-device selection — a model pick in the LLM tab's provider modal, or the
270637
+ * Local Models master toggle in Profile → Advanced. `storage` events only fire
270638
+ * cross-tab, so this covers the same-tab case (e.g. a nav-bar badge that has to
270639
+ * update the instant a model is picked in a modal above it).
270371
270640
  */
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
- }
270641
+ const LOCAL_LLM_CHANGED_EVENT = 'ibl:local-llm-changed';
270376
270642
  /**
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.
270643
+ * Reactive view of the on-device model selection (`ibl_local_llm_enabled` +
270644
+ * `ibl_local_llm_model` in localStorage). Reads on mount so it is
270645
+ * SSR/hydration-safe (nothing resolves until the client mounts) — and re-reads
270646
+ * on cross-tab `storage` events and same-tab {@link LOCAL_LLM_CHANGED_EVENT}
270647
+ * events.
270389
270648
  */
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);
270649
+ function useSelectedLocalModel() {
270650
+ const [sel, setSel] = useState({
270651
+ isLocal: false,
270652
+ model: null,
270653
+ modelId: null,
270397
270654
  });
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;
270655
+ useEffect(() => {
270656
+ const read = () => {
270657
+ var _a;
270658
+ const enabled = isLocalLLMEnabled();
270659
+ const id = getLocalLLMModel();
270660
+ const model = id ? ((_a = LOCAL_MODELS.find((m) => m.id === id)) !== null && _a !== void 0 ? _a : null) : null;
270661
+ // Self-heal the derived tool-support cache. Local chat routes on
270662
+ // `ibl_local_llm_tool_support`, but an older selection may have left it
270663
+ // stale/false — which makes streaming reject a genuinely tool-capable
270664
+ // model. The catalog is the source of truth; reconcile here. Write only on
270665
+ // a real mismatch so it can't loop.
270666
+ if (enabled && model && getLocalLLMToolSupport() !== model.tool_support) {
270667
+ setLocalLLMToolSupport(model.tool_support);
270668
+ }
270669
+ setSel({ isLocal: enabled && !!id, model, modelId: id });
270670
+ };
270671
+ read();
270672
+ window.addEventListener('storage', read);
270673
+ window.addEventListener(LOCAL_LLM_CHANGED_EVENT, read);
270674
+ return () => {
270675
+ window.removeEventListener('storage', read);
270676
+ window.removeEventListener(LOCAL_LLM_CHANGED_EVENT, read);
270677
+ };
270678
+ }, []);
270679
+ return sel;
270403
270680
  }
270404
270681
 
270405
270682
  const AGENT_LLM_TAB_LABELS = {
@@ -270407,18 +270684,47 @@ const AGENT_LLM_TAB_LABELS = {
270407
270684
  title: 'LLM Configuration',
270408
270685
  description: 'Configure the language model settings for your agent.',
270409
270686
  },
270687
+ 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
270688
  search: {
270411
270689
  placeholder: 'Search Providers',
270412
270690
  },
270691
+ providerLogoAlt: (providerName) => `${providerName} logo`,
270413
270692
  toasts: {
270414
270693
  updateSuccess: 'LLM updated successfully',
270415
270694
  updateError: 'Failed to update LLM',
270416
270695
  },
270417
270696
  providerModal: {
270418
270697
  title: 'LLM Selection',
270419
- description: 'Select one of the models provided by this provider.',
270698
+ description: (providerName) => `Select one of the agents provided by ${providerName}`,
270420
270699
  searchPlaceholder: 'Search',
270421
270700
  helpText: 'Choose your preferred LLM from the available provider to tailor your experience.',
270701
+ providerIconAlt: (providerName) => `${providerName} icon`,
270702
+ tooLargeTitle: 'This model may be too large for your system',
270703
+ tooLargeDescription: (modelName, modelSize) => `${modelName} needs about ${modelSize}. It may run very slowly or fail to load on this machine. Download anyway?`,
270704
+ cancel: 'Cancel',
270705
+ downloadAnyway: 'Download anyway',
270706
+ alreadyDownloadingTitle: 'A model is already downloading',
270707
+ unnamedModel: 'A model',
270708
+ 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.`,
270709
+ gotIt: 'Got it',
270710
+ announceDownloaded: (modelName) => `${modelName} downloaded`,
270711
+ announceCancelled: 'Download cancelled',
270712
+ announceFailed: 'Download failed',
270713
+ announceStarted: (modelName) => `Started downloading ${modelName}`,
270714
+ localModel: {
270715
+ onDevice: 'On-device',
270716
+ starting: 'Starting…',
270717
+ cancel: 'Cancel',
270718
+ inUse: 'In use',
270719
+ downloadFailedRetry: 'Download failed — retry',
270720
+ ariaDownload: (modelName, modelSize) => `Download ${modelName}, ${modelSize}, on-device model`,
270721
+ ariaStarting: (modelName) => `Starting download of ${modelName}`,
270722
+ ariaDownloading: (modelName, percent) => `Cancel download of ${modelName}, ${percent} percent`,
270723
+ ariaInstalled: (modelName) => `Use ${modelName}, on-device model`,
270724
+ ariaSelected: (modelName) => `Selected ${modelName}, on-device model, in use`,
270725
+ ariaError: (modelName) => `Retry download of ${modelName}`,
270726
+ ariaErrorWithReason: (modelName, reason) => `Retry download of ${modelName}, ${reason}`,
270727
+ },
270422
270728
  },
270423
270729
  };
270424
270730
  function buildLLMTabLabels(t) {
@@ -270427,18 +270733,47 @@ function buildLLMTabLabels(t) {
270427
270733
  title: t('llmTabLabels.header.title'),
270428
270734
  description: t('llmTabLabels.header.description'),
270429
270735
  },
270736
+ infoBox: t('llmTabLabels.infoBox'),
270430
270737
  search: {
270431
270738
  placeholder: t('llmTabLabels.search.placeholder'),
270432
270739
  },
270740
+ providerLogoAlt: (providerName) => t('llmTabLabels.providerLogoAlt', { providerName }),
270433
270741
  toasts: {
270434
270742
  updateSuccess: t('llmTabLabels.toasts.updateSuccess'),
270435
270743
  updateError: t('llmTabLabels.toasts.updateError'),
270436
270744
  },
270437
270745
  providerModal: {
270438
270746
  title: t('llmTabLabels.providerModal.title'),
270439
- description: t('llmTabLabels.providerModal.description'),
270747
+ description: (providerName) => t('llmTabLabels.providerModal.description', { providerName }),
270440
270748
  searchPlaceholder: t('llmTabLabels.providerModal.searchPlaceholder'),
270441
270749
  helpText: t('llmTabLabels.providerModal.helpText'),
270750
+ providerIconAlt: (providerName) => t('llmTabLabels.providerModal.providerIconAlt', { providerName }),
270751
+ tooLargeTitle: t('llmTabLabels.providerModal.tooLargeTitle'),
270752
+ tooLargeDescription: (modelName, modelSize) => t('llmTabLabels.providerModal.tooLargeDescription', { modelName, modelSize }),
270753
+ cancel: t('llmTabLabels.providerModal.cancel'),
270754
+ downloadAnyway: t('llmTabLabels.providerModal.downloadAnyway'),
270755
+ alreadyDownloadingTitle: t('llmTabLabels.providerModal.alreadyDownloadingTitle'),
270756
+ unnamedModel: t('llmTabLabels.providerModal.unnamedModel'),
270757
+ alreadyDownloadingDescription: (modelName) => t('llmTabLabels.providerModal.alreadyDownloadingDescription', { modelName }),
270758
+ gotIt: t('llmTabLabels.providerModal.gotIt'),
270759
+ announceDownloaded: (modelName) => t('llmTabLabels.providerModal.announceDownloaded', { modelName }),
270760
+ announceCancelled: t('llmTabLabels.providerModal.announceCancelled'),
270761
+ announceFailed: t('llmTabLabels.providerModal.announceFailed'),
270762
+ announceStarted: (modelName) => t('llmTabLabels.providerModal.announceStarted', { modelName }),
270763
+ localModel: {
270764
+ onDevice: t('llmTabLabels.providerModal.localModel.onDevice'),
270765
+ starting: t('llmTabLabels.providerModal.localModel.starting'),
270766
+ cancel: t('llmTabLabels.providerModal.localModel.cancel'),
270767
+ inUse: t('llmTabLabels.providerModal.localModel.inUse'),
270768
+ downloadFailedRetry: t('llmTabLabels.providerModal.localModel.downloadFailedRetry'),
270769
+ ariaDownload: (modelName, modelSize) => t('llmTabLabels.providerModal.localModel.ariaDownload', { modelName, modelSize }),
270770
+ ariaStarting: (modelName) => t('llmTabLabels.providerModal.localModel.ariaStarting', { modelName }),
270771
+ ariaDownloading: (modelName, percent) => t('llmTabLabels.providerModal.localModel.ariaDownloading', { modelName, percent }),
270772
+ ariaInstalled: (modelName) => t('llmTabLabels.providerModal.localModel.ariaInstalled', { modelName }),
270773
+ ariaSelected: (modelName) => t('llmTabLabels.providerModal.localModel.ariaSelected', { modelName }),
270774
+ ariaError: (modelName) => t('llmTabLabels.providerModal.localModel.ariaError', { modelName }),
270775
+ ariaErrorWithReason: (modelName, reason) => t('llmTabLabels.providerModal.localModel.ariaErrorWithReason', { modelName, reason }),
270776
+ },
270442
270777
  },
270443
270778
  };
270444
270779
  }
@@ -270450,38 +270785,287 @@ function resolveLLMTabLabels(override, t) {
270450
270785
  return deepMerge(base, override);
270451
270786
  }
270452
270787
 
270453
- function LLMProviderModal({ isOpen, onClose, onSelect, llmProvider, isSelecting, mentorSettings, llms, getLLMProviderDetails, labels = AGENT_LLM_TAB_LABELS.providerModal, }) {
270788
+ /**
270789
+ * A local (on-device) model presented in the same list as cloud models. It uses
270790
+ * the same provider logo as the cloud rows; the model name gets its own line and
270791
+ * the on-device badge, size, and download/status affordance sit on a subtitle
270792
+ * line beneath it. One click toggles download ⇄ cancel; once installed a click
270793
+ * selects it.
270794
+ */
270795
+ function LocalModelRow({ name, size, logo, status, progress, disabled = false, disabledReason, errorMessage, onActivate, labels = AGENT_LLM_TAB_LABELS.providerModal.localModel, }) {
270796
+ const selected = status === 'selected';
270797
+ const downloading = status === 'downloading' || status === 'starting';
270798
+ // A resting row (nothing downloading) can still be disabled when another
270799
+ // model is pulling; a row that is itself downloading stays clickable (cancel).
270800
+ const isBusyDisabled = disabled && !downloading && !selected;
270801
+ const inert = isBusyDisabled || selected;
270802
+ const pct = Math.round(Math.max(0, Math.min(100, progress)));
270803
+ const ariaLabel = (() => {
270804
+ switch (status) {
270805
+ case 'not-installed':
270806
+ return labels.ariaDownload(name, size);
270807
+ case 'starting':
270808
+ return labels.ariaStarting(name);
270809
+ case 'downloading':
270810
+ return labels.ariaDownloading(name, pct);
270811
+ case 'installed':
270812
+ return labels.ariaInstalled(name);
270813
+ case 'selected':
270814
+ return labels.ariaSelected(name);
270815
+ case 'error':
270816
+ return errorMessage
270817
+ ? labels.ariaErrorWithReason(name, errorMessage)
270818
+ : labels.ariaError(name);
270819
+ }
270820
+ })();
270821
+ 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: () => {
270822
+ if (!inert)
270823
+ onActivate();
270824
+ }, className: cn(
270825
+ // `isolate` makes the button its own stacking context so the absolute
270826
+ // progress fill (z-0) layers above the button's background and below the
270827
+ // content (z-10) — without it the fill can render behind the background.
270828
+ 'group relative isolate flex items-center gap-3 overflow-hidden rounded-lg border p-4 text-left transition-colors', selected
270829
+ ? 'cursor-default border-blue-500 bg-blue-50'
270830
+ : isBusyDisabled
270831
+ ? 'cursor-not-allowed border-gray-200 opacity-60'
270832
+ : '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",
270833
+ // Minimum width so the bar is visible the moment a download starts
270834
+ // (progress 0 / "starting"), then tracks real progress.
270835
+ 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', {
270836
+ grayscale: isBusyDisabled,
270837
+ }), 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" })] }))] })] })] }));
270838
+ }
270839
+
270840
+ function LLMProviderModal({ isOpen, onClose, onSelect, llmProvider, isSelecting, mentorSettings, llms, getLLMProviderDetails: getLLMProviderDetails$1 = getLLMProviderDetails, labels = AGENT_LLM_TAB_LABELS.providerModal, }) {
270841
+ var _a, _b;
270454
270842
  const [searchQuery, setSearchQuery] = React__default.useState('');
270843
+ // On-device model download state (Tauri desktop only; `isAvailable` is false
270844
+ // on web, which hides every local row below).
270845
+ const { isAvailable, state: downloadState, ollamaStatus, systemMemory, startDownload, cancelDownload, } = useModelDownload();
270846
+ // Mirror the device-global local selection so picks re-render immediately.
270847
+ const [localSel, setLocalSel] = React__default.useState(() => ({
270848
+ enabled: isLocalLLMEnabled(),
270849
+ model: getLocalLLMModel(),
270850
+ }));
270851
+ // Model awaiting the "may be too large" confirmation before its download
270852
+ // starts; null when no confirmation is pending.
270853
+ const [pendingModel, setPendingModel] = React__default.useState(null);
270854
+ // Name of the model currently downloading, shown in an info dialog when the
270855
+ // user clicks a DIFFERENT model (one on-device download at a time). Null when
270856
+ // no such notice is showing.
270857
+ const [busyDownloadName, setBusyDownloadName] = React__default.useState(null);
270858
+ // The model THIS picker asked to download. Modal-local (not the shared, churny
270859
+ // download state) so it survives the hook's once-per-load reset / remounts /
270860
+ // reloads that wipe `downloadState.activeModel` — the pull's progress events
270861
+ // carry no model id, so once `activeModel` is lost nothing restores it and the
270862
+ // row can't recognize its own in-flight download (bar advances, row still says
270863
+ // "Download"). Cleared only on a terminal status, so the reset's transient
270864
+ // `idle` doesn't drop it mid-pull.
270865
+ const [requestedModel, setRequestedModel] = React__default.useState(null);
270866
+ React__default.useEffect(() => {
270867
+ if (downloadState.status === 'completed' ||
270868
+ downloadState.status === 'cancelled' ||
270869
+ downloadState.status === 'error') {
270870
+ setRequestedModel(null);
270871
+ }
270872
+ }, [downloadState.status]);
270873
+ // Which on-device model is downloading right now: prefer this picker's own
270874
+ // request, fall back to the shared state's id when present.
270875
+ const downloadingModel = requestedModel !== null && requestedModel !== void 0 ? requestedModel : downloadState.activeModel;
270876
+ // The last on-device model whose download reported `completed`. Held sticky so
270877
+ // its row shows "installed" straight through the gap between the terminal
270878
+ // `completed` event and the /api/tags refresh landing — otherwise the completed
270879
+ // handler's checkStatus churns downloadState.status (completed → checking →
270880
+ // idle) first and the row flickers back to a "Download" button.
270881
+ const [lastCompleted, setLastCompleted] = React__default.useState(null);
270882
+ React__default.useEffect(() => {
270883
+ if (downloadState.status === 'completed' && downloadingModel) {
270884
+ setLastCompleted(downloadingModel);
270885
+ }
270886
+ }, [downloadState.status, downloadingModel]);
270455
270887
  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()));
270888
+ var _a, _b;
270889
+ // Guard `chat_models` (not just `llmProvider`): a provider can come back from
270890
+ // the API without a models array, and an undefined `.filter` here throws in
270891
+ // render and unmounts the whole dialog. Coalesce so the `.map` consumer below
270892
+ // always gets an array.
270893
+ return ((_b = (_a = llmProvider === null || llmProvider === void 0 ? void 0 : llmProvider.chat_models) === null || _a === void 0 ? void 0 : _a.filter((llm) => {
270894
+ var _a;
270895
+ const query = searchQuery.toLowerCase();
270896
+ // Match either label: rows are shown by `display_name`, so searching
270897
+ // what you can read has to work, but the raw key stays searchable for
270898
+ // anyone who knows it (e.g. "gpt-4.1" or "amazon.nova").
270899
+ return (llm.llm_name.toLowerCase().includes(query) ||
270900
+ ((_a = llm.display_name) !== null && _a !== void 0 ? _a : '').toLowerCase().includes(query));
270901
+ })) !== null && _b !== void 0 ? _b : []);
270457
270902
  }, [searchQuery, llmProvider]);
270903
+ // Local models belonging to THIS provider (merged into the same list).
270904
+ const localModels = React__default.useMemo(() => {
270905
+ if (!isAvailable)
270906
+ return [];
270907
+ const key = getProviderName(llmProvider.name);
270908
+ return LOCAL_MODELS.filter((m) => getProviderName(m.provider) === key &&
270909
+ m.name.toLowerCase().includes(searchQuery.toLowerCase()));
270910
+ }, [isAvailable, llmProvider.name, searchQuery]);
270458
270911
  const switchLLMAllowed = canSwitchLLm(llmProvider);
270459
270912
  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, }) {
270913
+ // Only an actual pull counts as "busy". A status refresh ('checking') must NOT
270914
+ // otherwise a stale `activeModel` makes a row flash a progress bar every time
270915
+ // status is re-checked.
270916
+ const busyDownloading = downloadState.status === 'downloading';
270917
+ const statusFor = (m) => {
270918
+ if (localSel.enabled && localSel.model === m.id)
270919
+ return 'selected';
270920
+ const active = sameModel(downloadingModel, m.id);
270921
+ if (active && downloadState.status === 'error')
270922
+ return 'error';
270923
+ if (active && busyDownloading)
270924
+ return downloadState.progress > 0 ? 'downloading' : 'starting';
270925
+ // `lastCompleted` bridges the just-finished → tags-refreshed gap so the row
270926
+ // stays "installed" instead of flickering back to "Download".
270927
+ if (sameModel(lastCompleted, m.id) ||
270928
+ isLocalModelInstalled(m.id, ollamaStatus === null || ollamaStatus === void 0 ? void 0 : ollamaStatus.installed_models))
270929
+ return 'installed';
270930
+ return 'not-installed';
270931
+ };
270932
+ // A cloud model is "in use" when it's the mentor's selected LLM and local mode
270933
+ // is off.
270934
+ const isCloudActive = (llm) => (mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_name) === llm.llm_name &&
270935
+ (mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_provider) === llmProvider.name &&
270936
+ !localSel.enabled;
270937
+ // Order the WHOLE list — cloud + local — available (usable now) first,
270938
+ // unavailable last, with the in-use model leading. "Available" for a cloud
270939
+ // model means the provider is switchable (has credentials + models); for a
270940
+ // local model it's the install state. So an installed on-device model outranks
270941
+ // cloud models whose provider has no credentials. Ranked by state, not the
270942
+ // transient download status, so rows don't jump mid-pull; a stable sort keeps
270943
+ // the original (catalog) order within each rank.
270944
+ const cloudRank = (llm) => isCloudActive(llm) ? 0 : switchLLMAllowed && switchProviderAllowed ? 1 : 2;
270945
+ const localRank = (m) => {
270946
+ const s = statusFor(m);
270947
+ return s === 'selected' ? 0 : s === 'installed' ? 1 : 2;
270948
+ };
270949
+ const sortedModels = [
270950
+ ...filteredLLMs.map((llm) => ({ kind: 'cloud', llm, rank: cloudRank(llm) })),
270951
+ ...localModels.map((model) => ({ kind: 'local', model, rank: localRank(model) })),
270952
+ ].sort((a, b) => a.rank - b.rank);
270953
+ // Preserve the "model may be too large" prompt before a download starts. Opens
270954
+ // the confirmation AlertDialog below (layered on top of the picker), rather
270955
+ // than a toast. `modelExceedsCapacity` compares the model's size to this
270956
+ // machine's RAM/VRAM; when memory is unknown it returns false, so the download
270957
+ // simply proceeds rather than blocking on incomplete data.
270958
+ const startLocalDownload = (m) => {
270959
+ if (modelExceedsCapacity(m, systemMemory)) {
270960
+ setPendingModel(m);
270961
+ return;
270962
+ }
270963
+ setRequestedModel(m.id);
270964
+ startDownload(m.id);
270965
+ };
270966
+ const activateLocal = (m, status) => {
270967
+ var _a;
270968
+ switch (status) {
270969
+ case 'not-installed':
270970
+ case 'error':
270971
+ // Only one on-device model downloads at a time. If ANY pull is already
270972
+ // running, don't start another — starting concurrent pulls floods every
270973
+ // useModelDownload instance with progress events and can hang the app.
270974
+ // Block even when the running model is unknown (activeModel wiped); the
270975
+ // user cancels by clicking the downloading row itself. Selecting an
270976
+ // already-installed model is unaffected (it starts no pull).
270977
+ if (busyDownloading && !sameModel(downloadingModel, m.id)) {
270978
+ const dl = LOCAL_MODELS.find((x) => sameModel(x.id, downloadingModel));
270979
+ setBusyDownloadName((_a = dl === null || dl === void 0 ? void 0 : dl.name) !== null && _a !== void 0 ? _a : labels.unnamedModel);
270980
+ return;
270981
+ }
270982
+ startLocalDownload(m);
270983
+ break;
270984
+ case 'starting':
270985
+ case 'downloading':
270986
+ cancelDownload();
270987
+ break;
270988
+ case 'installed':
270989
+ // Use this on-device model — device-global, mutually exclusive with cloud.
270990
+ setLocalLLMModel(m.id);
270991
+ setLocalLLMEnabled(true);
270992
+ // Persist tool-calling support so local chat routes to the MCP/tool
270993
+ // bridge. Without this the flag stays at its default (false) and
270994
+ // streaming chat rejects the model with "tool_support=false" even though
270995
+ // the catalog marks it tool-capable.
270996
+ setLocalLLMToolSupport(m.tool_support);
270997
+ setLocalSel({ enabled: true, model: m.id });
270998
+ // Notify same-tab listeners (e.g. a nav-bar on-device badge).
270999
+ window.dispatchEvent(new Event(LOCAL_LLM_CHANGED_EVENT));
271000
+ break;
271001
+ }
271002
+ };
271003
+ const selectCloud = (llmName) => {
271004
+ // Picking a cloud model turns local mode off so routing uses the cloud LLM.
271005
+ if (localSel.enabled) {
271006
+ setLocalLLMEnabled(false);
271007
+ setLocalSel((prev) => ({ ...prev, enabled: false }));
271008
+ window.dispatchEvent(new Event(LOCAL_LLM_CHANGED_EVENT));
271009
+ }
271010
+ onSelect(llmProvider.name, llmName);
271011
+ };
271012
+ // Announce download lifecycle changes once (not every %) for screen readers.
271013
+ const activeLocal = LOCAL_MODELS.find((m) => sameModel(m.id, downloadingModel));
271014
+ const liveMessage = !activeLocal
271015
+ ? ''
271016
+ : downloadState.status === 'completed'
271017
+ ? labels.announceDownloaded(activeLocal.name)
271018
+ : downloadState.status === 'cancelled'
271019
+ ? labels.announceCancelled
271020
+ : downloadState.status === 'error'
271021
+ ? labels.announceFailed
271022
+ : downloadState.status === 'checking' ||
271023
+ (downloadState.status === 'downloading' && downloadState.progress === 0)
271024
+ ? labels.announceStarted(activeLocal.name)
271025
+ : '';
271026
+ 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) => {
271027
+ if (item.kind === 'cloud') {
271028
+ const llm = item.llm;
271029
+ const isActive = isCloudActive(llm);
271030
+ const providerDetails = getLLMProviderDetails$1(llmProvider.name, llm.llm_name);
271031
+ const isDisabled = !switchLLMAllowed || isSelecting || isActive || !switchProviderAllowed;
271032
+ return (jsxs("button", { "data-model": llm.llm_name, disabled: isDisabled, onClick: () => {
271033
+ selectCloud(llm.llm_name);
271034
+ }, className: cn('flex cursor-pointer items-center gap-3 rounded-lg border border-gray-200 p-4 transition-colors', {
271035
+ // Matches the provider grid's inactive treatment: tinted
271036
+ // card, faded logo, full-strength label.
271037
+ 'cursor-not-allowed border-gray-100 bg-gray-50': isDisabled && !isActive,
271038
+ 'hover:border-blue-500 hover:bg-blue-50': !isDisabled,
271039
+ 'cursor-not-allowed border-blue-500 bg-blue-50': isActive,
271040
+ }), 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', {
271041
+ // `grayscale` alone is a no-op on already-black marks,
271042
+ // so fade them as well — same rule as the provider grid.
271043
+ 'opacity-40 grayscale': isDisabled && !isActive,
271044
+ }), 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}`));
271045
+ }
271046
+ const m = item.model;
271047
+ const status = statusFor(m);
271048
+ 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}`));
271049
+ }) }), jsx(AlertDialog, { open: !!pendingModel, onOpenChange: (open) => {
271050
+ if (!open)
271051
+ setPendingModel(null);
271052
+ }, 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: () => {
271053
+ if (pendingModel) {
271054
+ setRequestedModel(pendingModel.id);
271055
+ startDownload(pendingModel.id);
271056
+ }
271057
+ setPendingModel(null);
271058
+ }, children: labels.downloadAnyway })] })] }) }), jsx(AlertDialog, { open: !!busyDownloadName, onOpenChange: (open) => {
271059
+ if (!open)
271060
+ setBusyDownloadName(null);
271061
+ }, 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 }) })] }) })] }) }));
271062
+ }
271063
+
271064
+ function AgentLLMTab({ showConfigurationHeader = true, mentorId: mentorIdOverride, getLLMProviderDetails: getLLMProviderDetails$1 = getLLMProviderDetails, labels: labelsOverride, }) {
270482
271065
  var _a;
270483
271066
  const t = useT();
270484
- const { tenantKey, mentorId, username, enableRBAC } = useAgentSettings();
271067
+ const { tenantKey, mentorId: contextMentorId, username, enableRBAC } = useAgentSettings();
271068
+ const mentorId = mentorIdOverride || contextMentorId;
270485
271069
  const labels = React__default.useMemo(() => resolveLLMTabLabels(labelsOverride, t), [labelsOverride, t]);
270486
271070
  const [searchQuery, setSearchQuery] = React__default.useState('');
270487
271071
  const [selectedLLMProvider, setSelectedLLMProvider] = React__default.useState(null);
@@ -270499,13 +271083,44 @@ function AgentLLMTab({ showConfigurationHeader = true, getLLMProviderDetails, la
270499
271083
  }, {
270500
271084
  skip: !tenantKey || !username,
270501
271085
  });
271086
+ // Providers that only offer on-device (local) models — surfaced as grid cards
271087
+ // (Tauri desktop only) so their downloadable models are reachable even when the
271088
+ // backend LLM list has no matching cloud provider.
271089
+ const localOnlyProviders = React__default.useMemo(() => {
271090
+ if (!isTauriApp$1())
271091
+ return [];
271092
+ const cloudKeys = new Set((llmProviders !== null && llmProviders !== void 0 ? llmProviders : []).map((p) => getProviderName(p.name)));
271093
+ const seen = new Set();
271094
+ const result = [];
271095
+ for (const model of LOCAL_MODELS) {
271096
+ const key = getProviderName(model.provider);
271097
+ if (cloudKeys.has(key) || seen.has(key))
271098
+ continue;
271099
+ seen.add(key);
271100
+ result.push({ key, provider: model.provider });
271101
+ }
271102
+ // Alphabetical among themselves; the whole local-only block still renders
271103
+ // after the API providers.
271104
+ return result.sort((a, b) => compareLLMProvidersByDisplayName(a.provider, b.provider, getLLMProviderDetails$1));
271105
+ }, [llmProviders, getLLMProviderDetails$1]);
270502
271106
  // Two alphabetical groups: providers we have a usable LLM key for first, then
270503
271107
  // the ones we don't. Ordering is independent of the search filter below, so it
270504
271108
  // holds for whatever the filter leaves.
270505
- const sortedLLMProviders = React__default.useMemo(() => sortLLMProvidersByCredentials(llmProviders !== null && llmProviders !== void 0 ? llmProviders : [], getLLMProviderDetails), [llmProviders, getLLMProviderDetails]);
271109
+ const sortedLLMProviders = React__default.useMemo(() => sortLLMProvidersByCredentials(llmProviders !== null && llmProviders !== void 0 ? llmProviders : [], getLLMProviderDetails$1), [llmProviders, getLLMProviderDetails$1]);
270506
271110
  const [editMentor, { isLoading: isEditingMentor }] = useEditMentorMutation();
270507
271111
  const isDisabled = isMentorSettingsLoading || isLoadingLLMProviders || isEditingMentor;
270508
271112
  const isLoading = isMentorSettingsLoading || isLoadingLLMProviders;
271113
+ // The highlighted (selected) provider card must reflect the model chat
271114
+ // actually uses. When on-device mode is on, that's the selected local model's
271115
+ // provider (e.g. Llama 3.2 → Meta) — NOT the agent's stale cloud
271116
+ // `llm_provider` (which stays e.g. OpenAI). Falls back to the cloud provider
271117
+ // when local mode is off. Reactive so it updates the instant a model is picked.
271118
+ const selectedLocal = useSelectedLocalModel();
271119
+ const activeProviderKey = selectedLocal.isLocal && selectedLocal.model
271120
+ ? getProviderName(selectedLocal.model.provider)
271121
+ : (mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_provider)
271122
+ ? getProviderName(mentorSettings.llm_provider)
271123
+ : '';
270509
271124
  async function updateMentorLLM(llmProvider, llmName) {
270510
271125
  try {
270511
271126
  await editMentor({
@@ -270522,57 +271137,70 @@ function AgentLLMTab({ showConfigurationHeader = true, getLLMProviderDetails, la
270522
271137
  }
270523
271138
  catch (error) {
270524
271139
  console.error(JSON.stringify(error));
270525
- const errorMessage = extractErrorMessage(error, labels.toasts.updateError);
271140
+ const errorMessage = extractErrorMessage$1(error, labels.toasts.updateError);
270526
271141
  toast.error(errorMessage);
270527
271142
  console.error(JSON.stringify({ tenant: tenantKey, error }));
270528
271143
  }
270529
271144
  }
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: {
271145
+ 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
271146
  overflowY: 'auto',
270532
271147
  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 }))] }) })] }));
271148
+ }, 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",
271149
+ // @ts-ignore
271150
+ 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
271151
+ .filter((model) => model.name.toLowerCase().includes(searchQuery.toLowerCase()))
271152
+ .map((model) => {
271153
+ const providerDetails = getLLMProviderDetails$1(model.name);
271154
+ const isActive = !!activeProviderKey && getProviderName(model.name) === activeProviderKey;
271155
+ // The generated LLM response type doesn't declare the
271156
+ // credential flags the backend actually sends, so read them
271157
+ // through the access-shaped view the helper expects.
271158
+ const isGrayed = !canAccessProvider(model) && !isActive;
271159
+ 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', {
271160
+ 'border-blue-500': isActive,
271161
+ // Tint the whole card too: desaturating a logo that
271162
+ // is already black changes nothing, so the card
271163
+ // itself has to carry some of the inactive signal.
271164
+ 'border-gray-100 bg-gray-50': isGrayed,
271165
+ }), onClick: () => {
271166
+ if (isDisabled || disabled)
271167
+ return;
271168
+ setSelectedLLMProvider({
271169
+ id: model.id,
271170
+ name: model.name,
271171
+ logo: model.logo,
271172
+ description: model.description,
271173
+ chat_models: model.chat_models,
271174
+ // @ts-ignore - has_credentials property does not exist on LLMResponse type
271175
+ has_credentials: model === null || model === void 0 ? void 0 : model.has_credentials,
271176
+ // @ts-ignore - main_has_credentials property does not exist on LLMResponse type
271177
+ main_has_credentials: model === null || model === void 0 ? void 0 : model.main_has_credentials,
271178
+ // @ts-ignore - can_use_main_keys property does not exist on LLMResponse type
271179
+ can_use_main_keys: model === null || model === void 0 ? void 0 : model.can_use_main_keys,
271180
+ });
271181
+ }, 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', {
271182
+ // `grayscale` only desaturates, so already-black
271183
+ // marks (OpenAI, xAI) looked untouched. Fading is
271184
+ // what actually reads as inactive at any hue.
271185
+ 'opacity-40 grayscale': isGrayed,
271186
+ }), width: 32, height: 32, loading: "lazy" }) }), jsx("span", { className: "text-sm font-medium text-gray-900", children: providerDetails.name })] }, model.id));
271187
+ }), localOnlyProviders.map((lp) => {
271188
+ const details = getLLMProviderDetails$1(lp.provider);
271189
+ 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: () => {
271190
+ if (isDisabled || disabled)
271191
+ return;
271192
+ setSelectedLLMProvider({
271193
+ id: -1,
271194
+ name: lp.provider,
271195
+ logo: details.logo,
271196
+ description: null,
271197
+ chat_models: [],
271198
+ });
271199
+ }, 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}`));
271200
+ })] })) })), selectedLLMProvider && mentorSettings && (jsx(LLMProviderModal, { isOpen: true, onClose: () => setSelectedLLMProvider(null), onSelect: updateMentorLLM, llmProvider: selectedLLMProvider, isSelecting: isEditingMentor, mentorSettings: {
271201
+ llm_name: mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_name,
271202
+ llm_provider: mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_provider,
271203
+ }, llms: (llmProviders !== null && llmProviders !== void 0 ? llmProviders : []), getLLMProviderDetails: getLLMProviderDetails$1, labels: labels.providerModal }))] })] })] }));
270576
271204
  }
270577
271205
 
270578
271206
  function useTools({ labels }) {
@@ -337443,5 +338071,5 @@ var index_es = /*#__PURE__*/Object.freeze({
337443
338071
  vectorsRatio: vectorsRatio
337444
338072
  });
337445
338073
 
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 };
338074
+ 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
338075
  //# sourceMappingURL=index.esm.js.map