mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 22:13:47 +00:00
Improved workspace performance (#680)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
This commit is contained in:
1 parent
721d5c9070
commit
906d9ba058
16 files changed
+994
-494
No files matched your search
@@ -56,6 +56,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
private bool autoSaveEnabled;
|
||||
private string currentWorkspaceName = string.Empty;
|
||||
private Guid currentWorkspaceId = Guid.Empty;
|
||||
private Guid currentChatThreadId = Guid.Empty;
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
private HashSet<FileAttachment> chatDocumentPaths = [];
|
||||
|
||||
@@ -197,8 +198,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
//
|
||||
if (this.ChatThread is not null)
|
||||
{
|
||||
this.currentChatThreadId = this.ChatThread.ChatId;
|
||||
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId);
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
}
|
||||
|
||||
@@ -214,12 +216,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.mustStoreChat = false;
|
||||
|
||||
if(this.Workspaces is not null)
|
||||
await this.Workspaces.StoreChat(this.ChatThread);
|
||||
await this.Workspaces.StoreChatAsync(this.ChatThread);
|
||||
else
|
||||
await WorkspaceBehaviour.StoreChat(this.ChatThread);
|
||||
await WorkspaceBehaviour.StoreChatAsync(this.ChatThread);
|
||||
|
||||
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId);
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
}
|
||||
|
||||
@@ -227,14 +229,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
{
|
||||
this.Logger.LogInformation($"Try to load the chat '{this.loadChat.ChatId}' now.");
|
||||
this.mustLoadChat = false;
|
||||
this.ChatThread = await WorkspaceBehaviour.LoadChat(this.loadChat);
|
||||
this.ChatThread = await WorkspaceBehaviour.LoadChatAsync(this.loadChat);
|
||||
|
||||
if(this.ChatThread is not null)
|
||||
{
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
this.Logger.LogInformation($"The chat '{this.ChatThread!.ChatId}' with title '{this.ChatThread.Name}' ({this.ChatThread.Blocks.Count} messages) was loaded successfully.");
|
||||
|
||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId);
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
await this.SelectProviderWhenLoadingChat();
|
||||
}
|
||||
@@ -260,7 +262,49 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task SyncWorkspaceHeaderWithChatThreadAsync()
|
||||
{
|
||||
if (this.ChatThread is null)
|
||||
{
|
||||
if (this.currentChatThreadId != Guid.Empty || this.currentWorkspaceId != Guid.Empty || !string.IsNullOrWhiteSpace(this.currentWorkspaceName))
|
||||
{
|
||||
this.currentChatThreadId = Guid.Empty;
|
||||
this.currentWorkspaceId = Guid.Empty;
|
||||
this.currentWorkspaceName = string.Empty;
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard: If ChatThread ID and WorkspaceId haven't changed, skip entirely.
|
||||
// Using ID-based comparison instead of name-based to correctly handle
|
||||
// temporary chats where the workspace name is always empty.
|
||||
if (this.currentChatThreadId == this.ChatThread.ChatId
|
||||
&& this.currentWorkspaceId == this.ChatThread.WorkspaceId)
|
||||
return;
|
||||
|
||||
this.currentChatThreadId = this.ChatThread.ChatId;
|
||||
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
||||
var loadedWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId);
|
||||
|
||||
// Only notify the parent when the name actually changed to prevent
|
||||
// an infinite render loop: WorkspaceName → UpdateWorkspaceName →
|
||||
// StateHasChanged → re-render → OnParametersSetAsync → WorkspaceName → ...
|
||||
if (this.currentWorkspaceName != loadedWorkspaceName)
|
||||
{
|
||||
this.currentWorkspaceName = loadedWorkspaceName;
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsProviderSelected => this.Provider.UsedLLMProvider != LLMProviders.NONE;
|
||||
|
||||
@@ -428,8 +472,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
|
||||
return;
|
||||
|
||||
// We need to blur the focus away from the input field
|
||||
// to be able to clear the field:
|
||||
// Blur the focus away from the input field to be able to clear it:
|
||||
await this.inputField.BlurAsync();
|
||||
|
||||
// Create a new chat thread if necessary:
|
||||
@@ -520,8 +563,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
// Clear the input field:
|
||||
await this.inputField.FocusAsync();
|
||||
|
||||
this.userInput = string.Empty;
|
||||
this.chatDocumentPaths.Clear();
|
||||
|
||||
await this.inputField.BlurAsync();
|
||||
|
||||
// Enable the stream state for the chat component:
|
||||
@@ -583,9 +628,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
// the workspace gets updated automatically when the chat is saved.
|
||||
//
|
||||
if (this.Workspaces is not null)
|
||||
await this.Workspaces.StoreChat(this.ChatThread);
|
||||
await this.Workspaces.StoreChatAsync(this.ChatThread);
|
||||
else
|
||||
await WorkspaceBehaviour.StoreChat(this.ChatThread);
|
||||
await WorkspaceBehaviour.StoreChatAsync(this.ChatThread);
|
||||
|
||||
this.hasUnsavedChanges = false;
|
||||
}
|
||||
@@ -621,9 +666,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
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);
|
||||
await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, this.ChatThread.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false);
|
||||
else
|
||||
await this.Workspaces.DeleteChat(chatPath, askForConfirmation: false, unloadChat: true);
|
||||
await this.Workspaces.DeleteChatAsync(chatPath, askForConfirmation: false, unloadChat: true);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -665,6 +710,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
// to reset the chat thread:
|
||||
//
|
||||
this.ChatThread = null;
|
||||
this.currentChatThreadId = Guid.Empty;
|
||||
this.currentWorkspaceId = Guid.Empty;
|
||||
this.currentWorkspaceName = string.Empty;
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
@@ -739,13 +785,13 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
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);
|
||||
await WorkspaceBehaviour.DeleteChatAsync(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.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId);
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
}
|
||||
|
||||
@@ -758,12 +804,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if (this.ChatThread is not null)
|
||||
{
|
||||
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId);
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
this.currentChatThreadId = this.ChatThread.ChatId;
|
||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.currentChatThreadId = Guid.Empty;
|
||||
this.currentWorkspaceId = Guid.Empty;
|
||||
this.currentWorkspaceName = string.Empty;
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
@@ -785,6 +833,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.currentChatThreadId = Guid.Empty;
|
||||
this.currentWorkspaceId = Guid.Empty;
|
||||
|
||||
this.currentWorkspaceName = string.Empty;
|
||||
|
||||
@@ -4,6 +4,7 @@ public enum TreeItemType
|
||||
{
|
||||
NONE,
|
||||
|
||||
LOADING,
|
||||
CHAT,
|
||||
WORKSPACE,
|
||||
}
|
||||
@@ -1,93 +1,114 @@
|
||||
@inherits MSGComponentBase
|
||||
<MudTreeView T="ITreeItem" Items="@this.treeItems" SelectionMode="SelectionMode.SingleSelection" Hover="@true" ExpandOnClick="@true" Class="ma-3">
|
||||
<ItemTemplate Context="item">
|
||||
@switch (item.Value)
|
||||
@if (this.isInitialLoading)
|
||||
{
|
||||
<MudStack Class="ma-3" Spacing="1">
|
||||
<MudSkeleton Width="40%" Height="30px"/>
|
||||
@for (var i = 0; i < 10; i++)
|
||||
{
|
||||
case TreeDivider:
|
||||
<li style="min-height: 1em;">
|
||||
<MudDivider Style="margin-top: 1em; width: 90%; border-width: 3pt;"/>
|
||||
</li>
|
||||
break;
|
||||
|
||||
case TreeItemData treeItem:
|
||||
@if (treeItem.Type is TreeItemType.CHAT)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@treeItem.Children" OnClick="@(() => this.LoadChat(treeItem.Path, true))">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
@if (string.IsNullOrWhiteSpace(treeItem.Text))
|
||||
{
|
||||
@T("Empty chat")
|
||||
}
|
||||
else
|
||||
{
|
||||
@treeItem.ShortenedText
|
||||
}
|
||||
</MudText>
|
||||
<div style="justify-self: end;">
|
||||
|
||||
<MudTooltip Text="@T("Move to workspace")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.MoveChat(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.RenameChat(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Delete")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteChat(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
}
|
||||
else if (treeItem.Type is TreeItemType.WORKSPACE)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@treeItem.Children">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
@treeItem.Text
|
||||
</MudText>
|
||||
<div style="justify-self: end;">
|
||||
<MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.RenameWorkspace(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Delete")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteWorkspace(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@treeItem.Children">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
@treeItem.Text
|
||||
</MudText>
|
||||
</div>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
}
|
||||
break;
|
||||
|
||||
case TreeButton treeButton:
|
||||
<li>
|
||||
<div class="mud-treeview-item-content" style="background-color: unset;">
|
||||
<div class="mud-treeview-item-arrow"></div>
|
||||
<MudButton StartIcon="@treeButton.Icon" Variant="Variant.Filled" OnClick="@treeButton.Action">
|
||||
@treeButton.Text
|
||||
</MudButton>
|
||||
</div>
|
||||
</li>
|
||||
break;
|
||||
<MudSkeleton Width="95%" Height="26px"/>
|
||||
}
|
||||
</ItemTemplate>
|
||||
</MudTreeView>
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTreeView T="ITreeItem" Items="@this.treeItems" SelectionMode="SelectionMode.SingleSelection" Hover="@true" ExpandOnClick="@true" Class="ma-3">
|
||||
<ItemTemplate Context="item">
|
||||
@switch (item.Value)
|
||||
{
|
||||
case TreeDivider:
|
||||
<li style="min-height: 1em;">
|
||||
<MudDivider Style="margin-top: 1em; width: 90%; border-width: 3pt;"/>
|
||||
</li>
|
||||
break;
|
||||
|
||||
case TreeItemData treeItem:
|
||||
@if (treeItem.Type is TreeItemType.LOADING)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@false" Items="@treeItem.Children">
|
||||
<BodyContent>
|
||||
<MudSkeleton Width="85%" Height="22px"/>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
}
|
||||
else if (treeItem.Type is TreeItemType.CHAT)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@treeItem.Children" OnClick="@(() => this.LoadChatAsync(treeItem.Path, true))">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
@if (string.IsNullOrWhiteSpace(treeItem.Text))
|
||||
{
|
||||
@T("Empty chat")
|
||||
}
|
||||
else
|
||||
{
|
||||
@treeItem.ShortenedText
|
||||
}
|
||||
</MudText>
|
||||
<div style="justify-self: end;">
|
||||
|
||||
<MudTooltip Text="@T("Move to workspace")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.MoveChatAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.RenameChatAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Delete")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteChatAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
}
|
||||
else if (treeItem.Type is TreeItemType.WORKSPACE)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@treeItem.Children" OnClick="@(() => this.OnWorkspaceClicked(treeItem))">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
@treeItem.Text
|
||||
</MudText>
|
||||
<div style="justify-self: end;">
|
||||
<MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.RenameWorkspaceAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Delete")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteWorkspaceAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@treeItem.Children">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
@treeItem.Text
|
||||
</MudText>
|
||||
</div>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
}
|
||||
break;
|
||||
|
||||
case TreeButton treeButton:
|
||||
<li>
|
||||
<div class="mud-treeview-item-content" style="background-color: unset;">
|
||||
<div class="mud-treeview-item-arrow"></div>
|
||||
<MudButton StartIcon="@treeButton.Icon" Variant="Variant.Filled" OnClick="@treeButton.Action">
|
||||
@treeButton.Text
|
||||
</MudButton>
|
||||
</div>
|
||||
</li>
|
||||
break;
|
||||
}
|
||||
</ItemTemplate>
|
||||
</MudTreeView>
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Text;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
@@ -29,31 +29,64 @@ public partial class Workspaces : MSGComponentBase
|
||||
public bool ExpandRootNodes { get; set; } = true;
|
||||
|
||||
private const Placement WORKSPACE_ITEM_TOOLTIP_PLACEMENT = Placement.Bottom;
|
||||
private readonly SemaphoreSlim treeLoadingSemaphore = new(1, 1);
|
||||
private readonly List<TreeItemData<ITreeItem>> treeItems = [];
|
||||
private readonly HashSet<Guid> loadingWorkspaceChatLists = [];
|
||||
|
||||
private readonly List<TreeItemData<ITreeItem>> treeItems = new();
|
||||
private CancellationTokenSource? prefetchCancellationTokenSource;
|
||||
private bool isInitialLoading = true;
|
||||
private bool isDisposed;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
//
|
||||
// Notice: In order to get the server-based loading to work, we need to respect the following rules:
|
||||
// - We must have initial tree items
|
||||
// - Those initial tree items cannot have children
|
||||
// - When assigning the tree items to the MudTreeViewItem component, we must set the Value property to the value of the item
|
||||
//
|
||||
// We won't await the loading of the tree items here,
|
||||
// to avoid blocking the UI thread:
|
||||
_ = this.LoadTreeItems();
|
||||
_ = this.LoadTreeItemsAsync(startPrefetch: true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task LoadTreeItems()
|
||||
private async Task LoadTreeItemsAsync(bool startPrefetch = true, bool forceReload = false)
|
||||
{
|
||||
await this.treeLoadingSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (this.isDisposed)
|
||||
return;
|
||||
|
||||
if (forceReload)
|
||||
await WorkspaceBehaviour.ForceReloadWorkspaceTreeAsync();
|
||||
|
||||
var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
|
||||
this.BuildTreeItems(snapshot);
|
||||
this.isInitialLoading = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.treeLoadingSemaphore.Release();
|
||||
}
|
||||
|
||||
await this.SafeStateHasChanged();
|
||||
|
||||
if (startPrefetch)
|
||||
await this.StartPrefetchAsync();
|
||||
}
|
||||
|
||||
private void BuildTreeItems(WorkspaceTreeCacheSnapshot snapshot)
|
||||
{
|
||||
this.treeItems.Clear();
|
||||
|
||||
var workspaceChildren = new List<TreeItemData<ITreeItem>>();
|
||||
foreach (var workspace in snapshot.Workspaces)
|
||||
workspaceChildren.Add(this.CreateWorkspaceTreeItem(workspace));
|
||||
|
||||
workspaceChildren.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = false,
|
||||
Value = new TreeButton(WorkspaceBranch.WORKSPACES, 1, T("Add workspace"), Icons.Material.Filled.LibraryAdd, this.AddWorkspaceAsync),
|
||||
});
|
||||
|
||||
this.treeItems.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expanded = this.ExpandRootNodes,
|
||||
@@ -66,7 +99,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
Icon = Icons.Material.Filled.Folder,
|
||||
Expandable = true,
|
||||
Path = "root",
|
||||
Children = await this.LoadWorkspaces(),
|
||||
Children = workspaceChildren,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -76,7 +109,10 @@ public partial class Workspaces : MSGComponentBase
|
||||
Value = new TreeDivider(),
|
||||
});
|
||||
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
var temporaryChatsChildren = new List<TreeItemData<ITreeItem>>();
|
||||
foreach (var temporaryChat in snapshot.TemporaryChats.OrderByDescending(x => x.LastEditTime))
|
||||
temporaryChatsChildren.Add(CreateChatTreeItem(temporaryChat, WorkspaceBranch.TEMPORARY_CHATS, depth: 1, icon: Icons.Material.Filled.Timer));
|
||||
|
||||
this.treeItems.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expanded = this.ExpandRootNodes,
|
||||
@@ -89,234 +125,219 @@ public partial class Workspaces : MSGComponentBase
|
||||
Icon = Icons.Material.Filled.Timer,
|
||||
Expandable = true,
|
||||
Path = "temp",
|
||||
Children = await this.LoadTemporaryChats(),
|
||||
Children = temporaryChatsChildren,
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private TreeItemData<ITreeItem> CreateWorkspaceTreeItem(WorkspaceTreeWorkspace workspace)
|
||||
{
|
||||
var children = new List<TreeItemData<ITreeItem>>();
|
||||
if (workspace.ChatsLoaded)
|
||||
{
|
||||
foreach (var workspaceChat in workspace.Chats.OrderByDescending(x => x.LastEditTime))
|
||||
children.Add(CreateChatTreeItem(workspaceChat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat));
|
||||
}
|
||||
else if (this.loadingWorkspaceChatLists.Contains(workspace.WorkspaceId))
|
||||
children.AddRange(this.CreateLoadingRows(workspace.WorkspacePath));
|
||||
|
||||
children.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = false,
|
||||
Value = new TreeButton(WorkspaceBranch.WORKSPACES, 2, T("Add chat"), Icons.Material.Filled.AddComment, () => this.AddChatAsync(workspace.WorkspacePath)),
|
||||
});
|
||||
|
||||
return new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = true,
|
||||
Value = new TreeItemData
|
||||
{
|
||||
Type = TreeItemType.WORKSPACE,
|
||||
Depth = 1,
|
||||
Branch = WorkspaceBranch.WORKSPACES,
|
||||
Text = workspace.Name,
|
||||
Icon = Icons.Material.Filled.Description,
|
||||
Expandable = true,
|
||||
Path = workspace.WorkspacePath,
|
||||
Children = children,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private IReadOnlyCollection<TreeItemData<ITreeItem>> CreateLoadingRows(string workspacePath)
|
||||
{
|
||||
return
|
||||
[
|
||||
this.CreateLoadingTreeItem(workspacePath, "loading_1"),
|
||||
this.CreateLoadingTreeItem(workspacePath, "loading_2"),
|
||||
this.CreateLoadingTreeItem(workspacePath, "loading_3"),
|
||||
];
|
||||
}
|
||||
|
||||
private TreeItemData<ITreeItem> CreateLoadingTreeItem(string workspacePath, string suffix)
|
||||
{
|
||||
return new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = false,
|
||||
Value = new TreeItemData
|
||||
{
|
||||
Type = TreeItemType.LOADING,
|
||||
Depth = 2,
|
||||
Branch = WorkspaceBranch.WORKSPACES,
|
||||
Text = T("Loading chats..."),
|
||||
Icon = Icons.Material.Filled.HourglassTop,
|
||||
Expandable = false,
|
||||
Path = Path.Join(workspacePath, suffix),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static TreeItemData<ITreeItem> CreateChatTreeItem(WorkspaceTreeChat chat, WorkspaceBranch branch, int depth, string icon)
|
||||
{
|
||||
return new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = false,
|
||||
Value = new TreeItemData
|
||||
{
|
||||
Type = TreeItemType.CHAT,
|
||||
Depth = depth,
|
||||
Branch = branch,
|
||||
Text = chat.Name,
|
||||
Icon = icon,
|
||||
Expandable = false,
|
||||
Path = chat.ChatPath,
|
||||
LastEditTime = chat.LastEditTime,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async Task SafeStateHasChanged()
|
||||
{
|
||||
if (this.isDisposed)
|
||||
return;
|
||||
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyCollection<TreeItemData<ITreeItem>>> LoadTemporaryChats()
|
||||
private async Task StartPrefetchAsync()
|
||||
{
|
||||
var tempChildren = new List<TreeItemData>();
|
||||
|
||||
// Get the temp root directory:
|
||||
var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats");
|
||||
|
||||
// Ensure the directory exists:
|
||||
Directory.CreateDirectory(temporaryDirectories);
|
||||
|
||||
// Enumerate the chat directories:
|
||||
foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories))
|
||||
if (this.prefetchCancellationTokenSource is not null)
|
||||
{
|
||||
// Read or create the `name` file (self-heal):
|
||||
var chatNamePath = Path.Join(tempChatDirPath, "name");
|
||||
string chatName;
|
||||
try
|
||||
await this.prefetchCancellationTokenSource.CancelAsync();
|
||||
this.prefetchCancellationTokenSource.Dispose();
|
||||
}
|
||||
|
||||
this.prefetchCancellationTokenSource = new CancellationTokenSource();
|
||||
await this.PrefetchWorkspaceChatsAsync(this.prefetchCancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
private async Task PrefetchWorkspaceChatsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await WorkspaceBehaviour.TryPrefetchRemainingChatsAsync(async _ =>
|
||||
{
|
||||
if (!File.Exists(chatNamePath))
|
||||
{
|
||||
chatName = T("Unnamed chat");
|
||||
await File.WriteAllTextAsync(chatNamePath, chatName, Encoding.UTF8);
|
||||
}
|
||||
else
|
||||
{
|
||||
chatName = await File.ReadAllTextAsync(chatNamePath, Encoding.UTF8);
|
||||
if (string.IsNullOrWhiteSpace(chatName))
|
||||
{
|
||||
chatName = T("Unnamed chat");
|
||||
await File.WriteAllTextAsync(chatNamePath, chatName, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
chatName = T("Unnamed chat");
|
||||
}
|
||||
|
||||
// Read the last change time of the chat:
|
||||
var chatThreadPath = Path.Join(tempChatDirPath, "thread.json");
|
||||
var lastEditTime = File.GetLastWriteTimeUtc(chatThreadPath);
|
||||
|
||||
tempChildren.Add(new TreeItemData
|
||||
{
|
||||
Type = TreeItemType.CHAT,
|
||||
Depth = 1,
|
||||
Branch = WorkspaceBranch.TEMPORARY_CHATS,
|
||||
Text = chatName,
|
||||
Icon = Icons.Material.Filled.Timer,
|
||||
Expandable = false,
|
||||
Path = tempChatDirPath,
|
||||
LastEditTime = lastEditTime,
|
||||
});
|
||||
if (this.isDisposed || cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected when the component is hidden or disposed.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.LogWarning(ex, "Failed while prefetching workspace chats.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnWorkspaceClicked(TreeItemData treeItem)
|
||||
{
|
||||
if (treeItem.Type is not TreeItemType.WORKSPACE)
|
||||
return;
|
||||
|
||||
if (!Guid.TryParse(Path.GetFileName(treeItem.Path), out var workspaceId))
|
||||
return;
|
||||
|
||||
await this.EnsureWorkspaceChatsLoadedAsync(workspaceId);
|
||||
}
|
||||
|
||||
private async Task EnsureWorkspaceChatsLoadedAsync(Guid workspaceId)
|
||||
{
|
||||
var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
|
||||
var hasWorkspace = false;
|
||||
var chatsLoaded = false;
|
||||
|
||||
foreach (var workspace in snapshot.Workspaces)
|
||||
{
|
||||
if (workspace.WorkspaceId != workspaceId)
|
||||
continue;
|
||||
|
||||
hasWorkspace = true;
|
||||
chatsLoaded = workspace.ChatsLoaded;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!hasWorkspace || chatsLoaded || !this.loadingWorkspaceChatLists.Add(workspaceId))
|
||||
return;
|
||||
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
|
||||
try
|
||||
{
|
||||
await WorkspaceBehaviour.GetWorkspaceChatsAsync(workspaceId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.loadingWorkspaceChatLists.Remove(workspaceId);
|
||||
}
|
||||
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}
|
||||
|
||||
public async Task ForceRefreshFromDiskAsync()
|
||||
{
|
||||
if (this.prefetchCancellationTokenSource is not null)
|
||||
{
|
||||
await this.prefetchCancellationTokenSource.CancelAsync();
|
||||
this.prefetchCancellationTokenSource.Dispose();
|
||||
this.prefetchCancellationTokenSource = null;
|
||||
}
|
||||
|
||||
var result = new List<TreeItemData<ITreeItem>>(tempChildren.OrderByDescending(n => n.LastEditTime).Select(n => new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = false,
|
||||
Value = n,
|
||||
}));
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyCollection<TreeItemData<ITreeItem>>> LoadWorkspaces()
|
||||
{
|
||||
var workspaces = new List<TreeItemData<ITreeItem>>();
|
||||
this.loadingWorkspaceChatLists.Clear();
|
||||
this.isInitialLoading = true;
|
||||
|
||||
//
|
||||
// Search for workspace folders in the data directory:
|
||||
//
|
||||
|
||||
// Get the workspace root directory:
|
||||
var workspaceDirectories = Path.Join(SettingsManager.DataDirectory, "workspaces");
|
||||
|
||||
// Ensure the directory exists:
|
||||
Directory.CreateDirectory(workspaceDirectories);
|
||||
|
||||
// Enumerate the workspace directories:
|
||||
foreach (var workspaceDirPath in Directory.EnumerateDirectories(workspaceDirectories))
|
||||
{
|
||||
// Read or create the `name` file (self-heal):
|
||||
var workspaceNamePath = Path.Join(workspaceDirPath, "name");
|
||||
string workspaceName;
|
||||
try
|
||||
{
|
||||
if (!File.Exists(workspaceNamePath))
|
||||
{
|
||||
workspaceName = T("Unnamed workspace");
|
||||
await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8);
|
||||
}
|
||||
else
|
||||
{
|
||||
workspaceName = await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8);
|
||||
if (string.IsNullOrWhiteSpace(workspaceName))
|
||||
{
|
||||
workspaceName = T("Unnamed workspace");
|
||||
await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
workspaceName = T("Unnamed workspace");
|
||||
}
|
||||
|
||||
workspaces.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = true,
|
||||
Value = new TreeItemData
|
||||
{
|
||||
Type = TreeItemType.WORKSPACE,
|
||||
Depth = 1,
|
||||
Branch = WorkspaceBranch.WORKSPACES,
|
||||
Text = workspaceName,
|
||||
Icon = Icons.Material.Filled.Description,
|
||||
Expandable = true,
|
||||
Path = workspaceDirPath,
|
||||
Children = await this.LoadWorkspaceChats(workspaceDirPath),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
workspaces.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = false,
|
||||
Value = new TreeButton(WorkspaceBranch.WORKSPACES, 1, T("Add workspace"),Icons.Material.Filled.LibraryAdd, this.AddWorkspace),
|
||||
});
|
||||
return workspaces;
|
||||
await this.SafeStateHasChanged();
|
||||
await this.LoadTreeItemsAsync(startPrefetch: true, forceReload: true);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyCollection<TreeItemData<ITreeItem>>> LoadWorkspaceChats(string workspacePath)
|
||||
public async Task StoreChatAsync(ChatThread chat, bool reloadTreeItems = false)
|
||||
{
|
||||
var workspaceChats = new List<TreeItemData>();
|
||||
await WorkspaceBehaviour.StoreChatAsync(chat);
|
||||
|
||||
// Enumerate the workspace directory:
|
||||
foreach (var chatPath in Directory.EnumerateDirectories(workspacePath))
|
||||
{
|
||||
// Read or create the `name` file (self-heal):
|
||||
var chatNamePath = Path.Join(chatPath, "name");
|
||||
string chatName;
|
||||
try
|
||||
{
|
||||
if (!File.Exists(chatNamePath))
|
||||
{
|
||||
chatName = T("Unnamed chat");
|
||||
await File.WriteAllTextAsync(chatNamePath, chatName, Encoding.UTF8);
|
||||
}
|
||||
else
|
||||
{
|
||||
chatName = await File.ReadAllTextAsync(chatNamePath, Encoding.UTF8);
|
||||
if (string.IsNullOrWhiteSpace(chatName))
|
||||
{
|
||||
chatName = T("Unnamed chat");
|
||||
await File.WriteAllTextAsync(chatNamePath, chatName, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
chatName = T("Unnamed chat");
|
||||
}
|
||||
|
||||
// Read the last change time of the chat:
|
||||
var chatThreadPath = Path.Join(chatPath, "thread.json");
|
||||
var lastEditTime = File.GetLastWriteTimeUtc(chatThreadPath);
|
||||
|
||||
workspaceChats.Add(new TreeItemData
|
||||
{
|
||||
Type = TreeItemType.CHAT,
|
||||
Depth = 2,
|
||||
Branch = WorkspaceBranch.WORKSPACES,
|
||||
Text = chatName,
|
||||
Icon = Icons.Material.Filled.Chat,
|
||||
Expandable = false,
|
||||
Path = chatPath,
|
||||
LastEditTime = lastEditTime,
|
||||
});
|
||||
}
|
||||
if (reloadTreeItems)
|
||||
this.loadingWorkspaceChatLists.Clear();
|
||||
|
||||
var result = new List<TreeItemData<ITreeItem>>(workspaceChats.OrderByDescending(n => n.LastEditTime).Select(n => new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = false,
|
||||
Value = n,
|
||||
}));
|
||||
|
||||
result.Add(new()
|
||||
{
|
||||
Expandable = false,
|
||||
Value = new TreeButton(WorkspaceBranch.WORKSPACES, 2, T("Add chat"),Icons.Material.Filled.AddComment, () => this.AddChat(workspacePath)),
|
||||
});
|
||||
|
||||
return result;
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}
|
||||
|
||||
public async Task StoreChat(ChatThread chat, bool reloadTreeItems = true)
|
||||
private async Task<ChatThread?> LoadChatAsync(string? chatPath, bool switchToChat)
|
||||
{
|
||||
await WorkspaceBehaviour.StoreChat(chat);
|
||||
|
||||
// Reload the tree items:
|
||||
if(reloadTreeItems)
|
||||
await this.LoadTreeItems();
|
||||
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task<ChatThread?> LoadChat(string? chatPath, bool switchToChat)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(chatPath))
|
||||
if (string.IsNullOrWhiteSpace(chatPath))
|
||||
return null;
|
||||
|
||||
if(!Directory.Exists(chatPath))
|
||||
if (!Directory.Exists(chatPath))
|
||||
return null;
|
||||
|
||||
// Check if the chat has unsaved changes:
|
||||
if (switchToChat && await MessageBus.INSTANCE.SendMessageUseFirstResult<bool, bool>(this, Event.HAS_CHAT_UNSAVED_CHANGES))
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{ x => x.Message, T("Are you sure you want to load another chat? All unsaved changes will be lost.") },
|
||||
};
|
||||
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Load Chat"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
@@ -344,15 +365,15 @@ public partial class Workspaces : MSGComponentBase
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task DeleteChat(string? chatPath, bool askForConfirmation = true, bool unloadChat = true)
|
||||
public async Task DeleteChatAsync(string? chatPath, bool askForConfirmation = true, bool unloadChat = true)
|
||||
{
|
||||
var chat = await this.LoadChat(chatPath, false);
|
||||
var chat = await this.LoadChatAsync(chatPath, false);
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
if (askForConfirmation)
|
||||
{
|
||||
var workspaceName = await WorkspaceBehaviour.LoadWorkspaceName(chat.WorkspaceId);
|
||||
var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(chat.WorkspaceId);
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{
|
||||
@@ -370,16 +391,10 @@ public partial class Workspaces : MSGComponentBase
|
||||
return;
|
||||
}
|
||||
|
||||
string chatDirectory;
|
||||
if (chat.WorkspaceId == Guid.Empty)
|
||||
chatDirectory = Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString());
|
||||
else
|
||||
chatDirectory = Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString());
|
||||
|
||||
Directory.Delete(chatDirectory, true);
|
||||
await this.LoadTreeItems();
|
||||
await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, chat.WorkspaceId, chat.ChatId, askForConfirmation: false);
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
|
||||
if(unloadChat && this.CurrentChatThread?.ChatId == chat.ChatId)
|
||||
if (unloadChat && this.CurrentChatThread?.ChatId == chat.ChatId)
|
||||
{
|
||||
this.CurrentChatThread = null;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
@@ -387,9 +402,9 @@ public partial class Workspaces : MSGComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RenameChat(string? chatPath)
|
||||
private async Task RenameChatAsync(string? chatPath)
|
||||
{
|
||||
var chat = await this.LoadChat(chatPath, false);
|
||||
var chat = await this.LoadChatAsync(chatPath, false);
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
@@ -410,24 +425,24 @@ public partial class Workspaces : MSGComponentBase
|
||||
return;
|
||||
|
||||
chat.Name = (dialogResult.Data as string)!;
|
||||
if(this.CurrentChatThread?.ChatId == chat.ChatId)
|
||||
if (this.CurrentChatThread?.ChatId == chat.ChatId)
|
||||
{
|
||||
this.CurrentChatThread.Name = chat.Name;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
|
||||
await this.StoreChat(chat);
|
||||
await this.LoadTreeItems();
|
||||
await WorkspaceBehaviour.StoreChatAsync(chat);
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}
|
||||
|
||||
private async Task RenameWorkspace(string? workspacePath)
|
||||
|
||||
private async Task RenameWorkspaceAsync(string? workspacePath)
|
||||
{
|
||||
if(workspacePath is null)
|
||||
if (workspacePath is null)
|
||||
return;
|
||||
|
||||
var workspaceId = Guid.Parse(Path.GetFileName(workspacePath));
|
||||
var workspaceName = await WorkspaceBehaviour.LoadWorkspaceName(workspaceId);
|
||||
var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(workspaceId);
|
||||
var dialogParameters = new DialogParameters<SingleInputDialog>
|
||||
{
|
||||
{ x => x.Message, string.Format(T("Please enter a new or edit the name for your workspace '{0}':"), workspaceName) },
|
||||
@@ -447,10 +462,11 @@ public partial class Workspaces : MSGComponentBase
|
||||
var alteredWorkspaceName = (dialogResult.Data as string)!;
|
||||
var workspaceNamePath = Path.Join(workspacePath, "name");
|
||||
await File.WriteAllTextAsync(workspaceNamePath, alteredWorkspaceName, Encoding.UTF8);
|
||||
await this.LoadTreeItems();
|
||||
await WorkspaceBehaviour.UpdateWorkspaceNameInCacheAsync(workspaceId, alteredWorkspaceName);
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}
|
||||
|
||||
private async Task AddWorkspace()
|
||||
private async Task AddWorkspaceAsync()
|
||||
{
|
||||
var dialogParameters = new DialogParameters<SingleInputDialog>
|
||||
{
|
||||
@@ -472,23 +488,23 @@ public partial class Workspaces : MSGComponentBase
|
||||
var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString());
|
||||
Directory.CreateDirectory(workspacePath);
|
||||
|
||||
var workspaceName = (dialogResult.Data as string)!;
|
||||
var workspaceNamePath = Path.Join(workspacePath, "name");
|
||||
await File.WriteAllTextAsync(workspaceNamePath, (dialogResult.Data as string)!, Encoding.UTF8);
|
||||
await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8);
|
||||
await WorkspaceBehaviour.AddWorkspaceToCacheAsync(workspaceId, workspacePath, workspaceName);
|
||||
|
||||
await this.LoadTreeItems();
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}
|
||||
|
||||
private async Task DeleteWorkspace(string? workspacePath)
|
||||
private async Task DeleteWorkspaceAsync(string? workspacePath)
|
||||
{
|
||||
if(workspacePath is null)
|
||||
if (workspacePath is null)
|
||||
return;
|
||||
|
||||
var workspaceId = Guid.Parse(Path.GetFileName(workspacePath));
|
||||
var workspaceName = await WorkspaceBehaviour.LoadWorkspaceName(workspaceId);
|
||||
var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(workspaceId);
|
||||
|
||||
// Determine how many chats are in the workspace:
|
||||
var chatCount = Directory.EnumerateDirectories(workspacePath).Count();
|
||||
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{ x => x.Message, string.Format(T("Are you sure you want to delete the workspace '{0}'? This will also delete {1} chat(s) in this workspace."), workspaceName, chatCount) },
|
||||
@@ -500,12 +516,13 @@ public partial class Workspaces : MSGComponentBase
|
||||
return;
|
||||
|
||||
Directory.Delete(workspacePath, true);
|
||||
await this.LoadTreeItems();
|
||||
await WorkspaceBehaviour.RemoveWorkspaceFromCacheAsync(workspaceId);
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}
|
||||
|
||||
private async Task MoveChat(string? chatPath)
|
||||
private async Task MoveChatAsync(string? chatPath)
|
||||
{
|
||||
var chat = await this.LoadChat(chatPath, false);
|
||||
var chat = await this.LoadChatAsync(chatPath, false);
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
@@ -525,22 +542,9 @@ public partial class Workspaces : MSGComponentBase
|
||||
if (workspaceId == Guid.Empty)
|
||||
return;
|
||||
|
||||
// Delete the chat from the current workspace or the temporary storage:
|
||||
if (chat.WorkspaceId == Guid.Empty)
|
||||
{
|
||||
// Case: The chat is stored in the temporary storage:
|
||||
await this.DeleteChat(Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString()), askForConfirmation: false, unloadChat: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Case: The chat is stored in a workspace.
|
||||
await this.DeleteChat(Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString()), askForConfirmation: false, unloadChat: false);
|
||||
}
|
||||
await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, chat.WorkspaceId, chat.ChatId, askForConfirmation: false);
|
||||
|
||||
// Update the chat's workspace:
|
||||
chat.WorkspaceId = workspaceId;
|
||||
|
||||
// Handle the case where the chat is the active chat:
|
||||
if (this.CurrentChatThread?.ChatId == chat.ChatId)
|
||||
{
|
||||
this.CurrentChatThread = chat;
|
||||
@@ -548,12 +552,12 @@ public partial class Workspaces : MSGComponentBase
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
|
||||
await this.StoreChat(chat);
|
||||
await WorkspaceBehaviour.StoreChatAsync(chat);
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}
|
||||
|
||||
private async Task AddChat(string workspacePath)
|
||||
private async Task AddChatAsync(string workspacePath)
|
||||
{
|
||||
// Check if the chat has unsaved changes:
|
||||
if (await MessageBus.INSTANCE.SendMessageUseFirstResult<bool, bool>(this, Event.HAS_CHAT_UNSAVED_CHANGES))
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
@@ -579,9 +583,9 @@ public partial class Workspaces : MSGComponentBase
|
||||
|
||||
var chatPath = Path.Join(workspacePath, chat.ChatId.ToString());
|
||||
|
||||
await this.StoreChat(chat);
|
||||
await this.LoadChat(chatPath, switchToChat: true);
|
||||
await this.LoadTreeItems();
|
||||
await WorkspaceBehaviour.StoreChatAsync(chat);
|
||||
await this.LoadChatAsync(chatPath, switchToChat: true);
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
@@ -591,11 +595,20 @@ public partial class Workspaces : MSGComponentBase
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.PLUGINS_RELOADED:
|
||||
await this.LoadTreeItems();
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
await this.ForceRefreshFromDiskAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
this.isDisposed = true;
|
||||
this.prefetchCancellationTokenSource?.Cancel();
|
||||
this.prefetchCancellationTokenSource?.Dispose();
|
||||
this.prefetchCancellationTokenSource = null;
|
||||
|
||||
base.DisposeResources();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in new issue
Block a user