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] 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.