2026-02-01 14:50:19 +01:00
using AIStudio.Dialogs.Settings ;
2026-04-09 10:01:24 +02:00
using AIStudio.Settings.DataModel ;
2026-07-03 15:02:20 +02:00
using AIStudio.Tools.AssistantSessions ;
2026-07-15 12:53:30 +02:00
using AIStudio.Tools.Media ;
using AIStudio.Tools.Services ;
2024-07-14 21:46:17 +02:00
using Microsoft.AspNetCore.Components ;
2025-03-12 11:31:01 +01:00
using DialogOptions = AIStudio . Dialogs . DialogOptions ;
2024-08-21 08:30:01 +02:00
namespace AIStudio.Components ;
2024-07-14 21:46:17 +02:00
2026-08-09 20:00:38 +02:00
public partial class AssistantBlock < TSettings > : MSGComponentBase , IAssistantCategoryMember where TSettings : IComponent
2024-07-14 21:46:17 +02:00
{
2026-07-03 15:02:20 +02:00
/// <summary>
/// Describes the assistant session indicator shown on top of the assistant icon.
/// </summary>
/// <param name="Icon">The icon that communicates the session status.</param>
/// <param name="Color">The color that communicates the session status.</param>
/// <param name="Tooltip">The tooltip text that explains the session status.</param>
private sealed record AssistantSessionIndicatorData ( string Icon , Color Color , string Tooltip );
2024-07-14 21:46:17 +02:00
[Parameter]
public string Name { get ; set ; } = string . Empty ;
2026-01-12 20:43:45 +01:00
2024-07-14 21:46:17 +02:00
[Parameter]
public string Description { get ; set ; } = string . Empty ;
2026-01-12 20:43:45 +01:00
2024-07-14 21:46:17 +02:00
[Parameter]
public string Icon { get ; set ; } = Icons . Material . Filled . DisabledByDefault ;
2026-01-12 20:43:45 +01:00
2024-07-14 21:46:17 +02:00
[Parameter]
public string ButtonText { get ; set ; } = "Start" ;
2026-01-12 20:43:45 +01:00
2024-07-14 21:46:17 +02:00
[Parameter]
public string Link { get ; set ; } = string . Empty ;
2026-01-12 20:43:45 +01:00
2026-07-05 15:20:29 +02:00
[Parameter]
public EventCallback OnClick { get ; set ; }
2026-04-09 10:01:24 +02:00
[Parameter]
public bool Disabled { get ; set ; }
[Parameter]
public RenderFragment ? SecurityBadge { get ; set ; }
2026-07-15 21:00:25 +02:00
[Parameter]
public RenderFragment ? AdditionalActions { get ; set ; }
2026-01-12 20:43:45 +01:00
[Parameter]
public Tools . Components Component { get ; set ; } = Tools . Components . NONE ;
2026-07-03 15:02:20 +02:00
/// <summary>
/// Gets or sets the optional assistant session instance ID represented by this block.
/// </summary>
[Parameter]
public string AssistantSessionInstanceId { get ; set ; } = string . Empty ;
2026-01-12 20:43:45 +01:00
[Parameter]
public PreviewFeatures RequiredPreviewFeature { get ; set ; } = PreviewFeatures . NONE ;
2026-08-09 20:00:38 +02:00
/// <summary>
/// Gets or sets the assistant category this block belongs to, if any.
/// </summary>
[CascadingParameter]
public AssistantCategoryBlock ? Category { get ; set ; }
2024-09-15 12:30:07 +02:00
[Inject]
private MudTheme ColorTheme { get ; init ; } = null !;
2026-01-12 20:43:45 +01:00
2025-03-12 11:31:01 +01:00
[Inject]
private IDialogService DialogService { get ; init ; } = null !;
2026-07-03 15:02:20 +02:00
[Inject]
private AssistantSessionService AssistantSessionService { get ; init ; } = null !;
2026-07-15 12:53:30 +02:00
[Inject]
private MediaTranscriptionService MediaTranscriptionService { get ; init ; } = null !;
2025-03-12 11:31:01 +01:00
private async Task OpenSettingsDialog ()
{
2026-02-01 14:50:19 +01:00
if (! this . HasSettingsPanel )
return ;
2025-03-12 11:31:01 +01:00
var dialogParameters = new DialogParameters ();
2026-01-12 20:43:45 +01:00
2025-04-27 09:06:05 +02:00
await this . DialogService . ShowAsync < TSettings >( T ( "Open Settings" ), dialogParameters , DialogOptions . FULLSCREEN );
2024-09-15 12:30:07 +02:00
}
2026-01-12 20:43:45 +01:00
2026-07-15 12:53:30 +02:00
private string BorderColor => this . AssistantSessionSnapshot ?. IsActive is true || this . MediaImportSnapshot ?. IsBusy is true ? this . ColorTheme . GetActivityIndicatorColor ( this . SettingsManager ) : this . SettingsManager . IsDarkMode switch
2024-09-15 12:30:07 +02:00
{
2026-07-03 15:02:20 +02:00
true => this . ColorTheme . GetCurrentPalette ( this . SettingsManager ). GrayDefault ,
false => this . ColorTheme . GetCurrentPalette ( this . SettingsManager ). GrayDefault ,
2024-09-15 12:30:07 +02:00
};
2026-07-03 15:02:20 +02:00
private string BlockStyle => $"border-width: 3px; border-color: {this.BorderColor}; border-radius: 12px; border-style: solid; max-width: 20em;" ;
2026-01-12 20:43:45 +01:00
2026-08-09 20:00:38 +02:00
/// <inheritdoc />
public bool IsVisible => this . SettingsManager . IsAssistantVisible ( this . Component , assistantName : this . Name , requiredPreviewFeature : this . RequiredPreviewFeature );
2026-02-01 14:50:19 +01:00
private bool HasSettingsPanel => typeof ( TSettings ) != typeof ( NoSettingsPanel );
2026-07-03 15:02:20 +02:00
2026-07-05 15:20:29 +02:00
private bool HasStartAction => this . OnClick . HasDelegate ;
2026-07-03 15:02:20 +02:00
/// <summary>
/// Gets the newest assistant session snapshot represented by this block.
/// </summary>
private AssistantSessionSnapshot ? AssistantSessionSnapshot => string . IsNullOrWhiteSpace ( this . AssistantSessionInstanceId )
? this . AssistantSessionService . GetSnapshots (). FirstOrDefault ( snapshot => snapshot . Key . Component == this . Component )
: this . AssistantSessionService . GetSnapshots (). FirstOrDefault ( snapshot => snapshot . Key . InstanceId == this . AssistantSessionInstanceId );
2026-07-15 12:53:30 +02:00
private MediaImportOwner CurrentMediaImportOwner => MediaImportOwner . ForAssistant ( new AssistantSessionKey ( this . Component , this . AssistantSessionInstanceId ));
2026-08-02 21:41:12 +02:00
private MediaImportSnapshot ? MediaImportSnapshot => this . MediaTranscriptionService . GetSnapshots ()
. FirstOrDefault ( snapshot => this . OwnedByThisBlock ( snapshot . Owner ));
/// <summary>
/// Gets whether a media-import owner belongs to the assistant represented by this block.
/// </summary>
/// <remarks>
/// Owners that persist their own sources are keyed by the stored document rather than by an
/// assistant session, so this block aggregates all of them for its component. Without a session
/// instance we aggregate every owner of the component, otherwise we match the exact owner.
/// </remarks>
/// <param name="owner">The media-import owner to test.</param>
/// <returns><c>true</c> when this block represents the owner.</returns>
private bool OwnedByThisBlock ( MediaImportOwner owner )
{
if ( owner . Kind . PersistsOwnSources ())
return owner . Kind == this . Component . MediaOwnerKind ();
if ( string . IsNullOrWhiteSpace ( this . AssistantSessionInstanceId ))
return owner . Kind is MediaImportOwnerKind . ASSISTANT && owner . Id . StartsWith ( $"{this.Component}:" , StringComparison . Ordinal );
return owner == this . CurrentMediaImportOwner ;
}
2026-07-15 12:53:30 +02:00
2026-07-03 15:02:20 +02:00
/// <summary>
/// Gets the assistant session indicator shown on top of the assistant icon.
/// </summary>
2026-07-15 12:53:30 +02:00
private AssistantSessionIndicatorData ? AssistantSessionIndicator => this . MediaImportSnapshot ?. Status switch
{
MediaImportStatus . QUEUED or MediaImportStatus . RUNNING or MediaImportStatus . CANCELING => new ( Icons . Material . Filled . ChangeCircle , Color . Info , this . T ( "Media is still being prepared." )),
MediaImportStatus . SUCCEEDED => new ( Icons . Material . Filled . TaskAlt , Color . Success , this . T ( "The media transcript is ready." )),
MediaImportStatus . WARNING => new ( Icons . Material . Filled . WarningAmber , Color . Warning , this . T ( "Media transcription completed with a warning. Open the assistant to review it." )),
MediaImportStatus . FAILED => new ( Icons . Material . Filled . Error , Color . Error , this . T ( "Media transcription failed. Open the assistant to review it." )),
MediaImportStatus . CANCELLED => new ( Icons . Material . Filled . Cancel , Color . Warning , this . T ( "Media transcription was canceled. Open the assistant to review it." )),
_ => this . AssistantSessionIndicatorWithoutMedia ,
};
private AssistantSessionIndicatorData ? AssistantSessionIndicatorWithoutMedia => this . AssistantSessionSnapshot ?. Status switch
2026-07-03 15:02:20 +02:00
{
AssistantSessionStatus . RUNNING or AssistantSessionStatus . CANCELING => new ( Icons . Material . Filled . ChangeCircle , Color . Info , this . T ( "Assistant is still running." )),
AssistantSessionStatus . COMPLETED => new ( Icons . Material . Filled . TaskAlt , Color . Success , this . T ( "The result is ready." )),
AssistantSessionStatus . FAILED => new ( Icons . Material . Filled . Error , Color . Error , this . T ( "Assistant failed. Open it to review the result." )),
AssistantSessionStatus . CANCELED => new ( Icons . Material . Filled . Cancel , Color . Warning , this . T ( "Assistant was canceled. Open it to review the result." )),
_ => null ,
};
2026-07-15 12:53:30 +02:00
protected override async Task OnInitializedAsync ()
{
this . MediaTranscriptionService . StateChanged += this . OnMediaImportStateChanged ;
2026-08-09 20:00:38 +02:00
this . Category ?. RegisterAssistant ( this );
2026-07-15 12:53:30 +02:00
await base . OnInitializedAsync ();
}
private void OnMediaImportStateChanged ( MediaImportOwner owner )
{
2026-08-02 21:41:12 +02:00
if ( this . OwnedByThisBlock ( owner ))
2026-08-25 18:53:09 +02:00
this . InvokeAsync ( this . StateHasChanged ). Observe ( $"{nameof(AssistantBlock<TSettings>)}: rendering a media import change" );
2026-07-15 12:53:30 +02:00
}
protected override void DisposeResources ()
{
this . MediaTranscriptionService . StateChanged -= this . OnMediaImportStateChanged ;
2026-08-09 20:00:38 +02:00
this . Category ?. UnregisterAssistant ( this );
2026-07-15 12:53:30 +02:00
base . DisposeResources ();
}
2026-07-03 15:02:20 +02:00
/// <summary>
/// Refreshes the block when assistant session activity changes.
/// </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 Task ProcessIncomingMessage < T >( ComponentBase ? sendingComponent , Event triggeredEvent , T ? data ) where T : default
{
if ( triggeredEvent is Event . ASSISTANT_SESSION_CHANGED or Event . ASSISTANT_SESSION_FINISHED )
this . StateHasChanged ();
return base . ProcessIncomingMessage ( sendingComponent , triggeredEvent , data );
}
}