Backend API
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { AzureOpenAI, type OpenAI } from 'openai';
import { AIIntegration } from 'devextreme-react/common/ai-integration';
import type { AIResponse, RequestParams, Response } from 'devextreme-react/common/ai-integration';
import type { ValidationRule } from 'devextreme-react/common';
import { Button } from 'devextreme-react/button';
import type { ButtonTypes } from 'devextreme-react/button';
import {
Form,
Item,
GroupItem,
ButtonItem,
} from 'devextreme-react/form';
import type { FormRef } from 'devextreme-react/form';
import { TextArea } from 'devextreme-react/text-area';
import type { TextAreaTypes } from 'devextreme-react/text-area';
import notify from 'devextreme/ui/notify';
import type { ITextBoxOptions } from 'devextreme-react/text-box';
import type { IDateBoxOptions } from 'devextreme-react/date-box';
import type { INumberBoxOptions } from 'devextreme-react/number-box';
import { AzureOpenAIConfig, defaultText } from './data.ts';
type AIMessage = (OpenAI.ChatCompletionUserMessageParam | OpenAI.ChatCompletionSystemMessageParam) & {
content: string;
};
const stylingMode = 'filled';
const amountDueEditorOptions: ITextBoxOptions = { placeholder: '$0.00', stylingMode };
const amountDueAIOptions = { instruction: 'Format as the following: $0.00' };
const statementDueEditorOptions: IDateBoxOptions = { placeholder: 'MM/DD/YYYY', stylingMode };
const statementDueAIOptions = { instruction: 'Format as the following: MM/DD/YYYY' };
const textEditorOptions: ITextBoxOptions = { stylingMode };
const phoneEditorOptions: ITextBoxOptions = { placeholder: '(000) 000-0000', stylingMode };
const phoneAIOptions = { instruction: 'Format as the following: (000) 000-0000' };
const emailValidationRules: ValidationRule[] = [{ type: 'email' }];
const emailAIOptions = { instruction: 'Do not fill this field if the text contains an invalid email address. A valid email is in the following format: email@example.com' };
const zipEditorOptions: INumberBoxOptions = { stylingMode, mode: 'text' };
const zipAIOptions = { instruction: 'If the text does not contain a ZIP, determine the ZIP code from the provided address.' };
const resetButtonOptions: ButtonTypes.Properties = {
stylingMode: 'outlined',
type: 'normal',
};
const smartPasteButtonOptions: ButtonTypes.Properties = {
stylingMode: 'contained',
type: 'default',
};
const colCountByScreen = {
xs: 2,
sm: 2,
md: 2,
lg: 2,
};
const showNotification = (message: string, of: string, isError?: boolean, offset?: string): void => {
notify({
message,
position: {
my: 'bottom center',
at: 'bottom center',
of,
offset: offset ?? '0 -50',
},
width: 'fit-content',
maxWidth: 'fit-content',
minWidth: 'fit-content',
}, isError ? 'error' : 'info', 1500);
};
const aiService = new AzureOpenAI(AzureOpenAIConfig);
export async function getAIResponse(messages: AIMessage[], signal: AbortSignal): Promise<AIResponse> {
const params = {
messages,
model: AzureOpenAIConfig.deployment,
max_completion_tokens: 1000,
temperature: 0.7,
};
const response = await aiService.chat.completions.create(params, { signal });
return response.choices[0].message?.content ?? '';
}
export const aiIntegration = new AIIntegration({
sendRequest({ prompt }: RequestParams): Response {
const controller = new AbortController();
const signal = controller.signal;
const aiPrompt: AIMessage[] = [
{ role: 'system', content: prompt.system ?? '' },
{ role: 'user', content: prompt.user ?? '' },
];
const promise = getAIResponse(aiPrompt, signal);
promise.catch((): void => {
showNotification('Something went wrong. Please try again.', '#form', true);
});
return {
promise,
abort: (): void => {
controller.abort();
},
};
},
});
const App = () => {
const formRef = useRef<FormRef>(null);
const [text, setText] = useState<string>(defaultText);
const onCopy = useCallback(() => {
navigator.clipboard.writeText(text);
showNotification('Text copied to clipboard', '#textarea', false, '0 -20');
}, [text]);
const shortcutHandler = useCallback((event: KeyboardEvent): void => {
if (event.ctrlKey && event.shiftKey) {
navigator.clipboard.readText()
.then((clipboardText: string): void => {
if (clipboardText) {
formRef.current?.instance().smartPaste(clipboardText);
} else {
showNotification('Clipboard is empty. Copy text before pasting', '#form');
}
})
.catch((): void => {
showNotification('Could not access the clipboard', '#form');
});
}
}, []);
useEffect(() => {
formRef.current?.instance().registerKeyHandler('V', shortcutHandler);
}, [shortcutHandler]);
const onTextAreaValueChanged = useCallback((e: TextAreaTypes.ValueChangedEvent): void => {
setText(e.value);
}, []);
return (
<>
<div id="textarea-label" className="instruction">
Copy text from the editor below to the clipboard. Edit the text to see how your changes affect Smart Paste result.
</div>
<div className="instruction">
Paste text from the clipboard to populate the form. Press Ctrl+Shift+V (when the form is focused) or use the "Smart Paste" button under the form.
</div>
<div className="textarea-container">
<Button
text="Copy Text"
icon="copy"
stylingMode="contained"
type="default"
width="fit-content"
onClick={onCopy}
/>
<TextArea
id="textarea"
inputAttr={{ 'aria-labelledby': 'textarea-label' }}
value={text}
stylingMode="filled"
height="100%"
onValueChanged={onTextAreaValueChanged}
/>
</div>
<Form
id="form"
ref={formRef}
labelMode="outside"
labelLocation="top"
showColonAfterLabel={false}
minColWidth={220}
aiIntegration={aiIntegration}
>
<GroupItem colCountByScreen={colCountByScreen} caption="Billing Summary">
<Item
dataField="Amount Due"
editorType="dxTextBox"
editorOptions={amountDueEditorOptions}
aiOptions={amountDueAIOptions}
/>
<Item
dataField="Statement Date"
editorType="dxDateBox"
editorOptions={statementDueEditorOptions}
aiOptions={statementDueAIOptions}
/>
</GroupItem>
<GroupItem colCountByScreen={colCountByScreen} caption="Billing Information">
<Item
dataField="First Name"
editorType="dxTextBox"
editorOptions={textEditorOptions}
/>
<Item
dataField="Last Name"
editorType="dxTextBox"
editorOptions={textEditorOptions}
/>
<Item
dataField="Phone Number"
editorType="dxTextBox"
editorOptions={phoneEditorOptions}
aiOptions={phoneAIOptions}
/>
<Item
dataField="Email"
editorType="dxTextBox"
editorOptions={textEditorOptions}
aiOptions={emailAIOptions}
validationRules={emailValidationRules}
/>
</GroupItem>
<GroupItem colCountByScreen={colCountByScreen} caption="Billing Address">
<Item
dataField="Street Address"
editorType="dxTextBox"
editorOptions={textEditorOptions}
/>
<Item
dataField="City"
editorType="dxTextBox"
editorOptions={textEditorOptions}
/>
<Item
dataField="State/Province/Region"
editorType="dxTextBox"
editorOptions={textEditorOptions}
/>
<Item
dataField="ZIP"
editorType="dxNumberBox"
editorOptions={zipEditorOptions}
aiOptions={zipAIOptions}
/>
</GroupItem>
<GroupItem cssClass="buttons-group" colCountByScreen={colCountByScreen}>
<ButtonItem buttonOptions={smartPasteButtonOptions} name="smartPaste" />
<ButtonItem buttonOptions={resetButtonOptions} name="reset" />
</GroupItem>
</Form>
</>
);
};
export default App;
import React, {
useCallback, useEffect, useRef, useState,
} from 'react';
import { AzureOpenAI } from 'openai';
import { AIIntegration } from 'devextreme-react/common/ai-integration';
import { Button } from 'devextreme-react/button';
import {
Form, Item, GroupItem, ButtonItem,
} from 'devextreme-react/form';
import { TextArea } from 'devextreme-react/text-area';
import notify from 'devextreme/ui/notify';
import { AzureOpenAIConfig, defaultText } from './data.js';
const stylingMode = 'filled';
const amountDueEditorOptions = { placeholder: '$0.00', stylingMode };
const amountDueAIOptions = { instruction: 'Format as the following: $0.00' };
const statementDueEditorOptions = { placeholder: 'MM/DD/YYYY', stylingMode };
const statementDueAIOptions = { instruction: 'Format as the following: MM/DD/YYYY' };
const textEditorOptions = { stylingMode };
const phoneEditorOptions = { placeholder: '(000) 000-0000', stylingMode };
const phoneAIOptions = { instruction: 'Format as the following: (000) 000-0000' };
const emailValidationRules = [{ type: 'email' }];
const emailAIOptions = {
instruction:
'Do not fill this field if the text contains an invalid email address. A valid email is in the following format: email@example.com',
};
const zipEditorOptions = { stylingMode, mode: 'text' };
const zipAIOptions = {
instruction:
'If the text does not contain a ZIP, determine the ZIP code from the provided address.',
};
const resetButtonOptions = {
stylingMode: 'outlined',
type: 'normal',
};
const smartPasteButtonOptions = {
stylingMode: 'contained',
type: 'default',
};
const colCountByScreen = {
xs: 2,
sm: 2,
md: 2,
lg: 2,
};
const showNotification = (message, of, isError, offset) => {
notify(
{
message,
position: {
my: 'bottom center',
at: 'bottom center',
of,
offset: offset ?? '0 -50',
},
width: 'fit-content',
maxWidth: 'fit-content',
minWidth: 'fit-content',
},
isError ? 'error' : 'info',
1500,
);
};
const aiService = new AzureOpenAI(AzureOpenAIConfig);
export async function getAIResponse(messages, signal) {
const params = {
messages,
model: AzureOpenAIConfig.deployment,
max_completion_tokens: 1000,
temperature: 0.7,
};
const response = await aiService.chat.completions.create(params, { signal });
return response.choices[0].message?.content ?? '';
}
export const aiIntegration = new AIIntegration({
sendRequest({ prompt }) {
const controller = new AbortController();
const signal = controller.signal;
const aiPrompt = [
{ role: 'system', content: prompt.system ?? '' },
{ role: 'user', content: prompt.user ?? '' },
];
const promise = getAIResponse(aiPrompt, signal);
promise.catch(() => {
showNotification('Something went wrong. Please try again.', '#form', true);
});
return {
promise,
abort: () => {
controller.abort();
},
};
},
});
const App = () => {
const formRef = useRef(null);
const [text, setText] = useState(defaultText);
const onCopy = useCallback(() => {
navigator.clipboard.writeText(text);
showNotification('Text copied to clipboard', '#textarea', false, '0 -20');
}, [text]);
const shortcutHandler = useCallback((event) => {
if (event.ctrlKey && event.shiftKey) {
navigator.clipboard
.readText()
.then((clipboardText) => {
if (clipboardText) {
formRef.current?.instance().smartPaste(clipboardText);
} else {
showNotification('Clipboard is empty. Copy text before pasting', '#form');
}
})
.catch(() => {
showNotification('Could not access the clipboard', '#form');
});
}
}, []);
useEffect(() => {
formRef.current?.instance().registerKeyHandler('V', shortcutHandler);
}, [shortcutHandler]);
const onTextAreaValueChanged = useCallback((e) => {
setText(e.value);
}, []);
return (
<>
<div
id="textarea-label"
className="instruction"
>
Copy text from the editor below to the clipboard. Edit the text to see how your changes
affect Smart Paste result.
</div>
<div className="instruction">
Paste text from the clipboard to populate the form. Press Ctrl+Shift+V (when the form is
focused) or use the "Smart Paste" button under the form.
</div>
<div className="textarea-container">
<Button
text="Copy Text"
icon="copy"
stylingMode="contained"
type="default"
width="fit-content"
onClick={onCopy}
/>
<TextArea
id="textarea"
inputAttr={{ 'aria-labelledby': 'textarea-label' }}
value={text}
stylingMode="filled"
height="100%"
onValueChanged={onTextAreaValueChanged}
/>
</div>
<Form
id="form"
ref={formRef}
labelMode="outside"
labelLocation="top"
showColonAfterLabel={false}
minColWidth={220}
aiIntegration={aiIntegration}
>
<GroupItem
colCountByScreen={colCountByScreen}
caption="Billing Summary"
>
<Item
dataField="Amount Due"
editorType="dxTextBox"
editorOptions={amountDueEditorOptions}
aiOptions={amountDueAIOptions}
/>
<Item
dataField="Statement Date"
editorType="dxDateBox"
editorOptions={statementDueEditorOptions}
aiOptions={statementDueAIOptions}
/>
</GroupItem>
<GroupItem
colCountByScreen={colCountByScreen}
caption="Billing Information"
>
<Item
dataField="First Name"
editorType="dxTextBox"
editorOptions={textEditorOptions}
/>
<Item
dataField="Last Name"
editorType="dxTextBox"
editorOptions={textEditorOptions}
/>
<Item
dataField="Phone Number"
editorType="dxTextBox"
editorOptions={phoneEditorOptions}
aiOptions={phoneAIOptions}
/>
<Item
dataField="Email"
editorType="dxTextBox"
editorOptions={textEditorOptions}
aiOptions={emailAIOptions}
validationRules={emailValidationRules}
/>
</GroupItem>
<GroupItem
colCountByScreen={colCountByScreen}
caption="Billing Address"
>
<Item
dataField="Street Address"
editorType="dxTextBox"
editorOptions={textEditorOptions}
/>
<Item
dataField="City"
editorType="dxTextBox"
editorOptions={textEditorOptions}
/>
<Item
dataField="State/Province/Region"
editorType="dxTextBox"
editorOptions={textEditorOptions}
/>
<Item
dataField="ZIP"
editorType="dxNumberBox"
editorOptions={zipEditorOptions}
aiOptions={zipAIOptions}
/>
</GroupItem>
<GroupItem
cssClass="buttons-group"
colCountByScreen={colCountByScreen}
>
<ButtonItem
buttonOptions={smartPasteButtonOptions}
name="smartPaste"
/>
<ButtonItem
buttonOptions={resetButtonOptions}
name="reset"
/>
</GroupItem>
</Form>
</>
);
};
export default App;
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.tsx';
ReactDOM.render(
<App />,
document.getElementById('app'),
);
export const AzureOpenAIConfig = {
dangerouslyAllowBrowser: true,
deployment: 'demo-mini',
apiVersion: '2024-02-01',
endpoint: 'https://public-api.devexpress.com/demo-openai',
apiKey: 'DEMO',
};
export const defaultText = `Payment: Amount - $123.00
Statement Date: 10/15/2024
Name: John Smith
Contact: (123) 456-7890
Email: john@myemail.com
Address:
- 123 Elm St Apt 4B
- New York, NY 10001
`;
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.js';
ReactDOM.render(<App />, document.getElementById('app'));
export const AzureOpenAIConfig = {
dangerouslyAllowBrowser: true,
deployment: 'demo-mini',
apiVersion: '2024-02-01',
endpoint: 'https://public-api.devexpress.com/demo-openai',
apiKey: 'DEMO',
};
export const defaultText = `Payment: Amount - $123.00
Statement Date: 10/15/2024
Name: John Smith
Contact: (123) 456-7890
Email: john@myemail.com
Address:
- 123 Elm St Apt 4B
- New York, NY 10001
`;
<!DOCTYPE html>
<html lang="en">
<head>
<title>DevExtreme Demo</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" />
<link rel="stylesheet" type="text/css" href="https://cdn3.devexpress.com/jslib/26.1.5/css/dx.light.css" />
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" />
<link rel="stylesheet" type="text/css" href="./bundle.css" />
</head>
<body class="dx-viewport">
<div class="demo-container">
<div id="app"></div>
</div>
<script src="bundles/vendor/react.vendor.js"></script>
<script src="./bundle.js"></script>
</body>
</html>
#app {
display: grid;
grid-template-columns: 1fr 2fr;
grid-template-rows: auto auto;
gap: 24px 40px;
min-width: 720px;
max-width: 900px;
margin: auto;
}
.instruction {
color: var(--dx-texteditor-color-label);
}
.textarea-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.dx-layout-manager .dx-field-item.dx-last-row {
padding-top: 4px;
}
.dx-toast-info .dx-toast-icon {
display: none;
}
.buttons-group {
display: flex;
width: 100%;
justify-content: end;
}
.buttons-group .dx-item-content {
gap: 8px;
}
.buttons-group .dx-field-item:not(.dx-first-col),
.buttons-group .dx-field-item:not(.dx-last-col) {
padding: 0;
}
.buttons-group .dx-item {
flex: unset !important;
}
Use the following APIs to activate Smart Paste in our Form component:
- aiIntegration - accepts an AIIntegration object that contains AI Service settings.
- 'smartPaste' – adds a built-in Smart Paste button to the Form (see name for additional information). To use this capability in code, call the smartPaste(text) method. This sample leverages this method and implements a custom shortcut to activate Smart Paste.
Configure each Form item using aiOptions:
- disabled - prevents AI-generated text from being pasted into this item.
- instruction - specifies item instruction for the AI service.