Fix document analysis provider selection freezes

This commit is contained in:
Peer Hogeterp 2026-09-14 14:20:01 +02:00
parent 6e735523c6
commit 894c509f4d
7 changed files with 80 additions and 34 deletions

View File

@ -184,7 +184,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
this.formChangeTimer.Elapsed += (_, _) =>
{
this.formChangeTimer.Stop();
this.OnFormChange().Observe($"{nameof(AssistantBase<TSettings>)}: handling a form change");
this.InvokeAsync(this.OnFormChange).Observe($"{nameof(AssistantBase<TSettings>)}: handling a form change");
};
this.MightPreselectValues();

View File

@ -104,13 +104,13 @@ else
@T("Note: This setting only takes effect when this policy is exported and distributed via a configuration plugin to other users. When enabled, users will only see the document selection interface and cannot view or modify the policy details. This setting does NOT affect your local view - you will always see the full policy definition for policies you create.")
</MudJustifiedText>
<ConfigurationMinConfidenceSelection Disabled="@(() => this.IsNoPolicySelectedOrProtected)" RestrictToGlobalMinimumConfidence="true" SelectedValue="@(() => this.policyMinimumProviderConfidence)" SelectionUpdateAsync="@(async level => await this.PolicyMinimumConfidenceWasChangedAsync(level))" />
<ConfigurationMinConfidenceSelection Disabled="@(() => this.IsNoPolicySelectedOrProtected)" RestrictToGlobalMinimumConfidence="true" SelectedValue="@(() => this.policyMinimumProviderConfidence)" SelectionUpdate="@this.PolicyMinimumConfidenceWasChanged" />
<ToolSelectionField Component="@this.Component" SelectedToolIds="@this.policyAllowedToolIds" SelectedToolIdsChanged="@this.PolicyAllowedToolsWasChangedAsync" Disabled="@this.IsNoPolicySelectedOrProtected" Label="@T("Tools this policy permits")" Help="@T("Only the tools selected here can be used by the AI for an analysis with this policy. Every tool still has to meet the confidence requirements of the selected provider, so a tool may remain unavailable even when this policy permits it.")"/>
<ToolSelectionField Component="@this.Component" SelectedToolIds="@this.policyAllowedToolIds" SelectedToolIdsChanged="@this.PolicyAllowedToolsWasChanged" Disabled="@this.IsNoPolicySelectedOrProtected" Label="@T("Tools this policy permits")" Help="@T("Only the tools selected here can be used by the AI for an analysis with this policy. Every tool still has to meet the confidence requirements of the selected provider, so a tool may remain unavailable even when this policy permits it.")"/>
<ConfigurationProviderSelection Component="Components.DOCUMENT_ANALYSIS_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => this.IsNoPolicySelectedOrProtected)" SelectedValue="@(() => this.policyPreselectedProviderId)" SelectionUpdate="@this.PolicyPreselectedProviderWasChanged" ExplicitMinimumConfidence="@this.GetPolicyMinimumConfidenceLevel()"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => this.IsNoPolicySelected)" SelectedValue="@(() => this.policyPreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdateAsync="@(async selection => await this.PolicyPreselectedProfileWasChangedAsync(selection))" OptionHelp="@T("Choose whether the policy should use the app default profile, no profile, or a specific profile.")"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => this.IsNoPolicySelected)" SelectedValue="@(() => this.policyPreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@this.PolicyPreselectedProfileWasChanged" OptionHelp="@T("Choose whether the policy should use the app default profile, no profile, or a specific profile.")"/>
<MudTextSwitch Disabled="@(this.IsNoPolicySelected || (this.selectedPolicy?.IsEnterpriseConfiguration ?? true))" Label="@T("Would you like to protect this policy so that you cannot accidentally edit or delete it?")" Value="@this.policyIsProtected" ValueChanged="async state => await this.PolicyProtectionWasChanged(state)" LabelOn="@T("Yes, protect this policy")" LabelOff="@T("No, the policy can be edited")" />

View File

@ -180,6 +180,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
protected override void ResetForm()
{
this.loadedDocumentPaths.Clear();
this.policyNameWasEdited = false;
if (!this.MightPreselectValues())
{
this.policyName = string.Empty;
@ -220,6 +221,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
this.policyAllowedToolIds = [..this.selectedPolicy.AllowedToolIds];
this.policyPreselectedProviderId = this.selectedPolicy.PreselectedProvider;
this.policyPreselectedProfile = ProfilePreselection.FromStoredValue(this.selectedPolicy.PreselectedProfile);
this.policyNameWasEdited = false;
return true;
}
@ -259,6 +261,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
return;
// The preselected profile is always user-adjustable, even for protected policies and enterprise configurations:
var hasChanges = this.selectedPolicy.PreselectedProfile != this.policyPreselectedProfile;
this.selectedPolicy.PreselectedProfile = this.policyPreselectedProfile;
// Enterprise configurations cannot be modified at all:
@ -268,6 +271,17 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
var canEditProtectedFields = force || (!this.selectedPolicy.IsProtected && !this.policyIsProtected);
if (canEditProtectedFields)
{
hasChanges = hasChanges
|| this.policyNameWasEdited
|| this.selectedPolicy.PreselectedProvider != this.policyPreselectedProviderId
|| this.selectedPolicy.PolicyDescription != this.policyDescription
|| this.selectedPolicy.IsProtected != this.policyIsProtected
|| this.selectedPolicy.HidePolicyDefinition != this.policyHidePolicyDefinition
|| this.selectedPolicy.AnalysisRules != this.policyAnalysisRules
|| this.selectedPolicy.OutputRules != this.policyOutputRules
|| this.selectedPolicy.MinimumProviderConfidence != this.policyMinimumProviderConfidence
|| !this.selectedPolicy.AllowedToolIds.SetEquals(this.policyAllowedToolIds);
this.selectedPolicy.PreselectedProvider = this.policyPreselectedProviderId;
this.selectedPolicy.PolicyName = this.policyName;
this.selectedPolicy.PolicyDescription = this.policyDescription;
@ -279,7 +293,11 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
this.selectedPolicy.AllowedToolIds = [..this.policyAllowedToolIds];
}
if (!hasChanges)
return;
await this.SettingsManager.StoreSettings();
this.policyNameWasEdited = false;
}
private DataDocumentAnalysisPolicy? selectedPolicy;
@ -298,6 +316,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
/// </remarks>
private bool documentSelectionExpanded;
private string policyName = string.Empty;
private bool policyNameWasEdited;
private string policyDescription = string.Empty;
private string policyAnalysisRules = string.Empty;
private string policyOutputRules = string.Empty;
@ -477,6 +496,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
return;
this.selectedPolicy.PolicyName = this.policyName;
this.policyNameWasEdited = true;
}
private async Task PolicyProtectionWasChanged(bool state)
@ -488,7 +508,6 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
return;
this.policyIsProtected = state;
this.selectedPolicy.IsProtected = state;
this.policyDefinitionExpanded = !state;
this.documentSelectionExpanded = state;
await this.AutoSave(true);
@ -503,7 +522,6 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
return;
this.policyHidePolicyDefinition = state;
this.selectedPolicy.HidePolicyDefinition = state;
await this.AutoSave(true);
}
@ -580,16 +598,18 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
/// <summary>
/// Takes over the tools this policy permits.
/// </summary>
private async Task PolicyAllowedToolsWasChangedAsync(HashSet<string> allowedToolIds)
private void PolicyAllowedToolsWasChanged(HashSet<string> allowedToolIds)
{
this.policyAllowedToolIds = allowedToolIds;
await this.AutoSave();
if (this.selectedPolicy is not null)
this.selectedPolicy.AllowedToolIds = [..allowedToolIds];
}
private async Task PolicyMinimumConfidenceWasChangedAsync(ConfidenceLevel level)
private void PolicyMinimumConfidenceWasChanged(ConfidenceLevel level)
{
this.policyMinimumProviderConfidence = level;
await this.AutoSave();
if (this.selectedPolicy is not null)
this.selectedPolicy.MinimumProviderConfidence = level;
this.ApplyPolicyPreselection();
}
@ -605,14 +625,13 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
this.ApplyPolicyPreselection();
}
private async Task PolicyPreselectedProfileWasChangedAsync(ProfilePreselection selection)
private void PolicyPreselectedProfileWasChanged(ProfilePreselection selection)
{
this.policyPreselectedProfile = selection;
if (this.selectedPolicy is not null)
this.selectedPolicy.PreselectedProfile = this.policyPreselectedProfile;
this.CurrentProfile = this.ResolveProfileSelection();
await this.AutoSave();
}
#region Overrides of MSGComponentBase

View File

@ -4213,6 +4213,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILESELECTION::T3654011106"] = "Open P
-- You can switch between your profiles here
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILESELECTION::T918741365"] = "You can switch between your profiles here"
-- No LLM providers meet the confidence requirements. Configure an eligible provider in the app settings.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T1220991024"] = "No LLM providers meet the confidence requirements. Configure an eligible provider in the app settings."
-- Audio input possible
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T1742581112"] = "Audio input possible"

View File

@ -1,7 +1,10 @@
@using AIStudio.Settings
@inherits MSGComponentBase
@{
var availableProviderItems = this.GetAvailableProviderSelectionItems().ToList();
}
<MudSelect T="Provider" Value="@this.ProviderSettings" ValueChanged="@this.SelectionChanged" Validation="@this.ValidateProvider" Margin="Margin.Dense" Label="@T("Provider")" Class="mb-3 rounded-lg" OuterClass="flex-grow-0" Variant="Variant.Outlined" Disabled="@this.Disabled">
@foreach (var providerItem in this.GetAvailableProviderSelectionItems())
@foreach (var providerItem in availableProviderItems)
{
<MudSelectItem Value="@providerItem.Provider">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="w-100" Wrap="Wrap.NoWrap">
@ -21,3 +24,9 @@
</MudSelectItem>
}
</MudSelect>
@if (availableProviderItems.Count is 0)
{
<MudText Typo="Typo.body2" Color="Color.Error" Class="mb-3">
@T("No LLM providers meet the confidence requirements. Configure an eligible provider in the app settings.")
</MudText>
}

View File

@ -33,6 +33,7 @@ public sealed class SettingsManager
private readonly ILogger<SettingsManager> logger;
private readonly RustService rustService;
private readonly SemaphoreSlim settingsWriteSemaphore = new(1, 1);
/// <summary>
/// The settings manager.
@ -294,41 +295,56 @@ public sealed class SettingsManager
/// </summary>
public async Task StoreSettings()
{
if(!this.IsSetUp)
await this.settingsWriteSemaphore.WaitAsync();
try
{
this.logger.LogWarning("Cannot store settings, because the configuration is not set up yet.");
return;
}
if(!this.IsSetUp)
{
this.logger.LogWarning("Cannot store settings, because the configuration is not set up yet.");
return;
}
if(this.SettingsWriteBlocked)
if(this.SettingsWriteBlocked)
{
this.logger.LogWarning($"Cannot store settings, because settings writes are blocked. Reason: '{this.SettingsWriteBlockReason}'.");
return;
}
var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS);
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
await this.StoreSettingsSnapshot(settingsJson, settingsPath);
await this.StoreCurrentVersionBackup(this.ConfigurationData.Version, settingsJson);
}
finally
{
this.logger.LogWarning($"Cannot store settings, because settings writes are blocked. Reason: '{this.SettingsWriteBlockReason}'.");
return;
this.settingsWriteSemaphore.Release();
}
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
await this.StoreSettingsSnapshot(this.ConfigurationData, settingsPath);
await this.StoreCurrentVersionBackup(this.ConfigurationData);
}
private static string GetBackupSettingsFilename(Version version) => $"settings.{version.ToString().ToLowerInvariant()}.json";
private static string GetBackupSettingsPath(Version version) => Path.Combine(ConfigDirectory!, GetBackupSettingsFilename(version));
private async Task StoreCurrentVersionBackup(Data settingsData)
private Task StoreCurrentVersionBackup(Data settingsData) =>
this.StoreCurrentVersionBackup(settingsData.Version, JsonSerializer.Serialize(settingsData, JSON_OPTIONS));
private async Task StoreCurrentVersionBackup(Version settingsVersion, string settingsJson)
{
if(settingsData.Version != CURRENT_SETTINGS_VERSION)
if(settingsVersion != CURRENT_SETTINGS_VERSION)
{
this.logger.LogWarning($"Skipping settings backup because the settings version '{settingsData.Version}' is not the current version '{CURRENT_SETTINGS_VERSION}'.");
this.logger.LogWarning($"Skipping settings backup because the settings version '{settingsVersion}' is not the current version '{CURRENT_SETTINGS_VERSION}'.");
return;
}
var backupSettingsPath = GetBackupSettingsPath(CURRENT_SETTINGS_VERSION);
await this.StoreSettingsSnapshot(settingsData, backupSettingsPath);
await this.StoreSettingsSnapshot(settingsJson, backupSettingsPath);
this.logger.LogInformation($"Stored the settings backup file '{backupSettingsPath}'.");
}
private async Task StoreSettingsSnapshot(Data settingsData, string settingsPath)
private Task StoreSettingsSnapshot(Data settingsData, string settingsPath) =>
this.StoreSettingsSnapshot(JsonSerializer.Serialize(settingsData, JSON_OPTIONS), settingsPath);
private async Task StoreSettingsSnapshot(string settingsJson, string settingsPath)
{
if(!Directory.Exists(ConfigDirectory))
{
@ -336,8 +352,6 @@ public sealed class SettingsManager
Directory.CreateDirectory(ConfigDirectory!);
}
var settingsJson = JsonSerializer.Serialize(settingsData, JSON_OPTIONS);
//
// We write the new settings next to the previous ones and replace them afterwards, so that
// no crash can leave a half-written settings file behind. The temporary file has to live in
@ -349,7 +363,7 @@ public sealed class SettingsManager
try
{
await File.WriteAllTextAsync(tempFile, settingsJson);
File.Move(tempFile, settingsPath, true);
await Task.Run(() => File.Move(tempFile, settingsPath, true));
}
catch
{

View File

@ -26,6 +26,7 @@
- Improved the list of your attached files: every file now appears under the folder it came from, and each folder is named only once, no matter in which order you attached your files.
- Changed how provider trust and provider confidence work together. Marking a provider as trustworthy in a configuration no longer also satisfies a required confidence level: one says who runs the provider, the other how confidential it is. Organizations raise a provider's level in their own confidence scheme instead. This applies beyond local data sources, for example, when a model reads a page from your intranet.
- Fixed the abilities AI Studio assumed for many models. We checked the families against their documentation: some models gained image input, reasoning, or tool calling, others lost an ability they never had.
- Fixed the Document Analysis assistant becoming unresponsive when choosing an LLM provider.
- Fixed model names that a provider writes in its own way not being recognized at all, such as the colon Ollama puts before the variant. Those models were treated as plain text models and lost every other ability.
- Fixed a model resold under a plain name not getting the abilities it really has.
- Fixed image and video generation models showing up among the chat models.