diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index eeb90c5b..862fb9f6 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -1594,15 +1594,30 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1142475422"] = "Are you s -- Stop generation UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1317408357"] = "Stop generation" +-- Insert heading formatting +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1347686985"] = "Insert heading formatting" + -- Save chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1516264254"] = "Save chat" +-- Insert italic formatting +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T182467363"] = "Insert italic formatting" + -- Type your input here... UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1849313532"] = "Type your input here..." +-- Insert bulleted list formatting +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1850228954"] = "Insert bulleted list formatting" + -- Your Prompt (use selected instance '{0}', provider '{1}') UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1967611328"] = "Your Prompt (use selected instance '{0}', provider '{1}')" +-- Insert code formatting +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2278685708"] = "Insert code formatting" + +-- Insert bold formatting +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T265791106"] = "Insert bold formatting" + -- 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." diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor index 3c49a4b5..5686f49c 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor +++ b/app/MindWork AI Studio/Components/ChatComponent.razor @@ -55,6 +55,22 @@ Style="@this.UserInputStyle"/> + + + + + + + + + + + + + + + + @if ( this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY) @@ -127,4 +143,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..79f065ba 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -6,6 +6,7 @@ using AIStudio.Settings.DataModel; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; +using Microsoft.JSInterop; using DialogOptions = AIStudio.Dialogs.DialogOptions; @@ -13,6 +14,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 +44,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 +84,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 +475,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 +1042,4 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable } #endregion -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/wwwroot/app.js b/app/MindWork AI Studio/wwwroot/app.js index aa6b8e2b..0db881e9 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 : 'bold text' + insertedText = `**${text}**` + selectionStart = start + 2 + selectionEnd = selectionStart + text.length + break + } + + case 'italic': { + const text = hasSelection ? selectedText : 'italic text' + 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 = 'Heading' + 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 = 'code' + insertedText = `\`\`\`\n${text}\n\`\`\`` + selectionStart = start + 4 + 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 +}