Support multiple web search backends: SearXNG, Staan, and Tavily (#948)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-09-04 22:14:29 +02:00
1 parent 4d8d30e15e
commit 4141e7c504
57 files changed
+3734 -1088

No files matched your search

@@ -28,31 +28,53 @@
<MudAlert Severity="Severity.Error" Class="mb-4">@this.validationMessage</MudAlert>
}
<MudPaper Class="pa-3 mb-4 border-dashed border rounded-lg">
@foreach (var property in this.toolDefinition.SettingsSchema.Properties)
@foreach (var warning in this.GetSettingsWarnings())
{
var fieldName = property.Key;
var field = property.Value;
var fieldOptions = field.GetOptions();
if (fieldOptions.Count > 0)
{
<MudSelect T="string" Label="@this.GetFieldLabel(fieldName, field)" Value="@this.GetValue(fieldName)" ValueChanged="@(value => this.UpdateValue(fieldName, value))" Variant="Variant.Outlined" Margin="Margin.Dense" HelperText="@this.GetFieldDescription(fieldName, field)" Placeholder="@this.GetFieldPlaceholder(fieldName, field)" Class="mb-3" Disabled="@this.IsFieldDisabled(fieldName)">
@if (!this.toolDefinition.SettingsSchema.Required.Contains(fieldName))
{
<MudSelectItem T="string" Value="@string.Empty">@T("Not set")</MudSelectItem>
}
@foreach (var option in fieldOptions)
{
<MudSelectItem T="string" Value="@option.Value">@option.Label</MudSelectItem>
}
</MudSelect>
}
else
{
<MudTextField T="string" Label="@this.GetFieldLabel(fieldName, field)" Value="@this.GetValue(fieldName)" ValueChanged="@(value => this.UpdateValue(fieldName, value))" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3" HelperText="@this.GetFieldDescription(fieldName, field)" Placeholder="@this.GetFieldPlaceholder(fieldName, field)" InputType="@(field.Secret ? InputType.Password : InputType.Text)" Disabled="@this.IsFieldDisabled(fieldName)" />
}
<MudAlert Severity="Severity.Warning" Class="mb-4">@warning</MudAlert>
}
@foreach (var group in this.BuildVisibleFieldGroups())
{
<MudPaper Class="pa-3 mb-4 border-dashed border rounded-lg">
@if (this.ShowsGroupHeader(group))
{
<MudStack Row="@true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Wrap="Wrap.Wrap" Class="mb-3">
<MudText Typo="Typo.subtitle2">@this.GetGroupLabel(group.Key)</MudText>
<MudStack Row="@true" AlignItems="AlignItems.Center" Spacing="2" Wrap="Wrap.Wrap">
@foreach (var link in this.GetGroupLinks(group.Key))
{
<MudButton Variant="Variant.Filled" Size="Size.Small" StartIcon="@link.Icon" Href="@link.Url" Target="_blank">
@link.Label
</MudButton>
}
</MudStack>
</MudStack>
}
@foreach (var property in group.Fields)
{
var fieldName = property.Key;
var field = property.Value;
var fieldOptions = field.GetOptions();
if (fieldOptions.Count > 0)
{
<MudSelect T="string" Label="@this.GetFieldLabel(fieldName, field)" Value="@this.GetValue(fieldName)" ValueChanged="@(value => this.UpdateValue(fieldName, value))" Variant="Variant.Outlined" Margin="Margin.Dense" HelperText="@this.GetFieldDescription(fieldName, field)" Placeholder="@this.GetFieldPlaceholder(fieldName, field)" Class="mb-3" Disabled="@this.IsFieldDisabled(fieldName)">
@if (!this.toolDefinition.SettingsSchema.Required.Contains(fieldName))
{
<MudSelectItem T="string" Value="@string.Empty">@T("Not set")</MudSelectItem>
}
@foreach (var option in fieldOptions)
{
<MudSelectItem T="string" Value="@option.Value">@option.Label</MudSelectItem>
}
</MudSelect>
}
else
{
<MudTextField T="string" Label="@this.GetFieldLabel(fieldName, field)" Value="@this.GetValue(fieldName)" ValueChanged="@(value => this.UpdateValue(fieldName, value))" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3" HelperText="@this.GetFieldDescription(fieldName, field)" Placeholder="@this.GetFieldPlaceholder(fieldName, field)" InputType="@(field.Secret ? InputType.Password : InputType.Text)" Disabled="@this.IsFieldDisabled(fieldName)" />
}
}
</MudPaper>
}
</MudPaper>
}
</DialogContent>
<DialogActions>
@@ -18,6 +18,7 @@ public partial class ToolSettingsDialog : SettingsDialogBase
private ToolDefinition? toolDefinition;
private IToolImplementation? implementation;
private Dictionary<string, string> values = new(StringComparer.Ordinal);
private IReadOnlyList<FieldGroup> fieldGroups = [];
private string validationMessage = string.Empty;
protected override async Task OnInitializedAsync()
@@ -28,11 +29,105 @@ public partial class ToolSettingsDialog : SettingsDialogBase
{
this.implementation = this.ToolRegistry.GetImplementation(this.toolDefinition.ImplementationKey);
this.values = await this.ToolSettingsService.GetSettingsAsync(this.toolDefinition);
this.fieldGroups = BuildFieldGroups(this.toolDefinition);
}
}
private string GetValue(string fieldName) => this.values.GetValueOrDefault(fieldName, string.Empty);
/// <summary>
/// Splits the tool's settings fields into the groups the tool declared for them.
/// </summary>
/// <remarks>
/// Groups appear in the order in which their first field appears in the schema, and the
/// fields keep the order the tool wrote them in. That is the order the fields have always
/// been rendered in, so a tool without groups looks exactly as it did before: one group
/// with an empty name, holding everything.<br/><br/>
/// A schema does not change while the dialog is open, so this runs once rather than on
/// every render.
/// </remarks>
private static IReadOnlyList<FieldGroup> BuildFieldGroups(ToolDefinition definition)
{
var groups = new List<FieldGroup>();
var groupIndexByKey = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var property in definition.SettingsSchema.Properties)
{
if (!groupIndexByKey.TryGetValue(property.Value.Group, out var groupIndex))
{
groupIndex = groups.Count;
groupIndexByKey[property.Value.Group] = groupIndex;
groups.Add(new FieldGroup(property.Value.Group, []));
}
groups[groupIndex].Fields.Add(property);
}
return groups;
}
/// <summary>
/// The groups as they are rendered right now, without the fields the tool is hiding.
/// </summary>
/// <remarks>
/// Which fields make sense can depend on what is filled in, so this is built on every
/// render rather than once: a field the tool starts to offer has to appear as soon as the
/// value it depends on changes. A group whose every field is hidden is left out entirely,
/// so no empty box is rendered.<br/><br/>
/// Cheap enough to be called more than once per render: a tool has a handful of settings,
/// and asking the tool about one of them costs a dictionary lookup or two.
/// </remarks>
private IReadOnlyList<FieldGroup> BuildVisibleFieldGroups()
{
if (this.implementation is null)
return this.fieldGroups;
var visibleGroups = new List<FieldGroup>();
foreach (var group in this.fieldGroups)
{
var visibleFields = group.Fields.Where(field => this.implementation.IsSettingsFieldVisible(field.Key, this.values)).ToList();
if (visibleFields.Count > 0)
visibleGroups.Add(new FieldGroup(group.Key, visibleFields));
}
return visibleGroups;
}
/// <summary>
/// Whether one group shows a heading above its fields.
/// </summary>
/// <remarks>
/// A tool that declares no groups has a single nameless group holding everything, and a
/// heading above the only box would say nothing the dialog's title does not say already.
/// As soon as there is a second box, each of them has to state which one it is — the box
/// holding the fields that belong to no group in particular included.<br/><br/>
/// It counts the boxes that are actually rendered, so a group the tool hides entirely does
/// not leave the remaining box with a heading it does not need.
/// </remarks>
private bool ShowsGroupHeader(FieldGroup group) => this.BuildVisibleFieldGroups().Count > 1 || !string.IsNullOrEmpty(group.Key);
/// <remarks>
/// The ungrouped fields have no name of their own, so the label hook hands back their
/// empty group name. A tool may still name them through that same hook; when it does not,
/// they are simply what is left over next to the named groups.
/// </remarks>
private string GetGroupLabel(string groupKey)
{
var label = this.implementation?.GetSettingsGroupLabel(groupKey) ?? groupKey;
return string.IsNullOrEmpty(label) ? T("General") : label;
}
private IReadOnlyList<ToolSettingsGroupLink> GetGroupLinks(string groupKey) => this.implementation?.GetSettingsGroupLinks(groupKey) ?? [];
/// <summary>
/// What the tool wants to say about the settings as they stand right now.
/// </summary>
/// <remarks>
/// Asked on every render, so a warning follows the value it is about instead of waiting for
/// the next save. These are not errors: they describe settings that are allowed and do
/// something other than what they look like, and the dialog saves them either way.
/// </remarks>
private IReadOnlyList<string> GetSettingsWarnings() => this.implementation?.GetSettingsWarnings(this.values) ?? [];
private string GetFieldLabel(string fieldName, ToolSettingsFieldDefinition fieldDefinition) =>
this.implementation?.GetSettingsFieldLabel(fieldName, fieldDefinition) ?? fieldDefinition.Title;
@@ -81,4 +176,8 @@ public partial class ToolSettingsDialog : SettingsDialogBase
await this.ToolSettingsService.SaveSettingsAsync(this.toolDefinition, this.values);
this.MudDialog.Close();
}
/// <param name="Key">The group's name from the schema, or empty for the ungrouped fields.</param>
/// <param name="Fields">The fields of this group, in the order the tool declared them.</param>
private sealed record FieldGroup(string Key, List<KeyValuePair<string, ToolSettingsFieldDefinition>> Fields);
}