Building a Custom AI Text Editor using Microsoft’s Azure OpenAI

By | April 22, 2024

In today’s world, the demand for intelligent applications that can analyze and transform text is increasing rapidly. AI-powered text editors are becoming popular for generating content as well as data restructuring tools for many developers and businesses.

For any business, it is important to have data stored in the CRM system consistently and according to business standards. Users need to be trained to follow these standards or formats when entering data into the CRM system. However, mistakes are often made, leading to inconsistencies in the data.

Artificial Intelligence (AI) can be a solution to this problem. We aim to develop an AI Text Editor capable of analyzing user-input text and offering suggestions for restructuring it based on predefined formats.

We’ll use the capabilities of Azure OpenAI service, a platform for natural language processing tasks, that will allow our text to be analyzed and transformed into the desired format. In this blog, we’ll guide you on how to build a custom AI text editor using PCF (Power Apps Component Framework) and Azure OpenAI service.

Create Azure OpenAI service and get Azure OpenAI API Key and Endpoint

  1. Currently, at the time of writing this blog, access to Azure OpenAI service in Azure is limited. You need to apply here for access.
  2. Once you get access from Microsoft, login to the Azure Portal at https://portal.azure.com and create Azure OpenAI service.
    AI Text Editor using Azure OpenAI
  3. Configure the service according to your needs. This may include selecting a pricing tier, specifying a location for the service, and setting up resource groups and tags. You can refer to this article to create an Azure OpenAI service.
  4. Once the Azure OpenAI Service resource is created, navigate to it in the Azure portal and You should find your API key and endpoint listed in the Key and Endpoint section under Resource Management as shown below. These are the credentials you’ll use to authenticate and access the OpenAI API. AI Text Editor using Azure OpenAI
  5. To call Azure OpenAI API and generate text, you need to deploy an appropriate model that suits the requirement.
  6. To create and manage deployments, you can use Azure AI Studio (preview). https://oai.azure.com. Create a new deployment and save the “Deployment name” to be used for calling Azure OpenAI. Here, we have deployed “gpt-35-turbo” model with version “0613”.
    AI Text Editor using Azure OpenAI
  7. By following these steps, we have obtained three values: API Key, Endpoint, and Deployment ID/Name, which are required for calling the Azure OpenAI service. In the next section, we will see how to use these values to use generative AI in the PCF control.

Create field level Power Apps Control Framework control:

Here, we will create a field-level PCF control that can be bound to a multiline or single line of text fields in the Power Apps. You can follow the steps outlined in the following article to create a field PCF control, Create a field level model driven apps control.

  • Install @azure/openai module: Once the PCF control is created, use the “npm install @azure/openai” command to install the Azure OpenAI libraries in the project.
  • Create a property named “Prompt” in ControlManifest.Input.xml for the user to insert the format they want to convert the text into while configuring the control through Power Apps.

AI Text Editor using Azure OpenAI

  • Create a “App.tsx” file to add Fluent UI components and call it in the index.tsx file.

AI Text Editor using Azure OpenAI

  • In the App.tsx file, create a UI which consists following elements,

Multiline Text Field: Editable text field where user can enter their data.

Submit button: A button to execute or call Azure OpenAI API to generate a text based on the instruction provided.

Information Label: When utilizing AI-generated text, it is crucial to review the content carefully before use. This is a message to the user to review AI generated content before use.

 
<div
style={{
width: "100%",
display: "flex",
flexDirection: "column",
position: "relative",
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
padding: "10px 0px 10px 0px",
}}
>
<div style={{ flexGrow: 1, marginRight: "10px" }}>
<TextField
type=""
multiline
rows={10}
autoAdjustHeight
value={inputValue}
disabled={isButtonDisabled}
onChange={handleChange}
/>
</div>
</div>
<div>
<PrimaryButton
text="Submit"
onClick={handleSubmit}
disabled={isButtonDisabled}
/>
</div>
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}
>
{isLoading && (
<Spinner
label="Generating..."
styles={{
label: {
fontWeight: "bold",
color: "blue",
fontFamily: "Segoe UI",
},
}}
/>
)}
</div>
</div>

Using of Azure OpenAI in PCF:

  • In the onSubmitClick function, ensure to input the endpoint and azureApiKey obtained from the Azure AI Studio as explained above. It is recommended to use a secure method to access or use Keys and endpoints in the code.
  • Azure OpenAI needs a system instruction/message and a user prompt to generate content. It generates the content based on the instruction provided in the natural language.
  • In this demo, we are providing the static system message alongside the user prompt. Obtain the result using Azure OpenAI’s getChatCompletions(deploymentId, messages, options) Once the response is received, the output message will be sent back to the control and bound to the text field.
 
// Function to handle form submission
const onSubmitClick = async () => {
const endpoint = ""; // OpenAI endpoint
const azureApiKey = ""; // Azure API key

// Chat messages to be sent to OpenAI
const messages: ChatRequestMessage[] = [
{ role: "system", content:"You are an AI Text Editor where you primary role is to analyze text and help users transform it into a desired structure. " },
{ role: "user", content: prompt +" : "+ inputValue },
];

try {
// Confirmation dialog before proceeding
const confirmationResult = await context.navigation.openConfirmDialog(
{
text: "Proceeding will result in the restructuring of data. Click 'Ok' to proceed with the process. Please ensure that you have backed up any important data before proceeding.",
title: "Caution: Data Restructuring Ahead",
},
{ height: 200, width: 450 }
);

if (!confirmationResult.confirmed) return;

// Disable button and show loading spinner
setIsButtonDisabled(true);
setIsLoading(true);

// Check for empty input or prompt
if (!inputValue.trim() || inputValue == "" || prompt == "") {
return;
}

// Initialize OpenAI client
const client = new OpenAIClient(
endpoint,
new AzureKeyCredential(azureApiKey)
);
const deploymentId = ""; // Deployment ID
const options: GetChatCompletionsOptions = {
temperature:0,
topP: 0,
frequencyPenalty: 0.0,
presencePenalty: 0.0,
maxTokens: 800,
stop: [],
};

// Get completion from OpenAI
const result = await client.getChatCompletions(
deploymentId,
messages,
options
);

// Extract result value
const resultValue = result.choices[0].message?.content;

// Update input value and notify parent component
setInputValue(resultValue || "");
notifyChange?.(resultValue || "");

// Enable button and hide loading spinner
setIsButtonDisabled(false);
setIsLoading(false);
} catch (error:any) {
// Show error dialog and log error
context.navigation.openConfirmDialog(
{
text: error.message,
title: error.code,
},
{ height: 200, width: 450 }
);
console.error("An error occurred:", error.message);
setIsButtonDisabled(false);
setIsLoading(false);
}
};
  • Build the solution and import the solution.zip file in Dataverse. While configuration of control in the description field we are providing the prompt to convert the paragraph into a number list as shown below:
AI Text Editor using Azure OpenAI
  • In the description field of CRM, we are providing a random paragraph to demonstrate the conversion to number list.
AI Text Editor using Azure OpenAI
  • Once the input has been submitted, the output from OpenAI is displayed in the description field as shown below.
AI Text Editor using Azure OpenAI

index.ts

import { IInputs, IOutputs } from "./generated/ManifestTypes";
import * as React from "react";
import { ITextProps, AITextControl } from "./components/App";

export class AIDescriptionEditorControl
implements ComponentFramework.ReactControl<IInputs, IOutputs>
{
private theComponent: ComponentFramework.ReactControl<IInputs, IOutputs>;
private _navigation: ComponentFramework.Navigation;
/**
* Empty constructor.
*/
constructor() {}
private notifyOutputChanged: () => void;
private _container: HTMLDivElement;
private _value: any;
private _context: ComponentFramework.Context;
/**
* Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
* Data-set values are not initialized here, use updateView.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions.
* @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
* @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface.
*/
public init(
context: ComponentFramework.Context,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement
): void {
this.notifyOutputChanged = notifyOutputChanged;
// this._context = context;
// this._container = document.createElement("div");
// this._value = context.parameters.sampleProperty.raw || "";
// container.appendChild(this._container);
}
public async notifyOnSelect(value: string) {
this._value = value != null || value != undefined ? value : null;
this.notifyOutputChanged();
}
/**
* Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions
* @returns ReactElement root react element for the control
*/
public updateView(
context: ComponentFramework.Context
): React.ReactElement {
// Prepare props for the AITextControl component
let descriptionProps: ITextProps = {};
descriptionProps = {
value: context.parameters.AIDescriptionEditorControl.raw ?? "",
prompt: context.parameters.Prompt.raw ?? "",
context,
notifyChange: this.notifyOnSelect.bind(this),
};
//Render the AITextControl component
return (
React.createElement(AITextControl, descriptionProps)
);
}

/**
* It is called by the framework prior to a control receiving new data.
* @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as "bound" or "output"
*/
public getOutputs(): IOutputs {
return {
AIDescriptionEditorControl: this._value != null ? this._value : null,
};
}

/**
* Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
* i.e. cancelling any pending remote calls, removing listeners, etc.
*/
public destroy(): void {
// Add code to cleanup control if necessary
}
}

App.tsx

 
import * as React from "react";
import { TextField, PrimaryButton, Spinner, Label } from "@fluentui/react";
import { ChatRequestMessage, OpenAIClient, AzureKeyCredential, GetChatCompletionsOptions } from "@azure/openai";

// Props interface for the AI Text Control component
export interface ITextProps {
context?: any; // Context for the component
prompt ?: any; // prompt for the text
value?: string; // Initial value for the text field
notifyChange?: (value: string) => void; // Callback function to notify parent component of changes
}

// AI Text Control component
export const AITextControl: React.FC<ITextProps> = ({
context,
prompt ,
value,
notifyChange,
}) => {
// State variables
const [inputValue, setInputValue] = React.useState<string>(value || ""); // State for input value
const [isButtonDisabled, setIsButtonDisabled] = React.useState<boolean>(false); // State for button disabled state
const [isLoading, setIsLoading] = React.useState<boolean>(false); // State for loading spinner

// Function to handle input change
const onFieldTextChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
) => {
if (newValue !== undefined) {
setInputValue(newValue);
notifyChange?.(newValue);
}
};

// Function to handle form submission
const onSubmitClick = async () => {
const endpoint = "<YOUR_ENDPOINT_KEY_GOES_HERE>"; // OpenAI endpoint
const azureApiKey = "<YOUR_AZUREAPIKEY_GOES_HERE>"; // Azure API key

// Chat messages to be sent to OpenAI
const messages: ChatRequestMessage[] = [
{ role: "system", content:"You are an AI Text Editor where you primary role is to analyze text and help users transform it into a desired structure. " },
{ role: "user", content: prompt +" : "+ inputValue },
];

try {
// Confirmation dialog before proceeding
const confirmationResult = await context.navigation.openConfirmDialog(
{
text: "Proceeding will result in the restructuring of data. Click 'Ok' to proceed with the process. Please ensure that you have backed up any important data before proceeding.",
title: "Caution: Data Restructuring Ahead",
},
{ height: 200, width: 450 }
);

if (!confirmationResult.confirmed) return;

// Disable button and show loading spinner
setIsButtonDisabled(true);
setIsLoading(true);

// Check for empty input or prompt
if (!inputValue.trim() || inputValue == "" || prompt == "") {
return;
}

// Initialize OpenAI client
const client = new OpenAIClient(
endpoint,
new AzureKeyCredential(azureApiKey)
);
const deploymentId = "<YOUR_DEPLOYMENTID_GOES_HERE>"; // Deployment ID
const options: GetChatCompletionsOptions = {
temperature:0,
topP: 0,
frequencyPenalty: 0.0,
presencePenalty: 0.0,
maxTokens: 800,
stop: [],
};

// Get completion from OpenAI
const result = await client.getChatCompletions(
deploymentId,
messages,
options
);

// Extract result value
const resultValue = result.choices[0].message?.content;

// Update input value and notify parent component
setInputValue(resultValue || "");
notifyChange?.(resultValue || "");

// Enable button and hide loading spinner
setIsButtonDisabled(false);
setIsLoading(false);
} catch (error:any) {
// Show error dialog and log error
context.navigation.openConfirmDialog(
{
text: error.message,
title: error.code,
},
{ height: 200, width: 450 }
);
console.error("An error occurred:", error.message);
setIsButtonDisabled(false);
setIsLoading(false);
}
};
// Render component
return (
<div
style={{
width: "100%",
display: "flex",
flexDirection: "column",
position: "relative",
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
padding: "10px 0px 0px 0px",
}}
>
<div style={{ flexGrow: 1, marginRight: "10px" }}>
<TextField
type=""
multiline
rows={10}
autoAdjustHeight
value={inputValue}
disabled={isButtonDisabled}
onChange={onFieldTextChange}
/>
</div>
</div>
<Label styles={{ root: { fontSize: 11, fontFamily: "Calibri" } }}>
*The text generated by AI may not always be accurate or reliable. Verify
the inpromption before saving.
</Label>
<div>
<PrimaryButton
text="Submit"
onClick={onSubmitClick}
disabled={isButtonDisabled}
/>
</div>
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}
>
{/* Loading spinner */}
{isLoading && (
<Spinner
label="Generating..."
styles={{
label: {
fontWeight: "bold",
color: "blue",
fontFamily: "Segoe UI",
},
}}
/>
)}
</div>
</div>
);
};
 

Preview

Conclusion

In this blog post, we’ve explored how to build an AI Text Editor using Azure OpenAI. With this powerful tool, users can analyze and transform text with ease, making it a useful control across all dataverse tables for content generation, data restructuring, and more.

copilot