From 38103121c9a1544dcbb823bc927521a4062206ce Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 19 Oct 2025 11:51:28 +0200 Subject: [PATCH] Extended managed configuration (#560) --- .../ConfigurationMultiSelect.razor.cs | 2 + .../Components/ConfigurationSelect.razor.cs | 1 + .../Settings/SettingsPanelApp.razor | 4 +- .../Plugins/configuration/plugin.lua | 12 + .../Settings/DataModel/DataApp.cs | 2 +- .../Settings/ManagedConfiguration.Parsing.cs | 736 ++++++++++++++++++ .../Settings/ManagedConfiguration.Register.cs | 274 +++++++ .../Settings/ManagedConfiguration.cs | 414 +++++++--- .../Tools/PluginSystem/PluginConfiguration.cs | 6 + .../PluginSystem/PluginFactory.Loading.cs | 19 +- .../wwwroot/changelog/v0.9.52.md | 1 + 11 files changed, 1351 insertions(+), 120 deletions(-) create mode 100644 app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs create mode 100644 app/MindWork AI Studio/Settings/ManagedConfiguration.Register.cs diff --git a/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs b/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs index f2e5ed51..1c5df8b8 100644 --- a/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs @@ -33,8 +33,10 @@ public partial class ConfigurationMultiSelect : ConfigurationBaseCore /// protected override bool Stretch => true; + /// protected override Variant Variant => Variant.Outlined; + /// protected override string Label => this.OptionDescription; #endregion diff --git a/app/MindWork AI Studio/Components/ConfigurationSelect.razor.cs b/app/MindWork AI Studio/Components/ConfigurationSelect.razor.cs index 14c73eea..e5780c9d 100644 --- a/app/MindWork AI Studio/Components/ConfigurationSelect.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationSelect.razor.cs @@ -36,6 +36,7 @@ public partial class ConfigurationSelect : ConfigurationBaseCore /// protected override string Label => this.OptionDescription; + /// protected override Variant Variant => Variant.Outlined; #endregion diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor index 14f187e9..cbc6e281 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor @@ -16,14 +16,14 @@ - + @if (this.SettingsManager.ConfigurationData.App.PreviewVisibility > PreviewVisibility.NONE) { var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList(); if (availablePreviewFeatures.Count > 0) { - + } } diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 7e24e999..cff866f7 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -74,6 +74,18 @@ CONFIG["SETTINGS"] = {} -- Allowed values are: true, false -- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false +-- Configure the visibility of preview features: +-- Allowed values are: NONE, RELEASE_CANDIDATE, BETA, ALPHA, PROTOTYPE, EXPERIMENTAL +-- Please note: +-- I: that this setting does not hide features that are already enabled. +-- II: lower levels include all features of the higher levels. E.g. BETA includes RELEASE_CANDIDATE features. +-- CONFIG["SETTINGS"]["DataApp.PreviewVisibility"] = "NONE" + +-- Configure the enabled preview features: +-- Allowed values are can be found in https://github.com/MindWorkAI/AI-Studio/app/MindWork%20AI%20Studio/Settings/DataModel/PreviewFeatures.cs +-- Examples are PRE_WRITER_MODE_2024, PRE_RAG_2024, PRE_DOCUMENT_ANALYSIS_2025. +-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_DOCUMENT_ANALYSIS_2025" } + -- Example chat templates for this configuration: CONFIG["CHAT_TEMPLATES"] = {} diff --git a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs index 67be8b9b..a09f8324 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs @@ -60,7 +60,7 @@ public sealed class DataApp(Expression>? configSelection = n /// /// The enabled preview features. /// - public HashSet EnabledPreviewFeatures { get; set; } = new(); + public HashSet EnabledPreviewFeatures { get; set; } = ManagedConfiguration.Register(configSelection, n => n.EnabledPreviewFeatures, []); /// /// Should we preselect a provider for the entire app? diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs new file mode 100644 index 00000000..cbe3b968 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs @@ -0,0 +1,736 @@ +using System.Globalization; +using System.Linq.Expressions; + +using AIStudio.Settings.DataModel; + +using Lua; + +namespace AIStudio.Settings; + +public static partial class ManagedConfiguration +{ + /// + /// Attempts to process the configuration settings from a Lua table for enum types. + /// + /// + /// When the configuration is successfully processed, it updates the configuration metadata with the configured value. + /// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID. + /// The setting's value is set to the configured value. + /// + /// 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. + /// An unused parameter to help with type inference for enum types. You might ignore it when calling the method. + /// The type of the configuration class. + /// The type of the property within the configuration class. + /// True when the configuration was successfully processed, otherwise false. + public static bool TryProcessConfiguration( + Expression> configSelection, + Expression> propertyExpression, + Guid configPluginId, + LuaTable settings, + bool dryRun, + TValue? _ = default) + where TValue : Enum + { + // + // Handle configured enum values + // + + // Check if that configuration was registered: + if(!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + var successful = false; + var configuredValue = configMeta.Default; + + // Step 1 -- try to read the Lua value out of the Lua table: + if(settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredEnumValue)) + { + // Step 2 -- try to read the Lua value as a string: + if(configuredEnumValue.TryRead(out var configuredEnumText)) + { + // Step 3 -- try to parse the string as the enum type: + if (Enum.TryParse(typeof(TValue), configuredEnumText, true, out var configuredEnum)) + { + configuredValue = (TValue)configuredEnum; + successful = true; + } + } + } + + if(dryRun) + return successful; + + return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + } + + /// + /// Attempts to process the configuration settings from a Lua table for ISpanParsable types. + /// + /// + /// When the configuration is successfully processed, it updates the configuration metadata with the configured value. + /// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID. + /// The setting's value is set to the configured value. + /// + /// 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. + /// An unused parameter to help with type inference. You might ignore it when calling the method. + /// The type of the configuration class. + /// The type of the property within the configuration class. + /// True when the configuration was successfully processed, otherwise false. + public static bool TryProcessConfiguration( + Expression> configSelection, + Expression> propertyExpression, + Guid configPluginId, + LuaTable settings, + bool dryRun, + ISpanParsable? _ = null) + where TValue : struct, ISpanParsable + { + // + // Handle configured ISpanParsable values + // + + // Check if that configuration was registered: + if(!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + var successful = false; + var configuredValue = configMeta.Default; + + // Step 1 -- try to read the Lua value out of the Lua table: + if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaValue)) + { + // Step 2a -- try to read the Lua value as a string: + if (configuredLuaValue.Type is LuaValueType.String && configuredLuaValue.TryRead(out var configuredLuaValueText)) + { + // Step 3 -- try to parse the string as the target type: + if (TValue.TryParse(configuredLuaValueText, CultureInfo.InvariantCulture, out var configuredParsedValue)) + { + configuredValue = configuredParsedValue; + successful = true; + } + } + + // Step 2b -- try to read the Lua value: + if(configuredLuaValue.TryRead(out var configuredLuaValueInstance)) + { + configuredValue = configuredLuaValueInstance; + successful = true; + } + } + + if(dryRun) + return successful; + + return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + } + + /// + /// Attempts to process the configuration settings from a Lua table for string values. + /// + /// + /// When the configuration is successfully processed, it updates the configuration metadata with the configured value. + /// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID. + /// The setting's value is set to the configured value. + /// + /// 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. + /// True when the configuration was successfully processed, otherwise false. + public static bool TryProcessConfiguration( + Expression> configSelection, + Expression> propertyExpression, + Guid configPluginId, + LuaTable settings, + bool dryRun) + { + // + // Handle configured string values + // + + // Check if that configuration was registered: + if(!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + var successful = false; + var configuredValue = configMeta.Default; + + // Step 1 -- try to read the Lua value out of the Lua table: + if(settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredTextValue)) + { + // Step 2 -- try to read the Lua value as a string: + if(configuredTextValue.TryRead(out var configuredText)) + { + configuredValue = configuredText; + successful = true; + } + } + + return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + } + + /// + /// Attempts to process the configuration settings from a Lua table for ISpanParsable list types. + /// + /// + /// When the configuration is successfully processed, it updates the configuration metadata with the configured value. + /// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID. + /// The setting's value is set to the configured value. + /// + /// 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. + /// An unused parameter to help with type inference for ISpanParsable types. You might ignore it when calling the method. + /// The type of the configuration class. + /// The type of the property within the configuration class. It is also the type of the list + /// elements, which must implement ISpanParsable. + /// True when the configuration was successfully processed, otherwise false. + + // ReSharper disable MethodOverloadWithOptionalParameter + public static bool TryProcessConfiguration( + Expression> configSelection, + Expression>> propertyExpression, + Guid configPluginId, + LuaTable settings, + bool dryRun, + ISpanParsable? _ = null) + where TValue : ISpanParsable + { + // + // Handle configured ISpanParsable lists + // + + // Check if that configuration was registered: + if (!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + var successful = false; + var configuredValue = configMeta.Default; + + // 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 list to hold the parsed values: + var len = valueTable.ArrayLength; + var list = new List(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 2a -- 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 (TValue.TryParse(configuredLuaValueText, CultureInfo.InvariantCulture, out var configuredParsedValue)) + list.Add(configuredParsedValue); + } + + // Step 2b -- try to read the Lua value: + if (value.TryRead(out var configuredLuaValueInstance)) + list.Add(configuredLuaValueInstance); + } + + configuredValue = list; + successful = true; + } + + if (dryRun) + return successful; + + return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + } + + // ReSharper restore MethodOverloadWithOptionalParameter + + /// + /// Attempts to process the configuration settings from a Lua table for enum list types. + /// + /// + /// When the configuration is successfully processed, it updates the configuration metadata with the configured value. + /// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID. + /// The setting's value is set to the configured value. + /// + /// 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 list + /// elements, which must be an enum. + /// True when the configuration was successfully processed, otherwise false. + public static bool TryProcessConfiguration( + Expression> configSelection, + Expression>> propertyExpression, + Guid configPluginId, + LuaTable settings, + bool dryRun) + where TValue : Enum + { + // + // Handle configured enum lists + // + + // Check if that configuration was registered: + if(!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + var successful = false; + var configuredValue = configMeta.Default; + + // 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 list to hold the parsed values: + var len = valueTable.ArrayLength; + var list = new List(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)) + list.Add((TValue)configuredEnum); + } + } + + configuredValue = list; + successful = true; + } + + if(dryRun) + return successful; + + return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + } + + /// + /// Attempts to process the configuration settings from a Lua table for string list types. + /// + /// + /// When the configuration is successfully processed, it updates the configuration metadata with the configured value. + /// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID. + /// The setting's value is set to the configured value. + /// + /// 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. + /// True when the configuration was successfully processed, otherwise false. + public static bool TryProcessConfiguration( + Expression> configSelection, + Expression>> propertyExpression, + Guid configPluginId, + LuaTable settings, + bool dryRun) + { + // + // Handle configured string lists + // + + // Check if that configuration was registered: + if(!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + var successful = false; + var configuredValue = configMeta.Default; + + // 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 list to hold the parsed values: + var len = valueTable.ArrayLength; + var list = new List(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)) + list.Add(configuredLuaValueText); + } + + configuredValue = list; + successful = true; + } + + if(dryRun) + return successful; + + return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + } + + /// + /// Attempts to process the configuration settings from a Lua table for ISpanParsable set types. + /// + /// + /// When the configuration is successfully processed, it updates the configuration metadata with the configured value. + /// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID. + /// The setting's value is set to the configured value. + /// + /// 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. + /// An unused parameter to help with type inference for ISpanParsable types. You might ignore it when calling the method. + /// 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 implement ISpanParsable. + /// True when the configuration was successfully processed, otherwise false. + + // ReSharper disable MethodOverloadWithOptionalParameter + public static bool TryProcessConfiguration( + Expression> configSelection, + Expression>> propertyExpression, + Guid configPluginId, + LuaTable settings, + bool dryRun, + ISpanParsable? _ = null) + where TValue : ISpanParsable + { + // + // Handle configured ISpanParsable sets + // + + // Check if that configuration was registered: + if (!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + var successful = false; + var configuredValue = configMeta.Default; + + // 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 2a -- 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 (TValue.TryParse(configuredLuaValueText, CultureInfo.InvariantCulture, out var configuredParsedValue)) + set.Add(configuredParsedValue); + } + + // Step 2b -- try to read the Lua value: + if (value.TryRead(out var configuredLuaValueInstance)) + set.Add(configuredLuaValueInstance); + } + + configuredValue = set; + successful = true; + } + + if (dryRun) + return successful; + + return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + } + + // ReSharper restore MethodOverloadWithOptionalParameter + + /// + /// Attempts to process the configuration settings from a Lua table for enum set types. + /// + /// + /// When the configuration is successfully processed, it updates the configuration metadata with the configured value. + /// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID. + /// The setting's value is set to the configured value. + /// + /// 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 TryProcessConfiguration( + Expression> configSelection, + Expression>> propertyExpression, + Guid configPluginId, + LuaTable settings, + bool dryRun) + where TValue : Enum + { + // + // Handle configured enum sets + // + + // Check if that configuration was registered: + if(!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + var successful = false; + var configuredValue = configMeta.Default; + + // 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; + + return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + } + + /// + /// Attempts to process the configuration settings from a Lua table for string set types. + /// + /// + /// When the configuration is successfully processed, it updates the configuration metadata with the configured value. + /// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID. + /// The setting's value is set to the configured value. + /// + /// 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. + /// True when the configuration was successfully processed, otherwise false. + public static bool TryProcessConfiguration( + Expression> configSelection, + Expression>> propertyExpression, + Guid configPluginId, + LuaTable settings, + bool dryRun) + { + // + // Handle configured string sets + // + + // Check if that configuration was registered: + if(!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + var successful = false; + var configuredValue = configMeta.Default; + + // 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)) + set.Add(configuredLuaValueText); + } + + configuredValue = set; + successful = true; + } + + if(dryRun) + return successful; + + return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + } + + /// + /// Attempts to process the configuration settings from a Lua table for string dictionary types. + /// + /// + /// When the configuration is successfully processed, it updates the configuration metadata with the configured value. + /// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID. + /// The setting's value is set to the configured value. + /// + /// 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. + /// True when the configuration was successfully processed, otherwise false. + public static bool TryProcessConfiguration( + Expression> configSelection, + Expression>> propertyExpression, + Guid configPluginId, + LuaTable settings, + bool dryRun) + { + // + // Handle configured string dictionaries (both keys and values are strings) + // + + // Check if that configuration was registered: + if(!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + var successful = false; + var configuredValue = configMeta.Default; + + // 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 dictionary to hold the parsed key-value pairs. + // Instead of using ArrayLength, we use HashMapCount to get the number of key-value pairs: + var len = valueTable.HashMapCount; + if (len > 0) + configuredValue.Clear(); + + // In order to iterate over all key-value pairs in the Lua table, we have to use TryGetNext. + // Thus, we initialize the previous key variable to Nil and keep calling TryGetNext until + // there are no more pairs: + var previousKey = LuaValue.Nil; + while(valueTable.TryGetNext(previousKey, out var pair)) + { + // Update the previous key for the next iteration: + previousKey = pair.Key; + + // Try to read both the key and the value as strings: + var hadKey = pair.Key.TryRead(out var key); + var hadValue = pair.Value.TryRead(out var value); + + // If both key and value were read successfully, add them to the dictionary: + if (hadKey && hadValue) + configuredValue[key] = value; + } + + successful = true; + } + + if(dryRun) + return successful; + + return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + } + + /// + /// Handles the parsed configuration value based on whether the parsing was successful and whether it's a dry run. + /// + /// The ID of the related configuration plugin. + /// When true, no changes will be applied. + /// Indicates whether the configuration value was successfully parsed. + /// The configuration metadata. + /// >The parsed configuration value. + /// The type of the configuration class. + /// The type of the configuration property value. + /// True when the configuration was successfully processed, otherwise false. + private static bool HandleParsedValue( + Guid configPluginId, + bool dryRun, + bool successful, + ConfigMeta configMeta, + TValue configuredValue) + { + if(dryRun) + return successful; + + switch (successful) + { + case true: + // + // Case: the setting was configured, and we could read the value successfully. + // + + // Set the configured value and lock the managed state: + configMeta.SetValue(configuredValue); + configMeta.LockManagedState(configPluginId); + break; + + case false when configMeta.IsLocked && configMeta.MangedByConfigPluginId == 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 + // case only when the setting was locked and managed by the same configuration plugin. + // + // 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 + // configuration setting, allowing it to be reconfigured by a different plugin or left unchanged. + // + configMeta.ResetManagedState(); + break; + + case false: + // + // Case: the setting was not configured, or we could not read the value successfully. + // We do not change the setting, and it remains at whatever value it had before. + // + break; + } + + return successful; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.Register.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.Register.cs new file mode 100644 index 00000000..fbc33767 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.Register.cs @@ -0,0 +1,274 @@ +using System.Linq.Expressions; + +using AIStudio.Settings.DataModel; + +namespace AIStudio.Settings; + +public static partial class ManagedConfiguration +{ + /// + /// Registers a configuration setting with a default value. + /// + /// + /// When called from the JSON deserializer, the configSelection parameter will be null. + /// In this case, the method will return the default value without registering the setting. + /// + /// The expression to select the configuration class. + /// The expression to select the property within the configuration class. + /// The default value to use when the setting is not configured. + /// The type of the configuration class. + /// The type of the property within the configuration class. + /// The default value. + public static TValue Register( + Expression>? configSelection, + Expression> propertyExpression, + TValue defaultValue) + where TValue : struct + { + // When called from the JSON deserializer by using the standard constructor, + // we ignore the register call and return the default value: + if (configSelection is null) + return defaultValue; + + var configPath = Path(configSelection, propertyExpression); + + // If the metadata already exists for this configuration path, we return the default value: + if (METADATA.ContainsKey(configPath)) + return defaultValue; + + // Not registered yet, so we register it now: + METADATA[configPath] = new ConfigMeta(configSelection, propertyExpression) + { + Default = defaultValue, + }; + + return defaultValue; + } + + /// + /// Registers a configuration setting with a default value. + /// + /// + /// When called from the JSON deserializer, the configSelection parameter will be null. + /// In this case, the method will return the default value without registering the setting. + /// + /// The expression to select the configuration class. + /// The expression to select the property within the configuration class. + /// The default value to use when the setting is not configured. + /// The type of the configuration class. + /// The default value. + public static string Register( + Expression>? configSelection, + Expression> propertyExpression, + string defaultValue) + { + // When called from the JSON deserializer by using the standard constructor, + // we ignore the register call and return the default value: + if(configSelection is null) + return defaultValue; + + var configPath = Path(configSelection, propertyExpression); + + // If the metadata already exists for this configuration path, we return the default value: + if (METADATA.ContainsKey(configPath)) + return defaultValue; + + // Not registered yet, so we register it now: + METADATA[configPath] = new ConfigMeta(configSelection, propertyExpression) + { + Default = defaultValue, + }; + + return defaultValue; + } + + /// + /// Registers a configuration setting with a default value for a IList of TValue. + /// + /// + /// If the configSelection parameter is null, the method returns a list containing the default value + /// without registering the configuration setting. + /// + /// The expression to select the configuration class. + /// The expression to select the property within the configuration class. + /// The default value to use when the setting is not configured. + /// The type of the configuration class. + /// The type of the elements in the list within the configuration class. + /// A list containing the default value. + public static List Register( + Expression>? configSelection, + Expression>> propertyExpression, + TValue defaultValue) + { + // When called from the JSON deserializer by using the standard constructor, + // we ignore the register call and return the default value: + if(configSelection is null) + return [defaultValue]; + + var configPath = Path(configSelection, propertyExpression); + + // If the metadata already exists for this configuration path, we return the default value: + if (METADATA.ContainsKey(configPath)) + return [defaultValue]; + + // Not registered yet, so we register it now: + METADATA[configPath] = new ConfigMeta>(configSelection, propertyExpression) + { + Default = [defaultValue], + }; + + return [defaultValue]; + } + + /// + /// Registers a configuration setting with multiple default values. + /// + /// + /// When called with a null configSelection parameter, the method ignores the register call and directly returns the default values. + /// If the configuration path already exists in the metadata, the method also returns the default values without registering new metadata. + /// + /// The expression used to select the configuration class. + /// The expression used to select the property within the configuration class. + /// The list of default values to be used when the configuration setting is not defined. + /// The type of the configuration class. + /// The type of the elements within the property list. + /// The list of default values. + public static List Register( + Expression>? configSelection, + Expression>> propertyExpression, + IList defaultValues) + { + // When called from the JSON deserializer by using the standard constructor, + // we ignore the register call and return the default value: + if(configSelection is null) + return [..defaultValues]; + + var configPath = Path(configSelection, propertyExpression); + + // If the metadata already exists for this configuration path, we return the default value: + if (METADATA.ContainsKey(configPath)) + return [..defaultValues]; + + // Not registered yet, so we register it now: + METADATA[configPath] = new ConfigMeta>(configSelection, propertyExpression) + { + Default = [..defaultValues], + }; + + return [..defaultValues]; + } + + /// + /// Registers a configuration setting with a default value. + /// + /// + /// When called with a null configSelection, this method returns the default value without registering the setting. + /// + /// The expression to select the configuration class. + /// The expression to select the set within the configuration class. + /// The default value to use when the setting is not configured. + /// The type of the configuration class. + /// The type of the values within the set. + /// A set containing the default value. + public static HashSet Register( + Expression>? configSelection, + Expression>> propertyExpression, + TValue defaultValue) + { + // When called from the JSON deserializer by using the standard constructor, + // we ignore the register call and return the default value: + if (configSelection is null) + return [defaultValue]; + + var configPath = Path(configSelection, propertyExpression); + + // If the metadata already exists for this configuration path, we return the default value: + if (METADATA.ContainsKey(configPath)) + return [defaultValue]; + + // Not registered yet, so we register it now: + METADATA[configPath] = new ConfigMeta>(configSelection, propertyExpression) + { + Default = new HashSet { defaultValue }, + }; + + return [defaultValue]; + } + + /// + /// Registers a configuration setting with a collection of default values. + /// + /// + /// When the method is invoked with a null configSelection, the configuration path + /// is ignored, and the specified default values are returned without registration. + /// + /// The expression that selects the configuration class from the root Data model. + /// The expression to select the property within the configuration class. + /// The default collection of values to use when the setting is not configured. + /// The type of the configuration class from which the property is selected. + /// The type of the elements in the collection associated with the configuration property. + /// A set containing the default values. + public static HashSet Register( + Expression>? configSelection, + Expression>> propertyExpression, + IList defaultValues) + { + // When called from the JSON deserializer by using the standard constructor, + // we ignore the register call and return the default value: + if (configSelection is null) + return [..defaultValues]; + + var configPath = Path(configSelection, propertyExpression); + + // If the metadata already exists for this configuration path, we return the default value: + if (METADATA.ContainsKey(configPath)) + return [..defaultValues]; + + // Not registered yet, so we register it now: + METADATA[configPath] = new ConfigMeta>(configSelection, propertyExpression) + { + Default = new HashSet(defaultValues), + }; + + return [..defaultValues]; + } + + /// + /// Registers a configuration setting with a default dictionary of string key-value pairs. + /// + /// + /// When the method is invoked with a null configSelection, the configuration path + /// is ignored, and the specified default values are returned without registration. + /// + /// The expression that selects the configuration class from the root Data model. + /// The expression to select the property within the configuration class. + /// The default dictionary of values to use when the setting is not configured. + /// The type of the configuration class from which the property is selected. + /// >The type of the dictionary within the configuration class. + /// A dictionary containing the default values. + public static TDict Register( + Expression>? configSelection, + Expression>> propertyExpression, + TDict defaultValues) + where TDict : IDictionary, new() + { + // When called from the JSON deserializer by using the standard constructor, + // we ignore the register call and return the default value: + if (configSelection is null) + return new(); + + var configPath = Path(configSelection, propertyExpression); + + // If the metadata already exists for this configuration path, we return the default value: + if (METADATA.ContainsKey(configPath)) + return defaultValues; + + // Not registered yet, so we register it now: + METADATA[configPath] = new ConfigMeta>(configSelection, propertyExpression) + { + Default = defaultValues, + }; + + return defaultValues; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs index 3767cd66..5cc7a700 100644 --- a/app/MindWork AI Studio/Settings/ManagedConfiguration.cs +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs @@ -4,62 +4,34 @@ using System.Linq.Expressions; using AIStudio.Settings.DataModel; using AIStudio.Tools.PluginSystem; -using Lua; - namespace AIStudio.Settings; -public static class ManagedConfiguration +public static partial class ManagedConfiguration { private static readonly ConcurrentDictionary METADATA = new(); - - /// - /// Registers a configuration setting with a default value. - /// - /// - /// When called from the JSON deserializer, the configSelection parameter will be null. - /// In this case, the method will return the default value without registering the setting. - /// - /// The expression to select the configuration class. - /// The expression to select the property within the configuration class. - /// The default value to use when the setting is not configured. - /// The type of the configuration class. - /// The type of the property within the configuration class. - /// The default value. - public static TValue Register(Expression>? configSelection, Expression> propertyExpression, TValue defaultValue) - { - // When called from the JSON deserializer by using the standard constructor, - // we ignore the register call and return the default value: - if(configSelection is null) - return defaultValue; - - var configPath = Path(configSelection, propertyExpression); - - // If the metadata already exists for this configuration path, we return the default value: - if (METADATA.ContainsKey(configPath)) - return defaultValue; - - METADATA[configPath] = new ConfigMeta(configSelection, propertyExpression) - { - Default = defaultValue, - }; - - return defaultValue; - } /// - /// Attempts to retrieve the configuration metadata for a given configuration selection and property expression. + /// Attempts to retrieve the configuration metadata for a given configuration selection and + /// property expression (enum-based). /// /// - /// When no configuration metadata is found, it returns a NoConfig instance with the default value set to default(TValue). - /// This allows the caller to handle the absence of configuration gracefully. In such cases, the return value of the method will be false. + /// When no configuration metadata is found, it returns a NoConfig instance with the default + /// value set to default(TValue). This allows the caller to handle the absence of configuration + /// gracefully. In such cases, the return value of the method will be false. /// /// The expression to select the configuration class. - /// The expression to select the property within the configuration class. - /// The output parameter that will hold the configuration metadata if found. + /// The expression to select the property within the + /// configuration class. + /// The output parameter that will hold the configuration metadata + /// if found. /// The type of the configuration class. /// The type of the property within the configuration class. /// True if the configuration metadata was found, otherwise false. - public static bool TryGet(Expression> configSelection, Expression> propertyExpression, out ConfigMeta configMeta) + public static bool TryGet( + Expression> configSelection, + Expression> propertyExpression, + out ConfigMeta configMeta) + where TValue : Enum { var configPath = Path(configSelection, propertyExpression); if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta meta) @@ -77,77 +49,185 @@ public static class ManagedConfiguration } /// - /// Attempts to process the configuration settings from a Lua table. + /// Attempts to retrieve the configuration metadata for a given configuration selection and + /// property expression (string-based). /// /// - /// When the configuration is successfully processed, it updates the configuration metadata with the configured value. - /// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID. - /// The setting's value is set to the configured value. + /// When no configuration metadata is found, it returns a NoConfig instance with the default + /// value set to default(TValue). This allows the caller to handle the absence of configuration + /// gracefully. In such cases, the return value of the method will be false. /// - /// 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 expression to select the property within the + /// configuration class. + /// The output parameter that will hold the configuration metadata + /// if found. /// The type of the configuration class. - /// The type of the property within the configuration class. - /// True when the configuration was successfully processed, otherwise false. - public static bool TryProcessConfiguration(Expression> configSelection, Expression> propertyExpression, Guid configPluginId, LuaTable settings, bool dryRun) + /// True if the configuration metadata was found, otherwise false. + public static bool TryGet( + Expression> configSelection, + Expression> propertyExpression, + out ConfigMeta configMeta) { - if(!TryGet(configSelection, propertyExpression, out var configMeta)) - return false; - - var (configuredValue, successful) = configMeta.Default switch + var configPath = Path(configSelection, propertyExpression); + if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta meta) { - Enum => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredEnumValue) && configuredEnumValue.TryRead(out var configuredEnumText) && Enum.TryParse(typeof(TValue), configuredEnumText, true, out var configuredEnum) ? ((TValue)configuredEnum, true) : (configMeta.Default, false), - Guid => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredGuidValue) && configuredGuidValue.TryRead(out var configuredGuidText) && Guid.TryParse(configuredGuidText, out var configuredGuid) ? ((TValue)(object)configuredGuid, true) : (configMeta.Default, false), - - string => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredTextValue) && configuredTextValue.TryRead(out var configuredText) ? ((TValue)(object)configuredText, true) : (configMeta.Default, false), - bool => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredBoolValue) && configuredBoolValue.TryRead(out var configuredState) ? ((TValue)(object)configuredState, true) : (configMeta.Default, false), - - int => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredIntValue) && configuredIntValue.TryRead(out var configuredInt) ? ((TValue)(object)configuredInt, true) : (configMeta.Default, false), - double => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredDoubleValue) && configuredDoubleValue.TryRead(out var configuredDouble) ? ((TValue)(object)configuredDouble, true) : (configMeta.Default, false), - float => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredFloatValue) && configuredFloatValue.TryRead(out var configuredFloat) ? ((TValue)(object)configuredFloat, true) : (configMeta.Default, false), - - _ => (configMeta.Default, false), - }; - - if(dryRun) - return successful; - - switch (successful) - { - case true: - // - // Case: the setting was configured, and we could read the value successfully. - // - configMeta.SetValue(configuredValue); - configMeta.LockManagedState(configPluginId); - break; - - case false when configMeta.IsLocked && configMeta.MangedByConfigPluginId == 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 - // case only when the setting was locked and managed by the same configuration plugin. - // - // 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 - // configuration setting, allowing it to be reconfigured by a different plugin or left unchanged. - // - configMeta.ResetManagedState(); - break; - - case false: - // - // Case: the setting was not configured, or we could not read the value successfully. - // We do not change the setting, and it remains at whatever value it had before. - // - break; + configMeta = meta; + return true; } - return successful; + configMeta = new NoConfig(configSelection, propertyExpression) + { + Default = string.Empty, + }; + return false; + } + + /// + /// Attempts to retrieve the configuration metadata for a given configuration selection and + /// property expression (ISpanParsable-based). + /// + /// + /// When no configuration metadata is found, it returns a NoConfig instance with the default + /// value set to default(TValue). This allows the caller to handle the absence of configuration + /// gracefully. In such cases, the return value of the method will be false. + /// + /// The expression to select the configuration class. + /// The expression to select the property within the + /// configuration class. + /// The output parameter that will hold the configuration metadata + /// if found. + /// An optional parameter to help with method overload resolution. + /// The type of the configuration class. + /// The type of the property within the configuration class. + /// True if the configuration metadata was found, otherwise false. + + // ReSharper disable MethodOverloadWithOptionalParameter + public static bool TryGet( + Expression> configSelection, + Expression> propertyExpression, + out ConfigMeta configMeta, + ISpanParsable? _ = null) + where TValue : struct, ISpanParsable + { + var configPath = Path(configSelection, propertyExpression); + if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta meta) + { + configMeta = meta; + return true; + } + + configMeta = new NoConfig(configSelection, propertyExpression) + { + Default = default!, + }; + return false; + } + + // ReSharper restore MethodOverloadWithOptionalParameter + + /// + /// Attempts to retrieve the configuration metadata for a list-based setting. + /// + /// + /// When no configuration metadata is found, it returns a NoConfig instance with the default + /// value set to an empty list. This allows the caller to handle the absence of configuration + /// gracefully. In such cases, the return value of the method will be false. + /// + /// The expression to select the configuration class. + /// The expression to select the property within the + /// configuration class. + /// The output parameter that will hold the configuration metadata + /// if found. + /// The type of the configuration class. + /// The type of the property within the configuration class. + /// True if the configuration metadata was found, otherwise false. + public static bool TryGet( + Expression> configSelection, + Expression>> propertyExpression, + out ConfigMeta> configMeta) + { + var configPath = Path(configSelection, propertyExpression); + if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta> meta) + { + configMeta = meta; + return true; + } + + configMeta = new NoConfig>(configSelection, propertyExpression) + { + Default = [], + }; + return false; + } + + /// + /// Attempts to retrieve the configuration metadata for a set-based setting. + /// + /// + /// When no configuration metadata is found, it returns a NoConfig instance with the default + /// value set to an empty set. This allows the caller to handle the absence of configuration + /// gracefully. In such cases, the return value of the method will be false. + /// + /// The expression to select the configuration class. + /// The expression to select the property within the + /// configuration class. + /// The output parameter that will hold the configuration metadata + /// if found. + /// The type of the configuration class. + /// The type of the property within the configuration class. + /// True if the configuration metadata was found, otherwise false. + public static bool TryGet( + Expression> configSelection, + Expression>> propertyExpression, + out ConfigMeta> configMeta) + { + var configPath = Path(configSelection, propertyExpression); + if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta> meta) + { + configMeta = meta; + return true; + } + + configMeta = new NoConfig>(configSelection, propertyExpression) + { + Default = new HashSet(), + }; + return false; + } + + /// + /// Attempts to retrieve the configuration metadata for a string dictionary-based setting. + /// + /// + /// When no configuration metadata is found, it returns a NoConfig instance with the default + /// value set to an empty dictionary. This allows the caller to handle the absence of configuration + /// gracefully. In such cases, the return value of the method will be false. + /// + /// The expression to select the configuration class. + /// The expression to select the property within the + /// configuration class. + /// The output parameter that will hold the configuration metadata + /// if found. + /// The type of the configuration class. + /// True if the configuration metadata was found, otherwise false. + public static bool TryGet( + Expression> configSelection, + Expression>> propertyExpression, + out ConfigMeta> configMeta) + { + var configPath = Path(configSelection, propertyExpression); + if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta> meta) + { + configMeta = meta; + return true; + } + + configMeta = new NoConfig>(configSelection, propertyExpression) + { + Default = new Dictionary(), + }; + return false; } /// @@ -162,23 +242,135 @@ public static class ManagedConfiguration /// The type of the configuration class. /// The type of the property within the configuration class. /// True if the configuration setting is left over and was reset, otherwise false. - public static bool IsConfigurationLeftOver(Expression> configSelection, Expression> propertyExpression, IEnumerable availablePlugins) + public static bool IsConfigurationLeftOver( + Expression> configSelection, + Expression> propertyExpression, + IEnumerable availablePlugins) + where TValue : Enum { if (!TryGet(configSelection, propertyExpression, out var configMeta)) return false; - if(configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) + if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) return false; - - // Check if the configuration plugin ID is valid against the available plugin IDs: + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); if (plugin is null) { - // Remove the locked state: configMeta.ResetManagedState(); return true; } - + + return false; + } + + public static bool IsConfigurationLeftOver( + Expression> configSelection, + Expression> propertyExpression, + IEnumerable availablePlugins) + { + if (!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) + return false; + + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); + if (plugin is null) + { + configMeta.ResetManagedState(); + return true; + } + + return false; + } + + // ReSharper disable MethodOverloadWithOptionalParameter + public static bool IsConfigurationLeftOver( + Expression> configSelection, + Expression> propertyExpression, + IEnumerable availablePlugins, + ISpanParsable? _ = null) + where TValue : struct, ISpanParsable + { + if (!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) + return false; + + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); + if (plugin is null) + { + configMeta.ResetManagedState(); + return true; + } + + return false; + } + + // ReSharper restore MethodOverloadWithOptionalParameter + + public static bool IsConfigurationLeftOver( + Expression> configSelection, + Expression>> propertyExpression, + IEnumerable availablePlugins) + { + if (!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) + return false; + + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); + if (plugin is null) + { + configMeta.ResetManagedState(); + return true; + } + + return false; + } + + public static bool IsConfigurationLeftOver( + Expression> configSelection, + Expression>> propertyExpression, + IEnumerable availablePlugins) + { + if (!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) + return false; + + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); + if (plugin is null) + { + configMeta.ResetManagedState(); + return true; + } + + return false; + } + + public static bool IsConfigurationLeftOver( + Expression> configSelection, + Expression>> propertyExpression, + IEnumerable availablePlugins) + { + if (!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) + return false; + + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); + if (plugin is null) + { + configMeta.ResetManagedState(); + return true; + } + return false; } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index 97041a6f..943caa48 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -61,6 +61,12 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT // Config: allow the user to add providers? ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun); + // 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); + // Handle configured LLM providers: PluginConfigurationObject.TryParse(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, x => x.NextProviderNum, mainTable, this.Id, ref this.configObjects, dryRun); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 92f77344..dfe3ad2d 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -1,7 +1,6 @@ using System.Text; using AIStudio.Settings; -using AIStudio.Settings.DataModel; using Lua; using Lua.Standard; @@ -138,16 +137,24 @@ public static partial class PluginFactory if(PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, configObjectList)) wasConfigurationChanged = true; - // Check for update behavior: - if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UpdateInterval, AVAILABLE_PLUGINS)) + // Check for the update interval: + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UpdateInterval, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; - // Check for update installation behavior: - if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UpdateInstallation, AVAILABLE_PLUGINS)) + // Check for the update installation method: + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UpdateInstallation, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; // Check for users allowed to added providers: - if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.AllowUserToAddProvider, AVAILABLE_PLUGINS)) + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.AllowUserToAddProvider, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + // Check for preview visibility: + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreviewVisibility, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + // Check for enabled preview features: + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.EnabledPreviewFeatures, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; if (wasConfigurationChanged) diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.52.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.52.md index 536b4ca2..d31ab5e8 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.52.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.52.md @@ -1,4 +1,5 @@ # v0.9.52, build 227 (2025-09-xx xx:xx UTC) - Added a feature so that matching results from data sources (local data sources as well as external ones via the ERI interface) are now also displayed at the end of a chat. All sources that come directly from the AI (like web searches) appear first, followed by those that come from the data sources. This source display works regardless of whether the AI actually used these sources, so users always get all the relevant information. +- Added the ability to manage the preview feature visibility and enabled preview features by using enterprise IT configurations. - Improved developer experience by detecting development environments and disabling update prompts in those environments. - Fixed an issue where external data sources using the ERI interface weren't using the correct source names during the augmentation phase. \ No newline at end of file