Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/MindWork AI Studio/Assistants/AssistantBase.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ protected override async Task OnInitializedAsync()
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,33 +255,80 @@ protected override async Task OnInitializedAsync()

private async Task AutoSave(bool force = false)
{
if(this.selectedPolicy is null)
//
// A pending store is a property of the settings, not of the selected policy: the value has
// been written into its policy already, so what is still outstanding is the store itself.
// It therefore outlives a form reset and a switch to another policy, and only a completed
// store clears it.
//
var hasChanges = this.policyStorePending;
if(this.selectedPolicy is { } policy)
hasChanges |= this.ApplyFormToPolicy(policy, force);

if (!hasChanges)
return;

await this.SettingsManager.StoreSettings();
this.policyStorePending = false;
}

/// <summary>
/// Takes the form values over into the given policy.
/// </summary>
/// <param name="policy">The policy to write the form values to.</param>
/// <param name="force">Whether the protected fields may be written as well.</param>
/// <returns>True when this changed anything about the policy, false otherwise.</returns>
private bool ApplyFormToPolicy(DataDocumentAnalysisPolicy policy, bool force)
{
// The preselected profile is always user-adjustable, even for protected policies and enterprise configurations:
this.selectedPolicy.PreselectedProfile = this.policyPreselectedProfile;
var hasChanges = policy.PreselectedProfile != this.policyPreselectedProfile;
policy.PreselectedProfile = this.policyPreselectedProfile;

// Enterprise configurations cannot be modified at all:
if(this.selectedPolicy.IsEnterpriseConfiguration)
return;

var canEditProtectedFields = force || (!this.selectedPolicy.IsProtected && !this.policyIsProtected);
if (canEditProtectedFields)
{
this.selectedPolicy.PreselectedProvider = this.policyPreselectedProviderId;
this.selectedPolicy.PolicyName = this.policyName;
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 = [..this.policyAllowedToolIds];
}

await this.SettingsManager.StoreSettings();
if(policy.IsEnterpriseConfiguration)
return hasChanges;

var canEditProtectedFields = force || (!policy.IsProtected && !this.policyIsProtected);
if (!canEditProtectedFields)
return hasChanges;

hasChanges = hasChanges
|| policy.PolicyName != this.policyName
|| policy.PreselectedProvider != this.policyPreselectedProviderId
|| policy.PolicyDescription != this.policyDescription
|| policy.IsProtected != this.policyIsProtected
|| policy.HidePolicyDefinition != this.policyHidePolicyDefinition
|| policy.AnalysisRules != this.policyAnalysisRules
|| policy.OutputRules != this.policyOutputRules
|| policy.MinimumProviderConfidence != this.policyMinimumProviderConfidence
|| !policy.AllowedToolIds.SetEquals(this.policyAllowedToolIds);

policy.PreselectedProvider = this.policyPreselectedProviderId;
policy.PolicyName = this.policyName;
policy.PolicyDescription = this.policyDescription;
policy.IsProtected = this.policyIsProtected;
policy.HidePolicyDefinition = this.policyHidePolicyDefinition;
policy.AnalysisRules = this.policyAnalysisRules;
policy.OutputRules = this.policyOutputRules;
policy.MinimumProviderConfidence = this.policyMinimumProviderConfidence;
policy.AllowedToolIds = [..this.policyAllowedToolIds];
return hasChanges;
}

/// <summary>
/// Whether the given policy may take over an edit of one of its protected fields right now.
/// </summary>
/// <remarks>
/// The handlers which write their value straight into the policy have to ask this themselves.
/// ApplyFormToPolicy asks the same question, but it never gets to judge their fields: they have
/// already brought policy and form in line, so nothing is left for it to compare. The markup
/// disables those controls for a protected policy and an enterprise policy is always a protected
/// one, which is why nobody should ever reach a handler that way -- this keeps the rule in the
/// code as well, where the next handler will look for it. The form value counts alongside the
/// stored one, because the protection switch is flipped before the store which writes it has run.
/// </remarks>
private bool AcceptsProtectedFieldEdits(DataDocumentAnalysisPolicy policy) => policy is { IsEnterpriseConfiguration: false, IsProtected: false } && !this.policyIsProtected;

private DataDocumentAnalysisPolicy? selectedPolicy;
private bool policyIsProtected;
private bool policyHidePolicyDefinition;
Expand All @@ -298,6 +345,21 @@ private async Task AutoSave(bool force = false)
/// </remarks>
private bool documentSelectionExpanded;
private string policyName = string.Empty;

/// <summary>
/// Whether an edit already applied to a policy still waits to be written to the settings file.
/// </summary>
/// <remarks>
/// Some handlers apply their value to the selected policy at once, because the rest of the
/// assistant reads it back from there right away: the policy list has to show a new name while
/// it is being typed, and the provider preselection is recomputed from the policy, not from the
/// form. Doing so leaves the auto-save nothing to compare the form against -- form and policy
/// already agree -- so every such handler has to announce the store itself. That is what this
/// flag is for. It belongs to no particular policy: the value has long arrived where it
/// belongs, only the file has not caught up yet, which is why a form reset or a switch to
/// another policy does not clear it. Only a completed store does.
/// </remarks>
private bool policyStorePending;
private string policyDescription = string.Empty;
private string policyAnalysisRules = string.Empty;
private string policyOutputRules = string.Empty;
Expand Down Expand Up @@ -477,6 +539,7 @@ private void PolicyNameWasChanged()
return;

this.selectedPolicy.PolicyName = this.policyName;
this.policyStorePending = true;
}

private async Task PolicyProtectionWasChanged(bool state)
Expand All @@ -488,7 +551,6 @@ private async Task PolicyProtectionWasChanged(bool state)
return;

this.policyIsProtected = state;
this.selectedPolicy.IsProtected = state;
this.policyDefinitionExpanded = !state;
this.documentSelectionExpanded = state;
await this.AutoSave(true);
Expand All @@ -503,7 +565,6 @@ private async Task PolicyHidePolicyDefinitionWasChanged(bool state)
return;

this.policyHidePolicyDefinition = state;
this.selectedPolicy.HidePolicyDefinition = state;
await this.AutoSave(true);
}

Expand Down Expand Up @@ -580,39 +641,50 @@ private Profile ResolveProfileSelection()
/// <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 { } policy || !this.AcceptsProtectedFieldEdits(policy))
return;

policy.AllowedToolIds = [..allowedToolIds];
this.policyStorePending = true;
}

private async Task PolicyMinimumConfidenceWasChangedAsync(ConfidenceLevel level)
private void PolicyMinimumConfidenceWasChanged(ConfidenceLevel level)
{
this.policyMinimumProviderConfidence = level;
await this.AutoSave();

if (this.selectedPolicy is { } policy && this.AcceptsProtectedFieldEdits(policy))
{
policy.MinimumProviderConfidence = level;
this.policyStorePending = true;
}

this.ApplyPolicyPreselection();
}

private void PolicyPreselectedProviderWasChanged(string providerId)
{
if (this.selectedPolicy is null)
if (this.selectedPolicy is not { } policy || !this.AcceptsProtectedFieldEdits(policy))
return;

this.policyPreselectedProviderId = providerId;
this.selectedPolicy.PreselectedProvider = providerId;
policy.PreselectedProvider = providerId;
this.policyStorePending = true;
this.ProviderSettings = Settings.Provider.NONE;
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.policyStorePending = true;
}

this.CurrentProfile = this.ResolveProfileSelection();
await this.AutoSave();
}

#region Overrides of MSGComponentBase
Expand Down
6 changes: 6 additions & 0 deletions app/MindWork AI Studio/Assistants/I18N/allTexts.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4216,6 +4216,12 @@ 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 are configured yet. Add a provider in the app settings.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T1166628228"] = "No LLM providers are configured yet. Add a provider in the app settings."

-- 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"

Expand Down
13 changes: 11 additions & 2 deletions app/MindWork AI Studio/Components/ProviderSelection.razor
Original file line number Diff line number Diff line change
@@ -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">
Expand All @@ -20,4 +23,10 @@
</MudStack>
</MudSelectItem>
}
</MudSelect>
</MudSelect>
@if (availableProviderItems.Count is 0 && this.GetEmptySelectionHint() is { } emptySelectionHint)
{
<MudText Typo="Typo.body2" Color="Color.Error" Class="mb-3">
@emptySelectionHint
</MudText>
}
22 changes: 22 additions & 0 deletions app/MindWork AI Studio/Components/ProviderSelection.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@ private IEnumerable<ProviderSelectionItem> GetAvailableProviderSelectionItems()
yield return new(provider, this.GetCapabilityIcons(provider));
}

/// <summary>
/// Says why there is nothing to choose from, or nothing at all when that is not the user's doing.
/// </summary>
/// <remarks>
/// An empty list has two causes the user can act on, and they lead to different places in the
/// settings: there is no provider yet, or none of the configured ones reaches the confidence
/// this component asks for. Naming the wrong one sends the user looking in the wrong place --
/// a first start has nobody to blame for a confidence level it never set. A missing or invalid
/// component is a third case and neither of those: it is a defect, it was logged as one, and
/// any explanation offered to the user here would be a guess.
/// </remarks>
private string? GetEmptySelectionHint()
{
if (this.Component is null or Tools.Components.NONE)
return null;

if (!this.SettingsManager.GetAllProviders().Any(x => x.UsedLLMProvider is not LLMProviders.NONE))
return this.T("No LLM providers are configured yet. Add a provider in the app settings.");

return this.T("No LLM providers meet the confidence requirements. Configure an eligible provider in the app settings.");
}

private IReadOnlyList<CapabilityIcon> GetCapabilityIcons(AIStudio.Settings.Provider provider)
{
var profile = provider.GetModelProfile();
Expand Down
Loading
Loading