diff --git a/app/MindWork AI Studio/Components/AssistantBase.razor b/app/MindWork AI Studio/Components/AssistantBase.razor
new file mode 100644
index 00000000..e04bb340
--- /dev/null
+++ b/app/MindWork AI Studio/Components/AssistantBase.razor
@@ -0,0 +1,39 @@
+@using AIStudio.Chat
+
+ @this.Title
+
+
+
+
+
+
+ @this.Description
+
+
+ @if (this.Body is not null)
+ {
+ @this.Body
+ }
+
+
+ @if (this.inputIssues.Any())
+ {
+
+ Issues
+
+ @foreach (var issue in this.inputIssues)
+ {
+
+ @issue
+
+ }
+
+
+ }
+
+ @if (this.resultingContentBlock is not null)
+ {
+
+ }
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/AssistantBase.razor.cs b/app/MindWork AI Studio/Components/AssistantBase.razor.cs
new file mode 100644
index 00000000..a15afe4a
--- /dev/null
+++ b/app/MindWork AI Studio/Components/AssistantBase.razor.cs
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/AssistantBaseCore.cs b/app/MindWork AI Studio/Components/AssistantBaseCore.cs
new file mode 100644
index 00000000..5508b14f
--- /dev/null
+++ b/app/MindWork AI Studio/Components/AssistantBaseCore.cs
@@ -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)
+ {
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor b/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor
index a7b8132d..1e5e214d 100644
--- a/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor
+++ b/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor
@@ -1,68 +1,29 @@
@page "/assistant/icons"
-@using AIStudio.Chat
@using AIStudio.Settings
+@inherits AssistantBaseCore
-
- Icon Finder
-
+
-
-
-
-
- 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.
-
-
-
-
-
-
- @foreach (var source in Enum.GetValues())
- {
- @source.Name()
- }
-
- @if (this.selectedIconSource is not IconSources.GENERIC)
- {
- Open website
- }
-
-
-
- @foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
- {
-
- }
-
-
-
- Find icons
-
-
-
- @if (this.inputIssues.Any())
+
+
+ @foreach (var source in Enum.GetValues())
{
-
- Issues
-
- @foreach (var issue in this.inputIssues)
- {
-
- @issue
-
- }
-
-
+ @source.Name()
}
+
+ @if (this.selectedIconSource is not IconSources.GENERIC)
+ {
+ Open website
+ }
+
- @if (this.resultingContentBlock is not null)
- {
-
- }
-
-
\ No newline at end of file
+
+ @foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
+ {
+
+ }
+
+
+
+ Find icon
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor.cs b/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor.cs
index 4f173f7e..5443cf80 100644
--- a/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor.cs
+++ b/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor.cs
@@ -1,45 +1,26 @@
-using AIStudio.Chat;
using AIStudio.Provider;
-using AIStudio.Settings;
-
-using Microsoft.AspNetCore.Components;
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 IconSources selectedIconSource;
-
- #region Overrides of ComponentBase
-
- protected override async Task OnAfterRenderAsync(bool firstRender)
+
+ public AssistantIconFinder()
{
- // 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);
+ this.Title = "Icon Finder";
+ this.Description =
+ """
+ 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.
+ """;
}
-
- #endregion
-
+
private string? ValidatingContext(string context)
{
if(string.IsNullOrWhiteSpace(context))
@@ -58,72 +39,24 @@ public partial class AssistantIconFinder : ComponentBase
private async Task FindIcon()
{
- await this.form.Validate();
+ await this.form!.Validate();
if (!this.inputIsValid)
return;
- //
- // Create a new chat thread:
- //
- 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.inputContext}
- ```
- """,
- },
- });
-
- //
- // 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.CreateChatThread();
+ var time = this.AddUserRequest(
+ $"""
+ {this.selectedIconSource.Prompt()} I search for an icon for the following context:
+
+ ```
+ {this.inputContext}
+ ```
+ """);
- 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);
+ await this.AddAIResponseAsync(time);
}
- 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 don't want you to translate my requests word-for-word into US English. Instead, you should provide keywords