Skip to content

Commit cf4df17

Browse files
authored
AI Column: Write JS Framework demos, DataGrid, Angular (#31745)
1 parent 8a2f960 commit cf4df17

File tree

14 files changed

+2374
-0
lines changed

14 files changed

+2374
-0
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { AzureOpenAI, OpenAI } from 'openai';
2+
import { Injectable } from '@angular/core';
3+
import notify from 'devextreme/ui/notify';
4+
import {
5+
AIIntegration,
6+
type RequestParams,
7+
type Response,
8+
} from 'devextreme-angular/common/ai-integration';
9+
10+
type AIMessage = (OpenAI.ChatCompletionUserMessageParam | OpenAI.ChatCompletionSystemMessageParam) & {
11+
content: string;
12+
};
13+
14+
const AzureOpenAIConfig = {
15+
dangerouslyAllowBrowser: true,
16+
deployment: 'gpt-4o-mini',
17+
apiVersion: '2024-02-01',
18+
endpoint: 'https://public-api.devexpress.com/demo-openai',
19+
apiKey: 'DEMO',
20+
};
21+
22+
const RATE_LIMIT_RETRY_DELAY_MS = 30000;
23+
const MAX_PROMPT_SIZE = 5000;
24+
25+
const service = new AzureOpenAI(AzureOpenAIConfig);
26+
27+
async function getAIResponse(messages: AIMessage[], signal: AbortSignal) {
28+
const params = {
29+
messages,
30+
model: AzureOpenAIConfig.deployment,
31+
max_tokens: 1000,
32+
temperature: 0.7,
33+
};
34+
35+
const response = await service.chat.completions.create(params, { signal });
36+
const result = response.choices[0].message?.content;
37+
38+
if (!result) {
39+
throw new Error('AI response returned empty content');
40+
}
41+
42+
return result;
43+
}
44+
45+
async function getAIResponseRecursive(messages: AIMessage[], signal: AbortSignal): Promise<string> {
46+
return getAIResponse(messages, signal)
47+
.catch(async (error) => {
48+
if (!error.message.includes('Connection error')) {
49+
return Promise.reject(error);
50+
}
51+
52+
notify({
53+
message: 'You have reached the AI rate limits of this demo. Retrying in 30 seconds...',
54+
width: 'auto',
55+
type: 'error',
56+
displayTime: 5000,
57+
});
58+
59+
await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_RETRY_DELAY_MS));
60+
61+
return getAIResponseRecursive(messages, signal);
62+
});
63+
}
64+
65+
const aiIntegration = new AIIntegration({
66+
sendRequest({ prompt }: RequestParams): Response {
67+
const isValidRequest = JSON.stringify(prompt.user).length < MAX_PROMPT_SIZE;
68+
69+
if (!isValidRequest) {
70+
return {
71+
promise: Promise.reject(new Error('Request is too large')),
72+
abort: () => {},
73+
};
74+
}
75+
76+
const controller = new AbortController();
77+
const signal = controller.signal;
78+
79+
if (!prompt.user || !prompt.system) {
80+
throw new Error('Invalid prompt data');
81+
}
82+
83+
const aiPrompt: AIMessage[] = [
84+
{ role: 'system', content: prompt.system },
85+
{ role: 'user', content: prompt.user },
86+
];
87+
88+
const promise = getAIResponseRecursive(aiPrompt, signal);
89+
90+
const result: Response = {
91+
promise,
92+
abort: () => {
93+
controller.abort();
94+
},
95+
};
96+
97+
return result;
98+
},
99+
});
100+
101+
@Injectable()
102+
export class AiService {
103+
getAiIntegration() {
104+
return aiIntegration;
105+
}
106+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
::ng-deep #gridContainer .ai__cell {
2+
background-color: var(--dx-datagrid-row-alternation-bg);
3+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<dx-data-grid
2+
id="gridContainer"
3+
keyExpr="ID"
4+
[dataSource]="vehicles"
5+
[showBorders]="true"
6+
[aiIntegration]="aiIntegration"
7+
(onAIColumnRequestCreating)="onAIColumnRequestCreating($event)"
8+
>
9+
<dxo-data-grid-grouping [contextMenuEnabled]="false" />
10+
<dxo-data-grid-paging [pageSize]="10" />
11+
12+
<dxi-data-grid-column
13+
dataField="TrademarkName"
14+
caption="Trademark"
15+
[width]="200"
16+
cellTemplate="trademarkTemplate"
17+
/>
18+
<div *dxTemplate="let model of 'trademarkTemplate'">
19+
<trademark
20+
[id]="model.data.ID"
21+
[name]="model.data.Name"
22+
[trademarkName]="model.data.TrademarkName"
23+
(showInfo)="showInfo(model.data)"
24+
/>
25+
</div>
26+
<dxi-data-grid-column
27+
dataField="Price"
28+
format="currency"
29+
alignment="left"
30+
[width]="100"
31+
/>
32+
<dxi-data-grid-column
33+
dataField="CategoryName"
34+
caption="Category"
35+
[minWidth]="180"
36+
cellTemplate="categoryTemplate"
37+
/>
38+
<div *dxTemplate="let model of 'categoryTemplate'">
39+
<category [category]="model.data.CategoryName" />
40+
</div>
41+
<dxi-data-grid-column dataField="Modification" [width]="180" />
42+
<dxi-data-grid-column dataField="Horsepower" [width]="140" />
43+
<dxi-data-grid-column
44+
dataField="BodyStyleName"
45+
caption="Body Style"
46+
[width]="180"
47+
/>
48+
<dxi-data-grid-column
49+
name="AI Column"
50+
caption="AI Column"
51+
cssClass="ai__cell"
52+
type="ai"
53+
[fixed]="true"
54+
fixedPosition="right"
55+
[width]="200"
56+
>
57+
<dxo-data-grid-ai
58+
prompt="Identify the country where this vehicle model is originally manufactured or developed, based on its brand, model, and specifications."
59+
mode="auto"
60+
noDataText="No data"
61+
/>
62+
</dxi-data-grid-column>
63+
</dx-data-grid>
64+
65+
<dx-popup
66+
title="Image Info"
67+
[width]="360"
68+
[height]="260"
69+
[(visible)]="popupVisible"
70+
[dragEnabled]="false"
71+
[hideOnOutsideClick]="true"
72+
(onHiding)="hideInfo()"
73+
>
74+
<dxo-position at="center" my="center" collision="fit" />
75+
<div *dxTemplate="let _ of 'content'">
76+
<license-info *ngIf="currentVehicle" [vehicle]="currentVehicle" />
77+
</div>
78+
</dx-popup>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { NgModule, Component, enableProdMode } from '@angular/core';
2+
import { BrowserModule } from '@angular/platform-browser';
3+
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
4+
import { DxDataGridModule, DxPopupModule } from 'devextreme-angular';
5+
import type { AIIntegration } from 'devextreme-angular/common/ai-integration';
6+
import { Service, type Vehicle } from './app.service';
7+
import { AiService } from './ai.service';
8+
import { Trademark } from './trademark/trademark.component';
9+
import { Category } from './category/category.component';
10+
import { LicenseInfo } from './license-info/license-info.component';
11+
12+
if (!/localhost/.test(document.location.host)) {
13+
enableProdMode();
14+
}
15+
16+
let modulePrefix = '';
17+
// @ts-ignore
18+
if (window && window.config?.packageConfigPaths) {
19+
modulePrefix = '/app';
20+
}
21+
22+
@Component({
23+
selector: 'demo-app',
24+
templateUrl: `.${modulePrefix}/app.component.html`,
25+
styleUrls: [`.${modulePrefix}/app.component.css`],
26+
providers: [Service, AiService],
27+
})
28+
export class AppComponent {
29+
vehicles: Vehicle[];
30+
31+
popupVisible = false;
32+
33+
currentVehicle: Vehicle | null = null;
34+
35+
aiIntegration: AIIntegration;
36+
37+
constructor(service: Service, aiService: AiService) {
38+
this.vehicles = service.getVehicles();
39+
this.aiIntegration = aiService.getAiIntegration();
40+
}
41+
42+
showInfo(vehicle: Vehicle) {
43+
this.currentVehicle = vehicle;
44+
this.popupVisible = true;
45+
}
46+
47+
hideInfo() {
48+
this.popupVisible = false;
49+
}
50+
51+
onAIColumnRequestCreating(e: { data: Partial<Vehicle>[] }) {
52+
e.data = e.data.map((item) => ({
53+
ID: item.ID,
54+
TrademarkName: item.TrademarkName,
55+
Name: item.Name,
56+
Modification: item.Modification,
57+
}));
58+
}
59+
}
60+
61+
@NgModule({
62+
imports: [
63+
BrowserModule,
64+
DxDataGridModule,
65+
DxPopupModule,
66+
],
67+
declarations: [
68+
AppComponent,
69+
Trademark,
70+
Category,
71+
LicenseInfo,
72+
],
73+
bootstrap: [AppComponent],
74+
})
75+
export class AppModule { }
76+
77+
platformBrowserDynamic().bootstrapModule(AppModule);

0 commit comments

Comments
 (0)