2026-03-19 23:49:21 +00:00
using System.Text ;
2026-02-24 10:31:16 +00:00
using AIStudio.Dialogs.Settings ;
2026-02-23 14:01:00 +00:00
using AIStudio.Settings ;
2026-03-19 23:49:21 +00:00
using AIStudio.Tools.PluginSystem ;
2025-09-30 19:53:56 +00:00
using AIStudio.Tools.PluginSystem.Assistants ;
using AIStudio.Tools.PluginSystem.Assistants.DataModel ;
2026-03-02 14:24:18 +00:00
using Lua ;
2025-09-30 19:53:56 +00:00
using Microsoft.AspNetCore.Components ;
2026-02-24 12:20:46 +00:00
using Microsoft.AspNetCore.WebUtilities ;
2025-09-30 19:53:56 +00:00
namespace AIStudio.Assistants.Dynamic ;
public partial class AssistantDynamic : AssistantBaseCore < SettingsDialogDynamic >
{
[Parameter]
public AssistantForm ? RootComponent { get ; set ; } = null ! ;
2025-11-10 16:01:49 +00:00
protected override string Title = > this . title ;
protected override string Description = > this . description ;
protected override string SystemPrompt = > this . systemPrompt ;
protected override bool AllowProfiles = > this . allowProfiles ;
2026-02-23 14:01:00 +00:00
protected override bool ShowProfileSelection = > this . showFooterProfileSelection ;
2025-11-10 16:01:49 +00:00
protected override string SubmitText = > this . submitText ;
protected override Func < Task > SubmitAction = > this . Submit ;
2026-03-03 19:43:14 +00:00
// Dynamic assistants do not have dedicated settings yet.
// Reuse chat-level provider filtering/preselection instead of NONE.
protected override Tools . Components Component = > Tools . Components . CHAT ;
2025-09-30 19:53:56 +00:00
private string title = string . Empty ;
private string description = string . Empty ;
private string systemPrompt = string . Empty ;
private bool allowProfiles = true ;
2025-11-10 16:01:49 +00:00
private string submitText = string . Empty ;
2026-02-23 14:01:00 +00:00
private bool showFooterProfileSelection = true ;
2026-03-02 14:24:18 +00:00
private PluginAssistants ? assistantPlugin ;
2025-11-10 16:01:49 +00:00
2026-03-19 23:49:21 +00:00
private readonly AssistantState assistantState = new ( ) ;
2026-02-24 10:31:16 +00:00
private readonly Dictionary < string , string > imageCache = new ( ) ;
2026-03-10 14:43:40 +00:00
private readonly HashSet < string > executingButtonActions = [ ] ;
2026-03-16 13:15:29 +00:00
private readonly HashSet < string > executingSwitchActions = [ ] ;
2026-02-24 10:31:16 +00:00
private string pluginPath = string . Empty ;
2026-02-24 12:20:46 +00:00
private const string ASSISTANT_QUERY_KEY = "assistantId" ;
2026-03-22 19:41:30 +00:00
#region Implementation of AssistantBase
2025-09-30 19:53:56 +00:00
protected override void OnInitialized ( )
{
2026-03-22 19:41:30 +00:00
var pluginAssistant = this . ResolveAssistantPlugin ( ) ;
if ( pluginAssistant is null )
2025-09-30 19:53:56 +00:00
{
2026-02-24 12:20:46 +00:00
this . Logger . LogWarning ( "AssistantDynamic could not resolve a registered assistant plugin." ) ;
base . OnInitialized ( ) ;
return ;
2025-09-30 19:53:56 +00:00
}
2025-11-10 16:01:49 +00:00
2026-03-22 19:41:30 +00:00
this . assistantPlugin = pluginAssistant ;
this . RootComponent = pluginAssistant . RootComponent ;
this . title = pluginAssistant . AssistantTitle ;
this . description = pluginAssistant . AssistantDescription ;
this . systemPrompt = pluginAssistant . SystemPrompt ;
this . submitText = pluginAssistant . SubmitText ;
this . allowProfiles = pluginAssistant . AllowProfiles ;
this . showFooterProfileSelection = ! pluginAssistant . HasEmbeddedProfileSelection ;
this . pluginPath = pluginAssistant . PluginPath ;
2026-02-24 12:20:46 +00:00
var rootComponent = this . RootComponent ;
if ( rootComponent is not null )
2025-11-10 16:01:49 +00:00
{
2026-03-10 17:57:48 +00:00
this . InitializeComponentState ( rootComponent . Children ) ;
2025-11-10 16:01:49 +00:00
}
2026-02-24 12:20:46 +00:00
2025-09-30 19:53:56 +00:00
base . OnInitialized ( ) ;
}
2026-03-22 19:41:30 +00:00
protected override void ResetForm ( )
{
this . assistantState . Clear ( ) ;
var rootComponent = this . RootComponent ;
if ( rootComponent is not null )
this . InitializeComponentState ( rootComponent . Children ) ;
}
protected override bool MightPreselectValues ( )
{
// Dynamic assistants have arbitrary fields supplied via plugins, so there
// isn't a built-in settings section to prefill values. Always return
// false to keep the plugin-specified defaults.
return false ;
}
#endregion
#region Implementation of dynamic plugin init
2025-09-30 19:53:56 +00:00
2026-02-24 12:20:46 +00:00
private PluginAssistants ? ResolveAssistantPlugin ( )
{
2026-03-22 19:41:30 +00:00
var pluginAssistants = PluginFactory . RunningPlugins . OfType < PluginAssistants > ( )
2026-03-16 14:44:15 +00:00
. Where ( plugin = > this . SettingsManager . IsPluginEnabled ( plugin ) )
. ToList ( ) ;
2026-03-22 19:41:30 +00:00
if ( pluginAssistants . Count = = 0 )
2026-02-24 12:20:46 +00:00
return null ;
var requestedPluginId = this . TryGetAssistantIdFromQuery ( ) ;
2026-03-22 19:41:30 +00:00
if ( requestedPluginId is not { } id ) return pluginAssistants . First ( ) ;
2026-02-24 12:20:46 +00:00
2026-03-22 19:41:30 +00:00
var requestedPlugin = pluginAssistants . FirstOrDefault ( p = > p . Id = = id ) ;
return requestedPlugin ? ? pluginAssistants . First ( ) ;
2026-02-24 12:20:46 +00:00
}
private Guid ? TryGetAssistantIdFromQuery ( )
{
var uri = this . NavigationManager . ToAbsoluteUri ( this . NavigationManager . Uri ) ;
if ( string . IsNullOrWhiteSpace ( uri . Query ) )
return null ;
var query = QueryHelpers . ParseQuery ( uri . Query ) ;
if ( ! query . TryGetValue ( ASSISTANT_QUERY_KEY , out var values ) )
return null ;
var value = values . FirstOrDefault ( ) ;
if ( string . IsNullOrWhiteSpace ( value ) )
return null ;
if ( Guid . TryParse ( value , out var assistantId ) )
return assistantId ;
this . Logger . LogWarning ( "AssistantDynamic query parameter '{Parameter}' is not a valid GUID." , value ) ;
return null ;
}
2026-03-22 19:41:30 +00:00
#endregion
2025-09-30 19:53:56 +00:00
2026-02-24 10:31:16 +00:00
private string ResolveImageSource ( AssistantImage image )
{
if ( string . IsNullOrWhiteSpace ( image . Src ) )
return string . Empty ;
if ( this . imageCache . TryGetValue ( image . Src , out var cached ) & & ! string . IsNullOrWhiteSpace ( cached ) )
return cached ;
2026-03-22 19:41:30 +00:00
var resolved = image . ResolveSource ( this . pluginPath ) ;
2026-02-24 10:31:16 +00:00
this . imageCache [ image . Src ] = resolved ;
return resolved ;
}
2026-03-02 14:24:18 +00:00
private async Task < string > CollectUserPromptAsync ( )
{
if ( this . assistantPlugin ? . HasCustomPromptBuilder ! = true ) return this . CollectUserPromptFallback ( ) ;
var input = this . BuildPromptInput ( ) ;
var prompt = await this . assistantPlugin . TryBuildPromptAsync ( input , this . cancellationTokenSource ? . Token ? ? CancellationToken . None ) ;
return ! string . IsNullOrWhiteSpace ( prompt ) ? prompt : this . CollectUserPromptFallback ( ) ;
}
private LuaTable BuildPromptInput ( )
{
2026-03-21 01:03:05 +00:00
var state = new LuaTable ( ) ;
2026-03-02 14:24:18 +00:00
var rootComponent = this . RootComponent ;
2026-03-21 01:03:05 +00:00
state = rootComponent is not null
2026-03-19 23:49:21 +00:00
? this . assistantState . ToLuaTable ( rootComponent . Children )
: new LuaTable ( ) ;
2026-03-02 14:24:18 +00:00
var profile = new LuaTable
{
["Name"] = this . currentProfile . Name ,
["NeedToKnow"] = this . currentProfile . NeedToKnow ,
["Actions"] = this . currentProfile . Actions ,
["Num"] = this . currentProfile . Num ,
} ;
2026-03-21 01:03:05 +00:00
state [ "profile" ] = profile ;
2026-03-02 14:24:18 +00:00
2026-03-21 01:03:05 +00:00
return state ;
2026-03-02 14:24:18 +00:00
}
private string CollectUserPromptFallback ( )
2025-11-10 16:01:49 +00:00
{
var prompt = string . Empty ;
2026-02-24 12:20:46 +00:00
var rootComponent = this . RootComponent ;
2026-03-22 19:41:30 +00:00
return rootComponent is null ? prompt : this . CollectUserPromptFallback ( rootComponent . Children ) ;
2026-03-10 17:57:48 +00:00
}
private void InitializeComponentState ( IEnumerable < IAssistantComponent > components )
{
foreach ( var component in components )
2025-11-10 16:01:49 +00:00
{
2026-03-19 23:49:21 +00:00
if ( component is IStatefulAssistantComponent statefulComponent )
statefulComponent . InitializeState ( this . assistantState ) ;
2025-11-11 14:57:15 +00:00
2026-03-10 17:57:48 +00:00
if ( component . Children . Count > 0 )
this . InitializeComponentState ( component . Children ) ;
}
2025-11-10 16:01:49 +00:00
}
2026-02-24 16:21:50 +00:00
private static string MergeClass ( string customClass , string fallback )
{
2026-03-22 19:41:30 +00:00
var trimmedCustom = customClass . Trim ( ) ;
var trimmedFallback = fallback . Trim ( ) ;
2026-02-24 16:21:50 +00:00
if ( string . IsNullOrEmpty ( trimmedCustom ) )
return trimmedFallback ;
2026-03-22 19:41:30 +00:00
return string . IsNullOrEmpty ( trimmedFallback ) ? trimmedCustom : $"{trimmedCustom} {trimmedFallback}" ;
2026-02-24 16:21:50 +00:00
}
private string? GetOptionalStyle ( string? style ) = > string . IsNullOrWhiteSpace ( style ) ? null : style ;
2026-03-09 12:23:35 +00:00
2026-03-10 14:43:40 +00:00
private bool IsButtonActionRunning ( string buttonName ) = > this . executingButtonActions . Contains ( buttonName ) ;
2026-03-16 13:15:29 +00:00
private bool IsSwitchActionRunning ( string switchName ) = > this . executingSwitchActions . Contains ( switchName ) ;
2026-03-10 14:43:40 +00:00
private async Task ExecuteButtonActionAsync ( AssistantButton button )
{
if ( this . assistantPlugin is null | | button . Action is null | | string . IsNullOrWhiteSpace ( button . Name ) )
return ;
if ( ! this . executingButtonActions . Add ( button . Name ) )
return ;
try
{
var input = this . BuildPromptInput ( ) ;
var cancellationToken = this . cancellationTokenSource ? . Token ? ? CancellationToken . None ;
var result = await this . assistantPlugin . TryInvokeButtonActionAsync ( button , input , cancellationToken ) ;
if ( result is not null )
2026-03-16 13:15:29 +00:00
this . ApplyActionResult ( result , AssistantComponentType . BUTTON ) ;
2026-03-10 14:43:40 +00:00
}
finally
{
this . executingButtonActions . Remove ( button . Name ) ;
await this . InvokeAsync ( this . StateHasChanged ) ;
}
}
2026-03-16 13:15:29 +00:00
private async Task ExecuteSwitchChangedAsync ( AssistantSwitch switchComponent , bool value )
{
if ( string . IsNullOrWhiteSpace ( switchComponent . Name ) )
return ;
2026-03-19 23:49:21 +00:00
this . assistantState . Bools [ switchComponent . Name ] = value ;
2026-03-16 13:15:29 +00:00
if ( this . assistantPlugin is null | | switchComponent . OnChanged is null )
{
await this . InvokeAsync ( this . StateHasChanged ) ;
return ;
}
if ( ! this . executingSwitchActions . Add ( switchComponent . Name ) )
return ;
try
{
var input = this . BuildPromptInput ( ) ;
var cancellationToken = this . cancellationTokenSource ? . Token ? ? CancellationToken . None ;
var result = await this . assistantPlugin . TryInvokeSwitchChangedAsync ( switchComponent , input , cancellationToken ) ;
if ( result is not null )
this . ApplyActionResult ( result , AssistantComponentType . SWITCH ) ;
}
finally
{
this . executingSwitchActions . Remove ( switchComponent . Name ) ;
await this . InvokeAsync ( this . StateHasChanged ) ;
}
}
private void ApplyActionResult ( LuaTable result , AssistantComponentType sourceType )
2026-03-10 14:43:40 +00:00
{
2026-03-21 01:03:05 +00:00
if ( ! result . TryGetValue ( "state" , out var statesValue ) )
2026-03-10 14:43:40 +00:00
return ;
2026-03-21 01:03:05 +00:00
if ( ! statesValue . TryRead < LuaTable > ( out var stateTable ) )
2026-03-10 14:43:40 +00:00
{
2026-03-21 01:03:05 +00:00
this . Logger . LogWarning ( $"Assistant {sourceType} callback returned a non-table 'state' value. The result is ignored." ) ;
2026-03-10 14:43:40 +00:00
return ;
}
2026-03-21 01:03:05 +00:00
foreach ( var component in stateTable )
2026-03-10 14:43:40 +00:00
{
2026-03-21 01:03:05 +00:00
if ( ! component . Key . TryRead < string > ( out var componentName ) | | string . IsNullOrWhiteSpace ( componentName ) )
2026-03-10 14:43:40 +00:00
continue ;
2026-03-21 01:03:05 +00:00
if ( ! component . Value . TryRead < LuaTable > ( out var componentUpdate ) )
{
this . Logger . LogWarning ( $"Assistant {sourceType} callback returned a non-table update for '{componentName}'. The result is ignored." ) ;
continue ;
}
this . TryApplyComponentUpdate ( componentName , componentUpdate , sourceType ) ;
2026-03-10 14:43:40 +00:00
}
}
2026-03-21 01:03:05 +00:00
private void TryApplyComponentUpdate ( string componentName , LuaTable componentUpdate , AssistantComponentType sourceType )
{
if ( componentUpdate . TryGetValue ( "Value" , out var value ) )
this . TryApplyFieldUpdate ( componentName , value , sourceType ) ;
if ( ! componentUpdate . TryGetValue ( "Props" , out var propsValue ) )
return ;
if ( ! propsValue . TryRead < LuaTable > ( out var propsTable ) )
{
this . Logger . LogWarning ( $"Assistant {sourceType} callback returned a non-table 'Props' value for '{componentName}'. The props update is ignored." ) ;
return ;
}
var rootComponent = this . RootComponent ;
if ( rootComponent is null | | ! TryFindNamedComponent ( rootComponent . Children , componentName , out var component ) )
{
this . Logger . LogWarning ( $"Assistant {sourceType} callback tried to update props of unknown component '{componentName}'. The props update is ignored." ) ;
return ;
}
this . ApplyPropUpdates ( component , propsTable , sourceType ) ;
}
2026-03-16 13:15:29 +00:00
private void TryApplyFieldUpdate ( string fieldName , LuaValue value , AssistantComponentType sourceType )
2026-03-10 14:43:40 +00:00
{
2026-03-19 23:49:21 +00:00
if ( this . assistantState . TryApplyValue ( fieldName , value , out var expectedType ) )
2026-03-10 14:43:40 +00:00
return ;
2026-03-19 23:49:21 +00:00
if ( ! string . IsNullOrWhiteSpace ( expectedType ) )
2026-03-10 14:43:40 +00:00
{
2026-03-19 23:49:21 +00:00
this . Logger . LogWarning ( $"Assistant {sourceType} callback tried to write an invalid value to '{fieldName}'. Expected {expectedType}." ) ;
2026-03-16 19:06:50 +00:00
return ;
}
2026-03-19 23:49:21 +00:00
this . Logger . LogWarning ( $"Assistant {sourceType} callback tried to update unknown field '{fieldName}'. The value is ignored." ) ;
2026-03-10 14:43:40 +00:00
}
2026-03-21 01:03:05 +00:00
private void ApplyPropUpdates ( IAssistantComponent component , LuaTable propsTable , AssistantComponentType sourceType )
{
var propSpec = ComponentPropSpecs . SPECS . GetValueOrDefault ( component . Type ) ;
foreach ( var prop in propsTable )
{
if ( ! prop . Key . TryRead < string > ( out var propName ) | | string . IsNullOrWhiteSpace ( propName ) )
continue ;
if ( propSpec is not null & & propSpec . NonWriteable . Contains ( propName , StringComparer . Ordinal ) )
{
this . Logger . LogWarning ( $"Assistant {sourceType} callback tried to update non-writeable prop '{propName}' on component '{GetComponentName(component)}'. The value is ignored." ) ;
continue ;
}
if ( ! AssistantLuaConversion . TryReadScalarOrStructuredValue ( prop . Value , out var convertedValue ) )
{
this . Logger . LogWarning ( $"Assistant {sourceType} callback returned an unsupported value for prop '{propName}' on component '{GetComponentName(component)}'. The props update is ignored." ) ;
continue ;
}
component . Props [ propName ] = convertedValue ;
}
}
private static bool TryFindNamedComponent ( IEnumerable < IAssistantComponent > components , string componentName , out IAssistantComponent component )
{
foreach ( var candidate in components )
{
if ( candidate is INamedAssistantComponent named & & string . Equals ( named . Name , componentName , StringComparison . Ordinal ) )
{
component = candidate ;
return true ;
}
if ( candidate . Children . Count > 0 & & TryFindNamedComponent ( candidate . Children , componentName , out component ) )
return true ;
}
component = null ! ;
return false ;
}
private static string GetComponentName ( IAssistantComponent component ) = > component is INamedAssistantComponent named ? named . Name : component . Type . ToString ( ) ;
2026-03-13 00:51:48 +00:00
private EventCallback < HashSet < string > > CreateMultiselectDropdownChangedCallback ( string fieldName ) = >
EventCallback . Factory . Create < HashSet < string > > ( this , values = >
{
2026-03-19 23:49:21 +00:00
this . assistantState . MultiSelect [ fieldName ] = values ;
2026-03-13 00:51:48 +00:00
} ) ;
2026-03-09 12:23:35 +00:00
private string? ValidateProfileSelection ( AssistantProfileSelection profileSelection , Profile ? profile )
2026-02-23 14:01:00 +00:00
{
2026-03-22 19:41:30 +00:00
if ( profile ! = null & & profile ! = Profile . NO_PROFILE ) return null ;
2026-03-19 23:49:21 +00:00
return ! string . IsNullOrWhiteSpace ( profileSelection . ValidationMessage ) ? profileSelection . ValidationMessage : this . T ( "Please select one of your profiles." ) ;
2026-02-23 14:01:00 +00:00
}
2025-11-10 16:01:49 +00:00
private async Task Submit ( )
{
this . CreateChatThread ( ) ;
2026-03-02 14:24:18 +00:00
var time = this . AddUserRequest ( await this . CollectUserPromptAsync ( ) ) ;
2025-11-10 16:01:49 +00:00
await this . AddAIResponseAsync ( time ) ;
}
2026-03-10 17:57:48 +00:00
private string CollectUserPromptFallback ( IEnumerable < IAssistantComponent > components )
{
2026-03-19 23:49:21 +00:00
var prompt = new StringBuilder ( ) ;
2026-03-10 17:57:48 +00:00
foreach ( var component in components )
{
2026-03-19 23:49:21 +00:00
if ( component is IStatefulAssistantComponent statefulComponent )
prompt . Append ( statefulComponent . UserPromptFallback ( this . assistantState ) ) ;
2026-03-10 17:57:48 +00:00
2026-03-19 23:49:21 +00:00
if ( component . Children . Count > 0 )
2026-03-10 17:57:48 +00:00
{
2026-03-19 23:49:21 +00:00
prompt . Append ( this . CollectUserPromptFallback ( component . Children ) ) ;
2026-03-10 17:57:48 +00:00
}
2026-03-13 00:51:48 +00:00
}
2026-03-19 23:49:21 +00:00
return prompt . ToString ( ) ;
2026-03-13 00:51:48 +00:00
}
2026-02-23 14:01:00 +00:00
}