diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 81324295..6bc9e4df 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -3253,6 +3253,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3045856778"] = "Move Chat to -- Please enter a new or edit the name for your workspace '{0}': UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T323280982"] = "Please enter a new or edit the name for your workspace '{0}':" +-- There is already a workspace with this name. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3249036008"] = "There is already a workspace with this name. Please choose a different name." + -- Please enter a workspace name. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3288132732"] = "Please enter a workspace name." @@ -5794,6 +5797,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEDIALOG::T25417398"] = "Update from v{0 -- Install later UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEDIALOG::T2936430090"] = "Install later" +-- Add workspace +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T1586005241"] = "Add workspace" + +-- Workspace name +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T295876489"] = "Workspace name" + +-- There is already a workspace with this name. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3249036008"] = "There is already a workspace with this name. Please choose a different name." + +-- Please enter a workspace name. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3288132732"] = "Please enter a workspace name." + +-- Create a new workspace +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T591103325"] = "Create a new workspace" + -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T900713019"] = "Cancel" diff --git a/app/MindWork AI Studio/Components/Workspaces.razor.cs b/app/MindWork AI Studio/Components/Workspaces.razor.cs index e370b699..c7bd7e35 100644 --- a/app/MindWork AI Studio/Components/Workspaces.razor.cs +++ b/app/MindWork AI Studio/Components/Workspaces.razor.cs @@ -3,7 +3,6 @@ using System.Text.Json; using AIStudio.Chat; using AIStudio.Dialogs; -using AIStudio.Settings; using AIStudio.Tools.AIJobs; using Microsoft.AspNetCore.Components; @@ -101,6 +100,27 @@ public partial class Workspaces : MSGComponentBase private string GetAddChatToWorkspaceTooltip(string workspaceName) => string.Format(T("Start a new chat in workspace '{0}'"), workspaceName); + private async Task> CreateWorkspaceNameValidationAsync(Guid excludedWorkspaceId = default, string? originalWorkspaceName = null) + { + var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); + return workspaceName => + { + var normalizedWorkspaceName = WorkspaceBehaviour.NormalizeWorkspaceName(workspaceName ?? string.Empty); + if (string.IsNullOrWhiteSpace(normalizedWorkspaceName)) + return null; + + if (!string.IsNullOrWhiteSpace(originalWorkspaceName) && + string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(originalWorkspaceName), normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase)) + return null; + + var nameExists = snapshot.Workspaces.Any(workspace => + workspace.WorkspaceId != excludedWorkspaceId && + string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(workspace.Name), normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase)); + + return nameExists ? T("There is already a workspace with this name. Please choose a different name.") : null; + }; + } + private void BuildTreeItems(WorkspaceTreeCacheSnapshot snapshot) { this.treeItems.Clear(); @@ -720,6 +740,7 @@ public partial class Workspaces : MSGComponentBase { x => x.ConfirmColor, Color.Info }, { x => x.AllowEmptyInput, false }, { x => x.EmptyInputErrorMessage, T("Please enter a workspace name.") }, + { x => x.AdditionalValidation, await this.CreateWorkspaceNameValidationAsync(workspaceId, workspaceName) }, }; var dialogReference = await this.DialogService.ShowAsync(T("Rename Workspace"), dialogParameters, DialogOptions.FULLSCREEN); @@ -728,9 +749,9 @@ public partial class Workspaces : MSGComponentBase return; var alteredWorkspaceName = (dialogResult.Data as string)!; - var workspaceNamePath = Path.Join(workspacePath, "name"); - await File.WriteAllTextAsync(workspaceNamePath, alteredWorkspaceName, Encoding.UTF8); - await WorkspaceBehaviour.UpdateWorkspaceNameInCacheAsync(workspaceId, alteredWorkspaceName); + if (!await WorkspaceBehaviour.RenameWorkspaceAsync(workspaceId, alteredWorkspaceName)) + return; + await this.LoadTreeItemsAsync(startPrefetch: false); } @@ -745,6 +766,7 @@ public partial class Workspaces : MSGComponentBase { x => x.ConfirmColor, Color.Info }, { x => x.AllowEmptyInput, false }, { x => x.EmptyInputErrorMessage, T("Please enter a workspace name.") }, + { x => x.AdditionalValidation, await this.CreateWorkspaceNameValidationAsync() }, }; var dialogReference = await this.DialogService.ShowAsync(T("Add Workspace"), dialogParameters, DialogOptions.FULLSCREEN); @@ -752,14 +774,9 @@ public partial class Workspaces : MSGComponentBase if (dialogResult is null || dialogResult.Canceled) return; - var workspaceId = Guid.NewGuid(); - 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, workspaceName, Encoding.UTF8); - await WorkspaceBehaviour.AddWorkspaceToCacheAsync(workspaceId, workspacePath, workspaceName); + if (await WorkspaceBehaviour.CreateWorkspaceAsync(workspaceName) is null) + return; await this.LoadTreeItemsAsync(startPrefetch: false); } diff --git a/app/MindWork AI Studio/Dialogs/SingleInputDialog.razor.cs b/app/MindWork AI Studio/Dialogs/SingleInputDialog.razor.cs index c858b38c..301bf937 100644 --- a/app/MindWork AI Studio/Dialogs/SingleInputDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/SingleInputDialog.razor.cs @@ -31,6 +31,9 @@ public partial class SingleInputDialog : MSGComponentBase [Parameter] public string EmptyInputErrorMessage { get; set; } = string.Empty; + [Parameter] + public Func? AdditionalValidation { get; set; } + private static readonly Dictionary USER_INPUT_ATTRIBUTES = new(); private MudForm form = null!; @@ -52,8 +55,8 @@ public partial class SingleInputDialog : MSGComponentBase { if (!this.AllowEmptyInput && string.IsNullOrWhiteSpace(value)) return string.IsNullOrWhiteSpace(this.EmptyInputErrorMessage) ? T("Please enter a value.") : this.EmptyInputErrorMessage; - - return null; + + return this.AdditionalValidation?.Invoke(value); } private void Cancel() => this.MudDialog.Cancel(); diff --git a/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor b/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor index 05493cff..9d4be74d 100644 --- a/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor +++ b/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor @@ -5,17 +5,38 @@ @this.Message - @foreach (var (workspaceName, workspaceId) in this.workspaces) + @foreach (var workspace in this.workspaces) { - + } + + + @T("Create a new workspace") + + + + + + + + + @T("Cancel") - + @this.ConfirmText diff --git a/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs b/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs index ca4b625e..b6edb43f 100644 --- a/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs @@ -1,11 +1,14 @@ using AIStudio.Components; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; namespace AIStudio.Dialogs; public partial class WorkspaceSelectionDialog : MSGComponentBase { + private readonly record struct WorkspaceSelectionItem(Guid WorkspaceId, string Name); + [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; @@ -18,8 +21,13 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase [Parameter] public string ConfirmText { get; set; } = "OK"; - private readonly Dictionary workspaces = new(); + private readonly List workspaces = []; + private MudForm createWorkspaceForm = null!; private Guid selectedWorkspace; + private string newWorkspaceName = string.Empty; + private bool isCreatingWorkspace; + private string? createWorkspaceError; + private string? createWorkspaceErrorName; #region Overrides of ComponentBase @@ -29,7 +37,7 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); foreach (var workspace in snapshot.Workspaces) - this.workspaces[workspace.Name] = workspace.WorkspaceId; + this.workspaces.Add(new(workspace.WorkspaceId, workspace.Name)); this.StateHasChanged(); await base.OnInitializedAsync(); @@ -37,6 +45,71 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase #endregion + private string? ValidateNewWorkspaceName(string? workspaceName) + { + var normalizedWorkspaceName = WorkspaceBehaviour.NormalizeWorkspaceName(workspaceName ?? string.Empty); + if (string.IsNullOrWhiteSpace(normalizedWorkspaceName)) + return T("Please enter a workspace name."); + + if (this.IsWorkspaceNameExisting(normalizedWorkspaceName)) + return T("There is already a workspace with this name. Please choose a different name."); + + if (this.createWorkspaceError is not null && string.Equals(this.createWorkspaceErrorName, normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase)) + return this.createWorkspaceError; + + return null; + } + + private bool IsWorkspaceNameExisting(string normalizedWorkspaceName) + { + return this.workspaces.Any(workspace => + string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(workspace.Name), normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase)); + } + + private async Task HandleNewWorkspaceNameKeyDown(KeyboardEventArgs keyEvent) + { + var key = keyEvent.Key.ToLowerInvariant(); + var code = keyEvent.Code.ToLowerInvariant(); + if (key is not "enter" && code is not "enter" and not "numpadenter") + return; + + if (keyEvent is { AltKey: true } or { CtrlKey: true } or { MetaKey: true }) + return; + + await this.CreateWorkspaceAsync(); + } + + private async Task CreateWorkspaceAsync() + { + this.createWorkspaceError = null; + this.createWorkspaceErrorName = null; + await this.createWorkspaceForm.Validate(); + if (!this.createWorkspaceForm.IsValid) + return; + + this.isCreatingWorkspace = true; + try + { + var workspace = await WorkspaceBehaviour.CreateWorkspaceAsync(this.newWorkspaceName); + if (workspace is null) + { + this.createWorkspaceError = T("There is already a workspace with this name. Please choose a different name."); + this.createWorkspaceErrorName = WorkspaceBehaviour.NormalizeWorkspaceName(this.newWorkspaceName); + await this.createWorkspaceForm.Validate(); + return; + } + + this.workspaces.Add(new(workspace.Value.WorkspaceId, workspace.Value.Name)); + this.selectedWorkspace = workspace.Value.WorkspaceId; + this.newWorkspaceName = string.Empty; + this.createWorkspaceForm.ResetValidation(); + } + finally + { + this.isCreatingWorkspace = false; + } + } + private void Cancel() => this.MudDialog.Cancel(); private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.selectedWorkspace)); diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 198926dc..1147167d 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -3255,6 +3255,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3045856778"] = "Chat in den -- Please enter a new or edit the name for your workspace '{0}': UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T323280982"] = "Bitte geben Sie einen neuen Namen für ihren Arbeitsbereich „{0}“ ein oder bearbeiten Sie ihn:" +-- There is already a workspace with this name. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3249036008"] = "Es gibt bereits einen Arbeitsbereich mit diesem Namen. Bitte wählen Sie einen anderen Namen." + -- Please enter a workspace name. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3288132732"] = "Bitte geben Sie einen Namen für diesen Arbeitsbereich ein." @@ -5796,6 +5799,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEDIALOG::T25417398"] = "Aktualisieren v -- Install later UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEDIALOG::T2936430090"] = "Später installieren" +-- Add workspace +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T1586005241"] = "Arbeitsbereich hinzufügen" + +-- Workspace name +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T295876489"] = "Name des Arbeitsbereichs" + +-- There is already a workspace with this name. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3249036008"] = "Es gibt bereits einen Arbeitsbereich mit diesem Namen. Bitte wählen Sie einen anderen Namen." + +-- Please enter a workspace name. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3288132732"] = "Bitte geben Sie einen Namen für diesen Arbeitsbereich ein." + +-- Create a new workspace +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T591103325"] = "Neuen Arbeitsbereich erstellen" + -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T900713019"] = "Abbrechen" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 557713e1..498df5c3 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -3255,6 +3255,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3045856778"] = "Move Chat to -- Please enter a new or edit the name for your workspace '{0}': UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T323280982"] = "Please enter a new or edit the name for your workspace '{0}':" +-- There is already a workspace with this name. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3249036008"] = "There is already a workspace with this name. Please choose a different name." + -- Please enter a workspace name. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3288132732"] = "Please enter a workspace name." @@ -5796,6 +5799,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEDIALOG::T25417398"] = "Update from v{0 -- Install later UI_TEXT_CONTENT["AISTUDIO::DIALOGS::UPDATEDIALOG::T2936430090"] = "Install later" +-- Add workspace +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T1586005241"] = "Add workspace" + +-- Workspace name +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T295876489"] = "Workspace name" + +-- There is already a workspace with this name. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3249036008"] = "There is already a workspace with this name. Please choose a different name." + +-- Please enter a workspace name. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3288132732"] = "Please enter a workspace name." + +-- Create a new workspace +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T591103325"] = "Create a new workspace" + -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T900713019"] = "Cancel" diff --git a/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs b/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs index 9397db85..7305d715 100644 --- a/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs +++ b/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs @@ -76,9 +76,9 @@ public static class WorkspaceBehaviour private static readonly TimeSpan PREFETCH_DELAY_DURATION = TimeSpan.FromMilliseconds(45); - private static string WorkspaceRootDirectory => Path.Join(SettingsManager.DataDirectory, "workspaces"); + private static readonly string WORKSPACE_ROOT_DIRECTORY = Path.Join(SettingsManager.DataDirectory, "workspaces"); - private static string TemporaryChatsRootDirectory => Path.Join(SettingsManager.DataDirectory, "tempChats"); + private static readonly string TEMPORARY_CHATS_ROOT_DIRECTORY = Path.Join(SettingsManager.DataDirectory, "tempChats"); private static SemaphoreSlim GetChatSemaphore(Guid workspaceId, Guid chatId) { @@ -156,9 +156,9 @@ public static class WorkspaceBehaviour private static async Task> ReadTemporaryChatsCoreAsync() { var chats = new List(); - Directory.CreateDirectory(TemporaryChatsRootDirectory); + Directory.CreateDirectory(TEMPORARY_CHATS_ROOT_DIRECTORY); - foreach (var tempChatPath in Directory.EnumerateDirectories(TemporaryChatsRootDirectory)) + foreach (var tempChatPath in Directory.EnumerateDirectories(TEMPORARY_CHATS_ROOT_DIRECTORY)) { if (!Guid.TryParse(Path.GetFileName(tempChatPath), out var chatId)) continue; @@ -188,8 +188,8 @@ public static class WorkspaceBehaviour WORKSPACE_TREE_CACHE.Workspaces.Clear(); WORKSPACE_TREE_CACHE.WorkspaceOrder.Clear(); - Directory.CreateDirectory(WorkspaceRootDirectory); - foreach (var workspacePath in Directory.EnumerateDirectories(WorkspaceRootDirectory)) + Directory.CreateDirectory(WORKSPACE_ROOT_DIRECTORY); + foreach (var workspacePath in Directory.EnumerateDirectories(WORKSPACE_ROOT_DIRECTORY)) { if (!Guid.TryParse(Path.GetFileName(workspacePath), out var workspaceId)) continue; @@ -259,6 +259,13 @@ public static class WorkspaceBehaviour return false; } + private static bool WorkspaceNameExistsCore(string workspaceName, Guid excludedWorkspaceId = default) + { + return WORKSPACE_TREE_CACHE.Workspaces.Values.Any(workspace => + workspace.WorkspaceId != excludedWorkspaceId && + string.Equals(workspace.WorkspaceName.Trim(), workspaceName, StringComparison.OrdinalIgnoreCase)); + } + private static async Task ThreadContainsTermsAsync(WorkspaceTreeChat chat, IReadOnlyList terms, CancellationToken token) { var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(chat.WorkspaceId, chat.ChatId, nameof(ThreadContainsTermsAsync)); @@ -587,6 +594,100 @@ public static class WorkspaceBehaviour WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); } } + + public static string NormalizeWorkspaceName(string workspaceName) => workspaceName.Trim(); + + public static async Task IsWorkspaceNameExistingAsync(string workspaceName, Guid excludedWorkspaceId = default) + { + var normalizedWorkspaceName = NormalizeWorkspaceName(workspaceName); + if (string.IsNullOrWhiteSpace(normalizedWorkspaceName)) + return false; + + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + await EnsureTreeShellLoadedCoreAsync(); + return WorkspaceNameExistsCore(normalizedWorkspaceName, excludedWorkspaceId); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + public static async Task CreateWorkspaceAsync(string workspaceName) + { + var normalizedWorkspaceName = NormalizeWorkspaceName(workspaceName); + if (string.IsNullOrWhiteSpace(normalizedWorkspaceName)) + return null; + + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + await EnsureTreeShellLoadedCoreAsync(); + if (WorkspaceNameExistsCore(normalizedWorkspaceName)) + return null; + + var workspaceId = Guid.NewGuid(); + var workspacePath = Path.Join(WORKSPACE_ROOT_DIRECTORY, workspaceId.ToString()); + Directory.CreateDirectory(workspacePath); + + var workspaceNamePath = Path.Join(workspacePath, "name"); + await File.WriteAllTextAsync(workspaceNamePath, normalizedWorkspaceName, Encoding.UTF8); + + var workspace = new WorkspaceCacheEntry + { + WorkspaceId = workspaceId, + WorkspacePath = workspacePath, + WorkspaceName = normalizedWorkspaceName, + Chats = [], + ChatsLoaded = false, + }; + WORKSPACE_TREE_CACHE.Workspaces[workspaceId] = workspace; + WORKSPACE_TREE_CACHE.WorkspaceOrder.Add(workspaceId); + + return ToPublicWorkspace(workspace); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + public static async Task RenameWorkspaceAsync(Guid workspaceId, string workspaceName) + { + var normalizedWorkspaceName = NormalizeWorkspaceName(workspaceName); + if (string.IsNullOrWhiteSpace(normalizedWorkspaceName)) + return false; + + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + await EnsureTreeShellLoadedCoreAsync(); + if (!WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace)) + return false; + + var workspaceNamePath = Path.Join(workspace.WorkspacePath, "name"); + if (string.Equals(workspace.WorkspaceName.Trim(), normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase)) + { + await File.WriteAllTextAsync(workspaceNamePath, normalizedWorkspaceName, Encoding.UTF8); + workspace.WorkspaceName = normalizedWorkspaceName; + return true; + } + + if (WorkspaceNameExistsCore(normalizedWorkspaceName, workspaceId)) + return false; + + await File.WriteAllTextAsync(workspaceNamePath, normalizedWorkspaceName, Encoding.UTF8); + workspace.WorkspaceName = normalizedWorkspaceName; + + return true; + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } public static bool IsChatExisting(LoadChat loadChat) { @@ -668,7 +769,7 @@ public static class WorkspaceBehaviour // Not in cache — read from disk and update cache in the same semaphore scope // to avoid a second semaphore acquisition via UpdateWorkspaceNameInCacheAsync: - var workspacePath = Path.Join(WorkspaceRootDirectory, workspaceId.ToString()); + var workspacePath = Path.Join(WORKSPACE_ROOT_DIRECTORY, workspaceId.ToString()); var workspaceNamePath = Path.Join(workspacePath, "name"); string workspaceName; @@ -756,7 +857,7 @@ public static class WorkspaceBehaviour private static async Task EnsureWorkspace(Guid workspaceId, string workspaceName) { - var workspacePath = Path.Join(WorkspaceRootDirectory, workspaceId.ToString()); + var workspacePath = Path.Join(WORKSPACE_ROOT_DIRECTORY, workspaceId.ToString()); var workspaceNamePath = Path.Join(workspacePath, "name"); if (!Path.Exists(workspacePath)) @@ -786,4 +887,4 @@ public static class WorkspaceBehaviour public static async Task EnsureBiasWorkspace() => await EnsureWorkspace(KnownWorkspaces.BIAS_WORKSPACE_ID, "Bias of the Day"); public static async Task EnsureERIServerWorkspace() => await EnsureWorkspace(KnownWorkspaces.ERI_SERVER_WORKSPACE_ID, "ERI Servers"); -} +} \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md index 8cf1b5c9..d4642138 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md @@ -6,5 +6,7 @@ - Added startup path and Linux package type details to the information page to make support easier. - Added the option to search for chats in all workspaces. - Improved workspaces by adding a shortcut to start a new chat directly from each workspace row. +- Improved workspaces by allowing new workspaces to be created while moving a chat. - Improved the enterprise configuration details on the information page by showing where each configuration comes from and which configuration slot was used. +- Fixed workspace creation and renaming to prevent new workspaces from using an existing name. - Upgraded dependencies. \ No newline at end of file