diff --git a/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor b/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor index 6d9d7b89c..5ad7eb256 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 1c5df8b8e..e924b4fda 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 @@ private string GetMultiSelectionText(List? selectedValues) 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 0b2e5c0e5..cbc33d794 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor @@ -25,7 +25,7 @@ var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList(); if (availablePreviewFeatures.Count > 0) { - + } } diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs index 81c2b7e5c..70b6d24a3 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 @@ private IEnumerable> GetFilteredTranscriptionPro 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/Settings/ConfigMeta.cs b/app/MindWork AI Studio/Settings/ConfigMeta.cs index f8d50ecc5..6b81c3e8d 100644 --- a/app/MindWork AI Studio/Settings/ConfigMeta.cs +++ b/app/MindWork AI Studio/Settings/ConfigMeta.cs @@ -28,14 +28,14 @@ public ConfigMeta(Expression> configSelection, 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 ConfigMeta(Expression> configSelection, Expression - /// 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/ManagedConfiguration.Parsing.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs index 99b952038..e4cf5f2e0 100644 --- a/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs @@ -581,6 +581,90 @@ configuredLuaList.Type is LuaValueType.Table && 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 @@ private static bool HandleParsedValue( // 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 @@ private static bool HandleParsedValue( // // 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 5cc7a700b..363cccc1d 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 bool IsConfigurationLeftOver( 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 bool IsConfigurationLeftOver( 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 bool IsConfigurationLeftOver( 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 bool IsConfigurationLeftOver( 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 bool IsConfigurationLeftOver( 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 bool IsConfigurationLeftOver( 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 15e845c1f..b4007b9d3 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -121,8 +121,8 @@ private bool TryProcessConfiguration(bool dryRun, out string message) // 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); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 40bc37c08..be6de5783 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -219,6 +219,9 @@ plugin.Type is PluginType.CONFIGURATION && 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 227c02fe1..2865df75e 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.2.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.2.2.md @@ -11,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.