Backend API
<template>
<div
class="instruction"
id="textarea-label"
>
Copy text from the editor below to the clipboard.
Edit the text to see how your changes affect Smart Paste result.
</div>
<div class="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 class="textarea-container">
<DxButton
text="Copy Text"
icon="copy"
stylingMode="contained"
type="default"
width="fit-content"
@click="onCopy()"
/>
<DxTextArea
id="textarea"
:input-attr="{ 'aria-labelledby': 'textarea-label' }"
v-model:value="text"
stylingMode="filled"
height="100%"
/>
</div>
<DxForm
id="form"
ref="formRef"
labelMode="outside"
labelLocation="top"
:showColonAfterLabel="false"
:minColWidth="220"
:aiIntegration="aiIntegration"
>
<DxGroupItem
:colCountByScreen="colCountByScreen"
caption="Billing Summary"
>
<DxItem
dataField="Amount Due"
editorType="dxTextBox"
:editorOptions="amountDueEditorOptions"
:aiOptions="amountDueAIOptions"
/>
<DxItem
dataField="Statement Date"
editorType="dxDateBox"
:editorOptions="statementDueEditorOptions"
:aiOptions="statementDueAIOptions"
/>
</DxGroupItem>
<DxGroupItem
:colCountByScreen="colCountByScreen"
caption="Billing Information"
>
<DxItem
dataField="First Name"
editorType="dxTextBox"
:editorOptions="textEditorOptions"
/>
<DxItem
dataField="Last Name"
editorType="dxTextBox"
:editorOptions="textEditorOptions"
/>
<DxItem
dataField="Phone Number"
editorType="dxTextBox"
:editorOptions="phoneEditorOptions"
:aiOptions="phoneAIOptions"
/>
<DxItem
dataField="Email"
editorType="dxTextBox"
:editorOptions="textEditorOptions"
:aiOptions="emailAIOptions"
:validationRules="emailValidationRules"
/>
</DxGroupItem>
<DxGroupItem
:colCountByScreen="colCountByScreen"
caption="Billing Address"
>
<DxItem
dataField="Street Address"
editorType="dxTextBox"
:editorOptions="textEditorOptions"
/>
<DxItem
dataField="City"
editorType="dxTextBox"
:editorOptions="textEditorOptions"
/>
<DxItem
dataField="State/Province/Region"
editorType="dxTextBox"
:editorOptions="textEditorOptions"
/>
<DxItem
dataField="ZIP"
editorType="dxNumberBox"
:editorOptions="zipEditorOptions"
:aiOptions="zipAIOptions"
/>
</DxGroupItem>
<DxGroupItem
cssClass="buttons-group"
:colCountByScreen="colCountByScreen"
>
<DxButtonItem
:buttonOptions="smartPasteButtonOptions"
name="smartPaste"
/>
<DxButtonItem
:buttonOptions="resetButtonOptions"
name="reset"
/>
</DxGroupItem>
</DxForm>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { AzureOpenAI, type OpenAI } from 'openai';
import {
DxForm, DxItem, DxButtonItem, DxGroupItem,
} from 'devextreme-vue/form';
import type { ValidationRule } from 'devextreme-vue/common';
import type { RequestParams, Response } from 'devextreme-vue/common/ai-integration';
import { DxButton, type DxButtonTypes } from 'devextreme-vue/button';
import DxTextArea from 'devextreme-vue/text-area';
import { AIIntegration } from 'devextreme-vue/common/ai-integration';
import notify from 'devextreme/ui/notify';
import { AzureOpenAIConfig, defaultText } from './data.ts';
const text = ref(defaultText);
const formRef = ref();
type AIMessage = (
OpenAI.ChatCompletionUserMessageParam | OpenAI.ChatCompletionSystemMessageParam
) & {
content: string;
};
const aiService = new AzureOpenAI(AzureOpenAIConfig);
async function getAIResponse(messages: AIMessage[], signal: AbortSignal) {
const params = {
messages,
model: AzureOpenAIConfig.deployment,
max_tokens: 1000,
temperature: 0.7,
};
const response = await aiService.chat.completions.create(params, { signal });
const result = response.choices[0].message?.content;
return result || '';
}
const aiIntegration = new AIIntegration({
sendRequest({ prompt }: RequestParams) {
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(() => {
showNotification('Something went wrong. Please try again.', '#form', true);
});
const result: Response = {
promise,
abort: () => {
controller.abort();
},
};
return result;
},
});
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: 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 = { stylingMode, mode: 'text', value: null };
const zipAIOptions = { instruction: 'If the text does not contain a ZIP, determine the ZIP code from the provided address.' };
const resetButtonOptions: DxButtonTypes.Properties = {
stylingMode: 'outlined',
type: 'normal',
};
const smartPasteButtonOptions: DxButtonTypes.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) => {
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 onCopy = () => {
navigator.clipboard.writeText(text.value);
showNotification('Text copied to clipboard', '#textarea', false, '0 -20');
};
onMounted(() => {
formRef.value.instance.registerKeyHandler('V', (event: KeyboardEvent) => {
if (event.ctrlKey && event.shiftKey) {
navigator.clipboard.readText()
.then((clipboardText) => {
if (clipboardText) {
formRef.value.instance.smartPaste(clipboardText);
} else {
showNotification('Clipboard is empty. Copy text before pasting', '#form');
}
})
.catch(() => {
showNotification('Could not access the clipboard', '#form');
});
}
});
});
</script>
<style scoped>
#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;
}
</style>
<template>
<div id="template-content">
<i
id="helpedInfo"
class="dx-icon dx-icon-info"
/>
Additional
<br>
{{ data.text }}
<DxTooltip
target="#helpedInfo"
show-event="mouseenter"
hide-event="mouseleave"
>
<template #content>
<div id="tooltip-content">This field must not exceed 200 characters</div>
</template>
</DxTooltip>
</div>
</template>
<script setup lang="ts">
import { DxTooltip } from 'devextreme-vue/tooltip';
withDefaults(defineProps<{
data?: Record<string, any>
}>(), {
data: () => ({} as Record<string, any>),
});
</script>
<style scoped>
#helpedInfo {
color: #42a5f5;
}
#tooltip-content {
font-size: 14px;
font-weight: bold;
}
#template-content {
display: inline-flex;
}
</style>
<template>
<div>
<i :class="`dx-icon dx-icon-${icon}`"/>{{ data.text }}
</div>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
data?: Record<string, any>
icon?: string
}>(), {
data: () => ({} as Record<string, any>),
icon: 'info',
});
</script>
window.exports = window.exports || {};
window.config = {
transpiler: 'plugin-babel',
meta: {
'*.vue': {
loader: 'vue-loader',
},
'*.ts': {
loader: 'demo-ts-loader',
},
'*.svg': {
loader: 'svg-loader',
},
'devextreme/time_zone_utils.js': {
'esModule': true,
},
'devextreme/localization.js': {
'esModule': true,
},
'devextreme/viz/palette.js': {
'esModule': true,
},
'openai': {
'esModule': true,
},
},
paths: {
'project:': '../../../../',
'npm:': 'https://cdn.jsdelivr.net/npm/',
'bundles:': '../../../../bundles/',
'externals:': '../../../../bundles/externals/',
},
map: {
'vue': 'npm:vue@3.4.27/dist/vue.esm-browser.js',
'@vue/shared': 'npm:@vue/shared@3.4.27/dist/shared.cjs.prod.js',
'vue-loader': 'npm:dx-systemjs-vue-browser@1.1.2/index.js',
'demo-ts-loader': 'project:utils/demo-ts-loader.js',
'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js',
'svg-loader': 'project:utils/svg-loader.js',
'mitt': 'npm:mitt/dist/mitt.umd.js',
'rrule': 'npm:rrule@2.6.4/dist/es5/rrule.js',
'luxon': 'npm:luxon@3.4.4/build/global/luxon.min.js',
'es6-object-assign': 'npm:es6-object-assign',
'devextreme': 'npm:devextreme@link:../../packages/devextreme/artifacts/npm/devextreme/cjs',
'devextreme-vue': 'npm:devextreme-vue@link:../../packages/devextreme-vue/npm/cjs',
'openai': 'externals:openai.bundle.js',
'devextreme-quill': 'npm:devextreme-quill@1.7.8/dist/dx-quill.min.js',
'devexpress-diagram': 'npm:devexpress-diagram@2.2.24/dist/dx-diagram.js',
'devexpress-gantt': 'npm:devexpress-gantt@4.1.64/dist/dx-gantt.js',
'inferno': 'npm:inferno@8.2.3/dist/inferno.min.js',
'inferno-compat': 'npm:inferno-compat/dist/inferno-compat.min.js',
'inferno-create-element': 'npm:inferno-create-element@8.2.3/dist/inferno-create-element.min.js',
'inferno-dom': 'npm:inferno-dom/dist/inferno-dom.min.js',
'inferno-hydrate': 'npm:inferno-hydrate/dist/inferno-hydrate.min.js',
'inferno-clone-vnode': 'npm:inferno-clone-vnode/dist/inferno-clone-vnode.min.js',
'inferno-create-class': 'npm:inferno-create-class/dist/inferno-create-class.min.js',
'inferno-extras': 'npm:inferno-extras/dist/inferno-extras.min.js',
'@preact/signals-core': 'npm:@preact/signals-core@1.8.0/dist/signals-core.min.js',
'plugin-babel': 'npm:systemjs-plugin-babel@0.0.25/plugin-babel.js',
'systemjs-babel-build': 'npm:systemjs-plugin-babel@0.0.25/systemjs-babel-browser.js',
// Prettier
'prettier/standalone': 'npm:prettier@2.8.8/standalone.js',
'prettier/parser-html': 'npm:prettier@2.8.8/parser-html.js',
},
packages: {
'devextreme-vue': {
main: 'index.js',
},
'devextreme-vue/common': {
main: 'index.js',
},
'devextreme': {
defaultExtension: 'js',
},
'devextreme/events/utils': {
main: 'index',
},
'devextreme/common/core/events/utils': {
main: 'index',
},
'devextreme/events': {
main: 'index',
},
'es6-object-assign': {
main: './index.js',
defaultExtension: 'js',
},
},
packageConfigPaths: [
'npm:@devextreme/*/package.json',
],
babelOptions: {
sourceMaps: false,
stage0: true,
},
};
window.process = {
env: {
NODE_ENV: 'production',
},
};
System.config(window.config);
// eslint-disable-next-line
const useTgzInCSB = ['openai'];
let packagesInfo = {
"openai": {
"version": "4.73.1"
}
};
export const AzureOpenAIConfig = {
dangerouslyAllowBrowser: true,
deployment: 'gpt-4o-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 { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
<!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/25.2.3/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" />
<script src="https://cdn.jsdelivr.net/npm/typescript@5.9.3/lib/typescript.js"></script>
<script type="module">
import * as vueCompilerSFC from "https://cdn.jsdelivr.net/npm/@vue/compiler-sfc@3.4.27/dist/compiler-sfc.esm-browser.js";
window.vueCompilerSFC = vueCompilerSFC;
</script>
<script src="https://cdn.jsdelivr.net/npm/core-js@2.6.12/client/shim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@0.21.3/dist/system.js"></script>
<script type="text/javascript" src="config.js"></script>
<script type="text/javascript">
System.import("./index.ts");
</script>
</head>
<body class="dx-viewport">
<div class="demo-container">
<div id="app"> </div>
</div>
</body>
</html>
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.