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";