Merge branch 'main' into Embedding_API

This commit is contained in:
PaulKoudelka 2026-02-20 14:02:06 +01:00
commit 6381f236ea
12 changed files with 275 additions and 42 deletions

View File

@ -14,8 +14,22 @@
SelectedValuesChanged="@this.OptionChanged">
@foreach (var data in this.Data)
{
<MudSelectItemExtended Value="@data.Value">
@data.Name
var isLockedValue = this.IsLockedValue(data.Value);
<MudSelectItemExtended Value="@data.Value" Disabled="@isLockedValue" Style="@(isLockedValue ? "pointer-events:auto !important;" : null)">
@if (isLockedValue)
{
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.FlexStart" Wrap="Wrap.NoWrap">
@* MudTooltip.RootStyle is set as a workaround for issue -> https://github.com/MudBlazor/MudBlazor/issues/10882 *@
<MudTooltip Text="@this.LockedTooltip()" Arrow="true" Placement="Placement.Right" RootStyle="display:inline-flex;">
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Error" Size="Size.Small" Class="mr-1"/>
</MudTooltip>
@data.Name
</MudStack>
}
else
{
@data.Name
}
</MudSelectItemExtended>
}
</MudSelectExtended>

View File

@ -27,6 +27,12 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
/// </summary>
[Parameter]
public Action<HashSet<TData>> SelectionUpdate { get; set; } = _ => { };
/// <summary>
/// Determines whether a specific item is locked by a configuration plugin.
/// </summary>
[Parameter]
public Func<TData, bool> IsItemLocked { get; set; } = _ => false;
#region Overrides of ConfigurationBase
@ -62,4 +68,12 @@ public partial class ConfigurationMultiSelect<TData> : 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));
}

View File

@ -25,11 +25,11 @@
var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList();
if (availablePreviewFeatures.Count > 0)
{
<ConfigurationMultiSelect OptionDescription="@T("Select preview features")" SelectedValues="@(() => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures.Where(x => !x.IsReleased()).ToHashSet())" Data="@availablePreviewFeatures" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedValue)" OptionHelp="@T("Which preview features would you like to enable?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.IsLocked"/>
<ConfigurationMultiSelect OptionDescription="@T("Select preview features")" SelectedValues="@this.GetSelectedPreviewFeatures" Data="@availablePreviewFeatures" SelectionUpdate="@this.UpdateEnabledPreviewFeatures" OptionHelp="@T("Which preview features would you like to enable?")" IsItemLocked="@this.IsPluginContributedPreviewFeature" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.IsLocked"/>
}
}
<ConfigurationProviderSelection Component="Components.APP_SETTINGS" Data="@this.AvailableLLMProvidersFunc()" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProvider = selectedValue)" HelpText="@(() => T("Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence."))"/>
<ConfigurationProviderSelection Component="Components.APP_SETTINGS" Data="@this.AvailableLLMProvidersFunc()" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProvider = selectedValue)" HelpText="@(() => T("Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence."))" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Preselect one of your profiles?")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProfile = selectedValue)" OptionHelp="@T("Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreselectedProfile, out var meta) && meta.IsLocked"/>
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))

View File

@ -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<PreviewFeatures> 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<PreviewFeatures> GetSelectedPreviewFeatures()
{
var enabled = this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures.Where(x => !x.IsReleased()).ToHashSet();
enabled.UnionWith(this.GetPluginContributedPreviewFeatures());
return enabled;
}
private void UpdateEnabledPreviewFeatures(HashSet<PreviewFeatures> selectedFeatures)
{
selectedFeatures.UnionWith(this.GetPluginContributedPreviewFeatures());
this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedFeatures;
}
private async Task UpdateLangBehaviour(LangBehavior behavior)

View File

@ -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.

View File

@ -28,14 +28,14 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
private Expression<Func<TClass, TValue>> PropertyExpression { get; }
/// <summary>
/// Indicates whether the configuration is managed by a plugin and is therefore locked.
/// Indicates whether the configuration is locked by a configuration plugin.
/// </summary>
public bool IsLocked { get; private set; }
/// <summary>
/// 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.
/// </summary>
public Guid MangedByConfigPluginId { get; private set; }
public Guid LockedByConfigPluginId { get; private set; }
/// <summary>
/// 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<TClass, TValue> : ConfigMetaBase
public required TValue Default { get; init; }
/// <summary>
/// Locks the configuration state, indicating that it is managed by a specific plugin.
/// Indicates whether a plugin contribution is available.
/// </summary>
/// <param name="pluginId">The ID of the plugin that is managing this configuration.</param>
public void LockManagedState(Guid pluginId)
public bool HasPluginContribution { get; private set; }
/// <summary>
/// The additive value contribution provided by a configuration plugin.
/// </summary>
public TValue PluginContribution { get; private set; } = default!;
/// <summary>
/// The ID of the plugin that provided the additive value contribution.
/// </summary>
public Guid PluginContributionByConfigPluginId { get; private set; }
/// <summary>
/// Locks the configuration state, indicating that it is controlled by a specific plugin.
/// </summary>
/// <param name="pluginId">The ID of the plugin that is locking this configuration.</param>
public void LockConfiguration(Guid pluginId)
{
this.IsLocked = true;
this.MangedByConfigPluginId = pluginId;
this.LockedByConfigPluginId = pluginId;
}
/// <summary>
/// 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.
/// </summary>
public void ResetManagedState()
public void ResetLockedConfiguration()
{
this.IsLocked = false;
this.MangedByConfigPluginId = Guid.Empty;
this.LockedByConfigPluginId = Guid.Empty;
this.Reset();
}
/// <summary>
/// Unlocks the configuration state without changing the current value.
/// </summary>
public void UnlockConfiguration()
{
this.IsLocked = false;
this.LockedByConfigPluginId = Guid.Empty;
}
/// <summary>
/// Stores an additive plugin contribution.
/// </summary>
public void SetPluginContribution(TValue value, Guid pluginId)
{
this.PluginContribution = value;
this.PluginContributionByConfigPluginId = pluginId;
this.HasPluginContribution = true;
}
/// <summary>
/// Clears the additive plugin contribution without changing the current value.
/// </summary>
public void ClearPluginContribution()
{
this.PluginContribution = default!;
this.PluginContributionByConfigPluginId = Guid.Empty;
this.HasPluginContribution = false;
}
/// <summary>
/// Resets the configuration property to its default value.
/// </summary>
public void Reset()
private void Reset()
{
var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData);
var memberExpression = this.PropertyExpression.GetMemberExpression();

View File

@ -65,7 +65,7 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
/// <summary>
/// Should we preselect a provider for the entire app?
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProvider, string.Empty);
/// <summary>
/// Should we preselect a profile for the entire app?

View File

@ -581,6 +581,90 @@ public static partial class ManagedConfiguration
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
/// <param name="settings">The Lua table containing the settings to process.</param>
/// <param name="configSelection">The expression to select the configuration class.</param>
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
/// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
/// <typeparam name="TValue">The type of the property within the configuration class. It is also the type of the set
/// elements, which must be an enum.</typeparam>
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
public static bool TryProcessConfigurationWithPluginContribution<TClass, TValue>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, ISet<TValue>>> 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<TValue>();
// 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<LuaTable>(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<TValue>(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<string>(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<TValue>(currentValue);
merged.UnionWith(configuredValue);
configMeta.SetValue(merged);
configMeta.SetPluginContribution(new HashSet<TValue>(configuredValue), configPluginId);
}
else if (configMeta.HasPluginContribution && configMeta.PluginContributionByConfigPluginId == configPluginId)
{
configMeta.ClearPluginContribution();
}
if (configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId)
configMeta.UnlockConfiguration();
return successful;
}
/// <summary>
/// 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:

View File

@ -9,6 +9,7 @@ namespace AIStudio.Settings;
public static partial class ManagedConfiguration
{
private static readonly ConcurrentDictionary<string, IConfig> METADATA = new();
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
/// <summary>
/// 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;
}
/// <summary>
/// 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.
/// </summary>
public static bool IsPluginContributionLeftOver<TClass, TValue>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
IEnumerable<IAvailablePlugin> 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;
}

View File

@ -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);

View File

@ -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;

View File

@ -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.