From caec3bfd2c32721627cbad4b87a830748d6fc1de Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 2 Jan 2025 13:16:47 +0100 Subject: [PATCH] Improved chat UI (#240) --- .../Components/ChatComponent.razor | 91 +++ .../Components/ChatComponent.razor.cs | 619 ++++++++++++++++++ .../Components/InnerScrolling.razor.cs | 7 +- .../Components/Workspaces.razor.cs | 9 +- app/MindWork AI Studio/Pages/Chat.razor | 178 ++--- app/MindWork AI Studio/Pages/Chat.razor.cs | 568 +--------------- .../Settings/DataModel/DataWorkspace.cs | 5 + app/MindWork AI Studio/Tools/Event.cs | 4 + .../wwwroot/changelog/v0.9.23.md | 1 + 9 files changed, 835 insertions(+), 647 deletions(-) create mode 100644 app/MindWork AI Studio/Components/ChatComponent.razor create mode 100644 app/MindWork AI Studio/Components/ChatComponent.razor.cs diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor new file mode 100644 index 0000000..961d65c --- /dev/null +++ b/app/MindWork AI Studio/Components/ChatComponent.razor @@ -0,0 +1,91 @@ +@using AIStudio.Settings.DataModel +@using AIStudio.Chat + +@inherits MSGComponentBase + + + + @if (this.ChatThread is not null) + { + @foreach (var block in this.ChatThread.Blocks.OrderBy(n => n.Time)) + { + @if (!block.HideFromUser) + { + + } + } + } + + + + + + + @if ( + this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES + && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY) + { + + + + } + + @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY) + { + + + + } + + + + + + @if (!string.IsNullOrWhiteSpace(this.currentWorkspaceName)) + { + + + + } + + @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) + { + + + + } + + @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES) + { + + + + } + + @if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence) + { + + } + + + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs new file mode 100644 index 0000000..c22b385 --- /dev/null +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -0,0 +1,619 @@ +using AIStudio.Chat; +using AIStudio.Dialogs; +using AIStudio.Provider; +using AIStudio.Settings; +using AIStudio.Settings.DataModel; + +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; + +using DialogOptions = AIStudio.Dialogs.DialogOptions; + +namespace AIStudio.Components; + +public partial class ChatComponent : MSGComponentBase, IAsyncDisposable +{ + [Parameter] + public ChatThread? ChatThread { get; set; } + + [Parameter] + public EventCallback ChatThreadChanged { get; set; } + + [Parameter] + public Settings.Provider Provider { get; set; } + + [Parameter] + public EventCallback ProviderChanged { get; set; } + + [Parameter] + public Action WorkspaceName { get; set; } = _ => { }; + + [Parameter] + public Workspaces? Workspaces { get; set; } + + [Inject] + private ILogger Logger { get; set; } = null!; + + [Inject] + private ThreadSafeRandom RNG { get; init; } = null!; + + [Inject] + private IDialogService DialogService { get; init; } = null!; + + private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Bottom; + private static readonly Dictionary USER_INPUT_ATTRIBUTES = new(); + + private Profile currentProfile = Profile.NO_PROFILE; + private bool hasUnsavedChanges; + private bool mustScrollToBottomAfterRender; + private InnerScrolling scrollingArea = null!; + private byte scrollRenderCountdown; + private bool isStreaming; + private string userInput = string.Empty; + private bool mustStoreChat; + private bool mustLoadChat; + private LoadChat loadChat; + private bool autoSaveEnabled; + private string currentWorkspaceName = string.Empty; + private Guid currentWorkspaceId = Guid.Empty; + + // Unfortunately, we need the input field reference to blur the focus away. Without + // this, we cannot clear the input field. + private MudTextField inputField = null!; + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.WORKSPACE_LOADED_CHAT_CHANGED ]); + + // Configure the spellchecking for the user input: + this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES); + + this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT); + var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages(Event.SEND_TO_CHAT).FirstOrDefault(); + if (deferredContent is not null) + { + this.ChatThread = deferredContent; + await this.ChatThreadChanged.InvokeAsync(this.ChatThread); + + if (this.ChatThread is not null) + { + if (string.IsNullOrWhiteSpace(this.ChatThread.Name)) + { + var firstUserBlock = this.ChatThread.Blocks.FirstOrDefault(x => x.Role == ChatRole.USER); + if (firstUserBlock is not null) + { + this.ChatThread.Name = firstUserBlock.Content switch + { + ContentText textBlock => this.ExtractThreadName(textBlock.Text), + _ => "Thread" + }; + } + } + + if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) + { + this.autoSaveEnabled = true; + this.mustStoreChat = true; + + // Ensure the workspace exists: + if(this.ChatThread.WorkspaceId == KnownWorkspaces.ERI_SERVER_WORKSPACE_ID) + await WorkspaceBehaviour.EnsureERIServerWorkspace(); + + else if (this.ChatThread.WorkspaceId == KnownWorkspaces.BIAS_WORKSPACE_ID) + await WorkspaceBehaviour.EnsureBiasWorkspace(); + } + } + + if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading) + { + this.mustScrollToBottomAfterRender = true; + this.scrollRenderCountdown = 2; + this.StateHasChanged(); + } + } + + var deferredLoading = MessageBus.INSTANCE.CheckDeferredMessages(Event.LOAD_CHAT).FirstOrDefault(); + if (deferredLoading != default) + { + this.loadChat = deferredLoading; + this.mustLoadChat = true; + } + + await this.SelectProviderWhenLoadingChat(); + await base.OnInitializedAsync(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && this.ChatThread is not null && this.mustStoreChat) + { + this.mustStoreChat = false; + + if(this.Workspaces is not null) + await this.Workspaces.StoreChat(this.ChatThread); + else + await WorkspaceBehaviour.StoreChat(this.ChatThread); + + this.currentWorkspaceId = this.ChatThread.WorkspaceId; + this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId); + this.WorkspaceName(this.currentWorkspaceName); + } + + if (firstRender && this.mustLoadChat) + { + this.mustLoadChat = false; + this.ChatThread = await WorkspaceBehaviour.LoadChat(this.loadChat); + + if(this.ChatThread is not null) + { + this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId); + this.WorkspaceName(this.currentWorkspaceName); + await this.SelectProviderWhenLoadingChat(); + } + + this.StateHasChanged(); + await this.ChatThreadChanged.InvokeAsync(this.ChatThread); + } + + if(this.mustScrollToBottomAfterRender) + { + if (--this.scrollRenderCountdown == 0) + { + await this.scrollingArea.ScrollToBottom(); + this.mustScrollToBottomAfterRender = false; + } + else + { + this.StateHasChanged(); + } + } + + await base.OnAfterRenderAsync(firstRender); + } + + #endregion + + private bool IsProviderSelected => this.Provider.UsedLLMProvider != LLMProviders.NONE; + + private string ProviderPlaceholder => this.IsProviderSelected ? "Type your input here..." : "Select a provider first"; + + private string InputLabel => this.IsProviderSelected ? $"Your Prompt (use selected instance '{this.Provider.InstanceName}', provider '{this.Provider.UsedLLMProvider.ToName()}')" : "Select a provider first"; + + private bool CanThreadBeSaved => this.ChatThread is not null && this.ChatThread.Blocks.Count > 0; + + private string TooltipAddChatToWorkspace => $"Start new chat in workspace \"{this.currentWorkspaceName}\""; + + private string UserInputStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty; + + private string UserInputClass => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? "confidence-border" : string.Empty; + + private string ExtractThreadName(string firstUserInput) + { + // We select the first 10 words of the user input: + var words = firstUserInput.Split(' ', StringSplitOptions.RemoveEmptyEntries); + var threadName = string.Join(' ', words.Take(10)); + + // If the thread name is empty, we use a default name: + if (string.IsNullOrWhiteSpace(threadName)) + threadName = "Thread"; + + return threadName; + } + + private async Task ProfileWasChanged(Profile profile) + { + this.currentProfile = profile; + if(this.ChatThread is null) + return; + + this.ChatThread = this.ChatThread with + { + SystemPrompt = $""" + {SystemPrompts.DEFAULT} + + {this.currentProfile.ToSystemPrompt()} + """ + }; + + await this.ChatThreadChanged.InvokeAsync(this.ChatThread); + } + + private async Task InputKeyEvent(KeyboardEventArgs keyEvent) + { + this.hasUnsavedChanges = true; + var key = keyEvent.Code.ToLowerInvariant(); + + // Was the enter key (either enter or numpad enter) pressed? + var isEnter = key is "enter" or "numpadenter"; + + // Was a modifier key pressed as well? + var isModifier = keyEvent.AltKey || keyEvent.CtrlKey || keyEvent.MetaKey || keyEvent.ShiftKey; + + // Depending on the user's settings, might react to shortcuts: + switch (this.SettingsManager.ConfigurationData.Chat.ShortcutSendBehavior) + { + case SendBehavior.ENTER_IS_SENDING: + if (!isModifier && isEnter) + await this.SendMessage(); + break; + + case SendBehavior.MODIFER_ENTER_IS_SENDING: + if (isEnter && isModifier) + await this.SendMessage(); + break; + } + } + + private async Task SendMessage() + { + if (!this.IsProviderSelected) + return; + + // We need to blur the focus away from the input field + // to be able to clear the field: + await this.inputField.BlurAsync(); + + // Create a new chat thread if necessary: + var threadName = this.ExtractThreadName(this.userInput); + + if (this.ChatThread is null) + { + this.ChatThread = new() + { + SelectedProvider = this.Provider.Id, + WorkspaceId = this.currentWorkspaceId, + ChatId = Guid.NewGuid(), + Name = threadName, + Seed = this.RNG.Next(), + SystemPrompt = $""" + {SystemPrompts.DEFAULT} + + {this.currentProfile.ToSystemPrompt()} + """, + Blocks = [], + }; + + await this.ChatThreadChanged.InvokeAsync(this.ChatThread); + } + else + { + // Set the thread name if it is empty: + if (string.IsNullOrWhiteSpace(this.ChatThread.Name)) + this.ChatThread.Name = threadName; + } + + // + // Add the user message 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.userInput, + }, + }); + + // Save the chat: + if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) + { + await this.SaveThread(); + this.hasUnsavedChanges = false; + this.StateHasChanged(); + } + + // + // 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.ChatThread?.Blocks.Add(new ContentBlock + { + Time = time, + ContentType = ContentType.TEXT, + Role = ChatRole.AI, + Content = aiText, + }); + + // Clear the input field: + this.userInput = string.Empty; + + // Enable the stream state for the chat component: + this.isStreaming = true; + this.hasUnsavedChanges = true; + + if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading) + { + this.mustScrollToBottomAfterRender = true; + this.scrollRenderCountdown = 2; + } + + this.StateHasChanged(); + + this.Logger.LogDebug($"Start processing user input using provider '{this.Provider.InstanceName}' with model '{this.Provider.Model}'."); + + // 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.Provider.CreateProvider(this.Logger), this.SettingsManager, this.Provider.Model, this.ChatThread); + + // Save the chat: + if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) + { + await this.SaveThread(); + this.hasUnsavedChanges = false; + } + + // Disable the stream state: + this.isStreaming = false; + this.StateHasChanged(); + } + + private async Task SaveThread() + { + if(this.ChatThread is null) + return; + + if (!this.CanThreadBeSaved) + return; + + // + // When the workspace component is visible, we store the chat + // through the workspace component. The advantage of this is that + // the workspace gets updated automatically when the chat is saved. + // + if (this.Workspaces is not null) + await this.Workspaces.StoreChat(this.ChatThread); + else + await WorkspaceBehaviour.StoreChat(this.ChatThread); + + this.hasUnsavedChanges = false; + } + + private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false) + { + if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges) + { + var dialogParameters = new DialogParameters + { + { "Message", "Are you sure you want to start a new chat? All unsaved changes will be lost." }, + }; + + var dialogReference = await this.DialogService.ShowAsync("Delete Chat", dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + } + + if (this.ChatThread is not null && deletePreviousChat) + { + string chatPath; + if (this.ChatThread.WorkspaceId == Guid.Empty) + chatPath = Path.Join(SettingsManager.DataDirectory, "tempChats", this.ChatThread.ChatId.ToString()); + else + chatPath = Path.Join(SettingsManager.DataDirectory, "workspaces", this.ChatThread.WorkspaceId.ToString(), this.ChatThread.ChatId.ToString()); + + if(this.Workspaces is null) + await WorkspaceBehaviour.DeleteChat(this.DialogService, this.ChatThread.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false); + else + await this.Workspaces.DeleteChat(chatPath, askForConfirmation: false, unloadChat: true); + } + + this.isStreaming = false; + this.hasUnsavedChanges = false; + this.userInput = string.Empty; + + switch (this.SettingsManager.ConfigurationData.Chat.AddChatProviderBehavior) + { + case AddChatProviderBehavior.ADDED_CHATS_USE_DEFAULT_PROVIDER: + this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT); + await this.ProviderChanged.InvokeAsync(this.Provider); + break; + + default: + case AddChatProviderBehavior.ADDED_CHATS_USE_LATEST_PROVIDER: + if(this.Provider == default) + { + this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT); + await this.ProviderChanged.InvokeAsync(this.Provider); + } + + break; + } + + if (!useSameWorkspace) + { + this.ChatThread = null; + this.currentWorkspaceId = Guid.Empty; + this.currentWorkspaceName = string.Empty; + this.WorkspaceName(this.currentWorkspaceName); + } + else + { + this.ChatThread = new() + { + SelectedProvider = this.Provider.Id, + WorkspaceId = this.currentWorkspaceId, + ChatId = Guid.NewGuid(), + Name = string.Empty, + Seed = this.RNG.Next(), + SystemPrompt = $""" + {SystemPrompts.DEFAULT} + + {this.currentProfile.ToSystemPrompt()} + """, + Blocks = [], + }; + } + + this.userInput = string.Empty; + await this.ChatThreadChanged.InvokeAsync(this.ChatThread); + } + + private async Task MoveChatToWorkspace() + { + if(this.ChatThread is null) + return; + + if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges) + { + var confirmationDialogParameters = new DialogParameters + { + { "Message", "Are you sure you want to move this chat? All unsaved changes will be lost." }, + }; + + var confirmationDialogReference = await this.DialogService.ShowAsync("Unsaved Changes", confirmationDialogParameters, DialogOptions.FULLSCREEN); + var confirmationDialogResult = await confirmationDialogReference.Result; + if (confirmationDialogResult is null || confirmationDialogResult.Canceled) + return; + } + + var dialogParameters = new DialogParameters + { + { "Message", "Please select the workspace where you want to move the chat to." }, + { "SelectedWorkspace", this.ChatThread?.WorkspaceId }, + { "ConfirmText", "Move chat" }, + }; + + var dialogReference = await this.DialogService.ShowAsync("Move Chat to Workspace", dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + var workspaceId = dialogResult.Data is Guid id ? id : Guid.Empty; + if (workspaceId == Guid.Empty) + return; + + // Delete the chat from the current workspace or the temporary storage: + await WorkspaceBehaviour.DeleteChat(this.DialogService, this.ChatThread!.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false); + + this.ChatThread!.WorkspaceId = workspaceId; + await this.SaveThread(); + + this.currentWorkspaceId = this.ChatThread.WorkspaceId; + this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId); + this.WorkspaceName(this.currentWorkspaceName); + } + + private async Task LoadedChatChanged() + { + this.isStreaming = false; + this.hasUnsavedChanges = false; + this.userInput = string.Empty; + this.currentWorkspaceId = this.ChatThread?.WorkspaceId ?? Guid.Empty; + this.currentWorkspaceName = this.ChatThread is null ? string.Empty : await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId); + this.WorkspaceName(this.currentWorkspaceName); + + await this.SelectProviderWhenLoadingChat(); + + this.userInput = string.Empty; + if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading) + { + this.mustScrollToBottomAfterRender = true; + this.scrollRenderCountdown = 2; + this.StateHasChanged(); + } + } + + private async Task ResetState() + { + this.isStreaming = false; + this.hasUnsavedChanges = false; + this.userInput = string.Empty; + this.currentWorkspaceId = Guid.Empty; + + this.currentWorkspaceName = string.Empty; + this.WorkspaceName(this.currentWorkspaceName); + + this.ChatThread = null; + await this.ChatThreadChanged.InvokeAsync(this.ChatThread); + } + + private async Task SelectProviderWhenLoadingChat() + { + var chatProvider = this.ChatThread?.SelectedProvider; + switch (this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior) + { + default: + case LoadingChatProviderBehavior.USE_CHAT_PROVIDER_IF_AVAILABLE: + this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT, chatProvider); + break; + + case LoadingChatProviderBehavior.ALWAYS_USE_DEFAULT_CHAT_PROVIDER: + this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT); + break; + + case LoadingChatProviderBehavior.ALWAYS_USE_LATEST_CHAT_PROVIDER: + if(this.Provider == default) + this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT); + break; + } + + await this.ProviderChanged.InvokeAsync(this.Provider); + } + + private async Task ToggleWorkspaceOverlay() + { + await MessageBus.INSTANCE.SendMessage(this, Event.WORKSPACE_TOGGLE_OVERLAY); + } + + #region Overrides of MSGComponentBase + + public override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default + { + switch (triggeredEvent) + { + case Event.RESET_CHAT_STATE: + await this.ResetState(); + break; + + case Event.CHAT_STREAMING_DONE: + if(this.autoSaveEnabled) + await this.SaveThread(); + break; + + case Event.WORKSPACE_LOADED_CHAT_CHANGED: + await this.LoadedChatChanged(); + break; + } + } + + public override Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default + { + switch (triggeredEvent) + { + case Event.HAS_CHAT_UNSAVED_CHANGES: + if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) + return Task.FromResult((TResult?) (object) false); + + return Task.FromResult((TResult?)(object)this.hasUnsavedChanges); + } + + return Task.FromResult(default(TResult)); + } + + #endregion + + #region Implementation of IAsyncDisposable + + public async ValueTask DisposeAsync() + { + if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) + { + await this.SaveThread(); + this.hasUnsavedChanges = false; + } + } + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/InnerScrolling.razor.cs b/app/MindWork AI Studio/Components/InnerScrolling.razor.cs index 1bce390..126317c 100644 --- a/app/MindWork AI Studio/Components/InnerScrolling.razor.cs +++ b/app/MindWork AI Studio/Components/InnerScrolling.razor.cs @@ -32,6 +32,9 @@ public partial class InnerScrolling : MSGComponentBase [Parameter] public string Class { get; set; } = string.Empty; + [Parameter] + public string? MinWidth { get; set; } + [CascadingParameter] private MainLayout MainLayout { get; set; } = null!; @@ -71,7 +74,9 @@ public partial class InnerScrolling : MSGComponentBase #endregion - private string Styles => this.FillEntireHorizontalSpace ? $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); overflow-x: auto; min-width: 0;" : $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); flex-shrink: 0;"; + private string MinWidthStyle => string.IsNullOrWhiteSpace(this.MinWidth) ? string.Empty : $"min-width: {this.MinWidth};"; + + private string Styles => this.FillEntireHorizontalSpace ? $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); overflow-x: auto; min-width: 0; {this.MinWidthStyle}" : $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); flex-shrink: 0; {this.MinWidthStyle}"; private string Classes => this.FillEntireHorizontalSpace ? $"{this.Class} d-flex flex-column flex-grow-1" : $"{this.Class} d-flex flex-column"; diff --git a/app/MindWork AI Studio/Components/Workspaces.razor.cs b/app/MindWork AI Studio/Components/Workspaces.razor.cs index 2f96f02..1ec3c0e 100644 --- a/app/MindWork AI Studio/Components/Workspaces.razor.cs +++ b/app/MindWork AI Studio/Components/Workspaces.razor.cs @@ -31,9 +31,6 @@ public partial class Workspaces : ComponentBase [Parameter] public EventCallback CurrentChatThreadChanged { get; set; } - [Parameter] - public Func LoadedChatWasChanged { get; set; } = () => Task.CompletedTask; - [Parameter] public bool ExpandRootNodes { get; set; } = true; @@ -273,7 +270,7 @@ public partial class Workspaces : ComponentBase { this.CurrentChatThread = chat; await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread); - await this.LoadedChatWasChanged(); + await MessageBus.INSTANCE.SendMessage(this, Event.WORKSPACE_LOADED_CHAT_CHANGED); } return chat; @@ -325,7 +322,7 @@ public partial class Workspaces : ComponentBase { this.CurrentChatThread = null; await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread); - await this.LoadedChatWasChanged(); + await MessageBus.INSTANCE.SendMessage(this, Event.WORKSPACE_LOADED_CHAT_CHANGED); } } @@ -471,7 +468,7 @@ public partial class Workspaces : ComponentBase { this.CurrentChatThread = chat; await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread); - await this.LoadedChatWasChanged(); + await MessageBus.INSTANCE.SendMessage(this, Event.WORKSPACE_LOADED_CHAT_CHANGED); } await this.StoreChat(chat); diff --git a/app/MindWork AI Studio/Pages/Chat.razor b/app/MindWork AI Studio/Pages/Chat.razor index 5a386cc..2f56c2b 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor +++ b/app/MindWork AI Studio/Pages/Chat.razor @@ -1,7 +1,5 @@ @attribute [Route(Routes.CHAT)] -@using AIStudio.Chat @using AIStudio.Settings.DataModel - @inherits MSGComponentBase @@ -16,113 +14,81 @@ - - @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES - && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR - && !this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible) - { +@if (this.AreWorkspacesVisible) +{ + + + @if (this.AreWorkspacesHidden) + { + + + + + + } + @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES) + { + @if ((this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible) || this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.SIDEBAR_ALWAYS_VISIBLE) + { + @if (this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible) + { + + + + + + + + + + + } + else + { + + + + + + } + } + } + + + + + + + +} +else if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR) +{ + - } - @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES) - { - @if ((this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible) || this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.SIDEBAR_ALWAYS_VISIBLE) - { - @if (this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible) - { - - - - - - - - - - - } - else - { - - - - - - } - } - } - - - @if (this.chatThread is not null) - { - foreach (var block in this.chatThread.Blocks.OrderBy(n => n.Time)) - { - @if (!block.HideFromUser) - { - - } - } - } - - - - - - - @if ( - this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES - && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY) - { - - - - } - - @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY) - { - - - - } - - - - - - @if (!string.IsNullOrWhiteSpace(this.currentWorkspaceName)) - { - - - - } - - @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) - { - - - - } - - @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES) - { - - - - } - - @if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence) - { - - } - - - - - - + + + +} +else +{ + +} @if ( this.SettingsManager.ConfigurationData.Workspace.StorageBehavior != WorkspaceStorageBehavior.DISABLE_WORKSPACES @@ -134,11 +100,11 @@ Your workspaces - + - + } \ No newline at end of file diff --git a/app/MindWork AI Studio/Pages/Chat.razor.cs b/app/MindWork AI Studio/Pages/Chat.razor.cs index 74d27a7..8546b64 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor.cs +++ b/app/MindWork AI Studio/Pages/Chat.razor.cs @@ -1,595 +1,95 @@ using AIStudio.Chat; using AIStudio.Components; -using AIStudio.Dialogs; -using AIStudio.Provider; -using AIStudio.Settings; using AIStudio.Settings.DataModel; using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Web; -using DialogOptions = AIStudio.Dialogs.DialogOptions; +using Timer = System.Timers.Timer; namespace AIStudio.Pages; /// /// The chat page. /// -public partial class Chat : MSGComponentBase, IAsyncDisposable +public partial class Chat : MSGComponentBase { - [Inject] - private ThreadSafeRandom RNG { get; init; } = null!; - - [Inject] - private IDialogService DialogService { get; init; } = null!; - - [Inject] - private ILogger Logger { get; init; } = null!; - - private InnerScrolling scrollingArea = null!; - private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Bottom; - private static readonly Dictionary USER_INPUT_ATTRIBUTES = new(); - private AIStudio.Settings.Provider providerSettings; - private Profile currentProfile = Profile.NO_PROFILE; private ChatThread? chatThread; - private bool hasUnsavedChanges; - private bool isStreaming; - private string userInput = string.Empty; - private string currentWorkspaceName = string.Empty; - private Guid currentWorkspaceId = Guid.Empty; + private AIStudio.Settings.Provider providerSettings; private bool workspaceOverlayVisible; + private string currentWorkspaceName = string.Empty; private Workspaces? workspaces; - private bool mustScrollToBottomAfterRender; - private bool mustStoreChat; - private bool mustLoadChat; - private LoadChat loadChat; - private byte scrollRenderCountdown; - private bool autoSaveEnabled; + private double splitterPosition = 30; - // Unfortunately, we need the input field reference to blur the focus away. Without - // this, we cannot clear the input field. - private MudTextField inputField = null!; + private readonly Timer splitterSaveTimer = new(TimeSpan.FromSeconds(1.6)); #region Overrides of ComponentBase protected override async Task OnInitializedAsync() { - this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE ]); - - // Configure the spellchecking for the user input: - this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES); - - this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT); - var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages(Event.SEND_TO_CHAT).FirstOrDefault(); - if (deferredContent is not null) + this.splitterPosition = this.SettingsManager.ConfigurationData.Workspace.SplitterPosition; + this.splitterSaveTimer.AutoReset = false; + this.splitterSaveTimer.Elapsed += async (_, _) => { - this.chatThread = deferredContent; - if (this.chatThread is not null) - { - if (string.IsNullOrWhiteSpace(this.chatThread.Name)) - { - var firstUserBlock = this.chatThread.Blocks.FirstOrDefault(x => x.Role == ChatRole.USER); - if (firstUserBlock is not null) - { - this.chatThread.Name = firstUserBlock.Content switch - { - ContentText textBlock => this.ExtractThreadName(textBlock.Text), - _ => "Thread" - }; - } - } - - if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) - { - this.autoSaveEnabled = true; - this.mustStoreChat = true; - - // Ensure the workspace exists: - if(this.chatThread.WorkspaceId == KnownWorkspaces.ERI_SERVER_WORKSPACE_ID) - await WorkspaceBehaviour.EnsureERIServerWorkspace(); - - else if (this.chatThread.WorkspaceId == KnownWorkspaces.BIAS_WORKSPACE_ID) - await WorkspaceBehaviour.EnsureBiasWorkspace(); - } - } - - if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading) - { - this.mustScrollToBottomAfterRender = true; - this.scrollRenderCountdown = 2; - this.StateHasChanged(); - } - } + this.SettingsManager.ConfigurationData.Workspace.SplitterPosition = this.splitterPosition; + await this.SettingsManager.StoreSettings(); + }; - var deferredLoading = MessageBus.INSTANCE.CheckDeferredMessages(Event.LOAD_CHAT).FirstOrDefault(); - if (deferredLoading != default) - { - this.loadChat = deferredLoading; - this.mustLoadChat = true; - } - - this.SelectProviderWhenLoadingChat(); await base.OnInitializedAsync(); } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender && this.chatThread is not null && this.mustStoreChat) - { - this.mustStoreChat = false; - - if(this.workspaces is not null) - await this.workspaces.StoreChat(this.chatThread); - else - await WorkspaceBehaviour.StoreChat(this.chatThread); - - this.currentWorkspaceId = this.chatThread.WorkspaceId; - this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.chatThread.WorkspaceId); - } - - if (firstRender && this.mustLoadChat) - { - this.mustLoadChat = false; - this.chatThread = await WorkspaceBehaviour.LoadChat(this.loadChat); - - if(this.chatThread is not null) - { - this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.chatThread.WorkspaceId); - this.SelectProviderWhenLoadingChat(); - } - - this.StateHasChanged(); - } - - if(this.mustScrollToBottomAfterRender) - { - if (--this.scrollRenderCountdown == 0) - { - await this.scrollingArea.ScrollToBottom(); - this.mustScrollToBottomAfterRender = false; - } - else - { - this.StateHasChanged(); - } - } - - await base.OnAfterRenderAsync(firstRender); - } - + #endregion - - private bool IsProviderSelected => this.providerSettings.UsedLLMProvider != LLMProviders.NONE; - - private string ProviderPlaceholder => this.IsProviderSelected ? "Type your input here..." : "Select a provider first"; - - private string InputLabel => this.IsProviderSelected ? $"Your Prompt (use selected instance '{this.providerSettings.InstanceName}', provider '{this.providerSettings.UsedLLMProvider.ToName()}')" : "Select a provider first"; - - private bool CanThreadBeSaved => this.chatThread is not null && this.chatThread.Blocks.Count > 0; - - private string TooltipAddChatToWorkspace => $"Start new chat in workspace \"{this.currentWorkspaceName}\""; - - private string UserInputStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.providerSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty; - - private string UserInputClass => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? "confidence-border" : string.Empty; private string WorkspaceSidebarToggleIcon => this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible ? Icons.Material.Filled.ArrowCircleLeft : Icons.Material.Filled.ArrowCircleRight; - private void ProfileWasChanged(Profile profile) - { - this.currentProfile = profile; - if(this.chatThread is null) - return; + private bool AreWorkspacesVisible => this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES + && ((this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible) + || this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.SIDEBAR_ALWAYS_VISIBLE); - this.chatThread = this.chatThread with - { - SystemPrompt = $""" - {SystemPrompts.DEFAULT} - - {this.currentProfile.ToSystemPrompt()} - """ - }; - } - - private async Task SendMessage() - { - if (!this.IsProviderSelected) - return; - - // We need to blur the focus away from the input field - // to be able to clear the field: - await this.inputField.BlurAsync(); - - // Create a new chat thread if necessary: - var threadName = this.ExtractThreadName(this.userInput); - - if (this.chatThread is null) - { - this.chatThread = new() - { - SelectedProvider = this.providerSettings.Id, - WorkspaceId = this.currentWorkspaceId, - ChatId = Guid.NewGuid(), - Name = threadName, - Seed = this.RNG.Next(), - SystemPrompt = $""" - {SystemPrompts.DEFAULT} - - {this.currentProfile.ToSystemPrompt()} - """, - Blocks = [], - }; - } - else - { - // Set the thread name if it is empty: - if (string.IsNullOrWhiteSpace(this.chatThread.Name)) - this.chatThread.Name = threadName; - } - - // - // Add the user message 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.userInput, - }, - }); - - // Save the chat: - if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) - { - await this.SaveThread(); - this.hasUnsavedChanges = false; - this.StateHasChanged(); - } - - // - // 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.chatThread?.Blocks.Add(new ContentBlock - { - Time = time, - ContentType = ContentType.TEXT, - Role = ChatRole.AI, - Content = aiText, - }); - - // Clear the input field: - this.userInput = string.Empty; - - // Enable the stream state for the chat component: - this.isStreaming = true; - this.hasUnsavedChanges = true; - - if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading) - { - this.mustScrollToBottomAfterRender = true; - this.scrollRenderCountdown = 2; - } - - this.StateHasChanged(); - - // 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.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread); - - // Save the chat: - if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) - { - await this.SaveThread(); - this.hasUnsavedChanges = false; - } - - // Disable the stream state: - this.isStreaming = false; - this.StateHasChanged(); - } - - private async Task InputKeyEvent(KeyboardEventArgs keyEvent) - { - this.hasUnsavedChanges = true; - var key = keyEvent.Code.ToLowerInvariant(); - - // Was the enter key (either enter or numpad enter) pressed? - var isEnter = key is "enter" or "numpadenter"; - - // Was a modifier key pressed as well? - var isModifier = keyEvent.AltKey || keyEvent.CtrlKey || keyEvent.MetaKey || keyEvent.ShiftKey; - - // Depending on the user's settings, might react to shortcuts: - switch (this.SettingsManager.ConfigurationData.Chat.ShortcutSendBehavior) - { - case SendBehavior.ENTER_IS_SENDING: - if (!isModifier && isEnter) - await this.SendMessage(); - break; - - case SendBehavior.MODIFER_ENTER_IS_SENDING: - if (isEnter && isModifier) - await this.SendMessage(); - break; - } - } - - private void ToggleWorkspaceOverlay() - { - this.workspaceOverlayVisible = !this.workspaceOverlayVisible; - } + private bool AreWorkspacesHidden => this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES + && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR + && !this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible; private async Task ToggleWorkspaceSidebar() { this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible = !this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible; await this.SettingsManager.StoreSettings(); } - - private async Task SaveThread() - { - if(this.chatThread is null) - return; - - if (!this.CanThreadBeSaved) - return; - // - // When the workspace component is visible, we store the chat - // through the workspace component. The advantage of this is that - // the workspace gets updated automatically when the chat is saved. - // - if (this.workspaces is not null) - await this.workspaces.StoreChat(this.chatThread); - else - await WorkspaceBehaviour.StoreChat(this.chatThread); - - this.hasUnsavedChanges = false; + private void SplitterChanged(double position) + { + this.splitterPosition = position; + this.splitterSaveTimer.Stop(); + this.splitterSaveTimer.Start(); } - private string ExtractThreadName(string firstUserInput) + private void ToggleWorkspacesOverlay() { - // We select the first 10 words of the user input: - var words = firstUserInput.Split(' ', StringSplitOptions.RemoveEmptyEntries); - var threadName = string.Join(' ', words.Take(10)); - - // If the thread name is empty, we use a default name: - if (string.IsNullOrWhiteSpace(threadName)) - threadName = "Thread"; - - return threadName; - } - - private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false) - { - if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges) - { - var dialogParameters = new DialogParameters - { - { "Message", "Are you sure you want to start a new chat? All unsaved changes will be lost." }, - }; - - var dialogReference = await this.DialogService.ShowAsync("Delete Chat", dialogParameters, DialogOptions.FULLSCREEN); - var dialogResult = await dialogReference.Result; - if (dialogResult is null || dialogResult.Canceled) - return; - } - - if (this.chatThread is not null && deletePreviousChat) - { - string chatPath; - if (this.chatThread.WorkspaceId == Guid.Empty) - chatPath = Path.Join(SettingsManager.DataDirectory, "tempChats", this.chatThread.ChatId.ToString()); - else - chatPath = Path.Join(SettingsManager.DataDirectory, "workspaces", this.chatThread.WorkspaceId.ToString(), this.chatThread.ChatId.ToString()); - - if(this.workspaces is null) - await WorkspaceBehaviour.DeleteChat(this.DialogService, this.chatThread.WorkspaceId, this.chatThread.ChatId, askForConfirmation: false); - else - await this.workspaces.DeleteChat(chatPath, askForConfirmation: false, unloadChat: true); - } - - this.isStreaming = false; - this.hasUnsavedChanges = false; - this.userInput = string.Empty; - - switch (this.SettingsManager.ConfigurationData.Chat.AddChatProviderBehavior) - { - case AddChatProviderBehavior.ADDED_CHATS_USE_DEFAULT_PROVIDER: - this.providerSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT); - break; - - default: - case AddChatProviderBehavior.ADDED_CHATS_USE_LATEST_PROVIDER: - if(this.providerSettings == default) - this.providerSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT); - break; - } - - if (!useSameWorkspace) - { - this.chatThread = null; - this.currentWorkspaceId = Guid.Empty; - this.currentWorkspaceName = string.Empty; - } - else - { - this.chatThread = new() - { - SelectedProvider = this.providerSettings.Id, - WorkspaceId = this.currentWorkspaceId, - ChatId = Guid.NewGuid(), - Name = string.Empty, - Seed = this.RNG.Next(), - SystemPrompt = $""" - {SystemPrompts.DEFAULT} - - {this.currentProfile.ToSystemPrompt()} - """, - Blocks = [], - }; - } - - this.userInput = string.Empty; - } - - private async Task MoveChatToWorkspace() - { - if(this.chatThread is null) - return; - - if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges) - { - var confirmationDialogParameters = new DialogParameters - { - { "Message", "Are you sure you want to move this chat? All unsaved changes will be lost." }, - }; - - var confirmationDialogReference = await this.DialogService.ShowAsync("Unsaved Changes", confirmationDialogParameters, DialogOptions.FULLSCREEN); - var confirmationDialogResult = await confirmationDialogReference.Result; - if (confirmationDialogResult is null || confirmationDialogResult.Canceled) - return; - } - - var dialogParameters = new DialogParameters - { - { "Message", "Please select the workspace where you want to move the chat to." }, - { "SelectedWorkspace", this.chatThread?.WorkspaceId }, - { "ConfirmText", "Move chat" }, - }; - - var dialogReference = await this.DialogService.ShowAsync("Move Chat to Workspace", dialogParameters, DialogOptions.FULLSCREEN); - var dialogResult = await dialogReference.Result; - if (dialogResult is null || dialogResult.Canceled) - return; - - var workspaceId = dialogResult.Data is Guid id ? id : default; - if (workspaceId == Guid.Empty) - return; - - // Delete the chat from the current workspace or the temporary storage: - await WorkspaceBehaviour.DeleteChat(this.DialogService, this.chatThread!.WorkspaceId, this.chatThread.ChatId, askForConfirmation: false); - - this.chatThread!.WorkspaceId = workspaceId; - await this.SaveThread(); - - this.currentWorkspaceId = this.chatThread.WorkspaceId; - this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.chatThread.WorkspaceId); - } - - private async Task LoadedChatChanged() - { - // - // It should not happen that the workspace component is not loaded - // because the workspace component is calling this method. - // - if(this.workspaces is null) - return; - - this.isStreaming = false; - this.hasUnsavedChanges = false; - this.userInput = string.Empty; - this.currentWorkspaceId = this.chatThread?.WorkspaceId ?? Guid.Empty; - this.currentWorkspaceName = this.chatThread is null ? string.Empty : await WorkspaceBehaviour.LoadWorkspaceName(this.chatThread.WorkspaceId); - - this.SelectProviderWhenLoadingChat(); - - this.userInput = string.Empty; - if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading) - { - this.mustScrollToBottomAfterRender = true; - this.scrollRenderCountdown = 2; - this.StateHasChanged(); - } - } - - private void ResetState() - { - this.isStreaming = false; - this.hasUnsavedChanges = false; - this.userInput = string.Empty; - this.currentWorkspaceId = Guid.Empty; - this.currentWorkspaceName = string.Empty; - this.chatThread = null; - } - - private void SelectProviderWhenLoadingChat() - { - var chatProvider = this.chatThread?.SelectedProvider; - switch (this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior) - { - default: - case LoadingChatProviderBehavior.USE_CHAT_PROVIDER_IF_AVAILABLE: - this.providerSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT, chatProvider); - break; - - case LoadingChatProviderBehavior.ALWAYS_USE_DEFAULT_CHAT_PROVIDER: - this.providerSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT); - break; - - case LoadingChatProviderBehavior.ALWAYS_USE_LATEST_CHAT_PROVIDER: - if(this.providerSettings == default) - this.providerSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT); - break; - } + this.workspaceOverlayVisible = !this.workspaceOverlayVisible; + this.StateHasChanged(); } + + private double ReadSplitterPosition => this.AreWorkspacesHidden ? 6 : this.splitterPosition; #region Overrides of MSGComponentBase - public override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default + public override Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default { switch (triggeredEvent) { - case Event.RESET_CHAT_STATE: - this.ResetState(); - break; - - case Event.CHAT_STREAMING_DONE: - if(this.autoSaveEnabled) - await this.SaveThread(); + case Event.WORKSPACE_TOGGLE_OVERLAY: + this.ToggleWorkspacesOverlay(); break; } + + return Task.CompletedTask; } public override Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default { - switch (triggeredEvent) - { - case Event.HAS_CHAT_UNSAVED_CHANGES: - if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) - return Task.FromResult((TResult?) (object) false); - - return Task.FromResult((TResult?)(object)this.hasUnsavedChanges); - } - return Task.FromResult(default(TResult)); } #endregion - - #region Implementation of IAsyncDisposable - - public async ValueTask DisposeAsync() - { - if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) - { - await this.SaveThread(); - this.hasUnsavedChanges = false; - } - } - - #endregion } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataWorkspace.cs b/app/MindWork AI Studio/Settings/DataModel/DataWorkspace.cs index 2fee8cb..7317970 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataWorkspace.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataWorkspace.cs @@ -21,4 +21,9 @@ public sealed class DataWorkspace /// Indicates whether the sidebar is currently visible. /// public bool IsSidebarVisible { get; set; } = true; + + /// + /// The position of the splitter between the chat and the workspaces. + /// + public double SplitterPosition { get; set; } = 30; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Event.cs b/app/MindWork AI Studio/Tools/Event.cs index 2dde28a..37a855f 100644 --- a/app/MindWork AI Studio/Tools/Event.cs +++ b/app/MindWork AI Studio/Tools/Event.cs @@ -19,6 +19,10 @@ public enum Event LOAD_CHAT, CHAT_STREAMING_DONE, + // Workspace events: + WORKSPACE_LOADED_CHAT_CHANGED, + WORKSPACE_TOGGLE_OVERLAY, + // Send events: SEND_TO_GRAMMAR_SPELLING_ASSISTANT, SEND_TO_ICON_FINDER_ASSISTANT, diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.23.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.23.md index e8a2ba5..67a0b50 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.23.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.23.md @@ -1,5 +1,6 @@ # v0.9.23, build 198 (2024-12-xx xx:xx UTC) - Added an ERI server coding assistant as a preview feature behind the RAG feature flag. This helps you implement an ERI server to gain access to, e.g., your enterprise data from within AI Studio. +- Improved the chat UI: You can now set the aspect ratio between workspaces and chat as you like. - Improved provider requests by handling rate limits by retrying requests. - Improved the creation of the "the bias of the day" workspace; create that workspace only when the bias of the day feature is used. - Improved the save operation of settings by using a temporary file to avoid data loss in rare cases.