Refactor basic assistant concepts into a base component

This commit is contained in:
Thorsten Sommer 2024-07-14 18:45:53 +02:00
parent 85e429b9ee
commit f5cb574be8
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
5 changed files with 210 additions and 154 deletions

View File

@ -0,0 +1,39 @@
@using AIStudio.Chat
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
@this.Title
</MudText>
<InnerScrolling HeaderHeight="12.3em">
<ChildContent>
<MudForm @ref="@this.form" @bind-IsValid="@this.inputIsValid" @bind-Errors="@this.inputIssues" Class="pr-2">
<MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6">
@this.Description
</MudText>
@if (this.Body is not null)
{
@this.Body
}
</MudForm>
@if (this.inputIssues.Any())
{
<MudPaper Class="pr-2 mt-3" Outlined="@true">
<MudText Typo="Typo.h6">Issues</MudText>
<MudList Clickable="@true">
@foreach (var issue in this.inputIssues)
{
<MudListItem Icon="@Icons.Material.Filled.Error" IconColor="Color.Error">
@issue
</MudListItem>
}
</MudList>
</MudPaper>
}
@if (this.resultingContentBlock is not null)
{
<ContentBlockComponent Role="@this.resultingContentBlock.Role" Type="@this.resultingContentBlock.ContentType" Time="@this.resultingContentBlock.Time" Content="@this.resultingContentBlock.Content" Class="mr-2"/>
}
</ChildContent>
</InnerScrolling>

View File

@ -0,0 +1,104 @@
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public abstract partial class AssistantBase : ComponentBase
{
[Inject]
protected SettingsManager SettingsManager { get; set; } = null!;
[Inject]
protected IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
protected Random RNG { get; set; } = null!;
protected string Title { get; init; } = string.Empty;
protected string Description { get; init; } = string.Empty;
protected abstract string SystemPrompt { get; }
private protected virtual RenderFragment? Body => null;
protected AIStudio.Settings.Provider selectedProvider;
protected MudForm? form;
protected bool inputIsValid;
private ChatThread? chatThread;
private ContentBlock? resultingContentBlock;
private string[] inputIssues = [];
#region Overrides of ComponentBase
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)
this.form?.ResetValidation();
await base.OnAfterRenderAsync(firstRender);
}
#endregion
protected void CreateChatThread()
{
this.chatThread = new()
{
WorkspaceId = Guid.Empty,
ChatId = Guid.NewGuid(),
Name = string.Empty,
Seed = this.RNG.Next(),
SystemPrompt = this.SystemPrompt,
Blocks = [],
};
}
protected DateTimeOffset AddUserRequest(string request)
{
var time = DateTimeOffset.Now;
this.chatThread!.Blocks.Add(new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.USER,
Content = new ContentText
{
Text = request,
},
});
return time;
}
protected async Task AddAIResponseAsync(DateTimeOffset time)
{
var aiText = new ContentText
{
// We have to wait for the remote
// for the content stream:
InitialRemoteWait = true,
};
this.resultingContentBlock = new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.AI,
Content = aiText,
};
this.chatThread?.Blocks.Add(this.resultingContentBlock);
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
await aiText.CreateFromProviderAsync(this.selectedProvider.UsedProvider.CreateProvider(this.selectedProvider.InstanceName, this.selectedProvider.Hostname), this.JsRuntime, this.SettingsManager, this.selectedProvider.Model, this.chatThread);
}
}

View File

@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace AIStudio.Components;
//
// See https://stackoverflow.com/a/77300384/2258393 for why this class is needed
//
public abstract class AssistantBaseCore : AssistantBase
{
private protected sealed override RenderFragment Body => this.BuildRenderTree;
// Allow content to be provided by a .razor file but without
// overriding the content of the base class
protected new virtual void BuildRenderTree(RenderTreeBuilder builder)
{
}
}

View File

@ -1,26 +1,10 @@
@page "/assistant/icons" @page "/assistant/icons"
@using AIStudio.Chat
@using AIStudio.Settings @using AIStudio.Settings
@inherits AssistantBaseCore
<MudText Typo="Typo.h3" Class="mb-2 mr-3"> <MudTextField T="string" @bind-Text="@this.inputContext" Validation="@this.ValidatingContext" AdornmentIcon="@Icons.Material.Filled.TextFields" Adornment="Adornment.Start" Label="Your context" Variant="Variant.Outlined" Lines="3" AutoGrow="@true" MaxLines="12" Class="mb-3"/>
Icon Finder
</MudText>
<InnerScrolling HeaderHeight="12.3em"> <MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<ChildContent>
<MudForm @ref="@this.form" @bind-IsValid="@this.inputIsValid" @bind-Errors="@this.inputIssues" Class="pr-2">
<MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6">
Finding the right icon for a context, such as for a piece of text, is not easy. The first challenge:
You need to extract a concept from your context, such as from a text. Let's take an example where
your text contains statements about multiple departments. The sought-after concept could be "departments."
The next challenge is that we need to anticipate the bias of the icon designers: under the search term
"departments," there may be no relevant icons or only unsuitable ones. Depending on the icon source,
it might be more effective to search for "buildings," for instance. LLMs assist you with both steps.
</MudText>
<MudTextField T="string" @bind-Text="@this.inputContext" Validation="@this.ValidatingContext" AdornmentIcon="@Icons.Material.Filled.TextFields" Adornment="Adornment.Start" Label="Yout context" Variant="Variant.Outlined" Lines="3" AutoGrow="@true" MaxLines="12" Class="mb-3"/>
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudSelect T="IconSources" @bind-Value="@this.selectedIconSource" AdornmentIcon="@Icons.Material.Filled.Source" Adornment="Adornment.Start" Label="Your icon source" Variant="Variant.Outlined" Margin="Margin.Dense"> <MudSelect T="IconSources" @bind-Value="@this.selectedIconSource" AdornmentIcon="@Icons.Material.Filled.Source" Adornment="Adornment.Start" Label="Your icon source" Variant="Variant.Outlined" Margin="Margin.Dense">
@foreach (var source in Enum.GetValues<IconSources>()) @foreach (var source in Enum.GetValues<IconSources>())
{ {
@ -31,38 +15,15 @@
{ {
<MudButton Href="@this.selectedIconSource.URL()" Target="_blank" Variant="Variant.Filled" Size="Size.Medium">Open website</MudButton> <MudButton Href="@this.selectedIconSource.URL()" Target="_blank" Variant="Variant.Filled" Size="Size.Medium">Open website</MudButton>
} }
</MudStack> </MudStack>
<MudSelect T="Provider" @bind-Value="@this.selectedProvider" Validation="@this.ValidatingProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-3 rounded-lg" Variant="Variant.Outlined"> <MudSelect T="Provider" @bind-Value="@this.selectedProvider" Validation="@this.ValidatingProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-3 rounded-lg" Variant="Variant.Outlined">
@foreach (var provider in this.SettingsManager.ConfigurationData.Providers) @foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
{ {
<MudSelectItem Value="@provider"/> <MudSelectItem Value="@provider"/>
} }
</MudSelect> </MudSelect>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.FindIcon()"> <MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.FindIcon()">
Find icons Find icon
</MudButton> </MudButton>
</MudForm>
@if (this.inputIssues.Any())
{
<MudPaper Class="pr-2 mt-3" Outlined="@true">
<MudText Typo="Typo.h6">Issues</MudText>
<MudList Clickable="@true">
@foreach (var issue in this.inputIssues)
{
<MudListItem Icon="@Icons.Material.Filled.Error" IconColor="Color.Error">
@issue
</MudListItem>
}
</MudList>
</MudPaper>
}
@if (this.resultingContentBlock is not null)
{
<ContentBlockComponent Role="@this.resultingContentBlock.Role" Type="@this.resultingContentBlock.ContentType" Time="@this.resultingContentBlock.Time" Content="@this.resultingContentBlock.Content" Class="mr-2"/>
}
</ChildContent>
</InnerScrolling>

View File

@ -1,45 +1,26 @@
using AIStudio.Chat;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Pages.IconFinder; namespace AIStudio.Components.Pages.IconFinder;
public partial class AssistantIconFinder : ComponentBase public partial class AssistantIconFinder : AssistantBaseCore
{ {
[Inject]
private SettingsManager SettingsManager { get; set; } = null!;
[Inject]
public IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
public Random RNG { get; set; } = null!;
private ChatThread? chatThread;
private ContentBlock? resultingContentBlock;
private AIStudio.Settings.Provider selectedProvider;
private MudForm form = null!;
private bool inputIsValid;
private string[] inputIssues = [];
private string inputContext = string.Empty; private string inputContext = string.Empty;
private IconSources selectedIconSource; private IconSources selectedIconSource;
#region Overrides of ComponentBase public AssistantIconFinder()
protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
// Reset the validation when not editing and on the first render. this.Title = "Icon Finder";
// We don't want to show validation errors when the user opens the dialog. this.Description =
if(firstRender) """
this.form.ResetValidation(); Finding the right icon for a context, such as for a piece of text, is not easy. The first challenge:
You need to extract a concept from your context, such as from a text. Let's take an example where
await base.OnAfterRenderAsync(firstRender); your text contains statements about multiple departments. The sought-after concept could be "departments."
The next challenge is that we need to anticipate the bias of the icon designers: under the search term
"departments," there may be no relevant icons or only unsuitable ones. Depending on the icon source,
it might be more effective to search for "buildings," for instance. LLMs assist you with both steps.
""";
} }
#endregion
private string? ValidatingContext(string context) private string? ValidatingContext(string context)
{ {
if(string.IsNullOrWhiteSpace(context)) if(string.IsNullOrWhiteSpace(context))
@ -58,72 +39,24 @@ public partial class AssistantIconFinder : ComponentBase
private async Task FindIcon() private async Task FindIcon()
{ {
await this.form.Validate(); await this.form!.Validate();
if (!this.inputIsValid) if (!this.inputIsValid)
return; return;
// this.CreateChatThread();
// Create a new chat thread: var time = this.AddUserRequest(
//
this.chatThread = new()
{
WorkspaceId = Guid.Empty,
ChatId = Guid.NewGuid(),
Name = string.Empty,
Seed = this.RNG.Next(),
SystemPrompt = SYSTEM_PROMPT,
Blocks = [],
};
//
// Add the user's request to the thread:
//
var time = DateTimeOffset.Now;
this.chatThread.Blocks.Add(new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.USER,
Content = new ContentText
{
Text =
$""" $"""
{this.selectedIconSource.Prompt()} I search for an icon for the following context: {this.selectedIconSource.Prompt()} I search for an icon for the following context:
``` ```
{this.inputContext} {this.inputContext}
``` ```
""", """);
},
});
// await this.AddAIResponseAsync(time);
// Add the AI response to the thread:
//
var aiText = new ContentText
{
// We have to wait for the remote
// for the content stream:
InitialRemoteWait = true,
};
this.resultingContentBlock = new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.AI,
Content = aiText,
};
this.chatThread?.Blocks.Add(this.resultingContentBlock);
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
await aiText.CreateFromProviderAsync(this.selectedProvider.UsedProvider.CreateProvider(this.selectedProvider.InstanceName, this.selectedProvider.Hostname), this.JsRuntime, this.SettingsManager, this.selectedProvider.Model, this.chatThread);
} }
private const string SYSTEM_PROMPT = protected override string SystemPrompt =>
""" """
I can search for icons using US English keywords. Please help me come up with the right search queries. I can search for icons using US English keywords. Please help me come up with the right search queries.
I don't want you to translate my requests word-for-word into US English. Instead, you should provide keywords I don't want you to translate my requests word-for-word into US English. Instead, you should provide keywords