mirror of
				https://github.com/MindWorkAI/AI-Studio.git
				synced 2025-10-31 20:40:20 +00:00 
			
		
		
		
	Add workspaces & persistent chats (#23)
This commit is contained in:
		
							parent
							
								
									29263660fc
								
							
						
					
					
						commit
						59d0321625
					
				| @ -3,29 +3,35 @@ namespace AIStudio.Chat; | ||||
| /// <summary> | ||||
| /// Data structure for a chat thread. | ||||
| /// </summary> | ||||
| /// <param name="name">The name of the chat thread.</param> | ||||
| /// <param name="seed">The seed for the chat thread. Some providers use this to generate deterministic results.</param> | ||||
| /// <param name="systemPrompt">The system prompt for the chat thread.</param> | ||||
| /// <param name="blocks">The content blocks of the chat thread.</param> | ||||
| public sealed class ChatThread(string name, int seed, string systemPrompt, IEnumerable<ContentBlock> blocks) | ||||
| public sealed class ChatThread | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The unique identifier of the chat thread. | ||||
|     /// </summary> | ||||
|     public Guid ChatId { get; init; } | ||||
|      | ||||
|     /// <summary> | ||||
|     /// The unique identifier of the workspace. | ||||
|     /// </summary> | ||||
|     public Guid WorkspaceId { get; set; } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The name of the chat thread. Usually generated by an AI model or manually edited by the user. | ||||
|     /// </summary> | ||||
|     public string Name { get; set; } = name; | ||||
|     public string Name { get; set; } = string.Empty; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The seed for the chat thread. Some providers use this to generate deterministic results. | ||||
|     /// </summary> | ||||
|     public int Seed { get; set; } = seed; | ||||
|     public int Seed { get; init; } | ||||
|      | ||||
|     /// <summary> | ||||
|     /// The current system prompt for the chat thread. | ||||
|     /// </summary> | ||||
|     public string SystemPrompt { get; set; } = systemPrompt; | ||||
|      | ||||
|     public string SystemPrompt { get; init; } = string.Empty; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The content blocks of the chat thread. | ||||
|     /// </summary> | ||||
|     public List<ContentBlock> Blocks { get; init; } = blocks.ToList(); | ||||
|     public List<ContentBlock> Blocks { get; init; } = []; | ||||
| } | ||||
| @ -3,25 +3,22 @@ namespace AIStudio.Chat; | ||||
| /// <summary> | ||||
| /// A block of content in a chat thread. Might be any type of content, e.g., text, image, voice, etc. | ||||
| /// </summary> | ||||
| /// <param name="time">Time when the content block was created.</param> | ||||
| /// <param name="type">Type of the content block, e.g., text, image, voice, etc.</param> | ||||
| /// <param name="content">The content of the block.</param> | ||||
| public class ContentBlock(DateTimeOffset time, ContentType type, IContent content) | ||||
| public class ContentBlock | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Time when the content block was created. | ||||
|     /// </summary> | ||||
|     public DateTimeOffset Time => time; | ||||
|     public DateTimeOffset Time { get; init; } | ||||
|      | ||||
|     /// <summary> | ||||
|     /// Type of the content block, e.g., text, image, voice, etc. | ||||
|     /// </summary> | ||||
|     public ContentType ContentType => type; | ||||
|     public ContentType ContentType { get; init; } = ContentType.NONE; | ||||
|      | ||||
|     /// <summary> | ||||
|     /// The content of the block. | ||||
|     /// </summary> | ||||
|     public IContent Content => content; | ||||
|     public IContent? Content { get; init; } = null; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The role of the content block in the chat thread, e.g., user, AI, etc. | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| using System.Text.Json.Serialization; | ||||
| 
 | ||||
| using AIStudio.Provider; | ||||
| using AIStudio.Settings; | ||||
| 
 | ||||
| @ -11,15 +13,19 @@ public sealed class ContentImage : IContent | ||||
|     #region Implementation of IContent | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     [JsonIgnore] | ||||
|     public bool InitialRemoteWait { get; set; } = false; | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     [JsonIgnore] | ||||
|     public bool IsStreaming { get; set; } = false; | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     [JsonIgnore] | ||||
|     public Func<Task> StreamingDone { get; set; } = () => Task.CompletedTask; | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     [JsonIgnore] | ||||
|     public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask; | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| using System.Text.Json.Serialization; | ||||
| 
 | ||||
| using AIStudio.Provider; | ||||
| using AIStudio.Settings; | ||||
| 
 | ||||
| @ -17,14 +19,19 @@ public sealed class ContentText : IContent | ||||
|     #region Implementation of IContent | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     [JsonIgnore] | ||||
|     public bool InitialRemoteWait { get; set; } | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     // [JsonIgnore] | ||||
|     public bool IsStreaming { get; set; } | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     [JsonIgnore] | ||||
|     public Func<Task> StreamingDone { get; set; } = () => Task.CompletedTask; | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     [JsonIgnore] | ||||
|     public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask; | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| using System.Text.Json.Serialization; | ||||
| 
 | ||||
| using AIStudio.Provider; | ||||
| using AIStudio.Settings; | ||||
| 
 | ||||
| @ -6,6 +8,8 @@ namespace AIStudio.Chat; | ||||
| /// <summary> | ||||
| /// The interface for any content in the chat. | ||||
| /// </summary> | ||||
| [JsonDerivedType(typeof(ContentText), typeDiscriminator: "text")] | ||||
| [JsonDerivedType(typeof(ContentImage), typeDiscriminator: "image")] | ||||
| public interface IContent | ||||
| { | ||||
|     /// <summary> | ||||
| @ -13,22 +17,26 @@ public interface IContent | ||||
|     /// Does not indicate that the stream is finished; it only indicates that we are | ||||
|     /// waiting for the first response, i.e., wait for the remote to pick up the request. | ||||
|     /// </summary> | ||||
|     [JsonIgnore] | ||||
|     public bool InitialRemoteWait { get; set; } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Indicates whether the content is streaming right now. False, if the content is | ||||
|     /// either static or the stream has finished. | ||||
|     /// </summary> | ||||
|     [JsonIgnore] | ||||
|     public bool IsStreaming { get; set; } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// An action that is called when the content was changed during streaming. | ||||
|     /// </summary> | ||||
|     [JsonIgnore] | ||||
|     public Func<Task> StreamingEvent { get; set; } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// An action that is called when the streaming is done. | ||||
|     /// </summary> | ||||
|     [JsonIgnore] | ||||
|     public Func<Task> StreamingDone { get; set; } | ||||
|      | ||||
|     /// <summary> | ||||
|  | ||||
| @ -13,6 +13,7 @@ public partial class Changelog | ||||
|      | ||||
|     public static readonly Log[] LOGS =  | ||||
|     [ | ||||
|         new (160, "v0.7.0, build 160 (2024-07-13 08:21 UTC)", "v0.7.0.md"), | ||||
|         new (159, "v0.6.3, build 159 (2024-07-03 18:26 UTC)", "v0.6.3.md"), | ||||
|         new (158, "v0.6.2, build 158 (2024-07-01 18:03 UTC)", "v0.6.2.md"), | ||||
|         new (157, "v0.6.1, build 157 (2024-06-30 19:00 UTC)", "v0.6.1.md"), | ||||
|  | ||||
							
								
								
									
										3
									
								
								app/MindWork AI Studio/Components/Blocks/ITreeItem.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								app/MindWork AI Studio/Components/Blocks/ITreeItem.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| namespace AIStudio.Components.Blocks; | ||||
| 
 | ||||
| public interface ITreeItem; | ||||
| @ -51,6 +51,11 @@ public partial class InnerScrolling : MSGComponentBase | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
| 
 | ||||
|     public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default | ||||
|     { | ||||
|         return Task.FromResult(default(TResult)); | ||||
|     } | ||||
| 
 | ||||
|     #endregion | ||||
| 
 | ||||
|     private string Height => $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight});"; | ||||
|  | ||||
							
								
								
									
										3
									
								
								app/MindWork AI Studio/Components/Blocks/TreeButton.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								app/MindWork AI Studio/Components/Blocks/TreeButton.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| namespace AIStudio.Components.Blocks; | ||||
| 
 | ||||
| public readonly record struct TreeButton(WorkspaceBranch Branch, int Depth, string Text, string Icon, Func<Task> Action) : ITreeItem; | ||||
							
								
								
									
										3
									
								
								app/MindWork AI Studio/Components/Blocks/TreeDivider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								app/MindWork AI Studio/Components/Blocks/TreeDivider.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| namespace AIStudio.Components.Blocks; | ||||
| 
 | ||||
| public readonly record struct TreeDivider : ITreeItem; | ||||
							
								
								
									
										22
									
								
								app/MindWork AI Studio/Components/Blocks/TreeItemData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/MindWork AI Studio/Components/Blocks/TreeItemData.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| namespace AIStudio.Components.Blocks; | ||||
| 
 | ||||
| public class TreeItemData : ITreeItem | ||||
| { | ||||
|     public WorkspaceBranch Branch { get; init; } = WorkspaceBranch.NONE; | ||||
|      | ||||
|     public int Depth { get; init; } | ||||
|      | ||||
|     public string Text { get; init; } = string.Empty; | ||||
|      | ||||
|     public string ShortenedText => Text.Length > 30 ? this.Text[..30] + "..." : this.Text; | ||||
| 
 | ||||
|     public string Icon { get; init; } = string.Empty; | ||||
| 
 | ||||
|     public TreeItemType Type { get; init; } | ||||
| 
 | ||||
|     public string Path { get; init; } = string.Empty; | ||||
| 
 | ||||
|     public bool Expandable { get; init; } = true; | ||||
| 
 | ||||
|     public HashSet<ITreeItem> Children { get; init; } = []; | ||||
| } | ||||
							
								
								
									
										9
									
								
								app/MindWork AI Studio/Components/Blocks/TreeItemType.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/MindWork AI Studio/Components/Blocks/TreeItemType.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| namespace AIStudio.Components.Blocks; | ||||
| 
 | ||||
| public enum TreeItemType | ||||
| { | ||||
|     NONE, | ||||
|      | ||||
|     CHAT, | ||||
|     WORKSPACE, | ||||
| } | ||||
| @ -0,0 +1,9 @@ | ||||
| namespace AIStudio.Components.Blocks; | ||||
| 
 | ||||
| public enum WorkspaceBranch | ||||
| { | ||||
|     NONE, | ||||
|      | ||||
|     WORKSPACES, | ||||
|     TEMPORARY_CHATS, | ||||
| } | ||||
							
								
								
									
										88
									
								
								app/MindWork AI Studio/Components/Blocks/Workspaces.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								app/MindWork AI Studio/Components/Blocks/Workspaces.razor
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,88 @@ | ||||
| <MudTreeView T="ITreeItem" Items="@this.treeItems" MultiSelection="@false" Hover="@true" ExpandOnClick="@true"> | ||||
|     <ItemTemplate Context="item"> | ||||
|         @switch (item) | ||||
|         { | ||||
|             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" 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)) | ||||
|                                     { | ||||
|                                         @("Empty chat") | ||||
|                                     } | ||||
|                                     else | ||||
|                                     { | ||||
|                                         @treeItem.ShortenedText | ||||
|                                     } | ||||
|                                 </MudText> | ||||
|                                 <div style="justify-self: end;"> | ||||
|                                      | ||||
|                                     <MudTooltip Text="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="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="Delete" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT"> | ||||
|                                         <MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Inherit" 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" 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="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="Delete" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT"> | ||||
|                                         <MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Inherit" OnClick="() => this.DeleteWorkspace(treeItem.Path)"/> | ||||
|                                     </MudTooltip> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </BodyContent> | ||||
|                     </MudTreeViewItem> | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     <MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item" 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> | ||||
							
								
								
									
										499
									
								
								app/MindWork AI Studio/Components/Blocks/Workspaces.razor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										499
									
								
								app/MindWork AI Studio/Components/Blocks/Workspaces.razor.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,499 @@ | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| 
 | ||||
| using AIStudio.Chat; | ||||
| using AIStudio.Components.CommonDialogs; | ||||
| using AIStudio.Settings; | ||||
| using AIStudio.Tools; | ||||
| 
 | ||||
| using Microsoft.AspNetCore.Components; | ||||
| 
 | ||||
| using DialogOptions = AIStudio.Components.CommonDialogs.DialogOptions; | ||||
| 
 | ||||
| namespace AIStudio.Components.Blocks; | ||||
| 
 | ||||
| public partial class Workspaces : ComponentBase | ||||
| { | ||||
|     [Inject] | ||||
|     private SettingsManager SettingsManager { get; set; } = null!; | ||||
|      | ||||
|     [Inject] | ||||
|     private IDialogService DialogService { get; set; } = null!; | ||||
|      | ||||
|     [Inject] | ||||
|     public Random RNG { get; set; } = null!; | ||||
|      | ||||
|     [Parameter] | ||||
|     public ChatThread? CurrentChatThread { get; set; } | ||||
|      | ||||
|     [Parameter] | ||||
|     public EventCallback<ChatThread> CurrentChatThreadChanged { get; set; } | ||||
| 
 | ||||
|     [Parameter] | ||||
|     public Func<Task> LoadedChatWasChanged { get; set; } = () => Task.CompletedTask; | ||||
| 
 | ||||
|     private const Placement WORKSPACE_ITEM_TOOLTIP_PLACEMENT = Placement.Bottom; | ||||
|      | ||||
|     private static readonly JsonSerializerOptions JSON_OPTIONS = new() | ||||
|     { | ||||
|         WriteIndented = true, | ||||
|         AllowTrailingCommas = true, | ||||
|         PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||
|         DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, | ||||
|         PropertyNameCaseInsensitive = true, | ||||
|         Converters = | ||||
|         { | ||||
|             new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper), | ||||
|         } | ||||
|     }; | ||||
|      | ||||
|     private readonly HashSet<ITreeItem> treeItems = new(); | ||||
|      | ||||
|     #region Overrides of ComponentBase | ||||
| 
 | ||||
|     protected override async Task 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 | ||||
|         // | ||||
|          | ||||
|         await this.LoadTreeItems(); | ||||
|         await base.OnInitializedAsync(); | ||||
|     } | ||||
| 
 | ||||
|     #endregion | ||||
| 
 | ||||
|     private async Task LoadTreeItems() | ||||
|     { | ||||
|         this.treeItems.Clear(); | ||||
|         this.treeItems.Add(new TreeItemData | ||||
|         { | ||||
|             Depth = 0, | ||||
|             Branch = WorkspaceBranch.WORKSPACES, | ||||
|             Text = "Workspaces", | ||||
|             Icon = Icons.Material.Filled.Folder, | ||||
|             Expandable = true, | ||||
|             Path = "root", | ||||
|             Children = await this.LoadWorkspaces(), | ||||
|         }); | ||||
| 
 | ||||
|         this.treeItems.Add(new TreeDivider()); | ||||
|         this.treeItems.Add(new TreeItemData | ||||
|         { | ||||
|             Depth = 0, | ||||
|             Branch = WorkspaceBranch.TEMPORARY_CHATS, | ||||
|             Text = "Temporary chats", | ||||
|             Icon = Icons.Material.Filled.Timer, | ||||
|             Expandable = true, | ||||
|             Path = "temp", | ||||
|             Children = await this.LoadTemporaryChats(), | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<HashSet<ITreeItem>> LoadTemporaryChats() | ||||
|     { | ||||
|         var tempChildren = new HashSet<ITreeItem>(); | ||||
| 
 | ||||
|         // | ||||
|         // Search for workspace folders in the data directory: | ||||
|         // | ||||
|                          | ||||
|         // Get the workspace root directory:  | ||||
|         var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats"); | ||||
|                          | ||||
|         // Ensure the directory exists: | ||||
|         Directory.CreateDirectory(temporaryDirectories); | ||||
|                          | ||||
|         // Enumerate the workspace directories: | ||||
|         foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories)) | ||||
|         { | ||||
|             // Read the `name` file: | ||||
|             var chatNamePath = Path.Join(tempChatDirPath, "name"); | ||||
|             var chatName = await File.ReadAllTextAsync(chatNamePath, Encoding.UTF8); | ||||
|                              | ||||
|             tempChildren.Add(new TreeItemData | ||||
|             { | ||||
|                 Type = TreeItemType.CHAT, | ||||
|                 Depth = 1, | ||||
|                 Branch = WorkspaceBranch.TEMPORARY_CHATS, | ||||
|                 Text = chatName, | ||||
|                 Icon = Icons.Material.Filled.Timer, | ||||
|                 Expandable = false, | ||||
|                 Path = tempChatDirPath, | ||||
|             }); | ||||
|         } | ||||
|                          | ||||
|         return tempChildren; | ||||
|     } | ||||
|      | ||||
|     public async Task<string> LoadWorkspaceName(Guid workspaceId) | ||||
|     { | ||||
|         if(workspaceId == Guid.Empty) | ||||
|             return string.Empty; | ||||
|          | ||||
|         var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString()); | ||||
|         var workspaceNamePath = Path.Join(workspacePath, "name"); | ||||
|         return await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8); | ||||
|     } | ||||
|      | ||||
|     private async Task<HashSet<ITreeItem>> LoadWorkspaces() | ||||
|     { | ||||
|         var workspaces = new HashSet<ITreeItem>(); | ||||
|          | ||||
|         // | ||||
|         // 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 the `name` file: | ||||
|             var workspaceNamePath = Path.Join(workspaceDirPath, "name"); | ||||
|             var workspaceName = await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8); | ||||
|                                  | ||||
|             workspaces.Add(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 TreeButton(WorkspaceBranch.WORKSPACES, 1, "Add workspace",Icons.Material.Filled.LibraryAdd, this.AddWorkspace)); | ||||
|         return workspaces; | ||||
|     } | ||||
| 
 | ||||
|     private async Task<HashSet<ITreeItem>> LoadWorkspaceChats(string workspacePath) | ||||
|     { | ||||
|         var workspaceChats = new HashSet<ITreeItem>(); | ||||
|          | ||||
|         // Enumerate the workspace directory: | ||||
|         foreach (var chatPath in Directory.EnumerateDirectories(workspacePath)) | ||||
|         { | ||||
|             // Read the `name` file: | ||||
|             var chatNamePath = Path.Join(chatPath, "name"); | ||||
|             var chatName = await File.ReadAllTextAsync(chatNamePath, Encoding.UTF8); | ||||
|                                  | ||||
|             workspaceChats.Add(new TreeItemData | ||||
|             { | ||||
|                 Type = TreeItemType.CHAT, | ||||
|                 Depth = 2, | ||||
|                 Branch = WorkspaceBranch.WORKSPACES, | ||||
|                 Text = chatName, | ||||
|                 Icon = Icons.Material.Filled.Chat, | ||||
|                 Expandable = false, | ||||
|                 Path = chatPath, | ||||
|             }); | ||||
|         } | ||||
|                              | ||||
|         workspaceChats.Add(new TreeButton(WorkspaceBranch.WORKSPACES, 2, "Add chat",Icons.Material.Filled.AddComment, () => this.AddChat(workspacePath))); | ||||
|         return workspaceChats; | ||||
|     } | ||||
| 
 | ||||
|     public async Task StoreChat(ChatThread chat) | ||||
|     { | ||||
|         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()); | ||||
|          | ||||
|         // Ensure the directory exists: | ||||
|         Directory.CreateDirectory(chatDirectory); | ||||
|          | ||||
|         // Save the chat name: | ||||
|         var chatNamePath = Path.Join(chatDirectory, "name"); | ||||
|         await File.WriteAllTextAsync(chatNamePath, chat.Name); | ||||
|          | ||||
|         // Save the thread as thread.json: | ||||
|         var chatPath = Path.Join(chatDirectory, "thread.json"); | ||||
|         await File.WriteAllTextAsync(chatPath, JsonSerializer.Serialize(chat, JSON_OPTIONS), Encoding.UTF8); | ||||
|          | ||||
|         // Reload the tree items: | ||||
|         await this.LoadTreeItems(); | ||||
|         this.StateHasChanged(); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<ChatThread?> LoadChat(string? chatPath, bool switchToChat) | ||||
|     { | ||||
|         if(string.IsNullOrWhiteSpace(chatPath)) | ||||
|             return null; | ||||
| 
 | ||||
|         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 | ||||
|             { | ||||
|                 { "Message", "Are you sure you want to load another chat? All unsaved changes will be lost." }, | ||||
|             }; | ||||
|          | ||||
|             var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Load Chat", dialogParameters, DialogOptions.FULLSCREEN); | ||||
|             var dialogResult = await dialogReference.Result; | ||||
|             if (dialogResult.Canceled) | ||||
|                 return null; | ||||
|         } | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|             var chatData = await File.ReadAllTextAsync(Path.Join(chatPath, "thread.json"), Encoding.UTF8); | ||||
|             var chat = JsonSerializer.Deserialize<ChatThread>(chatData, JSON_OPTIONS); | ||||
|             if (switchToChat) | ||||
|             { | ||||
|                 this.CurrentChatThread = chat; | ||||
|                 await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread); | ||||
|                 await this.LoadedChatWasChanged(); | ||||
|             } | ||||
|              | ||||
|             return chat; | ||||
|         } | ||||
|         catch (Exception e) | ||||
|         { | ||||
|             Console.WriteLine(e); | ||||
|         } | ||||
|          | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     public async Task DeleteChat(string? chatPath, bool askForConfirmation = true, bool unloadChat = true) | ||||
|     { | ||||
|         var chat = await this.LoadChat(chatPath, false); | ||||
|         if (chat is null) | ||||
|             return; | ||||
| 
 | ||||
|         if (askForConfirmation) | ||||
|         { | ||||
|             var workspaceName = await this.LoadWorkspaceName(chat.WorkspaceId); | ||||
|             var dialogParameters = new DialogParameters | ||||
|             { | ||||
|                 { | ||||
|                     "Message", (chat.WorkspaceId == Guid.Empty) switch | ||||
|                     { | ||||
|                         true => $"Are you sure you want to delete the temporary chat '{chat.Name}'?", | ||||
|                         false => $"Are you sure you want to delete the chat '{chat.Name}' in the workspace '{workspaceName}'?", | ||||
|                     } | ||||
|                 }, | ||||
|             }; | ||||
| 
 | ||||
|             var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Chat", dialogParameters, DialogOptions.FULLSCREEN); | ||||
|             var dialogResult = await dialogReference.Result; | ||||
|             if (dialogResult.Canceled) | ||||
|                 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(); | ||||
|          | ||||
|         if(unloadChat && this.CurrentChatThread?.ChatId == chat.ChatId) | ||||
|         { | ||||
|             this.CurrentChatThread = null; | ||||
|             await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread); | ||||
|             await this.LoadedChatWasChanged(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private async Task RenameChat(string? chatPath) | ||||
|     { | ||||
|         var chat = await this.LoadChat(chatPath, false); | ||||
|         if (chat is null) | ||||
|             return; | ||||
|          | ||||
|         var dialogParameters = new DialogParameters | ||||
|         { | ||||
|             { "Message", $"Please enter a new or edit the name for your chat '{chat.Name}':" }, | ||||
|             { "UserInput", chat.Name }, | ||||
|             { "ConfirmText", "Rename" }, | ||||
|             { "ConfirmColor", Color.Info }, | ||||
|         }; | ||||
|          | ||||
|         var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>("Rename Chat", dialogParameters, DialogOptions.FULLSCREEN); | ||||
|         var dialogResult = await dialogReference.Result; | ||||
|         if (dialogResult.Canceled) | ||||
|             return; | ||||
| 
 | ||||
|         chat.Name = (dialogResult.Data as string)!; | ||||
|         await this.StoreChat(chat); | ||||
|         await this.LoadTreeItems(); | ||||
|     } | ||||
|      | ||||
|     private async Task RenameWorkspace(string? workspacePath) | ||||
|     { | ||||
|         if(workspacePath is null) | ||||
|             return; | ||||
|          | ||||
|         var workspaceId = Guid.Parse(Path.GetFileName(workspacePath)); | ||||
|         var workspaceName = await this.LoadWorkspaceName(workspaceId); | ||||
|         var dialogParameters = new DialogParameters | ||||
|         { | ||||
|             { "Message", $"Please enter a new or edit the name for your workspace '{workspaceName}':" }, | ||||
|             { "UserInput", workspaceName }, | ||||
|             { "ConfirmText", "Rename" }, | ||||
|             { "ConfirmColor", Color.Info }, | ||||
|         }; | ||||
|          | ||||
|         var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>("Rename Workspace", dialogParameters, DialogOptions.FULLSCREEN); | ||||
|         var dialogResult = await dialogReference.Result; | ||||
|         if (dialogResult.Canceled) | ||||
|             return; | ||||
|          | ||||
|         var alteredWorkspaceName = (dialogResult.Data as string)!; | ||||
|         var workspaceNamePath = Path.Join(workspacePath, "name"); | ||||
|         await File.WriteAllTextAsync(workspaceNamePath, alteredWorkspaceName, Encoding.UTF8); | ||||
|         await this.LoadTreeItems(); | ||||
|     } | ||||
| 
 | ||||
|     private async Task AddWorkspace() | ||||
|     { | ||||
|         var dialogParameters = new DialogParameters | ||||
|         { | ||||
|             { "Message", "Please name your workspace:" }, | ||||
|             { "UserInput", string.Empty }, | ||||
|             { "ConfirmText", "Add workspace" }, | ||||
|             { "ConfirmColor", Color.Info }, | ||||
|         }; | ||||
|          | ||||
|         var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>("Add Workspace", dialogParameters, DialogOptions.FULLSCREEN); | ||||
|         var dialogResult = await dialogReference.Result; | ||||
|         if (dialogResult.Canceled) | ||||
|             return; | ||||
|          | ||||
|         var workspaceId = Guid.NewGuid(); | ||||
|         var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString()); | ||||
|         Directory.CreateDirectory(workspacePath); | ||||
|          | ||||
|         var workspaceNamePath = Path.Join(workspacePath, "name"); | ||||
|         await File.WriteAllTextAsync(workspaceNamePath, (dialogResult.Data as string)!, Encoding.UTF8); | ||||
|          | ||||
|         await this.LoadTreeItems(); | ||||
|     } | ||||
|      | ||||
|     private async Task DeleteWorkspace(string? workspacePath) | ||||
|     { | ||||
|         if(workspacePath is null) | ||||
|             return; | ||||
|          | ||||
|         var workspaceId = Guid.Parse(Path.GetFileName(workspacePath)); | ||||
|         var workspaceName = await this.LoadWorkspaceName(workspaceId); | ||||
|          | ||||
|         // Determine how many chats are in the workspace: | ||||
|         var chatCount = Directory.EnumerateDirectories(workspacePath).Count(); | ||||
|          | ||||
|         var dialogParameters = new DialogParameters | ||||
|         { | ||||
|             { "Message", $"Are you sure you want to delete the workspace '{workspaceName}'? This will also delete {chatCount} chat(s) in this workspace." }, | ||||
|         }; | ||||
|          | ||||
|         var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Workspace", dialogParameters, DialogOptions.FULLSCREEN); | ||||
|         var dialogResult = await dialogReference.Result; | ||||
|         if (dialogResult.Canceled) | ||||
|             return; | ||||
|          | ||||
|         Directory.Delete(workspacePath, true); | ||||
|         await this.LoadTreeItems(); | ||||
|     } | ||||
|      | ||||
|     private async Task MoveChat(string? chatPath) | ||||
|     { | ||||
|         var chat = await this.LoadChat(chatPath, false); | ||||
|         if (chat is null) | ||||
|             return; | ||||
|          | ||||
|         var dialogParameters = new DialogParameters | ||||
|         { | ||||
|             { "Message", "Please select the workspace where you want to move the chat to." }, | ||||
|             { "SelectedWorkspace", chat.WorkspaceId }, | ||||
|             { "ConfirmText", "Move chat" }, | ||||
|         }; | ||||
|          | ||||
|         var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>("Move Chat to Workspace", dialogParameters, DialogOptions.FULLSCREEN); | ||||
|         var dialogResult = await dialogReference.Result; | ||||
|         if (dialogResult.Canceled) | ||||
|             return; | ||||
|          | ||||
|         var workspaceId = dialogResult.Data is Guid id ? id : default; | ||||
|         if (workspaceId == Guid.Empty) | ||||
|             return; | ||||
|          | ||||
|         // Delete the chat from the current workspace or the temporary storage: | ||||
|         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); | ||||
|         } | ||||
|          | ||||
|         // 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; | ||||
|             await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread); | ||||
|             await this.LoadedChatWasChanged(); | ||||
|         } | ||||
|          | ||||
|         await this.StoreChat(chat); | ||||
|     } | ||||
|      | ||||
|     private async Task AddChat(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 | ||||
|             { | ||||
|                 { "Message", "Are you sure you want to create a another chat? All unsaved changes will be lost." }, | ||||
|             }; | ||||
|          | ||||
|             var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Create Chat", dialogParameters, DialogOptions.FULLSCREEN); | ||||
|             var dialogResult = await dialogReference.Result; | ||||
|             if (dialogResult.Canceled) | ||||
|                 return; | ||||
|         } | ||||
|          | ||||
|         var workspaceId = Guid.Parse(Path.GetFileName(workspacePath)); | ||||
|         var chat = new ChatThread | ||||
|         { | ||||
|             WorkspaceId = workspaceId, | ||||
|             ChatId = Guid.NewGuid(), | ||||
|             Name = string.Empty, | ||||
|             Seed = this.RNG.Next(), | ||||
|             SystemPrompt = "You are a helpful assistant!", | ||||
|             Blocks = [], | ||||
|         }; | ||||
|          | ||||
|         var chatPath = Path.Join(workspacePath, chat.ChatId.ToString()); | ||||
|          | ||||
|         await this.StoreChat(chat); | ||||
|         await this.LoadChat(chatPath, switchToChat: true); | ||||
|         await this.LoadTreeItems(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,10 @@ | ||||
| <MudDialog> | ||||
|     <DialogContent> | ||||
|         <MudText Typo="Typo.body1">@this.Message</MudText> | ||||
|         <MudTextField T="string" @bind-Text="@this.UserInput" Variant="Variant.Outlined" AutoGrow="@false" Lines="1" Label="Chat name" AutoFocus="@true"/> | ||||
|     </DialogContent> | ||||
|     <DialogActions> | ||||
|         <MudButton OnClick="@this.Cancel" Variant="Variant.Filled">Cancel</MudButton> | ||||
|         <MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="@this.ConfirmColor">@this.ConfirmText</MudButton> | ||||
|     </DialogActions> | ||||
| </MudDialog> | ||||
| @ -0,0 +1,25 @@ | ||||
| using Microsoft.AspNetCore.Components; | ||||
| 
 | ||||
| namespace AIStudio.Components.CommonDialogs; | ||||
| 
 | ||||
| public partial class SingleInputDialog : ComponentBase | ||||
| { | ||||
|     [CascadingParameter] | ||||
|     private MudDialogInstance MudDialog { get; set; } = null!; | ||||
| 
 | ||||
|     [Parameter] | ||||
|     public string Message { get; set; } = string.Empty; | ||||
|      | ||||
|     [Parameter] | ||||
|     public string UserInput { get; set; } = string.Empty; | ||||
|      | ||||
|     [Parameter] | ||||
|     public string ConfirmText { get; set; } = "OK"; | ||||
| 
 | ||||
|     [Parameter] | ||||
|     public Color ConfirmColor { get; set; } = Color.Error; | ||||
| 
 | ||||
|     private void Cancel() => this.MudDialog.Cancel(); | ||||
|      | ||||
|     private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.UserInput)); | ||||
| } | ||||
| @ -0,0 +1,15 @@ | ||||
| <MudDialog> | ||||
|     <DialogContent> | ||||
|         <MudText Typo="Typo.body1">@this.Message</MudText> | ||||
|         <MudList Clickable="@true" @bind-SelectedValue="@this.selectedWorkspace"> | ||||
|             @foreach (var (workspaceName, workspaceId) in this.workspaces) | ||||
|             { | ||||
|                 <MudListItem Text="@workspaceName" Icon="@Icons.Material.Filled.Description" Value="@workspaceId" /> | ||||
|             } | ||||
|         </MudList> | ||||
|     </DialogContent> | ||||
|     <DialogActions> | ||||
|         <MudButton OnClick="@this.Cancel" Variant="Variant.Filled">Cancel</MudButton> | ||||
|         <MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Info">@this.ConfirmText</MudButton> | ||||
|     </DialogActions> | ||||
| </MudDialog> | ||||
| @ -0,0 +1,60 @@ | ||||
| using System.Text; | ||||
| 
 | ||||
| using AIStudio.Settings; | ||||
| 
 | ||||
| using Microsoft.AspNetCore.Components; | ||||
| 
 | ||||
| namespace AIStudio.Components.CommonDialogs; | ||||
| 
 | ||||
| public partial class WorkspaceSelectionDialog : ComponentBase | ||||
| { | ||||
|     [CascadingParameter] | ||||
|     private MudDialogInstance MudDialog { get; set; } = null!; | ||||
| 
 | ||||
|     [Parameter] | ||||
|     public string Message { get; set; } = string.Empty; | ||||
|      | ||||
|     [Parameter] | ||||
|     public Guid SelectedWorkspace { get; set; } = Guid.Empty; | ||||
|      | ||||
|     [Parameter] | ||||
|     public string ConfirmText { get; set; } = "OK"; | ||||
|      | ||||
|     private readonly Dictionary<string, Guid> workspaces = new(); | ||||
|     private object? selectedWorkspace; | ||||
| 
 | ||||
|     #region Overrides of ComponentBase | ||||
| 
 | ||||
|     protected override async Task OnInitializedAsync() | ||||
|     { | ||||
|         this.selectedWorkspace = this.SelectedWorkspace; | ||||
|          | ||||
|         // Get the workspace root directory:  | ||||
|         var workspaceDirectories = Path.Join(SettingsManager.DataDirectory, "workspaces"); | ||||
|         if(!Directory.Exists(workspaceDirectories)) | ||||
|         { | ||||
|             await base.OnInitializedAsync(); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Enumerate the workspace directories: | ||||
|         foreach (var workspaceDirPath in Directory.EnumerateDirectories(workspaceDirectories)) | ||||
|         { | ||||
|             // Read the `name` file: | ||||
|             var workspaceNamePath = Path.Join(workspaceDirPath, "name"); | ||||
|             var workspaceName = await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8); | ||||
|              | ||||
|             // Add the workspace to the list: | ||||
|             this.workspaces.Add(workspaceName, Guid.Parse(Path.GetFileName(workspaceDirPath))); | ||||
|         } | ||||
| 
 | ||||
|         this.StateHasChanged(); | ||||
|         await base.OnInitializedAsync(); | ||||
|     } | ||||
| 
 | ||||
|     #endregion | ||||
| 
 | ||||
|     private void Cancel() => this.MudDialog.Cancel(); | ||||
|      | ||||
|     private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.selectedWorkspace is Guid workspaceId ? workspaceId : default)); | ||||
| } | ||||
| @ -30,4 +30,21 @@ public static class ConfigurationSelectDataFactory | ||||
|         yield return new("Check every day", UpdateBehavior.DAILY); | ||||
|         yield return new ("Check every week", UpdateBehavior.WEEKLY); | ||||
|     } | ||||
|      | ||||
|     public static IEnumerable<ConfigurationSelectData<WorkspaceStorageBehavior>> GetWorkspaceStorageBehaviorData() | ||||
|     { | ||||
|         yield return new("Disable workspaces", WorkspaceStorageBehavior.DISABLE_WORKSPACES); | ||||
|         yield return new("Store chats automatically", WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY); | ||||
|         yield return new("Store chats manually", WorkspaceStorageBehavior.STORE_CHATS_MANUALLY); | ||||
|     } | ||||
|      | ||||
|     public static IEnumerable<ConfigurationSelectData<WorkspaceStorageTemporaryMaintenancePolicy>> GetWorkspaceStorageTemporaryMaintenancePolicyData() | ||||
|     { | ||||
|         yield return new("No automatic maintenance for temporary chats", WorkspaceStorageTemporaryMaintenancePolicy.NO_AUTOMATIC_MAINTENANCE); | ||||
|         yield return new("Delete temporary chats older than 7 days", WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_7_DAYS); | ||||
|         yield return new("Delete temporary chats older than 30 days", WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_30_DAYS); | ||||
|         yield return new("Delete temporary chats older than 90 days", WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_90_DAYS); | ||||
|         yield return new("Delete temporary chats older than 180 days", WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_180_DAYS); | ||||
|         yield return new("Delete temporary chats older than 1 year", WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_365_DAYS); | ||||
|     } | ||||
| } | ||||
| @ -10,8 +10,8 @@ | ||||
|                         <MudTooltip Text="Home" Placement="Placement.Right"> | ||||
|                             <MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home">Home</MudNavLink> | ||||
|                         </MudTooltip> | ||||
|                         <MudTooltip Text="Chats" Placement="Placement.Right"> | ||||
|                             <MudNavLink Href="/chat" Icon="@Icons.Material.Filled.Chat">Chats</MudNavLink> | ||||
|                         <MudTooltip Text="Chat" Placement="Placement.Right"> | ||||
|                             <MudNavLink Href="/chat" Icon="@Icons.Material.Filled.Chat">Chat</MudNavLink> | ||||
|                         </MudTooltip> | ||||
|                         <MudTooltip Text="Supporters" Placement="Placement.Right"> | ||||
|                             <MudNavLink Href="/supporters" Icon="@Icons.Material.Filled.Favorite" IconColor="Color.Error">Supporters</MudNavLink> | ||||
|  | ||||
| @ -3,6 +3,7 @@ using AIStudio.Settings; | ||||
| using AIStudio.Tools; | ||||
| 
 | ||||
| using Microsoft.AspNetCore.Components; | ||||
| using Microsoft.AspNetCore.Components.Routing; | ||||
| 
 | ||||
| using DialogOptions = AIStudio.Components.CommonDialogs.DialogOptions; | ||||
| 
 | ||||
| @ -27,6 +28,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver | ||||
|      | ||||
|     [Inject] | ||||
|     private ISnackbar Snackbar { get; init; } = null!; | ||||
|      | ||||
|     [Inject] | ||||
|     private NavigationManager NavigationManager { get; init; } = null!; | ||||
| 
 | ||||
|     public string AdditionalHeight { get; private set; } = "0em"; | ||||
|      | ||||
| @ -40,6 +44,8 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver | ||||
| 
 | ||||
|     protected override async Task OnInitializedAsync() | ||||
|     { | ||||
|         this.NavigationManager.RegisterLocationChangingHandler(this.OnLocationChanging); | ||||
|          | ||||
|         // | ||||
|         // We use the Tauri API (Rust) to get the data and config directories | ||||
|         // for this app. | ||||
| @ -49,7 +55,8 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver | ||||
|          | ||||
|         // Store the directories in the settings manager: | ||||
|         SettingsManager.ConfigDirectory = configDir; | ||||
|         SettingsManager.DataDirectory = dataDir; | ||||
|         SettingsManager.DataDirectory = Path.Join(dataDir, "data"); | ||||
|         Directory.CreateDirectory(SettingsManager.DataDirectory); | ||||
|          | ||||
|         // Ensure that all settings are loaded: | ||||
|         await this.SettingsManager.LoadSettings(); | ||||
| @ -60,6 +67,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver | ||||
|          | ||||
|         // Set the js runtime for the update service: | ||||
|         UpdateService.SetBlazorDependencies(this.JsRuntime, this.Snackbar); | ||||
|         TemporaryChatService.Initialize(); | ||||
|          | ||||
|         await base.OnInitializedAsync(); | ||||
|     } | ||||
| @ -91,6 +99,11 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) | ||||
|     { | ||||
|         return Task.FromResult<TResult?>(default); | ||||
|     } | ||||
| 
 | ||||
|     #endregion | ||||
| 
 | ||||
|     private async Task DismissUpdate() | ||||
| @ -145,4 +158,26 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver | ||||
|         this.StateHasChanged(); | ||||
|         await this.Rust.InstallUpdate(this.JsRuntime); | ||||
|     } | ||||
|      | ||||
|     private async ValueTask OnLocationChanging(LocationChangingContext context) | ||||
|     { | ||||
|         if (await MessageBus.INSTANCE.SendMessageUseFirstResult<bool, bool>(this, Event.HAS_CHAT_UNSAVED_CHANGES)) | ||||
|         { | ||||
|             var dialogParameters = new DialogParameters | ||||
|             { | ||||
|                 { "Message", "Are you sure you want to leave the chat page? All unsaved changes will be lost." }, | ||||
|             }; | ||||
|          | ||||
|             var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Leave Chat Page", dialogParameters, DialogOptions.FULLSCREEN); | ||||
|             var dialogResult = await dialogReference.Result; | ||||
|             if (dialogResult.Canceled) | ||||
|             { | ||||
|                 context.PreventNavigation(); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // User accepted to leave the chat page, reset the chat state: | ||||
|             await MessageBus.INSTANCE.SendMessage<bool>(this, Event.RESET_CHAT_STATE); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -2,7 +2,19 @@ | ||||
| @using AIStudio.Chat | ||||
| @using AIStudio.Settings | ||||
| 
 | ||||
| <MudText Typo="Typo.h3" Class="mb-2">Chats</MudText> | ||||
| @inherits AIStudio.Tools.MSGComponentBase | ||||
| 
 | ||||
| <MudText Typo="Typo.h3" Class="mb-2 mr-3"> | ||||
|     @if (this.chatThread is not null && this.chatThread.WorkspaceId != Guid.Empty) | ||||
|     { | ||||
|         @($"Chat in Workspace \"{this.currentWorkspaceName}\"") | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         @("Temporary Chat") | ||||
|     } | ||||
| </MudText> | ||||
| 
 | ||||
| <MudSelect T="Provider" @bind-Value="@this.selectedProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-2 rounded-lg" Variant="Variant.Outlined"> | ||||
|     @foreach (var provider in this.SettingsManager.ConfigurationData.Providers) | ||||
|     { | ||||
| @ -24,5 +36,64 @@ | ||||
|         <MudPaper Style="flex: 0 0 auto;"> | ||||
|             <MudTextField T="string" @ref="@this.inputField" @bind-Text="@this.userInput" Variant="Variant.Outlined" AutoGrow="@true" Lines="3" MaxLines="12" Label="@this.InputLabel" Placeholder="@this.ProviderPlaceholder" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Send" OnAdornmentClick="() => this.SendMessage()" ReadOnly="!this.IsProviderSelected || this.isStreaming" Immediate="@true" OnKeyUp="this.InputKeyEvent" UserAttributes="@USER_INPUT_ATTRIBUTES"/> | ||||
|         </MudPaper> | ||||
|         <MudPaper Class="mt-1" Outlined="@true"> | ||||
|             <MudToolBar WrapContent="true"> | ||||
|                 @if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES) | ||||
|                 { | ||||
|                     <MudTooltip Text="Your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> | ||||
|                         <MudIconButton Icon="@Icons.Material.Filled.SnippetFolder" OnClick="() => this.ToggleWorkspaces()"/> | ||||
|                     </MudTooltip> | ||||
|                 } | ||||
| 
 | ||||
|                 @if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY) | ||||
|                 { | ||||
|                     <MudTooltip Text="Save chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> | ||||
|                         <MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="() => this.SaveThread()" Disabled="@(!this.CanThreadBeSaved)"/> | ||||
|                     </MudTooltip> | ||||
|                 } | ||||
| 
 | ||||
|                 <MudTooltip Text="Start temporary chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> | ||||
|                     <MudIconButton Icon="@Icons.Material.Filled.AddComment" OnClick="() => this.StartNewChat(useSameWorkspace: false)"/> | ||||
|                 </MudTooltip> | ||||
| 
 | ||||
|                 @if (!string.IsNullOrWhiteSpace(this.currentWorkspaceName)) | ||||
|                 { | ||||
|                     <MudTooltip Text="@this.TooltipAddChatToWorkspace" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> | ||||
|                         <MudIconButton Icon="@Icons.Material.Filled.CommentBank" OnClick="() => this.StartNewChat(useSameWorkspace: true)"/> | ||||
|                     </MudTooltip> | ||||
|                 } | ||||
|                  | ||||
|                 @if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) | ||||
|                 { | ||||
|                     <MudTooltip Text="Delete this chat & start a new one" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> | ||||
|                         <MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true)" Disabled="@(!this.CanThreadBeSaved)"/> | ||||
|                     </MudTooltip> | ||||
|                 } | ||||
| 
 | ||||
|                 @if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES) | ||||
|                 { | ||||
|                     <MudTooltip Text="Move chat to (another) workspace" Placement="@TOOLBAR_TOOLTIP_PLACEMENT"> | ||||
|                         <MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="() => this.MoveChatToWorkspace()"/> | ||||
|                     </MudTooltip> | ||||
|                 } | ||||
|             </MudToolBar> | ||||
|         </MudPaper> | ||||
|     </FooterContent> | ||||
| </InnerScrolling> | ||||
| </InnerScrolling> | ||||
| 
 | ||||
| @if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior != WorkspaceStorageBehavior.DISABLE_WORKSPACES) | ||||
| { | ||||
|     <MudDrawer @bind-Open="@this.workspacesVisible" Width="40em" Height="100%" Anchor="Anchor.Start" Variant="DrawerVariant.Temporary" Elevation="1"> | ||||
|         <MudDrawerHeader> | ||||
|             <MudStack Row="@true" AlignItems="AlignItems.Center"> | ||||
|                 <MudText Typo="Typo.h6" Class="mr-3"> | ||||
|                     Your workspaces | ||||
|                 </MudText> | ||||
|                 <MudIconButton Icon="@Icons.Material.Filled.Close" Variant="Variant.Filled" Color="Color.Default" Size="Size.Small" OnClick="() => this.ToggleWorkspaces()"/> | ||||
|             </MudStack> | ||||
|         </MudDrawerHeader> | ||||
|         <MudDrawerContainer Class="ml-6"> | ||||
|             <Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" LoadedChatWasChanged="this.LoadedChatChanged"/> | ||||
|         </MudDrawerContainer> | ||||
|     </MudDrawer> | ||||
| } | ||||
| @ -1,16 +1,21 @@ | ||||
| using AIStudio.Chat; | ||||
| using AIStudio.Components.Blocks; | ||||
| using AIStudio.Components.CommonDialogs; | ||||
| using AIStudio.Provider; | ||||
| using AIStudio.Settings; | ||||
| using AIStudio.Tools; | ||||
| 
 | ||||
| using Microsoft.AspNetCore.Components; | ||||
| using Microsoft.AspNetCore.Components.Web; | ||||
| 
 | ||||
| using DialogOptions = AIStudio.Components.CommonDialogs.DialogOptions; | ||||
| 
 | ||||
| namespace AIStudio.Components.Pages; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The chat page. | ||||
| /// </summary> | ||||
| public partial class Chat : ComponentBase | ||||
| public partial class Chat : MSGComponentBase, IAsyncDisposable | ||||
| { | ||||
|     [Inject] | ||||
|     private SettingsManager SettingsManager { get; set; } = null!; | ||||
| @ -20,13 +25,22 @@ public partial class Chat : ComponentBase | ||||
| 
 | ||||
|     [Inject] | ||||
|     public Random RNG { get; set; } = null!; | ||||
|      | ||||
|     [Inject] | ||||
|     public IDialogService DialogService { get; set; } = null!; | ||||
| 
 | ||||
|     private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Bottom; | ||||
|     private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new(); | ||||
|      | ||||
|     private AIStudio.Settings.Provider selectedProvider; | ||||
|     private ChatThread? chatThread; | ||||
|     private bool hasUnsavedChanges; | ||||
|     private bool isStreaming; | ||||
|     private string userInput = string.Empty; | ||||
|     private string currentWorkspaceName = string.Empty; | ||||
|     private Guid currentWorkspaceId = Guid.Empty; | ||||
|     private bool workspacesVisible; | ||||
|     private Workspaces? workspaces; | ||||
|      | ||||
|     // Unfortunately, we need the input field reference to clear it after sending a message. | ||||
|     // This is necessary because we have to handle the key events ourselves. Otherwise, | ||||
| @ -37,14 +51,11 @@ public partial class Chat : ComponentBase | ||||
| 
 | ||||
|     protected override async Task OnInitializedAsync() | ||||
|     { | ||||
|         this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE ]); | ||||
|          | ||||
|         // Configure the spellchecking for the user input: | ||||
|         this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES); | ||||
|          | ||||
|         // For now, we just create a new chat thread. | ||||
|         // Later we want the chats to be persisted | ||||
|         // across page loads and organize them in | ||||
|         // a chat history & workspaces. | ||||
|         this.chatThread = new("Thread 1", this.RNG.Next(), "You are a helpful assistant!", []); | ||||
|         await base.OnInitializedAsync(); | ||||
|     } | ||||
| 
 | ||||
| @ -56,25 +67,60 @@ public partial class Chat : ComponentBase | ||||
| 
 | ||||
|     private string InputLabel => this.IsProviderSelected ? $"Your Prompt (use selected instance '{this.selectedProvider.InstanceName}', provider '{this.selectedProvider.UsedProvider.ToName()}')" : "Select a provider first"; | ||||
|      | ||||
|     private bool CanThreadBeSaved => this.chatThread is not null && this.chatThread.Blocks.Count > 0; | ||||
| 
 | ||||
|     private string TooltipAddChatToWorkspace => $"Start new chat in workspace \"{this.currentWorkspaceName}\""; | ||||
|      | ||||
|     private async Task SendMessage() | ||||
|     { | ||||
|         if (!this.IsProviderSelected) | ||||
|             return; | ||||
|          | ||||
|         // Create a new chat thread if necessary: | ||||
|         var threadName = this.ExtractThreadName(this.userInput); | ||||
| 
 | ||||
|         if (this.chatThread is null) | ||||
|         { | ||||
|             this.chatThread = new() | ||||
|             { | ||||
|                 WorkspaceId = this.currentWorkspaceId, | ||||
|                 ChatId = Guid.NewGuid(), | ||||
|                 Name = threadName, | ||||
|                 Seed = this.RNG.Next(), | ||||
|                 SystemPrompt = "You are a helpful assistant!", | ||||
|                 Blocks = [], | ||||
|             }; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             // Set the thread name if it is empty: | ||||
|             if (string.IsNullOrWhiteSpace(this.chatThread.Name)) | ||||
|                 this.chatThread.Name = threadName; | ||||
|         } | ||||
|          | ||||
|         // | ||||
|         // Add the user message to the thread: | ||||
|         // | ||||
|         var time = DateTimeOffset.Now; | ||||
|         this.chatThread?.Blocks.Add(new ContentBlock(time, ContentType.TEXT, new ContentText | ||||
|         this.chatThread?.Blocks.Add(new ContentBlock | ||||
|         { | ||||
|             // Text content properties: | ||||
|             Text = this.userInput, | ||||
|         }) | ||||
|         { | ||||
|             // Content block properties: | ||||
|             Time = time, | ||||
|             ContentType = ContentType.TEXT, | ||||
|             Role = ChatRole.USER, | ||||
|             Content = new ContentText | ||||
|             { | ||||
|                 Text = this.userInput, | ||||
|             }, | ||||
|         }); | ||||
| 
 | ||||
|         // Save the chat: | ||||
|         if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) | ||||
|         { | ||||
|             await this.SaveThread(); | ||||
|             this.hasUnsavedChanges = false; | ||||
|             this.StateHasChanged(); | ||||
|         } | ||||
| 
 | ||||
|         // | ||||
|         // Add the AI response to the thread: | ||||
|         // | ||||
| @ -85,9 +131,12 @@ public partial class Chat : ComponentBase | ||||
|             InitialRemoteWait = true, | ||||
|         }; | ||||
|          | ||||
|         this.chatThread?.Blocks.Add(new ContentBlock(time, ContentType.TEXT, aiText) | ||||
|         this.chatThread?.Blocks.Add(new ContentBlock | ||||
|         { | ||||
|             Time = time, | ||||
|             ContentType = ContentType.TEXT, | ||||
|             Role = ChatRole.AI, | ||||
|             Content = aiText, | ||||
|         }); | ||||
|          | ||||
|         // Clear the input field: | ||||
| @ -96,6 +145,7 @@ public partial class Chat : ComponentBase | ||||
|          | ||||
|         // Enable the stream state for the chat component: | ||||
|         this.isStreaming = true; | ||||
|         this.hasUnsavedChanges = true; | ||||
|         this.StateHasChanged(); | ||||
|          | ||||
|         // Use the selected provider to get the AI response. | ||||
| @ -103,6 +153,13 @@ public partial class Chat : ComponentBase | ||||
|         // content to be streamed. | ||||
|         await aiText.CreateFromProviderAsync(this.selectedProvider.UsedProvider.CreateProvider(this.selectedProvider.InstanceName, this.selectedProvider.Hostname), this.JsRuntime, this.SettingsManager, this.selectedProvider.Model, this.chatThread); | ||||
|          | ||||
|         // Save the chat: | ||||
|         if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) | ||||
|         { | ||||
|             await this.SaveThread(); | ||||
|             this.hasUnsavedChanges = false; | ||||
|         } | ||||
| 
 | ||||
|         // Disable the stream state: | ||||
|         this.isStreaming = false; | ||||
|         this.StateHasChanged(); | ||||
| @ -110,6 +167,7 @@ public partial class Chat : ComponentBase | ||||
| 
 | ||||
|     private async Task InputKeyEvent(KeyboardEventArgs keyEvent) | ||||
|     { | ||||
|         this.hasUnsavedChanges = true; | ||||
|         var key = keyEvent.Code.ToLowerInvariant(); | ||||
|          | ||||
|         // Was the enter key (either enter or numpad enter) pressed? | ||||
| @ -132,4 +190,216 @@ public partial class Chat : ComponentBase | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private void ToggleWorkspaces() | ||||
|     { | ||||
|         this.workspacesVisible = !this.workspacesVisible; | ||||
|     } | ||||
|      | ||||
|     private async Task SaveThread() | ||||
|     { | ||||
|         if(this.workspaces is null) | ||||
|             return; | ||||
|          | ||||
|         if(this.chatThread is null) | ||||
|             return; | ||||
|          | ||||
|         if (!this.CanThreadBeSaved) | ||||
|             return; | ||||
|          | ||||
|         await this.workspaces.StoreChat(this.chatThread); | ||||
|         this.hasUnsavedChanges = false; | ||||
|     } | ||||
|      | ||||
|     private string ExtractThreadName(string firstUserInput) | ||||
|     { | ||||
|         // We select the first 10 words of the user input: | ||||
|         var words = firstUserInput.Split(' ', StringSplitOptions.RemoveEmptyEntries); | ||||
|         var threadName = string.Join(' ', words.Take(10)); | ||||
|          | ||||
|         // If the thread name is empty, we use a default name: | ||||
|         if (string.IsNullOrWhiteSpace(threadName)) | ||||
|             threadName = "Thread"; | ||||
|          | ||||
|         return threadName; | ||||
|     } | ||||
| 
 | ||||
|     private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false) | ||||
|     { | ||||
|         if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges) | ||||
|         { | ||||
|             var dialogParameters = new DialogParameters | ||||
|             { | ||||
|                 { "Message", "Are you sure you want to start a new chat? All unsaved changes will be lost." }, | ||||
|             }; | ||||
|          | ||||
|             var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Chat", dialogParameters, DialogOptions.FULLSCREEN); | ||||
|             var dialogResult = await dialogReference.Result; | ||||
|             if (dialogResult.Canceled) | ||||
|                 return; | ||||
|         } | ||||
| 
 | ||||
|         if (this.chatThread is not null && this.workspaces is not null && deletePreviousChat) | ||||
|         { | ||||
|             string chatPath; | ||||
|             if (this.chatThread.WorkspaceId == Guid.Empty) | ||||
|             { | ||||
|                 chatPath = Path.Join(SettingsManager.DataDirectory, "tempChats", this.chatThread.ChatId.ToString()); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 chatPath = Path.Join(SettingsManager.DataDirectory, "workspaces", this.chatThread.WorkspaceId.ToString(), this.chatThread.ChatId.ToString()); | ||||
|             } | ||||
|              | ||||
|             await this.workspaces.DeleteChat(chatPath, askForConfirmation: false, unloadChat: true); | ||||
|         } | ||||
| 
 | ||||
|         this.isStreaming = false; | ||||
|         this.hasUnsavedChanges = false; | ||||
|         this.userInput = string.Empty; | ||||
| 
 | ||||
|         if (!useSameWorkspace) | ||||
|         { | ||||
|             this.chatThread = null; | ||||
|             this.currentWorkspaceId = Guid.Empty; | ||||
|             this.currentWorkspaceName = string.Empty; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             this.chatThread = new() | ||||
|             { | ||||
|                 WorkspaceId = this.currentWorkspaceId, | ||||
|                 ChatId = Guid.NewGuid(), | ||||
|                 Name = string.Empty, | ||||
|                 Seed = this.RNG.Next(), | ||||
|                 SystemPrompt = "You are a helpful assistant!", | ||||
|                 Blocks = [], | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         await this.inputField.Clear(); | ||||
|     } | ||||
| 
 | ||||
|     private async Task MoveChatToWorkspace() | ||||
|     { | ||||
|         if(this.chatThread is null) | ||||
|             return; | ||||
|          | ||||
|         if(this.workspaces is null) | ||||
|             return; | ||||
|          | ||||
|         if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges) | ||||
|         { | ||||
|             var confirmationDialogParameters = new DialogParameters | ||||
|             { | ||||
|                 { "Message", "Are you sure you want to move this chat? All unsaved changes will be lost." }, | ||||
|             }; | ||||
|          | ||||
|             var confirmationDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Unsaved Changes", confirmationDialogParameters, DialogOptions.FULLSCREEN); | ||||
|             var confirmationDialogResult = await confirmationDialogReference.Result; | ||||
|             if (confirmationDialogResult.Canceled) | ||||
|                 return; | ||||
|         } | ||||
|          | ||||
|         var dialogParameters = new DialogParameters | ||||
|         { | ||||
|             { "Message", "Please select the workspace where you want to move the chat to." }, | ||||
|             { "SelectedWorkspace", this.chatThread?.WorkspaceId }, | ||||
|             { "ConfirmText", "Move chat" }, | ||||
|         }; | ||||
|          | ||||
|         var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>("Move Chat to Workspace", dialogParameters, DialogOptions.FULLSCREEN); | ||||
|         var dialogResult = await dialogReference.Result; | ||||
|         if (dialogResult.Canceled) | ||||
|             return; | ||||
|          | ||||
|         var workspaceId = dialogResult.Data is Guid id ? id : default; | ||||
|         if (workspaceId == Guid.Empty) | ||||
|             return; | ||||
|          | ||||
|         // Delete the chat from the current workspace or the temporary storage: | ||||
|         if (this.chatThread!.WorkspaceId == Guid.Empty) | ||||
|         { | ||||
|             // Case: The chat is stored in the temporary storage: | ||||
|             await this.workspaces.DeleteChat(Path.Join(SettingsManager.DataDirectory, "tempChats", this.chatThread.ChatId.ToString()), askForConfirmation: false, unloadChat: false); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             // Case: The chat is stored in a workspace. | ||||
|             await this.workspaces.DeleteChat(Path.Join(SettingsManager.DataDirectory, "workspaces", this.chatThread.WorkspaceId.ToString(), this.chatThread.ChatId.ToString()), askForConfirmation: false, unloadChat: false); | ||||
|         } | ||||
|          | ||||
|         this.chatThread!.WorkspaceId = workspaceId; | ||||
|         await this.SaveThread(); | ||||
|          | ||||
|         this.currentWorkspaceId = this.chatThread.WorkspaceId; | ||||
|         this.currentWorkspaceName = await this.workspaces.LoadWorkspaceName(this.chatThread.WorkspaceId); | ||||
|     } | ||||
| 
 | ||||
|     private async Task LoadedChatChanged() | ||||
|     { | ||||
|         if(this.workspaces is null) | ||||
|             return; | ||||
|          | ||||
|         this.isStreaming = false; | ||||
|         this.hasUnsavedChanges = false; | ||||
|         this.userInput = string.Empty; | ||||
|         this.currentWorkspaceId = this.chatThread?.WorkspaceId ?? Guid.Empty; | ||||
|         this.currentWorkspaceName = this.chatThread is null ? string.Empty : await this.workspaces.LoadWorkspaceName(this.chatThread.WorkspaceId); | ||||
|          | ||||
|         await this.inputField.Clear(); | ||||
|     } | ||||
| 
 | ||||
|     private void ResetState() | ||||
|     { | ||||
|         this.isStreaming = false; | ||||
|         this.hasUnsavedChanges = false; | ||||
|         this.userInput = string.Empty; | ||||
|         this.currentWorkspaceId = Guid.Empty; | ||||
|         this.currentWorkspaceName = string.Empty; | ||||
|         this.chatThread = null; | ||||
|     } | ||||
| 
 | ||||
|     #region Overrides of MSGComponentBase | ||||
| 
 | ||||
|     public override Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default | ||||
|     { | ||||
|         switch (triggeredEvent) | ||||
|         { | ||||
|             case Event.RESET_CHAT_STATE: | ||||
|                 this.ResetState(); | ||||
|                 break; | ||||
|         } | ||||
|          | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
| 
 | ||||
|     public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default | ||||
|     { | ||||
|         switch (triggeredEvent) | ||||
|         { | ||||
|             case Event.HAS_CHAT_UNSAVED_CHANGES: | ||||
|                 if(this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) | ||||
|                     return Task.FromResult((TResult?) (object) false); | ||||
|                  | ||||
|                 return Task.FromResult((TResult?)(object)this.hasUnsavedChanges); | ||||
|         } | ||||
|          | ||||
|         return Task.FromResult(default(TResult)); | ||||
|     } | ||||
| 
 | ||||
|     #endregion | ||||
| 
 | ||||
|     #region Implementation of IAsyncDisposable | ||||
| 
 | ||||
|     public async ValueTask DisposeAsync() | ||||
|     { | ||||
|         if(this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) | ||||
|         { | ||||
|             await this.SaveThread(); | ||||
|             this.hasUnsavedChanges = false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #endregion | ||||
| } | ||||
| @ -62,5 +62,7 @@ | ||||
|         <ConfigurationOption OptionDescription="Enable spellchecking?" LabelOn="Spellchecking is enabled" LabelOff="Spellchecking is disabled" State="@(() => this.SettingsManager.ConfigurationData.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.EnableSpellchecking = updatedState)" OptionHelp="When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections." /> | ||||
|         <ConfigurationSelect OptionDescription="Shortcut to send input" SelectedValue="@(() => this.SettingsManager.ConfigurationData.ShortcutSendBehavior)" Data="@ConfigurationSelectDataFactory.GetSendBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.ShortcutSendBehavior = selectedValue)" OptionHelp="Do you want to use any shortcut to send your input?"/> | ||||
|         <ConfigurationSelect OptionDescription="Check for updates" SelectedValue="@(() => this.SettingsManager.ConfigurationData.UpdateBehavior)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.UpdateBehavior = selectedValue)" OptionHelp="How often should we check for app updates?"/> | ||||
|         <ConfigurationSelect OptionDescription="Workspace behavior" SelectedValue="@(() => this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior)" Data="@ConfigurationSelectDataFactory.GetWorkspaceStorageBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior = selectedValue)" OptionHelp="Should we store your chats?"/> | ||||
|         <ConfigurationSelect OptionDescription="Workspace maintenance" SelectedValue="@(() => this.SettingsManager.ConfigurationData.WorkspaceStorageTemporaryMaintenancePolicy)" Data="@ConfigurationSelectDataFactory.GetWorkspaceStorageTemporaryMaintenancePolicyData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.WorkspaceStorageTemporaryMaintenancePolicy = selectedValue)" OptionHelp="If and when should we delete your temporary chats?"/> | ||||
|     </MudPaper> | ||||
| </InnerScrolling> | ||||
| @ -31,6 +31,7 @@ builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>(); | ||||
| builder.Services.AddSingleton<SettingsManager>(); | ||||
| builder.Services.AddSingleton<Random>(); | ||||
| builder.Services.AddHostedService<UpdateService>(); | ||||
| builder.Services.AddHostedService<TemporaryChatService>(); | ||||
| builder.Services.AddRazorComponents() | ||||
|     .AddInteractiveServerComponents() | ||||
|     .AddHubOptions(options => | ||||
|  | ||||
| @ -81,7 +81,7 @@ public sealed class ProviderSelfHosted(string hostname) : BaseProvider($"{hostna | ||||
|         // Read the stream, line by line: | ||||
|         while(!streamReader.EndOfStream) | ||||
|         { | ||||
|             // Check if the token is cancelled: | ||||
|             // Check if the token is canceled: | ||||
|             if(token.IsCancellationRequested) | ||||
|                 yield break; | ||||
|              | ||||
|  | ||||
| @ -41,4 +41,14 @@ public sealed class Data | ||||
|     /// If and when we should look for updates. | ||||
|     /// </summary> | ||||
|     public UpdateBehavior UpdateBehavior { get; set; } = UpdateBehavior.ONCE_STARTUP; | ||||
|      | ||||
|     /// <summary> | ||||
|     /// The chat storage behavior. | ||||
|     /// </summary> | ||||
|     public WorkspaceStorageBehavior WorkspaceStorageBehavior { get; set; } = WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY; | ||||
|      | ||||
|     /// <summary> | ||||
|     /// The chat storage maintenance behavior. | ||||
|     /// </summary> | ||||
|     public WorkspaceStorageTemporaryMaintenancePolicy WorkspaceStorageTemporaryMaintenancePolicy { get; set; } = WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_90_DAYS; | ||||
| } | ||||
| @ -13,7 +13,7 @@ public enum SendBehavior | ||||
|      | ||||
|     /// <summary> | ||||
|     /// The user can send the input to the AI by pressing any modifier key | ||||
|     /// together with the enter key. Alternatively, the user can click the send | ||||
|     /// together with the enter key. Alternatively, the user can click the sent | ||||
|     /// button. The enter key alone adds a new line. | ||||
|     /// </summary> | ||||
|     MODIFER_ENTER_IS_SENDING, | ||||
|  | ||||
| @ -0,0 +1,9 @@ | ||||
| namespace AIStudio.Settings; | ||||
| 
 | ||||
| public enum WorkspaceStorageBehavior | ||||
| { | ||||
|     DISABLE_WORKSPACES, | ||||
|      | ||||
|     STORE_CHATS_AUTOMATICALLY, | ||||
|     STORE_CHATS_MANUALLY, | ||||
| } | ||||
| @ -0,0 +1,12 @@ | ||||
| namespace AIStudio.Settings; | ||||
| 
 | ||||
| public enum WorkspaceStorageTemporaryMaintenancePolicy | ||||
| { | ||||
|     NO_AUTOMATIC_MAINTENANCE, | ||||
|      | ||||
|     DELETE_OLDER_THAN_7_DAYS, | ||||
|     DELETE_OLDER_THAN_30_DAYS, | ||||
|     DELETE_OLDER_THAN_90_DAYS, | ||||
|     DELETE_OLDER_THAN_180_DAYS, | ||||
|     DELETE_OLDER_THAN_365_DAYS, | ||||
| } | ||||
| @ -10,4 +10,8 @@ public enum Event | ||||
|     // Update events: | ||||
|     USER_SEARCH_FOR_UPDATE, | ||||
|     UPDATE_AVAILABLE, | ||||
|      | ||||
|     // Chat events: | ||||
|     HAS_CHAT_UNSAVED_CHANGES, | ||||
|     RESET_CHAT_STATE, | ||||
| } | ||||
| @ -5,4 +5,6 @@ namespace AIStudio.Tools; | ||||
| public interface IMessageBusReceiver | ||||
| { | ||||
|     public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data); | ||||
|      | ||||
|     public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data); | ||||
| } | ||||
| @ -20,6 +20,8 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus | ||||
|     #region Implementation of IMessageBusReceiver | ||||
| 
 | ||||
|     public abstract Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data); | ||||
|      | ||||
|     public abstract Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data); | ||||
| 
 | ||||
|     #endregion | ||||
| 
 | ||||
| @ -37,6 +39,11 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus | ||||
|         await this.MessageBus.SendMessage(this, triggeredEvent, data); | ||||
|     } | ||||
|      | ||||
|     protected async Task<TResult?> SendMessageWithResult<TPayload, TResult>(Event triggeredEvent, TPayload? data) | ||||
|     { | ||||
|         return await this.MessageBus.SendMessageUseFirstResult<TPayload, TResult>(this, triggeredEvent, data); | ||||
|     } | ||||
|      | ||||
|     protected void ApplyFilters(ComponentBase[] components, Event[] events) | ||||
|     { | ||||
|         this.MessageBus.ApplyFilters(this, components, events); | ||||
|  | ||||
| @ -64,4 +64,23 @@ public sealed class MessageBus | ||||
|             this.sendingSemaphore.Release(); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public async Task<TResult?> SendMessageUseFirstResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data = default) | ||||
|     { | ||||
|         foreach (var (receiver, componentFilter) in this.componentFilters) | ||||
|         { | ||||
|             if (componentFilter.Length > 0 && sendingComponent is not null && !componentFilter.Contains(sendingComponent)) | ||||
|                 continue; | ||||
| 
 | ||||
|             var eventFilter = this.componentEvents[receiver]; | ||||
|             if (eventFilter.Length == 0 || eventFilter.Contains(triggeredEvent)) | ||||
|             { | ||||
|                 var result = await receiver.ProcessMessageWithResult<TPayload, TResult>(sendingComponent, triggeredEvent, data); | ||||
|                 if (result is not null) | ||||
|                     return (TResult) result; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         return default; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										71
									
								
								app/MindWork AI Studio/Tools/TemporaryChatService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								app/MindWork AI Studio/Tools/TemporaryChatService.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | ||||
| using AIStudio.Settings; | ||||
| 
 | ||||
| namespace AIStudio.Tools; | ||||
| 
 | ||||
| public class TemporaryChatService(SettingsManager settingsManager) : BackgroundService | ||||
| { | ||||
|     private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromDays(1); | ||||
|     private static bool IS_INITIALIZED; | ||||
| 
 | ||||
|     #region Overrides of BackgroundService | ||||
| 
 | ||||
|     protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||
|     { | ||||
|         while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED) | ||||
|             await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken); | ||||
| 
 | ||||
|         await settingsManager.LoadSettings(); | ||||
|         if(settingsManager.ConfigurationData.WorkspaceStorageTemporaryMaintenancePolicy is WorkspaceStorageTemporaryMaintenancePolicy.NO_AUTOMATIC_MAINTENANCE) | ||||
|         { | ||||
|             Console.WriteLine("Automatic maintenance of temporary chat storage is disabled. Exiting maintenance service."); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         await this.StartMaintenance(); | ||||
|         while (!stoppingToken.IsCancellationRequested) | ||||
|         { | ||||
|             await Task.Delay(CHECK_INTERVAL, stoppingToken); | ||||
|             await this.StartMaintenance(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #endregion | ||||
| 
 | ||||
|     private Task StartMaintenance() | ||||
|     { | ||||
|         var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats"); | ||||
|         if(!Directory.Exists(temporaryDirectories)) | ||||
|             return Task.CompletedTask; | ||||
|          | ||||
|         foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories)) | ||||
|         { | ||||
|             var chatPath = Path.Join(tempChatDirPath, "thread.json"); | ||||
|             var chatMetadata = new FileInfo(chatPath); | ||||
|             if (!chatMetadata.Exists) | ||||
|                 continue; | ||||
| 
 | ||||
|             var lastWriteTime = chatMetadata.LastWriteTimeUtc; | ||||
|             var deleteChat = settingsManager.ConfigurationData.WorkspaceStorageTemporaryMaintenancePolicy switch | ||||
|             { | ||||
|                 WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_7_DAYS => DateTime.UtcNow - lastWriteTime > TimeSpan.FromDays(7), | ||||
|                 WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_30_DAYS => DateTime.UtcNow - lastWriteTime > TimeSpan.FromDays(30), | ||||
|                 WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_90_DAYS => DateTime.UtcNow - lastWriteTime > TimeSpan.FromDays(90), | ||||
|                 WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_180_DAYS => DateTime.UtcNow - lastWriteTime > TimeSpan.FromDays(180), | ||||
|                 WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_365_DAYS => DateTime.UtcNow - lastWriteTime > TimeSpan.FromDays(365), | ||||
|                  | ||||
|                 WorkspaceStorageTemporaryMaintenancePolicy.NO_AUTOMATIC_MAINTENANCE => false, | ||||
|                 _ => false, | ||||
|             }; | ||||
|              | ||||
|             if(deleteChat) | ||||
|                 Directory.Delete(tempChatDirPath, true); | ||||
|         } | ||||
| 
 | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|      | ||||
|     public static void Initialize() | ||||
|     { | ||||
|         IS_INITIALIZED = true; | ||||
|     } | ||||
| } | ||||
| @ -14,10 +14,11 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver | ||||
|     private static ISnackbar? SNACKBAR; | ||||
|      | ||||
|     private readonly SettingsManager settingsManager; | ||||
|     private readonly TimeSpan updateInterval; | ||||
|     private readonly MessageBus messageBus; | ||||
|     private readonly Rust rust; | ||||
|      | ||||
|     private TimeSpan updateInterval; | ||||
|      | ||||
|     public UpdateService(MessageBus messageBus, SettingsManager settingsManager, Rust rust) | ||||
|     { | ||||
|         this.settingsManager = settingsManager; | ||||
| @ -26,8 +27,16 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver | ||||
| 
 | ||||
|         this.messageBus.RegisterComponent(this); | ||||
|         this.ApplyFilters([], [ Event.USER_SEARCH_FOR_UPDATE ]); | ||||
|          | ||||
|         this.updateInterval = settingsManager.ConfigurationData.UpdateBehavior switch | ||||
|     } | ||||
|      | ||||
|     #region Overrides of BackgroundService | ||||
| 
 | ||||
|     protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||
|     { | ||||
|         while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED) | ||||
|             await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken); | ||||
| 
 | ||||
|         this.updateInterval = this.settingsManager.ConfigurationData.UpdateBehavior switch | ||||
|         { | ||||
|             UpdateBehavior.NO_CHECK => Timeout.InfiniteTimeSpan, | ||||
|             UpdateBehavior.ONCE_STARTUP => Timeout.InfiniteTimeSpan, | ||||
| @ -38,21 +47,11 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver | ||||
|              | ||||
|             _ => TimeSpan.FromHours(1) | ||||
|         }; | ||||
|     } | ||||
|      | ||||
|     #region Overrides of BackgroundService | ||||
| 
 | ||||
|     protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||
|     { | ||||
|         while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED) | ||||
|         { | ||||
|             await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); | ||||
|         } | ||||
| 
 | ||||
|         await this.settingsManager.LoadSettings(); | ||||
|         if(this.settingsManager.ConfigurationData.UpdateBehavior != UpdateBehavior.NO_CHECK) | ||||
|             await this.CheckForUpdate(); | ||||
|          | ||||
|         if(this.settingsManager.ConfigurationData.UpdateBehavior is UpdateBehavior.NO_CHECK) | ||||
|             return; | ||||
|          | ||||
|         await this.CheckForUpdate(); | ||||
|         while (!stoppingToken.IsCancellationRequested) | ||||
|         { | ||||
|             await Task.Delay(this.updateInterval, stoppingToken); | ||||
| @ -73,6 +72,11 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) | ||||
|     { | ||||
|         return Task.FromResult<TResult?>(default); | ||||
|     } | ||||
| 
 | ||||
|     #endregion | ||||
| 
 | ||||
|  | ||||
| @ -13,9 +13,9 @@ | ||||
|       }, | ||||
|       "Microsoft.NET.ILLink.Tasks": { | ||||
|         "type": "Direct", | ||||
|         "requested": "[8.0.6, )", | ||||
|         "resolved": "8.0.6", | ||||
|         "contentHash": "E+lDylsTeP4ZiDmnEkiJ5wobnGaIJzFhOgZppznJCb69UZgbh6G3cfv1pnLDLLBx6JAgl0kAlnINDeT3uCuczQ==" | ||||
|         "requested": "[8.0.7, )", | ||||
|         "resolved": "8.0.7", | ||||
|         "contentHash": "iI52ptEKby2ymQ6B7h4TWbFmm85T4VvLgc/HvS45Yr3lgi4IIFbQtjON3bQbX/Vc94jXNSLvrDOp5Kh7SJyFYQ==" | ||||
|       }, | ||||
|       "MudBlazor": { | ||||
|         "type": "Direct", | ||||
|  | ||||
							
								
								
									
										16
									
								
								app/MindWork AI Studio/wwwroot/changelog/v0.7.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/MindWork AI Studio/wwwroot/changelog/v0.7.0.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| # v0.7.0, build 160 (2024-07-13 08:21 UTC) | ||||
| - Added workspaces for organizing your chats | ||||
| - Added temporary chats for quick conversations | ||||
| - Added configurable chat maintenance settings for temporary chats | ||||
| - Added configuration to disable workspace and temporary chat features; no chat will be stored in this case | ||||
| - Added possibility to rename chats | ||||
| - Added possibility to delete chats | ||||
| - Added possibility to move chats between workspaces | ||||
| - Added possibility to delete entire workspaces | ||||
| - Added possibility to rename workspaces | ||||
| - Added possibility to create new workspaces | ||||
| - Added feature to delete the current chat and start a new one | ||||
| - Added feature to start a new chat within the current workspace | ||||
| - Added a confirmation dialog for when unsaved changes are about to be lost | ||||
| - Show the current workspace in the title bar | ||||
| - Fixed a bug where the periodic, automatic update check would not work | ||||
							
								
								
									
										12
									
								
								metadata.txt
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								metadata.txt
									
									
									
									
									
								
							| @ -1,9 +1,9 @@ | ||||
| 0.6.3 | ||||
| 2024-07-03 18:26:31 UTC | ||||
| 159 | ||||
| 8.0.206 (commit bb12410699) | ||||
| 8.0.6 (commit 3b8b000a0e) | ||||
| 0.7.0 | ||||
| 2024-07-13 08:21:49 UTC | ||||
| 160 | ||||
| 8.0.107 (commit 1bdaef7265) | ||||
| 8.0.7 (commit 2aade6beb0) | ||||
| 1.79.0 (commit 129f3b996) | ||||
| 6.20.0 | ||||
| 1.6.1 | ||||
| ac6748e9eb5, release | ||||
| 09e1f8715f8, release | ||||
|  | ||||
							
								
								
									
										2
									
								
								runtime/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								runtime/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -2313,7 +2313,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "mindwork-ai-studio" | ||||
| version = "0.6.3" | ||||
| version = "0.7.0" | ||||
| dependencies = [ | ||||
|  "arboard", | ||||
|  "flexi_logger", | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| [package] | ||||
| name = "mindwork-ai-studio" | ||||
| version = "0.6.3" | ||||
| version = "0.7.0" | ||||
| edition = "2021" | ||||
| description = "MindWork AI Studio" | ||||
| authors = ["Thorsten Sommer"] | ||||
|  | ||||
| @ -6,7 +6,7 @@ | ||||
|   }, | ||||
|   "package": { | ||||
|     "productName": "MindWork AI Studio", | ||||
| 	"version": "0.6.3" | ||||
| 	"version": "0.7.0" | ||||
|   }, | ||||
|   "tauri": { | ||||
|     "allowlist": { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user