2026-09-04 15:48:07 +02:00
using AIStudio.Dialogs.Settings ;
using AIStudio.Provider ;
using AIStudio.Tools.ToolCallingSystem ;
using Microsoft.AspNetCore.Components ;
namespace AIStudio.Components ;
public partial class ToolSelection : MSGComponentBase
{
[Parameter]
public AIStudio . Tools . Components Component { get ; set ; } = AIStudio . Tools . Components . CHAT ;
[Parameter]
public required AIStudio . Settings . Provider LLMProvider { get ; set ; }
[Parameter]
public HashSet < string > SelectedToolIds { get ; set ; } = [];
[Parameter]
public EventCallback < HashSet < string >> SelectedToolIdsChanged { get ; set ; }
[Parameter]
public bool Disabled { get ; set ; }
[Parameter]
public string PopoverButtonClasses { get ; set ; } = string . Empty ;
[Inject]
private ToolRegistry ToolRegistry { get ; init ; } = null !;
[Inject]
private IDialogService DialogService { get ; init ; } = null !;
private bool showSelection ;
private IReadOnlyList < ToolCatalogItem > catalog = [];
protected override void OnParametersSet ()
{
this . SelectedToolIds = ToolSelectionRules . NormalizeSelection ( this . SelectedToolIds );
base . OnParametersSet ();
}
protected override async Task OnInitializedAsync ()
{
this . ApplyFilters ([], [ Event . CONFIGURATION_CHANGED ]);
await base . OnInitializedAsync ();
}
private ToolCallingAvailability ToolCallingAvailability => this . LLMProvider . GetToolCallingAvailability ();
private bool SupportsTools => this . ToolCallingAvailability . IsAvailable ;
private string ToolButtonTooltip => this . SupportsTools
? this . T ( "Select tools" )
: this . UnsupportedToolsMessage ;
private string UnsupportedToolsMessage => this . ToolCallingAvailability . Message ;
private ConfidenceLevel ProviderConfidence => this . LLMProvider == AIStudio . Settings . Provider . NONE
? ConfidenceLevel . NONE
: this . LLMProvider . UsedLLMProvider . GetConfidence ( this . SettingsManager ). Level ;
private async Task ToggleSelection ()
{
this . showSelection = ! this . showSelection ;
if ( this . showSelection )
this . catalog = await this . ToolRegistry . GetCatalogAsync ( this . Component );
}
private void Hide () => this . showSelection = false ;
/// <summary>
/// Whether this tool can be switched at all right now.
/// </summary>
/// <remarks>
/// The switch and the row click share this, so both agree on when a tool is out of reach: the
/// organization disabled it, it is not configured, the provider lacks the confidence it needs,
/// a response is running, or the model cannot call tools in the first place.
/// </remarks>
private bool IsRowDisabled ( ToolCatalogItem item ) => ! item . IsActive || ! item . ConfigurationState . IsConfigured || this . IsBlockedByProviderConfidence ( item ) ||
this . Disabled || ! this . SupportsTools ;
/// <summary>
/// Switches a tool when the user clicks anywhere in its row.
/// </summary>
/// <remarks>
/// Hitting the switch itself is needless precision work, so the text, the icon, and the empty
/// space count as well. Only the settings button is left out, because it sits outside the
/// button that spans the rest of the row.
/// </remarks>
private async Task ToggleToolFromRow ( ToolCatalogItem item )
{
if ( this . IsRowDisabled ( item ))
return ;
await this . ChangeSelection ( item . Definition . Id , ! this . SelectedToolIds . Contains ( item . Definition . Id ));
}
private async Task ChangeSelection ( string toolId , bool isSelected )
{
if ( isSelected && ! this . SettingsManager . IsToolActive ( toolId ))
return ;
var updated = new HashSet < string >( this . SelectedToolIds , StringComparer . Ordinal );
if ( isSelected )
updated . Add ( toolId );
else
updated . Remove ( toolId );
updated = ToolSelectionRules . NormalizeSelection ( updated );
this . SelectedToolIds = updated ;
await this . SelectedToolIdsChanged . InvokeAsync ( updated );
}
// The catalog already carries the resolved level, so there is nothing to look up again:
private static ConfidenceLevel GetMinimumProviderConfidence ( ToolCatalogItem item ) => item . MinimumProviderConfidence ;
private bool IsBlockedByProviderConfidence ( ToolCatalogItem item ) => ! ToolSelectionRules . IsProviderConfidenceAllowed ( this . ProviderConfidence , GetMinimumProviderConfidence ( item ));
private string? GetProviderConfidenceHint ( ToolCatalogItem item )
{
if (! this . IsBlockedByProviderConfidence ( item ))
return null ;
return string . Format (
this . T ( "This tool requires provider confidence {0}. The selected provider has {1}." ),
GetMinimumProviderConfidence ( item ). GetName (),
this . ProviderConfidence . GetName ());
}
2026-09-23 17:17:37 +02:00
/// <summary>
/// Every reason why this tool is out of reach right now, as one text.
/// </summary>
/// <remarks>
/// Several reasons can apply at once, such as a missing setting and a provider without the
/// confidence the tool needs. One justified paragraph reads better than a stack of short lines.
/// </remarks>
private string GetWarningText ( ToolCatalogItem item )
{
var warnings = new List < string >( 3 );
if (! item . ConfigurationState . IsConfigured )
warnings . Add ( string . IsNullOrWhiteSpace ( item . ConfigurationState . Message ) ? T ( "Required settings are missing. Configure this tool before enabling it." ) : item . ConfigurationState . Message );
if (! item . IsActive )
warnings . Add ( T ( "This tool has been disabled by your organization." ));
var providerConfidenceHint = this . GetProviderConfidenceHint ( item );
if (! string . IsNullOrWhiteSpace ( providerConfidenceHint ))
warnings . Add ( providerConfidenceHint );
return string . Join ( ' ' , warnings );
}
2026-09-04 15:48:07 +02:00
private async Task OpenSettings ( string toolId )
{
var parameters = new DialogParameters < ToolSettingsDialog >
{
{ x => x . ToolId , toolId },
};
var dialog = await this . DialogService . ShowAsync < ToolSettingsDialog >( null , parameters , Dialogs . DialogOptions . FULLSCREEN );
await dialog . Result ;
this . catalog = await this . ToolRegistry . GetCatalogAsync ( this . Component );
this . StateHasChanged ();
}
protected override async Task ProcessIncomingMessage < T >( ComponentBase ? sendingComponent , Event triggeredEvent , T ? data ) where T : default
{
switch ( triggeredEvent )
{
case Event . CONFIGURATION_CHANGED when this . showSelection :
this . catalog = await this . ToolRegistry . GetCatalogAsync ( this . Component );
await this . InvokeAsync ( this . StateHasChanged );
break ;
}
}
}