diff --git a/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor b/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor index 6d9d7b89..5ad7eb25 100644 --- a/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor +++ b/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor @@ -14,8 +14,22 @@ SelectedValuesChanged="@this.OptionChanged"> @foreach (var data in this.Data) { - - @data.Name + var isLockedValue = this.IsLockedValue(data.Value); + + @if (isLockedValue) + { + + @* MudTooltip.RootStyle is set as a workaround for issue -> https://github.com/MudBlazor/MudBlazor/issues/10882 *@ + + + + @data.Name + + } + else + { + @data.Name + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs b/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs index 1c5df8b8..e924b4fd 100644 --- a/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs @@ -27,6 +27,12 @@ public partial class ConfigurationMultiSelect : ConfigurationBaseCore /// [Parameter] public Action> SelectionUpdate { get; set; } = _ => { }; + + /// + /// Determines whether a specific item is locked by a configuration plugin. + /// + [Parameter] + public Func IsItemLocked { get; set; } = _ => false; #region Overrides of ConfigurationBase @@ -62,4 +68,12 @@ public partial class ConfigurationMultiSelect : ConfigurationBaseCore return string.Format(T("You have selected {0} preview features."), selectedValues.Count); } + + private bool IsLockedValue(TData value) => this.IsItemLocked(value); + + private string LockedTooltip() => + this.T( + "This feature is managed by your organization and has therefore been disabled.", + typeof(ConfigurationBase).Namespace, + nameof(ConfigurationBase)); } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor index a07fc65f..cbc33d79 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor @@ -25,11 +25,11 @@ var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList(); if (availablePreviewFeatures.Count > 0) { - + } } - + @if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager)) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs index 81c2b7e5..70b6d24a 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs @@ -27,7 +27,41 @@ public partial class SettingsPanelApp : SettingsPanelBase private void UpdatePreviewFeatures(PreviewVisibility previewVisibility) { this.SettingsManager.ConfigurationData.App.PreviewVisibility = previewVisibility; - this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = previewVisibility.FilterPreviewFeatures(this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures); + var filtered = previewVisibility.FilterPreviewFeatures(this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures); + filtered.UnionWith(this.GetPluginContributedPreviewFeatures()); + this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = filtered; + } + + private HashSet GetPluginContributedPreviewFeatures() + { + if (ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.HasPluginContribution) + return meta.PluginContribution.Where(x => !x.IsReleased()).ToHashSet(); + + return []; + } + + private bool IsPluginContributedPreviewFeature(PreviewFeatures feature) + { + if (feature.IsReleased()) + return false; + + if (!ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) || !meta.HasPluginContribution) + return false; + + return meta.PluginContribution.Contains(feature); + } + + private HashSet GetSelectedPreviewFeatures() + { + var enabled = this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures.Where(x => !x.IsReleased()).ToHashSet(); + enabled.UnionWith(this.GetPluginContributedPreviewFeatures()); + return enabled; + } + + private void UpdateEnabledPreviewFeatures(HashSet selectedFeatures) + { + selectedFeatures.UnionWith(this.GetPluginContributedPreviewFeatures()); + this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedFeatures; } private async Task UpdateLangBehaviour(LangBehavior behavior) diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 6fa2a8c9..5918b691 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -166,6 +166,11 @@ CONFIG["SETTINGS"] = {} -- Examples are PRE_WRITER_MODE_2024, PRE_RAG_2024, PRE_DOCUMENT_ANALYSIS_2025. -- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_DOCUMENT_ANALYSIS_2025" } +-- Configure the preselected provider. +-- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"]. +-- Please note: using an empty string ("") will lock the preselected provider selection, even though no valid preselected provider is found. +-- CONFIG["SETTINGS"]["DataApp.PreselectedProvider"] = "00000000-0000-0000-0000-000000000000" + -- Configure the preselected profile. -- It must be one of the profile IDs defined in CONFIG["PROFILES"]. -- Please note: using an empty string ("") will lock the preselected profile selection, even though no valid preselected profile is found. diff --git a/app/MindWork AI Studio/Settings/ConfigMeta.cs b/app/MindWork AI Studio/Settings/ConfigMeta.cs index f8d50ecc..6b81c3e8 100644 --- a/app/MindWork AI Studio/Settings/ConfigMeta.cs +++ b/app/MindWork AI Studio/Settings/ConfigMeta.cs @@ -28,14 +28,14 @@ public record ConfigMeta : ConfigMetaBase private Expression> PropertyExpression { get; } /// - /// Indicates whether the configuration is managed by a plugin and is therefore locked. + /// Indicates whether the configuration is locked by a configuration plugin. /// public bool IsLocked { get; private set; } /// - /// The ID of the plugin that manages this configuration. This is set when the configuration is locked. + /// The ID of the plugin that locked this configuration. /// - public Guid MangedByConfigPluginId { get; private set; } + public Guid LockedByConfigPluginId { get; private set; } /// /// The default value for the configuration property. This is used when resetting the property to its default state. @@ -43,30 +43,74 @@ public record ConfigMeta : ConfigMetaBase public required TValue Default { get; init; } /// - /// Locks the configuration state, indicating that it is managed by a specific plugin. + /// Indicates whether a plugin contribution is available. /// - /// The ID of the plugin that is managing this configuration. - public void LockManagedState(Guid pluginId) + public bool HasPluginContribution { get; private set; } + + /// + /// The additive value contribution provided by a configuration plugin. + /// + public TValue PluginContribution { get; private set; } = default!; + + /// + /// The ID of the plugin that provided the additive value contribution. + /// + public Guid PluginContributionByConfigPluginId { get; private set; } + + /// + /// Locks the configuration state, indicating that it is controlled by a specific plugin. + /// + /// The ID of the plugin that is locking this configuration. + public void LockConfiguration(Guid pluginId) { this.IsLocked = true; - this.MangedByConfigPluginId = pluginId; + this.LockedByConfigPluginId = pluginId; } /// - /// Resets the managed state of the configuration, allowing it to be modified again. + /// Resets the locked state of the configuration, allowing it to be modified again. /// This will also reset the property to its default value. /// - public void ResetManagedState() + public void ResetLockedConfiguration() { this.IsLocked = false; - this.MangedByConfigPluginId = Guid.Empty; + this.LockedByConfigPluginId = Guid.Empty; this.Reset(); } + + /// + /// Unlocks the configuration state without changing the current value. + /// + public void UnlockConfiguration() + { + this.IsLocked = false; + this.LockedByConfigPluginId = Guid.Empty; + } + + /// + /// Stores an additive plugin contribution. + /// + public void SetPluginContribution(TValue value, Guid pluginId) + { + this.PluginContribution = value; + this.PluginContributionByConfigPluginId = pluginId; + this.HasPluginContribution = true; + } + + /// + /// Clears the additive plugin contribution without changing the current value. + /// + public void ClearPluginContribution() + { + this.PluginContribution = default!; + this.PluginContributionByConfigPluginId = Guid.Empty; + this.HasPluginContribution = false; + } /// /// Resets the configuration property to its default value. /// - public void Reset() + private void Reset() { var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData); var memberExpression = this.PropertyExpression.GetMemberExpression(); diff --git a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs index 5671908f..a1def46f 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs @@ -65,7 +65,7 @@ public sealed class DataApp(Expression>? configSelection = n /// /// Should we preselect a provider for the entire app? /// - public string PreselectedProvider { get; set; } = string.Empty; + public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProvider, string.Empty); /// /// Should we preselect a profile for the entire app? diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs index 99b95203..e4cf5f2e 100644 --- a/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs @@ -581,6 +581,90 @@ public static partial class ManagedConfiguration return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); } + + /// + /// Attempts to process additive plugin contributions for enum set settings from a Lua table. + /// The contributed values are merged into the existing set, and the setting remains unlocked + /// so users can add additional values. + /// + /// The ID of the related configuration plugin. + /// The Lua table containing the settings to process. + /// The expression to select the configuration class. + /// The expression to select the property within the configuration class. + /// When true, the method will not apply any changes but only check if the configuration can be read. + /// The type of the configuration class. + /// The type of the property within the configuration class. It is also the type of the set + /// elements, which must be an enum. + /// True when the configuration was successfully processed, otherwise false. + public static bool TryProcessConfigurationWithPluginContribution( + Expression> configSelection, + Expression>> propertyExpression, + Guid configPluginId, + LuaTable settings, + bool dryRun) + where TValue : Enum + { + // + // Handle configured enum sets (additive merge) + // + + // Check if that configuration was registered: + if (!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + var successful = false; + var configuredValue = new HashSet(); + + // Step 1 -- try to read the Lua value (we expect a table) out of the Lua table: + if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) && + configuredLuaList.Type is LuaValueType.Table && + configuredLuaList.TryRead(out var valueTable)) + { + // Determine the length of the Lua table and prepare a set to hold the parsed values: + var len = valueTable.ArrayLength; + var set = new HashSet(len); + + // Iterate over each entry in the Lua table: + for (var index = 1; index <= len; index++) + { + // Retrieve the Lua value at the current index: + var value = valueTable[index]; + + // Step 2 -- try to read the Lua value as a string: + if (value.Type is LuaValueType.String && value.TryRead(out var configuredLuaValueText)) + { + // Step 3 -- try to parse the string as the target type: + if (Enum.TryParse(typeof(TValue), configuredLuaValueText, true, out var configuredEnum)) + set.Add((TValue)configuredEnum); + } + } + + configuredValue = set; + successful = true; + } + + if (dryRun) + return successful; + + if (successful) + { + var configInstance = configSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData); + var currentValue = propertyExpression.Compile().Invoke(configInstance); + var merged = new HashSet(currentValue); + merged.UnionWith(configuredValue); + configMeta.SetValue(merged); + configMeta.SetPluginContribution(new HashSet(configuredValue), configPluginId); + } + else if (configMeta.HasPluginContribution && configMeta.PluginContributionByConfigPluginId == configPluginId) + { + configMeta.ClearPluginContribution(); + } + + if (configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId) + configMeta.UnlockConfiguration(); + + return successful; + } /// /// Attempts to process the configuration settings from a Lua table for string set types. @@ -744,12 +828,12 @@ public static partial class ManagedConfiguration // Case: the setting was configured, and we could read the value successfully. // - // Set the configured value and lock the managed state: + // Set the configured value and lock the configuration: configMeta.SetValue(configuredValue); - configMeta.LockManagedState(configPluginId); + configMeta.LockConfiguration(configPluginId); break; - case false when configMeta.IsLocked && configMeta.MangedByConfigPluginId == configPluginId: + case false when configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId: // // Case: the setting was configured previously, but we could not read the value successfully. // This happens when the setting was removed from the configuration plugin. We handle that @@ -757,10 +841,10 @@ public static partial class ManagedConfiguration // // The other case, when the setting was locked and managed by a different configuration plugin, // is handled by the IsConfigurationLeftOver method, which checks if the configuration plugin - // is still available. If it is not available, it resets the managed state of the + // is still available. If it is not available, it resets the locked state of the // configuration setting, allowing it to be reconfigured by a different plugin or left unchanged. // - configMeta.ResetManagedState(); + configMeta.ResetLockedConfiguration(); break; case false: diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs index 5cc7a700..363cccc1 100644 --- a/app/MindWork AI Studio/Settings/ManagedConfiguration.cs +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs @@ -9,6 +9,7 @@ namespace AIStudio.Settings; public static partial class ManagedConfiguration { private static readonly ConcurrentDictionary METADATA = new(); + private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); /// /// Attempts to retrieve the configuration metadata for a given configuration selection and @@ -251,13 +252,13 @@ public static partial class ManagedConfiguration if (!TryGet(configSelection, propertyExpression, out var configMeta)) return false; - if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) + if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) return false; - var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); if (plugin is null) { - configMeta.ResetManagedState(); + configMeta.ResetLockedConfiguration(); return true; } @@ -272,13 +273,13 @@ public static partial class ManagedConfiguration if (!TryGet(configSelection, propertyExpression, out var configMeta)) return false; - if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) + if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) return false; - var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); if (plugin is null) { - configMeta.ResetManagedState(); + configMeta.ResetLockedConfiguration(); return true; } @@ -296,13 +297,13 @@ public static partial class ManagedConfiguration if (!TryGet(configSelection, propertyExpression, out var configMeta)) return false; - if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) + if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) return false; - var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); if (plugin is null) { - configMeta.ResetManagedState(); + configMeta.ResetLockedConfiguration(); return true; } @@ -319,13 +320,13 @@ public static partial class ManagedConfiguration if (!TryGet(configSelection, propertyExpression, out var configMeta)) return false; - if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) + if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) return false; - var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); if (plugin is null) { - configMeta.ResetManagedState(); + configMeta.ResetLockedConfiguration(); return true; } @@ -340,13 +341,38 @@ public static partial class ManagedConfiguration if (!TryGet(configSelection, propertyExpression, out var configMeta)) return false; - if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) + if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) return false; - var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); if (plugin is null) { - configMeta.ResetManagedState(); + configMeta.ResetLockedConfiguration(); + return true; + } + + return false; + } + + /// + /// Checks if a plugin contribution is left over from a configuration plugin that is no longer available. + /// If so, it clears the contribution and returns true. + /// + public static bool IsPluginContributionLeftOver( + Expression> configSelection, + Expression>> propertyExpression, + IEnumerable availablePlugins) + { + if (!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + if (!configMeta.HasPluginContribution || configMeta.PluginContributionByConfigPluginId == Guid.Empty) + return false; + + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.PluginContributionByConfigPluginId); + if (plugin is null) + { + configMeta.ClearPluginContribution(); return true; } @@ -361,13 +387,13 @@ public static partial class ManagedConfiguration if (!TryGet(configSelection, propertyExpression, out var configMeta)) return false; - if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) + if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) return false; - var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); if (plugin is null) { - configMeta.ResetManagedState(); + configMeta.ResetLockedConfiguration(); return true; } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index d28064e0..b4007b9d 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -121,8 +121,8 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT // Config: preview features visibility ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreviewVisibility, this.Id, settingsTable, dryRun); - // Config: enabled preview features - ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.EnabledPreviewFeatures, this.Id, settingsTable, dryRun); + // Config: enabled preview features (plugin contribution; users can enable additional features) + ManagedConfiguration.TryProcessConfigurationWithPluginContribution(x => x.App, x => x.EnabledPreviewFeatures, this.Id, settingsTable, dryRun); // Config: hide some assistants? ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.HiddenAssistants, this.Id, settingsTable, dryRun); @@ -148,6 +148,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT // Handle configured document analysis policies: PluginConfigurationObject.TryParse(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, x => x.NextDocumentAnalysisPolicyNum, mainTable, this.Id, ref this.configObjects, dryRun); + // Config: preselected provider? + ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun); + // Config: preselected profile? ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProfile, Guid.Empty, this.Id, settingsTable, dryRun); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 9fa39bde..be6de578 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -187,6 +187,10 @@ public static partial class PluginFactory if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, configObjectList)) wasConfigurationChanged = true; + // Check for a preselected provider: + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreselectedProvider, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + // Check for a preselected profile: if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreselectedProfile, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; @@ -215,6 +219,9 @@ public static partial class PluginFactory if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.EnabledPreviewFeatures, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; + if(ManagedConfiguration.IsPluginContributionLeftOver(x => x.App, x => x.EnabledPreviewFeatures, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + // Check for the transcription provider: if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UseTranscriptionProvider, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.2.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.2.2.md index b059b034..2865df75 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.2.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.2.2.md @@ -3,6 +3,7 @@ - Added an app setting to enable administration options for IT staff to configure and maintain organization-wide settings. - Added an option to export all provider types (LLMs, embeddings, transcriptions) so you can use them in a configuration plugin. You'll be asked if you want to export the related API key too. API keys will be encrypted in the export. This feature only shows up when administration options are enabled. - Added an option in the app settings to create an encryption secret, which is required to encrypt values (for example, API keys) in configuration plugins. This feature only shows up when administration options are enabled. +- Added the option to set a predefined provider for the entire app via configuration plugins. - Added support for using multiple enterprise configurations simultaneously. Enabled organizations to apply configurations based on employee affiliations, such as departments and working groups. See the enterprise configuration documentation for details. - Added the `DEPLOYED_USING_CONFIG_SERVER` field for configuration plugins so enterprise-managed plugins can be identified explicitly. Administrators should update their configuration plugins accordingly. See the enterprise configuration documentation for details. - Improved the enterprise configuration synchronization to be fail-safe on unstable or unavailable internet connections (for example, during business travel). If metadata checks or downloads fail, AI Studio keeps the current configuration plugins unchanged. @@ -10,6 +11,7 @@ - Improved the workspaces experience by using a different color for the delete button to avoid confusion. - Improved single-input dialogs (e.g., renaming chats) so pressing `Enter` confirmed immediately and the input field focused automatically when the dialog opened. - Improved the plugins page by adding an action to open the plugin source link. The action opens website URLs in an external browser, supports `mailto:` links for direct email composition. +- Improved the configuration plugins by making `EnabledPreviewFeatures` additive rather than exclusive. Users can now enable additional preview features without being restricted to those selected by the configuration plugin. - Improved the system language detection for locale values such as `C` and variants like `de_DE.UTF-8`, enabling AI Studio to apply the matching UI language more reliably. - Fixed an issue where leftover enterprise configuration plugins could remain active after organizational assignment changes during longer absences (for example, vacation), which could lead to configuration conflicts. - Fixed an issue where manually saving chats in workspace manual-storage mode could appear unreliable during response streaming. The save button is now disabled while streaming to prevent partial saves.