Business Central (Part 29)
Neil Haddley • May 24, 2024
PromptDialog (Part 3)
I extended the PromptDialog to render Markdown-formatted LLM responses as HTML. Because LLMs typically return Markdown, displaying the raw text was hard to read. I added a controladdin that uses the ShowdownJS library to convert Markdown to HTML and inject it into the page.

I provided a prompt that would return a Markdown formatted response

The Markdown formatted result was hard to read

I added a controladdin to the PromptDialog

The CreateHTMLFromMarkdown JavaScript function is the key move

I opened the file in the Sources panel

Debugging the JavaScript
MarkdownBoxFunctions.js
JAVASCRIPT
1function CreateHTMLFromMarkdown(markdown) { 2 3 const container = document.getElementById("controlAddIn"); 4 5 if (!container) return; 6 7 container.innerHTML = ""; 8 9 // https://showdownjs.com/docs/markdown-syntax/#tables 10 11 const converter = new showdown.Converter({ tables: true }); 12 13 const htmlContent = converter.makeHtml(markdown); 14 15 const contentDiv = document.createElement("div"); 16 17 contentDiv.id = "response"; 18 19 contentDiv.style.cssText = "overflow: auto; padding: 10px;"; 20 21 contentDiv.innerHTML = htmlContent; 22 23 container.appendChild(contentDiv); 24 25 // Adjust content height dynamically after rendering 26 27 setTimeout(() => { 28 29 contentDiv.style.height = `calc(100vh)`; 30 31 }, 0); 32 33 // Notify Business Central that control is ready 34 35 Microsoft.Dynamics.NAV.InvokeExtensibilityMethod("ControlReady"); 36 37}
Pag50100.LLM.al
AL
1namespace LLM.LLM; 2 3using System.AI; 4 5controladdin MarkdownBox 6{ 7 RequestedHeight = 500; 8 MaximumHeight = 700; 9 VerticalStretch = false; 10 VerticalShrink = false; 11 HorizontalStretch = true; 12 HorizontalShrink = true; 13 Scripts = 'showdown.min.js', 'MarkdownBoxFunctions.js'; 14 // ShowdownJS v 2.0 is released under the MIT license. Previous versions are released under BSD. https://github.com/showdownjs/showdown 15 16 event ControlReady() 17 procedure CreateHTMLFromMarkdown(markdown: Text); 18} 19 20enumextension 50100 LLM extends "Copilot Capability" 21{ 22 value(50100; "LLM") { } 23} 24 25page 50100 LLM 26{ 27 ApplicationArea = All; 28 Caption = 'LLM'; 29 PageType = PromptDialog; 30 Extensible = false; 31 DataCaptionExpression = Prompt; 32 IsPreview = true; 33 34 35 layout 36 { 37 #region input section 38 area(Prompt) 39 { 40 field(PromptField; Prompt) 41 { 42 ShowCaption = false; 43 MultiLine = true; 44 InstructionalText = 'Message Large Language Model'; 45 } 46 } 47 #endregion 48 49 #region output section 50 area(Content) 51 { 52 usercontrol(Formatted; MarkdownBox) 53 { 54 ApplicationArea = All; 55 } 56 57 /*field(OutputFiled; Output) 58 { 59 ShowCaption = false; 60 MultiLine = true; 61 }*/ 62 } 63 #endregion 64 } 65 actions 66 { 67 #region prompt guide 68 area(PromptGuide) 69 { 70 action("Capital City") 71 { 72 ApplicationArea = All; 73 Caption = 'Capital City'; 74 ToolTip = 'What is the Capital of ...'; 75 76 trigger OnAction() 77 begin 78 Prompt := 'What is the Capital of <Country>?'; 79 end; 80 } 81 } 82 #endregion 83 84 #region system actions 85 area(SystemActions) 86 { 87 systemaction(Generate) 88 { 89 Caption = 'Generate'; 90 ToolTip = 'Generate a response.'; 91 92 trigger OnAction() 93 begin 94 RunGeneration(); 95 CurrPage.Formatted.CreateHTMLFromMarkdown(Output); 96 end; 97 } 98 systemaction(OK) 99 { 100 Caption = 'OK'; 101 ToolTip = 'Save the result.'; 102 } 103 systemaction(Cancel) 104 { 105 Caption = 'Cancel'; 106 ToolTip = 'Discard the result.'; 107 } 108 systemaction(Regenerate) 109 { 110 Caption = 'Regenerate'; 111 ToolTip = 'Regenerate a response.'; 112 113 trigger OnAction() 114 begin 115 RunGeneration(); 116 CurrPage.Formatted.CreateHTMLFromMarkdown(Output); 117 end; 118 } 119 #endregion 120 } 121 } 122 var 123 Prompt: Text; 124 Output: Text; 125 126 127 local procedure RunGeneration() 128 var 129 CopilotCapability: Codeunit "Copilot Capability"; 130 AzureOpenAI: Codeunit "Azure OpenAI"; 131 AOAIDeployments: Codeunit "AOAI Deployments"; 132 AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params"; 133 AOAIChatMessages: Codeunit "AOAI Chat Messages"; 134 AOAIOperationResponse: Codeunit "AOAI Operation Response"; 135 UserMessage: TextBuilder; 136 begin 137 138 // CopilotCapability.UnregisterCapability(Enum::"Copilot Capability"::LLM); 139 140 if not CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::LLM) then 141 CopilotCapability.RegisterCapability(Enum::"Copilot Capability"::LLM, 'https://about:none'); 142 143 AzureOpenAI.SetAuthorization(Enum::"AOAI Model Type"::"Chat Completions", GetEndpoint(), GetDeployment(), GetApiKey()); 144 AzureOpenAI.SetCopilotCapability(Enum::"Copilot Capability"::LLM); 145 146 147 AOAIChatCompletionParams.SetMaxTokens(2500); 148 AOAIChatCompletionParams.SetTemperature(0); 149 AOAIChatMessages.AddUserMessage(Prompt); 150 151 AzureOpenAI.GenerateChatCompletion(AOAIChatMessages, AOAIChatCompletionParams, AOAIOperationResponse); 152 if (AOAIOperationResponse.IsSuccess()) then 153 Output := AOAIChatMessages.GetLastMessage() 154 155 else 156 Output := ('Error: ' + AOAIOperationResponse.GetError()); 157 158 end; 159 160 161 local procedure GetEndpoint(): Text 162 var 163 begin 164 exit('https://haddley-ai.openai.azure.com'); 165 end; 166 167 local procedure GetDeployment(): Text 168 var 169 begin 170 exit('gpt-4o'); 171 end; 172 173 [NonDebuggable] 174 local procedure GetApiKey(): Text //SecretText 175 var 176 begin 177 exit('1234567890123456789012'); 178 end; 179}