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