2024-07-14 19:46:17 +00:00
using AIStudio.Chat ;
using AIStudio.Provider ;
using AIStudio.Settings ;
2026-02-01 13:50:19 +00:00
using AIStudio.Dialogs.Settings ;
2026-07-03 13:02:20 +00:00
using AIStudio.Tools.AIJobs ;
using AIStudio.Tools.AssistantSessions ;
2025-02-15 14:41:12 +00:00
using AIStudio.Tools.Services ;
2024-07-14 19:46:17 +00:00
using Microsoft.AspNetCore.Components ;
2025-01-01 14:49:27 +00:00
using MudBlazor.Utilities ;
using Timer = System . Timers . Timer ;
2024-09-01 18:10:03 +00:00
2025-03-12 10:31:01 +00:00
using DialogOptions = AIStudio . Dialogs . DialogOptions ;
2024-08-21 06:30:01 +00:00
namespace AIStudio.Assistants ;
2024-07-14 19:46:17 +00:00
2025-04-27 07:06:05 +00:00
public abstract partial class AssistantBase < TSettings > : AssistantLowerBase where TSettings : IComponent
2024-07-14 19:46:17 +00:00
{
2025-03-12 10:31:01 +00:00
[Inject]
private IDialogService DialogService { get ; init ; } = null ! ;
2024-07-14 19:46:17 +00:00
[Inject]
protected IJSRuntime JsRuntime { get ; init ; } = null ! ;
2024-08-13 06:57:58 +00:00
[Inject]
protected ISnackbar Snackbar { get ; init ; } = null ! ;
[Inject]
2024-09-01 18:10:03 +00:00
protected RustService RustService { get ; init ; } = null ! ;
2024-08-13 06:57:58 +00:00
2024-08-18 10:32:18 +00:00
[Inject]
protected NavigationManager NavigationManager { get ; init ; } = null ! ;
2024-09-01 18:10:03 +00:00
[Inject]
2025-03-12 10:31:01 +00:00
protected ILogger < AssistantBase < TSettings > > Logger { get ; init ; } = null ! ;
2024-09-01 18:10:03 +00:00
2024-09-15 10:30:07 +00:00
[Inject]
private MudTheme ColorTheme { get ; init ; } = null ! ;
2026-07-03 13:02:20 +00:00
[Inject]
protected AssistantSessionService AssistantSessionService { get ; init ; } = null ! ;
/// <summary>
/// Gets the job service used to run assistant-created chats independently from the assistant UI.
/// </summary>
[Inject]
protected AIJobService AIJobService { get ; init ; } = null ! ;
2024-09-15 10:30:07 +00:00
2024-07-14 19:46:17 +00:00
protected abstract string Title { get ; }
protected abstract string Description { get ; }
protected abstract string SystemPrompt { get ; }
2026-02-01 13:50:19 +00:00
protected abstract Tools . Components Component { get ; }
2024-09-04 13:44:23 +00:00
2026-07-03 13:02:20 +00:00
protected virtual Func < string > Result2Copy = > ( ) = > this . ResultingContentBlock is null ? string . Empty : this . ResultingContentBlock . Content switch
2024-08-23 06:59:30 +00:00
{
ContentText textBlock = > textBlock . Text ,
_ = > string . Empty ,
} ;
2024-08-18 19:48:35 +00:00
2025-01-01 14:49:27 +00:00
protected abstract void ResetForm ( ) ;
2024-08-18 19:48:35 +00:00
protected abstract bool MightPreselectValues ( ) ;
2024-07-14 19:46:17 +00:00
2024-09-11 21:08:02 +00:00
protected abstract string SubmitText { get ; }
protected abstract Func < Task > SubmitAction { get ; }
2025-03-12 10:31:01 +00:00
2024-09-11 21:08:02 +00:00
protected virtual bool SubmitDisabled = > false ;
2024-07-14 19:46:17 +00:00
private protected virtual RenderFragment ? Body = > null ;
2026-07-05 15:48:00 +00:00
private protected virtual RenderFragment ? AfterSubmitContent = > null ;
private protected virtual RenderFragment ? BelowSubmitContent = > null ;
2024-08-13 06:57:58 +00:00
protected virtual bool ShowResult = > true ;
2025-01-01 14:49:27 +00:00
protected virtual bool ShowEntireChatThread = > false ;
2024-09-08 19:01:51 +00:00
protected virtual bool AllowProfiles = > true ;
2024-09-09 13:08:16 +00:00
protected virtual bool ShowProfileSelection = > true ;
2024-08-13 18:53:09 +00:00
protected virtual bool ShowDedicatedProgress = > false ;
2024-10-28 14:41:00 +00:00
protected virtual bool ShowSendTo = > true ;
protected virtual bool ShowCopyResult = > true ;
protected virtual bool ShowReset = > true ;
2024-08-13 06:57:58 +00:00
2026-04-16 14:12:45 +00:00
protected virtual string? SendToChatVisibleUserPromptPrefix = > null ;
protected virtual string? SendToChatVisibleUserPromptContent = > null ;
protected virtual string? SendToChatVisibleUserPromptText
{
get
{
if ( string . IsNullOrWhiteSpace ( this . SendToChatVisibleUserPromptPrefix ) )
return null ;
if ( string . IsNullOrWhiteSpace ( this . SendToChatVisibleUserPromptContent ) )
return this . SendToChatVisibleUserPromptPrefix ;
return $"" "
{ this . SendToChatVisibleUserPromptPrefix }
{ this . SendToChatVisibleUserPromptContent }
"" ";
}
}
protected virtual ChatThread ConvertToChatThread = > this . CreateSendToChatThread ( ) ;
2024-08-18 19:48:35 +00:00
2026-04-15 17:40:53 +00:00
private protected virtual RenderFragment ? HeaderActions = > null ;
private protected virtual RenderFragment ? AfterResultContent = > null ;
2024-08-18 10:32:18 +00:00
protected virtual IReadOnlyList < IButtonData > FooterButtons = > [ ] ;
2026-02-01 13:50:19 +00:00
protected virtual bool HasSettingsPanel = > typeof ( TSettings ) ! = typeof ( NoSettingsPanel ) ;
2024-07-16 08:28:13 +00:00
2026-07-03 13:02:20 +00:00
private readonly Timer formChangeTimer = new ( TimeSpan . FromSeconds ( 1.6 ) ) ;
2026-04-16 14:48:47 +00:00
protected MudForm ? Form ;
protected CancellationTokenSource ? CancellationTokenSource ;
2026-07-03 13:02:20 +00:00
private bool isDisposed ;
private AssistantSessionKey assistantSessionKey ;
private Guid ? assistantSessionId ;
private AssistantSessionSnapshot ? pendingRenderedAssistantSessionSnapshot ;
2025-04-26 16:55:23 +00:00
2026-07-03 13:02:20 +00:00
/// <summary>
/// Gets whether the Blazor component instance has already been disposed.
/// </summary>
protected bool IsAssistantComponentDisposed = > this . isDisposed ;
/// <summary>
/// Gets whether this component has attached an assistant session snapshot.
/// </summary>
protected bool HasAssistantSession = > this . assistantSessionId is not null ;
/// <summary>
/// Gets the assistant-specific identifier used to distinguish session slots.
/// </summary>
protected virtual string AssistantSessionInstanceId = > this . GetType ( ) . FullName ? ? this . Component . ToString ( ) ;
2024-07-14 19:46:17 +00:00
#region Overrides of ComponentBase
2024-09-04 13:44:23 +00:00
protected override async Task OnInitializedAsync ( )
{
2025-04-27 14:13:15 +00:00
await base . OnInitializedAsync ( ) ;
2026-03-14 09:37:31 +00:00
if ( ! this . SettingsManager . IsAssistantVisible ( this . Component , assistantName : this . Title ) )
{
this . Logger . LogInformation ( "Assistant '{AssistantTitle}' is hidden. Redirecting to the assistants overview." , this . Title ) ;
this . NavigationManager . NavigateTo ( Routes . ASSISTANTS ) ;
return ;
}
2025-04-27 14:13:15 +00:00
2025-01-01 14:49:27 +00:00
this . formChangeTimer . AutoReset = false ;
this . formChangeTimer . Elapsed + = async ( _ , _ ) = >
{
this . formChangeTimer . Stop ( ) ;
await this . OnFormChange ( ) ;
} ;
2024-09-04 13:44:23 +00:00
this . MightPreselectValues ( ) ;
2026-04-16 14:48:47 +00:00
this . ProviderSettings = this . SettingsManager . GetPreselectedProvider ( this . Component ) ;
this . CurrentProfile = this . SettingsManager . GetPreselectedProfile ( this . Component ) ;
this . CurrentChatTemplate = this . SettingsManager . GetPreselectedChatTemplate ( this . Component ) ;
2026-07-03 13:02:20 +00:00
this . assistantSessionKey = new ( this . Component , this . AssistantSessionInstanceId ) ;
await this . AttachAssistantSessionIfAvailable ( ) ;
2024-09-04 13:44:23 +00:00
}
2024-07-15 14:56:28 +00:00
protected override async Task OnParametersSetAsync ( )
{
// Configure the spellchecking for the user input:
this . SettingsManager . InjectSpellchecking ( USER_INPUT_ATTRIBUTES ) ;
await base . OnParametersSetAsync ( ) ;
}
2024-07-14 19:46:17 +00:00
protected override async Task OnAfterRenderAsync ( bool firstRender )
{
// Reset the validation when not editing and on the first render.
// We don't want to show validation errors when the user opens the dialog.
if ( firstRender )
2026-04-16 14:48:47 +00:00
this . Form ? . ResetValidation ( ) ;
2026-07-03 13:02:20 +00:00
if ( this . pendingRenderedAssistantSessionSnapshot is { } snapshot )
{
this . pendingRenderedAssistantSessionSnapshot = null ;
await this . OnAssistantSessionRenderedAsync ( snapshot ) ;
}
2024-07-14 19:46:17 +00:00
await base . OnAfterRenderAsync ( firstRender ) ;
}
#endregion
2024-09-11 21:08:02 +00:00
2025-04-27 14:13:15 +00:00
private string TB ( string fallbackEN ) = > this . T ( fallbackEN , typeof ( AssistantBase < TSettings > ) . Namespace , nameof ( AssistantBase < TSettings > ) ) ;
2026-06-21 09:52:02 +00:00
private string SubmitButtonStyle = > this . SettingsManager . ConfigurationData . Confidence . ShowProviderConfidence ? this . ProviderSettings . UsedLLMProvider . GetConfidence ( this . SettingsManager ) . StyleBorder ( this . SettingsManager ) : string . Empty ;
2026-03-14 09:37:31 +00:00
private IReadOnlyList < Tools . Components > VisibleSendToAssistants = > Enum . GetValues < AIStudio . Tools . Components > ( )
. Where ( this . CanSendToAssistant )
. OrderBy ( component = > component . Name ( ) . Length )
. ToArray ( ) ;
2024-07-14 19:46:17 +00:00
2024-07-16 18:06:04 +00:00
protected string? ValidatingProvider ( AIStudio . Settings . Provider provider )
{
2024-09-13 19:50:00 +00:00
if ( provider . UsedLLMProvider = = LLMProviders . NONE )
2025-05-17 12:22:10 +00:00
return this . TB ( "Please select a provider." ) ;
2024-07-16 18:06:04 +00:00
return null ;
}
2025-01-01 14:49:27 +00:00
2025-04-26 16:55:23 +00:00
private async Task Start ( )
{
2026-07-03 13:02:20 +00:00
var activeSession = this . AssistantSessionService . TryGetSnapshot ( this . assistantSessionKey ) ;
if ( activeSession ? . IsActive ? ? false )
{
await this . AttachAssistantSession ( activeSession , restoreClientOnlyContent : true ) ;
return ;
}
this . CancellationTokenSource = new ( ) ;
this . IsProcessing = true ;
var startedSession = await this . AssistantSessionService . TryBeginAsync ( this . assistantSessionKey , this . Title , this . CancellationTokenSource , this . ChatThread , this . CaptureAssistantSessionState ( ) , this ) ;
if ( startedSession . IsActive is not true | | startedSession . Key ! = this . assistantSessionKey )
{
this . CancellationTokenSource . Dispose ( ) ;
this . CancellationTokenSource = null ;
return ;
}
this . assistantSessionId = startedSession . SessionId ;
await this . RefreshAssistantUIAsync ( ) ;
var sessionStatus = AssistantSessionStatus . COMPLETED ;
var errorMessage = string . Empty ;
try
2025-04-26 16:55:23 +00:00
{
await this . SubmitAction ( ) ;
2026-07-03 13:02:20 +00:00
if ( this . CancellationTokenSource ? . IsCancellationRequested ? ? false )
sessionStatus = AssistantSessionStatus . CANCELED ;
}
catch ( OperationCanceledException )
{
sessionStatus = AssistantSessionStatus . CANCELED ;
}
catch ( ProviderRequestException e )
{
sessionStatus = AssistantSessionStatus . FAILED ;
errorMessage = e . UserMessage ;
this . Logger . LogError ( e , "The provider request failed for assistant '{AssistantTitle}'. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'" , this . Title , e . StatusCode , e . ReasonPhrase , e . ResponseBody ) ;
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . CloudOff , e . UserMessage ) ) ;
}
catch ( Exception e )
{
sessionStatus = AssistantSessionStatus . FAILED ;
errorMessage = e . Message ;
this . Logger . LogError ( e , "The assistant session '{AssistantTitle}' failed." , this . Title ) ;
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . Stream , string . Format ( this . TB ( "The assistant failed. The message is: '{0}'" ) , e . Message ) ) ) ;
}
finally
{
this . IsProcessing = false ;
var sessionCancellationTokenSource = this . CancellationTokenSource ;
this . CancellationTokenSource = null ;
if ( this . assistantSessionId is { } sessionId )
{
await this . AssistantSessionService . CompleteAsync ( this . assistantSessionKey , sessionId , sessionStatus , errorMessage , this . ChatThread , this . CaptureAssistantSessionState ( ) , this ) ;
if ( ! this . isDisposed )
_ = this . AssistantSessionService . TryTakeInactiveSnapshot ( this . assistantSessionKey ) ;
}
sessionCancellationTokenSource ? . Dispose ( ) ;
await this . RefreshAssistantUIAsync ( ) ;
2025-04-26 16:55:23 +00:00
}
}
2025-01-01 14:49:27 +00:00
private void TriggerFormChange ( FormFieldChangedEventArgs _ )
{
this . formChangeTimer . Stop ( ) ;
this . formChangeTimer . Start ( ) ;
}
2024-07-16 18:06:04 +00:00
2025-01-01 14:49:27 +00:00
/// <summary>
/// This method is called after any form field has changed.
/// </summary>
/// <remarks>
/// This method is called after a delay of 1.6 seconds. This is to prevent
/// the method from being called too often. This method is called after
/// the user has stopped typing or selecting options.
/// </remarks>
protected virtual Task OnFormChange ( ) = > Task . CompletedTask ;
/// <summary>
/// Add an issue to the UI.
/// </summary>
/// <param name="issue">The issue to add.</param>
protected void AddInputIssue ( string issue )
{
2026-07-03 13:02:20 +00:00
Array . Resize ( ref this . InputIssues , this . InputIssues . Length + 1 ) ;
this . InputIssues [ ^ 1 ] = issue ;
2026-04-16 14:48:47 +00:00
this . InputIsValid = false ;
2026-07-03 13:02:20 +00:00
_ = this . RefreshAssistantUIAsync ( ) ;
2025-01-01 14:49:27 +00:00
}
2026-02-01 13:50:19 +00:00
/// <summary>
/// Clear all input issues.
/// </summary>
protected void ClearInputIssues ( )
{
2026-07-03 13:02:20 +00:00
this . InputIssues = [ ] ;
2026-04-16 14:48:47 +00:00
this . InputIsValid = true ;
2026-07-03 13:02:20 +00:00
_ = this . RefreshAssistantUIAsync ( ) ;
2026-02-01 13:50:19 +00:00
}
2025-01-01 14:49:27 +00:00
2024-07-14 19:46:17 +00:00
protected void CreateChatThread ( )
{
2026-04-16 14:48:47 +00:00
this . ChatThread = new ( )
2024-07-14 19:46:17 +00:00
{
2026-01-18 19:36:04 +00:00
IncludeDateTime = false ,
2026-04-16 14:48:47 +00:00
SelectedProvider = this . ProviderSettings . Id ,
SelectedProfile = this . AllowProfiles ? this . CurrentProfile . Id : Profile . NO_PROFILE . Id ,
2025-01-02 13:50:54 +00:00
SystemPrompt = this . SystemPrompt ,
2024-07-14 19:46:17 +00:00
WorkspaceId = Guid . Empty ,
ChatId = Guid . NewGuid ( ) ,
2025-05-17 12:22:10 +00:00
Name = string . Format ( this . TB ( "Assistant - {0}" ) , this . Title ) ,
2024-07-14 19:46:17 +00:00
Blocks = [ ] ,
} ;
}
2024-10-28 14:41:00 +00:00
protected Guid CreateChatThread ( Guid workspaceId , string name )
{
var chatId = Guid . NewGuid ( ) ;
2026-04-16 14:48:47 +00:00
this . ChatThread = new ( )
2024-10-28 14:41:00 +00:00
{
2026-01-18 19:36:04 +00:00
IncludeDateTime = false ,
2026-04-16 14:48:47 +00:00
SelectedProvider = this . ProviderSettings . Id ,
SelectedProfile = this . AllowProfiles ? this . CurrentProfile . Id : Profile . NO_PROFILE . Id ,
2025-01-02 13:50:54 +00:00
SystemPrompt = this . SystemPrompt ,
2024-10-28 14:41:00 +00:00
WorkspaceId = workspaceId ,
ChatId = chatId ,
Name = name ,
Blocks = [ ] ,
} ;
return chatId ;
}
2026-02-01 13:50:19 +00:00
protected virtual void ResetProviderAndProfileSelection ( )
{
2026-04-16 14:48:47 +00:00
this . ProviderSettings = this . SettingsManager . GetPreselectedProvider ( this . Component ) ;
this . CurrentProfile = this . SettingsManager . GetPreselectedProfile ( this . Component ) ;
this . CurrentChatTemplate = this . SettingsManager . GetPreselectedChatTemplate ( this . Component ) ;
2026-02-01 13:50:19 +00:00
}
2024-07-14 19:46:17 +00:00
2025-12-30 17:30:32 +00:00
protected DateTimeOffset AddUserRequest ( string request , bool hideContentFromUser = false , params List < FileAttachment > attachments )
2024-07-14 19:46:17 +00:00
{
var time = DateTimeOffset . Now ;
2026-04-16 14:48:47 +00:00
this . LastUserPrompt = new ContentText
2025-02-10 17:49:22 +00:00
{
Text = request ,
2025-12-30 17:30:32 +00:00
FileAttachments = attachments ,
2025-02-10 17:49:22 +00:00
} ;
2026-04-16 14:48:47 +00:00
this . ChatThread ! . Blocks . Add ( new ContentBlock
2024-07-14 19:46:17 +00:00
{
Time = time ,
ContentType = ContentType . TEXT ,
2024-10-28 14:41:00 +00:00
HideFromUser = hideContentFromUser ,
2024-07-14 19:46:17 +00:00
Role = ChatRole . USER ,
2026-04-16 14:48:47 +00:00
Content = this . LastUserPrompt ,
2024-07-14 19:46:17 +00:00
} ) ;
return time ;
}
2025-01-01 14:49:27 +00:00
protected async Task < string > AddAIResponseAsync ( DateTimeOffset time , bool hideContentFromUser = false )
2024-07-14 19:46:17 +00:00
{
2026-04-16 14:48:47 +00:00
var manageCancellationLocally = this . CancellationTokenSource is null ;
this . CancellationTokenSource ? ? = new CancellationTokenSource ( ) ;
2025-05-17 17:17:29 +00:00
2024-07-14 19:46:17 +00:00
var aiText = new ContentText
{
// We have to wait for the remote
// for the content stream:
InitialRemoteWait = true ,
} ;
2026-07-03 13:02:20 +00:00
aiText . StreamingEvent = async ( ) = >
{
await this . CheckpointAssistantSession ( ) ;
await this . RefreshAssistantUIAsync ( ) ;
} ;
aiText . StreamingDone = async ( ) = >
{
await this . CheckpointAssistantSession ( ) ;
await this . RefreshAssistantUIAsync ( ) ;
} ;
this . ResultingContentBlock = new ContentBlock
2024-07-14 19:46:17 +00:00
{
Time = time ,
ContentType = ContentType . TEXT ,
Role = ChatRole . AI ,
Content = aiText ,
2025-01-01 14:49:27 +00:00
HideFromUser = hideContentFromUser ,
2024-07-14 19:46:17 +00:00
} ;
2024-11-23 12:04:02 +00:00
2026-04-16 14:48:47 +00:00
if ( this . ChatThread is not null )
2024-11-23 12:04:02 +00:00
{
2026-07-03 13:02:20 +00:00
this . ChatThread . Blocks . Add ( this . ResultingContentBlock ) ;
2026-04-16 14:48:47 +00:00
this . ChatThread . SelectedProvider = this . ProviderSettings . Id ;
2024-11-23 12:04:02 +00:00
}
2026-07-03 13:02:20 +00:00
this . IsProcessing = true ;
await this . CheckpointAssistantSession ( ) ;
await this . RefreshAssistantUIAsync ( ) ;
2025-04-26 16:55:23 +00:00
2026-05-25 15:32:54 +00:00
try
{
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
this . ChatThread = await aiText . CreateFromProviderAsync ( this . ProviderSettings . CreateProvider ( ) , this . ProviderSettings . Model , this . LastUserPrompt , this . ChatThread , this . CancellationTokenSource ! . Token ) ;
// Return the AI response:
return aiText . Text ;
}
catch ( ProviderRequestException e )
2025-05-17 17:17:29 +00:00
{
2026-05-25 15:32:54 +00:00
this . Logger . LogError ( e , "The provider request failed for assistant '{AssistantTitle}'. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'" , this . Title , e . StatusCode , e . ReasonPhrase , e . ResponseBody ) ;
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . CloudOff , e . UserMessage ) ) ;
2026-07-03 13:02:20 +00:00
if ( this . ResultingContentBlock is not null & & string . IsNullOrWhiteSpace ( aiText . Text ) )
2026-05-25 15:32:54 +00:00
{
2026-07-03 13:02:20 +00:00
this . ChatThread ? . Blocks . Remove ( this . ResultingContentBlock ) ;
this . ResultingContentBlock = null ;
2026-05-25 15:32:54 +00:00
}
return string . Empty ;
2025-05-17 17:17:29 +00:00
}
2026-05-25 15:32:54 +00:00
finally
{
2026-07-03 13:02:20 +00:00
this . IsProcessing = this . assistantSessionId is not null & & ( this . AssistantSessionService . TryGetSnapshot ( this . assistantSessionKey ) ? . IsActive ? ? false ) ;
await this . CheckpointAssistantSession ( ) ;
await this . RefreshAssistantUIAsync ( ) ;
2025-05-17 17:17:29 +00:00
2026-05-25 15:32:54 +00:00
if ( manageCancellationLocally )
{
this . CancellationTokenSource ? . Dispose ( ) ;
this . CancellationTokenSource = null ;
}
}
2024-08-13 06:57:58 +00:00
}
2026-07-03 13:02:20 +00:00
/// <summary>
/// Starts the current assistant chat thread as a regular background-capable chat generation job.
/// </summary>
/// <remarks>
/// Use this when an assistant creates a chat and hands it over to the chat page instead of
/// rendering the answer inside the assistant UI.
/// </remarks>
/// <param name="time">The timestamp to use for the AI response block.</param>
/// <param name="hideContentFromUser">Whether the AI response block should be hidden from the user.</param>
/// <param name="isForeground">Whether the chat job should start as the current foreground job.</param>
/// <returns>A task that completes after the chat job was registered.</returns>
protected async Task StartChatGenerationJobAsync ( DateTimeOffset time , bool hideContentFromUser = false , bool isForeground = true )
{
if ( this . ChatThread is null )
return ;
var aiText = new ContentText
{
InitialRemoteWait = true ,
} ;
this . ResultingContentBlock = new ContentBlock
{
Time = time ,
ContentType = ContentType . TEXT ,
Role = ChatRole . AI ,
Content = aiText ,
HideFromUser = hideContentFromUser ,
} ;
this . ChatThread . Blocks . Add ( this . ResultingContentBlock ) ;
this . ChatThread . SelectedProvider = this . ProviderSettings . Id ;
await this . CheckpointAssistantSession ( ) ;
await this . AIJobService . TryStartChatGenerationAsync ( new ChatGenerationRequest
{
ChatThread = this . ChatThread ,
AIText = aiText ,
LastUserPrompt = this . LastUserPrompt ,
ProviderSettings = this . ProviderSettings ,
IsForeground = isForeground ,
} ) ;
}
2024-08-13 06:57:58 +00:00
2025-03-17 17:00:26 +00:00
private async Task CancelStreaming ( )
{
2026-07-03 13:02:20 +00:00
await this . AssistantSessionService . CancelAsync ( this . assistantSessionKey , this ) ;
2025-03-17 17:00:26 +00:00
}
2024-08-23 06:59:30 +00:00
protected async Task CopyToClipboard ( )
2024-08-13 18:53:09 +00:00
{
2024-09-01 18:10:03 +00:00
await this . RustService . CopyText2Clipboard ( this . Snackbar , this . Result2Copy ( ) ) ;
2024-08-13 18:53:09 +00:00
}
2026-04-16 14:12:45 +00:00
private ChatThread CreateSendToChatThread ( )
{
2026-04-16 14:48:47 +00:00
var originalChatThread = this . ChatThread ? ? new ChatThread ( ) ;
2026-04-16 14:12:45 +00:00
if ( string . IsNullOrWhiteSpace ( this . SendToChatVisibleUserPromptText ) )
return originalChatThread with
{
SystemPrompt = SystemPrompts . DEFAULT ,
} ;
var earliestBlock = originalChatThread . Blocks . MinBy ( x = > x . Time ) ;
var visiblePromptTime = earliestBlock is null
? DateTimeOffset . Now
: earliestBlock . Time = = DateTimeOffset . MinValue
? earliestBlock . Time
: earliestBlock . Time . AddTicks ( - 1 ) ;
var transferredBlocks = originalChatThread . Blocks
. Select ( block = > block . Role is ChatRole . USER
? block . DeepClone ( changeHideState : true )
: block . DeepClone ( ) )
. ToList ( ) ;
transferredBlocks . Insert ( 0 , new ContentBlock
{
Time = visiblePromptTime ,
ContentType = ContentType . TEXT ,
HideFromUser = false ,
Role = ChatRole . USER ,
Content = new ContentText
{
Text = this . SendToChatVisibleUserPromptText ,
} ,
} ) ;
return originalChatThread with
{
SystemPrompt = SystemPrompts . DEFAULT ,
Blocks = transferredBlocks ,
} ;
}
2024-08-13 18:53:09 +00:00
2024-08-13 06:57:58 +00:00
private static string? GetButtonIcon ( string icon )
{
if ( string . IsNullOrWhiteSpace ( icon ) )
return null ;
return icon ;
2024-07-14 19:46:17 +00:00
}
2024-08-18 10:32:18 +00:00
2025-03-12 10:31:01 +00:00
protected async Task OpenSettingsDialog ( )
{
2026-02-01 13:50:19 +00:00
if ( ! this . HasSettingsPanel )
return ;
2025-03-12 10:31:01 +00:00
var dialogParameters = new DialogParameters ( ) ;
2025-03-16 14:19:17 +00:00
await this . DialogService . ShowAsync < TSettings > ( null , dialogParameters , DialogOptions . FULLSCREEN ) ;
2025-03-12 10:31:01 +00:00
}
2026-07-03 13:02:20 +00:00
protected async Task SendToAssistant ( Tools . Components destination , SendToButton sendToButton )
2024-08-18 10:32:18 +00:00
{
2026-03-14 09:37:31 +00:00
if ( ! this . CanSendToAssistant ( destination ) )
2026-07-03 13:02:20 +00:00
return ;
2024-11-02 10:55:09 +00:00
2024-10-28 14:41:00 +00:00
var contentToSend = sendToButton = = default ? string . Empty : sendToButton . UseResultingContentBlockData switch
2024-08-18 10:32:18 +00:00
{
2024-08-18 19:48:35 +00:00
false = > sendToButton . GetText ( ) ,
2026-07-03 13:02:20 +00:00
true = > this . ResultingContentBlock ? . Content switch
2024-08-18 10:32:18 +00:00
{
ContentText textBlock = > textBlock . Text ,
_ = > string . Empty ,
} ,
} ;
2024-08-22 19:27:16 +00:00
var sendToData = destination . GetData ( ) ;
2026-07-03 13:02:20 +00:00
if ( destination is not Tools . Components . CHAT & & this . AssistantSessionService . GetSnapshots ( ) . Any ( snapshot = > snapshot . IsActive & & snapshot . Key . Component = = destination ) )
{
await this . MessageBus . SendWarning ( new ( Icons . Material . Filled . Apps , this . TB ( "This assistant is already running. AI Studio opens the running session instead." ) ) ) ;
this . NavigationManager . NavigateTo ( sendToData . Route ) ;
return ;
}
if ( destination is not Tools . Components . CHAT )
await this . AssistantSessionService . ClearInactiveSessionsForComponentAsync ( destination ) ;
2024-08-18 19:48:35 +00:00
switch ( destination )
{
2024-09-04 13:44:23 +00:00
case Tools . Components . CHAT :
2026-04-15 17:40:53 +00:00
if ( sendToButton . SendToChatAsInput )
MessageBus . INSTANCE . DeferMessage ( this , Event . SEND_TO_CHAT_INPUT , contentToSend ) ;
else
{
var convertedChatThread = this . ConvertToChatThread ;
2026-04-16 14:48:47 +00:00
convertedChatThread = convertedChatThread with { SelectedProvider = this . ProviderSettings . Id } ;
2026-04-15 17:40:53 +00:00
MessageBus . INSTANCE . DeferMessage ( this , sendToData . Event , convertedChatThread ) ;
}
2024-08-18 19:48:35 +00:00
break ;
default :
2024-08-22 19:27:16 +00:00
MessageBus . INSTANCE . DeferMessage ( this , sendToData . Event , contentToSend ) ;
2024-08-18 19:48:35 +00:00
break ;
}
2024-08-22 19:27:16 +00:00
this . NavigationManager . NavigateTo ( sendToData . Route ) ;
2024-08-18 10:32:18 +00:00
}
2026-03-14 09:37:31 +00:00
private bool CanSendToAssistant ( Tools . Components component )
{
if ( ! component . AllowSendTo ( ) )
return false ;
return this . SettingsManager . IsAssistantVisible ( component , withLogging : false ) ;
}
2024-08-18 19:48:35 +00:00
private async Task InnerResetForm ( )
{
2026-07-03 13:02:20 +00:00
if ( this . AssistantSessionService . TryGetSnapshot ( this . assistantSessionKey ) ? . IsActive ? ? false )
return ;
await this . AssistantSessionService . ClearAsync ( this . assistantSessionKey ) ;
this . assistantSessionId = null ;
this . ResultingContentBlock = null ;
2026-04-16 14:48:47 +00:00
this . ProviderSettings = Settings . Provider . NONE ;
2024-08-18 19:48:35 +00:00
2024-08-21 06:30:01 +00:00
await this . JsRuntime . ClearDiv ( RESULT_DIV_ID ) ;
2024-08-18 19:48:35 +00:00
await this . JsRuntime . ClearDiv ( AFTER_RESULT_DIV_ID ) ;
2025-01-01 14:49:27 +00:00
this . ResetForm ( ) ;
2026-02-01 13:50:19 +00:00
this . ResetProviderAndProfileSelection ( ) ;
2024-08-18 19:48:35 +00:00
2026-04-16 14:48:47 +00:00
this . InputIsValid = false ;
2026-07-03 13:02:20 +00:00
this . InputIssues = [ ] ;
2024-08-18 19:48:35 +00:00
2026-04-16 14:48:47 +00:00
this . Form ? . ResetValidation ( ) ;
2026-07-03 13:02:20 +00:00
await this . RefreshAssistantUIAsync ( ) ;
2026-04-16 14:48:47 +00:00
this . Form ? . ResetValidation ( ) ;
2024-08-18 19:48:35 +00:00
}
2024-09-15 10:30:07 +00:00
private string GetResetColor ( ) = > this . SettingsManager . IsDarkMode switch
{
true = > $"background-color: #804000" ,
false = > $"background-color: {this.ColorTheme.GetCurrentPalette(this.SettingsManager).Warning.Value}" ,
} ;
private string GetSendToColor ( ) = > this . SettingsManager . IsDarkMode switch
{
true = > $"background-color: #004080" ,
false = > $"background-color: {this.ColorTheme.GetCurrentPalette(this.SettingsManager).InfoLighten}" ,
} ;
2025-04-27 07:06:05 +00:00
#region Overrides of MSGComponentBase
2024-09-15 10:30:07 +00:00
2025-04-27 07:06:05 +00:00
protected override void DisposeResources ( )
2024-09-15 10:30:07 +00:00
{
2026-07-03 13:02:20 +00:00
this . isDisposed = true ;
2025-08-15 19:16:51 +00:00
try
{
this . formChangeTimer . Stop ( ) ;
this . formChangeTimer . Dispose ( ) ;
}
catch
{
// ignore
}
base . DisposeResources ( ) ;
2024-09-15 10:30:07 +00:00
}
#endregion
2026-07-03 13:02:20 +00:00
#region Assistant sessions
/// <summary>
/// Stores the current assistant UI and chat state in the active assistant session.
/// </summary>
/// <returns>A task that completes after the checkpoint was stored and published.</returns>
private Task CheckpointAssistantSession ( )
{
if ( this . assistantSessionId is null )
return Task . CompletedTask ;
return this . AssistantSessionService . CheckpointAsync ( this . assistantSessionKey , this . assistantSessionId . Value , this . Title , this . ChatThread , this . CaptureAssistantSessionState ( ) , this ) ;
}
/// <summary>
/// Allows derived assistants to restore client-only UI after a session was attached.
/// </summary>
/// <param name="snapshot">The assistant session snapshot that was attached.</param>
/// <returns>A task that completes after derived UI restore work has finished.</returns>
protected virtual Task OnAssistantSessionAttachedAsync ( AssistantSessionSnapshot snapshot ) = > Task . CompletedTask ;
/// <summary>
/// Allows derived assistants to restore DOM-dependent client-only UI after an attached session was rendered.
/// </summary>
/// <param name="snapshot">The assistant session snapshot that was rendered.</param>
/// <returns>A task that completes after derived UI restore work has finished.</returns>
protected virtual Task OnAssistantSessionRenderedAsync ( AssistantSessionSnapshot snapshot ) = > Task . CompletedTask ;
/// <summary>
/// Handles assistant session change events for the current assistant instance.
/// </summary>
/// <typeparam name="T">The message payload type.</typeparam>
/// <param name="sendingComponent">The component that sent the message, if any.</param>
/// <param name="triggeredEvent">The event that was triggered.</param>
/// <param name="data">The message payload.</param>
/// <returns>A task that completes after the message was processed.</returns>
protected override async Task ProcessIncomingMessage < T > ( ComponentBase ? sendingComponent , Event triggeredEvent , T ? data ) where T : default
{
if ( ReferenceEquals ( sendingComponent , this ) )
return ;
switch ( triggeredEvent )
{
case Event . ASSISTANT_SESSION_CHANGED :
case Event . ASSISTANT_SESSION_FINISHED :
if ( data is AssistantSessionSnapshot snapshot & & snapshot . Key = = this . assistantSessionKey )
{
await this . AttachAssistantSession ( snapshot , restoreClientOnlyContent : triggeredEvent is Event . ASSISTANT_SESSION_FINISHED ) ;
if ( triggeredEvent is Event . ASSISTANT_SESSION_FINISHED )
_ = this . AssistantSessionService . TryTakeInactiveSnapshot ( this . assistantSessionKey ) ;
}
break ;
}
}
/// <summary>
/// Attaches the component to an existing assistant session if one is available.
/// </summary>
/// <returns>A task that completes after the session was attached.</returns>
private async Task AttachAssistantSessionIfAvailable ( )
{
var snapshot = this . AssistantSessionService . TryGetSnapshot ( this . assistantSessionKey ) ;
if ( snapshot ? . IsActive ? ? false )
{
await this . AttachAssistantSession ( snapshot , restoreClientOnlyContent : true ) ;
return ;
}
snapshot = this . AssistantSessionService . TryTakeInactiveSnapshot ( this . assistantSessionKey ) ;
if ( snapshot is null )
return ;
await this . AttachAssistantSession ( snapshot , restoreClientOnlyContent : true ) ;
}
/// <summary>
/// Applies an assistant session snapshot to this component instance.
/// </summary>
/// <param name="snapshot">The snapshot to attach.</param>
/// <param name="restoreClientOnlyContent">Whether derived assistants should restore client-only UI state.</param>
/// <returns>A task that completes after the component was refreshed.</returns>
private async Task AttachAssistantSession ( AssistantSessionSnapshot snapshot , bool restoreClientOnlyContent )
{
this . assistantSessionId = snapshot . SessionId ;
this . ImportAssistantSessionState ( snapshot . State ) ;
this . ChatThread = snapshot . ChatThread ? ? this . ChatThread ;
this . IsProcessing = snapshot . IsActive ;
if ( ! snapshot . IsActive )
this . CancellationTokenSource = null ;
if ( restoreClientOnlyContent )
await this . OnAssistantSessionAttachedAsync ( snapshot ) ;
if ( restoreClientOnlyContent )
this . pendingRenderedAssistantSessionSnapshot = snapshot ;
await this . RefreshAssistantUIAsync ( ) ;
}
/// <summary>
/// Refreshes the component when it is still mounted.
/// </summary>
/// <returns>A task that completes after the renderer was notified.</returns>
private async Task RefreshAssistantUIAsync ( )
{
if ( this . isDisposed )
return ;
try
{
await this . InvokeAsync ( this . StateHasChanged ) ;
}
catch ( InvalidOperationException )
{
// The component may already have left the renderer while a background session is finishing.
}
}
/// <summary>
/// Captures the base assistant state and assistant-specific typed state values for session restore.
/// </summary>
/// <returns>A dictionary containing the current assistant state.</returns>
private Dictionary < string , IAssistantSessionSnapshotField > CaptureAssistantSessionState ( )
{
var state = new AssistantSessionStateWriter ( ) ;
state . Set ( PROVIDER_SETTINGS_STATE_KEY , this . ProviderSettings ) ;
state . Set ( INPUT_IS_VALID_STATE_KEY , this . InputIsValid ) ;
state . Set ( CURRENT_PROFILE_STATE_KEY , this . CurrentProfile ) ;
state . Set ( CURRENT_CHAT_TEMPLATE_STATE_KEY , this . CurrentChatTemplate ) ;
state . Set ( CHAT_THREAD_STATE_KEY , this . ChatThread ) ;
state . Set ( LAST_USER_PROMPT_STATE_KEY , this . LastUserPrompt ) ;
state . Set ( RESULTING_CONTENT_BLOCK_STATE_KEY , this . ResultingContentBlock ) ;
state . Set ( INPUT_ISSUES_STATE_KEY , this . InputIssues ) ;
state . Set ( IS_PROCESSING_STATE_KEY , this . IsProcessing ) ;
this . CaptureCustomAssistantSessionState ( state ) ;
return state . ToDictionary ( ) ;
}
/// <summary>
/// Captures assistant-specific state values.
/// </summary>
/// <param name="state">The typed state writer to update.</param>
protected virtual void CaptureCustomAssistantSessionState ( AssistantSessionStateWriter state ) { }
/// <summary>
/// Restores the base assistant state and assistant-specific typed state values from a session snapshot.
/// </summary>
/// <param name="state">The captured assistant state to import.</param>
private void ImportAssistantSessionState ( IReadOnlyDictionary < string , IAssistantSessionSnapshotField > state )
{
var reader = new AssistantSessionStateReader ( state , this . Title ) ;
reader . Restore ( PROVIDER_SETTINGS_STATE_KEY , value = > this . ProviderSettings = value ) ;
reader . Restore ( INPUT_IS_VALID_STATE_KEY , value = > this . InputIsValid = value ) ;
reader . Restore ( CURRENT_PROFILE_STATE_KEY , value = > this . CurrentProfile = value ) ;
reader . Restore ( CURRENT_CHAT_TEMPLATE_STATE_KEY , value = > this . CurrentChatTemplate = value ) ;
reader . Restore ( CHAT_THREAD_STATE_KEY , value = > this . ChatThread = value ) ;
reader . Restore ( LAST_USER_PROMPT_STATE_KEY , value = > this . LastUserPrompt = value ) ;
reader . Restore ( RESULTING_CONTENT_BLOCK_STATE_KEY , value = > this . ResultingContentBlock = value ) ;
reader . Restore ( INPUT_ISSUES_STATE_KEY , value = > this . InputIssues = value ) ;
reader . Restore ( IS_PROCESSING_STATE_KEY , value = > this . IsProcessing = value ) ;
this . RestoreCustomAssistantSessionState ( reader ) ;
}
/// <summary>
/// Restores assistant-specific state values.
/// </summary>
/// <param name="state">The typed state reader to read from.</param>
protected virtual void RestoreCustomAssistantSessionState ( AssistantSessionStateReader state ) { }
#endregion
}