Skip to content

Commit 81ff037

Browse files
authored
Form Smart Paste: Implement Demos (DevExpress#30986)
1 parent f3b9e6b commit 81ff037

31 files changed

+1950
-7
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#form-container {
2+
display: grid;
3+
grid-template-columns: 1fr 2fr;
4+
grid-template-rows: auto auto;
5+
gap: 24px 40px;
6+
min-width: 720px;
7+
max-width: 900px;
8+
margin: auto;
9+
}
10+
11+
.instruction {
12+
color: var(--dx-texteditor-color-label);
13+
}
14+
15+
.textarea-container {
16+
display: flex;
17+
flex-direction: column;
18+
gap: 16px;
19+
}
20+
21+
::ng-deep .dx-layout-manager .dx-field-item.dx-last-row {
22+
padding-top: 4px;
23+
}
24+
25+
::ng-deep .dx-toast-info .dx-toast-icon {
26+
display: none;
27+
}
28+
29+
::ng-deep .buttons-group {
30+
display: flex;
31+
width: 100%;
32+
justify-content: end;
33+
}
34+
35+
::ng-deep .buttons-group .dx-item-content {
36+
gap: 8px;
37+
}
38+
39+
::ng-deep .buttons-group .dx-field-item:not(.dx-first-col),
40+
::ng-deep .buttons-group .dx-field-item:not(.dx-last-col) {
41+
padding: 0;
42+
}
43+
44+
::ng-deep .buttons-group .dx-item {
45+
flex: unset !important;
46+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<div id="form-container">
2+
<div class="instruction" id="textarea-label"
3+
>Copy text from the editor below to the clipboard. Edit the text to see how
4+
your changes affect Smart Paste result.</div
5+
>
6+
<div class="instruction"
7+
>Paste text from the clipboard to populate the form. Press Ctrl+Shift+V or
8+
use the "Smart Paste" button under the form.</div
9+
>
10+
<div class="textarea-container">
11+
<dx-button
12+
id="copy"
13+
text="Copy Text"
14+
icon="copy"
15+
stylingMode="contained"
16+
type="default"
17+
width="fit-content"
18+
(onClick)="onCopy()"
19+
></dx-button>
20+
<dx-text-area
21+
id="textarea"
22+
[elementAttr]="{ 'aria-labelledby': 'textarea-label' }"
23+
[(value)]="text"
24+
stylingMode="filled"
25+
height="100%"
26+
></dx-text-area>
27+
</div>
28+
<dx-form
29+
id="form"
30+
labelMode="outside"
31+
labelLocation="top"
32+
[showColonAfterLabel]="false"
33+
[minColWidth]="220"
34+
[aiIntegration]="aiIntegration"
35+
stylingMode="filled"
36+
>
37+
<dxi-form-item
38+
itemType="group"
39+
[colCountByScreen]="colCountByScreen"
40+
caption="Billing Summary"
41+
>
42+
<dxi-form-item
43+
dataField="Amount Due"
44+
editorType="dxTextBox"
45+
[editorOptions]="amountDueEditorOptions"
46+
[aiOptions]="amountDueAIOptions"
47+
></dxi-form-item>
48+
<dxi-form-item
49+
dataField="Statement Date"
50+
editorType="dxDateBox"
51+
[editorOptions]="statementDueEditorOptions"
52+
[aiOptions]="statementDueAIOptions"
53+
></dxi-form-item>
54+
</dxi-form-item>
55+
<dxi-form-item
56+
itemType="group"
57+
[colCountByScreen]="colCountByScreen"
58+
caption="Billing Information"
59+
>
60+
<dxi-form-item
61+
dataField="First Name"
62+
editorType="dxTextBox"
63+
[editorOptions]="textEditorOptions"
64+
></dxi-form-item>
65+
<dxi-form-item
66+
dataField="Last Name"
67+
editorType="dxTextBox"
68+
[editorOptions]="textEditorOptions"
69+
></dxi-form-item>
70+
<dxi-form-item
71+
dataField="Phone Number"
72+
editorType="dxTextBox"
73+
[editorOptions]="phoneEditorOptions"
74+
[aiOptions]="phoneAIOptions"
75+
>
76+
</dxi-form-item>
77+
<dxi-form-item
78+
dataField="Email"
79+
editorType="dxTextBox"
80+
[editorOptions]="textEditorOptions"
81+
[aiOptions]="emailAIOptions"
82+
>
83+
<dxi-validation-rule type="email"></dxi-validation-rule>
84+
</dxi-form-item>
85+
</dxi-form-item>
86+
<dxi-form-item
87+
itemType="group"
88+
[colCountByScreen]="colCountByScreen"
89+
caption="Billing Address"
90+
>
91+
<dxi-form-item
92+
dataField="Street Address"
93+
editorType="dxTextBox"
94+
[editorOptions]="textEditorOptions"
95+
></dxi-form-item>
96+
<dxi-form-item
97+
dataField="City"
98+
editorType="dxTextBox"
99+
[editorOptions]="textEditorOptions"
100+
></dxi-form-item>
101+
<dxi-form-item
102+
dataField="State/Province/Region"
103+
editorType="dxTextBox"
104+
[editorOptions]="textEditorOptions"
105+
></dxi-form-item>
106+
<dxi-form-item
107+
dataField="ZIP"
108+
editorType="dxNumberBox"
109+
[editorOptions]="zipEditorOptions"
110+
[aiOptions]="zipAIOptions"
111+
></dxi-form-item>
112+
</dxi-form-item>
113+
<dxi-form-item
114+
itemType="group"
115+
cssClass="buttons-group"
116+
[colCountByScreen]="colCountByScreen"
117+
>
118+
<dxi-form-item
119+
itemType="button"
120+
name="smartPaste"
121+
[buttonOptions]="smartPasteButtonOptions"
122+
></dxi-form-item>
123+
<dxi-form-item
124+
itemType="button"
125+
name="reset"
126+
[buttonOptions]="resetButtonOptions"
127+
></dxi-form-item>
128+
</dxi-form-item>
129+
</dx-form>
130+
</div>
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { NgModule, Component, ViewChild, enableProdMode } from '@angular/core';
2+
import { BrowserModule } from '@angular/platform-browser';
3+
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
4+
import { AzureOpenAI, type OpenAI } from 'openai';
5+
import { DxTextAreaModule } from 'devextreme-angular';
6+
import {
7+
AIIntegration,
8+
RequestParams,
9+
Response,
10+
} from 'devextreme-angular/common/ai-integration';
11+
import { DxButtonModule, type DxButtonTypes } from 'devextreme-angular/ui/button';
12+
import { DxFormModule, DxFormComponent } from 'devextreme-angular/ui/form';
13+
import notify from 'devextreme/ui/notify';
14+
15+
import { Service } from './app.service';
16+
17+
if (!/localhost/.test(document.location.host)) {
18+
enableProdMode();
19+
}
20+
21+
let modulePrefix = '';
22+
// @ts-ignore
23+
if (window && window.config?.packageConfigPaths) {
24+
modulePrefix = '/app';
25+
}
26+
27+
const stylingMode = 'filled';
28+
29+
type AIMessage = (OpenAI.ChatCompletionUserMessageParam | OpenAI.ChatCompletionSystemMessageParam) & {
30+
content: string;
31+
};
32+
33+
const showNotification = (message: string, of: string, isError?: boolean, offset?: string) => {
34+
notify({
35+
message,
36+
position: {
37+
my: 'bottom center',
38+
at: 'bottom center',
39+
of,
40+
offset: offset ?? '0 -50',
41+
},
42+
width: 'fit-content',
43+
maxWidth: 'fit-content',
44+
minWidth: 'fit-content',
45+
}, isError ? 'error' : 'info', 1500);
46+
};
47+
48+
@Component({
49+
selector: 'demo-app',
50+
templateUrl: `.${modulePrefix}/app.component.html`,
51+
styleUrls: [`.${modulePrefix}/app.component.css`],
52+
providers: [Service],
53+
})
54+
export class AppComponent {
55+
@ViewChild(DxFormComponent, { static: false }) form: DxFormComponent;
56+
57+
colCountByScreen = {
58+
xs: 2,
59+
sm: 2,
60+
md: 2,
61+
lg: 2,
62+
};
63+
64+
amountDueEditorOptions = { placeholder: '$0.00', stylingMode };
65+
66+
amountDueAIOptions = { instruction: 'Format as the following: $0.00' };
67+
68+
statementDueEditorOptions = { placeholder: 'MM/DD/YYYY', stylingMode };
69+
70+
statementDueAIOptions = { instruction: 'Format as the following: MM/DD/YYYY' };
71+
72+
textEditorOptions = { stylingMode };
73+
74+
phoneEditorOptions = { placeholder: '(000) 000-0000', stylingMode };
75+
76+
phoneAIOptions = { instruction: 'Format as the following: (000) 000-0000' };
77+
78+
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' };
79+
80+
zipEditorOptions = { stylingMode, mode: 'text', value: null };
81+
82+
zipAIOptions = { instruction: 'If the text does not contain a ZIP, determine the ZIP code from the provided address.' };
83+
84+
resetButtonOptions: DxButtonTypes.Properties = {
85+
stylingMode: 'outlined',
86+
type: 'normal',
87+
};
88+
89+
smartPasteButtonOptions: DxButtonTypes.Properties = {
90+
stylingMode: 'contained',
91+
type: 'default',
92+
};
93+
94+
azureOpenAIConfig: any;
95+
96+
aiService: AzureOpenAI;
97+
98+
aiIntegration: AIIntegration;
99+
100+
valueContent: string;
101+
102+
text: string;
103+
104+
constructor(service: Service) {
105+
this.azureOpenAIConfig = service.getAzureOpenAIConfig();
106+
107+
this.aiService = new AzureOpenAI(this.azureOpenAIConfig);
108+
this.aiIntegration = new AIIntegration({
109+
sendRequest: this.sendRequest.bind(this),
110+
});
111+
112+
this.text = service.getDefaultText();
113+
}
114+
115+
ngAfterViewInit() {
116+
const form = this.form.instance;
117+
118+
form.registerKeyHandler('V', (event: KeyboardEvent) => {
119+
if (event.ctrlKey && event.shiftKey) {
120+
navigator.clipboard.readText()
121+
.then((clipboardText) => {
122+
if (clipboardText) {
123+
form.smartPaste(clipboardText);
124+
} else {
125+
showNotification('Copy the text to paste into the form', '#form');
126+
}
127+
})
128+
.catch(() => {
129+
showNotification('Could not access the clipboard', '#form');
130+
});
131+
}
132+
});
133+
}
134+
135+
sendRequest({ prompt }: RequestParams): Response {
136+
const controller = new AbortController();
137+
const signal = controller.signal;
138+
139+
const aiPrompt: AIMessage[] = [
140+
{ role: 'system', content: prompt.system },
141+
{ role: 'user', content: prompt.user },
142+
];
143+
const promise = this.getAIResponse(aiPrompt, signal);
144+
145+
promise.catch(() => {
146+
showNotification('Something went wrong. Please try again.', '#form', true);
147+
});
148+
149+
const result: Response = {
150+
promise,
151+
abort: () => {
152+
controller.abort();
153+
},
154+
};
155+
156+
return result;
157+
}
158+
159+
async getAIResponse(messages: AIMessage[], signal: AbortSignal) {
160+
const params = {
161+
messages,
162+
model: this.azureOpenAIConfig.deployment,
163+
max_tokens: 1000,
164+
temperature: 0.7,
165+
};
166+
167+
const response = await this.aiService.chat.completions.create(params, { signal });
168+
const result = response.choices[0].message?.content;
169+
170+
return result;
171+
}
172+
173+
onCopy() {
174+
navigator.clipboard.writeText(this.text);
175+
showNotification('Text copied to clipboard', '#textarea', false, '0 -20');
176+
}
177+
}
178+
179+
@NgModule({
180+
imports: [
181+
BrowserModule,
182+
DxButtonModule,
183+
DxFormModule,
184+
DxTextAreaModule,
185+
],
186+
declarations: [AppComponent],
187+
bootstrap: [AppComponent],
188+
})
189+
export class AppModule { }
190+
191+
platformBrowserDynamic().bootstrapModule(AppModule);

0 commit comments

Comments
 (0)