diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 99257cc7..81324295 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -3196,15 +3196,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1469573738"] = "Delete" -- Rename Workspace UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1474303418"] = "Rename Workspace" +-- Clear search +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1511254342"] = "Clear search" + -- Rename Chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T156144855"] = "Rename Chat" -- Add workspace UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1586005241"] = "Add workspace" +-- Search chats +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1615077202"] = "Search chats" + +-- Start a new chat in workspace '{0}' +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1840064668"] = "Start a new chat in workspace '{0}'" + -- Add chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1874060138"] = "Add chat" +-- No chats found +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1886517101"] = "No chats found" + -- Create Chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1939006681"] = "Create Chat" @@ -3250,6 +3262,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3355849203"] = "Rename" -- Please enter a new or edit the name for your chat '{0}': UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3419791373"] = "Please enter a new or edit the name for your chat '{0}':" +-- Search chat contents +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3436662033"] = "Search chat contents" + -- Load Chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3555709365"] = "Load Chat" @@ -5956,6 +5971,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Learn about one co -- Localization UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Localization" +-- Hide search +UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T1281128983"] = "Hide search" + -- Reload your workspaces UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T194629703"] = "Reload your workspaces" @@ -5968,6 +5986,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T2813205227"] = "Open Chat Options" -- Disappearing Chat UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3046519404"] = "Disappearing Chat" +-- Search your workspaces +UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3059773282"] = "Search your workspaces" + -- Configure your workspaces UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3586092784"] = "Configure your workspaces" diff --git a/app/MindWork AI Studio/Components/Workspaces.razor b/app/MindWork AI Studio/Components/Workspaces.razor index 940d448d..c0185e3e 100644 --- a/app/MindWork AI Studio/Components/Workspaces.razor +++ b/app/MindWork AI Studio/Components/Workspaces.razor @@ -11,6 +11,36 @@ } else { + @if (this.SearchVisible) + { + + + + + + + + + + + @T("Search chat contents") + + @if (this.isSearchRunning) + { + + } + + + } + @switch (item.Value) @@ -71,19 +101,22 @@ else @treeItem.Text -
- - - + @if (!this.HasSearchQuery) + { +
+ + + - - - + + + - - - -
+ + + +
+ } diff --git a/app/MindWork AI Studio/Components/Workspaces.razor.cs b/app/MindWork AI Studio/Components/Workspaces.razor.cs index ef9c15c8..e370b699 100644 --- a/app/MindWork AI Studio/Components/Workspaces.razor.cs +++ b/app/MindWork AI Studio/Components/Workspaces.razor.cs @@ -32,14 +32,25 @@ public partial class Workspaces : MSGComponentBase [Parameter] public bool ExpandRootNodes { get; set; } = true; + [Parameter] + public bool SearchVisible { get; set; } + + [Parameter] + public EventCallback SearchVisibleChanged { get; set; } + private const Placement WORKSPACE_ITEM_TOOLTIP_PLACEMENT = Placement.Bottom; private readonly SemaphoreSlim treeLoadingSemaphore = new(1, 1); private readonly List> treeItems = []; private readonly HashSet loadingWorkspaceChatLists = []; private CancellationTokenSource? prefetchCancellationTokenSource; + private CancellationTokenSource? searchCancellationTokenSource; private bool isInitialLoading = true; private bool isDisposed; + private bool includeThreadContents; + private bool isSearchRunning; + private string searchText = string.Empty; + private long searchRevision; #region Overrides of ComponentBase @@ -54,6 +65,7 @@ public partial class Workspaces : MSGComponentBase private async Task LoadTreeItemsAsync(bool startPrefetch = true, bool forceReload = false) { + var shouldRunSearch = false; await this.treeLoadingSemaphore.WaitAsync(); try { @@ -64,7 +76,11 @@ public partial class Workspaces : MSGComponentBase await WorkspaceBehaviour.ForceReloadWorkspaceTreeAsync(); var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); - this.BuildTreeItems(snapshot); + if (this.HasSearchQuery) + shouldRunSearch = true; + else + this.BuildTreeItems(snapshot); + this.isInitialLoading = false; } finally @@ -72,12 +88,19 @@ public partial class Workspaces : MSGComponentBase this.treeLoadingSemaphore.Release(); } - await this.SafeStateHasChanged(); + if (shouldRunSearch) + await this.SearchWorkspaceItemsAsync(); + else + await this.SafeStateHasChanged(); if (startPrefetch) await this.StartPrefetchAsync(); } + private bool HasSearchQuery => this.SearchVisible && !string.IsNullOrWhiteSpace(this.searchText); + + private string GetAddChatToWorkspaceTooltip(string workspaceName) => string.Format(T("Start a new chat in workspace '{0}'"), workspaceName); + private void BuildTreeItems(WorkspaceTreeCacheSnapshot snapshot) { this.treeItems.Clear(); @@ -219,6 +242,109 @@ public partial class Workspaces : MSGComponentBase }; } + private void BuildSearchTreeItems(WorkspaceSearchSnapshot snapshot) + { + this.treeItems.Clear(); + + if (snapshot.Workspaces.Count == 0 && snapshot.TemporaryChats.Count == 0) + { + this.treeItems.Add(new TreeItemData + { + Expandable = false, + Value = new TreeItemData + { + Depth = 0, + Branch = WorkspaceBranch.NONE, + Text = T("No chats found"), + Icon = Icons.Material.Filled.Search, + Expandable = false, + Path = "search_empty", + }, + }); + + return; + } + + if (snapshot.Workspaces.Count > 0) + { + var workspaceChildren = new List>(); + foreach (var workspace in snapshot.Workspaces) + workspaceChildren.Add(this.CreateSearchWorkspaceTreeItem(workspace)); + + this.treeItems.Add(new TreeItemData + { + Expanded = true, + Expandable = true, + Value = new TreeItemData + { + Depth = 0, + Branch = WorkspaceBranch.WORKSPACES, + Text = T("Workspaces"), + Icon = Icons.Material.Filled.Folder, + Expandable = true, + Path = "search_workspaces", + Children = workspaceChildren, + }, + }); + } + + if (snapshot.Workspaces.Count > 0 && snapshot.TemporaryChats.Count > 0) + { + this.treeItems.Add(new TreeItemData + { + Expandable = false, + Value = new TreeDivider(), + }); + } + + if (snapshot.TemporaryChats.Count > 0) + { + var temporaryChatsChildren = new List>(); + foreach (var temporaryChat in snapshot.TemporaryChats) + temporaryChatsChildren.Add(this.CreateChatTreeItem(temporaryChat.Chat, WorkspaceBranch.TEMPORARY_CHATS, depth: 1, icon: Icons.Material.Filled.Timer)); + + this.treeItems.Add(new TreeItemData + { + Expanded = true, + Expandable = true, + Value = new TreeItemData + { + Depth = 0, + Branch = WorkspaceBranch.TEMPORARY_CHATS, + Text = T("Disappearing Chats"), + Icon = Icons.Material.Filled.Timer, + Expandable = true, + Path = "search_temp", + Children = temporaryChatsChildren, + }, + }); + } + } + + private TreeItemData CreateSearchWorkspaceTreeItem(WorkspaceSearchWorkspace workspace) + { + var children = new List>(); + foreach (var chat in workspace.Chats) + children.Add(this.CreateChatTreeItem(chat.Chat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat)); + + return new TreeItemData + { + Expanded = true, + 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 string GetTreeItemIcon(TreeItemData treeItem) { if (treeItem.Type is not TreeItemType.CHAT) @@ -289,6 +415,106 @@ public partial class Workspaces : MSGComponentBase } } + public async Task ToggleSearchAsync() + { + var searchVisible = !this.SearchVisible; + this.SearchVisible = searchVisible; + await this.SearchVisibleChanged.InvokeAsync(searchVisible); + + if (this.SearchVisible) + { + await this.SafeStateHasChanged(); + return; + } + + await this.CancelSearchAsync(); + this.searchText = string.Empty; + this.isSearchRunning = false; + await this.LoadTreeItemsAsync(startPrefetch: false); + } + + private async Task CancelSearchAsync() + { + this.searchRevision++; + if (this.searchCancellationTokenSource is not null) + { + await this.searchCancellationTokenSource.CancelAsync(); + this.searchCancellationTokenSource.Dispose(); + this.searchCancellationTokenSource = null; + } + } + + private async Task OnSearchTextChanged(string value) + { + this.searchText = value; + if (string.IsNullOrWhiteSpace(this.searchText)) + { + await this.CancelSearchAsync(); + this.isSearchRunning = false; + await this.LoadTreeItemsAsync(startPrefetch: false); + return; + } + + await this.SearchWorkspaceItemsAsync(); + } + + private async Task IncludeThreadContentsChanged(bool value) + { + this.includeThreadContents = value; + if (this.HasSearchQuery) + await this.SearchWorkspaceItemsAsync(); + } + + private async Task ClearSearchAsync() + { + this.searchText = string.Empty; + await this.CancelSearchAsync(); + this.isSearchRunning = false; + await this.LoadTreeItemsAsync(startPrefetch: false); + } + + private async Task SearchWorkspaceItemsAsync() + { + await this.CancelSearchAsync(); + + var text = this.searchText; + if (string.IsNullOrWhiteSpace(text)) + return; + + this.searchCancellationTokenSource = new CancellationTokenSource(); + var token = this.searchCancellationTokenSource.Token; + var revision = ++this.searchRevision; + + this.isSearchRunning = true; + await this.SafeStateHasChanged(); + + try + { + var snapshot = await WorkspaceBehaviour.SearchWorkspaceChatsAsync(text, this.includeThreadContents, token); + if (this.isDisposed || token.IsCancellationRequested || revision != this.searchRevision) + return; + + this.BuildSearchTreeItems(snapshot); + } + catch (OperationCanceledException) + { + // Expected when the user keeps typing or hides the search row. + } + catch (Exception ex) + { + this.Logger.LogWarning(ex, "Failed while searching workspace chats."); + this.BuildSearchTreeItems(new([], [])); + } + finally + { + if (revision == this.searchRevision) + { + this.isSearchRunning = false; + await this.SafeStateHasChanged(); + } + } + } + private async Task OnWorkspaceClicked(TreeItemData treeItem) { if (treeItem.Type is not TreeItemType.WORKSPACE) @@ -656,9 +882,12 @@ public partial class Workspaces : MSGComponentBase this.prefetchCancellationTokenSource?.Cancel(); this.prefetchCancellationTokenSource?.Dispose(); this.prefetchCancellationTokenSource = null; + this.searchCancellationTokenSource?.Cancel(); + this.searchCancellationTokenSource?.Dispose(); + this.searchCancellationTokenSource = null; base.DisposeResources(); } #endregion -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Pages/Chat.razor b/app/MindWork AI Studio/Pages/Chat.razor index f35a00a6..a7b85d53 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor +++ b/app/MindWork AI Studio/Pages/Chat.razor @@ -51,13 +51,16 @@ + + + - + } @@ -77,10 +80,13 @@ + + + - + } @@ -149,11 +155,14 @@ + + + - + } diff --git a/app/MindWork AI Studio/Pages/Chat.razor.cs b/app/MindWork AI Studio/Pages/Chat.razor.cs index 0f271076..6f3d2fbd 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor.cs +++ b/app/MindWork AI Studio/Pages/Chat.razor.cs @@ -23,6 +23,7 @@ public partial class Chat : MSGComponentBase private ChatThread? chatThread; private AIStudio.Settings.Provider providerSettings = AIStudio.Settings.Provider.NONE; private bool workspaceOverlayVisible; + private bool workspaceSearchVisible; private string currentWorkspaceName = string.Empty; private Workspaces? workspaces; private double splitterPosition = 30; @@ -51,6 +52,10 @@ public partial class Chat : MSGComponentBase private string WorkspaceSidebarToggleIcon => this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible ? Icons.Material.Filled.ArrowCircleLeft : Icons.Material.Filled.ArrowCircleRight; + private string WorkspaceSearchIcon => this.workspaceSearchVisible ? Icons.Material.Filled.SearchOff : Icons.Material.Filled.Search; + + private string WorkspaceSearchTooltip => this.workspaceSearchVisible ? T("Hide search") : T("Search your workspaces"); + private bool AreWorkspacesVisible => this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES && ((this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible) || this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.SIDEBAR_ALWAYS_VISIBLE); @@ -107,6 +112,14 @@ public partial class Chat : MSGComponentBase await this.workspaces.ForceRefreshFromDiskAsync(); } + private async Task ToggleWorkspaceSearch() + { + if (this.workspaces is null) + return; + + await this.workspaces.ToggleSearchAsync(); + } + #region Overrides of MSGComponentBase protected override void DisposeResources() 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 a88dbc8d..198926dc 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 @@ -3198,15 +3198,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1469573738"] = "Löschen" -- Rename Workspace UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1474303418"] = "Arbeitsbereich umbenennen" +-- Clear search +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1511254342"] = "Suche zurücksetzen" + -- Rename Chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T156144855"] = "Chat umbenennen" -- Add workspace UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1586005241"] = "Arbeitsbereich hinzufügen" +-- Search chats +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1615077202"] = "Chats durchsuchen" + +-- Start a new chat in workspace '{0}' +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1840064668"] = "Neuen Chat im Arbeitsbereich „{0}“ starten" + -- Add chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1874060138"] = "Chat hinzufügen" +-- No chats found +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1886517101"] = "Keine Chats gefunden" + -- Create Chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1939006681"] = "Chat erstellen" @@ -3252,6 +3264,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3355849203"] = "Umbenennen" -- Please enter a new or edit the name for your chat '{0}': UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3419791373"] = "Bitte geben Sie einen neuen Namen für ihren Chat „{0}“ ein oder bearbeiten Sie ihn:" +-- Search chat contents +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3436662033"] = "Chat-Inhalte durchsuchen" + -- Load Chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3555709365"] = "Chat laden" @@ -5958,6 +5973,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Lerne jeden Tag ei -- Localization UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Lokalisierung" +-- Hide search +UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T1281128983"] = "Suche ausblenden" + -- Reload your workspaces UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T194629703"] = "Arbeitsbereiche neu laden" @@ -5970,6 +5988,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T2813205227"] = "Chat-Optionen öffnen" -- Disappearing Chat UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3046519404"] = "Selbstlöschender Chat" +-- Search your workspaces +UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3059773282"] = "Arbeitsbereiche durchsuchen" + -- Configure your workspaces UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3586092784"] = "Konfigurieren Sie ihre Arbeitsbereiche" @@ -6270,12 +6291,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2989678330"] = "Kopiert den Fing -- Changelog UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Änderungsprotokoll" --- Vector store -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vektordatenbank" - -- External HTTPS custom root certificates are configured but not active. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3021325354"] = "Externe benutzerdefinierte Stammzertifikate sind konfiguriert, aber nicht aktiv." +-- Vector store +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vektordatenbank" + -- Enterprise configuration ID: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3092349641"] = "Unternehmenskonfigurations-ID:" 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 70eec49d..557713e1 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 @@ -3198,15 +3198,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1469573738"] = "Delete" -- Rename Workspace UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1474303418"] = "Rename Workspace" +-- Clear search +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1511254342"] = "Clear search" + -- Rename Chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T156144855"] = "Rename Chat" -- Add workspace UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1586005241"] = "Add workspace" +-- Search chats +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1615077202"] = "Search chats" + +-- Start a new chat in workspace '{0}' +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1840064668"] = "Start a new chat in workspace '{0}'" + -- Add chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1874060138"] = "Add chat" +-- No chats found +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1886517101"] = "No chats found" + -- Create Chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1939006681"] = "Create Chat" @@ -3252,6 +3264,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3355849203"] = "Rename" -- Please enter a new or edit the name for your chat '{0}': UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3419791373"] = "Please enter a new or edit the name for your chat '{0}':" +-- Search chat contents +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3436662033"] = "Search chat contents" + -- Load Chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3555709365"] = "Load Chat" @@ -5958,6 +5973,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Learn about one co -- Localization UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Localization" +-- Hide search +UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T1281128983"] = "Hide search" + -- Reload your workspaces UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T194629703"] = "Reload your workspaces" @@ -5970,6 +5988,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T2813205227"] = "Open Chat Options" -- Disappearing Chat UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3046519404"] = "Disappearing Chat" +-- Search your workspaces +UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3059773282"] = "Search your workspaces" + -- Configure your workspaces UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T3586092784"] = "Configure your workspaces" @@ -6270,11 +6291,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2989678330"] = "Copies the root -- Changelog UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog" --- Vector store -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vector store" -- External HTTPS custom root certificates are configured but not active. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3021325354"] = "External HTTPS custom root certificates are configured but not active." +-- Vector store +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vector store" + -- Enterprise configuration ID: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3092349641"] = "Enterprise configuration ID:" diff --git a/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs b/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs index c03fccc8..9397db85 100644 --- a/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs +++ b/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs @@ -230,6 +230,92 @@ public static class WorkspaceBehaviour chats.RemoveAt(existingIndex); } + private static IReadOnlyList ParseSearchTerms(string searchText) => searchText + .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(term => !string.IsNullOrWhiteSpace(term)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + private static IReadOnlyList GetMissingTerms(string text, IReadOnlyList terms) => terms + .Where(term => text.IndexOf(term, StringComparison.OrdinalIgnoreCase) < 0) + .ToList(); + + private static bool ChatThreadContainsTerms(ChatThread thread, IReadOnlyList terms) + { + var matchedTerms = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var block in thread.Blocks) + { + if (block.HideFromUser || block.Content is not ContentText textContent || string.IsNullOrWhiteSpace(textContent.Text)) + continue; + + foreach (var term in terms) + if (textContent.Text.Contains(term, StringComparison.OrdinalIgnoreCase)) + matchedTerms.Add(term); + + if (matchedTerms.Count == terms.Count) + return true; + } + + return false; + } + + private static async Task ThreadContainsTermsAsync(WorkspaceTreeChat chat, IReadOnlyList terms, CancellationToken token) + { + var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(chat.WorkspaceId, chat.ChatId, nameof(ThreadContainsTermsAsync)); + if (!acquired) + return false; + + try + { + var threadPath = Path.Join(chat.ChatPath, "thread.json"); + if (!File.Exists(threadPath)) + return false; + + var chatData = await File.ReadAllTextAsync(threadPath, Encoding.UTF8, token); + token.ThrowIfCancellationRequested(); + var thread = JsonSerializer.Deserialize(chatData, JSON_OPTIONS); + return thread is not null && ChatThreadContainsTerms(thread, terms); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + LOG.LogWarning(ex, "Failed to search chat thread for workspace '{WorkspaceId}', chat '{ChatId}'.", chat.WorkspaceId, chat.ChatId); + return false; + } + finally + { + semaphore.Release(); + } + } + + private static async Task> SearchChatsAsync(IReadOnlyList chats, IReadOnlyList terms, bool includeThreadContents, CancellationToken token) + { + var results = new List(); + foreach (var chat in chats) + { + token.ThrowIfCancellationRequested(); + + var missingTerms = GetMissingTerms(chat.Name, terms); + if (missingTerms.Count == 0) + { + results.Add(new(chat, NameMatched: true, ThreadMatched: false)); + continue; + } + + if (!includeThreadContents) + continue; + + var threadMatched = await ThreadContainsTermsAsync(chat, missingTerms, token); + if (threadMatched) + results.Add(new(chat, NameMatched: false, ThreadMatched: true)); + } + + return results; + } + private static async Task UpdateCacheAfterChatStored(Guid workspaceId, Guid chatId, string chatDirectory, string chatName, DateTimeOffset lastEditTime) { await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); @@ -348,6 +434,55 @@ public static class WorkspaceBehaviour } } + public static async Task SearchWorkspaceChatsAsync(string searchText, bool includeThreadContents, CancellationToken token = default) + { + var terms = ParseSearchTerms(searchText); + if (terms.Count == 0) + return new([], []); + + List workspaces; + List temporaryChats; + + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(token); + try + { + await EnsureTreeShellLoadedCoreAsync(); + workspaces = []; + foreach (var workspaceId in WORKSPACE_TREE_CACHE.WorkspaceOrder) + { + token.ThrowIfCancellationRequested(); + if (!WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace)) + continue; + + if (!workspace.ChatsLoaded) + { + workspace.Chats = await ReadWorkspaceChatsCoreAsync(workspaceId, workspace.WorkspacePath); + workspace.ChatsLoaded = true; + } + + workspaces.Add(ToPublicWorkspace(workspace)); + } + + temporaryChats = WORKSPACE_TREE_CACHE.TemporaryChats.Select(ToPublicChat).ToList(); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + + var matchingWorkspaces = new List(); + foreach (var workspace in workspaces) + { + token.ThrowIfCancellationRequested(); + var matchingChats = await SearchChatsAsync(workspace.Chats, terms, includeThreadContents, token); + if (matchingChats.Count > 0) + matchingWorkspaces.Add(new(workspace.WorkspaceId, workspace.WorkspacePath, workspace.Name, matchingChats)); + } + + var matchingTemporaryChats = await SearchChatsAsync(temporaryChats, terms, includeThreadContents, token); + return new(matchingWorkspaces, matchingTemporaryChats); + } + public static async Task TryPrefetchRemainingChatsAsync(Func? onWorkspaceUpdated = null, CancellationToken token = default) { while (true) diff --git a/app/MindWork AI Studio/Tools/WorkspaceSearchResult.cs b/app/MindWork AI Studio/Tools/WorkspaceSearchResult.cs new file mode 100644 index 00000000..bd09ebff --- /dev/null +++ b/app/MindWork AI Studio/Tools/WorkspaceSearchResult.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools; + +public readonly record struct WorkspaceSearchResult(WorkspaceTreeChat Chat, bool NameMatched, bool ThreadMatched); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/WorkspaceSearchSnapshot.cs b/app/MindWork AI Studio/Tools/WorkspaceSearchSnapshot.cs new file mode 100644 index 00000000..9d1c2271 --- /dev/null +++ b/app/MindWork AI Studio/Tools/WorkspaceSearchSnapshot.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools; + +public readonly record struct WorkspaceSearchSnapshot(IReadOnlyList Workspaces, IReadOnlyList TemporaryChats); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/WorkspaceSearchWorkspace.cs b/app/MindWork AI Studio/Tools/WorkspaceSearchWorkspace.cs new file mode 100644 index 00000000..1c8ce2ac --- /dev/null +++ b/app/MindWork AI Studio/Tools/WorkspaceSearchWorkspace.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools; + +public readonly record struct WorkspaceSearchWorkspace(Guid WorkspaceId, string WorkspacePath, string Name, IReadOnlyList Chats); \ 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 ecdb4c5e..8cf1b5c9 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md @@ -4,6 +4,7 @@ - Added support for managed custom root certificate bundles and host allowlists for external HTTPS requests, helping Flatpak deployments connect to organization-internal services with private root CAs while keeping built-in cloud provider endpoints on system trust. - Added support for reading enterprise policy files from a Flatpak provisioning extension. - 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 the enterprise configuration details on the information page by showing where each configuration comes from and which configuration slot was used. - Upgraded dependencies. \ No newline at end of file