WIP: Multiselection support

This commit is contained in:
nilsk 2026-03-13 01:51:48 +01:00
parent a182cc438a
commit 133be5b325
4 changed files with 173 additions and 45 deletions

View File

@ -87,22 +87,42 @@
case AssistantComponentType.DROPDOWN:
if (component is AssistantDropdown assistantDropdown)
{
<DynamicAssistantDropdown Items="@assistantDropdown.Items"
@bind-Value="@this.dropdownFields[assistantDropdown.Name]"
Default="@assistantDropdown.Default"
Label="@assistantDropdown.Label"
HelperText="@assistantDropdown.HelperText"
OpenIcon="@AssistantComponentPropHelper.GetIconSvg(assistantDropdown.OpenIcon)"
CloseIcon="@AssistantComponentPropHelper.GetIconSvg(assistantDropdown.CloseIcon)"
IconColor="@AssistantComponentPropHelper.GetColor(assistantDropdown.IconColor, Color.Default)"
IconPosition="@AssistantComponentPropHelper.GetAdornment(assistantDropdown.IconPositon, Adornment.End)"
Variant="@AssistantComponentPropHelper.GetVariant(assistantDropdown.Variant, Variant.Outlined)"
IsMultiselect="@assistantDropdown.IsMultiselect"
HasSelectAll="@assistantDropdown.HasSelectAll"
SelectAllText="@assistantDropdown.SelectAllText"
Class="@assistantDropdown.Class"
Style="@this.GetOptionalStyle(assistantDropdown.Style)"
/>
if (assistantDropdown.IsMultiselect)
{
<DynamicAssistantDropdown Items="@assistantDropdown.Items"
SelectedValues="@this.multiselectDropdownFields[assistantDropdown.Name]"
SelectedValuesChanged="@this.CreateMultiselectDropdownChangedCallback(assistantDropdown.Name)"
Default="@assistantDropdown.Default"
Label="@assistantDropdown.Label"
HelperText="@assistantDropdown.HelperText"
OpenIcon="@AssistantComponentPropHelper.GetIconSvg(assistantDropdown.OpenIcon)"
CloseIcon="@AssistantComponentPropHelper.GetIconSvg(assistantDropdown.CloseIcon)"
IconColor="@AssistantComponentPropHelper.GetColor(assistantDropdown.IconColor, Color.Default)"
IconPosition="@AssistantComponentPropHelper.GetAdornment(assistantDropdown.IconPositon, Adornment.End)"
Variant="@AssistantComponentPropHelper.GetVariant(assistantDropdown.Variant, Variant.Outlined)"
IsMultiselect="@true"
HasSelectAll="@assistantDropdown.HasSelectAll"
SelectAllText="@assistantDropdown.SelectAllText"
Class="@assistantDropdown.Class"
Style="@this.GetOptionalStyle(assistantDropdown.Style)" />
}
else
{
<DynamicAssistantDropdown Items="@assistantDropdown.Items"
@bind-Value="@this.dropdownFields[assistantDropdown.Name]"
Default="@assistantDropdown.Default"
Label="@assistantDropdown.Label"
HelperText="@assistantDropdown.HelperText"
OpenIcon="@AssistantComponentPropHelper.GetIconSvg(assistantDropdown.OpenIcon)"
CloseIcon="@AssistantComponentPropHelper.GetIconSvg(assistantDropdown.CloseIcon)"
IconColor="@AssistantComponentPropHelper.GetColor(assistantDropdown.IconColor, Color.Default)"
IconPosition="@AssistantComponentPropHelper.GetAdornment(assistantDropdown.IconPositon, Adornment.End)"
Variant="@AssistantComponentPropHelper.GetVariant(assistantDropdown.Variant, Variant.Outlined)"
HasSelectAll="@assistantDropdown.HasSelectAll"
SelectAllText="@assistantDropdown.SelectAllText"
Class="@assistantDropdown.Class"
Style="@this.GetOptionalStyle(assistantDropdown.Style)" />
}
}
break;
case AssistantComponentType.BUTTON:

View File

@ -43,6 +43,7 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
private readonly Dictionary<string, string> inputFields = new();
private readonly Dictionary<string, string> dropdownFields = new();
private readonly Dictionary<string, HashSet<string>> multiselectDropdownFields = new();
private readonly Dictionary<string, bool> switchFields = new();
private readonly Dictionary<string, WebContentState> webContentFields = new();
private readonly Dictionary<string, FileContentState> fileContentFields = new();
@ -218,6 +219,8 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
fields[entry.Key] = entry.Value ?? string.Empty;
foreach (var entry in this.dropdownFields)
fields[entry.Key] = entry.Value ?? string.Empty;
foreach (var entry in this.multiselectDropdownFields)
fields[entry.Key] = CreateLuaArray(entry.Value);
foreach (var entry in this.switchFields)
fields[entry.Key] = entry.Value;
foreach (var entry in this.webContentFields)
@ -287,8 +290,18 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
this.inputFields.Add(textArea.Name, textArea.PrefillText);
break;
case AssistantComponentType.DROPDOWN:
if (component is AssistantDropdown dropdown && !this.dropdownFields.ContainsKey(dropdown.Name))
this.dropdownFields.Add(dropdown.Name, dropdown.Default.Display);
if (component is AssistantDropdown dropdown)
{
if (dropdown.IsMultiselect)
{
if (!this.multiselectDropdownFields.ContainsKey(dropdown.Name))
this.multiselectDropdownFields.Add(dropdown.Name, CreateInitialMultiselectValues(dropdown));
}
else if (!this.dropdownFields.ContainsKey(dropdown.Name))
{
this.dropdownFields.Add(dropdown.Name, dropdown.Default.Value);
}
}
break;
case AssistantComponentType.SWITCH:
if (component is AssistantSwitch switchComponent && !this.switchFields.ContainsKey(switchComponent.Name))
@ -399,6 +412,17 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
return;
}
if (this.multiselectDropdownFields.ContainsKey(fieldName))
{
if (value.TryRead<LuaTable>(out var multiselectDropdownValue))
this.multiselectDropdownFields[fieldName] = ReadStringValues(multiselectDropdownValue);
else if (value.TryRead<string>(out var singleDropdownValue))
this.multiselectDropdownFields[fieldName] = string.IsNullOrWhiteSpace(singleDropdownValue) ? [] : [singleDropdownValue];
else
this.LogFieldUpdateTypeMismatch(fieldName, "string[]");
return;
}
if (this.switchFields.ContainsKey(fieldName))
{
if (value.TryRead<bool>(out var boolValue))
@ -443,6 +467,12 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
this.Logger.LogWarning("Assistant BUTTON action tried to write an invalid value to '{FieldName}'. Expected {ExpectedType}.", fieldName, expectedType);
}
private EventCallback<HashSet<string>> CreateMultiselectDropdownChangedCallback(string fieldName) =>
EventCallback.Factory.Create<HashSet<string>>(this, values =>
{
this.multiselectDropdownFields[fieldName] = values;
});
private string? ValidateProfileSelection(AssistantProfileSelection profileSelection, Profile? profile)
{
if (profile == default || profile == Profile.NO_PROFILE)
@ -517,7 +547,9 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
if (component is AssistantDropdown dropdown)
{
prompt += $"{Environment.NewLine}context:{Environment.NewLine}{dropdown.UserPrompt}{Environment.NewLine}---{Environment.NewLine}";
if (this.dropdownFields.TryGetValue(dropdown.Name, out userInput))
if (dropdown.IsMultiselect && this.multiselectDropdownFields.TryGetValue(dropdown.Name, out var selections))
prompt += $"user prompt:{Environment.NewLine}{string.Join(Environment.NewLine, selections.OrderBy(static value => value, StringComparer.Ordinal))}";
else if (this.dropdownFields.TryGetValue(dropdown.Name, out userInput))
prompt += $"user prompt:{Environment.NewLine}{userInput}";
}
break;
@ -567,4 +599,36 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
return prompt;
}
private static HashSet<string> CreateInitialMultiselectValues(AssistantDropdown dropdown)
{
if (string.IsNullOrWhiteSpace(dropdown.Default.Value))
return [];
return [dropdown.Default.Value];
}
private static LuaTable CreateLuaArray(IEnumerable<string> values)
{
var luaArray = new LuaTable();
var index = 1;
foreach (var value in values.OrderBy(static value => value, StringComparer.Ordinal))
luaArray[index++] = value;
return luaArray;
}
private static HashSet<string> ReadStringValues(LuaTable values)
{
var parsedValues = new HashSet<string>(StringComparer.Ordinal);
foreach (var entry in values)
{
if (entry.Value.TryRead<string>(out var value) && !string.IsNullOrWhiteSpace(value))
parsedValues.Add(value);
}
return parsedValues;
}
}

View File

@ -1,27 +1,52 @@
@using AIStudio.Tools.PluginSystem.Assistants.DataModel
<MudStack Row="true" Class='@this.MergeClasses(this.Class, "mb-3")' Style="@this.Style">
<MudSelect
T="string"
Value="@this.Value"
ValueChanged="@(val => this.OnValueChanged(val))"
Label="@this.Label"
HelperText="@this.HelperText"
Placeholder="@this.Default.Value"
OpenIcon="@this.OpenIcon"
CloseIcon="@this.CloseIcon"
Adornment="@this.IconPosition"
AdornmentColor="@this.IconColor"
Variant="@this.Variant"
Margin="Margin.Normal"
MultiSelection="@this.IsMultiselect"
SelectAll="@this.HasSelectAll"
SelectAllText="@this.SelectAllText"
>
@foreach (var item in Items)
{
<MudSelectItem Value="@item.Value">
@item.Display
</MudSelectItem>
}
</MudSelect>
@if (this.IsMultiselect)
{
<MudSelect
T="string"
SelectedValues="@this.SelectedValues"
SelectedValuesChanged="@this.OnSelectedValuesChanged"
Label="@this.Label"
HelperText="@this.HelperText"
Placeholder="@this.Default.Display"
OpenIcon="@this.OpenIcon"
CloseIcon="@this.CloseIcon"
Adornment="@this.IconPosition"
AdornmentColor="@this.IconColor"
Variant="@this.Variant"
Margin="Margin.Normal"
MultiSelection="@true"
SelectAll="@this.HasSelectAll"
SelectAllText="@this.SelectAllText">
@foreach (var item in Items)
{
<MudSelectItem Value="@item.Value">
@item.Display
</MudSelectItem>
}
</MudSelect>
}
else
{
<MudSelect
T="string"
Value="@this.Value"
ValueChanged="@(val => this.OnValueChanged(val))"
Label="@this.Label"
HelperText="@this.HelperText"
Placeholder="@this.Default.Display"
OpenIcon="@this.OpenIcon"
CloseIcon="@this.CloseIcon"
Adornment="@this.IconPosition"
AdornmentColor="@this.IconColor"
Variant="@this.Variant"
Margin="Margin.Normal">
@foreach (var item in Items)
{
<MudSelectItem Value="@item.Value">
@item.Display
</MudSelectItem>
}
</MudSelect>
}
</MudStack>

View File

@ -1,5 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AIStudio.Tools.PluginSystem.Assistants.DataModel;
using Microsoft.AspNetCore.Components;
@ -17,6 +18,10 @@ namespace AIStudio.Components
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public HashSet<string> SelectedValues { get; set; } = [];
[Parameter] public EventCallback<HashSet<string>> SelectedValuesChanged { get; set; }
[Parameter] public string Label { get; set; } = string.Empty;
[Parameter] public string HelperText { get; set; } = string.Empty;
@ -52,6 +57,20 @@ namespace AIStudio.Components
}
}
private async Task OnSelectedValuesChanged(IEnumerable<string?>? newValues)
{
var updatedValues = newValues?
.Where(value => !string.IsNullOrWhiteSpace(value))
.Select(value => value!)
.ToHashSet(StringComparer.Ordinal) ?? [];
if (this.SelectedValues.SetEquals(updatedValues))
return;
this.SelectedValues = updatedValues;
await this.SelectedValuesChanged.InvokeAsync(updatedValues);
}
private string MergeClasses(string custom, string fallback)
{
var trimmedCustom = custom?.Trim() ?? string.Empty;