diff --git a/app/MindWork AI Studio/Components/MSGComponentBase.cs b/app/MindWork AI Studio/Components/MSGComponentBase.cs
index 08f5416f..940ec78e 100644
--- a/app/MindWork AI Studio/Components/MSGComponentBase.cs
+++ b/app/MindWork AI Studio/Components/MSGComponentBase.cs
@@ -1,9 +1,14 @@
+using AIStudio.Settings;
+
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver
{
+ [Inject]
+ protected SettingsManager SettingsManager { get; init; } = null!;
+
[Inject]
protected MessageBus MessageBus { get; init; } = null!;
diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
index 4581ee30..060e86bd 100644
--- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs
+++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
@@ -103,6 +103,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
new("Home", Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true),
new("Chat", Icons.Material.Filled.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false),
new("Assistants", Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false),
+ new("Writer", Icons.Material.Filled.Create, palette.DarkLighten, palette.GrayLight, Routes.WRITER, false),
new("Supporters", Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false),
new("About", Icons.Material.Filled.Info, palette.DarkLighten, palette.GrayLight, Routes.ABOUT, false),
new("Settings", Icons.Material.Filled.Settings, palette.DarkLighten, palette.GrayLight, Routes.SETTINGS, false),
diff --git a/app/MindWork AI Studio/Pages/Chat.razor.cs b/app/MindWork AI Studio/Pages/Chat.razor.cs
index 4a4d7ed0..8db196a2 100644
--- a/app/MindWork AI Studio/Pages/Chat.razor.cs
+++ b/app/MindWork AI Studio/Pages/Chat.razor.cs
@@ -17,9 +17,6 @@ namespace AIStudio.Pages;
///
public partial class Chat : MSGComponentBase, IAsyncDisposable
{
- [Inject]
- private SettingsManager SettingsManager { get; init; } = null!;
-
[Inject]
private ThreadSafeRandom RNG { get; init; } = null!;
diff --git a/app/MindWork AI Studio/Pages/Writer.razor b/app/MindWork AI Studio/Pages/Writer.razor
new file mode 100644
index 00000000..2f4df0b9
--- /dev/null
+++ b/app/MindWork AI Studio/Pages/Writer.razor
@@ -0,0 +1,55 @@
+@attribute [Route(Routes.WRITER)]
+@inherits MSGComponentBase
+
+
+ Writer
+
+
+
+
+
+
+
+
+
+
+ @if (this.isStreaming)
+ {
+
+ }
+
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Pages/Writer.razor.cs b/app/MindWork AI Studio/Pages/Writer.razor.cs
new file mode 100644
index 00000000..7b6a8cf5
--- /dev/null
+++ b/app/MindWork AI Studio/Pages/Writer.razor.cs
@@ -0,0 +1,177 @@
+using AIStudio.Chat;
+using AIStudio.Components;
+using AIStudio.Provider;
+
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Web;
+
+using Timer = System.Timers.Timer;
+
+namespace AIStudio.Pages;
+
+public partial class Writer : MSGComponentBase, IAsyncDisposable
+{
+ [Inject]
+ private ILogger Logger { get; init; } = null!;
+
+ private static readonly Dictionary USER_INPUT_ATTRIBUTES = new();
+ private readonly Timer typeTimer = new(TimeSpan.FromMilliseconds(1_500));
+
+ private MudTextField textField = null!;
+ private AIStudio.Settings.Provider providerSettings;
+ private ChatThread? chatThread;
+ private bool isStreaming;
+ private string userInput = string.Empty;
+ private string userDirection = string.Empty;
+ private string suggestion = string.Empty;
+
+ #region Overrides of ComponentBase
+
+ protected override async Task OnInitializedAsync()
+ {
+ this.ApplyFilters([], []);
+ this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
+ this.typeTimer.Elapsed += async (_, _) => await this.InvokeAsync(this.GetSuggestions);
+ this.typeTimer.AutoReset = false;
+
+ await base.OnInitializedAsync();
+ }
+
+ #endregion
+
+ #region Overrides of MSGComponentBase
+
+ public override Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
+ {
+ return Task.CompletedTask;
+ }
+
+ public override Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
+ {
+ return Task.FromResult(default(TResult));
+ }
+
+ #endregion
+
+ private bool IsProviderSelected => this.providerSettings.UsedLLMProvider != LLMProviders.NONE;
+
+ private async Task InputKeyEvent(KeyboardEventArgs keyEvent)
+ {
+ var key = keyEvent.Code.ToLowerInvariant();
+ var isTab = key is "tab";
+ var isModifier = keyEvent.AltKey || keyEvent.CtrlKey || keyEvent.MetaKey || keyEvent.ShiftKey;
+
+ if (isTab && !isModifier)
+ {
+ await this.textField.FocusAsync();
+ this.AcceptNextWord();
+ return;
+ }
+
+ if (isTab && isModifier)
+ {
+ await this.textField.FocusAsync();
+ this.AcceptEntireSuggestion();
+ return;
+ }
+
+ if(!isModifier)
+ {
+ this.typeTimer.Stop();
+ this.typeTimer.Start();
+ }
+ }
+
+ private async Task GetSuggestions()
+ {
+ if (!this.IsProviderSelected)
+ return;
+
+ this.chatThread ??= new()
+ {
+ WorkspaceId = Guid.Empty,
+ ChatId = Guid.NewGuid(),
+ Name = string.Empty,
+ Seed = 798798,
+ SystemPrompt = """
+ You are an assistant who helps with writing documents. You receive a sample
+ from a document as input. As output, you provide how the begun sentence could
+ continue. You give exactly one variant, not multiple. If the current sentence
+ is complete, you provide an empty response. You do not ask questions, and you
+ do not repeat the task.
+ """,
+ Blocks = [],
+ };
+
+ var time = DateTimeOffset.Now;
+ this.chatThread.Blocks.Clear();
+ this.chatThread.Blocks.Add(new ContentBlock
+ {
+ Time = time,
+ ContentType = ContentType.TEXT,
+ Role = ChatRole.USER,
+ Content = new ContentText
+ {
+ // We use the maximum 160 characters from the end of the text:
+ Text = this.userInput.Length > 160 ? this.userInput[^160..] : this.userInput,
+ },
+ });
+
+ var aiText = new ContentText
+ {
+ // We have to wait for the remote
+ // for the content stream:
+ InitialRemoteWait = true,
+ };
+
+ this.chatThread?.Blocks.Add(new ContentBlock
+ {
+ Time = time,
+ ContentType = ContentType.TEXT,
+ Role = ChatRole.AI,
+ Content = aiText,
+ });
+
+ this.isStreaming = true;
+ this.StateHasChanged();
+
+ await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
+ this.suggestion = aiText.Text;
+
+ this.isStreaming = false;
+ this.StateHasChanged();
+ }
+
+ private void AcceptEntireSuggestion()
+ {
+ if(this.userInput.Last() != ' ')
+ this.userInput += ' ';
+
+ this.userInput += this.suggestion;
+ this.suggestion = string.Empty;
+ this.StateHasChanged();
+ }
+
+ private void AcceptNextWord()
+ {
+ var words = this.suggestion.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if(words.Length == 0)
+ return;
+
+ if(this.userInput.Last() != ' ')
+ this.userInput += ' ';
+
+ this.userInput += words[0] + ' ';
+ this.suggestion = string.Join(' ', words.Skip(1));
+ this.StateHasChanged();
+ }
+
+ #region Implementation of IAsyncDisposable
+
+ public ValueTask DisposeAsync()
+ {
+ return ValueTask.CompletedTask;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Routes.razor.cs b/app/MindWork AI Studio/Routes.razor.cs
index 03ef143d..ea1a2d68 100644
--- a/app/MindWork AI Studio/Routes.razor.cs
+++ b/app/MindWork AI Studio/Routes.razor.cs
@@ -8,6 +8,7 @@ public sealed partial class Routes
public const string ASSISTANTS = "/assistants";
public const string SETTINGS = "/settings";
public const string SUPPORTERS = "/supporters";
+ public const string WRITER = "/writer";
// ReSharper disable InconsistentNaming
public const string ASSISTANT_TRANSLATION = "/assistant/translation";