2026-11-09 09:55:01 +01:00
using AIStudio.Chat ;
2026-11-15 10:32:46 +01:00
using AIStudio.Chat.Embedded ;
2026-11-09 09:55:01 +01:00
using AIStudio.Components ;
using Microsoft.AspNetCore.Components ;
namespace AIStudio.Assistants.BeyondFactsLab ;
/// <summary>
2026-11-09 12:42:59 +01:00
/// A chat of the BeyondFacts Lab, such as the one of a step below its form "Your details".
2026-11-09 09:55:01 +01:00
/// </summary>
/// <remarks>
2026-11-15 10:32:46 +01:00
/// The embedded chat of assistants with what the lab decides for each of its places: the opening request, the
/// introduction, the question before starting over, and, for every request, the system prompt with the guide, the
2026-11-15 11:40:15 +01:00
/// details, and the material, and the guidelines of the organization in front of the first message, see
/// BflConversationService.PrepareAsync. The count of tokens shows how full the context
2026-11-15 10:32:46 +01:00
/// of the model is, since every request carries the guide. It offers no "send to": the lab neither receives content
/// from other assistants nor hands its answers on.
2026-11-09 09:55:01 +01:00
/// </remarks>
2026-11-09 12:42:59 +01:00
public partial class BflChat : MSGComponentBase
2026-11-09 09:55:01 +01:00
{
/// <summary>
2026-11-09 12:42:59 +01:00
/// Gets or sets where the chat to show belongs, e.g., to a step of a preparation.
2026-11-09 09:55:01 +01:00
/// </summary>
[Parameter, EditorRequired]
2026-11-09 12:42:59 +01:00
public BflThreadLocation Location { get ; set ; } = null !;
/// <summary>
/// Gets or sets whether the chat shows a heading of its own; it needs none where its place already names it.
/// </summary>
[Parameter]
public bool ShowHeading { get ; set ; } = true ;
2026-11-09 09:55:01 +01:00
/// <summary>
/// Gets or sets the provider which answers.
/// </summary>
[Parameter, EditorRequired]
public AIStudio . Settings . Provider Provider { get ; set ; } = Settings . Provider . NONE ;
/// <summary>
/// Gets or sets why the provider cannot answer, or null when it can.
/// </summary>
[Parameter]
public string? ProviderIssue { get ; set ; }
[Inject]
private BflConversationService ConversationService { get ; init ; } = null !;
2026-11-09 20:00:01 +01:00
[Inject]
private BflMaterialService MaterialService { get ; init ; } = null !;
2026-11-09 09:55:01 +01:00
[Inject]
2026-11-09 12:42:59 +01:00
private ILogger < BflChat > Logger { get ; init ; } = null !;
2026-11-09 09:55:01 +01:00
/// <summary>
2026-11-15 10:32:46 +01:00
/// Readies a request of the chat shown, with the provider chosen; made anew whenever either changes.
2026-11-09 09:55:01 +01:00
/// </summary>
2026-11-15 10:32:46 +01:00
private Func < ChatThread , Task < EmbeddedChatPreparation >> prepareAsync = _ => Task . FromResult ( EmbeddedChatPreparation . REFUSED );
2026-11-09 09:55:01 +01:00
/// <summary>
2026-11-15 11:40:15 +01:00
/// Gets what the next request of the chat shown carries beside the conversation, for the count of tokens; made anew
/// whenever the chat or the provider changes.
2026-11-09 09:55:01 +01:00
/// </summary>
2026-11-15 10:32:46 +01:00
private Func < Task < string? >> systemPromptForCountAsync = () => Task . FromResult < string? >( null );
2026-11-09 09:55:01 +01:00
/// <summary>
2026-11-15 10:32:46 +01:00
/// Changes whenever the material of the preparation changes, since every request of a step carries it.
2026-11-09 09:55:01 +01:00
/// </summary>
2026-11-15 10:32:46 +01:00
private int materialsVersion ;
2026-11-09 09:55:01 +01:00
2026-11-15 11:40:15 +01:00
/// <summary>
/// Gets a number which changes whenever what the next request carries beside the conversation changed: the
/// material, or the guidelines of the organization after a configuration plugin was reloaded.
/// </summary>
private int ContextVersion => HashCode . Combine ( this . materialsVersion , this . SettingsManager . ConfigurationData . BeyondFactsLab . OrganizationRules );
2026-11-09 12:42:59 +01:00
/// <summary>
/// Gets what the chat says before its conversation begins.
/// </summary>
private string Introduction => this . Location switch
{
BflOnboardingThreadLocation => TA ( "Tell the lab what you are preparing for. It tells you whether it fits your task and how to work with it. You can also let the lab begin." ),
_ => TA ( "The lab opens the conversation of this step with what your details already say, and asks for what is missing. You can also write first." )
};
/// <summary>
/// Gets the name a message gets when the person exports it.
/// </summary>
private string ExportFileName => this . Location switch
{
BflStepThreadLocation stepLocation => stepLocation . Step . Title ,
_ => T ( "Is the lab right for my task?" )
};
/// <summary>
/// Gets the question before the conversation starts over.
/// </summary>
private string ResetQuestion => this . Location switch
{
BflStepThreadLocation => TA ( "Delete the conversation of this step and start it over? Your details stay as they are." ),
_ => TA ( "Delete this conversation and start it over?" )
};
2026-11-09 09:55:01 +01:00
/// <inheritdoc />
protected override async Task OnInitializedAsync ()
{
2026-11-09 20:00:01 +01:00
this . MaterialService . MaterialsChanged += this . OnMaterialsChangedAsync ;
2026-11-09 09:55:01 +01:00
await base . OnInitializedAsync ();
}
/// <inheritdoc />
protected override async Task OnParametersSetAsync ()
{
await base . OnParametersSetAsync ();
2026-11-15 10:32:46 +01:00
// Bound to the chat and the provider shown now, so that a request never takes those of the next render:
var location = this . Location ;
var provider = this . Provider ;
this . prepareAsync = thread => this . ConversationService . PrepareAsync ( location , provider , thread );
this . systemPromptForCountAsync = () => this . SystemPromptForCountAsync ( location , provider );
2026-11-09 09:55:01 +01:00
}
/// <inheritdoc />
protected override void DisposeResources ()
{
2026-11-09 20:00:01 +01:00
this . MaterialService . MaterialsChanged -= this . OnMaterialsChangedAsync ;
2026-11-09 09:55:01 +01:00
base . DisposeResources ();
}
/// <summary>
2026-11-15 10:32:46 +01:00
/// Creates the chat of the place shown, for a place which has no stored chat yet.
2026-11-09 09:55:01 +01:00
/// </summary>
2026-11-15 10:32:46 +01:00
private ChatThread CreateThread () => BflThreadFactory . Create ( this . Location );
2026-11-09 09:55:01 +01:00
2026-11-09 20:00:01 +01:00
/// <summary>
2026-11-15 10:32:46 +01:00
/// Lets the chat count again when the material of the preparation changed.
2026-11-09 20:00:01 +01:00
/// </summary>
/// <param name="projectId">The ID of the preparation whose material changed.</param>
private Task OnMaterialsChangedAsync ( Guid projectId )
{
if ( this . Location is not BflStepThreadLocation stepLocation || stepLocation . ProjectId != projectId )
return Task . CompletedTask ;
2026-11-15 10:32:46 +01:00
return this . InvokeAsync (() =>
2026-11-09 20:00:01 +01:00
{
2026-11-15 10:32:46 +01:00
this . materialsVersion ++;
2026-11-09 20:00:01 +01:00
this . StateHasChanged ();
});
}
2026-11-09 09:55:01 +01:00
/// <summary>
2026-11-15 11:40:15 +01:00
/// Puts together what the next request of a chat carries beside the conversation, for the count of tokens: the
/// system prompt, and the guidelines of the organization in front of the first message.
2026-11-09 09:55:01 +01:00
/// </summary>
2026-11-15 10:32:46 +01:00
/// <param name="location">Where the chat belongs.</param>
/// <param name="provider">The provider which answers.</param>
2026-11-15 11:40:15 +01:00
/// <returns>The text to count, or null when it cannot be put together now.</returns>
2026-11-15 10:32:46 +01:00
private async Task < string? > SystemPromptForCountAsync ( BflThreadLocation location , AIStudio . Settings . Provider provider )
2026-11-09 09:55:01 +01:00
{
try
{
2026-11-15 11:40:15 +01:00
return await this . ConversationService . ComposeTextForCountAsync ( location , provider );
2026-11-09 09:55:01 +01:00
}
catch ( Exception exception ) when ( exception is IOException or UnauthorizedAccessException or FormatException )
{
// Sending reports the same problem; the count only stays unknown:
this . Logger . LogDebug ( exception , "Could not count the tokens of a chat in the BeyondFacts Lab." );
2026-11-15 10:32:46 +01:00
return null ;
2026-11-09 09:55:01 +01:00
}
}
}