From 96f70fdf062d346576384f466ab167beab2fd264 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 15 Aug 2026 10:56:17 +0200 Subject: [PATCH] Added enterprise support to the embedding and transcription provider dialogs --- .../Dialogs/EmbeddingProviderDialog.razor | 17 +++++-- .../Dialogs/EmbeddingProviderDialog.razor.cs | 44 ++++++++++++++++--- .../Dialogs/TranscriptionProviderDialog.razor | 17 +++++-- .../TranscriptionProviderDialog.razor.cs | 44 ++++++++++++++++--- 4 files changed, 104 insertions(+), 18 deletions(-) diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor index 85e6e6ef..1abb680e 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor @@ -4,10 +4,16 @@ + @if (this.IsEnterpriseConfiguration) + { + + @T("This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.") + + } @* ReSharper disable once CSharpWarnings::CS8974 *@ - + @foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders))) { if (provider.ProvideEmbeddingAPI() || provider is LLMProviders.NONE) @@ -38,13 +44,14 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHostname" UserAttributes="@SPELLCHECK_ATTRIBUTES"/> } @if (this.DataLLMProvider.IsHostNeeded()) { - + @foreach (Host host in Enum.GetValues(typeof(Host))) { if (host.IsEmbeddingSupported()) @@ -69,6 +76,7 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.ValidateManuallyModel" UserAttributes="@SPELLCHECK_ATTRIBUTES" HelperText="@T("Currently, we cannot query the embedding models for the selected provider and/or host. Therefore, please enter the model name manually.")" @@ -76,7 +84,7 @@ } else { - + @T("Load") @if(this.availableModels.Count is 0) @@ -87,7 +95,7 @@ } else { - @@ -121,6 +129,7 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Lightbulb" AdornmentColor="Color.Info" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingInstanceName" UserAttributes="@SPELLCHECK_ATTRIBUTES" /> diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs index 4f7d39ab..074bc6fd 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs @@ -68,7 +68,14 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId /// [Parameter] public bool IsEditing { get; init; } - + + /// + /// Whether this embedding provider is managed by an enterprise configuration plugin. When true, + /// every field except the API key is locked, matching . + /// + [Parameter] + public bool IsEnterpriseConfiguration { get; set; } + [Inject] private RustService RustService { get; init; } = null!; @@ -85,6 +92,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId private bool dataIsValid; private string[] dataIssues = []; private string dataAPIKey = string.Empty; + private bool dataHadStoredAPIKeyOnLoad; private string dataManuallyModel = string.Empty; private string dataAPIKeyStorageIssue = string.Empty; private string dataEditingPreviousInstanceName = string.Empty; @@ -134,7 +142,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED, Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname, Host = this.DataHost, - IsEnterpriseConfiguration = false, + IsEnterpriseConfiguration = this.IsEnterpriseConfiguration, EnterpriseConfigurationPluginId = Guid.Empty, }; } @@ -174,11 +182,17 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId // Load the API key: var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED); if (requestedSecret.Success) + { this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption); + this.dataHadStoredAPIKeyOnLoad = !string.IsNullOrWhiteSpace(this.dataAPIKey); + } else { this.dataAPIKey = string.Empty; - if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED) + + // For an enterprise-managed provider, having no key yet is the expected first-run + // state, not a storage failure -- the user is just about to set their own key: + if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED && !this.IsEnterpriseConfiguration) { this.dataAPIKeyStorageIssue = string.Format(T("Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again."), requestedSecret.Issue); await this.form.Validate(); @@ -203,8 +217,12 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId #region Implementation of ISecretId - public string SecretId => this.DataLLMProvider.ToSecretId(); - + // Must mirror Settings.EmbeddingProvider.SecretId exactly: when editing an enterprise-managed + // provider, the key has to be stored under the same "ENT::"-prefixed keyring row that the + // app reads from at runtime (see BaseProvider.SecretId). Otherwise, a key entered here would + // silently end up in the wrong keyring row and never be found again. + public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.DataLLMProvider.ToSecretId()}" : this.DataLLMProvider.ToSecretId(); + public string SecretName => this.DataName; #endregion @@ -240,6 +258,22 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId await this.form.Validate(); return; } + + this.dataHadStoredAPIKeyOnLoad = true; + } + else if (this.dataHadStoredAPIKeyOnLoad) + { + // The user cleared a previously stored key. Without this, the old key would simply + // stay in the OS keyring untouched and keep being used: + var deleteResponse = await this.RustService.DeleteAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + if (!deleteResponse.Success) + { + this.dataAPIKeyStorageIssue = string.Format(T("Failed to remove the API key from the operating system. The message was: {0}. Please try again."), deleteResponse.Issue); + await this.form.Validate(); + return; + } + + this.dataHadStoredAPIKeyOnLoad = false; } this.MudDialog.Close(DialogResult.Ok(addedProviderSettings)); diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor index 78d2dea2..dd7fb33a 100644 --- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor @@ -4,10 +4,16 @@ + @if (this.IsEnterpriseConfiguration) + { + + @T("This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.") + + } @* ReSharper disable once CSharpWarnings::CS8974 *@ - + @foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders))) { if (provider.ProvideTranscriptionAPI() || provider is LLMProviders.NONE) @@ -38,13 +44,14 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHostname" UserAttributes="@SPELLCHECK_ATTRIBUTES"/> } @if (this.DataLLMProvider.IsHostNeeded()) { - + @foreach (Host host in Enum.GetValues(typeof(Host))) { if (host.IsTranscriptionSupported()) @@ -71,6 +78,7 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Dns" AdornmentColor="Color.Info" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.ValidateManuallyModel" UserAttributes="@SPELLCHECK_ATTRIBUTES" HelperText="@T("Currently, we cannot query the transcription models for the selected provider and/or host. Therefore, please enter the model name manually.")" @@ -78,7 +86,7 @@ } else { - + @T("Load") @if(this.availableModels.Count is 0) @@ -89,7 +97,7 @@ } else { - @@ -132,6 +140,7 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Lightbulb" AdornmentColor="Color.Info" + Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingInstanceName" UserAttributes="@SPELLCHECK_ATTRIBUTES" /> diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs index b75ff07d..00a9a080 100644 --- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs @@ -68,7 +68,14 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId /// [Parameter] public bool IsEditing { get; init; } - + + /// + /// Whether this transcription provider is managed by an enterprise configuration plugin. When + /// true, every field except the API key is locked, matching . + /// + [Parameter] + public bool IsEnterpriseConfiguration { get; set; } + [Inject] private RustService RustService { get; init; } = null!; @@ -85,6 +92,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId private bool dataIsValid; private string[] dataIssues = []; private string dataAPIKey = string.Empty; + private bool dataHadStoredAPIKeyOnLoad; private string dataManuallyModel = string.Empty; private string dataAPIKeyStorageIssue = string.Empty; private string dataEditingPreviousInstanceName = string.Empty; @@ -149,7 +157,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED, Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname, Host = this.DataHost, - IsEnterpriseConfiguration = false, + IsEnterpriseConfiguration = this.IsEnterpriseConfiguration, EnterpriseConfigurationPluginId = Guid.Empty, }; } @@ -189,11 +197,17 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId // Load the API key: var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED); if (requestedSecret.Success) + { this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption); + this.dataHadStoredAPIKeyOnLoad = !string.IsNullOrWhiteSpace(this.dataAPIKey); + } else { this.dataAPIKey = string.Empty; - if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED) + + // For an enterprise-managed provider, having no key yet is the expected first-run + // state, not a storage failure -- the user is just about to set their own key: + if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED && !this.IsEnterpriseConfiguration) { this.dataAPIKeyStorageIssue = string.Format(T("Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again."), requestedSecret.Issue); await this.form.Validate(); @@ -218,8 +232,12 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId #region Implementation of ISecretId - public string SecretId => this.DataLLMProvider.ToSecretId(); - + // Must mirror Settings.TranscriptionProvider.SecretId exactly: when editing an enterprise-managed + // provider, the key has to be stored under the same "ENT::"-prefixed keyring row that the + // app reads from at runtime (see BaseProvider.SecretId). Otherwise, a key entered here would + // silently end up in the wrong keyring row and never be found again. + public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.DataLLMProvider.ToSecretId()}" : this.DataLLMProvider.ToSecretId(); + public string SecretName => this.DataName; #endregion @@ -255,6 +273,22 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId await this.form.Validate(); return; } + + this.dataHadStoredAPIKeyOnLoad = true; + } + else if (this.dataHadStoredAPIKeyOnLoad) + { + // The user cleared a previously stored key. Without this, the old key would simply + // stay in the OS keyring untouched and keep being used: + var deleteResponse = await this.RustService.DeleteAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); + if (!deleteResponse.Success) + { + this.dataAPIKeyStorageIssue = string.Format(T("Failed to remove the API key from the operating system. The message was: {0}. Please try again."), deleteResponse.Issue); + await this.form.Validate(); + return; + } + + this.dataHadStoredAPIKeyOnLoad = false; } this.MudDialog.Close(DialogResult.Ok(addedProviderSettings));