From 906d9ba058865010d7c3eb0e0f7d92748fa5fbb5 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 5 Mar 2026 18:37:18 +0100 Subject: [PATCH 01/27] Improved workspace performance (#680) --- .../Assistants/I18N/allTexts.lua | 13 +- .../Components/ChatComponent.razor.cs | 79 ++- .../Components/TreeItemType.cs | 1 + .../Components/Workspaces.razor | 201 ++++--- .../Components/Workspaces.razor.cs | 551 ++++++++--------- .../Dialogs/WorkspaceSelectionDialog.razor.cs | 24 +- app/MindWork AI Studio/Pages/Chat.razor | 9 + app/MindWork AI Studio/Pages/Chat.razor.cs | 8 + .../plugin.lua | 13 +- .../plugin.lua | 13 +- .../Tools/Services/TemporaryChatService.cs | 1 + .../Tools/WorkspaceBehaviour.cs | 564 +++++++++++++++--- .../Tools/WorkspaceTreeCacheSnapshot.cs | 3 + .../Tools/WorkspaceTreeChat.cs | 4 + .../Tools/WorkspaceTreeWorkspace.cs | 3 + .../wwwroot/changelog/v26.3.1.md | 1 + 16 files changed, 994 insertions(+), 494 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/WorkspaceTreeCacheSnapshot.cs create mode 100644 app/MindWork AI Studio/Tools/WorkspaceTreeChat.cs create mode 100644 app/MindWork AI Studio/Tools/WorkspaceTreeWorkspace.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 02233950..eeb90c5b 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -2557,8 +2557,8 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1016188706"] = "Are you sure -- Move chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1133040906"] = "Move chat" --- Unnamed workspace -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1307384014"] = "Unnamed workspace" +-- Loading chats... +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1364857726"] = "Loading chats..." -- Delete UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1469573738"] = "Delete" @@ -2614,9 +2614,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T323280982"] = "Please enter -- Please enter a workspace name. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3288132732"] = "Please enter a workspace name." --- Unnamed chat -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3310482275"] = "Unnamed chat" - -- Rename UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3355849203"] = "Rename" @@ -4981,6 +4978,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Learn about one co -- Localization UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Localization" +-- Reload your workspaces +UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T194629703"] = "Reload your workspaces" + -- Hide your workspaces UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T2351468526"] = "Hide your workspaces" @@ -6519,3 +6519,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unnamed w -- Delete Chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Delete Chat" + +-- Unnamed chat +UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3310482275"] = "Unnamed chat" diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs index c7bd4dce..9c2b38a0 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -56,6 +56,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable private bool autoSaveEnabled; private string currentWorkspaceName = string.Empty; private Guid currentWorkspaceId = Guid.Empty; + private Guid currentChatThreadId = Guid.Empty; private CancellationTokenSource? cancellationTokenSource; private HashSet chatDocumentPaths = []; @@ -197,8 +198,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable // if (this.ChatThread is not null) { + this.currentChatThreadId = this.ChatThread.ChatId; this.currentWorkspaceId = this.ChatThread.WorkspaceId; - this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId); + this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId); this.WorkspaceName(this.currentWorkspaceName); } @@ -214,12 +216,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable this.mustStoreChat = false; if(this.Workspaces is not null) - await this.Workspaces.StoreChat(this.ChatThread); + await this.Workspaces.StoreChatAsync(this.ChatThread); else - await WorkspaceBehaviour.StoreChat(this.ChatThread); + await WorkspaceBehaviour.StoreChatAsync(this.ChatThread); this.currentWorkspaceId = this.ChatThread.WorkspaceId; - this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId); + this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId); this.WorkspaceName(this.currentWorkspaceName); } @@ -227,14 +229,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable { this.Logger.LogInformation($"Try to load the chat '{this.loadChat.ChatId}' now."); this.mustLoadChat = false; - this.ChatThread = await WorkspaceBehaviour.LoadChat(this.loadChat); + this.ChatThread = await WorkspaceBehaviour.LoadChatAsync(this.loadChat); if(this.ChatThread is not null) { await this.ChatThreadChanged.InvokeAsync(this.ChatThread); this.Logger.LogInformation($"The chat '{this.ChatThread!.ChatId}' with title '{this.ChatThread.Name}' ({this.ChatThread.Blocks.Count} messages) was loaded successfully."); - this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId); + this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId); this.WorkspaceName(this.currentWorkspaceName); await this.SelectProviderWhenLoadingChat(); } @@ -260,7 +262,49 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable await base.OnAfterRenderAsync(firstRender); } + protected override async Task OnParametersSetAsync() + { + await this.SyncWorkspaceHeaderWithChatThreadAsync(); + await base.OnParametersSetAsync(); + } + #endregion + + private async Task SyncWorkspaceHeaderWithChatThreadAsync() + { + if (this.ChatThread is null) + { + if (this.currentChatThreadId != Guid.Empty || this.currentWorkspaceId != Guid.Empty || !string.IsNullOrWhiteSpace(this.currentWorkspaceName)) + { + this.currentChatThreadId = Guid.Empty; + this.currentWorkspaceId = Guid.Empty; + this.currentWorkspaceName = string.Empty; + this.WorkspaceName(this.currentWorkspaceName); + } + + return; + } + + // Guard: If ChatThread ID and WorkspaceId haven't changed, skip entirely. + // Using ID-based comparison instead of name-based to correctly handle + // temporary chats where the workspace name is always empty. + if (this.currentChatThreadId == this.ChatThread.ChatId + && this.currentWorkspaceId == this.ChatThread.WorkspaceId) + return; + + this.currentChatThreadId = this.ChatThread.ChatId; + this.currentWorkspaceId = this.ChatThread.WorkspaceId; + var loadedWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId); + + // Only notify the parent when the name actually changed to prevent + // an infinite render loop: WorkspaceName → UpdateWorkspaceName → + // StateHasChanged → re-render → OnParametersSetAsync → WorkspaceName → ... + if (this.currentWorkspaceName != loadedWorkspaceName) + { + this.currentWorkspaceName = loadedWorkspaceName; + this.WorkspaceName(this.currentWorkspaceName); + } + } private bool IsProviderSelected => this.Provider.UsedLLMProvider != LLMProviders.NONE; @@ -428,8 +472,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable if(!this.ChatThread.IsLLMProviderAllowed(this.Provider)) return; - // We need to blur the focus away from the input field - // to be able to clear the field: + // Blur the focus away from the input field to be able to clear it: await this.inputField.BlurAsync(); // Create a new chat thread if necessary: @@ -520,8 +563,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable // Clear the input field: await this.inputField.FocusAsync(); + this.userInput = string.Empty; this.chatDocumentPaths.Clear(); + await this.inputField.BlurAsync(); // Enable the stream state for the chat component: @@ -583,9 +628,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable // the workspace gets updated automatically when the chat is saved. // if (this.Workspaces is not null) - await this.Workspaces.StoreChat(this.ChatThread); + await this.Workspaces.StoreChatAsync(this.ChatThread); else - await WorkspaceBehaviour.StoreChat(this.ChatThread); + await WorkspaceBehaviour.StoreChatAsync(this.ChatThread); this.hasUnsavedChanges = false; } @@ -621,9 +666,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable chatPath = Path.Join(SettingsManager.DataDirectory, "workspaces", this.ChatThread.WorkspaceId.ToString(), this.ChatThread.ChatId.ToString()); if(this.Workspaces is null) - await WorkspaceBehaviour.DeleteChat(this.DialogService, this.ChatThread.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false); + await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, this.ChatThread.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false); else - await this.Workspaces.DeleteChat(chatPath, askForConfirmation: false, unloadChat: true); + await this.Workspaces.DeleteChatAsync(chatPath, askForConfirmation: false, unloadChat: true); } // @@ -665,6 +710,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable // to reset the chat thread: // this.ChatThread = null; + this.currentChatThreadId = Guid.Empty; this.currentWorkspaceId = Guid.Empty; this.currentWorkspaceName = string.Empty; this.WorkspaceName(this.currentWorkspaceName); @@ -739,13 +785,13 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable return; // Delete the chat from the current workspace or the temporary storage: - await WorkspaceBehaviour.DeleteChat(this.DialogService, this.ChatThread!.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false); + await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, this.ChatThread!.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false); this.ChatThread!.WorkspaceId = workspaceId; await this.SaveThread(); this.currentWorkspaceId = this.ChatThread.WorkspaceId; - this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId); + this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId); this.WorkspaceName(this.currentWorkspaceName); } @@ -758,12 +804,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable if (this.ChatThread is not null) { this.currentWorkspaceId = this.ChatThread.WorkspaceId; - this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId); + this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(this.ChatThread.WorkspaceId); this.WorkspaceName(this.currentWorkspaceName); + this.currentChatThreadId = this.ChatThread.ChatId; this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources); } else { + this.currentChatThreadId = Guid.Empty; this.currentWorkspaceId = Guid.Empty; this.currentWorkspaceName = string.Empty; this.WorkspaceName(this.currentWorkspaceName); @@ -785,6 +833,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable this.isStreaming = false; this.hasUnsavedChanges = false; this.userInput = string.Empty; + this.currentChatThreadId = Guid.Empty; this.currentWorkspaceId = Guid.Empty; this.currentWorkspaceName = string.Empty; diff --git a/app/MindWork AI Studio/Components/TreeItemType.cs b/app/MindWork AI Studio/Components/TreeItemType.cs index f43823b4..35028056 100644 --- a/app/MindWork AI Studio/Components/TreeItemType.cs +++ b/app/MindWork AI Studio/Components/TreeItemType.cs @@ -4,6 +4,7 @@ public enum TreeItemType { NONE, + LOADING, CHAT, WORKSPACE, } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Workspaces.razor b/app/MindWork AI Studio/Components/Workspaces.razor index 80a81f60..56e5e59e 100644 --- a/app/MindWork AI Studio/Components/Workspaces.razor +++ b/app/MindWork AI Studio/Components/Workspaces.razor @@ -1,93 +1,114 @@ @inherits MSGComponentBase - - - @switch (item.Value) +@if (this.isInitialLoading) +{ + + + @for (var i = 0; i < 10; i++) { - case TreeDivider: -
  • - -
  • - break; - - case TreeItemData treeItem: - @if (treeItem.Type is TreeItemType.CHAT) - { - - -
    - - @if (string.IsNullOrWhiteSpace(treeItem.Text)) - { - @T("Empty chat") - } - else - { - @treeItem.ShortenedText - } - -
    - - - - - - - - - - - - -
    -
    -
    -
    - } - else if (treeItem.Type is TreeItemType.WORKSPACE) - { - - -
    - - @treeItem.Text - -
    - - - - - - - -
    -
    -
    -
    - } - else - { - - -
    - - @treeItem.Text - -
    -
    -
    - } - break; - - case TreeButton treeButton: -
  • -
    -
    - - @treeButton.Text - -
    -
  • - break; + } -
    -
    + +} +else +{ + + + @switch (item.Value) + { + case TreeDivider: +
  • + +
  • + break; + + case TreeItemData treeItem: + @if (treeItem.Type is TreeItemType.LOADING) + { + + + + + + } + else if (treeItem.Type is TreeItemType.CHAT) + { + + +
    + + @if (string.IsNullOrWhiteSpace(treeItem.Text)) + { + @T("Empty chat") + } + else + { + @treeItem.ShortenedText + } + +
    + + + + + + + + + + + + +
    +
    +
    +
    + } + else if (treeItem.Type is TreeItemType.WORKSPACE) + { + + +
    + + @treeItem.Text + +
    + + + + + + + +
    +
    +
    +
    + } + else + { + + +
    + + @treeItem.Text + +
    +
    +
    + } + break; + + case TreeButton treeButton: +
  • +
    +
    + + @treeButton.Text + +
    +
  • + break; + } +
    +
    +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Workspaces.razor.cs b/app/MindWork AI Studio/Components/Workspaces.razor.cs index f3564e65..106d5719 100644 --- a/app/MindWork AI Studio/Components/Workspaces.razor.cs +++ b/app/MindWork AI Studio/Components/Workspaces.razor.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using System.Text.Json; using AIStudio.Chat; @@ -29,31 +29,64 @@ public partial class Workspaces : MSGComponentBase public bool ExpandRootNodes { get; set; } = true; private const Placement WORKSPACE_ITEM_TOOLTIP_PLACEMENT = Placement.Bottom; + private readonly SemaphoreSlim treeLoadingSemaphore = new(1, 1); + private readonly List> treeItems = []; + private readonly HashSet loadingWorkspaceChatLists = []; - private readonly List> treeItems = new(); + private CancellationTokenSource? prefetchCancellationTokenSource; + private bool isInitialLoading = true; + private bool isDisposed; #region Overrides of ComponentBase protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - - // - // Notice: In order to get the server-based loading to work, we need to respect the following rules: - // - We must have initial tree items - // - Those initial tree items cannot have children - // - When assigning the tree items to the MudTreeViewItem component, we must set the Value property to the value of the item - // - // We won't await the loading of the tree items here, - // to avoid blocking the UI thread: - _ = this.LoadTreeItems(); + _ = this.LoadTreeItemsAsync(startPrefetch: true); } #endregion - private async Task LoadTreeItems() + private async Task LoadTreeItemsAsync(bool startPrefetch = true, bool forceReload = false) + { + await this.treeLoadingSemaphore.WaitAsync(); + try + { + if (this.isDisposed) + return; + + if (forceReload) + await WorkspaceBehaviour.ForceReloadWorkspaceTreeAsync(); + + var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); + this.BuildTreeItems(snapshot); + this.isInitialLoading = false; + } + finally + { + this.treeLoadingSemaphore.Release(); + } + + await this.SafeStateHasChanged(); + + if (startPrefetch) + await this.StartPrefetchAsync(); + } + + private void BuildTreeItems(WorkspaceTreeCacheSnapshot snapshot) { this.treeItems.Clear(); + + var workspaceChildren = new List>(); + foreach (var workspace in snapshot.Workspaces) + workspaceChildren.Add(this.CreateWorkspaceTreeItem(workspace)); + + workspaceChildren.Add(new TreeItemData + { + Expandable = false, + Value = new TreeButton(WorkspaceBranch.WORKSPACES, 1, T("Add workspace"), Icons.Material.Filled.LibraryAdd, this.AddWorkspaceAsync), + }); + this.treeItems.Add(new TreeItemData { Expanded = this.ExpandRootNodes, @@ -66,7 +99,7 @@ public partial class Workspaces : MSGComponentBase Icon = Icons.Material.Filled.Folder, Expandable = true, Path = "root", - Children = await this.LoadWorkspaces(), + Children = workspaceChildren, }, }); @@ -76,7 +109,10 @@ public partial class Workspaces : MSGComponentBase Value = new TreeDivider(), }); - await this.InvokeAsync(this.StateHasChanged); + var temporaryChatsChildren = new List>(); + foreach (var temporaryChat in snapshot.TemporaryChats.OrderByDescending(x => x.LastEditTime)) + temporaryChatsChildren.Add(CreateChatTreeItem(temporaryChat, WorkspaceBranch.TEMPORARY_CHATS, depth: 1, icon: Icons.Material.Filled.Timer)); + this.treeItems.Add(new TreeItemData { Expanded = this.ExpandRootNodes, @@ -89,234 +125,219 @@ public partial class Workspaces : MSGComponentBase Icon = Icons.Material.Filled.Timer, Expandable = true, Path = "temp", - Children = await this.LoadTemporaryChats(), + Children = temporaryChatsChildren, }, }); - + } + + private TreeItemData CreateWorkspaceTreeItem(WorkspaceTreeWorkspace workspace) + { + var children = new List>(); + if (workspace.ChatsLoaded) + { + foreach (var workspaceChat in workspace.Chats.OrderByDescending(x => x.LastEditTime)) + children.Add(CreateChatTreeItem(workspaceChat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat)); + } + else if (this.loadingWorkspaceChatLists.Contains(workspace.WorkspaceId)) + children.AddRange(this.CreateLoadingRows(workspace.WorkspacePath)); + + children.Add(new TreeItemData + { + Expandable = false, + Value = new TreeButton(WorkspaceBranch.WORKSPACES, 2, T("Add chat"), Icons.Material.Filled.AddComment, () => this.AddChatAsync(workspace.WorkspacePath)), + }); + + return new TreeItemData + { + Expandable = true, + Value = new TreeItemData + { + Type = TreeItemType.WORKSPACE, + Depth = 1, + Branch = WorkspaceBranch.WORKSPACES, + Text = workspace.Name, + Icon = Icons.Material.Filled.Description, + Expandable = true, + Path = workspace.WorkspacePath, + Children = children, + }, + }; + } + + private IReadOnlyCollection> CreateLoadingRows(string workspacePath) + { + return + [ + this.CreateLoadingTreeItem(workspacePath, "loading_1"), + this.CreateLoadingTreeItem(workspacePath, "loading_2"), + this.CreateLoadingTreeItem(workspacePath, "loading_3"), + ]; + } + + private TreeItemData CreateLoadingTreeItem(string workspacePath, string suffix) + { + return new TreeItemData + { + Expandable = false, + Value = new TreeItemData + { + Type = TreeItemType.LOADING, + Depth = 2, + Branch = WorkspaceBranch.WORKSPACES, + Text = T("Loading chats..."), + Icon = Icons.Material.Filled.HourglassTop, + Expandable = false, + Path = Path.Join(workspacePath, suffix), + }, + }; + } + + private static TreeItemData CreateChatTreeItem(WorkspaceTreeChat chat, WorkspaceBranch branch, int depth, string icon) + { + return new TreeItemData + { + Expandable = false, + Value = new TreeItemData + { + Type = TreeItemType.CHAT, + Depth = depth, + Branch = branch, + Text = chat.Name, + Icon = icon, + Expandable = false, + Path = chat.ChatPath, + LastEditTime = chat.LastEditTime, + }, + }; + } + + private async Task SafeStateHasChanged() + { + if (this.isDisposed) + return; + await this.InvokeAsync(this.StateHasChanged); } - private async Task>> LoadTemporaryChats() + private async Task StartPrefetchAsync() { - var tempChildren = new List(); - - // Get the temp root directory: - var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats"); - - // Ensure the directory exists: - Directory.CreateDirectory(temporaryDirectories); - - // Enumerate the chat directories: - foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories)) + if (this.prefetchCancellationTokenSource is not null) { - // Read or create the `name` file (self-heal): - var chatNamePath = Path.Join(tempChatDirPath, "name"); - string chatName; - try + await this.prefetchCancellationTokenSource.CancelAsync(); + this.prefetchCancellationTokenSource.Dispose(); + } + + this.prefetchCancellationTokenSource = new CancellationTokenSource(); + await this.PrefetchWorkspaceChatsAsync(this.prefetchCancellationTokenSource.Token); + } + + private async Task PrefetchWorkspaceChatsAsync(CancellationToken cancellationToken) + { + try + { + await WorkspaceBehaviour.TryPrefetchRemainingChatsAsync(async _ => { - if (!File.Exists(chatNamePath)) - { - chatName = T("Unnamed chat"); - await File.WriteAllTextAsync(chatNamePath, chatName, Encoding.UTF8); - } - else - { - chatName = await File.ReadAllTextAsync(chatNamePath, Encoding.UTF8); - if (string.IsNullOrWhiteSpace(chatName)) - { - chatName = T("Unnamed chat"); - await File.WriteAllTextAsync(chatNamePath, chatName, Encoding.UTF8); - } - } - } - catch - { - chatName = T("Unnamed chat"); - } - - // Read the last change time of the chat: - var chatThreadPath = Path.Join(tempChatDirPath, "thread.json"); - var lastEditTime = File.GetLastWriteTimeUtc(chatThreadPath); - - tempChildren.Add(new TreeItemData - { - Type = TreeItemType.CHAT, - Depth = 1, - Branch = WorkspaceBranch.TEMPORARY_CHATS, - Text = chatName, - Icon = Icons.Material.Filled.Timer, - Expandable = false, - Path = tempChatDirPath, - LastEditTime = lastEditTime, - }); + if (this.isDisposed || cancellationToken.IsCancellationRequested) + return; + + await this.LoadTreeItemsAsync(startPrefetch: false); + }, cancellationToken); + } + catch (OperationCanceledException) + { + // Expected when the component is hidden or disposed. + } + catch (Exception ex) + { + this.Logger.LogWarning(ex, "Failed while prefetching workspace chats."); + } + } + + private async Task OnWorkspaceClicked(TreeItemData treeItem) + { + if (treeItem.Type is not TreeItemType.WORKSPACE) + return; + + if (!Guid.TryParse(Path.GetFileName(treeItem.Path), out var workspaceId)) + return; + + await this.EnsureWorkspaceChatsLoadedAsync(workspaceId); + } + + private async Task EnsureWorkspaceChatsLoadedAsync(Guid workspaceId) + { + var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); + var hasWorkspace = false; + var chatsLoaded = false; + + foreach (var workspace in snapshot.Workspaces) + { + if (workspace.WorkspaceId != workspaceId) + continue; + + hasWorkspace = true; + chatsLoaded = workspace.ChatsLoaded; + break; + } + + if (!hasWorkspace || chatsLoaded || !this.loadingWorkspaceChatLists.Add(workspaceId)) + return; + + await this.LoadTreeItemsAsync(startPrefetch: false); + + try + { + await WorkspaceBehaviour.GetWorkspaceChatsAsync(workspaceId); + } + finally + { + this.loadingWorkspaceChatLists.Remove(workspaceId); + } + + await this.LoadTreeItemsAsync(startPrefetch: false); + } + + public async Task ForceRefreshFromDiskAsync() + { + if (this.prefetchCancellationTokenSource is not null) + { + await this.prefetchCancellationTokenSource.CancelAsync(); + this.prefetchCancellationTokenSource.Dispose(); + this.prefetchCancellationTokenSource = null; } - var result = new List>(tempChildren.OrderByDescending(n => n.LastEditTime).Select(n => new TreeItemData - { - Expandable = false, - Value = n, - })); - return result; - } - - private async Task>> LoadWorkspaces() - { - var workspaces = new List>(); + this.loadingWorkspaceChatLists.Clear(); + this.isInitialLoading = true; - // - // Search for workspace folders in the data directory: - // - - // Get the workspace root directory: - var workspaceDirectories = Path.Join(SettingsManager.DataDirectory, "workspaces"); - - // Ensure the directory exists: - Directory.CreateDirectory(workspaceDirectories); - - // Enumerate the workspace directories: - foreach (var workspaceDirPath in Directory.EnumerateDirectories(workspaceDirectories)) - { - // Read or create the `name` file (self-heal): - var workspaceNamePath = Path.Join(workspaceDirPath, "name"); - string workspaceName; - try - { - if (!File.Exists(workspaceNamePath)) - { - workspaceName = T("Unnamed workspace"); - await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8); - } - else - { - workspaceName = await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8); - if (string.IsNullOrWhiteSpace(workspaceName)) - { - workspaceName = T("Unnamed workspace"); - await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8); - } - } - } - catch - { - workspaceName = T("Unnamed workspace"); - } - - workspaces.Add(new TreeItemData - { - Expandable = true, - Value = new TreeItemData - { - Type = TreeItemType.WORKSPACE, - Depth = 1, - Branch = WorkspaceBranch.WORKSPACES, - Text = workspaceName, - Icon = Icons.Material.Filled.Description, - Expandable = true, - Path = workspaceDirPath, - Children = await this.LoadWorkspaceChats(workspaceDirPath), - }, - }); - } - - workspaces.Add(new TreeItemData - { - Expandable = false, - Value = new TreeButton(WorkspaceBranch.WORKSPACES, 1, T("Add workspace"),Icons.Material.Filled.LibraryAdd, this.AddWorkspace), - }); - return workspaces; + await this.SafeStateHasChanged(); + await this.LoadTreeItemsAsync(startPrefetch: true, forceReload: true); } - private async Task>> LoadWorkspaceChats(string workspacePath) + public async Task StoreChatAsync(ChatThread chat, bool reloadTreeItems = false) { - var workspaceChats = new List(); + await WorkspaceBehaviour.StoreChatAsync(chat); - // Enumerate the workspace directory: - foreach (var chatPath in Directory.EnumerateDirectories(workspacePath)) - { - // Read or create the `name` file (self-heal): - var chatNamePath = Path.Join(chatPath, "name"); - string chatName; - try - { - if (!File.Exists(chatNamePath)) - { - chatName = T("Unnamed chat"); - await File.WriteAllTextAsync(chatNamePath, chatName, Encoding.UTF8); - } - else - { - chatName = await File.ReadAllTextAsync(chatNamePath, Encoding.UTF8); - if (string.IsNullOrWhiteSpace(chatName)) - { - chatName = T("Unnamed chat"); - await File.WriteAllTextAsync(chatNamePath, chatName, Encoding.UTF8); - } - } - } - catch - { - chatName = T("Unnamed chat"); - } - - // Read the last change time of the chat: - var chatThreadPath = Path.Join(chatPath, "thread.json"); - var lastEditTime = File.GetLastWriteTimeUtc(chatThreadPath); - - workspaceChats.Add(new TreeItemData - { - Type = TreeItemType.CHAT, - Depth = 2, - Branch = WorkspaceBranch.WORKSPACES, - Text = chatName, - Icon = Icons.Material.Filled.Chat, - Expandable = false, - Path = chatPath, - LastEditTime = lastEditTime, - }); - } + if (reloadTreeItems) + this.loadingWorkspaceChatLists.Clear(); - var result = new List>(workspaceChats.OrderByDescending(n => n.LastEditTime).Select(n => new TreeItemData - { - Expandable = false, - Value = n, - })); - - result.Add(new() - { - Expandable = false, - Value = new TreeButton(WorkspaceBranch.WORKSPACES, 2, T("Add chat"),Icons.Material.Filled.AddComment, () => this.AddChat(workspacePath)), - }); - - return result; + await this.LoadTreeItemsAsync(startPrefetch: false); } - public async Task StoreChat(ChatThread chat, bool reloadTreeItems = true) + private async Task LoadChatAsync(string? chatPath, bool switchToChat) { - await WorkspaceBehaviour.StoreChat(chat); - - // Reload the tree items: - if(reloadTreeItems) - await this.LoadTreeItems(); - - this.StateHasChanged(); - } - - private async Task LoadChat(string? chatPath, bool switchToChat) - { - if(string.IsNullOrWhiteSpace(chatPath)) + if (string.IsNullOrWhiteSpace(chatPath)) return null; - if(!Directory.Exists(chatPath)) + if (!Directory.Exists(chatPath)) return null; - // Check if the chat has unsaved changes: if (switchToChat && await MessageBus.INSTANCE.SendMessageUseFirstResult(this, Event.HAS_CHAT_UNSAVED_CHANGES)) { var dialogParameters = new DialogParameters { { x => x.Message, T("Are you sure you want to load another chat? All unsaved changes will be lost.") }, }; - + var dialogReference = await this.DialogService.ShowAsync(T("Load Chat"), dialogParameters, DialogOptions.FULLSCREEN); var dialogResult = await dialogReference.Result; if (dialogResult is null || dialogResult.Canceled) @@ -344,15 +365,15 @@ public partial class Workspaces : MSGComponentBase return null; } - public async Task DeleteChat(string? chatPath, bool askForConfirmation = true, bool unloadChat = true) + public async Task DeleteChatAsync(string? chatPath, bool askForConfirmation = true, bool unloadChat = true) { - var chat = await this.LoadChat(chatPath, false); + var chat = await this.LoadChatAsync(chatPath, false); if (chat is null) return; if (askForConfirmation) { - var workspaceName = await WorkspaceBehaviour.LoadWorkspaceName(chat.WorkspaceId); + var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(chat.WorkspaceId); var dialogParameters = new DialogParameters { { @@ -370,16 +391,10 @@ public partial class Workspaces : MSGComponentBase return; } - string chatDirectory; - if (chat.WorkspaceId == Guid.Empty) - chatDirectory = Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString()); - else - chatDirectory = Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString()); - - Directory.Delete(chatDirectory, true); - await this.LoadTreeItems(); + await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, chat.WorkspaceId, chat.ChatId, askForConfirmation: false); + await this.LoadTreeItemsAsync(startPrefetch: false); - if(unloadChat && this.CurrentChatThread?.ChatId == chat.ChatId) + if (unloadChat && this.CurrentChatThread?.ChatId == chat.ChatId) { this.CurrentChatThread = null; await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread); @@ -387,9 +402,9 @@ public partial class Workspaces : MSGComponentBase } } - private async Task RenameChat(string? chatPath) + private async Task RenameChatAsync(string? chatPath) { - var chat = await this.LoadChat(chatPath, false); + var chat = await this.LoadChatAsync(chatPath, false); if (chat is null) return; @@ -410,24 +425,24 @@ public partial class Workspaces : MSGComponentBase return; chat.Name = (dialogResult.Data as string)!; - if(this.CurrentChatThread?.ChatId == chat.ChatId) + if (this.CurrentChatThread?.ChatId == chat.ChatId) { this.CurrentChatThread.Name = chat.Name; await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread); await MessageBus.INSTANCE.SendMessage(this, Event.WORKSPACE_LOADED_CHAT_CHANGED); } - await this.StoreChat(chat); - await this.LoadTreeItems(); + await WorkspaceBehaviour.StoreChatAsync(chat); + await this.LoadTreeItemsAsync(startPrefetch: false); } - - private async Task RenameWorkspace(string? workspacePath) + + private async Task RenameWorkspaceAsync(string? workspacePath) { - if(workspacePath is null) + if (workspacePath is null) return; var workspaceId = Guid.Parse(Path.GetFileName(workspacePath)); - var workspaceName = await WorkspaceBehaviour.LoadWorkspaceName(workspaceId); + var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(workspaceId); var dialogParameters = new DialogParameters { { x => x.Message, string.Format(T("Please enter a new or edit the name for your workspace '{0}':"), workspaceName) }, @@ -447,10 +462,11 @@ public partial class Workspaces : MSGComponentBase var alteredWorkspaceName = (dialogResult.Data as string)!; var workspaceNamePath = Path.Join(workspacePath, "name"); await File.WriteAllTextAsync(workspaceNamePath, alteredWorkspaceName, Encoding.UTF8); - await this.LoadTreeItems(); + await WorkspaceBehaviour.UpdateWorkspaceNameInCacheAsync(workspaceId, alteredWorkspaceName); + await this.LoadTreeItemsAsync(startPrefetch: false); } - private async Task AddWorkspace() + private async Task AddWorkspaceAsync() { var dialogParameters = new DialogParameters { @@ -472,23 +488,23 @@ public partial class Workspaces : MSGComponentBase var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString()); Directory.CreateDirectory(workspacePath); + var workspaceName = (dialogResult.Data as string)!; var workspaceNamePath = Path.Join(workspacePath, "name"); - await File.WriteAllTextAsync(workspaceNamePath, (dialogResult.Data as string)!, Encoding.UTF8); + await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8); + await WorkspaceBehaviour.AddWorkspaceToCacheAsync(workspaceId, workspacePath, workspaceName); - await this.LoadTreeItems(); + await this.LoadTreeItemsAsync(startPrefetch: false); } - private async Task DeleteWorkspace(string? workspacePath) + private async Task DeleteWorkspaceAsync(string? workspacePath) { - if(workspacePath is null) + if (workspacePath is null) return; var workspaceId = Guid.Parse(Path.GetFileName(workspacePath)); - var workspaceName = await WorkspaceBehaviour.LoadWorkspaceName(workspaceId); + var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(workspaceId); - // Determine how many chats are in the workspace: var chatCount = Directory.EnumerateDirectories(workspacePath).Count(); - var dialogParameters = new DialogParameters { { x => x.Message, string.Format(T("Are you sure you want to delete the workspace '{0}'? This will also delete {1} chat(s) in this workspace."), workspaceName, chatCount) }, @@ -500,12 +516,13 @@ public partial class Workspaces : MSGComponentBase return; Directory.Delete(workspacePath, true); - await this.LoadTreeItems(); + await WorkspaceBehaviour.RemoveWorkspaceFromCacheAsync(workspaceId); + await this.LoadTreeItemsAsync(startPrefetch: false); } - private async Task MoveChat(string? chatPath) + private async Task MoveChatAsync(string? chatPath) { - var chat = await this.LoadChat(chatPath, false); + var chat = await this.LoadChatAsync(chatPath, false); if (chat is null) return; @@ -525,22 +542,9 @@ public partial class Workspaces : MSGComponentBase if (workspaceId == Guid.Empty) return; - // Delete the chat from the current workspace or the temporary storage: - if (chat.WorkspaceId == Guid.Empty) - { - // Case: The chat is stored in the temporary storage: - await this.DeleteChat(Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString()), askForConfirmation: false, unloadChat: false); - } - else - { - // Case: The chat is stored in a workspace. - await this.DeleteChat(Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString()), askForConfirmation: false, unloadChat: false); - } + await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, chat.WorkspaceId, chat.ChatId, askForConfirmation: false); - // Update the chat's workspace: chat.WorkspaceId = workspaceId; - - // Handle the case where the chat is the active chat: if (this.CurrentChatThread?.ChatId == chat.ChatId) { this.CurrentChatThread = chat; @@ -548,12 +552,12 @@ public partial class Workspaces : MSGComponentBase await MessageBus.INSTANCE.SendMessage(this, Event.WORKSPACE_LOADED_CHAT_CHANGED); } - await this.StoreChat(chat); + await WorkspaceBehaviour.StoreChatAsync(chat); + await this.LoadTreeItemsAsync(startPrefetch: false); } - private async Task AddChat(string workspacePath) + private async Task AddChatAsync(string workspacePath) { - // Check if the chat has unsaved changes: if (await MessageBus.INSTANCE.SendMessageUseFirstResult(this, Event.HAS_CHAT_UNSAVED_CHANGES)) { var dialogParameters = new DialogParameters @@ -579,9 +583,9 @@ public partial class Workspaces : MSGComponentBase var chatPath = Path.Join(workspacePath, chat.ChatId.ToString()); - await this.StoreChat(chat); - await this.LoadChat(chatPath, switchToChat: true); - await this.LoadTreeItems(); + await WorkspaceBehaviour.StoreChatAsync(chat); + await this.LoadChatAsync(chatPath, switchToChat: true); + await this.LoadTreeItemsAsync(startPrefetch: false); } #region Overrides of MSGComponentBase @@ -591,11 +595,20 @@ public partial class Workspaces : MSGComponentBase switch (triggeredEvent) { case Event.PLUGINS_RELOADED: - await this.LoadTreeItems(); - await this.InvokeAsync(this.StateHasChanged); + await this.ForceRefreshFromDiskAsync(); break; } } + protected override void DisposeResources() + { + this.isDisposed = true; + this.prefetchCancellationTokenSource?.Cancel(); + this.prefetchCancellationTokenSource?.Dispose(); + this.prefetchCancellationTokenSource = null; + + base.DisposeResources(); + } + #endregion } \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs b/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs index 9a096e27..ca4b625e 100644 --- a/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs @@ -1,7 +1,4 @@ -using System.Text; - using AIStudio.Components; -using AIStudio.Settings; using Microsoft.AspNetCore.Components; @@ -29,25 +26,10 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase 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))); - } + var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); + foreach (var workspace in snapshot.Workspaces) + this.workspaces[workspace.Name] = workspace.WorkspaceId; this.StateHasChanged(); await base.OnInitializedAsync(); diff --git a/app/MindWork AI Studio/Pages/Chat.razor b/app/MindWork AI Studio/Pages/Chat.razor index 1b2df035..b1b48dc3 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor +++ b/app/MindWork AI Studio/Pages/Chat.razor @@ -48,6 +48,9 @@ + + + @@ -71,6 +74,9 @@ + + + @@ -137,6 +143,9 @@ + + + diff --git a/app/MindWork AI Studio/Pages/Chat.razor.cs b/app/MindWork AI Studio/Pages/Chat.razor.cs index 420828c6..9adef5bb 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor.cs +++ b/app/MindWork AI Studio/Pages/Chat.razor.cs @@ -98,6 +98,14 @@ public partial class Chat : MSGComponentBase await this.DialogService.ShowAsync(T("Open Workspaces Configuration"), dialogParameters, DialogOptions.FULLSCREEN); } + private async Task RefreshWorkspaces() + { + if (this.workspaces is null) + return; + + await this.workspaces.ForceRefreshFromDiskAsync(); + } + #region Overrides of MSGComponentBase protected override void DisposeResources() diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index c518d439..08d78e5a 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -2559,8 +2559,8 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1016188706"] = "Möchten Sie -- Move chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1133040906"] = "Chat verschieben" --- Unnamed workspace -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1307384014"] = "Unbenannter Arbeitsbereich" +-- Loading chats... +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1364857726"] = "Chats werden geladen..." -- Delete UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1469573738"] = "Löschen" @@ -2616,9 +2616,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T323280982"] = "Bitte geben S -- Please enter a workspace name. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3288132732"] = "Bitte geben Sie einen Namen für diesen Arbeitsbereich ein." --- Unnamed chat -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3310482275"] = "Unbenannter Chat" - -- Rename UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3355849203"] = "Umbenennen" @@ -4983,6 +4980,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Lerne jeden Tag ei -- Localization UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Lokalisierung" +-- Reload your workspaces +UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T194629703"] = "Arbeitsbereiche neu laden" + -- Hide your workspaces UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T2351468526"] = "Arbeitsbereiche ausblenden" @@ -6521,3 +6521,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unbenannt -- Delete Chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Chat löschen" + +-- Unnamed chat +UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3310482275"] = "Unbenannter Chat" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index a4fdfd5c..5ab2e446 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -2559,8 +2559,8 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1016188706"] = "Are you sure -- Move chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1133040906"] = "Move chat" --- Unnamed workspace -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1307384014"] = "Unnamed workspace" +-- Loading chats... +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1364857726"] = "Loading chats..." -- Delete UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1469573738"] = "Delete" @@ -2616,9 +2616,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T323280982"] = "Please enter -- Please enter a workspace name. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3288132732"] = "Please enter a workspace name." --- Unnamed chat -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3310482275"] = "Unnamed chat" - -- Rename UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3355849203"] = "Rename" @@ -4983,6 +4980,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Learn about one co -- Localization UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Localization" +-- Reload your workspaces +UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T194629703"] = "Reload your workspaces" + -- Hide your workspaces UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T2351468526"] = "Hide your workspaces" @@ -6521,3 +6521,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unnamed w -- Delete Chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Delete Chat" + +-- Unnamed chat +UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3310482275"] = "Unnamed chat" diff --git a/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs b/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs index 61a6e4c8..90203b2b 100644 --- a/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs +++ b/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs @@ -67,6 +67,7 @@ public sealed class TemporaryChatService(ILogger logger, S { logger.LogInformation($"Deleting temporary chat storage directory '{tempChatDirPath}' due to maintenance policy."); Directory.Delete(tempChatDirPath, true); + WorkspaceBehaviour.InvalidateWorkspaceTreeCache(); } } diff --git a/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs b/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs index 253b4431..c03fccc8 100644 --- a/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs +++ b/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs @@ -12,21 +12,73 @@ namespace AIStudio.Tools; public static class WorkspaceBehaviour { + private sealed class WorkspaceChatCacheEntry + { + public Guid WorkspaceId { get; init; } + + public Guid ChatId { get; init; } + + public string ChatPath { get; init; } = string.Empty; + + public string ChatName { get; set; } = string.Empty; + + public DateTimeOffset LastEditTime { get; set; } + + public bool IsTemporary { get; init; } + } + + private sealed class WorkspaceCacheEntry + { + public Guid WorkspaceId { get; init; } + + public string WorkspacePath { get; init; } = string.Empty; + + public string WorkspaceName { get; set; } = string.Empty; + + public bool ChatsLoaded { get; set; } + + public List Chats { get; set; } = []; + } + + private sealed class WorkspaceTreeCacheState + { + public Dictionary Workspaces { get; } = []; + + public List WorkspaceOrder { get; } = []; + + public List TemporaryChats { get; set; } = []; + + public bool IsShellLoaded { get; set; } + } + private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(WorkspaceBehaviour)); + private static readonly ConcurrentDictionary CHAT_STORAGE_SEMAPHORES = new(); + private static readonly SemaphoreSlim WORKSPACE_TREE_CACHE_SEMAPHORE = new(1, 1); + private static readonly WorkspaceTreeCacheState WORKSPACE_TREE_CACHE = new(); + + private static readonly TimeSpan SEMAPHORE_TIMEOUT = TimeSpan.FromSeconds(6); + private static volatile bool WORKSPACE_TREE_CACHE_INVALIDATED = true; private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(WorkspaceBehaviour).Namespace, nameof(WorkspaceBehaviour)); - /// - /// Semaphores for synchronizing chat storage operations per chat. - /// This prevents race conditions when multiple threads try to write - /// the same chat file simultaneously. - /// - private static readonly ConcurrentDictionary CHAT_STORAGE_SEMAPHORES = new(); + public static readonly JsonSerializerOptions JSON_OPTIONS = new() + { + WriteIndented = true, + AllowTrailingCommas = true, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + Converters = + { + new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper), + } + }; - /// - /// Timeout for acquiring the chat storage semaphore. - /// - private static readonly TimeSpan SEMAPHORE_TIMEOUT = TimeSpan.FromSeconds(6); + private static readonly TimeSpan PREFETCH_DELAY_DURATION = TimeSpan.FromMilliseconds(45); + + private static string WorkspaceRootDirectory => Path.Join(SettingsManager.DataDirectory, "workspaces"); + + private static string TemporaryChatsRootDirectory => Path.Join(SettingsManager.DataDirectory, "tempChats"); private static SemaphoreSlim GetChatSemaphore(Guid workspaceId, Guid chatId) { @@ -34,13 +86,6 @@ public static class WorkspaceBehaviour return CHAT_STORAGE_SEMAPHORES.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); } - /// - /// Tries to acquire the chat storage semaphore within the configured timeout. - /// - /// The workspace ID. - /// The chat ID. - /// The name of the calling method for logging purposes. - /// A tuple containing whether the semaphore was acquired and the semaphore instance. private static async Task<(bool Acquired, SemaphoreSlim Semaphore)> TryAcquireChatSemaphoreAsync(Guid workspaceId, Guid chatId, string callerName) { var semaphore = GetChatSemaphore(workspaceId, chatId); @@ -56,18 +101,357 @@ public static class WorkspaceBehaviour return (acquired, semaphore); } - public static readonly JsonSerializerOptions JSON_OPTIONS = new() + private static WorkspaceTreeChat ToPublicChat(WorkspaceChatCacheEntry chat) => new(chat.WorkspaceId, chat.ChatId, chat.ChatPath, chat.ChatName, chat.LastEditTime, chat.IsTemporary); + + private static WorkspaceTreeWorkspace ToPublicWorkspace(WorkspaceCacheEntry workspace) => new(workspace.WorkspaceId, + workspace.WorkspacePath, + workspace.WorkspaceName, + workspace.ChatsLoaded, + workspace.Chats.Select(ToPublicChat).ToList()); + + private static async Task ReadNameOrDefaultAsync(string nameFilePath, string fallbackName) { - WriteIndented = true, - AllowTrailingCommas = true, - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - Converters = + try { - new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper), + if (!File.Exists(nameFilePath)) + return fallbackName; + + var name = await File.ReadAllTextAsync(nameFilePath, Encoding.UTF8); + return string.IsNullOrWhiteSpace(name) ? fallbackName : name; } - }; + catch + { + return fallbackName; + } + } + + private static async Task> ReadWorkspaceChatsCoreAsync(Guid workspaceId, string workspacePath) + { + var chats = new List(); + if (!Directory.Exists(workspacePath)) + return chats; + + foreach (var chatPath in Directory.EnumerateDirectories(workspacePath)) + { + if (!Guid.TryParse(Path.GetFileName(chatPath), out var chatId)) + continue; + + var chatName = await ReadNameOrDefaultAsync(Path.Join(chatPath, "name"), TB("Unnamed chat")); + var chatThreadPath = Path.Join(chatPath, "thread.json"); + var lastEditTime = File.Exists(chatThreadPath) ? File.GetLastWriteTimeUtc(chatThreadPath) : DateTimeOffset.MinValue; + chats.Add(new WorkspaceChatCacheEntry + { + WorkspaceId = workspaceId, + ChatId = chatId, + ChatPath = chatPath, + ChatName = chatName, + LastEditTime = lastEditTime, + IsTemporary = false, + }); + } + + return chats.OrderByDescending(x => x.LastEditTime).ToList(); + } + + private static async Task> ReadTemporaryChatsCoreAsync() + { + var chats = new List(); + Directory.CreateDirectory(TemporaryChatsRootDirectory); + + foreach (var tempChatPath in Directory.EnumerateDirectories(TemporaryChatsRootDirectory)) + { + if (!Guid.TryParse(Path.GetFileName(tempChatPath), out var chatId)) + continue; + + var chatName = await ReadNameOrDefaultAsync(Path.Join(tempChatPath, "name"), TB("Unnamed chat")); + var chatThreadPath = Path.Join(tempChatPath, "thread.json"); + var lastEditTime = File.Exists(chatThreadPath) ? File.GetLastWriteTimeUtc(chatThreadPath) : DateTimeOffset.MinValue; + chats.Add(new WorkspaceChatCacheEntry + { + WorkspaceId = Guid.Empty, + ChatId = chatId, + ChatPath = tempChatPath, + ChatName = chatName, + LastEditTime = lastEditTime, + IsTemporary = true, + }); + } + + return chats.OrderByDescending(x => x.LastEditTime).ToList(); + } + + private static async Task EnsureTreeShellLoadedCoreAsync() + { + if (!WORKSPACE_TREE_CACHE_INVALIDATED && WORKSPACE_TREE_CACHE.IsShellLoaded) + return; + + WORKSPACE_TREE_CACHE.Workspaces.Clear(); + WORKSPACE_TREE_CACHE.WorkspaceOrder.Clear(); + + Directory.CreateDirectory(WorkspaceRootDirectory); + foreach (var workspacePath in Directory.EnumerateDirectories(WorkspaceRootDirectory)) + { + if (!Guid.TryParse(Path.GetFileName(workspacePath), out var workspaceId)) + continue; + + var workspaceName = await ReadNameOrDefaultAsync(Path.Join(workspacePath, "name"), TB("Unnamed workspace")); + WORKSPACE_TREE_CACHE.Workspaces[workspaceId] = new WorkspaceCacheEntry + { + WorkspaceId = workspaceId, + WorkspacePath = workspacePath, + WorkspaceName = workspaceName, + ChatsLoaded = false, + Chats = [], + }; + + WORKSPACE_TREE_CACHE.WorkspaceOrder.Add(workspaceId); + } + + WORKSPACE_TREE_CACHE.TemporaryChats = await ReadTemporaryChatsCoreAsync(); + WORKSPACE_TREE_CACHE.IsShellLoaded = true; + WORKSPACE_TREE_CACHE_INVALIDATED = false; + } + + private static void UpsertChatInCache(List chats, WorkspaceChatCacheEntry chat) + { + var existingIndex = chats.FindIndex(existing => existing.ChatId == chat.ChatId); + if (existingIndex >= 0) + chats[existingIndex] = chat; + else + chats.Add(chat); + + chats.Sort((a, b) => b.LastEditTime.CompareTo(a.LastEditTime)); + } + + private static void DeleteChatFromCache(List chats, Guid chatId) + { + var existingIndex = chats.FindIndex(existing => existing.ChatId == chatId); + if (existingIndex >= 0) + chats.RemoveAt(existingIndex); + } + + private static async Task UpdateCacheAfterChatStored(Guid workspaceId, Guid chatId, string chatDirectory, string chatName, DateTimeOffset lastEditTime) + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + if (!WORKSPACE_TREE_CACHE.IsShellLoaded || WORKSPACE_TREE_CACHE_INVALIDATED) + return; + + var chatCacheEntry = new WorkspaceChatCacheEntry + { + WorkspaceId = workspaceId, + ChatId = chatId, + ChatPath = chatDirectory, + ChatName = string.IsNullOrWhiteSpace(chatName) ? TB("Unnamed chat") : chatName, + LastEditTime = lastEditTime, + IsTemporary = workspaceId == Guid.Empty, + }; + + if (workspaceId == Guid.Empty) + { + UpsertChatInCache(WORKSPACE_TREE_CACHE.TemporaryChats, chatCacheEntry); + return; + } + + if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace) && workspace.ChatsLoaded) + UpsertChatInCache(workspace.Chats, chatCacheEntry); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + private static async Task UpdateCacheAfterChatDeleted(Guid workspaceId, Guid chatId) + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + if (!WORKSPACE_TREE_CACHE.IsShellLoaded || WORKSPACE_TREE_CACHE_INVALIDATED) + return; + + if (workspaceId == Guid.Empty) + { + DeleteChatFromCache(WORKSPACE_TREE_CACHE.TemporaryChats, chatId); + return; + } + + if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace) && workspace.ChatsLoaded) + DeleteChatFromCache(workspace.Chats, chatId); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + public static void InvalidateWorkspaceTreeCache() + { + WORKSPACE_TREE_CACHE_INVALIDATED = true; + } + + public static async Task ForceReloadWorkspaceTreeAsync() + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + WORKSPACE_TREE_CACHE_INVALIDATED = false; + WORKSPACE_TREE_CACHE.IsShellLoaded = false; + await EnsureTreeShellLoadedCoreAsync(); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + public static async Task GetOrLoadWorkspaceTreeShellAsync() + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + await EnsureTreeShellLoadedCoreAsync(); + var workspaces = WORKSPACE_TREE_CACHE.WorkspaceOrder + .Where(workspaceId => WORKSPACE_TREE_CACHE.Workspaces.ContainsKey(workspaceId)) + .Select(workspaceId => ToPublicWorkspace(WORKSPACE_TREE_CACHE.Workspaces[workspaceId])) + .ToList(); + var temporaryChats = WORKSPACE_TREE_CACHE.TemporaryChats.Select(ToPublicChat).ToList(); + return new WorkspaceTreeCacheSnapshot(workspaces, temporaryChats); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + public static async Task> GetWorkspaceChatsAsync(Guid workspaceId, bool forceRefresh = false) + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + await EnsureTreeShellLoadedCoreAsync(); + if (!WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace)) + return []; + + if (forceRefresh || !workspace.ChatsLoaded) + { + workspace.Chats = await ReadWorkspaceChatsCoreAsync(workspaceId, workspace.WorkspacePath); + workspace.ChatsLoaded = true; + } + + return workspace.Chats.Select(ToPublicChat).ToList(); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + public static async Task TryPrefetchRemainingChatsAsync(Func? onWorkspaceUpdated = null, CancellationToken token = default) + { + while (true) + { + token.ThrowIfCancellationRequested(); + Guid? workspaceToPrefetch = null; + + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(token); + try + { + await EnsureTreeShellLoadedCoreAsync(); + foreach (var workspaceId in WORKSPACE_TREE_CACHE.WorkspaceOrder) + { + if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace) && !workspace.ChatsLoaded) + { + workspaceToPrefetch = workspaceId; + break; + } + } + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + + if (workspaceToPrefetch is null) + return; + + await GetWorkspaceChatsAsync(workspaceToPrefetch.Value); + if (onWorkspaceUpdated is not null) + { + try + { + await onWorkspaceUpdated(workspaceToPrefetch.Value); + } + catch (Exception ex) + { + LOG.LogWarning(ex, "Failed to process callback after prefetching workspace '{WorkspaceId}'.", workspaceToPrefetch.Value); + } + } + + await Task.Delay(PREFETCH_DELAY_DURATION, token); + } + } + + public static async Task AddWorkspaceToCacheAsync(Guid workspaceId, string workspacePath, string workspaceName) + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + await EnsureTreeShellLoadedCoreAsync(); + if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace)) + { + workspace.WorkspaceName = workspaceName; + return; + } + + WORKSPACE_TREE_CACHE.Workspaces[workspaceId] = new WorkspaceCacheEntry + { + WorkspaceId = workspaceId, + WorkspacePath = workspacePath, + WorkspaceName = workspaceName, + Chats = [], + ChatsLoaded = false, + }; + WORKSPACE_TREE_CACHE.WorkspaceOrder.Add(workspaceId); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + public static async Task UpdateWorkspaceNameInCacheAsync(Guid workspaceId, string workspaceName) + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + await EnsureTreeShellLoadedCoreAsync(); + if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace)) + workspace.WorkspaceName = workspaceName; + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + public static async Task RemoveWorkspaceFromCacheAsync(Guid workspaceId) + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + if (!WORKSPACE_TREE_CACHE.IsShellLoaded || WORKSPACE_TREE_CACHE_INVALIDATED) + return; + + WORKSPACE_TREE_CACHE.Workspaces.Remove(workspaceId); + WORKSPACE_TREE_CACHE.WorkspaceOrder.Remove(workspaceId); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } public static bool IsChatExisting(LoadChat loadChat) { @@ -78,31 +462,28 @@ public static class WorkspaceBehaviour return Directory.Exists(chatPath); } - public static async Task StoreChat(ChatThread chat) + public static async Task StoreChatAsync(ChatThread chat) { - // Try to acquire the semaphore for this specific chat to prevent concurrent writes to the same file: - var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(chat.WorkspaceId, chat.ChatId, nameof(StoreChat)); + var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(chat.WorkspaceId, chat.ChatId, nameof(StoreChatAsync)); if (!acquired) return; try { - 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()); + var chatDirectory = chat.WorkspaceId == Guid.Empty + ? Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString()) + : 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); + + var lastEditTime = File.GetLastWriteTimeUtc(chatPath); + await UpdateCacheAfterChatStored(chat.WorkspaceId, chat.ChatId, chatDirectory, chat.Name, lastEditTime); } finally { @@ -110,10 +491,9 @@ public static class WorkspaceBehaviour } } - public static async Task LoadChat(LoadChat loadChat) + public static async Task LoadChatAsync(LoadChat loadChat) { - // Try to acquire the semaphore for this specific chat to prevent concurrent read/writes to the same file: - var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(loadChat.WorkspaceId, loadChat.ChatId, nameof(LoadChat)); + var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(loadChat.WorkspaceId, loadChat.ChatId, nameof(LoadChatAsync)); if (!acquired) return null; @@ -123,12 +503,11 @@ public static class WorkspaceBehaviour ? Path.Join(SettingsManager.DataDirectory, "tempChats", loadChat.ChatId.ToString()) : Path.Join(SettingsManager.DataDirectory, "workspaces", loadChat.WorkspaceId.ToString(), loadChat.ChatId.ToString()); - if(!Directory.Exists(chatPath)) + if (!Directory.Exists(chatPath)) return null; var chatData = await File.ReadAllTextAsync(Path.Join(chatPath, "thread.json"), Encoding.UTF8); - var chat = JsonSerializer.Deserialize(chatData, JSON_OPTIONS); - return chat; + return JsonSerializer.Deserialize(chatData, JSON_OPTIONS); } catch (Exception) { @@ -140,51 +519,68 @@ public static class WorkspaceBehaviour } } - public static async Task LoadWorkspaceName(Guid workspaceId) + public static async Task LoadWorkspaceNameAsync(Guid workspaceId) { - if(workspaceId == Guid.Empty) + if (workspaceId == Guid.Empty) return string.Empty; - - var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString()); - var workspaceNamePath = Path.Join(workspacePath, "name"); - + + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); try { - // If the name file does not exist or is empty, self-heal with a default name. - if (!File.Exists(workspaceNamePath)) + await EnsureTreeShellLoadedCoreAsync(); + if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var cachedWorkspace) && !string.IsNullOrWhiteSpace(cachedWorkspace.WorkspaceName)) + return cachedWorkspace.WorkspaceName; + + // Not in cache — read from disk and update cache in the same semaphore scope + // to avoid a second semaphore acquisition via UpdateWorkspaceNameInCacheAsync: + var workspacePath = Path.Join(WorkspaceRootDirectory, workspaceId.ToString()); + var workspaceNamePath = Path.Join(workspacePath, "name"); + string workspaceName; + + try { - var defaultName = TB("Unnamed workspace"); - Directory.CreateDirectory(workspacePath); - await File.WriteAllTextAsync(workspaceNamePath, defaultName, Encoding.UTF8); - return defaultName; + if (!File.Exists(workspaceNamePath)) + { + workspaceName = TB("Unnamed workspace"); + Directory.CreateDirectory(workspacePath); + await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8); + } + else + { + workspaceName = await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8); + if (string.IsNullOrWhiteSpace(workspaceName)) + { + workspaceName = TB("Unnamed workspace"); + await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8); + } + } } - - var name = await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8); - if (string.IsNullOrWhiteSpace(name)) + catch { - var defaultName = TB("Unnamed workspace"); - await File.WriteAllTextAsync(workspaceNamePath, defaultName, Encoding.UTF8); - return defaultName; + workspaceName = TB("Unnamed workspace"); } - - return name; + + // Update the cache directly (we already hold the semaphore): + if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace)) + workspace.WorkspaceName = workspaceName; + + return workspaceName; } - catch + finally { - // On any error, return a localized default without throwing. - return TB("Unnamed workspace"); + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); } } - public static async Task DeleteChat(IDialogService dialogService, Guid workspaceId, Guid chatId, bool askForConfirmation = true) + public static async Task DeleteChatAsync(IDialogService dialogService, Guid workspaceId, Guid chatId, bool askForConfirmation = true) { - var chat = await LoadChat(new(workspaceId, chatId)); + var chat = await LoadChatAsync(new(workspaceId, chatId)); if (chat is null) return; if (askForConfirmation) { - var workspaceName = await LoadWorkspaceName(chat.WorkspaceId); + var workspaceName = await LoadWorkspaceNameAsync(chat.WorkspaceId); var dialogParameters = new DialogParameters { { @@ -202,20 +598,20 @@ public static class WorkspaceBehaviour 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()); + var chatDirectory = chat.WorkspaceId == Guid.Empty + ? Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString()) + : Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString()); - // Try to acquire the semaphore to prevent deleting while another thread is writing: - var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(workspaceId, chatId, nameof(DeleteChat)); + var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(workspaceId, chatId, nameof(DeleteChatAsync)); if (!acquired) return; try { - Directory.Delete(chatDirectory, true); + if (Directory.Exists(chatDirectory)) + Directory.Delete(chatDirectory, true); + + await UpdateCacheAfterChatDeleted(workspaceId, chatId); } finally { @@ -225,16 +621,14 @@ public static class WorkspaceBehaviour private static async Task EnsureWorkspace(Guid workspaceId, string workspaceName) { - var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString()); + var workspacePath = Path.Join(WorkspaceRootDirectory, workspaceId.ToString()); var workspaceNamePath = Path.Join(workspacePath, "name"); - if(!Path.Exists(workspacePath)) + if (!Path.Exists(workspacePath)) Directory.CreateDirectory(workspacePath); try { - // When the name file is missing or empty, write it (self-heal). - // Otherwise, keep the existing name: if (!File.Exists(workspaceNamePath)) { await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8); @@ -250,9 +644,11 @@ public static class WorkspaceBehaviour { // Ignore IO issues to avoid interrupting background initialization. } + + await AddWorkspaceToCacheAsync(workspaceId, workspacePath, workspaceName); } - + public static async Task EnsureBiasWorkspace() => await EnsureWorkspace(KnownWorkspaces.BIAS_WORKSPACE_ID, "Bias of the Day"); - + public static async Task EnsureERIServerWorkspace() => await EnsureWorkspace(KnownWorkspaces.ERI_SERVER_WORKSPACE_ID, "ERI Servers"); -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Tools/WorkspaceTreeCacheSnapshot.cs b/app/MindWork AI Studio/Tools/WorkspaceTreeCacheSnapshot.cs new file mode 100644 index 00000000..8e36f5d5 --- /dev/null +++ b/app/MindWork AI Studio/Tools/WorkspaceTreeCacheSnapshot.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools; + +public readonly record struct WorkspaceTreeCacheSnapshot(IReadOnlyList Workspaces, IReadOnlyList TemporaryChats); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/WorkspaceTreeChat.cs b/app/MindWork AI Studio/Tools/WorkspaceTreeChat.cs new file mode 100644 index 00000000..3976667d --- /dev/null +++ b/app/MindWork AI Studio/Tools/WorkspaceTreeChat.cs @@ -0,0 +1,4 @@ +// ReSharper disable NotAccessedPositionalProperty.Global +namespace AIStudio.Tools; + +public readonly record struct WorkspaceTreeChat(Guid WorkspaceId, Guid ChatId, string ChatPath, string Name, DateTimeOffset LastEditTime, bool IsTemporary); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/WorkspaceTreeWorkspace.cs b/app/MindWork AI Studio/Tools/WorkspaceTreeWorkspace.cs new file mode 100644 index 00000000..d8eed9bb --- /dev/null +++ b/app/MindWork AI Studio/Tools/WorkspaceTreeWorkspace.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools; + +public readonly record struct WorkspaceTreeWorkspace(Guid WorkspaceId, string WorkspacePath, string Name, bool ChatsLoaded, IReadOnlyList Chats); \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index f5bd763b..18ba3eab 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -1,6 +1,7 @@ # v26.3.1, build 235 (2026-03-xx xx:xx UTC) - Improved the performance by caching the OS language detection and requesting the user language only once per app start. - Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations. +- Improved the workspace loading experience: when opening the chat for the first time, your workspaces now appear faster and load step by step in the background, with placeholder rows so the app feels responsive right away. - Improved the user-language logging by limiting language detection logs to a single entry per app start. - Improved the logbook readability by removing non-readable special characters from log entries. - Improved the logbook reliability by significantly reducing duplicate log entries. From 7e5e078cdb5203b01b49125feb9ed41e28ce001b Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 6 Mar 2026 19:45:46 +0100 Subject: [PATCH 02/27] Upgraded to Rust v1.94.0 (#682) --- app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md | 3 ++- metadata.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 18ba3eab..2dd6c538 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -5,4 +5,5 @@ - Improved the user-language logging by limiting language detection logs to a single entry per app start. - Improved the logbook readability by removing non-readable special characters from log entries. - Improved the logbook reliability by significantly reducing duplicate log entries. -- Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks Inga for reporting this issue and providing some context on how to reproduce it. \ No newline at end of file +- Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks Inga for reporting this issue and providing some context on how to reproduce it. +- Upgraded to Rust v1.94.0 \ No newline at end of file diff --git a/metadata.txt b/metadata.txt index 77e953d5..6660d464 100644 --- a/metadata.txt +++ b/metadata.txt @@ -3,7 +3,7 @@ 234 9.0.114 (commit 4c5aac3d56) 9.0.13 (commit 9ecbfd4f3f) -1.93.1 (commit 01f6ddf75) +1.94.0 (commit 4a4ef493e) 8.15.0 1.8.1 3eb367d4c9e, release From 0c702c579b14051b09f4770f74159eb207f1640d Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 10 Mar 2026 20:50:45 +0100 Subject: [PATCH 03/27] Improved the shortcut handling (#684) --- .../Layout/MainLayout.razor.cs | 2 +- .../Settings/SettingsManager.cs | 35 +++-- app/MindWork AI Studio/Tools/Event.cs | 1 + .../Tools/Services/GlobalShortcutService.cs | 140 ++++++++++++++---- .../wwwroot/changelog/v26.3.1.md | 1 + runtime/Cargo.lock | 53 +++---- runtime/Cargo.toml | 4 +- 7 files changed, 156 insertions(+), 80 deletions(-) diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index 07dfebd2..0fc41f7c 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -97,7 +97,6 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan // Set the snackbar for the update service: UpdateService.SetBlazorDependencies(this.Snackbar); - GlobalShortcutService.Initialize(); TemporaryChatService.Initialize(); // Should the navigation bar be open by default? @@ -251,6 +250,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan // Set up hot reloading for plugins: PluginFactory.SetUpHotReloading(); + await this.MessageBus.SendMessage(this, Event.STARTUP_COMPLETED); } }); break; diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index d4bfc7e3..12634ad6 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -63,18 +63,29 @@ public sealed class SettingsManager /// Loads the settings from the file system. /// public async Task LoadSettings() + { + var settingsSnapshot = await this.TryReadSettingsSnapshot(); + if (settingsSnapshot is not null) + this.ConfigurationData = settingsSnapshot; + } + + /// + /// Reads the settings from disk without mutating the current in-memory state. + /// + /// A (migrated) settings snapshot, or null if it could not be read. + public async Task TryReadSettingsSnapshot() { if(!this.IsSetUp) { this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet."); - return; + return null; } var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME); if(!File.Exists(settingsPath)) { this.logger.LogWarning("Cannot load settings, because the settings file does not exist."); - return; + return null; } // We read the `"Version": "V3"` line to determine the version of the settings file: @@ -87,30 +98,28 @@ public sealed class SettingsManager // Extract the version from the line: var settingsVersionText = line.Split('"')[3]; - + // Parse the version: Enum.TryParse(settingsVersionText, out Version settingsVersion); if(settingsVersion is Version.UNKNOWN) { this.logger.LogError("Unknown version of the settings file found."); - this.ConfigurationData = new(); - return; + return new(); } - - this.ConfigurationData = SettingsMigrations.Migrate(this.logger, settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS); - + + var settingsData = SettingsMigrations.Migrate(this.logger, settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS); + // // We filter the enabled preview features based on the preview visibility. // This is necessary when the app starts up: some preview features may have // been disabled or released from the last time the app was started. // - this.ConfigurationData.App.EnabledPreviewFeatures = this.ConfigurationData.App.PreviewVisibility.FilterPreviewFeatures(this.ConfigurationData.App.EnabledPreviewFeatures); - - return; + settingsData.App.EnabledPreviewFeatures = settingsData.App.PreviewVisibility.FilterPreviewFeatures(settingsData.App.EnabledPreviewFeatures); + return settingsData; } - + this.logger.LogError("Failed to read the version of the settings file."); - this.ConfigurationData = new(); + return new(); } /// diff --git a/app/MindWork AI Studio/Tools/Event.cs b/app/MindWork AI Studio/Tools/Event.cs index b3d3628f..aac82108 100644 --- a/app/MindWork AI Studio/Tools/Event.cs +++ b/app/MindWork AI Studio/Tools/Event.cs @@ -9,6 +9,7 @@ public enum Event CONFIGURATION_CHANGED, COLOR_THEME_CHANGED, STARTUP_PLUGIN_SYSTEM, + STARTUP_COMPLETED, STARTUP_ENTERPRISE_ENVIRONMENT, PLUGINS_RELOADED, SHOW_ERROR, diff --git a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs index 4515b78b..f0bca207 100644 --- a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs +++ b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs @@ -8,8 +8,16 @@ namespace AIStudio.Tools.Services; public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiver { - private static bool IS_INITIALIZED; + private static bool IS_STARTUP_COMPLETED; + private enum ShortcutSyncSource + { + CONFIGURATION_CHANGED, + STARTUP_COMPLETED, + PLUGINS_RELOADED, + } + + private readonly SemaphoreSlim registrationSemaphore = new(1, 1); private readonly ILogger logger; private readonly SettingsManager settingsManager; private readonly MessageBus messageBus; @@ -27,22 +35,19 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv this.rustService = rustService; this.messageBus.RegisterComponent(this); - this.ApplyFilters([], [Event.CONFIGURATION_CHANGED]); + this.ApplyFilters([], [Event.CONFIGURATION_CHANGED, Event.PLUGINS_RELOADED, Event.STARTUP_COMPLETED]); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - // Wait until the app is fully initialized: - while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED) - await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); - - // Register shortcuts on startup: - await this.RegisterAllShortcuts(); + this.logger.LogInformation("The global shortcut service was initialized."); + await Task.Delay(Timeout.InfiniteTimeSpan, stoppingToken); } public override async Task StopAsync(CancellationToken cancellationToken) { this.messageBus.Unregister(this); + this.registrationSemaphore.Dispose(); await base.StopAsync(cancellationToken); } @@ -53,7 +58,22 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv switch (triggeredEvent) { case Event.CONFIGURATION_CHANGED: - await this.RegisterAllShortcuts(); + if (!IS_STARTUP_COMPLETED) + return; + + await this.RegisterAllShortcuts(ShortcutSyncSource.CONFIGURATION_CHANGED); + break; + + case Event.STARTUP_COMPLETED: + IS_STARTUP_COMPLETED = true; + await this.RegisterAllShortcuts(ShortcutSyncSource.STARTUP_COMPLETED); + break; + + case Event.PLUGINS_RELOADED: + if (!IS_STARTUP_COMPLETED) + return; + + await this.RegisterAllShortcuts(ShortcutSyncSource.PLUGINS_RELOADED); break; } } @@ -62,33 +82,64 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv #endregion - private async Task RegisterAllShortcuts() + private async Task RegisterAllShortcuts(ShortcutSyncSource source) { - this.logger.LogInformation("Registering global shortcuts."); - foreach (var shortcutId in Enum.GetValues()) + await this.registrationSemaphore.WaitAsync(); + try { - if(shortcutId is Shortcut.NONE) - continue; - - var shortcut = this.GetShortcutValue(shortcutId); - var isEnabled = this.IsShortcutAllowed(shortcutId); - - if (isEnabled && !string.IsNullOrWhiteSpace(shortcut)) + this.logger.LogInformation("Registering global shortcuts (source='{Source}').", source); + foreach (var shortcutId in Enum.GetValues()) { - var success = await this.rustService.UpdateGlobalShortcut(shortcutId, shortcut); - if (success) - this.logger.LogInformation("Global shortcut '{ShortcutId}' ({Shortcut}) registered.", shortcutId, shortcut); + if(shortcutId is Shortcut.NONE) + continue; + + var shortcutState = await this.GetShortcutState(shortcutId, source); + var shortcut = shortcutState.Shortcut; + var isEnabled = shortcutState.IsEnabled; + this.logger.LogInformation( + "Sync shortcut '{ShortcutId}' (source='{Source}', enabled={IsEnabled}, configured='{Shortcut}').", + shortcutId, + source, + isEnabled, + shortcut); + + if (shortcutState.UsesPersistedFallback) + { + this.logger.LogWarning( + "Using persisted shortcut fallback for '{ShortcutId}' during startup completion (source='{Source}', configured='{Shortcut}').", + shortcutId, + source, + shortcut); + } + + if (isEnabled && !string.IsNullOrWhiteSpace(shortcut)) + { + var success = await this.rustService.UpdateGlobalShortcut(shortcutId, shortcut); + if (success) + this.logger.LogInformation("Global shortcut '{ShortcutId}' ({Shortcut}) registered.", shortcutId, shortcut); + else + this.logger.LogWarning("Failed to register global shortcut '{ShortcutId}' ({Shortcut}).", shortcutId, shortcut); + } else - this.logger.LogWarning("Failed to register global shortcut '{ShortcutId}' ({Shortcut}).", shortcutId, shortcut); - } - else - { - // Disable the shortcut when empty or feature is disabled: - await this.rustService.UpdateGlobalShortcut(shortcutId, string.Empty); - } - } + { + this.logger.LogInformation( + "Disabling global shortcut '{ShortcutId}' (source='{Source}', enabled={IsEnabled}, configured='{Shortcut}').", + shortcutId, + source, + isEnabled, + shortcut); - this.logger.LogInformation("Global shortcuts registration completed."); + // Disable the shortcut when empty or feature is disabled: + await this.rustService.UpdateGlobalShortcut(shortcutId, string.Empty); + } + } + + this.logger.LogInformation("Global shortcuts registration completed (source='{Source}').", source); + } + finally + { + this.registrationSemaphore.Release(); + } } private string GetShortcutValue(Shortcut name) => name switch @@ -107,5 +158,30 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv _ => true, }; - public static void Initialize() => IS_INITIALIZED = true; + private async Task GetShortcutState(Shortcut shortcutId, ShortcutSyncSource source) + { + var shortcut = this.GetShortcutValue(shortcutId); + var isEnabled = this.IsShortcutAllowed(shortcutId); + if (isEnabled && !string.IsNullOrWhiteSpace(shortcut)) + return new(shortcut, true, false); + + if (source is not ShortcutSyncSource.STARTUP_COMPLETED || shortcutId is not Shortcut.VOICE_RECORDING_TOGGLE) + return new(shortcut, isEnabled, false); + + var settingsSnapshot = await this.settingsManager.TryReadSettingsSnapshot(); + if (settingsSnapshot is null) + return new(shortcut, isEnabled, false); + + var fallbackShortcut = settingsSnapshot.App.ShortcutVoiceRecording; + var fallbackEnabled = + settingsSnapshot.App.EnabledPreviewFeatures.Contains(PreviewFeatures.PRE_SPEECH_TO_TEXT_2026) && + !string.IsNullOrWhiteSpace(settingsSnapshot.App.UseTranscriptionProvider); + + if (!fallbackEnabled || string.IsNullOrWhiteSpace(fallbackShortcut)) + return new(shortcut, isEnabled, false); + + return new(fallbackShortcut, true, true); + } + + private readonly record struct ShortcutState(string Shortcut, bool IsEnabled, bool UsesPersistedFallback); } diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 2dd6c538..25d39938 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -2,6 +2,7 @@ - Improved the performance by caching the OS language detection and requesting the user language only once per app start. - Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations. - Improved the workspace loading experience: when opening the chat for the first time, your workspaces now appear faster and load step by step in the background, with placeholder rows so the app feels responsive right away. +- Improved the reliability of the global voice recording shortcut so it stays available more consistently. - Improved the user-language logging by limiting language detection logs to a single entry per app start. - Improved the logbook readability by removing non-readable special characters from log entries. - Improved the logbook reliability by significantly reducing duplicate log entries. diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index 407a5627..c0161bfc 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -354,9 +354,9 @@ dependencies = [ [[package]] name = "brotli" -version = "6.0.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -2115,9 +2115,9 @@ dependencies = [ [[package]] name = "ico" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" dependencies = [ "byteorder", "png", @@ -2517,17 +2517,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json-patch" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9ad60d674508f3ca8f380a928cfe7b096bc729c4e2dbfe3852bc45da3ab30b" -dependencies = [ - "serde", - "serde_json", - "thiserror 1.0.63", -] - [[package]] name = "json-patch" version = "2.0.0" @@ -5095,9 +5084,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "1.8.1" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf327e247698d3f39af8aa99401c9708384290d1f5c544bf5d251d44c2fea22" +checksum = "3ae1f57c291a6ab8e1d2e6b8ad0a35ff769c9925deb8a89de85425ff08762d0c" dependencies = [ "anyhow", "base64 0.22.1", @@ -5157,15 +5146,15 @@ dependencies = [ [[package]] name = "tauri-build" -version = "1.5.3" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c6ec7a5c3296330c7818478948b422967ce4649094696c985f61d50076d29c" +checksum = "2db08694eec06f53625cfc6fff3a363e084e5e9a238166d2989996413c346453" dependencies = [ "anyhow", "cargo_toml", "dirs-next", "heck 0.5.0", - "json-patch 1.4.0", + "json-patch", "semver", "serde", "serde_json", @@ -5176,14 +5165,14 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93a9e3f5cebf779a63bf24903e714ec91196c307d8249a0008b882424328bcda" +checksum = "53438d78c4a037ffe5eafa19e447eea599bedfb10844cb08ec53c2471ac3ac3f" dependencies = [ "base64 0.21.7", "brotli", "ico", - "json-patch 2.0.0", + "json-patch", "plist", "png", "proc-macro2", @@ -5202,9 +5191,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1d0e989f54fe06c5ef0875c5e19cf96453d099a0a774d5192ab47e80471cdab" +checksum = "233988ac08c1ed3fe794cd65528d48d8f7ed4ab3895ca64cdaa6ad4d00c45c0b" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -5230,9 +5219,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33fda7d213e239077fad52e96c6b734cecedb30c2382118b64f94cb5103ff3a" +checksum = "8066855882f00172935e3fa7d945126580c34dcbabab43f5d4f0c2398a67d47b" dependencies = [ "gtk", "http 0.2.12", @@ -5251,9 +5240,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "0.14.10" +version = "0.14.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18c447dcd9b0f09c7dc4b752cc33e72788805bfd761fbda5692d30c48289efec" +checksum = "ce361fec1e186705371f1c64ae9dd2a3a6768bc530d0a2d5e75a634bb416ad4d" dependencies = [ "cocoa", "gtk", @@ -5271,9 +5260,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0c939e88d82903a0a7dfb28388b12a3c03504d6bd6086550edaa3b6d8beaa" +checksum = "c357952645e679de02cd35007190fcbce869b93ffc61b029f33fe02648453774" dependencies = [ "brotli", "ctor", @@ -5282,7 +5271,7 @@ dependencies = [ "heck 0.5.0", "html5ever", "infer", - "json-patch 2.0.0", + "json-patch", "kuchikiki", "log", "memchr", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b3c1b32e..6a71f79c 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -6,10 +6,10 @@ description = "MindWork AI Studio" authors = ["Thorsten Sommer"] [build-dependencies] -tauri-build = { version = "1.5", features = [] } +tauri-build = { version = "1.5.6", features = [] } [dependencies] -tauri = { version = "1.8", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog", "global-shortcut"] } +tauri = { version = "1.8.3", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog", "global-shortcut"] } tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" From 1c52d6f199b7feb8aebf7fb706eb4b3da0ef2a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peer=20Sch=C3=BCtt?= Date: Wed, 11 Mar 2026 09:59:00 +0100 Subject: [PATCH 04/27] Added Qwen 3.5 model capabilities (#685) --- .../Settings/ProviderExtensions.Alibaba.cs | 11 +++++++++++ .../Settings/ProviderExtensions.OpenSource.cs | 11 +++++++++++ app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md | 3 ++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.Alibaba.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.Alibaba.cs index c57aeeed..2a38c9fb 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.Alibaba.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.Alibaba.cs @@ -24,6 +24,17 @@ public static partial class ProviderExtensions Capability.CHAT_COMPLETION_API, ]; + // Check for Qwen 3.5: + if(modelName.StartsWith("qwen3.5")) + return + [ + Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, + Capability.TEXT_OUTPUT, + + Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + // Check for Qwen 3: if(modelName.StartsWith("qwen3")) return diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs index fefa87dc..dc30e53b 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs @@ -102,6 +102,17 @@ public static partial class ProviderExtensions Capability.CHAT_COMPLETION_API, ]; + // Check for Qwen 3.5: + if(modelName.IndexOf("qwen3.5") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, + Capability.TEXT_OUTPUT, + + Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + if(modelName.IndexOf("-vl-") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 25d39938..bea66b76 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -7,4 +7,5 @@ - Improved the logbook readability by removing non-readable special characters from log entries. - Improved the logbook reliability by significantly reducing duplicate log entries. - Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks Inga for reporting this issue and providing some context on how to reproduce it. -- Upgraded to Rust v1.94.0 \ No newline at end of file +- Upgraded to Rust v1.94.0 +- Added the model capabilities of the new Qwen3.5 model family. \ No newline at end of file From 8e2f6f6b6e2990e2f1716a888fa2ca589e1cfed4 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 12 Mar 2026 10:20:28 +0100 Subject: [PATCH 05/27] Downgraded to Rust v1.93.1 (#688) --- app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md | 5 ++--- metadata.txt | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index bea66b76..3cbd4960 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -1,4 +1,5 @@ # v26.3.1, build 235 (2026-03-xx xx:xx UTC) +- Added support for the new Qwen 3.5 model family. - Improved the performance by caching the OS language detection and requesting the user language only once per app start. - Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations. - Improved the workspace loading experience: when opening the chat for the first time, your workspaces now appear faster and load step by step in the background, with placeholder rows so the app feels responsive right away. @@ -6,6 +7,4 @@ - Improved the user-language logging by limiting language detection logs to a single entry per app start. - Improved the logbook readability by removing non-readable special characters from log entries. - Improved the logbook reliability by significantly reducing duplicate log entries. -- Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks Inga for reporting this issue and providing some context on how to reproduce it. -- Upgraded to Rust v1.94.0 -- Added the model capabilities of the new Qwen3.5 model family. \ No newline at end of file +- Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks Inga for reporting this issue and providing some context on how to reproduce it. \ No newline at end of file diff --git a/metadata.txt b/metadata.txt index 6660d464..77e953d5 100644 --- a/metadata.txt +++ b/metadata.txt @@ -3,7 +3,7 @@ 234 9.0.114 (commit 4c5aac3d56) 9.0.13 (commit 9ecbfd4f3f) -1.94.0 (commit 4a4ef493e) +1.93.1 (commit 01f6ddf75) 8.15.0 1.8.1 3eb367d4c9e, release From 3ea3f20c5b75c57ebeae10678e171167b1760ca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peer=20Sch=C3=BCtt?= Date: Thu, 12 Mar 2026 11:44:53 +0100 Subject: [PATCH 06/27] Improved Provider Lua Export (#687) --- app/MindWork AI Studio/Settings/Provider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Settings/Provider.cs b/app/MindWork AI Studio/Settings/Provider.cs index a2a0a0d3..0ccf272c 100644 --- a/app/MindWork AI Studio/Settings/Provider.cs +++ b/app/MindWork AI Studio/Settings/Provider.cs @@ -253,7 +253,7 @@ public sealed record Provider( ["AdditionalJsonApiParameters"] = "{{LuaTools.EscapeLuaString(this.AdditionalJsonApiParameters)}}", ["Model"] = { ["Id"] = "{{LuaTools.EscapeLuaString(this.Model.Id)}}", - ["DisplayName"] = "{{LuaTools.EscapeLuaString(this.Model.DisplayName ?? string.Empty)}}", + ["DisplayName"] = "{{LuaTools.EscapeLuaString(this.Model.DisplayName ?? this.Model.Id)}}", }, } """; From c1205022153621dea2c9ede28f8882510c1d98bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peer=20Sch=C3=BCtt?= Date: Thu, 12 Mar 2026 12:11:54 +0100 Subject: [PATCH 07/27] Improved additional API parameter validation (#686) Co-authored-by: Thorsten Sommer --- .../Assistants/I18N/allTexts.lua | 9 + .../Dialogs/ProviderDialog.razor | 4 +- .../Dialogs/ProviderDialog.razor.cs | 168 +++++++++++++++++- .../plugin.lua | 9 + .../plugin.lua | 9 + .../Provider/Anthropic/ProviderAnthropic.cs | 7 +- .../Provider/BaseProvider.cs | 104 ++++++++++- .../Provider/Mistral/ChatRequest.cs | 5 +- .../Provider/Mistral/ProviderMistral.cs | 7 +- .../wwwroot/changelog/v26.3.1.md | 1 + 10 files changed, 305 insertions(+), 18 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index eeb90c5b..d3a4c32b 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -3532,6 +3532,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1870831108"] = "Failed to l -- Please enter a model name. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1936099896"] = "Please enter a model name." +-- Additional API parameters must form a JSON object. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2051143391"] = "Additional API parameters must form a JSON object." + -- Model UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2189814010"] = "Model" @@ -3553,12 +3556,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2842060373"] = "Instance Na -- Show Expert Settings UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3361153305"] = "Show Expert Settings" +-- Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3502745319"] = "Invalid JSON: Add the parameters in proper JSON formatting, e.g., \\\"temperature\\\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though." + -- Show available models UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3763891899"] = "Show available models" -- This host uses the model configured at the provider level. No model selection is available. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3783329915"] = "This host uses the model configured at the provider level. No model selection is available." +-- Duplicate key '{0}' found. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3804472591"] = "Duplicate key '{0}' found." + -- Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T4116737656"] = "Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually." diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor index 96e94a2f..4c09da2f 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor @@ -160,7 +160,7 @@ @T("Please be aware: This section is for experts only. You are responsible for verifying the correctness of the additional parameters you provide to the API call. By default, AI Studio uses the OpenAI-compatible chat completions API, when that it is supported by the underlying service and model.") - + @@ -181,4 +181,4 @@ } - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index 216f49ee..9e84bea8 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -1,3 +1,6 @@ +using System.Text; +using System.Text.Json; + using AIStudio.Components; using AIStudio.Provider; using AIStudio.Provider.HuggingFace; @@ -334,7 +337,168 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId private void OnInputChangeExpertSettings() { - this.AdditionalJsonApiParameters = this.AdditionalJsonApiParameters.Trim().TrimEnd(',', ' '); + this.AdditionalJsonApiParameters = NormalizeAdditionalJsonApiParameters(this.AdditionalJsonApiParameters) + .Trim() + .TrimEnd(',', ' '); + } + + private string? ValidateAdditionalJsonApiParameters(string additionalParams) + { + if (string.IsNullOrWhiteSpace(additionalParams)) + return null; + + var normalized = NormalizeAdditionalJsonApiParameters(additionalParams); + if (!string.Equals(normalized, additionalParams, StringComparison.Ordinal)) + this.AdditionalJsonApiParameters = normalized; + + var json = $"{{{normalized}}}"; + try + { + if (!this.TryValidateJsonObjectWithDuplicateCheck(json, out var errorMessage)) + return errorMessage; + + return null; + } + catch (JsonException) + { + return T("Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though."); + } + } + + private static string NormalizeAdditionalJsonApiParameters(string input) + { + var sb = new StringBuilder(input.Length); + var inString = false; + var escape = false; + for (var i = 0; i < input.Length; i++) + { + var c = input[i]; + if (inString) + { + sb.Append(c); + if (escape) + { + escape = false; + continue; + } + + if (c == '\\') + { + escape = true; + continue; + } + + if (c == '"') + inString = false; + + continue; + } + + if (c == '"') + { + inString = true; + sb.Append(c); + continue; + } + + if (TryReadToken(input, i, "True", out var tokenLength)) + { + sb.Append("true"); + i += tokenLength - 1; + continue; + } + + if (TryReadToken(input, i, "False", out tokenLength)) + { + sb.Append("false"); + i += tokenLength - 1; + continue; + } + + if (TryReadToken(input, i, "Null", out tokenLength)) + { + sb.Append("null"); + i += tokenLength - 1; + continue; + } + + sb.Append(c); + } + + return sb.ToString(); + } + + private static bool TryReadToken(string input, int startIndex, string token, out int tokenLength) + { + tokenLength = 0; + if (startIndex + token.Length > input.Length) + return false; + + if (!input.AsSpan(startIndex, token.Length).SequenceEqual(token)) + return false; + + var beforeIndex = startIndex - 1; + if (beforeIndex >= 0 && IsIdentifierChar(input[beforeIndex])) + return false; + + var afterIndex = startIndex + token.Length; + if (afterIndex < input.Length && IsIdentifierChar(input[afterIndex])) + return false; + + tokenLength = token.Length; + return true; + } + + private static bool IsIdentifierChar(char c) => char.IsLetterOrDigit(c) || c == '_'; + + private bool TryValidateJsonObjectWithDuplicateCheck(string json, out string? errorMessage) + { + errorMessage = null; + var bytes = Encoding.UTF8.GetBytes(json); + var reader = new Utf8JsonReader(bytes, new JsonReaderOptions + { + AllowTrailingCommas = false, + CommentHandling = JsonCommentHandling.Disallow + }); + + var objectStack = new Stack>(); + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.StartObject: + objectStack.Push(new HashSet(StringComparer.Ordinal)); + break; + + case JsonTokenType.EndObject: + if (objectStack.Count > 0) + objectStack.Pop(); + break; + + case JsonTokenType.PropertyName: + if (objectStack.Count == 0) + { + errorMessage = T("Additional API parameters must form a JSON object."); + return false; + } + + var name = reader.GetString() ?? string.Empty; + if (!objectStack.Peek().Add(name)) + { + errorMessage = string.Format(T("Duplicate key '{0}' found."), name); + return false; + } + break; + } + } + + if (objectStack.Count != 0) + { + errorMessage = T("Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though."); + return false; + } + + return true; } private string GetExpertStyles => this.showExpertSettings ? "border-2 border-dashed rounded pa-2" : string.Empty; @@ -345,4 +509,4 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId "top_p": 0.9, "frequency_penalty": 0.0 """; -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 08d78e5a..339d8507 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -3534,6 +3534,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1870831108"] = "Der API-Sch -- Please enter a model name. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1936099896"] = "Bitte geben Sie einen Modellnamen ein." +-- Additional API parameters must form a JSON object. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2051143391"] = "Zusätzliche API-Parameter müssen ein JSON-Objekt bilden." + -- Model UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2189814010"] = "Modell" @@ -3555,12 +3558,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2842060373"] = "Instanzname -- Show Expert Settings UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3361153305"] = "Experten-Einstellungen anzeigen" +-- Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3502745319"] = "Ungültiges JSON: Fügen Sie die Parameter in korrektem JSON-Format hinzu, z. B. \"temperature\": 0.5. Entfernen Sie abschließende Kommas. Die üblichen umgebenden geschweiften Klammern {} dürfen jedoch nicht verwendet werden." + -- Show available models UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3763891899"] = "Verfügbare Modelle anzeigen" -- This host uses the model configured at the provider level. No model selection is available. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3783329915"] = "Dieser Host verwendet das auf Anbieterebene konfigurierte Modell. Es ist keine Modellauswahl verfügbar." +-- Duplicate key '{0}' found. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3804472591"] = "Doppelter Schlüssel '{0}' gefunden." + -- Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T4116737656"] = "Derzeit können wir die Modelle für den ausgewählten Anbieter und/oder Host nicht abfragen. Bitte geben Sie daher den Modellnamen manuell ein." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 5ab2e446..d5da8afc 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -3534,6 +3534,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1870831108"] = "Failed to l -- Please enter a model name. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1936099896"] = "Please enter a model name." +-- Additional API parameters must form a JSON object. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2051143391"] = "Additional API parameters must form a JSON object." + -- Model UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2189814010"] = "Model" @@ -3555,12 +3558,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2842060373"] = "Instance Na -- Show Expert Settings UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3361153305"] = "Show Expert Settings" +-- Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3502745319"] = "Invalid JSON: Add the parameters in proper JSON formatting, e.g., \\\"temperature\\\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though." + -- Show available models UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3763891899"] = "Show available models" -- This host uses the model configured at the provider level. No model selection is available. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3783329915"] = "This host uses the model configured at the provider level. No model selection is available." +-- Duplicate key '{0}' found. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3804472591"] = "Duplicate key '{0}' found." + -- Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T4116737656"] = "Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually." diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs index 5eb8fe2b..49a0e6ea 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -29,6 +29,9 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, " // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters("system"); + var maxTokens = 4_096; + if (TryPopIntParameter(apiParameters, "max_tokens", out var parsedMaxTokens)) + maxTokens = parsedMaxTokens; // Build the list of messages: var messages = await chatThread.Blocks.BuildMessagesAsync( @@ -73,7 +76,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, " Messages = [..messages], System = chatThread.PrepareSystemPrompt(settingsManager), - MaxTokens = apiParameters.TryGetValue("max_tokens", out var value) && value is int intValue ? intValue : 4_096, + MaxTokens = maxTokens, // Right now, we only support streaming completions: Stream = true, @@ -188,4 +191,4 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, " var modelResponse = await response.Content.ReadFromJsonAsync(JSON_SERIALIZER_OPTIONS, token); return modelResponse.Data; } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index 4acefc62..9b729824 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -731,7 +731,7 @@ public abstract class BaseProvider : IProvider, ISecretId /// Optional list of keys to remove from the final dictionary /// (case-insensitive). The parameters stream, model, and messages are removed by default. protected IDictionary ParseAdditionalApiParameters( - params List keysToRemove) + params string[] keysToRemove) { if(string.IsNullOrWhiteSpace(this.AdditionalJsonApiParameters)) return new Dictionary(); @@ -744,14 +744,23 @@ public abstract class BaseProvider : IProvider, ISecretId var dict = ConvertToDictionary(jsonDoc); // Some keys are always removed because we set them: - keysToRemove.Add("stream"); - keysToRemove.Add("model"); - keysToRemove.Add("messages"); + var removeSet = new HashSet(StringComparer.OrdinalIgnoreCase); + if (keysToRemove.Length > 0) + removeSet.UnionWith(keysToRemove); + + removeSet.Add("stream"); + removeSet.Add("model"); + removeSet.Add("messages"); // Remove the specified keys (case-insensitive): - var removeSet = new HashSet(keysToRemove, StringComparer.OrdinalIgnoreCase); - foreach (var key in removeSet) - dict.Remove(key); + if (removeSet.Count > 0) + { + foreach (var key in dict.Keys.ToList()) + { + if (removeSet.Contains(key)) + dict.Remove(key); + } + } return dict; } @@ -761,6 +770,85 @@ public abstract class BaseProvider : IProvider, ISecretId return new Dictionary(); } } + + protected static bool TryPopIntParameter(IDictionary parameters, string key, out int value) + { + value = default; + if (!TryPopParameter(parameters, key, out var raw) || raw is null) + return false; + + switch (raw) + { + case int i: + value = i; + return true; + + case long l when l is >= int.MinValue and <= int.MaxValue: + value = (int)l; + return true; + + case double d when d is >= int.MinValue and <= int.MaxValue: + value = (int)d; + return true; + + case decimal m when m is >= int.MinValue and <= int.MaxValue: + value = (int)m; + return true; + } + + return false; + } + + protected static bool TryPopBoolParameter(IDictionary parameters, string key, out bool value) + { + value = default; + if (!TryPopParameter(parameters, key, out var raw) || raw is null) + return false; + + switch (raw) + { + case bool b: + value = b; + return true; + + case string s when bool.TryParse(s, out var parsed): + value = parsed; + return true; + + case int i: + value = i != 0; + return true; + + case long l: + value = l != 0; + return true; + + case double d: + value = Math.Abs(d) > double.Epsilon; + return true; + + case decimal m: + value = m != 0; + return true; + } + + return false; + } + + private static bool TryPopParameter(IDictionary parameters, string key, out object? value) + { + value = null; + if (parameters.Count == 0) + return false; + + var foundKey = parameters.Keys.FirstOrDefault(k => string.Equals(k, key, StringComparison.OrdinalIgnoreCase)); + if (foundKey is null) + return false; + + value = parameters[foundKey]; + parameters.Remove(foundKey); + return true; + } private static IDictionary ConvertToDictionary(JsonElement element) { @@ -785,4 +873,4 @@ public abstract class BaseProvider : IProvider, ISecretId _ => string.Empty, }; -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/Mistral/ChatRequest.cs b/app/MindWork AI Studio/Provider/Mistral/ChatRequest.cs index 01a45a89..1d42081f 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ChatRequest.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ChatRequest.cs @@ -14,11 +14,12 @@ public readonly record struct ChatRequest( string Model, IList Messages, bool Stream, - int RandomSeed, + [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + int? RandomSeed, bool SafePrompt = false ) { // Attention: The "required" modifier is not supported for [JsonExtensionData]. [JsonExtensionData] public IDictionary AdditionalApiParameters { get; init; } = new Dictionary(); -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index f4cb07f4..485729fb 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -36,6 +36,8 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters(); + var safePrompt = TryPopBoolParameter(apiParameters, "safe_prompt", out var parsedSafePrompt) && parsedSafePrompt; + var randomSeed = TryPopIntParameter(apiParameters, "random_seed", out var parsedRandomSeed) ? parsedRandomSeed : (int?)null; // Build the list of messages: var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel); @@ -52,7 +54,8 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http // Right now, we only support streaming completions: Stream = true, - SafePrompt = apiParameters.TryGetValue("safe_prompt", out var value) && value is true, + RandomSeed = randomSeed, + SafePrompt = safePrompt, AdditionalApiParameters = apiParameters }, JSON_SERIALIZER_OPTIONS); @@ -165,4 +168,4 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http var modelResponse = await response.Content.ReadFromJsonAsync(token); return modelResponse; } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 3cbd4960..25befa8a 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -7,4 +7,5 @@ - Improved the user-language logging by limiting language detection logs to a single entry per app start. - Improved the logbook readability by removing non-readable special characters from log entries. - Improved the logbook reliability by significantly reducing duplicate log entries. +- Improved the validation of additional API parameters in the advanced provider settings to help catch formatting mistakes earlier. - Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks Inga for reporting this issue and providing some context on how to reproduce it. \ No newline at end of file From 65ec82cdcb6e331417c5e529555d6a62ffd7dc60 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 12 Mar 2026 12:51:44 +0100 Subject: [PATCH 08/27] Improved handling of file attachments in chats (#689) --- app/MindWork AI Studio/Chat/FileAttachment.cs | 6 ++- .../Tools/Rust/FileTypeFilter.cs | 45 +++++++++++++++++++ .../wwwroot/changelog/v26.3.1.md | 1 + 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Chat/FileAttachment.cs b/app/MindWork AI Studio/Chat/FileAttachment.cs index 1208303f..f364ed8f 100644 --- a/app/MindWork AI Studio/Chat/FileAttachment.cs +++ b/app/MindWork AI Studio/Chat/FileAttachment.cs @@ -84,6 +84,9 @@ public record FileAttachment(FileAttachmentType Type, string FileName, string Fi { var extension = Path.GetExtension(filePath).TrimStart('.').ToLowerInvariant(); + if (FileTypeFilter.Executables.FilterExtensions.Contains(extension)) + return FileAttachmentType.FORBIDDEN; + // Check if it's an image file: if (FileTypeFilter.AllImages.FilterExtensions.Contains(extension)) return FileAttachmentType.IMAGE; @@ -96,7 +99,8 @@ public record FileAttachment(FileAttachmentType Type, string FileName, string Fi if (FileTypeFilter.PDF.FilterExtensions.Contains(extension) || FileTypeFilter.Text.FilterExtensions.Contains(extension) || FileTypeFilter.AllOffice.FilterExtensions.Contains(extension) || - FileTypeFilter.AllSourceCode.FilterExtensions.Contains(extension)) + FileTypeFilter.AllSourceCode.FilterExtensions.Contains(extension) || + FileTypeFilter.IsAllowedSourceLikeFileName(filePath)) return FileAttachmentType.DOCUMENT; // All other file types are forbidden: diff --git a/app/MindWork AI Studio/Tools/Rust/FileTypeFilter.cs b/app/MindWork AI Studio/Tools/Rust/FileTypeFilter.cs index 03232070..d93f44e0 100644 --- a/app/MindWork AI Studio/Tools/Rust/FileTypeFilter.cs +++ b/app/MindWork AI Studio/Tools/Rust/FileTypeFilter.cs @@ -12,6 +12,51 @@ namespace AIStudio.Tools.Rust; public readonly record struct FileTypeFilter(string FilterName, string[] FilterExtensions) { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileTypeFilter).Namespace, nameof(FileTypeFilter)); + + private static string[] AllowedSourceLikeFileNames => + [ + "Dockerfile", + "Containerfile", + "Jenkinsfile", + "Makefile", + "GNUmakefile", + "Procfile", + "Vagrantfile", + "Tiltfile", + "Justfile", + "Brewfile", + "Caddyfile", + "Gemfile", + "Podfile", + "Fastfile", + "Appfile", + "Rakefile", + "Dangerfile", + "BUILD", + "WORKSPACE", + "BUCK", + ]; + + private static string[] AllowedSourceLikeFileNamePrefixes => + [ + "Dockerfile", + "Containerfile", + "Jenkinsfile", + "Procfile", + "Caddyfile", + ]; + + public static bool IsAllowedSourceLikeFileName(string filePath) + { + var fileName = Path.GetFileName(filePath); + if (string.IsNullOrWhiteSpace(fileName)) + return false; + + if (AllowedSourceLikeFileNames.Any(name => string.Equals(name, fileName, StringComparison.OrdinalIgnoreCase))) + return true; + + return AllowedSourceLikeFileNamePrefixes.Any(prefix => fileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); + } public static FileTypeFilter PDF => new(TB("PDF Files"), ["pdf"]); diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 25befa8a..1fc66926 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -7,5 +7,6 @@ - Improved the user-language logging by limiting language detection logs to a single entry per app start. - Improved the logbook readability by removing non-readable special characters from log entries. - Improved the logbook reliability by significantly reducing duplicate log entries. +- Improved file attachments in chats: configuration and project files such as `Dockerfile`, `Caddyfile`, `Makefile`, or `Jenkinsfile` are now included more reliably when you send them to the AI. - Improved the validation of additional API parameters in the advanced provider settings to help catch formatting mistakes earlier. - Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks Inga for reporting this issue and providing some context on how to reproduce it. \ No newline at end of file From e15f1ce54e54c87490bb6780ed67ced559069a82 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 14 Mar 2026 10:37:31 +0100 Subject: [PATCH 09/27] Fixed "Send to" options based on assistant visibility (#695) --- AGENTS.md | 25 ++++++++++++++++--- .../Assistants/AssistantBase.razor | 23 +++++++++-------- .../Assistants/AssistantBase.razor.cs | 22 +++++++++++++++- .../wwwroot/changelog/v26.3.1.md | 3 ++- 4 files changed, 58 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 02078f06..49524e17 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -144,7 +144,7 @@ Multi-level confidence scheme allows users to control which providers see which **Rust:** - Tauri 1.8 - Desktop application framework -- Rocket 0.5 - HTTPS API server +- Rocket - HTTPS API server - tokio - Async runtime - keyring - OS keyring integration - pdfium-render - PDF text extraction @@ -152,7 +152,7 @@ Multi-level confidence scheme allows users to control which providers see which **.NET:** - Blazor Server - UI framework -- MudBlazor 8.12 - Component library +- MudBlazor - Component library - LuaCSharp - Lua scripting engine - HtmlAgilityPack - HTML parsing - ReverseMarkdown - HTML to Markdown conversion @@ -168,7 +168,7 @@ Multi-level confidence scheme allows users to control which providers see which 1. Create changelog file: `app/MindWork AI Studio/wwwroot/changelog/vX.Y.Z.md` 2. Commit changelog -3. Run from `app/Build`: `dotnet run release --action ` +3. Run from `app/Build`: `dotnet run release --action ` 4. Create PR with version bump and changes 5. After PR merge, maintainer creates git tag: `vX.Y.Z` 6. GitHub Actions builds release binaries for all platforms @@ -183,3 +183,22 @@ Multi-level confidence scheme allows users to control which providers see which - **MudBlazor** - Component library requires DI setup in Program.cs - **Encryption** - Initialized before Rust service is marked ready - **Message Bus** - Singleton event bus for cross-component communication inside the .NET app + +## Changelogs +Changelogs are located in `app/MindWork AI Studio/wwwroot/changelog/` with filenames `vX.Y.Z.md`. These changelogs are meant to be for normal end-users +and should be written in a non-technical way, focusing on user-facing changes and improvements. Additionally, changes made regarding the plugin system +should be included in the changelog, especially if they affect how users can configure the app or if they introduce new capabilities for plugins. Plugin +developers should also be informed about these changes, as they might need to update their plugins accordingly. When adding entries to the changelog, +please ensure they are clear and concise, avoiding technical jargon where possible. Each entry starts with a dash and a space (`- `) and one of the +following words: + +- Added +- Improved +- Changed +- Fixed +- Updated +- Removed +- Downgraded +- Upgraded + +The entire changelog is sorted by these categories in the order shown above. The language used for the changelog is US English. \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor b/app/MindWork AI Studio/Assistants/AssistantBase.razor index 5fee5f0a..781bceaa 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor @@ -80,10 +80,10 @@ @if (!this.FooterButtons.Any(x => x.Type is ButtonTypes.SEND_TO)) { - @if (this.ShowSendTo) + @if (this.ShowSendTo && this.VisibleSendToAssistants.Count > 0) { - @foreach (var assistant in Enum.GetValues().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length)) + @foreach (var assistant in this.VisibleSendToAssistants) { @assistant.Name() @@ -112,14 +112,17 @@ break; case SendToButton sendToButton: - - @foreach (var assistant in Enum.GetValues().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length)) - { - - @assistant.Name() - - } - + @if (this.VisibleSendToAssistants.Count > 0) + { + + @foreach (var assistant in this.VisibleSendToAssistants) + { + + @assistant.Name() + + } + + } break; } } diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs index a91f2b57..632722ab 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs @@ -105,6 +105,13 @@ public abstract partial class AssistantBase : AssistantLowerBase wher protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); + + if (!this.SettingsManager.IsAssistantVisible(this.Component, assistantName: this.Title)) + { + this.Logger.LogInformation("Assistant '{AssistantTitle}' is hidden. Redirecting to the assistants overview.", this.Title); + this.NavigationManager.NavigateTo(Routes.ASSISTANTS); + return; + } this.formChangeTimer.AutoReset = false; this.formChangeTimer.Elapsed += async (_, _) => @@ -142,6 +149,11 @@ public abstract partial class AssistantBase : AssistantLowerBase wher private string TB(string fallbackEN) => this.T(fallbackEN, typeof(AssistantBase).Namespace, nameof(AssistantBase)); private string SubmitButtonStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.providerSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).StyleBorder(this.SettingsManager) : string.Empty; + + private IReadOnlyList VisibleSendToAssistants => Enum.GetValues() + .Where(this.CanSendToAssistant) + .OrderBy(component => component.Name().Length) + .ToArray(); protected string? ValidatingProvider(AIStudio.Settings.Provider provider) { @@ -339,7 +351,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher protected Task SendToAssistant(Tools.Components destination, SendToButton sendToButton) { - if (!destination.AllowSendTo()) + if (!this.CanSendToAssistant(destination)) return Task.CompletedTask; var contentToSend = sendToButton == default ? string.Empty : sendToButton.UseResultingContentBlockData switch @@ -369,6 +381,14 @@ public abstract partial class AssistantBase : AssistantLowerBase wher this.NavigationManager.NavigateTo(sendToData.Route); return Task.CompletedTask; } + + private bool CanSendToAssistant(Tools.Components component) + { + if (!component.AllowSendTo()) + return false; + + return this.SettingsManager.IsAssistantVisible(component, withLogging: false); + } private async Task InnerResetForm() { diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 1fc66926..6cf09bdc 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -9,4 +9,5 @@ - Improved the logbook reliability by significantly reducing duplicate log entries. - Improved file attachments in chats: configuration and project files such as `Dockerfile`, `Caddyfile`, `Makefile`, or `Jenkinsfile` are now included more reliably when you send them to the AI. - Improved the validation of additional API parameters in the advanced provider settings to help catch formatting mistakes earlier. -- Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks Inga for reporting this issue and providing some context on how to reproduce it. \ No newline at end of file +- Fixed an issue where assistants hidden via configuration plugins still appear in "Send to ..." menus. Thanks, Gunnar, for reporting this issue. +- Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks, Inga, for reporting this issue and providing some context on how to reproduce it. \ No newline at end of file From 5e4d9fd501bc16886c434cf8f862e8a0773e02fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peer=20Sch=C3=BCtt?= Date: Sat, 14 Mar 2026 10:41:50 +0100 Subject: [PATCH 10/27] Change rewrite assistant description & align US plugin icon (#692) --- .../icon.lua | 4 ++-- .../plugin.lua | 14 +++++++------- .../icon.lua | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua index c10dd294..1d3acd2a 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua @@ -1,9 +1,9 @@ SVG = [[ - + - + diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 339d8507..316ef87d 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -1291,7 +1291,7 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::ASSISTANTREWRITEIMPROVE:: UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::ASSISTANTREWRITEIMPROVE::T1714063121"] = "Satzstruktur" -- Rewrite & Improve Text -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::ASSISTANTREWRITEIMPROVE::T1994150308"] = "Text umschreiben & verbessern" +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::ASSISTANTREWRITEIMPROVE::T1994150308"] = "Text umformulieren & verbessern" -- Improve your text UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::ASSISTANTREWRITEIMPROVE::T2163831433"] = "Verbessern Sie ihren Text" @@ -2500,7 +2500,7 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2868740431"] = "Spezifische Anfo UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2899555955"] = "Wir werden weitere Assistenten für alltägliche Aufgaben entwickeln." -- We're working on offering AI Studio features in your browser via a plugin, allowing, e.g., for spell-checking or text rewriting directly in the browser. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T308543246"] = "Wir arbeiten daran, die Funktionen von AI Studio über ein Plugin auch in ihrem Browser anzubieten. So können Sie zum Beispiel direkt im Browser Rechtschreibprüfungen durchführen oder Texte umschreiben lassen." +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T308543246"] = "Wir arbeiten daran, die Funktionen von AI Studio über ein Plugin auch in ihrem Browser anzubieten. So können Sie zum Beispiel direkt im Browser Rechtschreibprüfungen durchführen oder Texte umformulieren lassen." -- There will be an interface for AI Studio to create content in other apps. You could, for example, create blog posts directly on the target platform or add entries to an internal knowledge management tool. This requires development work by the tool developers. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T3290746961"] = "Es wird eine Schnittstelle für AI Studio geben, um Inhalte in anderen Apps zu erstellen. So könnten Sie zum Beispiel Blogbeiträge direkt auf der Zielplattform verfassen oder Einträge zu einem internen Wissensmanagement-Tool hinzufügen. Dafür ist Entwicklungsarbeit durch die jeweiligen Tool-Entwickler erforderlich." @@ -4438,7 +4438,7 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T1462295644 UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T1621537655"] = "Satzstruktur vorauswählen" -- Assistant: Rewrite & Improve Text Options -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T1995708818"] = "Assistent: Text umschreiben & verbessern" +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T1995708818"] = "Assistent: Text umformulieren & verbessern" -- Which voice should be preselected for the sentence structure? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T2661599097"] = "Welche Stimme soll für die Satzstruktur vorausgewählt werden?" @@ -4447,7 +4447,7 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T2661599097 UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T28456020"] = "Wählen Sie einen Schreibstil aus" -- Rewrite & improve text options are preselected -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3303192024"] = "Optionen für „Text umschreiben & verbessern“ sind bereits vorausgewählt." +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3303192024"] = "Optionen für „Text umformulieren & verbessern“ sind bereits vorausgewählt." -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3448155331"] = "Schließen" @@ -4456,13 +4456,13 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3448155331 UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3547337928"] = "Welche Zielsprache soll vorausgewählt werden?" -- When enabled, you can preselect the rewrite & improve text options. This is might be useful when you prefer a specific language or LLM model. -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3657121735"] = "Wenn diese Option aktiviert ist, können Sie Optionen für „Text umschreiben & verbessern“ im Voraus auswählen. Das kann hilfreich sein, wenn Sie eine bestimmte Sprache oder ein bestimmtes LLM-Modell bevorzugen." +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3657121735"] = "Wenn diese Option aktiviert ist, können Sie Optionen für „Text umformulieren & verbessern“ im Voraus auswählen. Das kann hilfreich sein, wenn Sie eine bestimmte Sprache oder ein bestimmtes LLM-Modell bevorzugen." -- Preselect rewrite & improve text options? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3745021518"] = "Vorauswahl von Optionen für „Text umschreiben & verbessern“?" +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3745021518"] = "Vorauswahl von Optionen für „Text umformulieren & verbessern“?" -- No rewrite & improve text options are preselected -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T553954963"] = "Keine Optionen für „Text umschreiben & verbessern“ sind vorausgewählt" +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T553954963"] = "Keine Optionen für „Text umformulieren & verbessern“ sind vorausgewählt" -- When enabled, you can preselect synonym options. This is might be useful when you prefer a specific language or LLM model. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSYNONYMS::T183953912"] = "Wenn diese Option aktiviert ist, können Sie Synonymoptionen im Voraus auswählen. Dies kann nützlich sein, wenn Sie eine bestimmte Sprache oder ein bestimmtes LLM-Modell bevorzugen." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua index 75320473..211231ed 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua @@ -1,12 +1,12 @@ SVG = [[ - + - + - ]] \ No newline at end of file + ]] From 078d48ca6fd17227ced21bbd402fd89e204970d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peer=20Sch=C3=BCtt?= Date: Sat, 14 Mar 2026 10:54:52 +0100 Subject: [PATCH 11/27] Added a LLM halluzination reminder (#694) Co-authored-by: Thorsten Sommer --- .../Assistants/AssistantBase.razor | 3 +++ .../Assistants/I18N/allTexts.lua | 3 +++ .../Components/ChatComponent.razor | 4 +++- .../Components/HalluzinationReminder.razor | 5 +++++ .../Components/HalluzinationReminder.razor.cs | 15 +++++++++++++++ .../plugin.lua | 3 +++ .../plugin.lua | 3 +++ .../wwwroot/changelog/v26.3.1.md | 1 + 8 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 app/MindWork AI Studio/Components/HalluzinationReminder.razor create mode 100644 app/MindWork AI Studio/Components/HalluzinationReminder.razor.cs diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor b/app/MindWork AI Studio/Assistants/AssistantBase.razor index 781bceaa..3268612d 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor @@ -150,6 +150,9 @@ { } + + + diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index d3a4c32b..d67a06e4 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -1768,6 +1768,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T700666808"] = "Mana -- Available Data Sources UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T86053874"] = "Available Data Sources" +-- LLMs can make mistakes. Check important information. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::HALLUZINATIONREMINDER::T3528806904"] = "LLMs can make mistakes. Check important information." + -- Issues UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ISSUES::T3229841001"] = "Issues" diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor index 3c49a4b5..fb9c5e85 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor +++ b/app/MindWork AI Studio/Components/ChatComponent.razor @@ -124,7 +124,9 @@ } - + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/HalluzinationReminder.razor b/app/MindWork AI Studio/Components/HalluzinationReminder.razor new file mode 100644 index 00000000..d6c1046b --- /dev/null +++ b/app/MindWork AI Studio/Components/HalluzinationReminder.razor @@ -0,0 +1,5 @@ + + + @this.Text + + diff --git a/app/MindWork AI Studio/Components/HalluzinationReminder.razor.cs b/app/MindWork AI Studio/Components/HalluzinationReminder.razor.cs new file mode 100644 index 00000000..9970b0fa --- /dev/null +++ b/app/MindWork AI Studio/Components/HalluzinationReminder.razor.cs @@ -0,0 +1,15 @@ +using AIStudio.Tools.PluginSystem; +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +public partial class HalluzinationReminder: ComponentBase +{ + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(HalluzinationReminder).Namespace, nameof(HalluzinationReminder)); + + [Parameter] + public string Text { get; set; } = TB("LLMs can make mistakes. Check important information."); + + [Parameter] + public string ContainerClass { get; set; } = "mt-2 mb-1"; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 316ef87d..9a6d9860 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -1770,6 +1770,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T700666808"] = "Date -- Available Data Sources UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T86053874"] = "Verfügbare Datenquellen" +-- LLMs can make mistakes. Check important information. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::HALLUZINATIONREMINDER::T3528806904"] = "LLMs können Fehler machen. Überprüfen Sie wichtige Informationen." + -- Issues UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ISSUES::T3229841001"] = "Probleme" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index d5da8afc..08d6edf3 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -1770,6 +1770,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T700666808"] = "Mana -- Available Data Sources UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T86053874"] = "Available Data Sources" +-- LLMs can make mistakes. Check important information. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::HALLUZINATIONREMINDER::T3528806904"] = "LLMs can make mistakes. Check important information." + -- Issues UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ISSUES::T3229841001"] = "Issues" diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 6cf09bdc..48d0df57 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -1,5 +1,6 @@ # v26.3.1, build 235 (2026-03-xx xx:xx UTC) - Added support for the new Qwen 3.5 model family. +- Added a reminder in chats and assistants that LLMs can make mistakes, helping you double-check important information more easily. - Improved the performance by caching the OS language detection and requesting the user language only once per app start. - Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations. - Improved the workspace loading experience: when opening the chat for the first time, your workspaces now appear faster and load step by step in the background, with placeholder rows so the app feels responsive right away. From 2f5a300e74c219bc1465fb89c3c7098e42abbf30 Mon Sep 17 00:00:00 2001 From: Paul Koudelka <106623909+PaulKoudelka@users.noreply.github.com> Date: Sat, 14 Mar 2026 12:25:58 +0100 Subject: [PATCH 12/27] Handle Qdrant startup failures gracefully (#683) Co-authored-by: Thorsten Sommer --- .../Assistants/I18N/allTexts.lua | 15 ++ .../Pages/Information.razor.cs | 4 +- .../plugin.lua | 15 ++ .../plugin.lua | 15 ++ app/MindWork AI Studio/Program.cs | 59 ++++--- .../Tools/Databases/DatabaseClient.cs | 2 + .../Tools/Databases/NoDatabaseClient.cs | 24 +++ .../Qdrant/QdrantClientImplementation.cs | 4 +- .../Tools/Rust/QdrantInfo.cs | 4 + .../wwwroot/changelog/v26.3.1.md | 1 + runtime/src/app_window.rs | 10 +- runtime/src/qdrant.rs | 151 ++++++++++++++---- runtime/tauri.conf.json | 2 +- 13 files changed, 240 insertions(+), 66 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index d67a06e4..375ea097 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -5302,6 +5302,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to -- Motivation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation" +-- not available +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available" + -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." @@ -5323,6 +5326,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Check Pandoc Ins -- Versions UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions" +-- Database +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database" + -- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system." @@ -5908,6 +5914,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T3893997203"] = " -- Trust all LLM providers UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Trust all LLM providers" +-- Reason +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason" + +-- Unavailable +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable" + +-- Status +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status" + -- Storage size UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size" diff --git a/app/MindWork AI Studio/Pages/Information.razor.cs b/app/MindWork AI Studio/Pages/Information.razor.cs index 2027285f..1f3d946e 100644 --- a/app/MindWork AI Studio/Pages/Information.razor.cs +++ b/app/MindWork AI Studio/Pages/Information.razor.cs @@ -58,7 +58,9 @@ public partial class Information : MSGComponentBase private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}"; - private string VersionDatabase => $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}"; + private string VersionDatabase => this.DatabaseClient.IsAvailable + ? $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}" + : $"{T("Database")}: {this.DatabaseClient.Name} - {T("not available")}"; private string versionPandoc = TB("Determine Pandoc version, please wait..."); private PandocInstallation pandocInstallation; diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 9a6d9860..f70104b1 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -5304,6 +5304,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri wird verwe -- Motivation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation" +-- not available +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "nicht verfügbar" + -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek wird verwendet, um Excel- und OpenDocument-Tabellendateien zu lesen. Dies ist zum Beispiel notwendig, wenn Tabellen als Datenquelle für einen Chat verwendet werden sollen." @@ -5325,6 +5328,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Pandoc-Installat -- Versions UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versionen" +-- Database +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Datenbank" + -- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "Diese Bibliothek wird verwendet, um asynchrone Datenströme in Rust zu erstellen. Sie ermöglicht es uns, mit Datenströmen zu arbeiten, die asynchron bereitgestellt werden, wodurch sich Ereignisse oder Daten, die nach und nach eintreffen, leichter verarbeiten lassen. Wir nutzen dies zum Beispiel, um beliebige Daten aus dem Dateisystem an das Einbettungssystem zu übertragen." @@ -5910,6 +5916,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T3893997203"] = " -- Trust all LLM providers UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Allen LLM-Anbietern vertrauen" +-- Reason +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Grund" + +-- Unavailable +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Nicht verfügbar" + +-- Status +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status" + -- Storage size UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Speichergröße" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 08d6edf3..5c19cc15 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -5304,6 +5304,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to -- Motivation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation" +-- not available +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available" + -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." @@ -5325,6 +5328,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Check Pandoc Ins -- Versions UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions" +-- Database +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database" + -- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system." @@ -5910,6 +5916,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T3893997203"] = " -- Trust all LLM providers UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Trust all LLM providers" +-- Reason +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason" + +-- Unavailable +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable" + +-- Status +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status" + -- Storage size UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size" diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 85e97b07..70cc6831 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -86,37 +86,46 @@ internal sealed class Program } var qdrantInfo = await rust.GetQdrantInfo(); - if (qdrantInfo.Path == string.Empty) + DatabaseClient databaseClient; + if (!qdrantInfo.IsAvailable) { - Console.WriteLine("Error: Failed to get the Qdrant path from Rust."); - return; + Console.WriteLine($"Warning: Qdrant is not available. Starting without vector database. Reason: '{qdrantInfo.UnavailableReason ?? "unknown"}'."); + databaseClient = new NoDatabaseClient("Qdrant", qdrantInfo.UnavailableReason); } - - if (qdrantInfo.PortHttp == 0) + else { - Console.WriteLine("Error: Failed to get the Qdrant HTTP port from Rust."); - return; - } + if (qdrantInfo.Path == string.Empty) + { + Console.WriteLine("Error: Failed to get the Qdrant path from Rust."); + return; + } + + if (qdrantInfo.PortHttp == 0) + { + Console.WriteLine("Error: Failed to get the Qdrant HTTP port from Rust."); + return; + } - if (qdrantInfo.PortGrpc == 0) - { - Console.WriteLine("Error: Failed to get the Qdrant gRPC port from Rust."); - return; - } + if (qdrantInfo.PortGrpc == 0) + { + Console.WriteLine("Error: Failed to get the Qdrant gRPC port from Rust."); + return; + } - if (qdrantInfo.Fingerprint == string.Empty) - { - Console.WriteLine("Error: Failed to get the Qdrant fingerprint from Rust."); - return; - } - - if (qdrantInfo.ApiToken == string.Empty) - { - Console.WriteLine("Error: Failed to get the Qdrant API token from Rust."); - return; - } + if (qdrantInfo.Fingerprint == string.Empty) + { + Console.WriteLine("Error: Failed to get the Qdrant fingerprint from Rust."); + return; + } + + if (qdrantInfo.ApiToken == string.Empty) + { + Console.WriteLine("Error: Failed to get the Qdrant API token from Rust."); + return; + } - var databaseClient = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken); + databaseClient = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken); + } var builder = WebApplication.CreateBuilder(); builder.WebHost.ConfigureKestrel(kestrelServerOptions => diff --git a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs index b50aafe1..b80cba94 100644 --- a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs +++ b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs @@ -3,6 +3,8 @@ public abstract class DatabaseClient(string name, string path) { public string Name => name; + + public virtual bool IsAvailable => true; private string Path => path; diff --git a/app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs b/app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs new file mode 100644 index 00000000..7b3b0cd4 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs @@ -0,0 +1,24 @@ +using AIStudio.Tools.PluginSystem; + +namespace AIStudio.Tools.Databases; + +public sealed class NoDatabaseClient(string name, string? unavailableReason) : DatabaseClient(name, string.Empty) +{ + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(NoDatabaseClient).Namespace, nameof(NoDatabaseClient)); + + public override bool IsAvailable => false; + + public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo() + { + yield return (TB("Status"), TB("Unavailable")); + + if (!string.IsNullOrWhiteSpace(unavailableReason)) + yield return (TB("Reason"), unavailableReason); + + await Task.CompletedTask; + } + + public override void Dispose() + { + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs index 25f37253..60a13419 100644 --- a/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs +++ b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs @@ -43,8 +43,8 @@ public class QdrantClientImplementation : DatabaseClient private async Task GetVersion() { - var operation = await this.GrpcClient.HealthAsync(); - return "v"+operation.Version; + var operation = await this.GrpcClient.HealthAsync(); + return $"v{operation.Version}"; } private async Task GetCollectionsAmount() diff --git a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs index c847235f..5315eca7 100644 --- a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs +++ b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs @@ -5,6 +5,10 @@ /// public readonly record struct QdrantInfo { + public bool IsAvailable { get; init; } + + public string? UnavailableReason { get; init; } + public string Path { get; init; } public int PortHttp { get; init; } diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 48d0df57..e71582ca 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -10,5 +10,6 @@ - Improved the logbook reliability by significantly reducing duplicate log entries. - Improved file attachments in chats: configuration and project files such as `Dockerfile`, `Caddyfile`, `Makefile`, or `Jenkinsfile` are now included more reliably when you send them to the AI. - Improved the validation of additional API parameters in the advanced provider settings to help catch formatting mistakes earlier. +- Improved the app startup resilience by allowing AI Studio to continue without Qdrant if it fails to initialize. - Fixed an issue where assistants hidden via configuration plugins still appear in "Send to ..." menus. Thanks, Gunnar, for reporting this issue. - Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks, Inga, for reporting this issue and providing some context on how to reproduce it. \ No newline at end of file diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index c7e7bd74..0066cfae 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -107,16 +107,16 @@ pub fn start_tauri() { DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the data directory.")).unwrap(); CONFIG_DIRECTORY.set(app.path_resolver().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap(); - cleanup_qdrant(); - cleanup_dotnet_server(); - if is_dev() { #[cfg(debug_assertions)] create_startup_env_file(); } else { + cleanup_dotnet_server(); start_dotnet_server(); } - start_qdrant_server(); + + cleanup_qdrant(); + start_qdrant_server(app.path_resolver()); info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {data_path:?}"); switch_to_file_logging(data_path).map_err(|e| error!("Failed to switch logging to file: {e}")).unwrap(); @@ -1067,4 +1067,4 @@ fn set_pdfium_path(path_resolver: PathResolver) { let pdfium_source_path = pdfium_source_path.unwrap(); let pdfium_source_path = pdfium_source_path.to_str().unwrap().to_string(); *PDFIUM_LIB_PATH.lock().unwrap() = Some(pdfium_source_path.clone()); -} \ No newline at end of file +} diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index 41429431..dff8814f 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -12,9 +12,10 @@ use rocket::serde::json::Json; use rocket::serde::Serialize; use tauri::api::process::{Command, CommandChild, CommandEvent}; use crate::api_token::{APIToken}; -use crate::environment::DATA_DIRECTORY; +use crate::environment::{is_dev, DATA_DIRECTORY}; use crate::certificate_factory::generate_certificate; use std::path::PathBuf; +use tauri::PathResolver; use tempfile::{TempDir, Builder}; use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process}; use crate::sidecar_types::SidecarType; @@ -38,10 +39,24 @@ static API_TOKEN: Lazy = Lazy::new(|| { }); static TMPDIR: Lazy>> = Lazy::new(|| Mutex::new(None)); +static QDRANT_STATUS: Lazy> = Lazy::new(|| Mutex::new(QdrantStatus::default())); const PID_FILE_NAME: &str = "qdrant.pid"; const SIDECAR_TYPE:SidecarType = SidecarType::Qdrant; +#[derive(Default)] +struct QdrantStatus { + is_available: bool, + unavailable_reason: Option, +} + +fn qdrant_base_path() -> PathBuf { + let qdrant_directory = if is_dev() { "qdrant_test" } else { "qdrant" }; + Path::new(DATA_DIRECTORY.get().unwrap()) + .join("databases") + .join(qdrant_directory) +} + #[derive(Serialize)] pub struct ProvideQdrantInfo { path: String, @@ -49,34 +64,62 @@ pub struct ProvideQdrantInfo { port_grpc: u16, fingerprint: String, api_token: String, + is_available: bool, + unavailable_reason: Option, } #[get("/system/qdrant/info")] pub fn qdrant_port(_token: APIToken) -> Json { + let status = QDRANT_STATUS.lock().unwrap(); + let is_available = status.is_available; + let unavailable_reason = status.unavailable_reason.clone(); + Json(ProvideQdrantInfo { - path: Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant").to_str().unwrap().to_string(), - port_http: *QDRANT_SERVER_PORT_HTTP, - port_grpc: *QDRANT_SERVER_PORT_GRPC, - fingerprint: CERTIFICATE_FINGERPRINT.get().expect("Certificate fingerprint not available").to_string(), - api_token: API_TOKEN.to_hex_text().to_string(), + path: if is_available { + qdrant_base_path().to_string_lossy().to_string() + } else { + String::new() + }, + port_http: if is_available { *QDRANT_SERVER_PORT_HTTP } else { 0 }, + port_grpc: if is_available { *QDRANT_SERVER_PORT_GRPC } else { 0 }, + fingerprint: if is_available { + CERTIFICATE_FINGERPRINT.get().cloned().unwrap_or_default() + } else { + String::new() + }, + api_token: if is_available { + API_TOKEN.to_hex_text().to_string() + } else { + String::new() + }, + is_available, + unavailable_reason, }) } /// Starts the Qdrant server in a separate process. -pub fn start_qdrant_server(){ - - let base_path = DATA_DIRECTORY.get().unwrap(); - let path = Path::new(base_path).join("databases").join("qdrant"); +pub fn start_qdrant_server(path_resolver: PathResolver){ + let path = qdrant_base_path(); if !path.exists() { if let Err(e) = fs::create_dir_all(&path){ - error!(Source="Qdrant"; "The required directory to host the Qdrant database could not be created: {}", e.to_string()); + error!(Source="Qdrant"; "The required directory to host the Qdrant database could not be created: {}", e); + set_qdrant_unavailable(format!("The Qdrant data directory could not be created: {e}")); + return; }; } - let (cert_path, key_path) =create_temp_tls_files(&path).unwrap(); - - let storage_path = path.join("storage").to_str().unwrap().to_string(); - let snapshot_path = path.join("snapshots").to_str().unwrap().to_string(); - let init_path = path.join(".qdrant-initalized").to_str().unwrap().to_string(); + + let (cert_path, key_path) = match create_temp_tls_files(&path) { + Ok(paths) => paths, + Err(e) => { + error!(Source="Qdrant"; "TLS files for Qdrant could not be created: {e}"); + set_qdrant_unavailable(format!("TLS files for Qdrant could not be created: {e}")); + return; + } + }; + + let storage_path = path.join("storage").to_string_lossy().to_string(); + let snapshot_path = path.join("snapshots").to_string_lossy().to_string(); + let init_path = path.join(".qdrant-initialized").to_string_lossy().to_string(); let qdrant_server_environment = HashMap::from_iter([ (String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()), @@ -84,22 +127,52 @@ pub fn start_qdrant_server(){ (String::from("QDRANT_INIT_FILE_PATH"), init_path), (String::from("QDRANT__STORAGE__STORAGE_PATH"), storage_path), (String::from("QDRANT__STORAGE__SNAPSHOTS_PATH"), snapshot_path), - (String::from("QDRANT__TLS__CERT"), cert_path.to_str().unwrap().to_string()), - (String::from("QDRANT__TLS__KEY"), key_path.to_str().unwrap().to_string()), + (String::from("QDRANT__TLS__CERT"), cert_path.to_string_lossy().to_string()), + (String::from("QDRANT__TLS__KEY"), key_path.to_string_lossy().to_string()), (String::from("QDRANT__SERVICE__ENABLE_TLS"), "true".to_string()), (String::from("QDRANT__SERVICE__API_KEY"), API_TOKEN.to_hex_text().to_string()), ]); let server_spawn_clone = QDRANT_SERVER.clone(); + let qdrant_relative_source_path = "resources/databases/qdrant/config.yaml"; + let qdrant_source_path = match path_resolver.resolve_resource(qdrant_relative_source_path) { + Some(path) => path, + None => { + let reason = format!("The Qdrant config resource '{qdrant_relative_source_path}' could not be resolved."); + error!(Source = "Qdrant"; "{reason} Starting the app without Qdrant."); + set_qdrant_unavailable(reason); + return; + } + }; + + let qdrant_source_path_display = qdrant_source_path.to_string_lossy().to_string(); tauri::async_runtime::spawn(async move { - let (mut rx, child) = Command::new_sidecar("qdrant") - .expect("Failed to create sidecar for Qdrant") - .args(["--config-path", "resources/databases/qdrant/config.yaml"]) + let sidecar = match Command::new_sidecar("qdrant") { + Ok(sidecar) => sidecar, + Err(e) => { + let reason = format!("Failed to create sidecar for Qdrant: {e}"); + error!(Source = "Qdrant"; "{reason}"); + set_qdrant_unavailable(reason); + return; + } + }; + + let (mut rx, child) = match sidecar + .args(["--config-path", qdrant_source_path_display.as_str()]) .envs(qdrant_server_environment) .spawn() - .expect("Failed to spawn Qdrant server process."); + { + Ok(process) => process, + Err(e) => { + let reason = format!("Failed to spawn Qdrant server process with config path '{}': {e}", qdrant_source_path_display); + error!(Source = "Qdrant"; "{reason}"); + set_qdrant_unavailable(reason); + return; + } + }; let server_pid = child.pid(); + set_qdrant_available(); info!(Source = "Bootloader Qdrant"; "Qdrant server process started with PID={server_pid}."); log_potential_stale_process(path.join(PID_FILE_NAME), server_pid, SIDECAR_TYPE); @@ -137,7 +210,10 @@ pub fn stop_qdrant_server() { if let Some(server_process) = QDRANT_SERVER.lock().unwrap().take() { let server_kill_result = server_process.kill(); match server_kill_result { - Ok(_) => warn!(Source = "Qdrant"; "Qdrant server process was stopped."), + Ok(_) => { + set_qdrant_unavailable("Qdrant server was stopped.".to_string()); + warn!(Source = "Qdrant"; "Qdrant server process was stopped.") + }, Err(e) => error!(Source = "Qdrant"; "Failed to stop Qdrant server process: {e}."), } } else { @@ -148,7 +224,7 @@ pub fn stop_qdrant_server() { cleanup_qdrant(); } -/// Create temporary directory with TLS relevant files +/// Create a temporary directory with TLS relevant files pub fn create_temp_tls_files(path: &PathBuf) -> Result<(PathBuf, PathBuf), Box> { let cert = generate_certificate(); @@ -157,10 +233,10 @@ pub fn create_temp_tls_files(path: &PathBuf) -> Result<(PathBuf, PathBuf), Box Result<(), Box> { - let dir_path = Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant"); +fn set_qdrant_available() { + let mut status = QDRANT_STATUS.lock().unwrap(); + status.is_available = true; + status.unavailable_reason = None; +} - if !dir_path.exists() { +fn set_qdrant_unavailable(reason: String) { + let mut status = QDRANT_STATUS.lock().unwrap(); + status.is_available = false; + status.unavailable_reason = Some(reason); +} + +pub fn delete_old_certificates(path: PathBuf) -> Result<(), Box> { + if !path.exists() { return Ok(()); } - for entry in fs::read_dir(dir_path)? { + for entry in fs::read_dir(path)? { let entry = entry?; let path = entry.path(); diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index 27e0aae7..4381d359 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -68,7 +68,7 @@ "target/databases/qdrant/qdrant" ], "resources": [ - "resources/*" + "resources/**" ], "macOS": { "exceptionDomain": "localhost" From 24e72de9a24e5557889a5bcc5094ac4d083461e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peer=20Sch=C3=BCtt?= Date: Sat, 14 Mar 2026 12:40:40 +0100 Subject: [PATCH 13/27] Added formatting options to the chat (#690) --- .../Assistants/I18N/allTexts.lua | 15 +++ .../Components/ChatComponent.razor | 48 ++++++-- .../Components/ChatComponent.razor.cs | 25 +++- .../Components/ProfileSelection.razor | 4 +- .../plugin.lua | 15 +++ .../plugin.lua | 15 +++ app/MindWork AI Studio/wwwroot/app.js | 107 +++++++++++++++++- .../wwwroot/changelog/v26.3.1.md | 1 + 8 files changed, 214 insertions(+), 16 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 375ea097..6473f2e1 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -1591,6 +1591,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1133040906"] = "Move chat -- Are you sure you want to move this chat? All unsaved changes will be lost. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1142475422"] = "Are you sure you want to move this chat? All unsaved changes will be lost." +-- Bold +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1165397398"] = "Bold" + -- Stop generation UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1317408357"] = "Stop generation" @@ -1603,9 +1606,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1849313532"] = "Type your -- Your Prompt (use selected instance '{0}', provider '{1}') UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1967611328"] = "Your Prompt (use selected instance '{0}', provider '{1}')" +-- Code +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2036185364"] = "Code" + +-- Italic +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2377171085"] = "Italic" + -- Profile usage is disabled according to your chat template settings. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2670286472"] = "Profile usage is disabled according to your chat template settings." +-- Bulleted List +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2957125464"] = "Bulleted List" + -- Delete this chat & start a new one. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2991985411"] = "Delete this chat & start a new one." @@ -1624,6 +1636,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T3928697643"] = "Start new -- Start temporary chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T4113970938"] = "Start temporary chat" +-- Heading +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T4231005109"] = "Heading" + -- Please select the workspace where you want to move the chat to. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T474393241"] = "Please select the workspace where you want to move the chat to." diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor index fb9c5e85..897f04b4 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor +++ b/app/MindWork AI Studio/Components/ChatComponent.razor @@ -54,7 +54,8 @@ Class="@this.UserInputClass" Style="@this.UserInputStyle"/> - + + @if ( this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY) @@ -81,9 +82,9 @@ } + + - - @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) { @@ -98,7 +99,36 @@ } - + + + + + + + + + + + + + + + + + + + + + + + + + + @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) + { + + } + @if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence) { @@ -110,14 +140,8 @@ } - - - @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) - { - - } - + @if (!this.ChatThread.IsLLMProviderAllowed(this.Provider)) { @@ -129,4 +153,4 @@ - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs index 9c2b38a0..f734d620 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -13,6 +13,13 @@ namespace AIStudio.Components; public partial class ChatComponent : MSGComponentBase, IAsyncDisposable { + private const string CHAT_INPUT_ID = "chat-user-input"; + private const string MARKDOWN_CODE = "code"; + private const string MARKDOWN_BOLD = "bold"; + private const string MARKDOWN_ITALIC = "italic"; + private const string MARKDOWN_HEADING = "heading"; + private const string MARKDOWN_BULLET_LIST = "bullet_list"; + [Parameter] public ChatThread? ChatThread { get; set; } @@ -36,6 +43,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable [Inject] private IDialogService DialogService { get; init; } = null!; + + [Inject] + private IJSRuntime JsRuntime { get; init; } = null!; private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top; private static readonly Dictionary USER_INPUT_ATTRIBUTES = new(); @@ -73,6 +83,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable // Configure the spellchecking for the user input: this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES); + USER_INPUT_ATTRIBUTES["id"] = CHAT_INPUT_ID; // Get the preselected profile: this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT); @@ -463,6 +474,18 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable break; } } + + private async Task ApplyMarkdownFormat(string formatType) + { + if (this.IsInputForbidden()) + return; + + if(this.dataSourceSelectionComponent?.IsVisible ?? false) + this.dataSourceSelectionComponent.Hide(); + + this.userInput = await this.JsRuntime.InvokeAsync("formatChatInputMarkdown", CHAT_INPUT_ID, formatType); + this.hasUnsavedChanges = true; + } private async Task SendMessage(bool reuseLastUserPrompt = false) { @@ -1018,4 +1041,4 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable } #endregion -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Components/ProfileSelection.razor b/app/MindWork AI Studio/Components/ProfileSelection.razor index 02105589..36d2a35e 100644 --- a/app/MindWork AI Studio/Components/ProfileSelection.razor +++ b/app/MindWork AI Studio/Components/ProfileSelection.razor @@ -11,7 +11,7 @@ } else { - + } @@ -25,4 +25,4 @@ } - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index f70104b1..f943abbf 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -1593,6 +1593,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1133040906"] = "Chat vers -- Are you sure you want to move this chat? All unsaved changes will be lost. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1142475422"] = "Sind Sie sicher, dass Sie diesen Chat verschieben möchten? Alle ungespeicherten Änderungen gehen verloren." +-- Bold +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1165397398"] = "Fett" + -- Stop generation UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1317408357"] = "Generierung stoppen" @@ -1605,9 +1608,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1849313532"] = "Geben Sie -- Your Prompt (use selected instance '{0}', provider '{1}') UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1967611328"] = "Ihr Prompt (verwendete Instanz: '{0}', Anbieter: '{1}')" +-- Code +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2036185364"] = "Code" + +-- Italic +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2377171085"] = "Kursiv" + -- Profile usage is disabled according to your chat template settings. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2670286472"] = "Die Profilnutzung ist gemäß den Einstellungen ihrer Chat-Vorlage deaktiviert." +-- Bulleted List +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2957125464"] = "Aufzählungszeichen" + -- Delete this chat & start a new one. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2991985411"] = "Diesen Chat löschen & einen neuen beginnen." @@ -1626,6 +1638,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T3928697643"] = "Neuen Cha -- New disappearing chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T4113970938"] = "Neuen selbstlöschenden Chat starten" +-- Heading +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T4231005109"] = "Überschrift" + -- Please select the workspace where you want to move the chat to. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T474393241"] = "Bitte wählen Sie den Arbeitsbereich aus, in den Sie den Chat verschieben möchten." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 5c19cc15..029b3b17 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -1593,6 +1593,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1133040906"] = "Move chat -- Are you sure you want to move this chat? All unsaved changes will be lost. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1142475422"] = "Are you sure you want to move this chat? All unsaved changes will be lost." +-- Bold +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1165397398"] = "Bold" + -- Stop generation UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1317408357"] = "Stop generation" @@ -1605,9 +1608,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1849313532"] = "Type your -- Your Prompt (use selected instance '{0}', provider '{1}') UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1967611328"] = "Your Prompt (use selected instance '{0}', provider '{1}')" +-- Code +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2036185364"] = "Code" + +-- Italic +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2377171085"] = "Italic" + -- Profile usage is disabled according to your chat template settings. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2670286472"] = "Profile usage is disabled according to your chat template settings." +-- Bulleted List +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2957125464"] = "Bulleted List" + -- Delete this chat & start a new one. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2991985411"] = "Delete this chat & start a new one." @@ -1626,6 +1638,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T3928697643"] = "Start new -- New disappearing chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T4113970938"] = "New disappearing chat" +-- Heading +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T4231005109"] = "Heading" + -- Please select the workspace where you want to move the chat to. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T474393241"] = "Please select the workspace where you want to move the chat to." diff --git a/app/MindWork AI Studio/wwwroot/app.js b/app/MindWork AI Studio/wwwroot/app.js index aa6b8e2b..a2f8f967 100644 --- a/app/MindWork AI Studio/wwwroot/app.js +++ b/app/MindWork AI Studio/wwwroot/app.js @@ -25,4 +25,109 @@ window.clearDiv = function (divName) { window.scrollToBottom = function(element) { element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' }); -} \ No newline at end of file +} + +window.formatChatInputMarkdown = function (inputId, formatType) { + let input = document.getElementById(inputId) + if (input && input.tagName !== 'TEXTAREA' && input.tagName !== 'INPUT') + input = input.querySelector('textarea, input') + + if (!input) + return '' + + input.focus() + + const value = input.value ?? '' + const start = input.selectionStart ?? value.length + const end = input.selectionEnd ?? value.length + const hasSelection = end > start + const selectedText = value.substring(start, end) + + let insertedText = '' + let selectionStart = start + let selectionEnd = start + + switch (formatType) { + case 'bold': { + const text = hasSelection ? selectedText : '' + insertedText = `**${text}**` + selectionStart = start + 2 + selectionEnd = selectionStart + text.length + break + } + + case 'italic': { + const text = hasSelection ? selectedText : '' + insertedText = `*${text}*` + selectionStart = start + 1 + selectionEnd = selectionStart + text.length + break + } + + case 'heading': { + if (hasSelection) { + insertedText = selectedText + .split('\n') + .map(line => line.startsWith('# ') ? line : `# ${line}`) + .join('\n') + + selectionStart = start + selectionEnd = start + insertedText.length + } else { + const text = '' + insertedText = `# ${text}` + selectionStart = start + 2 + selectionEnd = selectionStart + text.length + } + + break + } + + case 'bullet_list': { + if (hasSelection) { + insertedText = selectedText + .split('\n') + .map(line => line.startsWith('- ') ? line : `- ${line}`) + .join('\n') + + selectionStart = start + selectionEnd = start + insertedText.length + } else { + insertedText = '- ' + selectionStart = start + 2 + selectionEnd = start + insertedText.length + } + + break + } + + case 'code': + default: { + if (hasSelection) { + if (selectedText.includes('\n')) { + insertedText = `\`\`\`\n${selectedText}\n\`\`\`` + selectionStart = start + 4 + selectionEnd = selectionStart + selectedText.length + } else { + insertedText = `\`${selectedText}\`` + selectionStart = start + 1 + selectionEnd = selectionStart + selectedText.length + } + } else { + const text = '' + insertedText = `\`${text}\`` + selectionStart = start + 1 + selectionEnd = selectionStart + text.length + } + + break + } + } + + const nextValue = value.slice(0, start) + insertedText + value.slice(end) + input.value = nextValue + input.setSelectionRange(selectionStart, selectionEnd) + input.dispatchEvent(new Event('input', { bubbles: true })) + + return nextValue +} diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index e71582ca..9585e6e4 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -1,6 +1,7 @@ # v26.3.1, build 235 (2026-03-xx xx:xx UTC) - Added support for the new Qwen 3.5 model family. - Added a reminder in chats and assistants that LLMs can make mistakes, helping you double-check important information more easily. +- Added the ability to format your user prompt in the chat using icons instead of typing Markdown directly. - Improved the performance by caching the OS language detection and requesting the user language only once per app start. - Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations. - Improved the workspace loading experience: when opening the chat for the first time, your workspaces now appear faster and load step by step in the background, with placeholder rows so the app feels responsive right away. From 4a8348b9df8931d5a64abdc965d714e6d16c3b53 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 14 Mar 2026 15:40:07 +0100 Subject: [PATCH 14/27] Added explicit profile behavior choices (#696) --- .../DocumentAnalysisAssistant.razor | 3 +- .../DocumentAnalysisAssistant.razor.cs | 29 ++++++---- .../Settings/SettingsDialogAgenda.razor | 4 +- .../SettingsDialogAssistantBias.razor | 4 +- .../Dialogs/Settings/SettingsDialogChat.razor | 4 +- .../Settings/SettingsDialogCoding.razor | 4 +- .../Settings/SettingsDialogERIServer.razor | 4 +- .../Settings/SettingsDialogLegalCheck.razor | 4 +- .../Settings/SettingsDialogMyTasks.razor | 4 +- .../SettingsDialogWritingEMails.razor | 4 +- .../ConfigurationSelectDataFactory.cs | 9 +++ .../Settings/ProfilePreselection.cs | 55 +++++++++++++++++++ .../Settings/ProfilePreselectionMode.cs | 8 +++ .../Settings/SettingsManager.cs | 32 +++++++++-- .../Tools/ComponentsExtensions.cs | 36 ++++++------ .../wwwroot/changelog/v26.3.1.md | 1 + 16 files changed, 155 insertions(+), 50 deletions(-) create mode 100644 app/MindWork AI Studio/Settings/ProfilePreselection.cs create mode 100644 app/MindWork AI Studio/Settings/ProfilePreselectionMode.cs diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor index 51dd8f7d..d1222bff 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor @@ -1,5 +1,6 @@ @attribute [Route(Routes.ASSISTANT_DOCUMENT_ANALYSIS)] @inherits AssistantBaseCore +@using AIStudio.Settings @using AIStudio.Settings.DataModel @@ -108,7 +109,7 @@ else - + diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs index f58e6619..419d4c9e 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs @@ -176,7 +176,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore loadedDocumentPaths = []; private readonly List> availableLLMProviders = new(); @@ -450,14 +450,21 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore x.Id == this.selectedPolicy.PreselectedProfile); + var policyProfile = this.SettingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == policyProfilePreselection.SpecificProfileId); if (policyProfile is not null) return policyProfile; } - return this.SettingsManager.GetPreselectedProfile(this.Component); + return this.SettingsManager.GetAppPreselectedProfile(); } private async Task PolicyMinimumConfidenceWasChangedAsync(ConfidenceLevel level) @@ -479,11 +486,11 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore - + @@ -42,4 +42,4 @@ @T("Close") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor index ec6776f0..2902c7a6 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor @@ -27,7 +27,7 @@ { } - + @@ -38,4 +38,4 @@ @T("Close") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor index 111b6a93..aa519781 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor @@ -18,7 +18,7 @@ - + @@ -33,4 +33,4 @@ @T("Close") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor index dde19c0c..818dda3c 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor @@ -20,10 +20,10 @@ } - + Close - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor index cb4e20ac..831cbce1 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor @@ -13,7 +13,7 @@ - + @T("Most ERI server options can be customized and saved directly in the ERI server assistant. For this, the ERI server assistant has an auto-save function.") @@ -25,4 +25,4 @@ @T("Close") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor index 42cb70d4..d15662e7 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor @@ -15,7 +15,7 @@ - + @@ -23,4 +23,4 @@ @T("Close") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor index 626f421d..827ad220 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor @@ -16,7 +16,7 @@ { } - + @@ -26,4 +26,4 @@ @T("Close") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor index 6f31f266..904635d2 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor @@ -21,7 +21,7 @@ - + @@ -29,4 +29,4 @@ @T("Close") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs b/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs index 7aa2441e..f31db876 100644 --- a/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs +++ b/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs @@ -204,6 +204,15 @@ public static class ConfigurationSelectDataFactory yield return new(profile.GetSafeName(), profile.Id); } + public static IEnumerable> GetComponentProfilesData(IEnumerable profiles) + { + yield return new(TB("Use app default profile"), ProfilePreselection.AppDefault); + yield return new(Profile.NO_PROFILE.GetSafeName(), ProfilePreselection.NoProfile); + + foreach (var profile in profiles) + yield return new(profile.GetSafeName(), ProfilePreselection.Specific(profile.Id)); + } + public static IEnumerable> GetTranscriptionProvidersData(IEnumerable transcriptionProviders) { foreach (var provider in transcriptionProviders) diff --git a/app/MindWork AI Studio/Settings/ProfilePreselection.cs b/app/MindWork AI Studio/Settings/ProfilePreselection.cs new file mode 100644 index 00000000..ba4b55e1 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ProfilePreselection.cs @@ -0,0 +1,55 @@ +namespace AIStudio.Settings; + +public readonly record struct ProfilePreselection +{ + public ProfilePreselectionMode Mode { get; } + + public string SpecificProfileId { get; } + + public bool UseAppDefault => this.Mode == ProfilePreselectionMode.USE_APP_DEFAULT; + + public bool DoNotPreselectProfile => this.Mode == ProfilePreselectionMode.USE_NO_PROFILE; + + public bool UseSpecificProfile => this.Mode == ProfilePreselectionMode.USE_SPECIFIC_PROFILE; + + public static ProfilePreselection AppDefault => new(ProfilePreselectionMode.USE_APP_DEFAULT, string.Empty); + + public static ProfilePreselection NoProfile => new(ProfilePreselectionMode.USE_NO_PROFILE, Profile.NO_PROFILE.Id); + + private ProfilePreselection(ProfilePreselectionMode mode, string specificProfileId) + { + this.Mode = mode; + this.SpecificProfileId = specificProfileId; + } + + public static ProfilePreselection Specific(string profileId) + { + if (string.IsNullOrWhiteSpace(profileId)) + throw new ArgumentException("A specific profile preselection requires a profile ID.", nameof(profileId)); + + if (profileId.Equals(Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase)) + throw new ArgumentException("Use NoProfile for the NO_PROFILE selection.", nameof(profileId)); + + return new(ProfilePreselectionMode.USE_SPECIFIC_PROFILE, profileId); + } + + public static ProfilePreselection FromStoredValue(string? storedValue) + { + if (string.IsNullOrWhiteSpace(storedValue)) + return AppDefault; + + if (storedValue.Equals(Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase)) + return NoProfile; + + return new(ProfilePreselectionMode.USE_SPECIFIC_PROFILE, storedValue); + } + + public static implicit operator string(ProfilePreselection preselection) => preselection.Mode switch + { + ProfilePreselectionMode.USE_APP_DEFAULT => string.Empty, + ProfilePreselectionMode.USE_NO_PROFILE => Profile.NO_PROFILE.Id, + ProfilePreselectionMode.USE_SPECIFIC_PROFILE => preselection.SpecificProfileId, + + _ => string.Empty, + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ProfilePreselectionMode.cs b/app/MindWork AI Studio/Settings/ProfilePreselectionMode.cs new file mode 100644 index 00000000..8addad93 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ProfilePreselectionMode.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Settings; + +public enum ProfilePreselectionMode +{ + USE_APP_DEFAULT, + USE_NO_PROFILE, + USE_SPECIFIC_PROFILE, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 12634ad6..911d827f 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -294,12 +294,32 @@ public sealed class SettingsManager public Profile GetPreselectedProfile(Tools.Components component) { - var preselection = component.PreselectedProfile(this); - if (preselection != Profile.NO_PROFILE) - return preselection; - - preselection = this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id.Equals(this.ConfigurationData.App.PreselectedProfile, StringComparison.OrdinalIgnoreCase)); - return preselection ?? Profile.NO_PROFILE; + var preselection = component.GetProfilePreselection(this); + if (preselection.DoNotPreselectProfile) + return Profile.NO_PROFILE; + + if (preselection.UseSpecificProfile) + { + var componentProfile = this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id.Equals(preselection.SpecificProfileId, StringComparison.OrdinalIgnoreCase)); + return componentProfile ?? Profile.NO_PROFILE; + } + + var appPreselection = ProfilePreselection.FromStoredValue(this.ConfigurationData.App.PreselectedProfile); + if (appPreselection.DoNotPreselectProfile || !appPreselection.UseSpecificProfile) + return Profile.NO_PROFILE; + + var appProfile = this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id.Equals(appPreselection.SpecificProfileId, StringComparison.OrdinalIgnoreCase)); + return appProfile ?? Profile.NO_PROFILE; + } + + public Profile GetAppPreselectedProfile() + { + var appPreselection = ProfilePreselection.FromStoredValue(this.ConfigurationData.App.PreselectedProfile); + if (appPreselection.DoNotPreselectProfile || !appPreselection.UseSpecificProfile) + return Profile.NO_PROFILE; + + var appProfile = this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id.Equals(appPreselection.SpecificProfileId, StringComparison.OrdinalIgnoreCase)); + return appProfile ?? Profile.NO_PROFILE; } public ChatTemplate GetPreselectedChatTemplate(Tools.Components component) diff --git a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs index 4e346d82..a5ff822d 100644 --- a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs +++ b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs @@ -133,24 +133,28 @@ public static class ComponentsExtensions return preselectedProvider ?? Settings.Provider.NONE; } - public static Profile PreselectedProfile(this Components component, SettingsManager settingsManager) => component switch + public static ProfilePreselection GetProfilePreselection(this Components component, SettingsManager settingsManager) { - Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Agenda.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, - Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Coding.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, - Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.EMail.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, - Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.LegalCheck.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, - Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, - Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, - Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, + var storedValue = component switch + { + Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.PreselectedProfile : string.Empty, + Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.PreselectedProfile : string.Empty, + Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.PreselectedProfile : string.Empty, + Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.PreselectedProfile : string.Empty, + Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.PreselectedProfile : string.Empty, + Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile : string.Empty, + Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.PreselectedProfile : string.Empty, + Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Chat.PreselectedProfile : string.Empty, - // The Document Analysis Assistant does not have a preselected profile at the component level. - // The profile is selected per policy instead. We do this inside the Document Analysis Assistant component: - Components.DOCUMENT_ANALYSIS_ASSISTANT => Profile.NO_PROFILE, - - Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, - - _ => Profile.NO_PROFILE, - }; + // The Document Analysis Assistant does not have a preselected profile at the component level. + // The profile is selected per policy instead. We do this inside the Document Analysis Assistant component: + Components.DOCUMENT_ANALYSIS_ASSISTANT => Profile.NO_PROFILE.Id, + + _ => string.Empty, + }; + + return ProfilePreselection.FromStoredValue(storedValue); + } public static ChatTemplate PreselectedChatTemplate(this Components component, SettingsManager settingsManager) => component switch { diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 9585e6e4..8346f0d6 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -2,6 +2,7 @@ - Added support for the new Qwen 3.5 model family. - Added a reminder in chats and assistants that LLMs can make mistakes, helping you double-check important information more easily. - Added the ability to format your user prompt in the chat using icons instead of typing Markdown directly. +- Added explicit profile behavior choices for assistants and the chat. You can now decide whether AI Studio should use the app default profile, no profile, or a specific profile. - Improved the performance by caching the OS language detection and requesting the user language only once per app start. - Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations. - Improved the workspace loading experience: when opening the chat for the first time, your workspaces now appear faster and load step by step in the background, with placeholder rows so the app feels responsive right away. From 03c4d72ab10efe3bd01d9e504a234ab2caad0123 Mon Sep 17 00:00:00 2001 From: Sabrina-devops Date: Mon, 16 Mar 2026 10:44:21 +0100 Subject: [PATCH 15/27] Added a slide assistant (#647) Co-authored-by: Thorsten Sommer --- .../DocumentAnalysisAssistant.razor | 2 +- .../Assistants/I18N/allTexts.lua | 346 +++++++++++++-- .../SlideBuilder/AudienceAgeGroup.cs | 10 + .../AudienceAgeGroupExtensions.cs | 26 ++ .../SlideBuilder/AudienceExpertise.cs | 11 + .../AudienceExpertiseExtensions.cs | 28 ++ .../AudienceOrganizationalLevel.cs | 13 + .../AudienceOrganizationalLevelExtensions.cs | 32 ++ .../SlideBuilder/AudienceProfile.cs | 17 + .../SlideBuilder/AudienceProfileExtensions.cs | 40 ++ .../SlideBuilder/SlideAssistant.razor | 69 +++ .../SlideBuilder/SlideAssistant.razor.cs | 408 ++++++++++++++++++ .../AssistantTextSummarizer.razor.cs | 3 +- .../Components/ChatComponent.razor | 1 - .../Settings/SettingsDialogAgenda.razor | 2 +- .../SettingsDialogAssistantBias.razor | 2 +- .../Dialogs/Settings/SettingsDialogChat.razor | 2 +- .../Settings/SettingsDialogCoding.razor | 2 +- .../Settings/SettingsDialogERIServer.razor | 2 +- .../Settings/SettingsDialogLegalCheck.razor | 2 +- .../Settings/SettingsDialogMyTasks.razor | 2 +- .../Settings/SettingsDialogSlideBuilder.razor | 34 ++ .../SettingsDialogSlideBuilder.razor.cs | 3 + .../SettingsDialogWritingEMails.razor | 2 +- app/MindWork AI Studio/Pages/Assistants.razor | 4 +- .../plugin.lua | 350 +++++++++++++-- .../plugin.lua | 346 +++++++++++++-- app/MindWork AI Studio/Program.cs | 2 +- app/MindWork AI Studio/Routes.razor.cs | 3 +- .../ConfigurationSelectDataFactory.cs | 27 +- .../Settings/DataModel/Data.cs | 2 + .../Settings/DataModel/DataSlideBuilder.cs | 62 +++ .../Tools/CommonLanguageExtensions.cs | 8 + app/MindWork AI Studio/Tools/Components.cs | 1 + .../Tools/ComponentsExtensions.cs | 5 + app/MindWork AI Studio/Tools/Event.cs | 1 + .../wwwroot/changelog/v26.3.1.md | 3 +- 37 files changed, 1732 insertions(+), 141 deletions(-) create mode 100644 app/MindWork AI Studio/Assistants/SlideBuilder/AudienceAgeGroup.cs create mode 100644 app/MindWork AI Studio/Assistants/SlideBuilder/AudienceAgeGroupExtensions.cs create mode 100644 app/MindWork AI Studio/Assistants/SlideBuilder/AudienceExpertise.cs create mode 100644 app/MindWork AI Studio/Assistants/SlideBuilder/AudienceExpertiseExtensions.cs create mode 100644 app/MindWork AI Studio/Assistants/SlideBuilder/AudienceOrganizationalLevel.cs create mode 100644 app/MindWork AI Studio/Assistants/SlideBuilder/AudienceOrganizationalLevelExtensions.cs create mode 100644 app/MindWork AI Studio/Assistants/SlideBuilder/AudienceProfile.cs create mode 100644 app/MindWork AI Studio/Assistants/SlideBuilder/AudienceProfileExtensions.cs create mode 100644 app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor create mode 100644 app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor.cs create mode 100644 app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSlideBuilder.razor create mode 100644 app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSlideBuilder.razor.cs create mode 100644 app/MindWork AI Studio/Settings/DataModel/DataSlideBuilder.cs diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor index d1222bff..edc2868e 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor @@ -109,7 +109,7 @@ else - + diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 6473f2e1..f62c031f 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -430,6 +430,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTA -- Load output rules from document UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2168201568"] = "Load output rules from document" +-- Choose whether the policy should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2214900121"] = "Choose whether the policy should use the app default profile, no profile, or a specific profile." + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2322771068"] = "Preselect a profile" + -- The analysis rules specify what the AI should pay particular attention to while reviewing the documents you provide, and which aspects it should highlight or save. For example, if you want to extract the potential of green hydrogen for agriculture from a variety of general publications, you can explicitly define this in the analysis rules. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T238145218"] = "The analysis rules specify what the AI should pay particular attention to while reviewing the documents you provide, and which aspects it should highlight or save. For example, if you want to extract the potential of green hydrogen for agriculture from a variety of general publications, you can explicitly define this in the analysis rules." @@ -1354,6 +1360,189 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::WRITINGSTYLESEXTENSIONS:: -- Marketing (advertisements, sales texts) UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::WRITINGSTYLESEXTENSIONS::T945714286"] = "Marketing (advertisements, sales texts)" +-- Children +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T188567026"] = "Children" + +-- Unspecified age group +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T300604284"] = "Unspecified age group" + +-- Adults +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T3335941460"] = "Adults" + +-- Teenagers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T3696960735"] = "Teenagers" + +-- No expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T1612807521"] = "No expertise" + +-- Intermediate expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T2131860427"] = "Intermediate expertise" + +-- Unspecified expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T2879202483"] = "Unspecified expertise" + +-- Basic expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T2909686714"] = "Basic expertise" + +-- Experts +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T3130182982"] = "Experts" + +-- Individual contributors +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T1893672448"] = "Individual contributors" + +-- Team leads +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2112906979"] = "Team leads" + +-- Board members +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2483400884"] = "Board members" + +-- Unspecified organizational level +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2633679224"] = "Unspecified organizational level" + +-- Trainees +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2678344512"] = "Trainees" + +-- Managers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T3187355853"] = "Managers" + +-- Executives +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T3317335174"] = "Executives" + +-- Journalists +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1589799277"] = "Journalists" + +-- Healthcare professionals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1652521346"] = "Healthcare professionals" + +-- Unspecified audience profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1845793571"] = "Unspecified audience profile" + +-- Lawyers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1959098902"] = "Lawyers" + +-- Investors +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T2516036290"] = "Investors" + +-- Students +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T2905889225"] = "Students" + +-- Scientists +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T332785734"] = "Scientists" + +-- Business professionals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T3670621687"] = "Business professionals" + +-- Engineers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T3904961809"] = "Engineers" + +-- Public officials +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T439009390"] = "Public officials" + +-- Software developers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T831424531"] = "Software developers" + +-- Important Aspects +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1379883528"] = "Important Aspects" + +-- Extent of the planned presentation +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1408740929"] = "Extent of the planned presentation" + +-- You might want to specify important aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1672597841"] = "You might want to specify important aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted." + +-- Details about the desired presentation +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1793579367"] = "Details about the desired presentation" + +-- Text content +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1820253043"] = "Text content" + +-- Slide Assistant +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1883918574"] = "Slide Assistant" + +-- Please provide a text or at least one valid document or image. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2013746884"] = "Please provide a text or at least one valid document or image." + +-- Content to derive slide from +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2086228265"] = "Content to derive slide from" + +-- Target language +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T237828418"] = "Target language" + +-- (Optional) Important Aspects +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T24391765"] = "(Optional) Important Aspects" + +-- Audience age group +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2496533563"] = "Audience age group" + +-- You can enter text, use one or more documents or images, or use both. At least one of these options is required. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2542045947"] = "You can enter text, use one or more documents or images, or use both. At least one of these options is required." + +-- Language +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2591284123"] = "Language" + +-- Audience organizational level +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2599228833"] = "Audience organizational level" + +-- Number of slides +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2823798965"] = "Number of slides" + +-- This assistant helps you create clear, structured slides from long texts or documents. Enter a presentation title and provide the content either as text or with one or more documents. Important aspects allow you to add instructions to the LLM regarding output or formatting. Set the number of slides either directly or based on your desired presentation duration. You can also specify the number of bullet points. If the default value of 0 is not changed, the LLM will independently determine how many slides or bullet points to generate. The output can be flexibly generated in various languages and tailored to a specific audience. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2910177051"] = "This assistant helps you create clear, structured slides from long texts or documents. Enter a presentation title and provide the content either as text or with one or more documents. Important aspects allow you to add instructions to the LLM regarding output or formatting. Set the number of slides either directly or based on your desired presentation duration. You can also specify the number of bullet points. If the default value of 0 is not changed, the LLM will independently determine how many slides or bullet points to generate. The output can be flexibly generated in various languages and tailored to a specific audience." + +-- The result of your previous slide builder session. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3000286990"] = "The result of your previous slide builder session." + +-- Please enter a title for the presentation. This will help the LLM to select more relevant content. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3013824309"] = "Please enter a title for the presentation. This will help the LLM to select more relevant content." + +-- Please provide a title +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3049299559"] = "Please provide a title" + +-- Create Slides +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3079776593"] = "Create Slides" + +-- Please specify the audience for the planned presentation. This will help the LLM to create a presentation that fits your needs. You can specify the audience profile, the age group, organizational level, and the expertise. You don't have to specify all of these parameters, for example you might only want to specify the audience profile and leave the other parameters at their default values. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3465256803"] = "Please specify the audience for the planned presentation. This will help the LLM to create a presentation that fits your needs. You can specify the audience profile, the age group, organizational level, and the expertise. You don't have to specify all of these parameters, for example you might only want to specify the audience profile and leave the other parameters at their default values." + +-- (Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3476149293"] = "(Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted." + +-- Audience +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3506118019"] = "Audience" + +-- Time specification (minutes) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3539067305"] = "Time specification (minutes)" + +-- Audience profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3649769130"] = "Audience profile" + +-- Attach documents +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3666048746"] = "Attach documents" + +-- Number of bullet points +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3796347042"] = "Number of bullet points" + +-- Custom target language +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3848935911"] = "Custom target language" + +-- Presentation title +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3893271035"] = "Presentation title" + +-- {0} - Slide Builder Session +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3922788056"] = "{0} - Slide Builder Session" + +-- Please specify the extent of the planned presentation. This can be the number of slides, the number of bullet points per slide, or the time specification for the presentation. This will help the LLM to create a presentation that fits your needs. Leave the default values if you don't have specific requirements regarding the extent of the presentation. You might only want to specify one of these parameters, for example the time specification, and leave the others at their default values. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T4131419342"] = "Please specify the extent of the planned presentation. This can be the number of slides, the number of bullet points per slide, or the time specification for the presentation. This will help the LLM to create a presentation that fits your needs. Leave the default values if you don't have specific requirements regarding the extent of the presentation. You might only want to specify one of these parameters, for example the time specification, and leave the others at their default values." + +-- Audience expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T4279519256"] = "Audience expertise" + +-- Title +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T617902505"] = "Title" + +-- Please provide a custom language. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T656744944"] = "Please provide a custom language." + -- Your word or phrase UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SYNONYM::ASSISTANTSYNONYMS::T1847246020"] = "Your word or phrase" @@ -3766,6 +3955,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1471770981" -- Preselect whether participants needs to arrive and depart UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1648427207"] = "Preselect whether participants needs to arrive and depart" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- Preselect a start time? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1901151023"] = "Preselect a start time?" @@ -3778,8 +3970,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1998244307" -- Preselect whether the meeting is virtual UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2084951012"] = "Preselect whether the meeting is virtual" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2322771068"] = "Preselect a profile" -- When enabled, you can preselect most agenda options. This is might be useful when you need to create similar agendas often. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2373110543"] = "When enabled, you can preselect most agenda options. This is might be useful when you need to create similar agendas often." @@ -3832,9 +4024,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T3709527588" -- Preselect a topic? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T3835166371"] = "Preselect a topic?" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T4004501229"] = "Preselect one of your profiles?" - -- Preselect the agenda language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T4055846391"] = "Preselect the agenda language" @@ -3871,15 +4060,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1608 -- Yes, you can only retrieve one bias per day UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1765683725"] = "Yes, you can only retrieve one bias per day" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- Reset UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T180921696"] = "Reset" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2221665527"] = "Would you like to preselect one of your profiles?" - -- No restriction. You can retrieve as many biases as you want per day. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2305356277"] = "No restriction. You can retrieve as many biases as you want per day." +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2322771068"] = "Preselect a profile" + -- Which language should be preselected? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2345162613"] = "Which language should be preselected?" @@ -3904,9 +4096,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T3848 -- Options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T3875604319"] = "Options are preselected" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T4004501229"] = "Preselect one of your profiles?" - -- Are you sure you want to reset your bias-of-the-day statistics? The system will no longer remember which biases you already know. As a result, biases you are already familiar with may be addressed again. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T405627382"] = "Are you sure you want to reset your bias-of-the-day statistics? The system will no longer remember which biases you already know. As a result, biases you are already familiar with may be addressed again." @@ -3937,8 +4126,11 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1773585398"] -- Provider selection when creating new chats UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T189306836"] = "Provider selection when creating new chats" --- Would you like to set one of your profiles as the default for chats? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1933521846"] = "Would you like to set one of your profiles as the default for chats?" +-- Choose whether chats should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1915793195"] = "Choose whether chats should use the app default profile, no profile, or a specific profile." + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T2322771068"] = "Preselect a profile" -- Apply default data source option when sending assistant results to chat UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T2510376349"] = "Apply default data source option when sending assistant results to chat" @@ -3976,9 +4168,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T3730599555"] -- Latest message is shown, after loading a chat UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T3755993611"] = "Latest message is shown, after loading a chat" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T4004501229"] = "Preselect one of your profiles?" - -- Do you want to apply the default data source options when sending assistant results to chat? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T4033153439"] = "Do you want to apply the default data source options when sending assistant results to chat?" @@ -4039,11 +4228,14 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083" -- Compiler messages are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1110902070"] = "Compiler messages are preselected" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- Preselect a programming language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2181567002"] = "Preselect a programming language" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2322771068"] = "Preselect a profile" -- When enabled, you can preselect the coding options. This is might be useful when you prefer a specific programming language or LLM model. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2619641701"] = "When enabled, you can preselect the coding options. This is might be useful when you prefer a specific programming language or LLM model." @@ -4060,9 +4252,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T3015105896" -- Coding options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T3567850751"] = "Coding options are preselected" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T4004501229"] = "Preselect one of your profiles?" - -- Preselect another programming language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T4230412334"] = "Preselect another programming language" @@ -4162,14 +4351,17 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T12806662 -- Preselect ERI server options? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1664055662"] = "Preselect ERI server options?" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- No ERI server options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1793785587"] = "No ERI server options are preselected" -- Most ERI server options can be customized and saved directly in the ERI server assistant. For this, the ERI server assistant has an auto-save function. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2093534613"] = "Most ERI server options can be customized and saved directly in the ERI server assistant. For this, the ERI server assistant has an auto-save function." --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2322771068"] = "Preselect a profile" -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T3448155331"] = "Close" @@ -4177,9 +4369,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T34481553 -- Assistant: ERI Server Options UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T3629372826"] = "Assistant: ERI Server Options" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T4004501229"] = "Preselect one of your profiles?" - -- ERI server options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T488190224"] = "ERI server options are preselected" @@ -4327,6 +4516,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1633101 -- Web content reader is not preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1701127912"] = "Web content reader is not preselected" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- Content cleaner agent is not preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1969816694"] = "Content cleaner agent is not preselected" @@ -4336,8 +4528,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2090693 -- When enabled, you can preselect some legal check options. This is might be useful when you prefer a specific LLM model. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2164667361"] = "When enabled, you can preselect some legal check options. This is might be useful when you prefer a specific LLM model." --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2322771068"] = "Preselect a profile" -- Legal check options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T252916114"] = "Legal check options are preselected" @@ -4357,17 +4549,17 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T3641773 -- Preselect the content cleaner agent? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T3649428096"] = "Preselect the content cleaner agent?" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T4004501229"] = "Preselect one of your profiles?" - -- Assistant: Legal Check Options UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T4033382756"] = "Assistant: Legal Check Options" -- Preselect the web content reader? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T629158142"] = "Preselect the web content reader?" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2322771068"] = "Preselect a profile" -- Which language should be preselected? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2345162613"] = "Which language should be preselected?" @@ -4390,9 +4582,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T3710380967 -- Options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T3875604319"] = "Options are preselected" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T4004501229"] = "Preselect one of your profiles?" - -- Preselect options? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T42672465"] = "Preselect options?" @@ -4480,6 +4669,69 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3745021518 -- No rewrite & improve text options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T553954963"] = "No rewrite & improve text options are preselected" +-- Preselect the audience expertise +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1017131030"] = "Preselect the audience expertise" + +-- When enabled, you can preselect slide builder options. This is might be useful when you prefer a specific language or LLM model. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1393378753"] = "When enabled, you can preselect slide builder options. This is might be useful when you prefer a specific language or LLM model." + +-- Preselect aspects for the LLM to focus on when generating slides, such as bullet points or specific topics to emphasize. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1528169602"] = "Preselect aspects for the LLM to focus on when generating slides, such as bullet points or specific topics to emphasize." + +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + +-- Preselect the audience organizational level +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2014662371"] = "Preselect the audience organizational level" + +-- Which audience organizational level should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T216511105"] = "Which audience organizational level should be preselected?" + +-- Preselect Slide Assistant options? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T227645894"] = "Preselect Slide Assistant options?" + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2322771068"] = "Preselect a profile" + +-- Which language should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2345162613"] = "Which language should be preselected?" + +-- Preselect another language +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2382415529"] = "Preselect another language" + +-- Preselect the language +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2571465005"] = "Preselect the language" + +-- Preselect the audience age group +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2645589441"] = "Preselect the audience age group" + +-- Assistant: Slide Assistant Options +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3215549988"] = "Assistant: Slide Assistant Options" + +-- Which audience expertise should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3228597992"] = "Which audience expertise should be preselected?" + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3448155331"] = "Close" + +-- Preselect important aspects +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3705987833"] = "Preselect important aspects" + +-- No Slide Assistant options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T4214398691"] = "No Slide Assistant options are preselected" + +-- Preselect the audience profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T861397972"] = "Preselect the audience profile" + +-- Slide Assistant options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T93124146"] = "Slide Assistant options are preselected" + +-- Which audience age group should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T956845877"] = "Which audience age group should be preselected?" + +-- Which audience profile should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T973572510"] = "Which audience profile should be preselected?" + -- When enabled, you can preselect synonym options. This is might be useful when you prefer a specific language or LLM model. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSYNONYMS::T183953912"] = "When enabled, you can preselect synonym options. This is might be useful when you prefer a specific language or LLM model." @@ -4699,6 +4951,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1417 -- Preselect another target language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1462295644"] = "Preselect another target language" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- Assistant: Writing E-Mails Options UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2021226503"] = "Assistant: Writing E-Mails Options" @@ -4708,8 +4963,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2116 -- Preselect your name for the closing salutation? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T221974240"] = "Preselect your name for the closing salutation?" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2322771068"] = "Preselect a profile" -- Preselect a writing style UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T28456020"] = "Preselect a writing style" @@ -4729,9 +4984,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3547 -- Preselect e-mail options? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3832719342"] = "Preselect e-mail options?" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T4004501229"] = "Preselect one of your profiles?" - -- Save UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1294818664"] = "Save" @@ -4918,6 +5170,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1617786407"] = "Coding" -- Analyze a text or an email for tasks you need to complete. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1728590051"] = "Analyze a text or an email for tasks you need to complete." +-- Slide Assistant +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1883918574"] = "Slide Assistant" + -- Text Summarizer UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1907192403"] = "Text Summarizer" @@ -4957,6 +5212,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3011450657"] = "My Tasks" -- E-Mail UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3026443472"] = "E-Mail" +-- Develop slide content based on a given topic and content. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T311912219"] = "Develop slide content based on a given topic and content." + -- Translate AI Studio text content into other languages UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3181803840"] = "Translate AI Studio text content into other languages" @@ -5689,6 +5947,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3491430707 -- Install updates automatically UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3569059463"] = "Install updates automatically" +-- Use app default profile +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3587225583"] = "Use app default profile" + -- Disable workspaces UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3612390107"] = "Disable workspaces" @@ -5869,6 +6130,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1546040625"] = "My Task -- Grammar & Spelling Assistant UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T166453786"] = "Grammar & Spelling Assistant" +-- Slide Assistant +UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1883918574"] = "Slide Assistant" + -- Legal Check Assistant UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1886447798"] = "Legal Check Assistant" diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceAgeGroup.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceAgeGroup.cs new file mode 100644 index 00000000..cdd893ea --- /dev/null +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceAgeGroup.cs @@ -0,0 +1,10 @@ +namespace AIStudio.Assistants.SlideBuilder; + +public enum AudienceAgeGroup +{ + UNSPECIFIED = 0, + + CHILDREN, + TEENAGERS, + ADULTS, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceAgeGroupExtensions.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceAgeGroupExtensions.cs new file mode 100644 index 00000000..d1f199c7 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceAgeGroupExtensions.cs @@ -0,0 +1,26 @@ +namespace AIStudio.Assistants.SlideBuilder; + +public static class AudienceAgeGroupExtensions +{ + private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceAgeGroupExtensions).Namespace, nameof(AudienceAgeGroupExtensions)); + + public static string Name(this AudienceAgeGroup ageGroup) => ageGroup switch + { + AudienceAgeGroup.UNSPECIFIED => TB("Unspecified age group"), + AudienceAgeGroup.CHILDREN => TB("Children"), + AudienceAgeGroup.TEENAGERS => TB("Teenagers"), + AudienceAgeGroup.ADULTS => TB("Adults"), + + _ => TB("Unspecified age group"), + }; + + public static string Prompt(this AudienceAgeGroup ageGroup) => ageGroup switch + { + AudienceAgeGroup.UNSPECIFIED => "Do not tailor the text to a specific age group.", + AudienceAgeGroup.CHILDREN => "Use simple, concrete language with short sentences and minimal jargon.", + AudienceAgeGroup.TEENAGERS => "Use clear, approachable language with relatable examples and limited jargon.", + AudienceAgeGroup.ADULTS => "Use adult-appropriate language with clear structure and direct explanations.", + + _ => "Do not tailor the text to a specific age group.", + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceExpertise.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceExpertise.cs new file mode 100644 index 00000000..07577783 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceExpertise.cs @@ -0,0 +1,11 @@ +namespace AIStudio.Assistants.SlideBuilder; + +public enum AudienceExpertise +{ + UNSPECIFIED = 0, + + NON_EXPERTS, + BASIC, + INTERMEDIATE, + EXPERTS, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceExpertiseExtensions.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceExpertiseExtensions.cs new file mode 100644 index 00000000..11649d42 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceExpertiseExtensions.cs @@ -0,0 +1,28 @@ +namespace AIStudio.Assistants.SlideBuilder; + +public static class AudienceExpertiseExtensions +{ + private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceExpertiseExtensions).Namespace, nameof(AudienceExpertiseExtensions)); + + public static string Name(this AudienceExpertise expertise) => expertise switch + { + AudienceExpertise.UNSPECIFIED => TB("Unspecified expertise"), + AudienceExpertise.NON_EXPERTS => TB("No expertise"), + AudienceExpertise.BASIC => TB("Basic expertise"), + AudienceExpertise.INTERMEDIATE => TB("Intermediate expertise"), + AudienceExpertise.EXPERTS => TB("Experts"), + + _ => TB("Unspecified expertise"), + }; + + public static string Prompt(this AudienceExpertise expertise) => expertise switch + { + AudienceExpertise.UNSPECIFIED => "Do not tailor the text to a specific expertise level.", + AudienceExpertise.NON_EXPERTS => "Avoid jargon and explain specialized concepts plainly.", + AudienceExpertise.BASIC => "Use simple terminology and briefly explain important technical terms.", + AudienceExpertise.INTERMEDIATE => "Assume some familiarity with the topic, but still explain important details clearly.", + AudienceExpertise.EXPERTS => "Assume deep familiarity with the topic and use precise domain-specific terminology.", + + _ => "Do not tailor the text to a specific expertise level.", + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceOrganizationalLevel.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceOrganizationalLevel.cs new file mode 100644 index 00000000..f0228fa5 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceOrganizationalLevel.cs @@ -0,0 +1,13 @@ +namespace AIStudio.Assistants.SlideBuilder; + +public enum AudienceOrganizationalLevel +{ + UNSPECIFIED = 0, + + TRAINEES, + INDIVIDUAL_CONTRIBUTORS, + TEAM_LEADS, + MANAGERS, + EXECUTIVES, + BOARD_MEMBERS, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceOrganizationalLevelExtensions.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceOrganizationalLevelExtensions.cs new file mode 100644 index 00000000..68bfed30 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceOrganizationalLevelExtensions.cs @@ -0,0 +1,32 @@ +namespace AIStudio.Assistants.SlideBuilder; + +public static class AudienceOrganizationalLevelExtensions +{ + private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceOrganizationalLevelExtensions).Namespace, nameof(AudienceOrganizationalLevelExtensions)); + + public static string Name(this AudienceOrganizationalLevel level) => level switch + { + AudienceOrganizationalLevel.UNSPECIFIED => TB("Unspecified organizational level"), + AudienceOrganizationalLevel.TRAINEES => TB("Trainees"), + AudienceOrganizationalLevel.INDIVIDUAL_CONTRIBUTORS => TB("Individual contributors"), + AudienceOrganizationalLevel.TEAM_LEADS => TB("Team leads"), + AudienceOrganizationalLevel.MANAGERS => TB("Managers"), + AudienceOrganizationalLevel.EXECUTIVES => TB("Executives"), + AudienceOrganizationalLevel.BOARD_MEMBERS => TB("Board members"), + + _ => TB("Unspecified organizational level"), + }; + + public static string Prompt(this AudienceOrganizationalLevel level) => level switch + { + AudienceOrganizationalLevel.UNSPECIFIED => "Do not tailor the text to a specific organizational level.", + AudienceOrganizationalLevel.TRAINEES => "Keep the content supportive and introductory. Explain context and avoid assuming prior organizational knowledge.", + AudienceOrganizationalLevel.INDIVIDUAL_CONTRIBUTORS => "Focus on execution, clarity, responsibilities, and practical next steps.", + AudienceOrganizationalLevel.TEAM_LEADS => "Focus on coordination, tradeoffs, risks, and concrete actions for a small team.", + AudienceOrganizationalLevel.MANAGERS => "Focus on planning, priorities, outcomes, risks, and resource implications.", + AudienceOrganizationalLevel.EXECUTIVES => "Focus on strategy, business impact, risks, and the decisions required.", + AudienceOrganizationalLevel.BOARD_MEMBERS => "Provide a concise executive-level summary with governance, strategy, risk, and decision relevance.", + + _ => "Do not tailor the text to a specific organizational level.", + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceProfile.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceProfile.cs new file mode 100644 index 00000000..d4196d60 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceProfile.cs @@ -0,0 +1,17 @@ +namespace AIStudio.Assistants.SlideBuilder; + +public enum AudienceProfile +{ + UNSPECIFIED = 0, + + STUDENTS, + SCIENTISTS, + LAWYERS, + INVESTORS, + ENGINEERS, + SOFTWARE_DEVELOPERS, + JOURNALISTS, + HEALTHCARE_PROFESSIONALS, + PUBLIC_OFFICIALS, + BUSINESS_PROFESSIONALS, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceProfileExtensions.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceProfileExtensions.cs new file mode 100644 index 00000000..67fb3827 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/AudienceProfileExtensions.cs @@ -0,0 +1,40 @@ +namespace AIStudio.Assistants.SlideBuilder; + +public static class AudienceProfileExtensions +{ + private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceProfileExtensions).Namespace, nameof(AudienceProfileExtensions)); + + public static string Name(this AudienceProfile profile) => profile switch + { + AudienceProfile.UNSPECIFIED => TB("Unspecified audience profile"), + AudienceProfile.STUDENTS => TB("Students"), + AudienceProfile.SCIENTISTS => TB("Scientists"), + AudienceProfile.LAWYERS => TB("Lawyers"), + AudienceProfile.INVESTORS => TB("Investors"), + AudienceProfile.ENGINEERS => TB("Engineers"), + AudienceProfile.SOFTWARE_DEVELOPERS => TB("Software developers"), + AudienceProfile.JOURNALISTS => TB("Journalists"), + AudienceProfile.HEALTHCARE_PROFESSIONALS => TB("Healthcare professionals"), + AudienceProfile.PUBLIC_OFFICIALS => TB("Public officials"), + AudienceProfile.BUSINESS_PROFESSIONALS => TB("Business professionals"), + + _ => TB("Unspecified audience profile"), + }; + + public static string Prompt(this AudienceProfile profile) => profile switch + { + AudienceProfile.UNSPECIFIED => "Do not tailor the text to a specific audience profile.", + AudienceProfile.STUDENTS => "Write for students. Keep it structured, easy to study, and focused on key takeaways.", + AudienceProfile.SCIENTISTS => "Use precise, technical language. Structure the content logically and focus on methods, evidence, and results.", + AudienceProfile.LAWYERS => "Write with precise wording. Emphasize definitions, implications, compliance, and risks.", + AudienceProfile.INVESTORS => "Focus on market potential, business model, differentiation, traction, risks, and financial upside.", + AudienceProfile.ENGINEERS => "Be technically precise and practical. Focus on systems, constraints, implementation, and tradeoffs.", + AudienceProfile.SOFTWARE_DEVELOPERS => "Use concise technical language. Focus on architecture, implementation details, tradeoffs, and maintainability.", + AudienceProfile.JOURNALISTS => "Be clear, factual, and concise. Highlight the most newsworthy points and explain relevance plainly.", + AudienceProfile.HEALTHCARE_PROFESSIONALS => "Use accurate professional language. Emphasize outcomes, safety, evidence, and practical implications.", + AudienceProfile.PUBLIC_OFFICIALS => "Focus on public impact, feasibility, budget, compliance, risks, and implementation in a neutral institutional tone.", + AudienceProfile.BUSINESS_PROFESSIONALS => "Be clear, practical, and concise. Focus on business relevance, decisions, and next steps.", + + _ => "Do not tailor the text to a specific audience profile.", + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor b/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor new file mode 100644 index 00000000..55b6a781 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor @@ -0,0 +1,69 @@ +@attribute [Route(Routes.ASSISTANT_SLIDE_BUILDER)] +@inherits AssistantBaseCore + + @T("Content to derive slide from") + + @T("You can enter text, use one or more documents or images, or use both. At least one of these options is required.") + + + + @T("Attach documents") + + + @T("Details about the desired presentation") + + @T("Title") + + @T("Please enter a title for the presentation. This will help the LLM to select more relevant content.") + + + + @T("Important Aspects") + + @T("You might want to specify important aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted.") + + + + @T("Extent of the planned presentation") + + @T("Please specify the extent of the planned presentation. This can be the number of slides, the number of bullet points per slide, or the time specification for the presentation. This will help the LLM to create a presentation that fits your needs. Leave the default values if you don't have specific requirements regarding the extent of the presentation. You might only want to specify one of these parameters, for example the time specification, and leave the others at their default values.") + + + + + + + + + + + + + + + + @T("Language") + + + @T("Audience") + + @T("Please specify the audience for the planned presentation. This will help the LLM to create a presentation that fits your needs. You can specify the audience profile, the age group, organizational level, and the expertise. You don't have to specify all of these parameters, for example you might only want to specify the audience profile and leave the other parameters at their default values.") + + + + + + diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor.cs new file mode 100644 index 00000000..e01320d5 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor.cs @@ -0,0 +1,408 @@ +using System.Text; +using AIStudio.Chat; +using AIStudio.Dialogs.Settings; + +namespace AIStudio.Assistants.SlideBuilder; + +public partial class SlideAssistant : AssistantBaseCore +{ + protected override Tools.Components Component => Tools.Components.SLIDE_BUILDER_ASSISTANT; + + protected override string Title => T("Slide Assistant"); + + protected override string Description => T("This assistant helps you create clear, structured slides from long texts or documents. Enter a presentation title and provide the content either as text or with one or more documents. Important aspects allow you to add instructions to the LLM regarding output or formatting. Set the number of slides either directly or based on your desired presentation duration. You can also specify the number of bullet points. If the default value of 0 is not changed, the LLM will independently determine how many slides or bullet points to generate. The output can be flexibly generated in various languages and tailored to a specific audience."); + + protected override string SystemPrompt => + $$$""" + You are a professional presentation editor and writer. + Create a clear, single-slide outline from the user's inputs. + + # Presentation title: + - IGNORE the language of the PRESENTATION_TITLE. + - Translate PRESENTATION_TITLE in: {{{this.selectedTargetLanguage.PromptGeneralPurpose(this.customTargetLanguage)}}} + + # Content + - You get the following inputs: PRESENTATION_TITLE, PRESENTATION_CONTENT, and any attached documents that may provide additional context or source material (DOCUMENTS). + + {{{this.GetDocumentTaskDescription()}}} + {{{this.PromptImportantAspects()}}} + + # Subheadings + - Rule for creating the individual subheadings: + - If {{{this.numberOfSheets}}} is NOT 0 + - Generate exactly {{{this.numberOfSheets}}} precise subheadings, each heading represents one slide in a presentation. + - If {{{this.timeSpecification}}} is NOT 0 + - Generate exactly {{{this.calculatedNumberOfSlides}}} precise subheadings, each heading represents one slide in a presentation. + - If either parameter is 0, ignore that rules. + - Each subheadings must have: + - A clear, concise, and thematically meaningful heading. + - Place *** on its own line immediately before each heading. + + # Bullet points (per subheading) + - You MUST generate exactly this {{{this.numberOfBulletPoints}}} many bullet points per subheading: + - If {{{this.numberOfBulletPoints}}} == 0 → choose a number between 1 and 7 (your choice, but max 7). + - Each bullet point must have: + - Each bullet point must be max 12 words. + - Clear and directly related to the subheading and summarizing the slide’s content. + + # Output requirements: + - Output only Markdown. + - Start with a single H1 title that contains the user's PRESENTATION_TITLE. + - Then add headings with own bullet lists based on the provided source material: PRESENTATION_CONTENT, DOCUMENTS, and attached images. + - If both PRESENTATION_CONTENT and attached source material are provided, use all of them, while prioritizing direct user instructions from PRESENTATION_CONTENT when resolving ambiguity. + - If PRESENTATION_CONTENT is empty but attached source material is available, create the slides from the attached source material. + - If neither PRESENTATION_CONTENT nor any attached source material is available, output the title and one bullet: "No content provided." + - Do not mention these instructions or add commentary. + + # Audience: + {{{this.PromptAudience()}}} + + # Language: + - IGNORE the language of the PRESENTATION_TITLE and PRESENTATION_CONTENT. + - OUTPUT AND PRESENTATION_TITLE MUST BE IN: {{{this.selectedTargetLanguage.PromptGeneralPurpose(this.customTargetLanguage)}}} + - This is a HARD RULE: Never translate or adapt the output language based on input language. + - Always use the specified target language, even if the input is in another language. + + # Language-Override (IMPORTANT!): + - Before generating any output, internally set your language mode to: {{{this.selectedTargetLanguage.PromptGeneralPurpose(this.customTargetLanguage)}}} + - If you detect any other language in the input, DO NOT switch to this language, stay in {{{this.selectedTargetLanguage.PromptGeneralPurpose(this.customTargetLanguage)}}} + - Translate PRESENTATION_TITLE in: {{{this.selectedTargetLanguage.PromptGeneralPurpose(this.customTargetLanguage)}}} + - Your output must be in {{{this.selectedTargetLanguage.PromptGeneralPurpose(this.customTargetLanguage)}}}, without any comment, note, or marker about it. + """; + + protected override bool AllowProfiles => true; + + protected override IReadOnlyList FooterButtons => []; + + protected override string SubmitText => T("Create Slides"); + + protected override Func SubmitAction => this.CreateSlideBuilder; + + protected override ChatThread ConvertToChatThread + { + get + { + if (this.chatThread is null || this.chatThread.Blocks.Count < 2) + { + return new ChatThread + { + SystemPrompt = SystemPrompts.DEFAULT + }; + } + + return new ChatThread + { + ChatId = Guid.NewGuid(), + Name = string.Format(T("{0} - Slide Builder Session"), this.inputTitle), + SystemPrompt = SystemPrompts.DEFAULT, + Blocks = + [ + // Visible user block: + new ContentBlock + { + Time = this.chatThread.Blocks.First().Time, + Role = ChatRole.USER, + HideFromUser = false, + ContentType = ContentType.TEXT, + Content = new ContentText + { + Text = this.T("The result of your previous slide builder session."), + FileAttachments = this.loadedDocumentPaths.ToList(), + } + }, + + // Hidden user block with inputContent data: + new ContentBlock + { + Time = this.chatThread.Blocks.First().Time, + Role = ChatRole.USER, + HideFromUser = true, + ContentType = ContentType.TEXT, + Content = new ContentText + { + Text = string.IsNullOrWhiteSpace(this.inputContent) + ? $""" + # PRESENTATION_TITLE + ``` + {this.inputTitle} + ``` + """ + + : $""" + # PRESENTATION_TITLE + ``` + {this.inputTitle} + ``` + + # PRESENTATION_CONTENT + ``` + {this.inputContent} + ``` + """, + } + }, + + // Then, append the last block of the current chat thread + // (which is expected to be the AI response): + this.chatThread.Blocks.Last(), + ] + }; + } + } + + protected override void ResetForm() + { + this.inputTitle = string.Empty; + this.inputContent = string.Empty; + this.loadedDocumentPaths.Clear(); + this.selectedAudienceProfile = AudienceProfile.UNSPECIFIED; + this.selectedAudienceAgeGroup = AudienceAgeGroup.UNSPECIFIED; + this.selectedAudienceOrganizationalLevel = AudienceOrganizationalLevel.UNSPECIFIED; + this.selectedAudienceExpertise = AudienceExpertise.UNSPECIFIED; + if (!this.MightPreselectValues()) + { + this.selectedTargetLanguage = CommonLanguages.AS_IS; + this.customTargetLanguage = string.Empty; + } + } + + protected override bool MightPreselectValues() + { + if (this.SettingsManager.ConfigurationData.SlideBuilder.PreselectOptions) + { + this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedTargetLanguage; + this.customTargetLanguage = this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedOtherLanguage; + this.selectedAudienceProfile = this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedAudienceProfile; + this.selectedAudienceAgeGroup = this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedAudienceAgeGroup; + this.selectedAudienceOrganizationalLevel = this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedAudienceOrganizationalLevel; + this.selectedAudienceExpertise = this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedAudienceExpertise; + this.importantAspects = this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedImportantAspects; + return true; + } + + return false; + } + + private string inputTitle = string.Empty; + private string inputContent = string.Empty; + private string customTargetLanguage = string.Empty; + private AudienceProfile selectedAudienceProfile; + private AudienceAgeGroup selectedAudienceAgeGroup; + private AudienceOrganizationalLevel selectedAudienceOrganizationalLevel; + private AudienceExpertise selectedAudienceExpertise; + private CommonLanguages selectedTargetLanguage; + private int numberOfSheets; + private int numberOfBulletPoints; + private int timeSpecification; + private int calculatedNumberOfSlides; + private string importantAspects = string.Empty; + private HashSet loadedDocumentPaths = []; + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages(Event.SEND_TO_SLIDE_BUILDER_ASSISTANT).FirstOrDefault(); + if (deferredContent is not null) + this.inputContent = deferredContent; + + await base.OnInitializedAsync(); + } + + #endregion + + private string? ValidatingTitle(string text) + { + if(string.IsNullOrWhiteSpace(text)) + return T("Please provide a title"); + + return null; + } + private string? ValidatingContext(string text) + { + if(string.IsNullOrWhiteSpace(text) && !this.HasValidInputDocuments()) + return T("Please provide a text or at least one valid document or image."); + + return null; + } + + private bool HasValidInputDocuments() => this.loadedDocumentPaths.Any(n => n is { Exists: true }); + + private async Task OnDocumentsChanged(HashSet _) + { + if(this.form is not null) + await this.form.Validate(); + } + + private string? ValidateCustomLanguage(string language) + { + if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language)) + return T("Please provide a custom language."); + + return null; + } + + private int CalculateNumberOfSlides() + { + return this.calculatedNumberOfSlides = (int)Math.Round(this.timeSpecification / 1.5); + } + + private string PromptImportantAspects() + { + if (string.IsNullOrWhiteSpace(this.importantAspects)) + return string.Empty; + + return $""" + + # Important aspects + Emphasize the following aspects in your presentation: + {this.importantAspects} + """; + } + + private string PromptAudience() + { + var prompts = new List(); + + if (this.selectedAudienceProfile is not AudienceProfile.UNSPECIFIED) + prompts.Add(this.selectedAudienceProfile.Prompt()); + + if (this.selectedAudienceAgeGroup is not AudienceAgeGroup.UNSPECIFIED) + prompts.Add(this.selectedAudienceAgeGroup.Prompt()); + + if (this.selectedAudienceOrganizationalLevel is not AudienceOrganizationalLevel.UNSPECIFIED) + prompts.Add(this.selectedAudienceOrganizationalLevel.Prompt()); + + if (this.selectedAudienceExpertise is not AudienceExpertise.UNSPECIFIED) + prompts.Add(this.selectedAudienceExpertise.Prompt()); + + if (prompts.Count == 0) + return " - Do not tailor the text to a specific audience."; + + return string.Join(Environment.NewLine, prompts.Select(prompt => $" - {prompt}")); + } + + private string GetDocumentTaskDescription() + { + var numDocuments = this.loadedDocumentPaths.Count(x => x is { Exists: true, IsImage: false }); + var numImages = this.loadedDocumentPaths.Count(x => x is { Exists: true, IsImage: true }); + + return (numDocuments, numImages) switch + { + (0, 1) => "Your task is to analyze a single image file attached as a document.", + (0, > 1) => $"Your task is to analyze {numImages} image file(s) attached as documents.", + + (1, 0) => "Your task is to analyze a single DOCUMENT.", + (1, 1) => "Your task is to analyze a single DOCUMENT and 1 image file attached as a document.", + (1, > 1) => $"Your task is to analyze a single DOCUMENT and {numImages} image file(s) attached as documents.", + + (> 0, 0) => $"Your task is to analyze {numDocuments} DOCUMENTS. Different DOCUMENTS are divided by a horizontal rule in markdown formatting followed by the name of the document.", + (> 0, 1) => $"Your task is to analyze {numDocuments} DOCUMENTS and 1 image file attached as a document. Different DOCUMENTS are divided by a horizontal rule in Markdown formatting followed by the name of the document.", + (> 0, > 0) => $"Your task is to analyze {numDocuments} DOCUMENTS and {numImages} image file(s) attached as documents. Different DOCUMENTS are divided by a horizontal rule in Markdown formatting followed by the name of the document.", + + _ => "Your task is to analyze a single DOCUMENT." + }; + } + + private async Task PromptLoadDocumentsContent() + { + if (this.loadedDocumentPaths.Count == 0) + return string.Empty; + + var documents = this.loadedDocumentPaths.Where(n => n is { Exists: true, IsImage: false }).ToList(); + var sb = new StringBuilder(); + + if (documents.Count > 0) + { + sb.AppendLine(""" + # DOCUMENTS: + + """); + } + + var numDocuments = 1; + foreach (var document in documents) + { + if (document.IsForbidden) + { + this.Logger.LogWarning($"Skipping forbidden file: '{document.FilePath}'."); + continue; + } + + var fileContent = await this.RustService.ReadArbitraryFileData(document.FilePath, int.MaxValue); + sb.AppendLine($""" + + ## DOCUMENT {numDocuments}: + File path: {document.FilePath} + Content: + ``` + {fileContent} + ``` + + --- + + """); + numDocuments++; + } + + var numImages = this.loadedDocumentPaths.Count(x => x is { IsImage: true, Exists: true }); + if (numImages > 0) + { + if (documents.Count == 0) + { + sb.AppendLine($""" + + There are {numImages} image file(s) attached as documents. + Please consider them as documents as well and use them to + answer accordingly. + + """); + } + else + { + sb.AppendLine($""" + + Additionally, there are {numImages} image file(s) attached. + Please consider them as documents as well and use them to + answer accordingly. + + """); + } + } + + return sb.ToString(); + } + + private async Task CreateSlideBuilder() + { + await this.form!.Validate(); + if (!this.inputIsValid) + return; + + this.calculatedNumberOfSlides = this.timeSpecification > 0 ? this.CalculateNumberOfSlides() : 0; + + this.CreateChatThread(); + var documentContent = await this.PromptLoadDocumentsContent(); + var imageAttachments = this.loadedDocumentPaths.Where(n => n is { Exists: true, IsImage: true }).ToList(); + + var time = this.AddUserRequest( + $""" + # PRESENTATION_TITLE + ``` + {this.inputTitle} + ``` + + # PRESENTATION_CONTENT + + ``` + {this.inputContent} + ``` + + {documentContent} + """, + hideContentFromUser: true, + imageAttachments); + + await this.AddAIResponseAsync(time); + } +} diff --git a/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs b/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs index 35dee799..b52d8549 100644 --- a/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs +++ b/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs @@ -138,7 +138,8 @@ public partial class AssistantTextSummarizer : AssistantBaseCore } - @if (!this.ChatThread.IsLLMProviderAllowed(this.Provider)) { diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAgenda.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAgenda.razor index a7b83b0a..dcaf18ff 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAgenda.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAgenda.razor @@ -34,7 +34,7 @@ } - + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor index 2902c7a6..40f3331f 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor @@ -27,7 +27,7 @@ { } - + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor index aa519781..d9ed5a90 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor @@ -18,7 +18,7 @@ - + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor index 818dda3c..6cfed1ac 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor @@ -20,7 +20,7 @@ } - + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor index 831cbce1..9f0e2272 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor @@ -13,7 +13,7 @@ - + @T("Most ERI server options can be customized and saved directly in the ERI server assistant. For this, the ERI server assistant has an auto-save function.") diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor index d15662e7..71947b14 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor @@ -15,7 +15,7 @@ - + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor index 827ad220..1fed1f08 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor @@ -16,7 +16,7 @@ { } - + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSlideBuilder.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSlideBuilder.razor new file mode 100644 index 00000000..1c8dad1f --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSlideBuilder.razor @@ -0,0 +1,34 @@ +@using AIStudio.Settings +@inherits SettingsDialogBase + + + + + + @T("Assistant: Slide Assistant Options") + + + + + + + + @if (this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedTargetLanguage is CommonLanguages.OTHER) + { + + } + + + + + + + + + + + + @T("Close") + + + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSlideBuilder.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSlideBuilder.razor.cs new file mode 100644 index 00000000..e6f093f8 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSlideBuilder.razor.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Dialogs.Settings; + +public partial class SettingsDialogSlideBuilder : SettingsDialogBase; \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor index 904635d2..ff96ced6 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor @@ -21,7 +21,7 @@ - + diff --git a/app/MindWork AI Studio/Pages/Assistants.razor b/app/MindWork AI Studio/Pages/Assistants.razor index 610a68cd..8e4896dc 100644 --- a/app/MindWork AI Studio/Pages/Assistants.razor +++ b/app/MindWork AI Studio/Pages/Assistants.razor @@ -37,7 +37,8 @@ (Components.AGENDA_ASSISTANT, PreviewFeatures.NONE), (Components.JOB_POSTING_ASSISTANT, PreviewFeatures.NONE), (Components.LEGAL_CHECK_ASSISTANT, PreviewFeatures.NONE), - (Components.ICON_FINDER_ASSISTANT, PreviewFeatures.NONE) + (Components.ICON_FINDER_ASSISTANT, PreviewFeatures.NONE), + (Components.SLIDE_BUILDER_ASSISTANT, PreviewFeatures.NONE) )) { @@ -51,6 +52,7 @@ + } diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index f943abbf..5af28bf7 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -432,6 +432,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTA -- Load output rules from document UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2168201568"] = "Regeln für die Ausgabe aus einem Dokument laden" +-- Choose whether the policy should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2214900121"] = "Wählen Sie, ob das Regelwerk das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2322771068"] = "Profil vorauswählen" + -- The analysis rules specify what the AI should pay particular attention to while reviewing the documents you provide, and which aspects it should highlight or save. For example, if you want to extract the potential of green hydrogen for agriculture from a variety of general publications, you can explicitly define this in the analysis rules. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T238145218"] = "Die Analyseregeln legen fest, worauf die KI bei der Prüfung der von Ihnen bereitgestellten Dokumente besonders achten und welche Aspekte sie hervorheben oder speichern soll. Wenn Sie beispielsweise das Potenzial von grünem Wasserstoff für die Landwirtschaft aus einer Vielzahl allgemeiner Publikationen extrahieren möchten, können Sie dies in den Analyseregeln explizit definieren." @@ -1276,7 +1282,7 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3646084045"] UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3848935911"] = "Benutzerdefinierte Zielsprache" -- Please select one of your profiles. -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T465395981"] = "Bitte wählen Sie eines ihrer Profile aus." +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T465395981"] = "Bitte wählen Sie eines Ihrer Profile aus." -- Text or email UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T534887559"] = "Text oder E-Mail" @@ -1356,6 +1362,189 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::WRITINGSTYLESEXTENSIONS:: -- Marketing (advertisements, sales texts) UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::WRITINGSTYLESEXTENSIONS::T945714286"] = "Marketing (Werbung, Verkaufstexte)" +-- Children +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T188567026"] = "Kinder" + +-- Unspecified age group +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T300604284"] = "Nicht angegebene Altersgruppe" + +-- Adults +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T3335941460"] = "Erwachsene" + +-- Teenagers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T3696960735"] = "Jugendliche" + +-- No expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T1612807521"] = "Keine Vorkenntnisse" + +-- Intermediate expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T2131860427"] = "Mittlere Kenntnisse" + +-- Unspecified expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T2879202483"] = "Nicht spezifizierte Fachkenntnisse" + +-- Basic expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T2909686714"] = "Grundlegende Kenntnisse" + +-- Experts +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T3130182982"] = "Experten" + +-- Individual contributors +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T1893672448"] = "Einzelne Mitwirkende" + +-- Team leads +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2112906979"] = "Teamleitung" + +-- Board members +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2483400884"] = "Vorstand" + +-- Unspecified organizational level +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2633679224"] = "Nicht angegebene Organisationsebene" + +-- Trainees +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2678344512"] = "Einstieg" + +-- Managers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T3187355853"] = "Management" + +-- Executives +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T3317335174"] = "Führung" + +-- Journalists +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1589799277"] = "Journalisten" + +-- Healthcare professionals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1652521346"] = "Medizinisches Fachpersonal" + +-- Unspecified audience profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1845793571"] = "Nicht angegebenes Zielgruppenprofil" + +-- Lawyers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1959098902"] = "Anwälte" + +-- Investors +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T2516036290"] = "Investoren" + +-- Students +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T2905889225"] = "Studierende" + +-- Scientists +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T332785734"] = "Wissenschaftler" + +-- Business professionals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T3670621687"] = "Business Professionals" + +-- Engineers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T3904961809"] = "Ingenieure" + +-- Public officials +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T439009390"] = "Amtsträger" + +-- Software developers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T831424531"] = "Softwareentwickler" + +-- Important Aspects +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1379883528"] = "Wichtige Aspekte" + +-- Extent of the planned presentation +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1408740929"] = "Umfang der geplanten Präsentation" + +-- You might want to specify important aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1672597841"] = "Vielleicht möchten Sie wichtige Aspekte angeben, die das LLM bei der Erstellung der Folien berücksichtigen soll. Zum Beispiel die Verwendung von Emojis oder bestimmte Themen, die hervorgehoben werden sollen." + +-- Details about the desired presentation +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1793579367"] = "Details zur gewünschten Präsentation" + +-- Text content +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1820253043"] = "Textinhalt" + +-- Slide Assistant +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1883918574"] = "Folienassistent" + +-- Please provide a text or at least one valid document or image. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2013746884"] = "Bitte geben Sie einen Text oder mindestens ein gültiges Dokument oder Bild an." + +-- Content to derive slide from +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2086228265"] = "Inhalt, aus dem die Folie erstellt werden soll" + +-- Target language +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T237828418"] = "Zielsprache" + +-- (Optional) Important Aspects +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T24391765"] = "(Optional) Wichtige Aspekte" + +-- Audience age group +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2496533563"] = "Altersgruppe" + +-- You can enter text, use one or more documents or images, or use both. At least one of these options is required. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2542045947"] = "Sie können Text eingeben, ein oder mehrere Dokumente oder Bilder verwenden oder beides nutzen. Mindestens eine dieser Optionen ist erforderlich." + +-- Language +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2591284123"] = "Sprache" + +-- Audience organizational level +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2599228833"] = "Organisatorische Ebene der Zielgruppe" + +-- Number of slides +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2823798965"] = "Anzahl der Folien" + +-- This assistant helps you create clear, structured slides from long texts or documents. Enter a presentation title and provide the content either as text or with one or more documents. Important aspects allow you to add instructions to the LLM regarding output or formatting. Set the number of slides either directly or based on your desired presentation duration. You can also specify the number of bullet points. If the default value of 0 is not changed, the LLM will independently determine how many slides or bullet points to generate. The output can be flexibly generated in various languages and tailored to a specific audience. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2910177051"] = "Dieser Assistent hilft Ihnen, aus langen Texten oder Dokumenten klare, strukturierte Folien zu erstellen. Geben Sie einen Titel für die Präsentation ein und stellen Sie den Inhalt entweder als Text oder über ein oder mehrere Dokumente bereit. Unter „Wichtige Aspekte“ können Sie dem LLM Anweisungen zur Ausgabe oder Formatierung geben. Legen Sie die Anzahl der Folien entweder direkt oder anhand der gewünschten Präsentationsdauer fest. Sie können auch die Anzahl der Aufzählungspunkte angeben. Wenn der Standardwert 0 nicht geändert wird, bestimmt das LLM selbstständig, wie viele Folien oder Aufzählungspunkte erstellt werden. Die Ausgabe kann flexibel in verschiedenen Sprachen erzeugt und auf eine bestimmte Zielgruppe zugeschnitten werden." + +-- The result of your previous slide builder session. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3000286990"] = "Das Ergebnis Ihrer vorherigen Folienassistent-Sitzung." + +-- Please enter a title for the presentation. This will help the LLM to select more relevant content. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3013824309"] = "Bitte geben Sie einen Titel für die Präsentation ein. Dies hilft dem LLM, relevantere Inhalte auszuwählen." + +-- Please provide a title +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3049299559"] = "Bitte geben Sie einen Titel ein" + +-- Create Slides +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3079776593"] = "Folien erstellen" + +-- Please specify the audience for the planned presentation. This will help the LLM to create a presentation that fits your needs. You can specify the audience profile, the age group, organizational level, and the expertise. You don't have to specify all of these parameters, for example you might only want to specify the audience profile and leave the other parameters at their default values. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3465256803"] = "Bitte geben Sie die Zielgruppe für die geplante Präsentation an. So kann das LLM eine Präsentation erstellen, die zu Ihren Anforderungen passt. Sie können das Profil der Zielgruppe, die Altersgruppe, die Organisationsebene und die Fachkenntnisse angeben. Sie müssen nicht alle diese Parameter festlegen; Sie können zum Beispiel nur das Profil der Zielgruppe angeben und die anderen Parameter bei ihren Standardwerten belassen." + +-- (Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3476149293"] = "(Optional) Gib Aspekte an, die das LLM beim Erstellen der Folien berücksichtigen soll. Zum Beispiel die Verwendung von Emojis oder bestimmte Themen, die hervorgehoben werden sollen." + +-- Audience +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3506118019"] = "Zielgruppe" + +-- Time specification (minutes) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3539067305"] = "Zeitangabe (Minuten)" + +-- Audience profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3649769130"] = "Profil der Zielgruppe" + +-- Attach documents +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3666048746"] = "Dokumente anhängen" + +-- Number of bullet points +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3796347042"] = "Anzahl der Aufzählungspunkte" + +-- Custom target language +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3848935911"] = "Benutzerdefinierte Zielsprache" + +-- Presentation title +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3893271035"] = "Präsentationstitel" + +-- {0} - Slide Builder Session +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3922788056"] = "{0} – Folienassistent-Sitzung" + +-- Please specify the extent of the planned presentation. This can be the number of slides, the number of bullet points per slide, or the time specification for the presentation. This will help the LLM to create a presentation that fits your needs. Leave the default values if you don't have specific requirements regarding the extent of the presentation. You might only want to specify one of these parameters, for example the time specification, and leave the others at their default values. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T4131419342"] = "Bitte geben Sie den Umfang der geplanten Präsentation an. Das kann die Anzahl der Folien, die Anzahl der Aufzählungspunkte pro Folie oder die Zeitvorgabe für die Präsentation sein. Das hilft dem LLM, eine Präsentation zu erstellen, die Ihren Anforderungen entspricht. Lassen Sie die Standardwerte unverändert, wenn Sie keine besonderen Anforderungen an den Umfang der Präsentation haben. Möglicherweise möchten Sie nur einen dieser Parameter angeben, zum Beispiel die Zeitvorgabe, und die anderen auf den Standardwerten belassen." + +-- Audience expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T4279519256"] = "Fachkenntnisse der Zielgruppe" + +-- Title +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T617902505"] = "Titel" + +-- Please provide a custom language. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T656744944"] = "Bitte geben Sie eine benutzerdefinierte Sprache an." + -- Your word or phrase UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SYNONYM::ASSISTANTSYNONYMS::T1847246020"] = "Ihr Wort oder Phrase" @@ -1897,7 +2086,7 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PREVIEWRELEASECANDIDATE::T3451939995"] = UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PREVIEWRELEASECANDIDATE::T696585888"] = "Release-Kandidaten sind der letzte Schritt, bevor eine Funktion als stabil gilt." -- Select one of your profiles -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEFORMSELECTION::T2003449133"] = "Wählen Sie eines ihrer Profile aus" +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEFORMSELECTION::T2003449133"] = "Wählen Sie eines Ihrer Profile aus" -- Open Profile Options UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEFORMSELECTION::T3654011106"] = "Profil-Optionen öffnen" @@ -3768,6 +3957,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1471770981" -- Preselect whether participants needs to arrive and depart UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1648427207"] = "Legen Sie im Voraus fest, ob Teilnehmer anreisen und abreisen müssen" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1766361623"] = "Wählen Sie, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + -- Preselect a start time? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1901151023"] = "Startzeit vorauswählen?" @@ -3780,8 +3972,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1998244307" -- Preselect whether the meeting is virtual UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2084951012"] = "Wählen Sie aus, ob das Meeting virtuell ist" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2221665527"] = "Möchten Sie eines ihrer Profile vorauswählen?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2322771068"] = "Profil vorauswählen" -- When enabled, you can preselect most agenda options. This is might be useful when you need to create similar agendas often. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2373110543"] = "Wenn diese Option aktiviert ist, können Sie die meisten Agendapunkte vorauswählen. Das kann hilfreich sein, wenn Sie häufig ähnliche Agenden erstellst." @@ -3834,9 +4026,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T3709527588" -- Preselect a topic? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T3835166371"] = "Ein Thema vorauswählen?" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T4004501229"] = "Eines ihrer Profile vorauswählen?" - -- Preselect the agenda language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T4055846391"] = "Wählen Sie die Sprache der Agenda vorab aus" @@ -3873,15 +4062,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1608 -- Yes, you can only retrieve one bias per day UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1765683725"] = "Ja, Sie können nur einmal pro Tag eine Voreingenommenheit abrufen." +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1766361623"] = "Wähle aus, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + -- Reset UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T180921696"] = "Zurücksetzen" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2221665527"] = "Möchten Sie eines ihrer Profile vorauswählen?" - -- No restriction. You can retrieve as many biases as you want per day. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2305356277"] = "Keine Einschränkung. Sie können beliebig viele Vorurteile pro Tag abrufen." +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2322771068"] = "Profil vorauswählen" + -- Which language should be preselected? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2345162613"] = "Welche Sprache soll vorausgewählt werden?" @@ -3906,9 +4098,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T3848 -- Options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T3875604319"] = "Optionen sind vorausgewählt" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T4004501229"] = "Möchten Sie eines ihrer Profile vorauswählen?" - -- Are you sure you want to reset your bias-of-the-day statistics? The system will no longer remember which biases you already know. As a result, biases you are already familiar with may be addressed again. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T405627382"] = "Sind Sie sicher, dass Sie ihre „Vorurteil des Tages“-Statistiken zurücksetzen möchten? Das System merkt sich dann nicht mehr, welche Verzerrungen Sie bereits kennen. Dadurch kann es sein, dass Ihnen bereits bekannte Verzerrungen erneut angezeigt werden." @@ -3939,8 +4128,11 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1773585398"] -- Provider selection when creating new chats UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T189306836"] = "Anbieterauswahl beim Erstellen neuer Chats" --- Would you like to set one of your profiles as the default for chats? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1933521846"] = "Möchten Sie eines ihrer Profile als Standardprofil für Chats festlegen?" +-- Choose whether chats should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1915793195"] = "Wählen Sie aus, ob Chats das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden sollen." + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T2322771068"] = "Profil vorauswählen" -- Apply default data source option when sending assistant results to chat UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T2510376349"] = "Standarddatenquelle verwenden, wenn Assistentenergebnisse in den Chat gesendet werden" @@ -3978,9 +4170,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T3730599555"] -- Latest message is shown, after loading a chat UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T3755993611"] = "Die neueste Nachricht wird nach dem Laden eines Chats angezeigt." --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T4004501229"] = "Eines ihrer Profile vorauswählen?" - -- Do you want to apply the default data source options when sending assistant results to chat? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T4033153439"] = "Möchten Sie die Standardoptionen für Datenquellen verwenden, wenn die Ergebnisse des Assistenten an den Chat gesendet werden?" @@ -4041,11 +4230,14 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083" -- Compiler messages are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1110902070"] = "Compiler-Nachrichten sind vorausgewählt" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1766361623"] = "Wählen Sie aus, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + -- Preselect a programming language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2181567002"] = "Programmiersprache vorauswählen" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2221665527"] = "Möchten Sie eines ihrer Profile vorauswählen?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2322771068"] = "Profil vorauswählen" -- When enabled, you can preselect the coding options. This is might be useful when you prefer a specific programming language or LLM model. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2619641701"] = "Wenn aktiviert, können Sie die Code-Optionen im Voraus auswählen. Das kann nützlich sein, wenn Sie eine bestimmte Programmiersprache oder ein bestimmtes LLM-Modell bevorzugen." @@ -4062,9 +4254,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T3015105896" -- Coding options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T3567850751"] = "Codierungsoptionen sind vorausgewählt" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T4004501229"] = "Eines ihrer Profile vorauswählen?" - -- Preselect another programming language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T4230412334"] = "Eine andere Programmiersprache vorauswählen" @@ -4164,14 +4353,17 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T12806662 -- Preselect ERI server options? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1664055662"] = "ERI-Serveroptionen vorauswählen?" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1766361623"] = "Wählen Sie, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + -- No ERI server options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1793785587"] = "Keine ERI-Serveroptionen sind vorausgewählt" -- Most ERI server options can be customized and saved directly in the ERI server assistant. For this, the ERI server assistant has an auto-save function. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2093534613"] = "Die meisten ERI-Serveroptionen können direkt im ERI-Server-Assistenten angepasst und gespeichert werden. Dazu verfügt der ERI-Server-Assistent über eine automatische Speicherfunktion." --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2221665527"] = "Möchten Sie eines ihrer Profile vorauswählen?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2322771068"] = "Profil vorauswählen" -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T3448155331"] = "Schließen" @@ -4179,9 +4371,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T34481553 -- Assistant: ERI Server Options UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T3629372826"] = "Assistent: ERI-Server-Optionen" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T4004501229"] = "Möchten Sie eines ihrer Profile vorauswählen?" - -- ERI server options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T488190224"] = "ERI-Serveroptionen sind vorausgewählt" @@ -4329,6 +4518,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1633101 -- Web content reader is not preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1701127912"] = "Der Web-Content-Reader zum Lesen von Webinhalten ist nicht vorausgewählt" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1766361623"] = "Wählen Sie aus, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + -- Content cleaner agent is not preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1969816694"] = "Agent zur Inhaltsbereinigung ist nicht vorausgewählt" @@ -4338,8 +4530,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2090693 -- When enabled, you can preselect some legal check options. This is might be useful when you prefer a specific LLM model. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2164667361"] = "Wenn aktiviert, können Sie einige rechtliche Prüfoptionen vorauswählen. Dies kann nützlich sein, wenn Sie ein bestimmtes LLM-Modell bevorzugen." --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2221665527"] = "Möchten Sie eines ihrer Profile vorauswählen?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2322771068"] = "Profil vorauswählen" -- Legal check options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T252916114"] = "Rechtsprüfungsoptionen sind vorausgewählt" @@ -4359,17 +4551,17 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T3641773 -- Preselect the content cleaner agent? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T3649428096"] = "Assistent zur Inhaltsbereinigungs vorauswählen?" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T4004501229"] = "Eines ihrer Profile vorauswählen?" - -- Assistant: Legal Check Options UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T4033382756"] = "Assistent: Optionen für rechtliche Prüfung" -- Preselect the web content reader? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T629158142"] = "Den Web-Content-Reader zum Lesen von Webinhalten vorauswählen?" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2221665527"] = "Möchten Sie eines ihrer Profile vorauswählen?" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T1766361623"] = "Wählen Sie, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2322771068"] = "Profil vorauswählen" -- Which language should be preselected? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2345162613"] = "Welche Sprache soll vorausgewählt werden?" @@ -4392,9 +4584,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T3710380967 -- Options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T3875604319"] = "Optionen sind vorausgewählt" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T4004501229"] = "Möchten Sie eines ihrer Profile vorauswählen?" - -- Preselect options? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T42672465"] = "Optionen vorauswählen?" @@ -4482,6 +4671,69 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3745021518 -- No rewrite & improve text options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T553954963"] = "Keine Optionen für „Text umformulieren & verbessern“ sind vorausgewählt" +-- Preselect the audience expertise +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1017131030"] = "Expertise der Zielgruppe vorauswählen" + +-- When enabled, you can preselect slide builder options. This is might be useful when you prefer a specific language or LLM model. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1393378753"] = "Wenn diese Option aktiviert ist, können Sie Optionen für den Folienassistent vorab auswählen. Dies kann nützlich sein, wenn Sie eine bestimmte Sprache oder ein bestimmtes LLM-Modell bevorzugen." + +-- Preselect aspects for the LLM to focus on when generating slides, such as bullet points or specific topics to emphasize. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1528169602"] = "Wählen Sie Aspekte vorab aus, auf die sich das LLM bei der Erstellung von Folien konzentrieren soll, z. B. Aufzählungspunkte oder bestimmte Themen, die hervorgehoben werden sollen." + +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1766361623"] = "Wählen Sie aus, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + +-- Preselect the audience organizational level +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2014662371"] = "Organisationsebene der Zielgruppe vorauswählen" + +-- Which audience organizational level should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T216511105"] = "Welche organisatorische Ebene der Zielgruppe soll vorausgewählt werden?" + +-- Preselect Slide Assistant options? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T227645894"] = "Optionen des Folienassistenten vorauswählen?" + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2322771068"] = "Profil vorauswählen" + +-- Which language should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2345162613"] = "Welche Sprache sollte vorausgewählt sein?" + +-- Preselect another language +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2382415529"] = "Andere Sprache vorauswählen" + +-- Preselect the language +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2571465005"] = "Sprache vorauswählen" + +-- Preselect the audience age group +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2645589441"] = "Altersgruppe der Zielgruppe vorauswählen" + +-- Assistant: Slide Assistant Options +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3215549988"] = "Assistent: Optionen für die Erstellung von Folien" + +-- Which audience expertise should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3228597992"] = "Welche Expertise der Zielgruppe sollte vorausgewählt werden?" + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3448155331"] = "Schließen" + +-- Preselect important aspects +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3705987833"] = "Wichtige Aspekte vorauswählen" + +-- No Slide Assistant options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T4214398691"] = "Keine Optionen für den Folienassistenten sind vorausgewählt." + +-- Preselect the audience profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T861397972"] = "Zielgruppenprofil vorauswählen" + +-- Slide Assistant options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T93124146"] = "Optionen des Folienassistenten sind vorausgewählt" + +-- Which audience age group should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T956845877"] = "Welche Altersgruppe der Zielgruppe sollte vorausgewählt sein?" + +-- Which audience profile should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T973572510"] = "Welches Zielgruppenprofil soll vorausgewählt sein?" + -- When enabled, you can preselect synonym options. This is might be useful when you prefer a specific language or LLM model. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSYNONYMS::T183953912"] = "Wenn diese Option aktiviert ist, können Sie Synonymoptionen im Voraus auswählen. Dies kann nützlich sein, wenn Sie eine bestimmte Sprache oder ein bestimmtes LLM-Modell bevorzugen." @@ -4701,6 +4953,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1417 -- Preselect another target language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1462295644"] = "Eine andere Zielsprache auswählen" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1766361623"] = "Wählen Sie, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + -- Assistant: Writing E-Mails Options UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2021226503"] = "Assistent: Optionen zum Schreiben von E-Mails" @@ -4710,8 +4965,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2116 -- Preselect your name for the closing salutation? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T221974240"] = "Ihren Namen für die Grußformel vorauswählen?" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2221665527"] = "Möchten Sie eines ihrer Profile vorauswählen?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2322771068"] = "Profil vorauswählen" -- Preselect a writing style UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T28456020"] = "Wähle einen Schreibstil vor" @@ -4731,9 +4986,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3547 -- Preselect e-mail options? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3832719342"] = "E-Mail-Optionen vorauswählen?" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T4004501229"] = "Eines ihrer Profile vorauswählen?" - -- Save UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1294818664"] = "Speichern" @@ -4920,6 +5172,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1617786407"] = "Programmieren" -- Analyze a text or an email for tasks you need to complete. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1728590051"] = "Analysieren Sie einen Text oder eine E-Mail nach Aufgaben, die Sie erledigen müssen." +-- Slide Assistant +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1883918574"] = "Folienassistent" + -- Text Summarizer UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1907192403"] = "Texte zusammenfassen" @@ -4959,6 +5214,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3011450657"] = "Meine Aufgaben" -- E-Mail UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3026443472"] = "E-Mail" +-- Develop slide content based on a given topic and content. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T311912219"] = "Folieninhalte basierend auf einem vorgegebenen Thema und Inhalt erstellen." + -- Translate AI Studio text content into other languages UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3181803840"] = "AI Studio Textinhalte in andere Sprachen übersetzen." @@ -5691,6 +5949,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3491430707 -- Install updates automatically UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3569059463"] = "Updates automatisch installieren" +-- Use app default profile +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3587225583"] = "Standardprofil der App verwenden" + -- Disable workspaces UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3612390107"] = "Arbeitsbereiche deaktivieren" @@ -5871,6 +6132,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1546040625"] = "Meine A -- Grammar & Spelling Assistant UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T166453786"] = "Grammatik- & Rechtschreib-Assistent" +-- Slide Assistant +UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1883918574"] = "Folienassistent" + -- Legal Check Assistant UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1886447798"] = "Rechtlichen Prüfungs-Assistent" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 029b3b17..7854e7de 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -432,6 +432,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTA -- Load output rules from document UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2168201568"] = "Load output rules from document" +-- Choose whether the policy should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2214900121"] = "Choose whether the policy should use the app default profile, no profile, or a specific profile." + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2322771068"] = "Preselect a profile" + -- The analysis rules specify what the AI should pay particular attention to while reviewing the documents you provide, and which aspects it should highlight or save. For example, if you want to extract the potential of green hydrogen for agriculture from a variety of general publications, you can explicitly define this in the analysis rules. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T238145218"] = "The analysis rules specify what the AI should pay particular attention to while reviewing the documents you provide, and which aspects it should highlight or save. For example, if you want to extract the potential of green hydrogen for agriculture from a variety of general publications, you can explicitly define this in the analysis rules." @@ -1356,6 +1362,189 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::WRITINGSTYLESEXTENSIONS:: -- Marketing (advertisements, sales texts) UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::WRITINGSTYLESEXTENSIONS::T945714286"] = "Marketing (advertisements, sales texts)" +-- Children +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T188567026"] = "Children" + +-- Unspecified age group +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T300604284"] = "Unspecified age group" + +-- Adults +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T3335941460"] = "Adults" + +-- Teenagers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T3696960735"] = "Teenagers" + +-- No expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T1612807521"] = "No expertise" + +-- Intermediate expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T2131860427"] = "Intermediate expertise" + +-- Unspecified expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T2879202483"] = "Unspecified expertise" + +-- Basic expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T2909686714"] = "Basic expertise" + +-- Experts +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T3130182982"] = "Experts" + +-- Individual contributors +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T1893672448"] = "Individual contributors" + +-- Team leads +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2112906979"] = "Team leads" + +-- Board members +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2483400884"] = "Board members" + +-- Unspecified organizational level +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2633679224"] = "Unspecified organizational level" + +-- Trainees +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2678344512"] = "Trainees" + +-- Managers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T3187355853"] = "Managers" + +-- Executives +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T3317335174"] = "Executives" + +-- Journalists +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1589799277"] = "Journalists" + +-- Healthcare professionals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1652521346"] = "Healthcare professionals" + +-- Unspecified audience profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1845793571"] = "Unspecified audience profile" + +-- Lawyers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1959098902"] = "Lawyers" + +-- Investors +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T2516036290"] = "Investors" + +-- Students +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T2905889225"] = "Students" + +-- Scientists +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T332785734"] = "Scientists" + +-- Business professionals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T3670621687"] = "Business professionals" + +-- Engineers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T3904961809"] = "Engineers" + +-- Public officials +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T439009390"] = "Public officials" + +-- Software developers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T831424531"] = "Software developers" + +-- Important Aspects +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1379883528"] = "Important Aspects" + +-- Extent of the planned presentation +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1408740929"] = "Extent of the planned presentation" + +-- You might want to specify important aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1672597841"] = "You might want to specify important aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted." + +-- Details about the desired presentation +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1793579367"] = "Details about the desired presentation" + +-- Text content +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1820253043"] = "Text content" + +-- Slide Assistant +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1883918574"] = "Slide Assistant" + +-- Please provide a text or at least one valid document or image. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2013746884"] = "Please provide a text or at least one valid document or image." + +-- Content to derive slide from +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2086228265"] = "Content to derive slide from" + +-- Target language +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T237828418"] = "Target language" + +-- (Optional) Important Aspects +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T24391765"] = "(Optional) Important Aspects" + +-- Audience age group +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2496533563"] = "Audience age group" + +-- You can enter text, use one or more documents or images, or use both. At least one of these options is required. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2542045947"] = "You can enter text, use one or more documents or images, or use both. At least one of these options is required." + +-- Language +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2591284123"] = "Language" + +-- Audience organizational level +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2599228833"] = "Audience organizational level" + +-- Number of slides +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2823798965"] = "Number of slides" + +-- This assistant helps you create clear, structured slides from long texts or documents. Enter a presentation title and provide the content either as text or with one or more documents. Important aspects allow you to add instructions to the LLM regarding output or formatting. Set the number of slides either directly or based on your desired presentation duration. You can also specify the number of bullet points. If the default value of 0 is not changed, the LLM will independently determine how many slides or bullet points to generate. The output can be flexibly generated in various languages and tailored to a specific audience. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2910177051"] = "This assistant helps you create clear, structured slides from long texts or documents. Enter a presentation title and provide the content either as text or with one or more documents. Important aspects allow you to add instructions to the LLM regarding output or formatting. Set the number of slides either directly or based on your desired presentation duration. You can also specify the number of bullet points. If the default value of 0 is not changed, the LLM will independently determine how many slides or bullet points to generate. The output can be flexibly generated in various languages and tailored to a specific audience." + +-- The result of your previous slide builder session. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3000286990"] = "The result of your previous slide builder session." + +-- Please enter a title for the presentation. This will help the LLM to select more relevant content. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3013824309"] = "Please enter a title for the presentation. This will help the LLM to select more relevant content." + +-- Please provide a title +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3049299559"] = "Please provide a title" + +-- Create Slides +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3079776593"] = "Create Slides" + +-- Please specify the audience for the planned presentation. This will help the LLM to create a presentation that fits your needs. You can specify the audience profile, the age group, organizational level, and the expertise. You don't have to specify all of these parameters, for example you might only want to specify the audience profile and leave the other parameters at their default values. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3465256803"] = "Please specify the audience for the planned presentation. This will help the LLM to create a presentation that fits your needs. You can specify the audience profile, the age group, organizational level, and the expertise. You don't have to specify all of these parameters, for example you might only want to specify the audience profile and leave the other parameters at their default values." + +-- (Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3476149293"] = "(Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted." + +-- Audience +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3506118019"] = "Audience" + +-- Time specification (minutes) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3539067305"] = "Time specification (minutes)" + +-- Audience profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3649769130"] = "Audience profile" + +-- Attach documents +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3666048746"] = "Attach documents" + +-- Number of bullet points +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3796347042"] = "Number of bullet points" + +-- Custom target language +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3848935911"] = "Custom target language" + +-- Presentation title +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3893271035"] = "Presentation title" + +-- {0} - Slide Builder Session +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3922788056"] = "{0} - Slide Builder Session" + +-- Please specify the extent of the planned presentation. This can be the number of slides, the number of bullet points per slide, or the time specification for the presentation. This will help the LLM to create a presentation that fits your needs. Leave the default values if you don't have specific requirements regarding the extent of the presentation. You might only want to specify one of these parameters, for example the time specification, and leave the others at their default values. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T4131419342"] = "Please specify the extent of the planned presentation. This can be the number of slides, the number of bullet points per slide, or the time specification for the presentation. This will help the LLM to create a presentation that fits your needs. Leave the default values if you don't have specific requirements regarding the extent of the presentation. You might only want to specify one of these parameters, for example the time specification, and leave the others at their default values." + +-- Audience expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T4279519256"] = "Audience expertise" + +-- Title +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T617902505"] = "Title" + +-- Please provide a custom language. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T656744944"] = "Please provide a custom language." + -- Your word or phrase UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SYNONYM::ASSISTANTSYNONYMS::T1847246020"] = "Your word or phrase" @@ -3768,6 +3957,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1471770981" -- Preselect whether participants needs to arrive and depart UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1648427207"] = "Preselect whether participants needs to arrive and depart" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- Preselect a start time? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1901151023"] = "Preselect a start time?" @@ -3780,8 +3972,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1998244307" -- Preselect whether the meeting is virtual UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2084951012"] = "Preselect whether the meeting is virtual" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2322771068"] = "Preselect a profile" -- When enabled, you can preselect most agenda options. This is might be useful when you need to create similar agendas often. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2373110543"] = "When enabled, you can preselect most agenda options. This is might be useful when you need to create similar agendas often." @@ -3834,9 +4026,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T3709527588" -- Preselect a topic? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T3835166371"] = "Preselect a topic?" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T4004501229"] = "Preselect one of your profiles?" - -- Preselect the agenda language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T4055846391"] = "Preselect the agenda language" @@ -3873,15 +4062,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1608 -- Yes, you can only retrieve one bias per day UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1765683725"] = "Yes, you can only retrieve one bias per day" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- Reset UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T180921696"] = "Reset" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2221665527"] = "Would you like to preselect one of your profiles?" - -- No restriction. You can retrieve as many biases as you want per day. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2305356277"] = "No restriction. You can retrieve as many biases as you want per day." +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2322771068"] = "Preselect a profile" + -- Which language should be preselected? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2345162613"] = "Which language should be preselected?" @@ -3906,9 +4098,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T3848 -- Options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T3875604319"] = "Options are preselected" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T4004501229"] = "Preselect one of your profiles?" - -- Are you sure you want to reset your bias-of-the-day statistics? The system will no longer remember which biases you already know. As a result, biases you are already familiar with may be addressed again. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T405627382"] = "Are you sure you want to reset your bias-of-the-day statistics? The system will no longer remember which biases you already know. As a result, biases you are already familiar with may be addressed again." @@ -3939,8 +4128,11 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1773585398"] -- Provider selection when creating new chats UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T189306836"] = "Provider selection when creating new chats" --- Would you like to set one of your profiles as the default for chats? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1933521846"] = "Would you like to set one of your profiles as the default for chats?" +-- Choose whether chats should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1915793195"] = "Choose whether chats should use the app default profile, no profile, or a specific profile." + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T2322771068"] = "Preselect a profile" -- Apply default data source option when sending assistant results to chat UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T2510376349"] = "Apply default data source option when sending assistant results to chat" @@ -3978,9 +4170,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T3730599555"] -- Latest message is shown, after loading a chat UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T3755993611"] = "Latest message is shown, after loading a chat" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T4004501229"] = "Preselect one of your profiles?" - -- Do you want to apply the default data source options when sending assistant results to chat? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T4033153439"] = "Do you want to apply the default data source options when sending assistant results to chat?" @@ -4041,11 +4230,14 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083" -- Compiler messages are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1110902070"] = "Compiler messages are preselected" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- Preselect a programming language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2181567002"] = "Preselect a programming language" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2322771068"] = "Preselect a profile" -- When enabled, you can preselect the coding options. This is might be useful when you prefer a specific programming language or LLM model. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2619641701"] = "When enabled, you can preselect the coding options. This is might be useful when you prefer a specific programming language or LLM model." @@ -4062,9 +4254,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T3015105896" -- Coding options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T3567850751"] = "Coding options are preselected" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T4004501229"] = "Preselect one of your profiles?" - -- Preselect another programming language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T4230412334"] = "Preselect another programming language" @@ -4164,14 +4353,17 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T12806662 -- Preselect ERI server options? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1664055662"] = "Preselect ERI server options?" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- No ERI server options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1793785587"] = "No ERI server options are preselected" -- Most ERI server options can be customized and saved directly in the ERI server assistant. For this, the ERI server assistant has an auto-save function. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2093534613"] = "Most ERI server options can be customized and saved directly in the ERI server assistant. For this, the ERI server assistant has an auto-save function." --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2322771068"] = "Preselect a profile" -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T3448155331"] = "Close" @@ -4179,9 +4371,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T34481553 -- Assistant: ERI Server Options UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T3629372826"] = "Assistant: ERI Server Options" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T4004501229"] = "Preselect one of your profiles?" - -- ERI server options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T488190224"] = "ERI server options are preselected" @@ -4329,6 +4518,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1633101 -- Web content reader is not preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1701127912"] = "Web content reader is not preselected" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- Content cleaner agent is not preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1969816694"] = "Content cleaner agent is not preselected" @@ -4338,8 +4530,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2090693 -- When enabled, you can preselect some legal check options. This is might be useful when you prefer a specific LLM model. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2164667361"] = "When enabled, you can preselect some legal check options. This is might be useful when you prefer a specific LLM model." --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2322771068"] = "Preselect a profile" -- Legal check options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T252916114"] = "Legal check options are preselected" @@ -4359,17 +4551,17 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T3641773 -- Preselect the content cleaner agent? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T3649428096"] = "Preselect the content cleaner agent?" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T4004501229"] = "Preselect one of your profiles?" - -- Assistant: Legal Check Options UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T4033382756"] = "Assistant: Legal Check Options" -- Preselect the web content reader? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T629158142"] = "Preselect the web content reader?" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2322771068"] = "Preselect a profile" -- Which language should be preselected? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2345162613"] = "Which language should be preselected?" @@ -4392,9 +4584,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T3710380967 -- Options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T3875604319"] = "Options are preselected" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T4004501229"] = "Preselect one of your profiles?" - -- Preselect options? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T42672465"] = "Preselect options?" @@ -4482,6 +4671,69 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3745021518 -- No rewrite & improve text options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T553954963"] = "No rewrite & improve text options are preselected" +-- Preselect the audience expertise +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1017131030"] = "Preselect the audience expertise" + +-- When enabled, you can preselect slide builder options. This is might be useful when you prefer a specific language or LLM model. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1393378753"] = "When enabled, you can preselect slide builder options. This is might be useful when you prefer a specific language or LLM model." + +-- Preselect aspects for the LLM to focus on when generating slides, such as bullet points or specific topics to emphasize. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1528169602"] = "Preselect aspects for the LLM to focus on when generating slides, such as bullet points or specific topics to emphasize." + +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + +-- Preselect the audience organizational level +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2014662371"] = "Preselect the audience organizational level" + +-- Which audience organizational level should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T216511105"] = "Which audience organizational level should be preselected?" + +-- Preselect Slide Assistant options? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T227645894"] = "Preselect Slide Assistant options?" + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2322771068"] = "Preselect a profile" + +-- Which language should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2345162613"] = "Which language should be preselected?" + +-- Preselect another language +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2382415529"] = "Preselect another language" + +-- Preselect the language +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2571465005"] = "Preselect the language" + +-- Preselect the audience age group +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2645589441"] = "Preselect the audience age group" + +-- Assistant: Slide Assistant Options +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3215549988"] = "Assistant: Slide Assistant Options" + +-- Which audience expertise should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3228597992"] = "Which audience expertise should be preselected?" + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3448155331"] = "Close" + +-- Preselect important aspects +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3705987833"] = "Preselect important aspects" + +-- No Slide Assistant options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T4214398691"] = "No Slide Assistant options are preselected" + +-- Preselect the audience profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T861397972"] = "Preselect the audience profile" + +-- Slide Assistant options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T93124146"] = "Slide Assistant options are preselected" + +-- Which audience age group should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T956845877"] = "Which audience age group should be preselected?" + +-- Which audience profile should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T973572510"] = "Which audience profile should be preselected?" + -- When enabled, you can preselect synonym options. This is might be useful when you prefer a specific language or LLM model. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSYNONYMS::T183953912"] = "When enabled, you can preselect synonym options. This is might be useful when you prefer a specific language or LLM model." @@ -4701,6 +4953,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1417 -- Preselect another target language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1462295644"] = "Preselect another target language" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- Assistant: Writing E-Mails Options UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2021226503"] = "Assistant: Writing E-Mails Options" @@ -4710,8 +4965,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2116 -- Preselect your name for the closing salutation? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T221974240"] = "Preselect your name for the closing salutation?" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2322771068"] = "Preselect a profile" -- Preselect a writing style UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T28456020"] = "Preselect a writing style" @@ -4731,9 +4986,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3547 -- Preselect e-mail options? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3832719342"] = "Preselect e-mail options?" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T4004501229"] = "Preselect one of your profiles?" - -- Save UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1294818664"] = "Save" @@ -4920,6 +5172,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1617786407"] = "Coding" -- Analyze a text or an email for tasks you need to complete. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1728590051"] = "Analyze a text or an email for tasks you need to complete." +-- Slide Assistant +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1883918574"] = "Slide Assistant" + -- Text Summarizer UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1907192403"] = "Text Summarizer" @@ -4959,6 +5214,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3011450657"] = "My Tasks" -- E-Mail UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3026443472"] = "E-Mail" +-- Develop slide content based on a given topic and content. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T311912219"] = "Develop slide content based on a given topic and content." + -- Translate AI Studio text content into other languages UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3181803840"] = "Translate AI Studio text content into other languages" @@ -5691,6 +5949,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3491430707 -- Install updates automatically UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3569059463"] = "Install updates automatically" +-- Use app default profile +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3587225583"] = "Use app default profile" + -- Disable workspaces UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3612390107"] = "Disable workspaces" @@ -5871,6 +6132,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1546040625"] = "My Task -- Grammar & Spelling Assistant UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T166453786"] = "Grammar & Spelling Assistant" +-- Slide Assistant +UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1883918574"] = "Slide Assistant" + -- Legal Check Assistant UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1886447798"] = "Legal Check Assistant" diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 70cc6831..ba765962 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -178,7 +178,7 @@ internal sealed class Program builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); - builder.Services.AddSingleton(databaseClient); + builder.Services.AddSingleton(databaseClient); builder.Services.AddHostedService(); builder.Services.AddHostedService(); diff --git a/app/MindWork AI Studio/Routes.razor.cs b/app/MindWork AI Studio/Routes.razor.cs index 836cab0e..92ff3067 100644 --- a/app/MindWork AI Studio/Routes.razor.cs +++ b/app/MindWork AI Studio/Routes.razor.cs @@ -22,6 +22,7 @@ public sealed partial class Routes public const string ASSISTANT_EMAIL = "/assistant/email"; public const string ASSISTANT_LEGAL_CHECK = "/assistant/legal-check"; public const string ASSISTANT_SYNONYMS = "/assistant/synonyms"; + public const string ASSISTANT_SLIDE_BUILDER = "/assistant/slide-builder"; public const string ASSISTANT_MY_TASKS = "/assistant/my-tasks"; public const string ASSISTANT_JOB_POSTING = "/assistant/job-posting"; public const string ASSISTANT_BIAS = "/assistant/bias-of-the-day"; @@ -29,4 +30,4 @@ public sealed partial class Routes public const string ASSISTANT_AI_STUDIO_I18N = "/assistant/ai-studio/i18n"; public const string ASSISTANT_DOCUMENT_ANALYSIS = "/assistant/document-analysis"; // ReSharper restore InconsistentNaming -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs b/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs index f31db876..0f8b9d5d 100644 --- a/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs +++ b/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs @@ -2,6 +2,7 @@ using AIStudio.Assistants.Agenda; using AIStudio.Assistants.Coding; using AIStudio.Assistants.IconFinder; using AIStudio.Assistants.RewriteImprove; +using AIStudio.Assistants.SlideBuilder; using AIStudio.Assistants.TextSummarizer; using AIStudio.Assistants.EMail; using AIStudio.Provider; @@ -197,6 +198,30 @@ public static class ConfigurationSelectDataFactory foreach (var voice in Enum.GetValues()) yield return new(voice.Name(), voice); } + + public static IEnumerable> GetSlideBuilderAudienceProfileData() + { + foreach (var profile in Enum.GetValues()) + yield return new(profile.Name(), profile); + } + + public static IEnumerable> GetSlideBuilderAudienceAgeGroupData() + { + foreach (var ageGroup in Enum.GetValues()) + yield return new(ageGroup.Name(), ageGroup); + } + + public static IEnumerable> GetSlideBuilderAudienceOrganizationalLevelData() + { + foreach (var level in Enum.GetValues()) + yield return new(level.Name(), level); + } + + public static IEnumerable> GetSlideBuilderAudienceExpertiseData() + { + foreach (var expertise in Enum.GetValues()) + yield return new(expertise.Name(), expertise); + } public static IEnumerable> GetProfilesData(IEnumerable profiles) { @@ -263,4 +288,4 @@ public static class ConfigurationSelectDataFactory foreach (var theme in Enum.GetValues()) yield return new(theme.GetName(), theme); } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs index f174d6c2..0013fe92 100644 --- a/app/MindWork AI Studio/Settings/DataModel/Data.cs +++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs @@ -118,6 +118,8 @@ public sealed class Data public DataEMail EMail { get; init; } = new(); + public DataSlideBuilder SlideBuilder { get; init; } = new(); + public DataLegalCheck LegalCheck { get; init; } = new(); public DataSynonyms Synonyms { get; init; } = new(); diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSlideBuilder.cs b/app/MindWork AI Studio/Settings/DataModel/DataSlideBuilder.cs new file mode 100644 index 00000000..1702cde4 --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataSlideBuilder.cs @@ -0,0 +1,62 @@ +using AIStudio.Assistants.SlideBuilder; +using AIStudio.Provider; + +namespace AIStudio.Settings.DataModel; + +public class DataSlideBuilder +{ + /// + /// Preselect any Slide Builder options? + /// + public bool PreselectOptions { get; set; } = true; + + /// + /// Preselect a profile? + /// + public string PreselectedProfile { get; set; } = string.Empty; + + /// + /// Preselect a Slide Builder provider? + /// + public string PreselectedProvider { get; set; } = string.Empty; + + /// + /// Preselect the target language? + /// + public CommonLanguages PreselectedTargetLanguage { get; set; } + + /// + /// Preselect any other language? + /// + public string PreselectedOtherLanguage { get; set; } = string.Empty; + + /// + /// Preselect the audience profile? + /// + public AudienceProfile PreselectedAudienceProfile { get; set; } + + /// + /// Preselect the audience age group? + /// + public AudienceAgeGroup PreselectedAudienceAgeGroup { get; set; } + + /// + /// Preselect the audience organizational level? + /// + public AudienceOrganizationalLevel PreselectedAudienceOrganizationalLevel { get; set; } + + /// + /// Preselect the audience expertise? + /// + public AudienceExpertise PreselectedAudienceExpertise { get; set; } + + /// + /// Preselect any important aspects that the Slide Builder should take into account? + /// + public string PreselectedImportantAspects { get; set; } = string.Empty; + + /// + /// The minimum confidence level required for a provider to be considered. + /// + public ConfidenceLevel MinimumProviderConfidence { get; set; } = ConfidenceLevel.NONE; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs b/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs index 02c24f75..734e1861 100644 --- a/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs +++ b/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs @@ -58,6 +58,14 @@ public static class CommonLanguageExtensions _ => $"Translate the given text in {language.Name()} ({language}).", }; + + public static string PromptGeneralPurpose(this CommonLanguages language, string customLanguage) => language switch + { + CommonLanguages.AS_IS => "Use the language the user input is written in for the output.", + CommonLanguages.OTHER => $"use the language {customLanguage} for your output.", + + _ => $"Use the language {language.Name()} ({language}) for your output.", + }; public static string NameSelecting(this CommonLanguages language) { diff --git a/app/MindWork AI Studio/Tools/Components.cs b/app/MindWork AI Studio/Tools/Components.cs index 45ccba91..02718736 100644 --- a/app/MindWork AI Studio/Tools/Components.cs +++ b/app/MindWork AI Studio/Tools/Components.cs @@ -19,6 +19,7 @@ public enum Components BIAS_DAY_ASSISTANT, ERI_ASSISTANT, DOCUMENT_ANALYSIS_ASSISTANT, + SLIDE_BUILDER_ASSISTANT, // ReSharper disable InconsistentNaming I18N_ASSISTANT, diff --git a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs index a5ff822d..70f06380 100644 --- a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs +++ b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs @@ -45,6 +45,7 @@ public static class ComponentsExtensions Components.ERI_ASSISTANT => TB("ERI Server"), Components.I18N_ASSISTANT => TB("Localization Assistant"), Components.DOCUMENT_ANALYSIS_ASSISTANT => TB("Document Analysis Assistant"), + Components.SLIDE_BUILDER_ASSISTANT => TB("Slide Assistant"), Components.CHAT => TB("New Chat"), @@ -66,6 +67,7 @@ public static class ComponentsExtensions Components.MY_TASKS_ASSISTANT => new(Event.SEND_TO_MY_TASKS_ASSISTANT, Routes.ASSISTANT_MY_TASKS), Components.JOB_POSTING_ASSISTANT => new(Event.SEND_TO_JOB_POSTING_ASSISTANT, Routes.ASSISTANT_JOB_POSTING), Components.DOCUMENT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_DOCUMENT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_DOCUMENT_ANALYSIS), + Components.SLIDE_BUILDER_ASSISTANT => new(Event.SEND_TO_SLIDE_BUILDER_ASSISTANT, Routes.ASSISTANT_SLIDE_BUILDER), Components.CHAT => new(Event.SEND_TO_CHAT, Routes.CHAT), @@ -88,6 +90,7 @@ public static class ComponentsExtensions Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence : default, Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence : default, Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.MinimumProviderConfidence : default, + Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.MinimumProviderConfidence : default, // The minimum confidence for the Document Analysis Assistant is set per policy. // We do this inside the Document Analysis Assistant component: @@ -116,6 +119,7 @@ public static class ComponentsExtensions Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : null, Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProvider) : null, Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.I18N.PreselectedProvider) : null, + Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : null, // The Document Analysis Assistant does not have a preselected provider at the component level. // The provider is selected per policy instead. We do this inside the Document Analysis Assistant component. @@ -144,6 +148,7 @@ public static class ComponentsExtensions Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.PreselectedProfile : string.Empty, Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile : string.Empty, Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.PreselectedProfile : string.Empty, + Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.PreselectedProfile : string.Empty, Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Chat.PreselectedProfile : string.Empty, // The Document Analysis Assistant does not have a preselected profile at the component level. diff --git a/app/MindWork AI Studio/Tools/Event.cs b/app/MindWork AI Studio/Tools/Event.cs index aac82108..4c90725d 100644 --- a/app/MindWork AI Studio/Tools/Event.cs +++ b/app/MindWork AI Studio/Tools/Event.cs @@ -55,4 +55,5 @@ public enum Event SEND_TO_MY_TASKS_ASSISTANT, SEND_TO_JOB_POSTING_ASSISTANT, SEND_TO_DOCUMENT_ANALYSIS_ASSISTANT, + SEND_TO_SLIDE_BUILDER_ASSISTANT } diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 8346f0d6..836e91b0 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -1,8 +1,9 @@ # v26.3.1, build 235 (2026-03-xx xx:xx UTC) - Added support for the new Qwen 3.5 model family. +- Added a slide assistant, which helps you turn longer texts or documents into clear, structured presentation slides. Many thanks to Sabrina `Sabrina-devops` for her wonderful work on this assistant. - Added a reminder in chats and assistants that LLMs can make mistakes, helping you double-check important information more easily. - Added the ability to format your user prompt in the chat using icons instead of typing Markdown directly. -- Added explicit profile behavior choices for assistants and the chat. You can now decide whether AI Studio should use the app default profile, no profile, or a specific profile. +- Improved the profile selection for assistants and the chat. You can now explicitly choose between the app default profile, no profile, or a specific profile. - Improved the performance by caching the OS language detection and requesting the user language only once per app start. - Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations. - Improved the workspace loading experience: when opening the chat for the first time, your workspaces now appear faster and load step by step in the background, with placeholder rows so the app feels responsive right away. From 56da27372cb4d51bbc4d74b16eb5909dbacef003 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 16 Mar 2026 12:43:48 +0100 Subject: [PATCH 16/27] Added support to load the system prompt from a file in chat templates (#697) (#697) --- app/MindWork AI Studio/Assistants/I18N/allTexts.lua | 3 +++ app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor | 1 + .../de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua | 3 +++ .../en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua | 3 +++ app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md | 1 + 5 files changed, 11 insertions(+) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index f62c031f..a5254585 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -2947,6 +2947,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T380891852"] = "Example -- Actions UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3865031940"] = "Actions" +-- Load system prompt from file +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3883091650"] = "Load system prompt from file" + -- Messages per page UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3893704289"] = "Messages per page" diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor index 0a72ef07..d13a44bb 100644 --- a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor @@ -55,6 +55,7 @@ @T("Use the default system prompt") + @T("Predefined User Input") diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 5af28bf7..bc520ce8 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -2949,6 +2949,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T380891852"] = "Beispiel -- Actions UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3865031940"] = "Aktionen" +-- Load system prompt from file +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3883091650"] = "System-Prompt aus Datei laden" + -- Messages per page UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3893704289"] = "Nachrichten pro Seite" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 7854e7de..c92c1ff3 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -2949,6 +2949,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T380891852"] = "Example -- Actions UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3865031940"] = "Actions" +-- Load system prompt from file +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3883091650"] = "Load system prompt from file" + -- Messages per page UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3893704289"] = "Messages per page" diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 836e91b0..ed3c9834 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -3,6 +3,7 @@ - Added a slide assistant, which helps you turn longer texts or documents into clear, structured presentation slides. Many thanks to Sabrina `Sabrina-devops` for her wonderful work on this assistant. - Added a reminder in chats and assistants that LLMs can make mistakes, helping you double-check important information more easily. - Added the ability to format your user prompt in the chat using icons instead of typing Markdown directly. +- Added the ability to load a system prompt from a file when creating or editing chat templates. - Improved the profile selection for assistants and the chat. You can now explicitly choose between the app default profile, no profile, or a specific profile. - Improved the performance by caching the OS language detection and requesting the user language only once per app start. - Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations. From dac0b74145e4e7322c648a2a48d4637dbcfec2fd Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 16 Mar 2026 22:36:51 +0100 Subject: [PATCH 17/27] Harden voice recording initialization (#698) --- .../Assistants/I18N/allTexts.lua | 6 + .../Components/VoiceRecorder.razor | 5 +- .../Components/VoiceRecorder.razor.cs | 132 ++++++++++++++---- .../plugin.lua | 6 + .../plugin.lua | 6 + app/MindWork AI Studio/Program.cs | 1 + .../Tools/AudioRecordingResult.cs | 8 ++ app/MindWork AI Studio/Tools/Event.cs | 1 + .../Tools/Services/GlobalShortcutService.cs | 20 ++- .../VoiceRecordingAvailabilityService.cs | 23 +++ .../Tools/SoundEffectsInitializationResult.cs | 10 ++ app/MindWork AI Studio/wwwroot/audio.js | 92 +++++++----- .../wwwroot/changelog/v26.3.1.md | 1 + 13 files changed, 242 insertions(+), 69 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/AudioRecordingResult.cs create mode 100644 app/MindWork AI Studio/Tools/Services/VoiceRecordingAvailabilityService.cs create mode 100644 app/MindWork AI Studio/Tools/SoundEffectsInitializationResult.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index a5254585..e899d79a 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -2725,6 +2725,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T428040679"] = "Content creation" -- Useful assistants UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T586430036"] = "Useful assistants" +-- Voice recording has been disabled for this session because audio playback could not be initialized on the client. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1123032432"] = "Voice recording has been disabled for this session because audio playback could not be initialized on the client." + -- Failed to create the transcription provider. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1689988905"] = "Failed to create the transcription provider." @@ -2734,6 +2737,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2144994226"] = "Failed to -- Stop recording and start transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T224155287"] = "Stop recording and start transcription" +-- Voice recording is unavailable because the client could not initialize audio playback. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2260302339"] = "Voice recording is unavailable because the client could not initialize audio playback." + -- Start recording your voice for a transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2372624045"] = "Start recording your voice for a transcription" diff --git a/app/MindWork AI Studio/Components/VoiceRecorder.razor b/app/MindWork AI Studio/Components/VoiceRecorder.razor index e247f439..a99afd14 100644 --- a/app/MindWork AI Studio/Components/VoiceRecorder.razor +++ b/app/MindWork AI Studio/Components/VoiceRecorder.razor @@ -1,9 +1,7 @@ -@using AIStudio.Settings.DataModel - @namespace AIStudio.Components @inherits MSGComponentBase -@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager) && !string.IsNullOrWhiteSpace(this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider)) +@if (this.ShouldRenderVoiceRecording) { @if (this.isTranscribing || this.isPreparing) @@ -16,6 +14,7 @@ ToggledChanged="@this.OnRecordingToggled" Icon="@Icons.Material.Filled.Mic" ToggledIcon="@Icons.Material.Filled.Stop" + Disabled="@(!this.IsVoiceRecordingAvailable)" Color="Color.Primary" ToggledColor="Color.Error"/> } diff --git a/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs b/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs index 73a95e8d..686656dd 100644 --- a/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs +++ b/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Provider; +using AIStudio.Settings.DataModel; using AIStudio.Tools.MIME; using AIStudio.Tools.Rust; using AIStudio.Tools.Services; @@ -21,24 +22,25 @@ public partial class VoiceRecorder : MSGComponentBase [Inject] private ISnackbar Snackbar { get; init; } = null!; + [Inject] + private VoiceRecordingAvailabilityService VoiceRecordingAvailabilityService { get; init; } = null!; + #region Overrides of MSGComponentBase protected override async Task OnInitializedAsync() { // Register for global shortcut events: - this.ApplyFilters([], [Event.TAURI_EVENT_RECEIVED]); + this.ApplyFilters([], [Event.TAURI_EVENT_RECEIVED, Event.VOICE_RECORDING_AVAILABILITY_CHANGED]); await base.OnInitializedAsync(); + } - try - { - // Initialize sound effects. This "warms up" the AudioContext and preloads all sounds for reliable playback: - await this.JsRuntime.InvokeVoidAsync("initSoundEffects"); - } - catch (Exception ex) - { - this.Logger.LogError(ex, "Failed to initialize sound effects."); - } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && this.ShouldRenderVoiceRecording) + await this.EnsureSoundEffectsAvailableAsync("during the first interactive render"); + + await base.OnAfterRenderAsync(firstRender); } protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default @@ -54,6 +56,10 @@ public partial class VoiceRecorder : MSGComponentBase } break; + + case Event.VOICE_RECORDING_AVAILABILITY_CHANGED: + this.StateHasChanged(); + break; } } @@ -62,6 +68,12 @@ public partial class VoiceRecorder : MSGComponentBase /// private async Task ToggleRecordingFromShortcut() { + if (!this.IsVoiceRecordingAvailable) + { + this.Logger.LogDebug("Ignoring shortcut: voice recording is unavailable in the current session."); + return; + } + // Don't allow toggle if transcription is in progress or preparing: if (this.isTranscribing || this.isPreparing) { @@ -85,27 +97,38 @@ public partial class VoiceRecorder : MSGComponentBase private string? finalRecordingPath; private DotNetObjectReference? dotNetReference; - private string Tooltip => this.isTranscribing - ? T("Transcription in progress...") - : this.isRecording - ? T("Stop recording and start transcription") - : T("Start recording your voice for a transcription"); + private bool ShouldRenderVoiceRecording => PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager) + && !string.IsNullOrWhiteSpace(this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider); + + private bool IsVoiceRecordingAvailable => this.ShouldRenderVoiceRecording + && this.VoiceRecordingAvailabilityService.IsAvailable; + + private string Tooltip => !this.VoiceRecordingAvailabilityService.IsAvailable + ? T("Voice recording is unavailable because the client could not initialize audio playback.") + : this.isTranscribing + ? T("Transcription in progress...") + : this.isRecording + ? T("Stop recording and start transcription") + : T("Start recording your voice for a transcription"); private async Task OnRecordingToggled(bool toggled) { if (toggled) { + if (!this.IsVoiceRecordingAvailable) + { + this.Logger.LogDebug("Ignoring recording start: voice recording is unavailable in the current session."); + return; + } + this.isPreparing = true; this.StateHasChanged(); - - try + + if (!await this.EnsureSoundEffectsAvailableAsync("before starting audio recording")) { - // Warm up sound effects: - await this.JsRuntime.InvokeVoidAsync("initSoundEffects"); - } - catch (Exception ex) - { - this.Logger.LogError(ex, "Failed to initialize sound effects."); + this.isPreparing = false; + this.StateHasChanged(); + return; } var mimeTypes = GetPreferredMimeTypes( @@ -416,11 +439,66 @@ public partial class VoiceRecorder : MSGComponentBase } } - private sealed class AudioRecordingResult + private async Task EnsureSoundEffectsAvailableAsync(string context) { - public string MimeType { get; init; } = string.Empty; + if (!this.ShouldRenderVoiceRecording) + return false; - public bool ChangedMimeType { get; init; } + if (!this.VoiceRecordingAvailabilityService.IsAvailable) + return false; + + try + { + var result = await this.JsRuntime.InvokeAsync("initSoundEffects"); + if (result.Success) + return true; + + var failureDetails = BuildSoundEffectsFailureDetails(result); + this.Logger.LogError("Failed to initialize sound effects {Context}. {FailureDetails}", context, failureDetails); + await this.DisableVoiceRecordingAsync(failureDetails); + } + catch (JSDisconnectedException ex) + { + this.Logger.LogError(ex, "Failed to initialize sound effects {Context}. The JS runtime disconnected.", context); + await this.DisableVoiceRecordingAsync("The JS runtime disconnected while initializing audio playback."); + } + catch (OperationCanceledException ex) + { + this.Logger.LogError(ex, "Failed to initialize sound effects {Context}. The interop call was canceled.", context); + await this.DisableVoiceRecordingAsync("The interop call for audio playback initialization was canceled."); + } + catch (Exception ex) + { + this.Logger.LogError(ex, "Failed to initialize sound effects {Context}.", context); + await this.DisableVoiceRecordingAsync(ex.Message); + } + + return false; + } + + private async Task DisableVoiceRecordingAsync(string reason) + { + if (!this.VoiceRecordingAvailabilityService.TryDisable(reason)) + return; + + this.Logger.LogWarning("Voice recording was disabled for the current session. Reason: {Reason}", reason); + await this.MessageBus.SendWarning(new(Icons.Material.Filled.MicOff, this.T("Voice recording has been disabled for this session because audio playback could not be initialized on the client."))); + await this.SendMessage(Event.VOICE_RECORDING_AVAILABILITY_CHANGED, reason); + this.StateHasChanged(); + } + + private static string BuildSoundEffectsFailureDetails(SoundEffectsInitializationResult result) + { + var details = new List(); + if (result.FailedPaths.Length > 0) + details.Add($"Failed sound files: {string.Join(", ", result.FailedPaths)}."); + + if (!string.IsNullOrWhiteSpace(result.ErrorMessage)) + details.Add($"Client error: {result.ErrorMessage}"); + + return details.Count > 0 + ? string.Join(" ", details) + : "The client did not provide additional details."; } #region Overrides of MSGComponentBase @@ -440,4 +518,4 @@ public partial class VoiceRecorder : MSGComponentBase } #endregion -} +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index bc520ce8..0a94f7a7 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -2727,6 +2727,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T428040679"] = "Erstellung von In -- Useful assistants UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T586430036"] = "Nützliche Assistenten" +-- Voice recording has been disabled for this session because audio playback could not be initialized on the client. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1123032432"] = "Die Sprachaufnahme wurde für diese Sitzung deaktiviert, da die Audiowiedergabe auf dem Client nicht initialisiert werden konnte." + -- Failed to create the transcription provider. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1689988905"] = "Der Anbieter für die Transkription konnte nicht erstellt werden." @@ -2736,6 +2739,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2144994226"] = "Audioaufn -- Stop recording and start transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T224155287"] = "Aufnahme beenden und Transkription starten" +-- Voice recording is unavailable because the client could not initialize audio playback. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2260302339"] = "Die Sprachaufnahme ist nicht verfügbar, da der Client die Audiowiedergabe nicht initialisieren konnte." + -- Start recording your voice for a transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2372624045"] = "Beginnen Sie mit der Aufnahme Ihrer Stimme für eine Transkription" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index c92c1ff3..2849c6d2 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -2727,6 +2727,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T428040679"] = "Content creation" -- Useful assistants UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T586430036"] = "Useful assistants" +-- Voice recording has been disabled for this session because audio playback could not be initialized on the client. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1123032432"] = "Voice recording has been disabled for this session because audio playback could not be initialized on the client." + -- Failed to create the transcription provider. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1689988905"] = "Failed to create the transcription provider." @@ -2736,6 +2739,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2144994226"] = "Failed to -- Stop recording and start transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T224155287"] = "Stop recording and start transcription" +-- Voice recording is unavailable because the client could not initialize audio playback. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2260302339"] = "Voice recording is unavailable because the client could not initialize audio playback." + -- Start recording your voice for a transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2372624045"] = "Start recording your voice for a transcription" diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index ba765962..f19344d6 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -169,6 +169,7 @@ internal sealed class Program builder.Services.AddMudMarkdownClipboardService(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddTransient(); diff --git a/app/MindWork AI Studio/Tools/AudioRecordingResult.cs b/app/MindWork AI Studio/Tools/AudioRecordingResult.cs new file mode 100644 index 00000000..cdde82ac --- /dev/null +++ b/app/MindWork AI Studio/Tools/AudioRecordingResult.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Tools; + +public sealed class AudioRecordingResult +{ + public string MimeType { get; init; } = string.Empty; + + public bool ChangedMimeType { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Event.cs b/app/MindWork AI Studio/Tools/Event.cs index 4c90725d..6e899a79 100644 --- a/app/MindWork AI Studio/Tools/Event.cs +++ b/app/MindWork AI Studio/Tools/Event.cs @@ -17,6 +17,7 @@ public enum Event SHOW_SUCCESS, TAURI_EVENT_RECEIVED, RUST_SERVICE_UNAVAILABLE, + VOICE_RECORDING_AVAILABILITY_CHANGED, // Update events: USER_SEARCH_FOR_UPDATE, diff --git a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs index f0bca207..7d701670 100644 --- a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs +++ b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs @@ -15,6 +15,7 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv CONFIGURATION_CHANGED, STARTUP_COMPLETED, PLUGINS_RELOADED, + VOICE_RECORDING_AVAILABILITY_CHANGED, } private readonly SemaphoreSlim registrationSemaphore = new(1, 1); @@ -22,20 +23,23 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv private readonly SettingsManager settingsManager; private readonly MessageBus messageBus; private readonly RustService rustService; + private readonly VoiceRecordingAvailabilityService voiceRecordingAvailabilityService; public GlobalShortcutService( ILogger logger, SettingsManager settingsManager, MessageBus messageBus, - RustService rustService) + RustService rustService, + VoiceRecordingAvailabilityService voiceRecordingAvailabilityService) { this.logger = logger; this.settingsManager = settingsManager; this.messageBus = messageBus; this.rustService = rustService; + this.voiceRecordingAvailabilityService = voiceRecordingAvailabilityService; this.messageBus.RegisterComponent(this); - this.ApplyFilters([], [Event.CONFIGURATION_CHANGED, Event.PLUGINS_RELOADED, Event.STARTUP_COMPLETED]); + this.ApplyFilters([], [Event.CONFIGURATION_CHANGED, Event.PLUGINS_RELOADED, Event.STARTUP_COMPLETED, Event.VOICE_RECORDING_AVAILABILITY_CHANGED]); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -75,6 +79,13 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv await this.RegisterAllShortcuts(ShortcutSyncSource.PLUGINS_RELOADED); break; + + case Event.VOICE_RECORDING_AVAILABILITY_CHANGED: + if (!IS_STARTUP_COMPLETED) + return; + + await this.RegisterAllShortcuts(ShortcutSyncSource.VOICE_RECORDING_AVAILABILITY_CHANGED); + break; } } @@ -152,8 +163,9 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv private bool IsShortcutAllowed(Shortcut name) => name switch { // Voice recording is a preview feature: - Shortcut.VOICE_RECORDING_TOGGLE => PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.settingsManager), - + Shortcut.VOICE_RECORDING_TOGGLE => PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.settingsManager) + && this.voiceRecordingAvailabilityService.IsAvailable, + // Other shortcuts are always allowed: _ => true, }; diff --git a/app/MindWork AI Studio/Tools/Services/VoiceRecordingAvailabilityService.cs b/app/MindWork AI Studio/Tools/Services/VoiceRecordingAvailabilityService.cs new file mode 100644 index 00000000..010176c7 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/VoiceRecordingAvailabilityService.cs @@ -0,0 +1,23 @@ +namespace AIStudio.Tools.Services; + +public sealed class VoiceRecordingAvailabilityService +{ + private readonly Lock stateLock = new(); + + public bool IsAvailable { get; private set; } = true; + + public string? DisableReason { get; private set; } + + public bool TryDisable(string reason) + { + lock (this.stateLock) + { + if (!this.IsAvailable) + return false; + + this.IsAvailable = false; + this.DisableReason = reason; + return true; + } + } +} diff --git a/app/MindWork AI Studio/Tools/SoundEffectsInitializationResult.cs b/app/MindWork AI Studio/Tools/SoundEffectsInitializationResult.cs new file mode 100644 index 00000000..70ec2c10 --- /dev/null +++ b/app/MindWork AI Studio/Tools/SoundEffectsInitializationResult.cs @@ -0,0 +1,10 @@ +namespace AIStudio.Tools; + +public sealed class SoundEffectsInitializationResult +{ + public bool Success { get; init; } + + public string[] FailedPaths { get; init; } = []; + + public string? ErrorMessage { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/audio.js b/app/MindWork AI Studio/wwwroot/audio.js index 689bc50f..4e9f40b5 100644 --- a/app/MindWork AI Studio/wwwroot/audio.js +++ b/app/MindWork AI Studio/wwwroot/audio.js @@ -21,59 +21,68 @@ const SOUND_EFFECT_PATHS = [ '/sounds/transcription_done.ogg' ]; +function createSoundEffectsInitResult(success, failedPaths = [], errorMessage = null) { + return { + success: success, + failedPaths: failedPaths, + errorMessage: errorMessage + }; +} + // Initialize the audio context with low-latency settings. // Should be called from a user interaction (click, keypress) // to satisfy browser autoplay policies: window.initSoundEffects = async function() { - if (soundEffectContext && soundEffectContext.state !== 'closed') { - // Already initialized, just ensure it's running: - if (soundEffectContext.state === 'suspended') { - await soundEffectContext.resume(); - } - - return; - } - try { - // Create the context with the interactive latency hint for the lowest latency: - soundEffectContext = new (window.AudioContext || window.webkitAudioContext)({ - latencyHint: 'interactive' - }); + if (soundEffectContext && soundEffectContext.state !== 'closed') { + // Already initialized, just ensure it's running: + if (soundEffectContext.state === 'suspended') { + await soundEffectContext.resume(); + } + } else { + // Create the context with the interactive latency hint for the lowest latency: + soundEffectContext = new (window.AudioContext || window.webkitAudioContext)({ + latencyHint: 'interactive' + }); - // Resume immediately (needed for Safari/macOS): - if (soundEffectContext.state === 'suspended') { - await soundEffectContext.resume(); + // Resume immediately (needed for Safari/macOS): + if (soundEffectContext.state === 'suspended') { + await soundEffectContext.resume(); + } + + // Reset the queue timing: + nextAvailablePlayTime = 0; + + // + // Play a very short silent buffer to "warm up" the audio pipeline. + // This helps prevent the first real sound from being cut off: + // + const silentBuffer = soundEffectContext.createBuffer(1, 1, soundEffectContext.sampleRate); + const silentSource = soundEffectContext.createBufferSource(); + silentSource.buffer = silentBuffer; + silentSource.connect(soundEffectContext.destination); + silentSource.start(0); + + console.log('Sound effects - AudioContext initialized with latency:', soundEffectContext.baseLatency); } - // Reset the queue timing: - nextAvailablePlayTime = 0; - - // - // Play a very short silent buffer to "warm up" the audio pipeline. - // This helps prevent the first real sound from being cut off: - // - const silentBuffer = soundEffectContext.createBuffer(1, 1, soundEffectContext.sampleRate); - const silentSource = soundEffectContext.createBufferSource(); - silentSource.buffer = silentBuffer; - silentSource.connect(soundEffectContext.destination); - silentSource.start(0); - - console.log('Sound effects - AudioContext initialized with latency:', soundEffectContext.baseLatency); - // Preload all sound effects in parallel: if (!soundEffectsPreloaded) { - await window.preloadSoundEffects(); + return await window.preloadSoundEffects(); } + + return createSoundEffectsInitResult(true); } catch (error) { console.warn('Failed to initialize sound effects:', error); + return createSoundEffectsInitResult(false, [], error?.message || String(error)); } }; // Preload all sound effect files into the cache: window.preloadSoundEffects = async function() { if (soundEffectsPreloaded) { - return; + return createSoundEffectsInitResult(true); } // Ensure that the context exists: @@ -84,10 +93,15 @@ window.preloadSoundEffects = async function() { } console.log('Sound effects - preloading', SOUND_EFFECT_PATHS.length, 'sound files...'); + const failedPaths = []; const preloadPromises = SOUND_EFFECT_PATHS.map(async (soundPath) => { try { const response = await fetch(soundPath); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const arrayBuffer = await response.arrayBuffer(); const audioBuffer = await soundEffectContext.decodeAudioData(arrayBuffer); soundEffectCache.set(soundPath, audioBuffer); @@ -95,12 +109,20 @@ window.preloadSoundEffects = async function() { console.log('Sound effects - preloaded:', soundPath, 'duration:', audioBuffer.duration.toFixed(2), 's'); } catch (error) { console.warn('Sound effects - failed to preload:', soundPath, error); + failedPaths.push(soundPath); } }); await Promise.all(preloadPromises); - soundEffectsPreloaded = true; - console.log('Sound effects - all files preloaded'); + soundEffectsPreloaded = failedPaths.length === 0; + + if (soundEffectsPreloaded) { + console.log('Sound effects - all files preloaded'); + return createSoundEffectsInitResult(true); + } + + console.warn('Sound effects - preload finished with failures:', failedPaths); + return createSoundEffectsInitResult(false, failedPaths, 'One or more sound effects could not be loaded.'); }; window.playSound = async function(soundPath) { diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index ed3c9834..11a01004 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -16,4 +16,5 @@ - Improved the validation of additional API parameters in the advanced provider settings to help catch formatting mistakes earlier. - Improved the app startup resilience by allowing AI Studio to continue without Qdrant if it fails to initialize. - Fixed an issue where assistants hidden via configuration plugins still appear in "Send to ..." menus. Thanks, Gunnar, for reporting this issue. +- Fixed an issue with voice recording where AI Studio could log errors and keep the feature available even though required parts failed to initialize. Voice recording is now disabled automatically for the current session in that case. - Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks, Inga, for reporting this issue and providing some context on how to reproduce it. \ No newline at end of file From 424006962b990238658a12cb9416cfc9e936595b Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 21 Mar 2026 12:54:52 +0100 Subject: [PATCH 18/27] Updated .NET (#699) --- .../MindWork AI Studio.csproj | 2 +- app/MindWork AI Studio/packages.lock.json | 24 +++++++++---------- .../wwwroot/changelog/v26.3.1.md | 3 ++- metadata.txt | 6 ++--- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj index d48580d0..7f494e0b 100644 --- a/app/MindWork AI Studio/MindWork AI Studio.csproj +++ b/app/MindWork AI Studio/MindWork AI Studio.csproj @@ -49,7 +49,7 @@ - + diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index 5770dcec..64dc0ee4 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -28,18 +28,18 @@ }, "Microsoft.Extensions.FileProviders.Embedded": { "type": "Direct", - "requested": "[9.0.12, )", - "resolved": "9.0.12", - "contentHash": "mJ89qzHqx6BWhD6ATEkWXQ3QGKkSy1zyALJOLjGB0N0O4znKPafR9DjEkKunpWpUQuvnudsrUdQCfseHIQl+Vw==", + "requested": "[9.0.14, )", + "resolved": "9.0.14", + "contentHash": "Mw7HO29yv8DIo2e//a/OdK1lFu47v7k9BaLQmdTp75i+i867FlgfS54fKuJl8KCC5YBCh8ov2+q9DHC5tLIoMg==", "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "9.0.12" + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.14" } }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[9.0.13, )", - "resolved": "9.0.13", - "contentHash": "f7t15I9ZXV7fNk3FIzPAlkJNG1A1tkSeDpRh+TFWEToGGqA+uj6uqU15I8YOkkYICNY2tqOVm2CMe6ScPFPwEg==" + "requested": "[9.0.14, )", + "resolved": "9.0.14", + "contentHash": "+MeWjj5sGq6Oj/l0E9RPMgXDyCIPxczzCbGuvuVTZFEGiy2S/atsfoAoKUnkEin/GeGpN+HenCzRmiQKSc99eQ==" }, "MudBlazor": { "type": "Direct", @@ -182,10 +182,10 @@ }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", - "resolved": "9.0.12", - "contentHash": "DIRWbcei4olf0EvIqAXJZiXnsaCCq6RP+sADmbz7FDMHAWIG2eEh50BeT/z9VEgmYfly3bXp2UCuS5hf3KK1Zw==", + "resolved": "9.0.14", + "contentHash": "zQHjufn8oR4VdjtrCQZNTfNKolDeT/VOhF/YFsZqaQMHZzTIMzWD56UpoEMQYbYwjxiTRzRGuNfFlINP0AcC6w==", "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.12" + "Microsoft.Extensions.Primitives": "9.0.14" } }, "Microsoft.Extensions.Localization": { @@ -223,8 +223,8 @@ }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "9.0.12", - "contentHash": "nmGbgxTfuvuEdcQ9NH5DEwAKDKB+c39dAcKQ4+sb8WpGA3pMIgAJfowC+aRH/6gFmdRq2ssRp031Uvv7rTrOMg==" + "resolved": "9.0.14", + "contentHash": "1bP1fEv6MdXvX4TsxrT94AE2aOIPI9p0xgVsxUliB91wDXHUwbBHV1hXKbfu0ZHEdBuYEusyTVoUwUXp71fh8w==" }, "Microsoft.JSInterop": { "type": "Transitive", diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 11a01004..5b653b08 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -17,4 +17,5 @@ - Improved the app startup resilience by allowing AI Studio to continue without Qdrant if it fails to initialize. - Fixed an issue where assistants hidden via configuration plugins still appear in "Send to ..." menus. Thanks, Gunnar, for reporting this issue. - Fixed an issue with voice recording where AI Studio could log errors and keep the feature available even though required parts failed to initialize. Voice recording is now disabled automatically for the current session in that case. -- Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks, Inga, for reporting this issue and providing some context on how to reproduce it. \ No newline at end of file +- Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks, Inga, for reporting this issue and providing some context on how to reproduce it. +- Updated .NET to v9.0.14 \ No newline at end of file diff --git a/metadata.txt b/metadata.txt index 77e953d5..825d821f 100644 --- a/metadata.txt +++ b/metadata.txt @@ -1,11 +1,11 @@ 26.2.2 2026-02-22 14:14:47 UTC 234 -9.0.114 (commit 4c5aac3d56) -9.0.13 (commit 9ecbfd4f3f) +9.0.115 (commit 45056ad45c) +9.0.14 (commit 19c07820cb) 1.93.1 (commit 01f6ddf75) 8.15.0 -1.8.1 +1.8.3 3eb367d4c9e, release osx-arm64 144.0.7543.0 From 070375c964335b001f777fcd784abf0f541a0b1e Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 21 Mar 2026 13:14:12 +0100 Subject: [PATCH 19/27] Released the document analysis assistant (#700) --- AGENTS.md | 1 + .../DocumentAnalysis/DocumentAnalysisAssistant.razor | 1 - app/MindWork AI Studio/Pages/Assistants.razor | 4 ++-- app/MindWork AI Studio/Plugins/configuration/plugin.lua | 4 ++-- .../Settings/DataModel/PreviewFeaturesExtensions.cs | 1 + app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md | 1 + 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 49524e17..6f47bcd2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -193,6 +193,7 @@ please ensure they are clear and concise, avoiding technical jargon where possib following words: - Added +- Released - Improved - Changed - Fixed diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor index edc2868e..4e7a38ee 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor @@ -3,7 +3,6 @@ @using AIStudio.Settings @using AIStudio.Settings.DataModel -
    diff --git a/app/MindWork AI Studio/Pages/Assistants.razor b/app/MindWork AI Studio/Pages/Assistants.razor index 8e4896dc..88fdd68a 100644 --- a/app/MindWork AI Studio/Pages/Assistants.razor +++ b/app/MindWork AI Studio/Pages/Assistants.razor @@ -32,7 +32,7 @@ @if (this.SettingsManager.IsAnyCategoryAssistantVisible("Business", (Components.EMAIL_ASSISTANT, PreviewFeatures.NONE), - (Components.DOCUMENT_ANALYSIS_ASSISTANT, PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025), + (Components.DOCUMENT_ANALYSIS_ASSISTANT, PreviewFeatures.NONE), (Components.MY_TASKS_ASSISTANT, PreviewFeatures.NONE), (Components.AGENDA_ASSISTANT, PreviewFeatures.NONE), (Components.JOB_POSTING_ASSISTANT, PreviewFeatures.NONE), @@ -46,7 +46,7 @@ - + diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 5918b691..c954fd78 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -163,8 +163,8 @@ CONFIG["SETTINGS"] = {} -- Configure the enabled preview features: -- Allowed values are can be found in https://github.com/MindWorkAI/AI-Studio/app/MindWork%20AI%20Studio/Settings/DataModel/PreviewFeatures.cs --- Examples are PRE_WRITER_MODE_2024, PRE_RAG_2024, PRE_DOCUMENT_ANALYSIS_2025. --- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_DOCUMENT_ANALYSIS_2025" } +-- Examples are PRE_WRITER_MODE_2024, PRE_RAG_2024, PRE_SPEECH_TO_TEXT_2026. +-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_SPEECH_TO_TEXT_2026" } -- Configure the preselected provider. -- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"]. diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs index 00399f0b..fa10ecad 100644 --- a/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs @@ -32,6 +32,7 @@ public static class PreviewFeaturesExtensions { PreviewFeatures.PRE_READ_PDF_2025 => true, PreviewFeatures.PRE_PLUGINS_2025 => true, + PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => true, _ => false }; diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 5b653b08..f611bcc8 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -4,6 +4,7 @@ - Added a reminder in chats and assistants that LLMs can make mistakes, helping you double-check important information more easily. - Added the ability to format your user prompt in the chat using icons instead of typing Markdown directly. - Added the ability to load a system prompt from a file when creating or editing chat templates. +- Released the document analysis assistant after an intense testing phase. - Improved the profile selection for assistants and the chat. You can now explicitly choose between the app default profile, no profile, or a specific profile. - Improved the performance by caching the OS language detection and requesting the user language only once per app start. - Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations. From 86e2f1baac6ca2da02aa3dbf79c65a98f46e1e85 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 21 Mar 2026 13:55:37 +0100 Subject: [PATCH 20/27] Updated agent instructions (#701) --- AGENTS.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 6f47bcd2..f960b509 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,14 @@ dotnet run build ``` This builds the .NET app as a Tauri "sidecar" binary, which is required even for development. +### Running .NET builds from an agent +- Do not run `.NET` builds such as `dotnet run build`, `dotnet build`, or similar build commands from an agent. Codex agents can hit a known sandbox issue during `.NET` builds, typically surfacing as `CSSM_ModuleLoad()` or other sandbox-related failures. +- Instead, ask the user to run the `.NET` build locally in their IDE and report the result back. +- Recommend the canonical repo build flow for the user: open an IDE terminal in the repository and run `cd app/Build && dotnet run build`. +- If the context fits better, it is also acceptable to ask the user to start the build using their IDE's built-in build action, as long as it is clear the build must be run locally by the user. +- After asking for the build, wait for the user's feedback before diagnosing issues, making follow-up changes, or suggesting the next step. +- Treat the user's build output, error messages, or success confirmation as the source of truth for further troubleshooting. +- For reference: https://github.com/openai/codex/issues/4915 ### Running Tests Currently, no automated test suite exists in the repository. @@ -177,12 +185,15 @@ Multi-level confidence scheme allows users to control which providers see which ## Important Development Notes - **File changes require Write/Edit tools** - Never use bash commands like `cat <` +- **End of file formatting** - Do not append an extra empty line at the end of files. - **Spaces in paths** - Always quote paths with spaces in bash commands +- **Agent-run .NET builds** - Do not run `.NET` builds from an agent. Ask the user to run the build locally in their IDE, preferably via `cd app/Build && dotnet run build` in an IDE terminal, then wait for their feedback before continuing. - **Debug environment** - Reads `startup.env` file with IPC credentials - **Production environment** - Runtime launches .NET sidecar with environment variables - **MudBlazor** - Component library requires DI setup in Program.cs - **Encryption** - Initialized before Rust service is marked ready - **Message Bus** - Singleton event bus for cross-component communication inside the .NET app +- **Empty lines** - Avoid adding extra empty lines at the end of files. ## Changelogs Changelogs are located in `app/MindWork AI Studio/wwwroot/changelog/` with filenames `vX.Y.Z.md`. These changelogs are meant to be for normal end-users From fb1a45074e200441964da0d9d01eea969a5d90e3 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 21 Mar 2026 14:03:08 +0100 Subject: [PATCH 21/27] Register slide assistant as configurable assistant (#702) --- app/MindWork AI Studio/Plugins/configuration/plugin.lua | 2 +- app/MindWork AI Studio/Settings/ConfigurableAssistant.cs | 1 + app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index c954fd78..37098f77 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -189,7 +189,7 @@ CONFIG["SETTINGS"] = {} -- TEXT_SUMMARIZER_ASSISTANT, EMAIL_ASSISTANT, LEGAL_CHECK_ASSISTANT, -- SYNONYMS_ASSISTANT, MY_TASKS_ASSISTANT, JOB_POSTING_ASSISTANT, -- BIAS_DAY_ASSISTANT, ERI_ASSISTANT, DOCUMENT_ANALYSIS_ASSISTANT, --- I18N_ASSISTANT +-- SLIDE_BUILDER_ASSISTANT, I18N_ASSISTANT -- CONFIG["SETTINGS"]["DataApp.HiddenAssistants"] = { "ERI_ASSISTANT", "I18N_ASSISTANT" } -- Configure a global shortcut for starting and stopping dictation. diff --git a/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs b/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs index f1076d9e..d2a8a76e 100644 --- a/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs +++ b/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs @@ -23,6 +23,7 @@ public enum ConfigurableAssistant BIAS_DAY_ASSISTANT, ERI_ASSISTANT, DOCUMENT_ANALYSIS_ASSISTANT, + SLIDE_BUILDER_ASSISTANT, // ReSharper disable InconsistentNaming I18N_ASSISTANT, diff --git a/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs b/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs index cb4e7fdc..29db307d 100644 --- a/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs +++ b/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs @@ -59,6 +59,7 @@ public static class AssistantVisibilityExtensions Components.BIAS_DAY_ASSISTANT => ConfigurableAssistant.BIAS_DAY_ASSISTANT, Components.ERI_ASSISTANT => ConfigurableAssistant.ERI_ASSISTANT, Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfigurableAssistant.DOCUMENT_ANALYSIS_ASSISTANT, + Components.SLIDE_BUILDER_ASSISTANT => ConfigurableAssistant.SLIDE_BUILDER_ASSISTANT, Components.I18N_ASSISTANT => ConfigurableAssistant.I18N_ASSISTANT, _ => ConfigurableAssistant.UNKNOWN, From df50fdb8b81e8342c32d7f3a81c2c8364c647e32 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 21 Mar 2026 14:26:36 +0100 Subject: [PATCH 22/27] Updated readme & the quick start guide (#703) --- README.md | 6 +++--- app/MindWork AI Studio/Pages/Home.razor.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 624cbfc8..a594ff41 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Since November 2024: Work on RAG (integration of your data and files) has begun. - [x] ~~Runtime: Extract data from txt / md / pdf / docx / xlsx files (PR [#374](https://github.com/MindWorkAI/AI-Studio/pull/374))~~ - [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs) - [x] ~~App: Implement dialog for checking & handling [pandoc](https://pandoc.org/) installation ([PR #393](https://github.com/MindWorkAI/AI-Studio/pull/393), [PR #487](https://github.com/MindWorkAI/AI-Studio/pull/487))~~ -- [ ] App: Implement external embedding providers +- [x] ~~App: Implement external embedding providers ([PR #654](https://github.com/MindWorkAI/AI-Studio/pull/654))~~ - [ ] App: Implement the process to vectorize one local file using embeddings - [x] ~~Runtime: Integration of the vector database [Qdrant](https://github.com/qdrant/qdrant) ([PR #580](https://github.com/MindWorkAI/AI-Studio/pull/580))~~ - [ ] App: Implement the continuous process of vectorizing data @@ -67,7 +67,7 @@ Since March 2025: We have started developing the plugin system. There will be la - [x] ~~Provide MindWork AI Studio in German ([PR #430](https://github.com/MindWorkAI/AI-Studio/pull/430), [PR #446](https://github.com/MindWorkAI/AI-Studio/pull/446), [PR #451](https://github.com/MindWorkAI/AI-Studio/pull/451), [PR #455](https://github.com/MindWorkAI/AI-Studio/pull/455), [PR #458](https://github.com/MindWorkAI/AI-Studio/pull/458), [PR #462](https://github.com/MindWorkAI/AI-Studio/pull/462), [PR #469](https://github.com/MindWorkAI/AI-Studio/pull/469), [PR #486](https://github.com/MindWorkAI/AI-Studio/pull/486))~~ - [x] ~~Add configuration plugins, which allow pre-defining some LLM providers in organizations ([PR #491](https://github.com/MindWorkAI/AI-Studio/pull/491), [PR #493](https://github.com/MindWorkAI/AI-Studio/pull/493), [PR #494](https://github.com/MindWorkAI/AI-Studio/pull/494), [PR #497](https://github.com/MindWorkAI/AI-Studio/pull/497))~~ - [ ] Add an app store for plugins, showcasing community-contributed plugins from public GitHub and GitLab repositories. This will enable AI Studio users to discover, install, and update plugins directly within the platform. -- [ ] Add assistant plugins +- [ ] Add assistant plugins ([PR #659](https://github.com/MindWorkAI/AI-Studio/pull/659)) @@ -79,6 +79,7 @@ Since March 2025: We have started developing the plugin system. There will be la +- v26.2.2: Added Qdrant as a building block for our local RAG preview, added an embedding test option to validate embedding providers, and improved enterprise and configuration plugins with preselected providers, additive preview features, support for multiple configurations, and more reliable synchronization. - v26.1.1: Added the option to attach files, including images, to chat templates; added support for source code file attachments in chats and document analysis; added a preview feature for recording your own voice for transcription; fixed various bugs in provider dialogs and profile selection. - v0.10.0: Added support for newer models like Mistral 3 & GPT 5.2, OpenRouter as LLM and embedding provider, the possibility to use file attachments in chats, and support for images as input. - v0.9.51: Added support for [Perplexity](https://www.perplexity.ai/); citations added so that LLMs can provide source references (e.g., some OpenAI models, Perplexity); added support for OpenAI's Responses API so that all text LLMs from OpenAI now work in MindWork AI Studio, including Deep Research models; web searches are now possible (some OpenAI models, Perplexity). @@ -90,7 +91,6 @@ Since March 2025: We have started developing the plugin system. There will be la - v0.9.39: Added the plugin system as a preview feature. - v0.9.31: Added Helmholtz & GWDG as LLM providers. This is a huge improvement for many researchers out there who can use these providers for free. We added DeepSeek as a provider as well. - v0.9.29: Added agents to support the RAG process (selecting the best data sources & validating retrieved data as part of the augmentation process) -- v0.9.26+: Added RAG for external data sources using our [ERI interface](https://mindworkai.org/#eri---external-retrieval-interface) as a preview feature. diff --git a/app/MindWork AI Studio/Pages/Home.razor.cs b/app/MindWork AI Studio/Pages/Home.razor.cs index 610d22b0..04c35db6 100644 --- a/app/MindWork AI Studio/Pages/Home.razor.cs +++ b/app/MindWork AI Studio/Pages/Home.razor.cs @@ -95,7 +95,7 @@ public partial class Home : MSGComponentBase ## Step 4: Load OpenAI Models 1. Ensure you have an internet connection and your API key is valid. 2. Click "Reload" to retrieve a list of available OpenAI models. - 3. Select "gpt-4o" to use the latest model. + 3. Select "gpt-5.4" to use a current model. 4. Provide a name for this combination of provider, API key, and model. This is called the "instance name". For example, you can name it based on the usage context, such as "Personal OpenAI" or "Work OpenAI". ## Step 5: Save the Provider From a2bd67eda308a3333fb4a6832ab77d838b29ce25 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 21 Mar 2026 18:05:06 +0100 Subject: [PATCH 23/27] Added config option for start page (#704) --- .../Assistants/I18N/allTexts.lua | 30 +++++ .../Settings/SettingsPanelApp.razor | 1 + .../Settings/SettingsPanelApp.razor.cs | 9 ++ app/MindWork AI Studio/Pages/Home.razor.cs | 17 +++ .../Plugins/configuration/plugin.lua | 10 ++ .../plugin.lua | 30 +++++ .../plugin.lua | 30 +++++ app/MindWork AI Studio/Settings/ConfigMeta.cs | 52 +++++++++ .../ConfigurationSelectDataFactory.cs | 11 ++ .../Settings/DataModel/Data.cs | 5 + .../Settings/DataModel/DataApp.cs | 5 + .../Settings/DataModel/StartPage.cs | 12 ++ .../Settings/DataModel/StartPageExtensions.cs | 17 +++ .../Settings/ManagedConfiguration.Parsing.cs | 103 +++++++++++++++++- .../Settings/ManagedConfiguration.cs | 94 +++++++++++----- .../Settings/ManagedConfigurationMode.cs | 14 +++ .../Settings/ManagedEditableDefaultState.cs | 8 ++ .../Settings/SettingsManager.cs | 12 ++ .../Tools/PluginSystem/PluginConfiguration.cs | 3 + .../PluginSystem/PluginFactory.Loading.cs | 4 + .../wwwroot/changelog/v26.3.1.md | 1 + 21 files changed, 436 insertions(+), 32 deletions(-) create mode 100644 app/MindWork AI Studio/Settings/DataModel/StartPage.cs create mode 100644 app/MindWork AI Studio/Settings/DataModel/StartPageExtensions.cs create mode 100644 app/MindWork AI Studio/Settings/ManagedConfigurationMode.cs create mode 100644 app/MindWork AI Studio/Settings/ManagedEditableDefaultState.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index e899d79a..6d4cc501 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -2269,6 +2269,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1364944735"] -- Select preview features UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1439783084"] = "Select preview features" +-- Your organization provided a default start page, but you can still change it. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1454730224"] = "Your organization provided a default start page, but you can still change it." + -- Select the desired behavior for the navigation bar. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Select the desired behavior for the navigation bar." @@ -2317,6 +2320,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591284123"] -- Administration settings are visible UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591866808"] = "Administration settings are visible" +-- Choose which page AI Studio should open first when you start the app. Changes take effect the next time you launch AI Studio. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2655930524"] = "Choose which page AI Studio should open first when you start the app. Changes take effect the next time you launch AI Studio." + -- Save energy? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?" @@ -2365,6 +2371,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T71162186"] = -- Energy saving is disabled UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T716338721"] = "Energy saving is disabled" +-- Start page +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T78084670"] = "Start page" + -- Preview feature visibility UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"] = "Preview feature visibility" @@ -5884,12 +5893,21 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1188453609 -- Show also prototype features: these are works in progress; expect bugs and missing features UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1245257804"] = "Show also prototype features: these are works in progress; expect bugs and missing features" +-- Settings +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1258653480"] = "Settings" + -- No key is sending the input UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1311973034"] = "No key is sending the input" -- Navigation never expands, no tooltips UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1402851833"] = "Navigation never expands, no tooltips" +-- Welcome +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1485461907"] = "Welcome" + +-- Assistants +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1614176092"] = "Assistants" + -- Store chats automatically UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1664293672"] = "Store chats automatically" @@ -5914,6 +5932,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2195945406 -- Install updates manually UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T220653235"] = "Install updates manually" +-- Plugins +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2222816203"] = "Plugins" + -- Also show features ready for release; these should be stable UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2301448762"] = "Also show features ready for release; these should be stable" @@ -5935,6 +5956,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2566503670 -- No minimum confidence level chosen UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2828607242"] = "No minimum confidence level chosen" +-- Supporters +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2929332068"] = "Supporters" + -- Do not specify the language UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2960082609"] = "Do not specify the language" @@ -5968,9 +5992,15 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3711207137 -- Also show features in alpha: these are in development; expect bugs and missing features UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4146964761"] = "Also show features in alpha: these are in development; expect bugs and missing features" +-- Information +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4256323669"] = "Information" + -- All preview features are hidden UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4289410063"] = "All preview features are hidden" +-- Chat +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T578410699"] = "Chat" + -- When possible, use the LLM provider which was used for each chat in the first place UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T75376144"] = "When possible, use the LLM provider which was used for each chat in the first place" diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor index cbc33d79..c5ae753f 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor @@ -17,6 +17,7 @@ + diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs index 70b6d24a..a5fbc06b 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs @@ -11,6 +11,15 @@ public partial class SettingsPanelApp : SettingsPanelBase var secret = EnterpriseEncryption.GenerateSecret(); await this.RustService.CopyText2Clipboard(this.Snackbar, secret); } + + private string GetStartPageHelpText() + { + var helpText = T("Choose which page AI Studio should open first when you start the app. Changes take effect the next time you launch AI Studio."); + if (!ManagedConfiguration.TryGet(x => x.App, x => x.StartPage, out var meta) || meta.ManagedMode is not ManagedConfigurationMode.EDITABLE_DEFAULT) + return helpText; + + return $"{helpText} {T("Your organization provided a default start page, but you can still change it.")}"; + } private IEnumerable> GetFilteredTranscriptionProviders() { diff --git a/app/MindWork AI Studio/Pages/Home.razor.cs b/app/MindWork AI Studio/Pages/Home.razor.cs index 04c35db6..40431683 100644 --- a/app/MindWork AI Studio/Pages/Home.razor.cs +++ b/app/MindWork AI Studio/Pages/Home.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Components; +using AIStudio.Settings.DataModel; using Microsoft.AspNetCore.Components; @@ -10,6 +11,9 @@ public partial class Home : MSGComponentBase { [Inject] private HttpClient HttpClient { get; init; } = null!; + + [Inject] + private NavigationManager NavigationManager { get; init; } = null!; private string LastChangeContent { get; set; } = string.Empty; @@ -27,6 +31,19 @@ public partial class Home : MSGComponentBase _ = this.ReadLastChangeAsync(); } + protected override Task OnAfterRenderAsync(bool firstRender) + { + if (this.SettingsManager.StartupStartPageRedirectHandled || !this.SettingsManager.HasCompletedInitialSettingsLoad) + return base.OnAfterRenderAsync(firstRender); + + this.SettingsManager.StartupStartPageRedirectHandled = true; + var startPageRoute = this.SettingsManager.ConfigurationData.App.StartPage.ToRoute(); + if (!string.IsNullOrWhiteSpace(startPageRoute)) + this.NavigationManager.NavigateTo(startPageRoute, replace: true); + + return base.OnAfterRenderAsync(firstRender); + } + private void InitializeAdvantagesItems() { this.itemsAdvantages = [ diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 37098f77..03a9b0f4 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -146,6 +146,16 @@ CONFIG["SETTINGS"] = {} -- Allowed values are: MANUAL, AUTOMATIC -- CONFIG["SETTINGS"]["DataApp.UpdateInstallation"] = "MANUAL" +-- Configure the page that should be opened when AI Studio starts. +-- Allowed values are: HOME, CHAT, ASSISTANTS, INFORMATION, PLUGINS, SUPPORTERS, SETTINGS +-- CONFIG["SETTINGS"]["DataApp.StartPage"] = "CHAT" +-- +-- Allow users to change the configured start page locally. +-- Allowed values are: true, false +-- When set to true, the configured start page becomes the organization default, +-- but users can still choose another start page in the app settings. +-- CONFIG["SETTINGS"]["DataApp.StartPage.AllowUserOverride"] = true + -- Configure the user permission to add providers: -- Allowed values are: true, false -- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 0a94f7a7..e9571a6c 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -2271,6 +2271,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1364944735"] -- Select preview features UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1439783084"] = "Vorschaufunktionen auswählen" +-- Your organization provided a default start page, but you can still change it. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1454730224"] = "Ihre Organisation hat eine Standard-Startseite festgelegt, die Sie jedoch ändern können." + -- Select the desired behavior for the navigation bar. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Wählen Sie das gewünschte Verhalten für die Navigationsleiste aus." @@ -2319,6 +2322,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591284123"] -- Administration settings are visible UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591866808"] = "Die Optionen für die Administration sind sichtbar." +-- Choose which page AI Studio should open first when you start the app. Changes take effect the next time you launch AI Studio. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2655930524"] = "Wählen Sie aus, welche Seite AI Studio beim Start der App zuerst öffnen soll. Änderungen werden beim nächsten Start von AI Studio wirksam." + -- Save energy? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Energie sparen?" @@ -2367,6 +2373,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T71162186"] = -- Energy saving is disabled UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T716338721"] = "Energiesparmodus ist deaktiviert" +-- Start page +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T78084670"] = "Startseite" + -- Preview feature visibility UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"] = "Sichtbarkeit der Vorschaufunktion" @@ -5886,12 +5895,21 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1188453609 -- Show also prototype features: these are works in progress; expect bugs and missing features UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1245257804"] = "Auch Prototyp-Funktionen anzeigen: Diese befinden sich noch in der Entwicklung; Fehler und fehlende Funktionen sind zu erwarten" +-- Settings +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1258653480"] = "Einstellungen" + -- No key is sending the input; you have to click the send button UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1311973034"] = "Keine Taste sendet die Eingabe; Sie müssen auf die Schaltfläche klicken, um zu senden" -- Navigation never expands, no tooltips; there are only icons UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1402851833"] = "Navigationsleiste wird nie erweitert, keine Tooltips; es werden nur Icons angezeigt" +-- Welcome +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1485461907"] = "Willkommen" + +-- Assistants +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1614176092"] = "Assistenten" + -- Store chats automatically UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1664293672"] = "Chats automatisch speichern" @@ -5916,6 +5934,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2195945406 -- Install updates manually UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T220653235"] = "Updates manuell installieren" +-- Plugins +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2222816203"] = "Plugins" + -- Also show features ready for release; these should be stable UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2301448762"] = "Auch Funktionen anzeigen, die bereit für die Veröffentlichung sind; diese sollten stabil sein." @@ -5937,6 +5958,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2566503670 -- No minimum confidence level chosen UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2828607242"] = "Kein Mindestvertrauensniveau ausgewählt" +-- Supporters +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2929332068"] = "Unterstützer" + -- Do not specify the language UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2960082609"] = "Sprache nicht festlegen" @@ -5970,9 +5994,15 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3711207137 -- Also show features in alpha: these are in development; expect bugs and missing features UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4146964761"] = "Zeige auch Funktionen im Alpha-Stadium an: Diese befinden sich in der Entwicklung; es werden Fehler und fehlende Funktionen auftreten." +-- Information +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4256323669"] = "Information" + -- All preview features are hidden UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4289410063"] = "Alle Vorschaufunktionen sind ausgeblendet" +-- Chat +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T578410699"] = "Chat" + -- When possible, use the LLM provider which was used for each chat in the first place UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T75376144"] = "Wenn möglich, verwende den LLM-Anbieter, der ursprünglich für jeden Chat verwendet wurde." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 2849c6d2..71f6c65a 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -2271,6 +2271,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1364944735"] -- Select preview features UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1439783084"] = "Select preview features" +-- Your organization provided a default start page, but you can still change it. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1454730224"] = "Your organization provided a default start page, but you can still change it." + -- Select the desired behavior for the navigation bar. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Select the desired behavior for the navigation bar." @@ -2319,6 +2322,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591284123"] -- Administration settings are visible UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591866808"] = "Administration settings are visible" +-- Choose which page AI Studio should open first when you start the app. Changes take effect the next time you launch AI Studio. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2655930524"] = "Choose which page AI Studio should open first when you start the app. Changes take effect the next time you launch AI Studio." + -- Save energy? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?" @@ -2367,6 +2373,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T71162186"] = -- Energy saving is disabled UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T716338721"] = "Energy saving is disabled" +-- Start page +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T78084670"] = "Start page" + -- Preview feature visibility UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"] = "Preview feature visibility" @@ -5886,12 +5895,21 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1188453609 -- Show also prototype features: these are works in progress; expect bugs and missing features UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1245257804"] = "Show also prototype features: these are works in progress; expect bugs and missing features" +-- Settings +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1258653480"] = "Settings" + -- No key is sending the input; you have to click the send button UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1311973034"] = "No key is sending the input; you have to click the send button" -- Navigation never expands, no tooltips; there are only icons UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1402851833"] = "Navigation never expands, no tooltips; there are only icons" +-- Welcome +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1485461907"] = "Welcome" + +-- Assistants +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1614176092"] = "Assistants" + -- Store chats automatically UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1664293672"] = "Store chats automatically" @@ -5916,6 +5934,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2195945406 -- Install updates manually UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T220653235"] = "Install updates manually" +-- Plugins +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2222816203"] = "Plugins" + -- Also show features ready for release; these should be stable UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2301448762"] = "Also show features ready for release; these should be stable" @@ -5937,6 +5958,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2566503670 -- No minimum confidence level chosen UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2828607242"] = "No minimum confidence level chosen" +-- Supporters +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2929332068"] = "Supporters" + -- Do not specify the language UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2960082609"] = "Do not specify the language" @@ -5970,9 +5994,15 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3711207137 -- Also show features in alpha: these are in development; expect bugs and missing features UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4146964761"] = "Also show features in alpha: these are in development; expect bugs and missing features" +-- Information +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4256323669"] = "Information" + -- All preview features are hidden UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4289410063"] = "All preview features are hidden" +-- Chat +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T578410699"] = "Chat" + -- When possible, use the LLM provider which was used for each chat in the first place UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T75376144"] = "When possible, use the LLM provider which was used for each chat in the first place" diff --git a/app/MindWork AI Studio/Settings/ConfigMeta.cs b/app/MindWork AI Studio/Settings/ConfigMeta.cs index 6b81c3e8..46a248b3 100644 --- a/app/MindWork AI Studio/Settings/ConfigMeta.cs +++ b/app/MindWork AI Studio/Settings/ConfigMeta.cs @@ -36,6 +36,16 @@ public record ConfigMeta : ConfigMetaBase /// The ID of the plugin that locked this configuration. /// public Guid LockedByConfigPluginId { get; private set; } + + /// + /// How this setting is managed by a configuration plugin, if at all. + /// + public ManagedConfigurationMode? ManagedMode { get; private set; } + + /// + /// The ID of the plugin that currently provides an editable default value. + /// + public Guid EditableDefaultByConfigPluginId { get; private set; } /// /// The default value for the configuration property. This is used when resetting the property to its default state. @@ -65,6 +75,8 @@ public record ConfigMeta : ConfigMetaBase { this.IsLocked = true; this.LockedByConfigPluginId = pluginId; + this.ManagedMode = ManagedConfigurationMode.LOCKED; + this.EditableDefaultByConfigPluginId = Guid.Empty; } /// @@ -75,6 +87,9 @@ public record ConfigMeta : ConfigMetaBase { this.IsLocked = false; this.LockedByConfigPluginId = Guid.Empty; + if (this.ManagedMode is ManagedConfigurationMode.LOCKED) + this.ManagedMode = null; + this.Reset(); } @@ -85,6 +100,30 @@ public record ConfigMeta : ConfigMetaBase { this.IsLocked = false; this.LockedByConfigPluginId = Guid.Empty; + if (this.ManagedMode is ManagedConfigurationMode.LOCKED) + this.ManagedMode = null; + } + + /// + /// Marks the setting as having an editable default provided by a configuration plugin. + /// + public void SetEditableDefaultConfiguration(Guid pluginId) + { + this.IsLocked = false; + this.LockedByConfigPluginId = Guid.Empty; + this.ManagedMode = ManagedConfigurationMode.EDITABLE_DEFAULT; + this.EditableDefaultByConfigPluginId = pluginId; + } + + /// + /// Clears the editable-default state without changing the current value. + /// + public void ClearEditableDefaultConfiguration() + { + if (this.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT) + this.ManagedMode = null; + + this.EditableDefaultByConfigPluginId = Guid.Empty; } /// @@ -129,4 +168,17 @@ public record ConfigMeta : ConfigMetaBase if (memberExpression.Member is System.Reflection.PropertyInfo propertyInfo) propertyInfo.SetValue(configInstance, value); } + + /// + /// Gets the current value of the configuration property. + /// + public TValue GetValue() + { + var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData); + var memberExpression = this.PropertyExpression.GetMemberExpression(); + if (memberExpression.Member is System.Reflection.PropertyInfo propertyInfo && propertyInfo.GetValue(configInstance) is TValue value) + return value; + + return default!; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs b/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs index 0f8b9d5d..c6465e5b 100644 --- a/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs +++ b/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs @@ -133,6 +133,17 @@ public static class ConfigurationSelectDataFactory yield return new(TB("Always expand navigation"), NavBehavior.ALWAYS_EXPAND); } + public static IEnumerable> GetStartPageData() + { + yield return new(TB("Welcome"), StartPage.HOME); + yield return new(TB("Chat"), StartPage.CHAT); + yield return new(TB("Assistants"), StartPage.ASSISTANTS); + yield return new(TB("Information"), StartPage.INFORMATION); + yield return new(TB("Plugins"), StartPage.PLUGINS); + yield return new(TB("Supporters"), StartPage.SUPPORTERS); + yield return new(TB("Settings"), StartPage.SETTINGS); + } + public static IEnumerable> GetIconSourcesData() { foreach (var source in Enum.GetValues()) diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs index 0013fe92..d6339739 100644 --- a/app/MindWork AI Studio/Settings/DataModel/Data.cs +++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs @@ -51,6 +51,11 @@ public sealed class Data /// public List EnabledPlugins { get; set; } = []; + /// + /// Metadata for managed settings that use a plugin-provided editable default. + /// + public Dictionary ManagedEditableDefaults { get; set; } = []; + /// /// The next provider number to use. /// diff --git a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs index a1def46f..3a62164b 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs @@ -52,6 +52,11 @@ public sealed class DataApp(Expression>? configSelection = n /// public NavBehavior NavigationBehavior { get; set; } = NavBehavior.NEVER_EXPAND_USE_TOOLTIPS; + /// + /// Which page should be opened first when the app starts? + /// + public StartPage StartPage { get; set; } = ManagedConfiguration.Register(configSelection, n => n.StartPage, StartPage.HOME); + /// /// The visibility setting for previews features. /// diff --git a/app/MindWork AI Studio/Settings/DataModel/StartPage.cs b/app/MindWork AI Studio/Settings/DataModel/StartPage.cs new file mode 100644 index 00000000..07b95b1a --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/StartPage.cs @@ -0,0 +1,12 @@ +namespace AIStudio.Settings.DataModel; + +public enum StartPage +{ + HOME, + CHAT, + ASSISTANTS, + INFORMATION, + PLUGINS, + SUPPORTERS, + SETTINGS, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/StartPageExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/StartPageExtensions.cs new file mode 100644 index 00000000..d37ca52b --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/StartPageExtensions.cs @@ -0,0 +1,17 @@ +namespace AIStudio.Settings.DataModel; + +public static class StartPageExtensions +{ + public static string ToRoute(this StartPage startPage) => startPage switch + { + StartPage.HOME => string.Empty, + StartPage.CHAT => Routes.CHAT, + StartPage.ASSISTANTS => Routes.ASSISTANTS, + StartPage.INFORMATION => Routes.ABOUT, + StartPage.PLUGINS => Routes.PLUGINS, + StartPage.SUPPORTERS => Routes.SUPPORTERS, + StartPage.SETTINGS => Routes.SETTINGS, + + _ => string.Empty, + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs index e4cf5f2e..4b453d27 100644 --- a/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs @@ -63,8 +63,10 @@ public static partial class ManagedConfiguration if(dryRun) return successful; - - return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + + var settingName = SettingName(propertyExpression); + var managedMode = ReadManagedConfigurationMode(propertyExpression, settings); + return HandleParsedScalarValue(configPluginId, dryRun, successful, configMeta, configuredValue, managedMode, settingName); } /// @@ -128,8 +130,10 @@ public static partial class ManagedConfiguration if(dryRun) return successful; - - return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + + var settingName = SettingName(propertyExpression); + var managedMode = ReadManagedConfigurationMode(propertyExpression, settings); + return HandleParsedScalarValue(configPluginId, dryRun, successful, configMeta, configuredValue, managedMode, settingName); } /// @@ -216,7 +220,9 @@ public static partial class ManagedConfiguration } } - return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + var settingName = SettingName(propertyExpression); + var managedMode = ReadManagedConfigurationMode(propertyExpression, settings); + return HandleParsedScalarValue(configPluginId, dryRun, successful, configMeta, configuredValue, managedMode, settingName); } /// @@ -857,4 +863,91 @@ public static partial class ManagedConfiguration return successful; } + + private static bool HandleParsedScalarValue( + Guid configPluginId, + bool dryRun, + bool successful, + ConfigMeta configMeta, + TValue configuredValue, + ManagedConfigurationMode managedMode, + string settingName) + { + if (dryRun) + return successful; + + switch (successful) + { + case true when managedMode is ManagedConfigurationMode.LOCKED: + ClearEditableDefaultState(settingName); + configMeta.ClearEditableDefaultConfiguration(); + configMeta.SetValue(configuredValue); + configMeta.LockConfiguration(configPluginId); + break; + + case true when managedMode is ManagedConfigurationMode.EDITABLE_DEFAULT: + var currentValueSerialized = SerializeManagedScalarValue(configMeta.GetValue()); + var configuredValueSerialized = SerializeManagedScalarValue(configuredValue); + + string lastAppliedValue; + if (!TryGetEditableDefaultState(settingName, out var editableDefaultState)) + { + configMeta.SetValue(configuredValue); + lastAppliedValue = configuredValueSerialized; + } + else + { + lastAppliedValue = editableDefaultState.LastAppliedValue; + if (string.Equals(currentValueSerialized, lastAppliedValue, StringComparison.Ordinal)) + { + configMeta.SetValue(configuredValue); + lastAppliedValue = configuredValueSerialized; + } + } + + SetEditableDefaultState(settingName, configPluginId, lastAppliedValue); + configMeta.UnlockConfiguration(); + configMeta.SetEditableDefaultConfiguration(configPluginId); + break; + + case false when configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId: + configMeta.ResetLockedConfiguration(); + break; + + case false when configMeta.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT + && TryGetEditableDefaultState(settingName, out var editableDefaultStateToRemove) + && editableDefaultStateToRemove.ConfigPluginId == configPluginId: + configMeta.ClearEditableDefaultConfiguration(); + ClearEditableDefaultState(settingName); + break; + } + + return successful; + } + + private static ManagedConfigurationMode ReadManagedConfigurationMode( + Expression> propertyExpression, + LuaTable settings) + { + var allowUserOverrideSettingName = $"{SettingsManager.ToSettingName(propertyExpression)}.AllowUserOverride"; + if (!settings.TryGetValue(allowUserOverrideSettingName, out var allowUserOverrideValue)) + return ManagedConfigurationMode.LOCKED; + + if (allowUserOverrideValue.TryRead(out var allowUserOverride)) + return allowUserOverride ? ManagedConfigurationMode.EDITABLE_DEFAULT : ManagedConfigurationMode.LOCKED; + + if (allowUserOverrideValue.TryRead(out var allowUserOverrideText) && bool.TryParse(allowUserOverrideText, out allowUserOverride)) + return allowUserOverride ? ManagedConfigurationMode.EDITABLE_DEFAULT : ManagedConfigurationMode.LOCKED; + + return ManagedConfigurationMode.LOCKED; + } + + private static string SerializeManagedScalarValue(TValue value) => value switch + { + null => string.Empty, + string text => text, + IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), + + _ => value.ToString() ?? string.Empty, + }; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs index 363cccc1..0e62f2c6 100644 --- a/app/MindWork AI Studio/Settings/ManagedConfiguration.cs +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs @@ -246,68 +246,68 @@ public static partial class ManagedConfiguration public static bool IsConfigurationLeftOver( Expression> configSelection, Expression> propertyExpression, - IEnumerable availablePlugins) + IReadOnlyList availablePlugins) where TValue : Enum { if (!TryGet(configSelection, propertyExpression, out var configMeta)) return false; - if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) - return false; - - var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); - if (plugin is null) + if (configMeta.LockedByConfigPluginId != Guid.Empty && configMeta.IsLocked) { - configMeta.ResetLockedConfiguration(); - return true; + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); + if (plugin is null) + { + configMeta.ResetLockedConfiguration(); + return true; + } } - return false; + return CleanupEditableDefaultState(configMeta, SettingName(propertyExpression), availablePlugins); } public static bool IsConfigurationLeftOver( Expression> configSelection, Expression> propertyExpression, - IEnumerable availablePlugins) + IReadOnlyList availablePlugins) { if (!TryGet(configSelection, propertyExpression, out var configMeta)) return false; - if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) - return false; - - var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); - if (plugin is null) + if (configMeta.LockedByConfigPluginId != Guid.Empty && configMeta.IsLocked) { - configMeta.ResetLockedConfiguration(); - return true; + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); + if (plugin is null) + { + configMeta.ResetLockedConfiguration(); + return true; + } } - return false; + return CleanupEditableDefaultState(configMeta, SettingName(propertyExpression), availablePlugins); } // ReSharper disable MethodOverloadWithOptionalParameter public static bool IsConfigurationLeftOver( Expression> configSelection, Expression> propertyExpression, - IEnumerable availablePlugins, + IReadOnlyList availablePlugins, ISpanParsable? _ = null) where TValue : struct, ISpanParsable { if (!TryGet(configSelection, propertyExpression, out var configMeta)) return false; - if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) - return false; - - var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); - if (plugin is null) + if (configMeta.LockedByConfigPluginId != Guid.Empty && configMeta.IsLocked) { - configMeta.ResetLockedConfiguration(); - return true; + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); + if (plugin is null) + { + configMeta.ResetLockedConfiguration(); + return true; + } } - return false; + return CleanupEditableDefaultState(configMeta, SettingName(propertyExpression), availablePlugins); } // ReSharper restore MethodOverloadWithOptionalParameter @@ -413,4 +413,44 @@ public static partial class ManagedConfiguration var configPath = $"{configName}.{className}.{propertyName}"; return configPath; } + + private static string SettingName(Expression> propertyExpression) => SettingsManager.ToSettingName(propertyExpression); + + private static bool TryGetEditableDefaultState(string settingName, out ManagedEditableDefaultState editableDefaultState) + { + return SETTINGS_MANAGER.ConfigurationData.ManagedEditableDefaults.TryGetValue(settingName, out editableDefaultState!); + } + + private static void SetEditableDefaultState(string settingName, Guid pluginId, string lastAppliedValue) + { + SETTINGS_MANAGER.ConfigurationData.ManagedEditableDefaults[settingName] = new() + { + ConfigPluginId = pluginId, + LastAppliedValue = lastAppliedValue, + }; + } + + private static bool ClearEditableDefaultState(string settingName) => SETTINGS_MANAGER.ConfigurationData.ManagedEditableDefaults.Remove(settingName); + + private static bool CleanupEditableDefaultState( + ConfigMeta configMeta, + string settingName, + IReadOnlyList availablePlugins) + { + if (!TryGetEditableDefaultState(settingName, out var editableDefaultState)) + { + if (configMeta.ManagedMode is not ManagedConfigurationMode.EDITABLE_DEFAULT) + return false; + + configMeta.ClearEditableDefaultConfiguration(); + return true; + } + + var plugin = availablePlugins.FirstOrDefault(x => x.Id == editableDefaultState.ConfigPluginId); + if (plugin is not null) + return false; + + configMeta.ClearEditableDefaultConfiguration(); + return ClearEditableDefaultState(settingName); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ManagedConfigurationMode.cs b/app/MindWork AI Studio/Settings/ManagedConfigurationMode.cs new file mode 100644 index 00000000..324a5748 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ManagedConfigurationMode.cs @@ -0,0 +1,14 @@ +namespace AIStudio.Settings; + +public enum ManagedConfigurationMode +{ + /// + /// The configuration is locked by a configuration plugin. The user cannot change the value of this setting, and it will be overridden by the plugin on each update. + /// + LOCKED, + + /// + /// The configuration has an editable default provided by a configuration plugin. The user can change the value of this setting. + /// + EDITABLE_DEFAULT, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ManagedEditableDefaultState.cs b/app/MindWork AI Studio/Settings/ManagedEditableDefaultState.cs new file mode 100644 index 00000000..8904c497 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ManagedEditableDefaultState.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Settings; + +public sealed class ManagedEditableDefaultState +{ + public Guid ConfigPluginId { get; init; } = Guid.Empty; + + public string LastAppliedValue { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 911d827f..50c8c03e 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -52,6 +52,16 @@ public sealed class SettingsManager /// public bool IsDarkMode { get; set; } + /// + /// Ensures that the startup start-page redirect is evaluated at most once per app session. + /// + public bool StartupStartPageRedirectHandled { get; set; } + + /// + /// Indicates that the initial settings load attempt has completed. + /// + public bool HasCompletedInitialSettingsLoad { get; private set; } + /// /// The configuration data. /// @@ -67,6 +77,8 @@ public sealed class SettingsManager var settingsSnapshot = await this.TryReadSettingsSnapshot(); if (settingsSnapshot is not null) this.ConfigurationData = settingsSnapshot; + + this.HasCompletedInitialSettingsLoad = true; } /// diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index b4007b9d..99031624 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -111,6 +111,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT // Config: how should updates be installed? ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UpdateInstallation, this.Id, settingsTable, dryRun); + + // Config: what should be the start page? + ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.StartPage, this.Id, settingsTable, dryRun); // Config: allow the user to add providers? ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index be6de578..f110e766 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -202,6 +202,10 @@ public static partial class PluginFactory // Check for the update installation method: if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UpdateInstallation, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; + + // Check for the start page: + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.StartPage, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; // Check for users allowed to added providers: if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.AllowUserToAddProvider, AVAILABLE_PLUGINS)) diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index f611bcc8..ba8f5c42 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -4,6 +4,7 @@ - Added a reminder in chats and assistants that LLMs can make mistakes, helping you double-check important information more easily. - Added the ability to format your user prompt in the chat using icons instead of typing Markdown directly. - Added the ability to load a system prompt from a file when creating or editing chat templates. +- Added a start-page setting, so AI Studio can now open directly on your preferred page when the app starts. Configuration plugins can also provide and optionally lock this default for organizations. - Released the document analysis assistant after an intense testing phase. - Improved the profile selection for assistants and the chat. You can now explicitly choose between the app default profile, no profile, or a specific profile. - Improved the performance by caching the OS language detection and requesting the user language only once per app start. From cf6226546edb98fe566910f5a25af27550956b44 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 21 Mar 2026 20:34:11 +0100 Subject: [PATCH 24/27] Added math rendering (#705) --- AGENTS.md | 1 + app/MindWork AI Studio/App.razor | 1 + .../Chat/ContentBlockComponent.razor | 26 +- .../Chat/ContentBlockComponent.razor.cs | 345 +++++++++++++++++- .../Chat/MathJaxBlock.razor | 5 + .../Chat/MathJaxBlock.razor.cs | 18 + app/MindWork AI Studio/wwwroot/app.css | 17 +- .../wwwroot/changelog/v26.3.1.md | 1 + app/MindWork AI Studio/wwwroot/chat-math.js | 287 +++++++++++++++ 9 files changed, 684 insertions(+), 17 deletions(-) create mode 100644 app/MindWork AI Studio/Chat/MathJaxBlock.razor create mode 100644 app/MindWork AI Studio/Chat/MathJaxBlock.razor.cs create mode 100644 app/MindWork AI Studio/wwwroot/chat-math.js diff --git a/AGENTS.md b/AGENTS.md index f960b509..6bf4eb5f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -193,6 +193,7 @@ Multi-level confidence scheme allows users to control which providers see which - **MudBlazor** - Component library requires DI setup in Program.cs - **Encryption** - Initialized before Rust service is marked ready - **Message Bus** - Singleton event bus for cross-component communication inside the .NET app +- **Naming conventions** - Constants, enum members, and `static readonly` fields use `UPPER_SNAKE_CASE` such as `MY_CONSTANT`. - **Empty lines** - Avoid adding extra empty lines at the end of files. ## Changelogs diff --git a/app/MindWork AI Studio/App.razor b/app/MindWork AI Studio/App.razor index b314b033..7df24793 100644 --- a/app/MindWork AI Studio/App.razor +++ b/app/MindWork AI Studio/App.razor @@ -27,6 +27,7 @@ + diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index 579e8bf2..8d0689da 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -96,11 +96,25 @@ } else { - - @if (textContent.Sources.Count > 0) - { - - } + var renderPlan = this.GetMarkdownRenderPlan(textContent.Text); +
    + @foreach (var segment in renderPlan.Segments) + { + var segmentContent = segment.GetContent(renderPlan.Source); + if (segment.Type is MarkdownRenderSegmentType.MARKDOWN) + { + + } + else + { + + } + } + @if (textContent.Sources.Count > 0) + { + + } +
    } } } @@ -135,4 +149,4 @@ } } - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 29e70487..e0b035ce 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -8,8 +8,20 @@ namespace AIStudio.Chat; /// /// The UI component for a chat content block, i.e., for any IContent. /// -public partial class ContentBlockComponent : MSGComponentBase +public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable { + private const string CHAT_MATH_SYNC_FUNCTION = "chatMath.syncContainer"; + private const string CHAT_MATH_DISPOSE_FUNCTION = "chatMath.disposeContainer"; + private const string HTML_START_TAG = "<"; + private const string HTML_END_TAG = ""; + private const string CODE_FENCE_MARKER_BACKTICK = "```"; + private const string CODE_FENCE_MARKER_TILDE = "~~~"; + private const string MATH_BLOCK_MARKER_DOLLAR = "$$"; + private const string MATH_BLOCK_MARKER_BRACKET_OPEN = """\["""; + private const string MATH_BLOCK_MARKER_BRACKET_CLOSE = """\]"""; + private const string HTML_CODE_FENCE_PREFIX = "```html"; + private static readonly string[] HTML_TAG_MARKERS = [ " protected override bool ShouldRender() { @@ -194,32 +221,320 @@ public partial class ContentBlockComponent : MSGComponentBase CodeBlock = { Theme = this.CodeColorPalette }, }; + private MarkdownRenderPlan GetMarkdownRenderPlan(string text) + { + if (ReferenceEquals(this.cachedMarkdownRenderPlanInput, text) || string.Equals(this.cachedMarkdownRenderPlanInput, text, StringComparison.Ordinal)) + return this.cachedMarkdownRenderPlan; + + this.cachedMarkdownRenderPlanInput = text; + this.cachedMarkdownRenderPlan = BuildMarkdownRenderPlan(text); + return this.cachedMarkdownRenderPlan; + } + + private async Task SyncMathRenderIfNeededAsync() + { + if (this.isDisposed) + return; + + if (!this.TryGetCompletedMathRenderState(out var mathRenderSignature)) + { + await this.DisposeMathContainerIfNeededAsync(); + return; + } + + if (string.Equals(this.lastMathRenderSignature, mathRenderSignature, StringComparison.Ordinal)) + return; + + await this.JsRuntime.InvokeVoidAsync(CHAT_MATH_SYNC_FUNCTION, this.mathContentContainer, mathRenderSignature); + this.lastMathRenderSignature = mathRenderSignature; + this.hasActiveMathContainer = true; + } + + private async Task DisposeMathContainerIfNeededAsync() + { + if (!this.hasActiveMathContainer) + { + this.lastMathRenderSignature = string.Empty; + return; + } + + try + { + await this.JsRuntime.InvokeVoidAsync(CHAT_MATH_DISPOSE_FUNCTION, this.mathContentContainer); + } + catch (JSDisconnectedException) + { + } + catch (ObjectDisposedException) + { + } + + this.hasActiveMathContainer = false; + this.lastMathRenderSignature = string.Empty; + } + + private bool TryGetCompletedMathRenderState(out string mathRenderSignature) + { + mathRenderSignature = string.Empty; + + if (this.HideContent || this.Type is not ContentType.TEXT || this.Content.IsStreaming || this.Content is not ContentText textContent || textContent.InitialRemoteWait) + return false; + + var renderPlan = this.GetMarkdownRenderPlan(textContent.Text); + mathRenderSignature = CreateMathRenderSignature(renderPlan); + return !string.IsNullOrEmpty(mathRenderSignature); + } + + private static string CreateMathRenderSignature(MarkdownRenderPlan renderPlan) + { + var hash = new HashCode(); + var mathSegmentCount = 0; + + foreach (var segment in renderPlan.Segments) + { + if (segment.Type is not MarkdownRenderSegmentType.MATH_BLOCK) + continue; + + mathSegmentCount++; + hash.Add(segment.Start); + hash.Add(segment.Length); + hash.Add(segment.GetContent(renderPlan.Source).GetHashCode(StringComparison.Ordinal)); + } + + return mathSegmentCount == 0 + ? string.Empty + : $"{mathSegmentCount}:{hash.ToHashCode()}"; + } + + private static MarkdownRenderPlan BuildMarkdownRenderPlan(string text) + { + var normalized = NormalizeMarkdownForRendering(text); + if (string.IsNullOrWhiteSpace(normalized)) + return MarkdownRenderPlan.EMPTY; + + var normalizedSpan = normalized.AsSpan(); + var segments = new List(); + var activeCodeFenceMarker = '\0'; + var activeMathBlockFenceType = MathBlockFenceType.NONE; + var markdownSegmentStart = 0; + var mathContentStart = 0; + + for (var lineStart = 0; lineStart < normalizedSpan.Length;) + { + var lineEnd = lineStart; + while (lineEnd < normalizedSpan.Length && normalizedSpan[lineEnd] is not '\r' and not '\n') + lineEnd++; + + var nextLineStart = lineEnd; + if (nextLineStart < normalizedSpan.Length) + { + if (normalizedSpan[nextLineStart] == '\r') + nextLineStart++; + + if (nextLineStart < normalizedSpan.Length && normalizedSpan[nextLineStart] == '\n') + nextLineStart++; + } + + var trimmedLine = TrimWhitespace(normalizedSpan[lineStart..lineEnd]); + if (activeMathBlockFenceType is MathBlockFenceType.NONE && TryUpdateCodeFenceState(trimmedLine, ref activeCodeFenceMarker)) + { + lineStart = nextLineStart; + continue; + } + + if (activeCodeFenceMarker != '\0') + { + lineStart = nextLineStart; + continue; + } + + if (activeMathBlockFenceType is MathBlockFenceType.NONE) + { + if (trimmedLine.SequenceEqual(MATH_BLOCK_MARKER_DOLLAR.AsSpan())) + { + AddMarkdownSegment(markdownSegmentStart, lineStart); + mathContentStart = nextLineStart; + activeMathBlockFenceType = MathBlockFenceType.DOLLAR; + lineStart = nextLineStart; + continue; + } + + if (trimmedLine.SequenceEqual(MATH_BLOCK_MARKER_BRACKET_OPEN.AsSpan())) + { + AddMarkdownSegment(markdownSegmentStart, lineStart); + mathContentStart = nextLineStart; + activeMathBlockFenceType = MathBlockFenceType.BRACKET; + lineStart = nextLineStart; + continue; + } + } + else if (activeMathBlockFenceType is MathBlockFenceType.DOLLAR && trimmedLine.SequenceEqual(MATH_BLOCK_MARKER_DOLLAR.AsSpan())) + { + var (start, end) = TrimLineBreaks(normalizedSpan, mathContentStart, lineStart); + segments.Add(new(MarkdownRenderSegmentType.MATH_BLOCK, start, end - start)); + + markdownSegmentStart = nextLineStart; + activeMathBlockFenceType = MathBlockFenceType.NONE; + lineStart = nextLineStart; + continue; + } + else if (activeMathBlockFenceType is MathBlockFenceType.BRACKET && trimmedLine.SequenceEqual(MATH_BLOCK_MARKER_BRACKET_CLOSE.AsSpan())) + { + var (start, end) = TrimLineBreaks(normalizedSpan, mathContentStart, lineStart); + segments.Add(new(MarkdownRenderSegmentType.MATH_BLOCK, start, end - start)); + + markdownSegmentStart = nextLineStart; + activeMathBlockFenceType = MathBlockFenceType.NONE; + lineStart = nextLineStart; + continue; + } + + lineStart = nextLineStart; + } + + if (activeMathBlockFenceType is not MathBlockFenceType.NONE) + return new(normalized, [new(MarkdownRenderSegmentType.MARKDOWN, 0, normalized.Length)]); + + AddMarkdownSegment(markdownSegmentStart, normalized.Length); + if (segments.Count == 0) + segments.Add(new(MarkdownRenderSegmentType.MARKDOWN, 0, normalized.Length)); + + return new(normalized, segments); + + void AddMarkdownSegment(int start, int end) + { + if (end <= start) + return; + + segments.Add(new(MarkdownRenderSegmentType.MARKDOWN, start, end - start)); + } + } + private static string NormalizeMarkdownForRendering(string text) { - var cleaned = text.RemoveThinkTags().Trim(); - if (string.IsNullOrWhiteSpace(cleaned)) + var textWithoutThinkTags = text.RemoveThinkTags(); + var trimmed = TrimWhitespace(textWithoutThinkTags.AsSpan()); + if (trimmed.IsEmpty) return string.Empty; - if (cleaned.Contains("```", StringComparison.Ordinal)) + var cleaned = trimmed.Length == textWithoutThinkTags.Length + ? textWithoutThinkTags + : trimmed.ToString(); + + if (cleaned.Contains(CODE_FENCE_MARKER_BACKTICK, StringComparison.Ordinal)) return cleaned; if (LooksLikeRawHtml(cleaned)) - return $"```html{Environment.NewLine}{cleaned}{Environment.NewLine}```"; + return $"{HTML_CODE_FENCE_PREFIX}{Environment.NewLine}{cleaned}{Environment.NewLine}{CODE_FENCE_MARKER_BACKTICK}"; return cleaned; } private static bool LooksLikeRawHtml(string text) { - var content = text.TrimStart(); - if (!content.StartsWith("<", StringComparison.Ordinal)) + var content = text.AsSpan(); + var start = 0; + while (start < content.Length && char.IsWhiteSpace(content[start])) + start++; + + content = content[start..]; + if (!content.StartsWith(HTML_START_TAG.AsSpan(), StringComparison.Ordinal)) return false; foreach (var marker in HTML_TAG_MARKERS) - if (content.Contains(marker, StringComparison.OrdinalIgnoreCase)) + if (content.IndexOf(marker.AsSpan(), StringComparison.OrdinalIgnoreCase) >= 0) return true; - return content.Contains("", StringComparison.Ordinal); + return content.IndexOf(HTML_END_TAG.AsSpan(), StringComparison.Ordinal) >= 0 + || content.IndexOf(HTML_SELF_CLOSING_TAG.AsSpan(), StringComparison.Ordinal) >= 0; + } + + private static bool TryUpdateCodeFenceState(ReadOnlySpan trimmedLine, ref char activeCodeFenceMarker) + { + var fenceMarker = '\0'; + if (trimmedLine.StartsWith(CODE_FENCE_MARKER_BACKTICK.AsSpan(), StringComparison.Ordinal)) + fenceMarker = '`'; + else if (trimmedLine.StartsWith(CODE_FENCE_MARKER_TILDE.AsSpan(), StringComparison.Ordinal)) + fenceMarker = '~'; + + if (fenceMarker == '\0') + return false; + + activeCodeFenceMarker = activeCodeFenceMarker == '\0' + ? fenceMarker + : activeCodeFenceMarker == fenceMarker + ? '\0' + : activeCodeFenceMarker; + + return true; + } + + private static ReadOnlySpan TrimWhitespace(ReadOnlySpan text) + { + var start = 0; + var end = text.Length - 1; + + while (start < text.Length && char.IsWhiteSpace(text[start])) + start++; + + while (end >= start && char.IsWhiteSpace(text[end])) + end--; + + return start > end ? ReadOnlySpan.Empty : text[start..(end + 1)]; + } + + private static (int Start, int End) TrimLineBreaks(ReadOnlySpan text, int start, int end) + { + while (start < end && text[start] is '\r' or '\n') + start++; + + while (end > start && text[end - 1] is '\r' or '\n') + end--; + + return (start, end); + } + + private enum MarkdownRenderSegmentType + { + MARKDOWN, + MATH_BLOCK, + } + + private enum MathBlockFenceType + { + NONE, + DOLLAR, + BRACKET, + } + + private sealed record MarkdownRenderPlan(string Source, IReadOnlyList Segments) + { + public static readonly MarkdownRenderPlan EMPTY = new(string.Empty, []); + } + + private sealed class MarkdownRenderSegment(MarkdownRenderSegmentType type, int start, int length) + { + private string? cachedContent; + + public MarkdownRenderSegmentType Type { get; } = type; + + public int Start { get; } = start; + + public int Length { get; } = length; + + public int RenderKey { get; } = HashCode.Combine(type, start, length); + + public string GetContent(string source) + { + if (this.cachedContent is not null) + return this.cachedContent; + + this.cachedContent = this.Start == 0 && this.Length == source.Length + ? source + : source.Substring(this.Start, this.Length); + + return this.cachedContent; + } } private async Task RemoveBlock() @@ -294,4 +609,14 @@ public partial class ContentBlockComponent : MSGComponentBase var result = await ReviewAttachmentsDialog.OpenDialogAsync(this.DialogService, this.Content.FileAttachments.ToHashSet()); this.Content.FileAttachments = result.ToList(); } -} \ No newline at end of file + + public async ValueTask DisposeAsync() + { + if (this.isDisposed) + return; + + this.isDisposed = true; + await this.DisposeMathContainerIfNeededAsync(); + this.Dispose(); + } +} diff --git a/app/MindWork AI Studio/Chat/MathJaxBlock.razor b/app/MindWork AI Studio/Chat/MathJaxBlock.razor new file mode 100644 index 00000000..6b203a4f --- /dev/null +++ b/app/MindWork AI Studio/Chat/MathJaxBlock.razor @@ -0,0 +1,5 @@ +@namespace AIStudio.Chat + +
    + @this.MathText +
    \ No newline at end of file diff --git a/app/MindWork AI Studio/Chat/MathJaxBlock.razor.cs b/app/MindWork AI Studio/Chat/MathJaxBlock.razor.cs new file mode 100644 index 00000000..c78db2c9 --- /dev/null +++ b/app/MindWork AI Studio/Chat/MathJaxBlock.razor.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Chat; + +public partial class MathJaxBlock +{ + [Parameter] + public string Value { get; init; } = string.Empty; + + [Parameter] + public string Class { get; init; } = string.Empty; + + private string RootClass => string.IsNullOrWhiteSpace(this.Class) + ? "chat-mathjax-block" + : $"chat-mathjax-block {this.Class}"; + + private string MathText => $"$${Environment.NewLine}{this.Value}{Environment.NewLine}$$"; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/app.css b/app/MindWork AI Studio/wwwroot/app.css index cd80c5a9..909d350d 100644 --- a/app/MindWork AI Studio/wwwroot/app.css +++ b/app/MindWork AI Studio/wwwroot/app.css @@ -150,4 +150,19 @@ .sources-card-header { top: 0em !important; left: 2.2em !important; -} \ No newline at end of file +} + +.chat-mathjax-block { + text-align: left; +} + +.chat-mathjax-block mjx-container[display="true"] { + text-align: left !important; + margin-left: 0 !important; + margin-right: 0 !important; +} + +.chat-mathjax-block mjx-container[display="true"] mjx-math { + margin-left: 0 !important; + margin-right: 0 !important; +} diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index ba8f5c42..cd5b5899 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -5,6 +5,7 @@ - Added the ability to format your user prompt in the chat using icons instead of typing Markdown directly. - Added the ability to load a system prompt from a file when creating or editing chat templates. - Added a start-page setting, so AI Studio can now open directly on your preferred page when the app starts. Configuration plugins can also provide and optionally lock this default for organizations. +- Added math rendering in chats for LaTeX display formulas, including block formats such as `$$ ... $$` and `\[ ... \]`. - Released the document analysis assistant after an intense testing phase. - Improved the profile selection for assistants and the chat. You can now explicitly choose between the app default profile, no profile, or a specific profile. - Improved the performance by caching the OS language detection and requesting the user language only once per app start. diff --git a/app/MindWork AI Studio/wwwroot/chat-math.js b/app/MindWork AI Studio/wwwroot/chat-math.js new file mode 100644 index 00000000..1c1bf0f6 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/chat-math.js @@ -0,0 +1,287 @@ +const MATH_JAX_SCRIPT_ID = 'mudblazor-markdown-mathjax' +const MATH_JAX_SCRIPT_SRC = '_content/MudBlazor.Markdown/MudBlazor.Markdown.MathJax.min.js' +const INTERSECTION_ROOT_MARGIN = '240px 0px 240px 0px' +const MAX_TYPES_PER_BATCH = 4 +const containerStates = new Map() +const pendingMathElements = new Set() + +let mathJaxReadyPromise = null +let batchScheduled = false +let typesetInProgress = false + +function applyMathJaxConfiguration() { + window.MathJax = window.MathJax ?? {} + window.MathJax.options = window.MathJax.options ?? {} + window.MathJax.options.enableMenu = false +} + +function isMathJaxReady() { + return typeof window.MathJax?.typesetPromise === 'function' || typeof window.MathJax?.typeset === 'function' +} + +function waitForMathJaxReady(attempt = 0) { + if (isMathJaxReady()) + return Promise.resolve() + + if (attempt >= 80) + return Promise.reject(new Error('MathJax did not finish loading in time.')) + + return new Promise((resolve, reject) => { + window.setTimeout(() => { + waitForMathJaxReady(attempt + 1).then(resolve).catch(reject) + }, 50) + }) +} + +function ensureMathJaxLoaded() { + if (isMathJaxReady()) + return Promise.resolve() + + if (mathJaxReadyPromise) + return mathJaxReadyPromise + + mathJaxReadyPromise = new Promise((resolve, reject) => { + applyMathJaxConfiguration() + let script = document.getElementById(MATH_JAX_SCRIPT_ID) + + const onLoad = () => { + waitForMathJaxReady().then(resolve).catch(reject) + } + + const onError = () => reject(new Error('Failed to load the MathJax script.')) + + if (!script) { + script = document.createElement('script') + script.id = MATH_JAX_SCRIPT_ID + script.type = 'text/javascript' + script.src = MATH_JAX_SCRIPT_SRC + script.addEventListener('load', onLoad, { once: true }) + script.addEventListener('error', onError, { once: true }) + document.head.appendChild(script) + return + } + + script.addEventListener('load', onLoad, { once: true }) + script.addEventListener('error', onError, { once: true }) + void waitForMathJaxReady().then(resolve).catch(() => {}) + }).catch(error => { + mathJaxReadyPromise = null + throw error + }) + + return mathJaxReadyPromise +} + +function createContainerState() { + return { + signature: '', + observer: null, + observedElements: new Set() + } +} + +function disconnectContainerState(state) { + if (state.observer) { + state.observer.disconnect() + state.observer = null + } + + for (const element of state.observedElements) + pendingMathElements.delete(element) + + state.observedElements.clear() +} + +function isNearViewport(element) { + const rect = element.getBoundingClientRect() + return rect.bottom >= -240 && rect.top <= window.innerHeight + 240 +} + +function queueElementForTypeset(element, signature) { + if (!element || !element.isConnected) + return + + if (element.dataset.chatMathProcessedSignature === signature) + return + + element.dataset.chatMathTargetSignature = signature + element.dataset.chatMathPending = 'true' + pendingMathElements.add(element) + schedulePendingTypeset(false) +} + +function schedulePendingTypeset(useIdleCallback) { + if (batchScheduled) + return + + batchScheduled = true + const flush = () => { + batchScheduled = false + void flushPendingTypeset() + } + + if (useIdleCallback && typeof window.requestIdleCallback === 'function') { + window.requestIdleCallback(flush, { timeout: 120 }) + return + } + + window.requestAnimationFrame(flush) +} + +async function flushPendingTypeset() { + if (typesetInProgress || pendingMathElements.size === 0) + return + + typesetInProgress = true + const elementsToTypeset = [] + + try { + await ensureMathJaxLoaded() + + for (const element of pendingMathElements) { + if (elementsToTypeset.length >= MAX_TYPES_PER_BATCH) + break + + if (!element.isConnected) { + pendingMathElements.delete(element) + continue + } + + const targetSignature = element.dataset.chatMathTargetSignature ?? '' + if (element.dataset.chatMathProcessedSignature === targetSignature) { + pendingMathElements.delete(element) + element.dataset.chatMathPending = 'false' + continue + } + + elementsToTypeset.push(element) + } + + if (elementsToTypeset.length === 0) + return + + for (const element of elementsToTypeset) + pendingMathElements.delete(element) + + if (typeof window.MathJax?.typesetClear === 'function') { + try { + window.MathJax.typesetClear(elementsToTypeset) + } catch (error) { + console.warn('chatMath: failed to clear previous MathJax state.', error) + } + } + + if (typeof window.MathJax?.typesetPromise === 'function') + await window.MathJax.typesetPromise(elementsToTypeset) + else if (typeof window.MathJax?.typeset === 'function') + window.MathJax.typeset(elementsToTypeset) + + for (const element of elementsToTypeset) { + element.dataset.chatMathProcessedSignature = element.dataset.chatMathTargetSignature ?? '' + element.dataset.chatMathPending = 'false' + } + } catch (error) { + console.warn('chatMath: failed to typeset math content.', error) + + for (const element of elementsToTypeset) + if (element.isConnected) + pendingMathElements.add(element) + } finally { + typesetInProgress = false + + if (pendingMathElements.size > 0) + schedulePendingTypeset(true) + } +} + +function createIntersectionObserver(state, signature) { + return new IntersectionObserver(entries => { + let queuedVisibleElement = false + + for (const entry of entries) { + if (!entry.isIntersecting) + continue + + const element = entry.target + state.observer?.unobserve(element) + state.observedElements.delete(element) + queueElementForTypeset(element, signature) + queuedVisibleElement = true + } + + if (queuedVisibleElement) + schedulePendingTypeset(true) + }, { + root: null, + rootMargin: INTERSECTION_ROOT_MARGIN, + threshold: 0.01 + }) +} + +function getMathElements(container) { + return Array.from(container.querySelectorAll('.chat-mathjax-block')) +} + +window.chatMath = { + syncContainer: async function(container, signature) { + if (!container) + return + + let state = containerStates.get(container) + if (!state) { + state = createContainerState() + containerStates.set(container, state) + } + + if (state.signature === signature) + return + + disconnectContainerState(state) + state.signature = signature + + const mathElements = getMathElements(container) + if (mathElements.length === 0) + return + + await ensureMathJaxLoaded() + + state.observer = createIntersectionObserver(state, signature) + + for (const element of mathElements) { + if (isNearViewport(element)) { + queueElementForTypeset(element, signature) + continue + } + + element.dataset.chatMathTargetSignature = signature + state.observer.observe(element) + state.observedElements.add(element) + } + + schedulePendingTypeset(false) + }, + + disposeContainer: function(container) { + if (!container) + return + + const state = containerStates.get(container) + if (!state) + return + + disconnectContainerState(state) + containerStates.delete(container) + + const mathElements = getMathElements(container) + for (const element of mathElements) + pendingMathElements.delete(element) + + if (typeof window.MathJax?.typesetClear === 'function' && mathElements.length > 0) { + try { + window.MathJax.typesetClear(mathElements) + } catch (error) { + console.warn('chatMath: failed to clear container MathJax state during dispose.', error) + } + } + } +} \ No newline at end of file From 309d36897eb406cef357f225152af2d3657d5352 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Mar 2026 14:11:30 +0100 Subject: [PATCH 25/27] Updated security-sensitive Rust dependencies (#706) --- .../wwwroot/changelog/v26.3.1.md | 2 + runtime/Cargo.lock | 599 +++++++++++------- runtime/Cargo.toml | 26 +- runtime/src/api_token.rs | 15 +- runtime/src/encryption.rs | 37 +- 5 files changed, 439 insertions(+), 240 deletions(-) diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index cd5b5899..db925ca1 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -21,4 +21,6 @@ - Fixed an issue where assistants hidden via configuration plugins still appear in "Send to ..." menus. Thanks, Gunnar, for reporting this issue. - Fixed an issue with voice recording where AI Studio could log errors and keep the feature available even though required parts failed to initialize. Voice recording is now disabled automatically for the current session in that case. - Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks, Inga, for reporting this issue and providing some context on how to reproduce it. +- Fixed security issues in the native app runtime by strengthening how AI Studio creates and protects the secret values used for its internal secure connection. +- Updated several security-sensitive Rust dependencies in the native runtime to address known vulnerabilities. - Updated .NET to v9.0.14 \ No newline at end of file diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index c0161bfc..a2e5815f 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -22,7 +22,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.12", ] [[package]] @@ -123,7 +123,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "synstructure", ] @@ -135,7 +135,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -157,7 +157,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -168,7 +168,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -233,21 +233,21 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "aws-lc-rs" -version = "1.13.3" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", + "untrusted 0.7.1", "zeroize", ] [[package]] name = "aws-lc-sys" -version = "0.30.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ - "bindgen", "cc", "cmake", "dunce", @@ -287,29 +287,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.6.0", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.93", - "which", -] - [[package]] name = "bit_field" version = "0.10.2" @@ -461,9 +438,9 @@ dependencies = [ [[package]] name = "calamine" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ae094b353c7810cd5efd2e69413ebb9354816138a387c09f7b90d4e826a49f" +checksum = "20ae05a4e39297eecf9a994210d27501318c37a9318201f8e11050add82bb6f0" dependencies = [ "atoi_simd", "byteorder", @@ -471,7 +448,7 @@ dependencies = [ "encoding_rs", "fast-float2", "log", - "quick-xml 0.38.4", + "quick-xml 0.39.2", "serde", "zip 7.4.0", ] @@ -513,15 +490,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfb" version = "0.7.3" @@ -564,6 +532,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + [[package]] name = "chrono" version = "0.4.40" @@ -589,17 +568,6 @@ dependencies = [ "inout", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clipboard-win" version = "5.4.0" @@ -775,6 +743,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.3.0" @@ -873,7 +850,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -883,7 +860,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -907,7 +884,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -918,7 +895,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -995,7 +972,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -1008,7 +985,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -1041,7 +1018,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -1100,7 +1077,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -1120,9 +1097,9 @@ dependencies = [ [[package]] name = "dunce" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" @@ -1167,12 +1144,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1204,9 +1181,9 @@ checksum = "f8eb564c5c7423d25c886fb561d1e4ee69f72354d16918afa32c08811f6b6a55" [[package]] name = "fastrand" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" @@ -1304,6 +1281,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1346,9 +1329,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1361,9 +1344,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1371,15 +1354,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1388,38 +1371,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1429,7 +1412,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1599,6 +1581,20 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + [[package]] name = "gif" version = "0.13.1" @@ -1828,6 +1824,9 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] [[package]] name = "heck" @@ -1877,15 +1876,6 @@ dependencies = [ "digest", ] -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "html5ever" version = "0.26.0" @@ -2238,9 +2228,15 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2400,15 +2396,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -2574,10 +2561,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "lazycell" -version = "1.3.0" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lebe" @@ -2587,9 +2574,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libdbus-sys" @@ -2635,6 +2622,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.7.4" @@ -2783,12 +2776,12 @@ dependencies = [ "aes", "arboard", "async-stream", + "aws-lc-rs", "base64 0.22.1", "bytes", "calamine", "cbc", "cfg-if", - "cipher", "file-format", "flexi_logger", "futures", @@ -2800,17 +2793,19 @@ dependencies = [ "pbkdf2", "pdfium-render", "pptx-to-md", - "rand 0.9.1", - "rand_chacha 0.9.0", + "rand 0.10.0", + "rand_chacha 0.10.0", "rcgen", - "reqwest 0.13.1", + "reqwest 0.13.2", "rocket", + "rustls-webpki 0.103.10", "serde", "serde_json", "sha2", "strum_macros", "sys-locale", "sysinfo", + "tar", "tauri", "tauri-build", "tauri-plugin-window-state", @@ -3210,9 +3205,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open" @@ -3226,9 +3221,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -3247,7 +3242,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -3273,9 +3268,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -3371,7 +3366,7 @@ dependencies = [ "console_error_panic_hook", "console_log", "image 0.25.2", - "itertools 0.14.0", + "itertools", "js-sys", "libloading", "log", @@ -3404,7 +3399,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -3527,7 +3522,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -3563,12 +3558,6 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "piston-float" version = "1.0.1" @@ -3641,12 +3630,12 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prettyplease" -version = "0.2.25" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -3706,7 +3695,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "version_check", "yansi", ] @@ -3731,9 +3720,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" dependencies = [ "encoding_rs", "memchr", @@ -3750,7 +3739,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash", "rustls 0.23.28", "socket2 0.6.2", "thiserror 2.0.12", @@ -3761,9 +3750,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "aws-lc-rs", "bytes", @@ -3771,7 +3760,7 @@ dependencies = [ "lru-slab", "rand 0.9.1", "ring", - "rustc-hash 2.1.1", + "rustc-hash", "rustls 0.23.28", "rustls-pki-types", "slab", @@ -3804,6 +3793,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.7.3" @@ -3839,6 +3834,17 @@ dependencies = [ "rand_core 0.9.0", ] +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -3869,6 +3875,16 @@ dependencies = [ "rand_core 0.9.0", ] +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core 0.10.0", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -3897,6 +3913,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "rand_hc" version = "0.2.0" @@ -4001,7 +4023,7 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4077,9 +4099,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64 0.22.1", "bytes", @@ -4150,7 +4172,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.15", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -4204,7 +4226,7 @@ dependencies = [ "proc-macro2", "quote", "rocket_http", - "syn 2.0.93", + "syn 2.0.117", "unicode-xid", "version_check", ] @@ -4245,12 +4267,6 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -4284,10 +4300,23 @@ dependencies = [ "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.21.12" @@ -4309,7 +4338,7 @@ dependencies = [ "aws-lc-rs", "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.3", + "rustls-webpki 0.103.10", "subtle", "zeroize", ] @@ -4337,11 +4366,12 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", + "zeroize", ] [[package]] @@ -4358,7 +4388,7 @@ dependencies = [ "rustls 0.23.28", "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.3", + "rustls-webpki 0.103.10", "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs", @@ -4378,19 +4408,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ "ring", - "untrusted", + "untrusted 0.9.0", ] [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -4442,7 +4472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ "ring", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -4537,7 +4567,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4562,7 +4592,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4613,7 +4643,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4635,7 +4665,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4655,7 +4685,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.12", "digest", ] @@ -4666,7 +4696,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.12", "digest", ] @@ -4852,14 +4882,14 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum_macros" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4881,9 +4911,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.93" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -4913,7 +4943,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4927,9 +4957,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.38.0" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe840c5b1afe259a5657392a4dbb74473a14c8db999c3ec2f4ae812e028a94da" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" dependencies = [ "libc", "memchr", @@ -5067,9 +5097,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.41" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -5300,14 +5330,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "cfg-if", "fastrand", - "rustix", - "windows-sys 0.52.0", + "getrandom 0.3.1", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] @@ -5353,7 +5384,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -5364,7 +5395,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -5446,9 +5477,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -5468,7 +5499,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -5657,7 +5688,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -5754,6 +5785,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -5908,6 +5945,24 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -5930,7 +5985,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -5964,7 +6019,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5978,6 +6033,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.7.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.0" @@ -5991,6 +6068,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.6.0", + "hashbrown 0.15.2", + "indexmap 2.7.0", + "semver", +] + [[package]] name = "web-sys" version = "0.3.69" @@ -6111,18 +6200,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix", -] - [[package]] name = "winapi" version = "0.3.9" @@ -6272,7 +6349,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -6283,7 +6360,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -6806,6 +6883,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + [[package]] name = "wit-bindgen-rt" version = "0.33.0" @@ -6815,6 +6912,74 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.7.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.6.0", + "indexmap 2.7.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.7.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "write16" version = "1.0.0" @@ -6893,7 +7058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "gethostname", - "rustix", + "rustix 0.38.34", "x11rb-protocol", ] @@ -6928,8 +7093,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", - "linux-raw-sys", - "rustix", + "linux-raw-sys 0.4.14", + "rustix 0.38.34", ] [[package]] @@ -6979,7 +7144,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "synstructure", ] @@ -7000,7 +7165,7 @@ checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -7020,7 +7185,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "synstructure", ] @@ -7041,7 +7206,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -7063,7 +7228,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 6a71f79c..acd606e1 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -15,18 +15,17 @@ serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" keyring = { version = "3.6.2", features = ["apple-native", "windows-native", "sync-secret-service"] } arboard = "3.6.1" -tokio = { version = "1.49.0", features = ["rt", "rt-multi-thread", "macros", "process"] } +tokio = { version = "1.50.0", features = ["rt", "rt-multi-thread", "macros", "process"] } tokio-stream = "0.1.18" -futures = "0.3.31" +futures = "0.3.32" async-stream = "0.3.6" flexi_logger = "0.31.8" log = { version = "0.4.29", features = ["kv"] } -once_cell = "1.21.3" +once_cell = "1.21.4" rocket = { version = "0.5.1", features = ["json", "tls"] } -rand = "0.9.1" -rand_chacha = "0.9" +rand = "0.10.0" +rand_chacha = "0.10.0" base64 = "0.22.1" -cipher = { version = "0.4.4", features = ["std"] } aes = "0.8.4" cbc = "0.1.2" pbkdf2 = "0.12.2" @@ -34,25 +33,28 @@ hmac = "0.12.1" sha2 = "0.10.8" rcgen = { version = "0.14.7", features = ["pem"] } file-format = "0.28.0" -calamine = "0.33.0" +calamine = "0.34.0" pdfium-render = "0.8.37" sys-locale = "0.3.2" cfg-if = "1.0.4" pptx-to-md = "0.4.0" -tempfile = "3.8" -strum_macros = "0.27" -sysinfo = "0.38.0" +tempfile = "3.27.0" +strum_macros = "0.28.0" +sysinfo = "0.38.4" # Fixes security vulnerability downstream, where the upstream is not fixed yet: time = "0.3.47" # -> Rocket bytes = "1.11.1" # -> almost every dependency +aws-lc-rs = "1.16.2" # -> reqwest +tar = "0.4.45" # -> Tauri v1 +rustls-webpki = "0.103.10" # -> tokio, reqwest [target.'cfg(target_os = "linux")'.dependencies] # See issue https://github.com/tauri-apps/tauri/issues/4470 -reqwest = { version = "0.13.1", features = ["native-tls-vendored"] } +reqwest = { version = "0.13.2", features = ["native-tls-vendored"] } # Fixes security vulnerability downstream, where the upstream is not fixed yet: -openssl = "0.10.75" +openssl = "0.10.76" # -> reqwest, Tauri v1 [target.'cfg(target_os = "windows")'.dependencies] windows-registry = "0.6.1" diff --git a/runtime/src/api_token.rs b/runtime/src/api_token.rs index e945095e..7fc8238f 100644 --- a/runtime/src/api_token.rs +++ b/runtime/src/api_token.rs @@ -1,4 +1,6 @@ -use rand::{RngCore, SeedableRng}; +use log::error; +use rand::rngs::SysRng; +use rand::{Rng, SeedableRng}; use rand_chacha::ChaChaRng; /// The API token data structure used to authenticate requests. @@ -36,7 +38,16 @@ impl APIToken { pub fn generate_api_token() -> APIToken { let mut token = [0u8; 32]; - let mut rng = ChaChaRng::from_os_rng(); + + // The API token authenticates privileged runtime requests. If the OS-backed + // RNG cannot provide a secure seed, we abort instead of using a weaker RNG + // because a predictable token would silently break the app's security model. + let mut sys_rng = SysRng; + let mut rng = ChaChaRng::try_from_rng(&mut sys_rng) + .unwrap_or_else(|e| { + error!(Source = "API Token"; "Failed to seed ChaChaRng from SysRng: {e}"); + panic!("Failed to seed ChaChaRng from SysRng: {e}"); + }); rng.fill_bytes(&mut token); APIToken::from_bytes(token.to_vec()) } \ No newline at end of file diff --git a/runtime/src/encryption.rs b/runtime/src/encryption.rs index 632915d7..41506855 100644 --- a/runtime/src/encryption.rs +++ b/runtime/src/encryption.rs @@ -4,10 +4,11 @@ use base64::Engine; use base64::prelude::BASE64_STANDARD; use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; use hmac::Hmac; -use log::info; +use log::{error, info}; use once_cell::sync::Lazy; use pbkdf2::pbkdf2; -use rand::{RngCore, SeedableRng}; +use rand::rngs::SysRng; +use rand::{Rng, SeedableRng}; use rocket::{data, Data, Request}; use rocket::data::ToByteUnit; use rocket::http::Status; @@ -31,15 +32,25 @@ pub static ENCRYPTION: Lazy = Lazy::new(|| { // We use a cryptographically secure pseudo-random number generator // to generate the secret password & salt. ChaCha20Rng is the algorithm - // of our choice: - let mut rng = rand_chacha::ChaChaRng::from_os_rng(); + // of our choice. If the OS-backed RNG is unavailable, we fail fast instead + // of falling back to a weaker RNG because these values protect the IPC + // channel and must remain cryptographically secure. + let mut sys_rng = SysRng; + let mut rng = rand_chacha::ChaChaRng::try_from_rng(&mut sys_rng) + .unwrap_or_else(|e| { + error!(Source = "Encryption"; "Failed to seed ChaChaRng from SysRng: {e}"); + panic!("Failed to seed ChaChaRng from SysRng: {e}"); + }); // Fill the secret key & salt with random bytes: rng.fill_bytes(&mut secret_key); rng.fill_bytes(&mut secret_key_salt); info!("Secret password for the IPC channel was generated successfully."); - Encryption::new(&secret_key, &secret_key_salt).unwrap() + Encryption::new(&secret_key, &secret_key_salt).unwrap_or_else(|e| { + error!(Source = "Encryption"; "Failed to initialize encryption for the IPC channel: {e}"); + panic!("Failed to initialize encryption for the IPC channel: {e}"); + }) }); /// The encryption struct used for the IPC channel. @@ -98,9 +109,14 @@ impl Encryption { /// Encrypts the given data. pub fn encrypt(&self, data: &str) -> Result { let cipher = Aes256CbcEnc::new(&self.key.into(), &self.iv.into()); - let encrypted = cipher.encrypt_padded_vec_mut::(data.as_bytes()); + let data = data.as_bytes(); + let mut buffer = vec![0u8; data.len() + 16]; + buffer[..data.len()].copy_from_slice(data); + let encrypted = cipher + .encrypt_padded_mut::(&mut buffer, data.len()) + .map_err(|e| format!("Error encrypting data: {e}"))?; let mut result = BASE64_STANDARD.encode(self.secret_key_salt); - result.push_str(&BASE64_STANDARD.encode(&encrypted)); + result.push_str(&BASE64_STANDARD.encode(encrypted)); Ok(EncryptedText::new(result)) } @@ -118,9 +134,12 @@ impl Encryption { } let cipher = Aes256CbcDec::new(&self.key.into(), &self.iv.into()); - let decrypted = cipher.decrypt_padded_vec_mut::(encrypted).map_err(|e| format!("Error decrypting data: {e}"))?; + let mut buffer = encrypted.to_vec(); + let decrypted = cipher + .decrypt_padded_mut::(&mut buffer) + .map_err(|e| format!("Error decrypting data: {e}"))?; - String::from_utf8(decrypted).map_err(|e| format!("Error converting decrypted data to string: {}", e)) + String::from_utf8(decrypted.to_vec()).map_err(|e| format!("Error converting decrypted data to string: {}", e)) } } From 90d65bb7d6f4ef14b47778716914b63742fd65a1 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Mar 2026 14:53:41 +0100 Subject: [PATCH 26/27] Added permissions to GitHub Actions jobs (#707) --- .github/workflows/build-and-release.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 091faafb..74351c33 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -14,6 +14,8 @@ jobs: read_metadata: name: Read metadata runs-on: ubuntu-latest + permissions: + contents: read outputs: formatted_version: ${{ steps.format_metadata.outputs.formatted_version }} formatted_build_time: ${{ steps.format_metadata.outputs.formatted_build_time }} @@ -80,6 +82,8 @@ jobs: build_main: name: Build app (${{ matrix.dotnet_runtime }}) needs: read_metadata + permissions: + contents: read strategy: fail-fast: true @@ -703,6 +707,7 @@ jobs: runs-on: ubuntu-latest needs: [build_main, read_metadata] if: startsWith(github.ref, 'refs/tags/v') + permissions: {} steps: - name: Create artifact directory run: mkdir -p $GITHUB_WORKSPACE/artifacts From e416552467623c8b41309445e9ca934db4eb0e23 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 23 Mar 2026 11:11:50 +0100 Subject: [PATCH 27/27] Fixed local Windows builds (#710) --- runtime/Cargo.lock | 17 ++++------------- runtime/Cargo.toml | 2 -- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index a2e5815f..57018f5b 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -238,7 +238,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", - "untrusted 0.7.1", "zeroize", ] @@ -2776,7 +2775,6 @@ dependencies = [ "aes", "arboard", "async-stream", - "aws-lc-rs", "base64 0.22.1", "bytes", "calamine", @@ -2798,7 +2796,6 @@ dependencies = [ "rcgen", "reqwest 0.13.2", "rocket", - "rustls-webpki 0.103.10", "serde", "serde_json", "sha2", @@ -4172,7 +4169,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.15", "libc", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] @@ -4408,7 +4405,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ "ring", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -4420,7 +4417,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -4472,7 +4469,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ "ring", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -5785,12 +5782,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index acd606e1..0fb62f1a 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -45,9 +45,7 @@ sysinfo = "0.38.4" # Fixes security vulnerability downstream, where the upstream is not fixed yet: time = "0.3.47" # -> Rocket bytes = "1.11.1" # -> almost every dependency -aws-lc-rs = "1.16.2" # -> reqwest tar = "0.4.45" # -> Tauri v1 -rustls-webpki = "0.103.10" # -> tokio, reqwest [target.'cfg(target_os = "linux")'.dependencies] # See issue https://github.com/tauri-apps/tauri/issues/4470