mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-02-05 10:49:07 +00:00
Improved chat UI (#240)
This commit is contained in:
parent
12b3d6fc3d
commit
caec3bfd2c
91
app/MindWork AI Studio/Components/ChatComponent.razor
Normal file
91
app/MindWork AI Studio/Components/ChatComponent.razor
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
@using AIStudio.Settings.DataModel
|
||||||
|
@using AIStudio.Chat
|
||||||
|
|
||||||
|
@inherits MSGComponentBase
|
||||||
|
|
||||||
|
<InnerScrolling FillEntireHorizontalSpace="@true" @ref="@this.scrollingArea" HeaderHeight="12.3em" MinWidth="36em">
|
||||||
|
<ChildContent>
|
||||||
|
@if (this.ChatThread is not null)
|
||||||
|
{
|
||||||
|
@foreach (var block in this.ChatThread.Blocks.OrderBy(n => n.Time))
|
||||||
|
{
|
||||||
|
@if (!block.HideFromUser)
|
||||||
|
{
|
||||||
|
<ContentBlockComponent Role="@block.Role" Type="@block.ContentType" Time="@block.Time" Content="@block.Content"/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</ChildContent>
|
||||||
|
<FooterContent>
|
||||||
|
<MudElement Style="flex: 0 0 auto;">
|
||||||
|
<MudTextField
|
||||||
|
T="string"
|
||||||
|
@ref="@this.inputField"
|
||||||
|
@bind-Text="@this.userInput"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
AutoGrow="@true"
|
||||||
|
Lines="3"
|
||||||
|
MaxLines="12"
|
||||||
|
Label="@this.InputLabel"
|
||||||
|
Placeholder="@this.ProviderPlaceholder"
|
||||||
|
Adornment="Adornment.End"
|
||||||
|
AdornmentIcon="@Icons.Material.Filled.Send"
|
||||||
|
OnAdornmentClick="() => this.SendMessage()"
|
||||||
|
ReadOnly="!this.IsProviderSelected || this.isStreaming"
|
||||||
|
Immediate="@true"
|
||||||
|
OnKeyUp="this.InputKeyEvent"
|
||||||
|
UserAttributes="@USER_INPUT_ATTRIBUTES"
|
||||||
|
Class="@this.UserInputClass"
|
||||||
|
Style="@this.UserInputStyle"/>
|
||||||
|
</MudElement>
|
||||||
|
<MudToolBar WrapContent="true" Gutters="@false" Class="border border-solid rounded" Style="border-color: lightgrey;">
|
||||||
|
@if (
|
||||||
|
this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
|
||||||
|
&& this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="Show your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.SnippetFolder" OnClick="() => this.ToggleWorkspaceOverlay()"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="Save chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="() => this.SaveThread()" Disabled="@(!this.CanThreadBeSaved)"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudTooltip Text="Start temporary chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.AddComment" OnClick="() => this.StartNewChat(useSameWorkspace: false)"/>
|
||||||
|
</MudTooltip>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(this.currentWorkspaceName))
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@this.TooltipAddChatToWorkspace" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.CommentBank" OnClick="() => this.StartNewChat(useSameWorkspace: true)"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="Delete this chat & start a new one" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true)" Disabled="@(!this.CanThreadBeSaved)"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="Move the chat to a workspace, or to another if it is already in one." Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="() => this.MoveChatToWorkspace()"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
|
||||||
|
{
|
||||||
|
<ConfidenceInfo Mode="ConfidenceInfoMode.ICON" LLMProvider="@this.Provider.UsedLLMProvider"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<ProfileSelection CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged"/>
|
||||||
|
</MudToolBar>
|
||||||
|
</FooterContent>
|
||||||
|
</InnerScrolling>
|
619
app/MindWork AI Studio/Components/ChatComponent.razor.cs
Normal file
619
app/MindWork AI Studio/Components/ChatComponent.razor.cs
Normal file
@ -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<ChatThread?> ChatThreadChanged { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Settings.Provider Provider { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<Settings.Provider> ProviderChanged { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Action<string> WorkspaceName { get; set; } = _ => { };
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Workspaces? Workspaces { get; set; }
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private ILogger<ChatComponent> 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<string, object?> 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<string> 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<ChatThread>(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<LoadChat>(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<ConfirmDialog>("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<ConfirmDialog>("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<WorkspaceSelectionDialog>("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<bool>(this, Event.WORKSPACE_TOGGLE_OVERLAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Overrides of MSGComponentBase
|
||||||
|
|
||||||
|
public override async Task ProcessIncomingMessage<T>(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<TResult?> ProcessMessageWithResult<TPayload, TResult>(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
|
||||||
|
}
|
@ -32,6 +32,9 @@ public partial class InnerScrolling : MSGComponentBase
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public string Class { get; set; } = string.Empty;
|
public string Class { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? MinWidth { get; set; }
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
private MainLayout MainLayout { get; set; } = null!;
|
private MainLayout MainLayout { get; set; } = null!;
|
||||||
|
|
||||||
@ -71,7 +74,9 @@ public partial class InnerScrolling : MSGComponentBase
|
|||||||
|
|
||||||
#endregion
|
#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";
|
private string Classes => this.FillEntireHorizontalSpace ? $"{this.Class} d-flex flex-column flex-grow-1" : $"{this.Class} d-flex flex-column";
|
||||||
|
|
||||||
|
@ -31,9 +31,6 @@ public partial class Workspaces : ComponentBase
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<ChatThread> CurrentChatThreadChanged { get; set; }
|
public EventCallback<ChatThread> CurrentChatThreadChanged { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public Func<Task> LoadedChatWasChanged { get; set; } = () => Task.CompletedTask;
|
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool ExpandRootNodes { get; set; } = true;
|
public bool ExpandRootNodes { get; set; } = true;
|
||||||
|
|
||||||
@ -273,7 +270,7 @@ public partial class Workspaces : ComponentBase
|
|||||||
{
|
{
|
||||||
this.CurrentChatThread = chat;
|
this.CurrentChatThread = chat;
|
||||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||||
await this.LoadedChatWasChanged();
|
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||||
}
|
}
|
||||||
|
|
||||||
return chat;
|
return chat;
|
||||||
@ -325,7 +322,7 @@ public partial class Workspaces : ComponentBase
|
|||||||
{
|
{
|
||||||
this.CurrentChatThread = null;
|
this.CurrentChatThread = null;
|
||||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||||
await this.LoadedChatWasChanged();
|
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -471,7 +468,7 @@ public partial class Workspaces : ComponentBase
|
|||||||
{
|
{
|
||||||
this.CurrentChatThread = chat;
|
this.CurrentChatThread = chat;
|
||||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||||
await this.LoadedChatWasChanged();
|
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.StoreChat(chat);
|
await this.StoreChat(chat);
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
@attribute [Route(Routes.CHAT)]
|
@attribute [Route(Routes.CHAT)]
|
||||||
@using AIStudio.Chat
|
|
||||||
@using AIStudio.Settings.DataModel
|
@using AIStudio.Settings.DataModel
|
||||||
|
|
||||||
@inherits MSGComponentBase
|
@inherits MSGComponentBase
|
||||||
|
|
||||||
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
|
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
|
||||||
@ -16,113 +14,81 @@
|
|||||||
</MudText>
|
</MudText>
|
||||||
|
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.providerSettings"/>
|
<ProviderSelection @bind-ProviderSettings="@this.providerSettings"/>
|
||||||
<MudStack Row="@true" Style="width: 100%; overflow: hidden;">
|
@if (this.AreWorkspacesVisible)
|
||||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
|
{
|
||||||
&& this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR
|
<MudSplitter Dimension="@this.ReadSplitterPosition" DimensionChanged="this.SplitterChanged" EnableSlide="@this.AreWorkspacesVisible" EnableMargin="@false" StartContentStyle="margin-right: 1em;" BarStyle="" EndContentStyle="margin-left: 1em;">
|
||||||
&& !this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible)
|
<StartContent>
|
||||||
{
|
@if (this.AreWorkspacesHidden)
|
||||||
|
{
|
||||||
|
<MudPaper Class="border border-solid rounded-lg">
|
||||||
|
<MudTooltip Text="Show your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Size="Size.Large" Icon="@this.WorkspaceSidebarToggleIcon" OnClick="() => this.ToggleWorkspaceSidebar()"/>
|
||||||
|
</MudTooltip>
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
@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)
|
||||||
|
{
|
||||||
|
<InnerScrolling HeaderHeight="12.3em" FillEntireHorizontalSpace="@true" Class="border border-solid rounded-lg" MinWidth="26em">
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTooltip Text="Hide your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
|
<MudIconButton Size="Size.Large" Icon="@this.WorkspaceSidebarToggleIcon" OnClick="() => this.ToggleWorkspaceSidebar()"/>
|
||||||
|
</MudTooltip>
|
||||||
|
</HeaderContent>
|
||||||
|
<ChildContent>
|
||||||
|
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" />
|
||||||
|
</ChildContent>
|
||||||
|
</InnerScrolling>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<InnerScrolling HeaderHeight="12.3em" FillEntireHorizontalSpace="@true" Class="border border-solid rounded-lg" MinWidth="26em">
|
||||||
|
<ChildContent>
|
||||||
|
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" />
|
||||||
|
</ChildContent>
|
||||||
|
</InnerScrolling>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</StartContent>
|
||||||
|
<EndContent>
|
||||||
|
|
||||||
|
<ChatComponent
|
||||||
|
@bind-ChatThread="@this.chatThread"
|
||||||
|
@bind-Provider="@this.providerSettings"
|
||||||
|
Workspaces="@this.workspaces"
|
||||||
|
WorkspaceName="name => this.currentWorkspaceName = name" />
|
||||||
|
|
||||||
|
</EndContent>
|
||||||
|
</MudSplitter>
|
||||||
|
}
|
||||||
|
else if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR)
|
||||||
|
{
|
||||||
|
<MudStack Row="@true" Style="width: 100%; overflow: hidden;">
|
||||||
<MudPaper Class="border border-solid rounded-lg">
|
<MudPaper Class="border border-solid rounded-lg">
|
||||||
<MudTooltip Text="Show your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
<MudTooltip Text="Show your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||||
<MudIconButton Size="Size.Large" Icon="@this.WorkspaceSidebarToggleIcon" OnClick="() => this.ToggleWorkspaceSidebar()"/>
|
<MudIconButton Size="Size.Large" Icon="@this.WorkspaceSidebarToggleIcon" OnClick="() => this.ToggleWorkspaceSidebar()"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
}
|
|
||||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
|
<ChatComponent
|
||||||
{
|
@bind-ChatThread="@this.chatThread"
|
||||||
@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)
|
@bind-Provider="@this.providerSettings"
|
||||||
{
|
Workspaces="@this.workspaces"
|
||||||
@if (this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible)
|
WorkspaceName="name => this.currentWorkspaceName = name"/>
|
||||||
{
|
</MudStack>
|
||||||
<InnerScrolling HeaderHeight="12.3em" Class="border border-solid rounded-lg">
|
}
|
||||||
<HeaderContent>
|
else
|
||||||
<MudTooltip Text="Hide your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
{
|
||||||
<MudIconButton Size="Size.Large" Icon="@this.WorkspaceSidebarToggleIcon" OnClick="() => this.ToggleWorkspaceSidebar()"/>
|
<ChatComponent
|
||||||
</MudTooltip>
|
@bind-ChatThread="@this.chatThread"
|
||||||
</HeaderContent>
|
@bind-Provider="@this.providerSettings"
|
||||||
<ChildContent>
|
Workspaces="@this.workspaces"
|
||||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" LoadedChatWasChanged="this.LoadedChatChanged"/>
|
WorkspaceName="name => this.currentWorkspaceName = name"/>
|
||||||
</ChildContent>
|
}
|
||||||
</InnerScrolling>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<InnerScrolling HeaderHeight="12.3em" Class="border border-solid rounded-lg">
|
|
||||||
<ChildContent>
|
|
||||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" LoadedChatWasChanged="this.LoadedChatChanged"/>
|
|
||||||
</ChildContent>
|
|
||||||
</InnerScrolling>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<InnerScrolling FillEntireHorizontalSpace="@true" @ref="@this.scrollingArea" HeaderHeight="12.3em">
|
|
||||||
<ChildContent>
|
|
||||||
@if (this.chatThread is not null)
|
|
||||||
{
|
|
||||||
foreach (var block in this.chatThread.Blocks.OrderBy(n => n.Time))
|
|
||||||
{
|
|
||||||
@if (!block.HideFromUser)
|
|
||||||
{
|
|
||||||
<ContentBlockComponent Role="@block.Role" Type="@block.ContentType" Time="@block.Time" Content="@block.Content"/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</ChildContent>
|
|
||||||
<FooterContent>
|
|
||||||
<MudElement Style="flex: 0 0 auto;">
|
|
||||||
<MudTextField T="string" @ref="@this.inputField" @bind-Text="@this.userInput" Variant="Variant.Outlined" AutoGrow="@true" Lines="3" MaxLines="12" Label="@this.InputLabel" Placeholder="@this.ProviderPlaceholder" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Send" OnAdornmentClick="() => this.SendMessage()" ReadOnly="!this.IsProviderSelected || this.isStreaming" Immediate="@true" OnKeyUp="this.InputKeyEvent" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="@this.UserInputClass" Style="@this.UserInputStyle"/>
|
|
||||||
</MudElement>
|
|
||||||
<MudToolBar WrapContent="true" Gutters="@false" Class="border border-solid rounded" Style="border-color: lightgrey;">
|
|
||||||
@if (
|
|
||||||
this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
|
|
||||||
&& this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY)
|
|
||||||
{
|
|
||||||
<MudTooltip Text="Show your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.SnippetFolder" OnClick="() => this.ToggleWorkspaceOverlay()"/>
|
|
||||||
</MudTooltip>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY)
|
|
||||||
{
|
|
||||||
<MudTooltip Text="Save chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="() => this.SaveThread()" Disabled="@(!this.CanThreadBeSaved)"/>
|
|
||||||
</MudTooltip>
|
|
||||||
}
|
|
||||||
|
|
||||||
<MudTooltip Text="Start temporary chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.AddComment" OnClick="() => this.StartNewChat(useSameWorkspace: false)"/>
|
|
||||||
</MudTooltip>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(this.currentWorkspaceName))
|
|
||||||
{
|
|
||||||
<MudTooltip Text="@this.TooltipAddChatToWorkspace" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.CommentBank" OnClick="() => this.StartNewChat(useSameWorkspace: true)"/>
|
|
||||||
</MudTooltip>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
|
||||||
{
|
|
||||||
<MudTooltip Text="Delete this chat & start a new one" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true)" Disabled="@(!this.CanThreadBeSaved)"/>
|
|
||||||
</MudTooltip>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
|
|
||||||
{
|
|
||||||
<MudTooltip Text="Move the chat to a workspace, or to another if it is already in one." Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="() => this.MoveChatToWorkspace()"/>
|
|
||||||
</MudTooltip>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
|
|
||||||
{
|
|
||||||
<ConfidenceInfo Mode="ConfidenceInfoMode.ICON" LLMProvider="@this.providerSettings.UsedLLMProvider"/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<ProfileSelection CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" />
|
|
||||||
</MudToolBar>
|
|
||||||
</FooterContent>
|
|
||||||
</InnerScrolling>
|
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
@if (
|
@if (
|
||||||
this.SettingsManager.ConfigurationData.Workspace.StorageBehavior != WorkspaceStorageBehavior.DISABLE_WORKSPACES
|
this.SettingsManager.ConfigurationData.Workspace.StorageBehavior != WorkspaceStorageBehavior.DISABLE_WORKSPACES
|
||||||
@ -134,11 +100,11 @@
|
|||||||
<MudText Typo="Typo.h6" Class="mr-3">
|
<MudText Typo="Typo.h6" Class="mr-3">
|
||||||
Your workspaces
|
Your workspaces
|
||||||
</MudText>
|
</MudText>
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Close" Variant="Variant.Filled" Color="Color.Default" Size="Size.Small" OnClick="() => this.ToggleWorkspaceOverlay()"/>
|
<MudIconButton Icon="@Icons.Material.Filled.Close" Variant="Variant.Filled" Color="Color.Default" Size="Size.Small" OnClick="() => this.ToggleWorkspacesOverlay()"/>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudDrawerHeader>
|
</MudDrawerHeader>
|
||||||
<MudDrawerContainer Class="ml-6">
|
<MudDrawerContainer Class="ml-6">
|
||||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" LoadedChatWasChanged="this.LoadedChatChanged"/>
|
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" />
|
||||||
</MudDrawerContainer>
|
</MudDrawerContainer>
|
||||||
</MudDrawer>
|
</MudDrawer>
|
||||||
}
|
}
|
@ -1,595 +1,95 @@
|
|||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
using AIStudio.Components;
|
using AIStudio.Components;
|
||||||
using AIStudio.Dialogs;
|
|
||||||
using AIStudio.Provider;
|
|
||||||
using AIStudio.Settings;
|
|
||||||
using AIStudio.Settings.DataModel;
|
using AIStudio.Settings.DataModel;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
|
||||||
|
|
||||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
namespace AIStudio.Pages;
|
namespace AIStudio.Pages;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The chat page.
|
/// The chat page.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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<Chat> Logger { get; init; } = null!;
|
|
||||||
|
|
||||||
private InnerScrolling scrollingArea = null!;
|
|
||||||
|
|
||||||
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Bottom;
|
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Bottom;
|
||||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
|
||||||
|
|
||||||
private AIStudio.Settings.Provider providerSettings;
|
|
||||||
private Profile currentProfile = Profile.NO_PROFILE;
|
|
||||||
private ChatThread? chatThread;
|
private ChatThread? chatThread;
|
||||||
private bool hasUnsavedChanges;
|
private AIStudio.Settings.Provider providerSettings;
|
||||||
private bool isStreaming;
|
|
||||||
private string userInput = string.Empty;
|
|
||||||
private string currentWorkspaceName = string.Empty;
|
|
||||||
private Guid currentWorkspaceId = Guid.Empty;
|
|
||||||
private bool workspaceOverlayVisible;
|
private bool workspaceOverlayVisible;
|
||||||
|
private string currentWorkspaceName = string.Empty;
|
||||||
private Workspaces? workspaces;
|
private Workspaces? workspaces;
|
||||||
private bool mustScrollToBottomAfterRender;
|
private double splitterPosition = 30;
|
||||||
private bool mustStoreChat;
|
|
||||||
private bool mustLoadChat;
|
|
||||||
private LoadChat loadChat;
|
|
||||||
private byte scrollRenderCountdown;
|
|
||||||
private bool autoSaveEnabled;
|
|
||||||
|
|
||||||
// Unfortunately, we need the input field reference to blur the focus away. Without
|
private readonly Timer splitterSaveTimer = new(TimeSpan.FromSeconds(1.6));
|
||||||
// this, we cannot clear the input field.
|
|
||||||
private MudTextField<string> inputField = null!;
|
|
||||||
|
|
||||||
#region Overrides of ComponentBase
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE ]);
|
this.splitterPosition = this.SettingsManager.ConfigurationData.Workspace.SplitterPosition;
|
||||||
|
this.splitterSaveTimer.AutoReset = false;
|
||||||
// Configure the spellchecking for the user input:
|
this.splitterSaveTimer.Elapsed += async (_, _) =>
|
||||||
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
|
||||||
|
|
||||||
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
|
|
||||||
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<ChatThread>(Event.SEND_TO_CHAT).FirstOrDefault();
|
|
||||||
if (deferredContent is not null)
|
|
||||||
{
|
{
|
||||||
this.chatThread = deferredContent;
|
this.SettingsManager.ConfigurationData.Workspace.SplitterPosition = this.splitterPosition;
|
||||||
if (this.chatThread is not null)
|
await this.SettingsManager.StoreSettings();
|
||||||
{
|
};
|
||||||
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<LoadChat>(Event.LOAD_CHAT).FirstOrDefault();
|
|
||||||
if (deferredLoading != default)
|
|
||||||
{
|
|
||||||
this.loadChat = deferredLoading;
|
|
||||||
this.mustLoadChat = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.SelectProviderWhenLoadingChat();
|
|
||||||
await base.OnInitializedAsync();
|
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
|
#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 string WorkspaceSidebarToggleIcon => this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible ? Icons.Material.Filled.ArrowCircleLeft : Icons.Material.Filled.ArrowCircleRight;
|
||||||
|
|
||||||
private void ProfileWasChanged(Profile profile)
|
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.currentProfile = profile;
|
|| this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.SIDEBAR_ALWAYS_VISIBLE);
|
||||||
if(this.chatThread is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.chatThread = this.chatThread with
|
private bool AreWorkspacesHidden => this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
|
||||||
{
|
&& this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR
|
||||||
SystemPrompt = $"""
|
&& !this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible;
|
||||||
{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 async Task ToggleWorkspaceSidebar()
|
private async Task ToggleWorkspaceSidebar()
|
||||||
{
|
{
|
||||||
this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible = !this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible;
|
this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible = !this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible;
|
||||||
await this.SettingsManager.StoreSettings();
|
await this.SettingsManager.StoreSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveThread()
|
|
||||||
{
|
|
||||||
if(this.chatThread is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!this.CanThreadBeSaved)
|
|
||||||
return;
|
|
||||||
|
|
||||||
//
|
private void SplitterChanged(double position)
|
||||||
// When the workspace component is visible, we store the chat
|
{
|
||||||
// through the workspace component. The advantage of this is that
|
this.splitterPosition = position;
|
||||||
// the workspace gets updated automatically when the chat is saved.
|
this.splitterSaveTimer.Stop();
|
||||||
//
|
this.splitterSaveTimer.Start();
|
||||||
if (this.workspaces is not null)
|
|
||||||
await this.workspaces.StoreChat(this.chatThread);
|
|
||||||
else
|
|
||||||
await WorkspaceBehaviour.StoreChat(this.chatThread);
|
|
||||||
|
|
||||||
this.hasUnsavedChanges = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ExtractThreadName(string firstUserInput)
|
private void ToggleWorkspacesOverlay()
|
||||||
{
|
{
|
||||||
// We select the first 10 words of the user input:
|
this.workspaceOverlayVisible = !this.workspaceOverlayVisible;
|
||||||
var words = firstUserInput.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
this.StateHasChanged();
|
||||||
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<ConfirmDialog>("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<ConfirmDialog>("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<WorkspaceSelectionDialog>("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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private double ReadSplitterPosition => this.AreWorkspacesHidden ? 6 : this.splitterPosition;
|
||||||
|
|
||||||
#region Overrides of MSGComponentBase
|
#region Overrides of MSGComponentBase
|
||||||
|
|
||||||
public override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||||
{
|
{
|
||||||
switch (triggeredEvent)
|
switch (triggeredEvent)
|
||||||
{
|
{
|
||||||
case Event.RESET_CHAT_STATE:
|
case Event.WORKSPACE_TOGGLE_OVERLAY:
|
||||||
this.ResetState();
|
this.ToggleWorkspacesOverlay();
|
||||||
break;
|
|
||||||
|
|
||||||
case Event.CHAT_STREAMING_DONE:
|
|
||||||
if(this.autoSaveEnabled)
|
|
||||||
await this.SaveThread();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(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));
|
return Task.FromResult(default(TResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#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
|
|
||||||
}
|
}
|
@ -21,4 +21,9 @@ public sealed class DataWorkspace
|
|||||||
/// Indicates whether the sidebar is currently visible.
|
/// Indicates whether the sidebar is currently visible.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsSidebarVisible { get; set; } = true;
|
public bool IsSidebarVisible { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The position of the splitter between the chat and the workspaces.
|
||||||
|
/// </summary>
|
||||||
|
public double SplitterPosition { get; set; } = 30;
|
||||||
}
|
}
|
@ -19,6 +19,10 @@ public enum Event
|
|||||||
LOAD_CHAT,
|
LOAD_CHAT,
|
||||||
CHAT_STREAMING_DONE,
|
CHAT_STREAMING_DONE,
|
||||||
|
|
||||||
|
// Workspace events:
|
||||||
|
WORKSPACE_LOADED_CHAT_CHANGED,
|
||||||
|
WORKSPACE_TOGGLE_OVERLAY,
|
||||||
|
|
||||||
// Send events:
|
// Send events:
|
||||||
SEND_TO_GRAMMAR_SPELLING_ASSISTANT,
|
SEND_TO_GRAMMAR_SPELLING_ASSISTANT,
|
||||||
SEND_TO_ICON_FINDER_ASSISTANT,
|
SEND_TO_ICON_FINDER_ASSISTANT,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# v0.9.23, build 198 (2024-12-xx xx:xx UTC)
|
# 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.
|
- 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 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 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.
|
- Improved the save operation of settings by using a temporary file to avoid data loss in rare cases.
|
||||||
|
Loading…
Reference in New Issue
Block a user