Skip to content

Commit 7ac14ed

Browse files
author
Raushen
committed
Merge branch '26_1' of https://github.com/DevExpress/DevExtreme into Skip-Remote-DataSource-Tests
2 parents 27b6d96 + a705e96 commit 7ac14ed

54 files changed

Lines changed: 69633 additions & 397 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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, responseSchema?: Record<string, unknown>) {
28+
const params: OpenAI.ChatCompletionCreateParamsNonStreaming = {
29+
messages,
30+
model: AzureOpenAIConfig.deployment,
31+
max_tokens: 1000,
32+
temperature: 0.7,
33+
response_format: {
34+
type: 'json_schema',
35+
json_schema: {
36+
name: 'grid_assistant_response',
37+
strict: false,
38+
schema: responseSchema,
39+
},
40+
},
41+
};
42+
43+
const response = await service.chat.completions.create(params, { signal });
44+
const result = response.choices[0].message?.content;
45+
46+
if (!result) {
47+
throw new Error('AI response returned empty content');
48+
}
49+
50+
return result;
51+
}
52+
53+
function getAIResponseRecursive(
54+
messages: AIMessage[],
55+
signal: AbortSignal,
56+
responseSchema?: Record<string, unknown>,
57+
): Promise<string> {
58+
return getAIResponse(messages, signal, responseSchema)
59+
.catch(async (error) => {
60+
if (!error.message.includes('Connection error')) {
61+
return Promise.reject(error);
62+
}
63+
64+
notify({
65+
message: 'Our demo AI service reached a temporary request limit. Retrying in 30 seconds.',
66+
width: 'auto',
67+
type: 'error',
68+
displayTime: 5000,
69+
});
70+
71+
await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_RETRY_DELAY_MS));
72+
73+
return getAIResponseRecursive(messages, signal, responseSchema);
74+
});
75+
}
76+
77+
const aiIntegration = new AIIntegration({
78+
sendRequest({ prompt, data }: RequestParams): Response {
79+
const isValidRequest = JSON.stringify(prompt.user).length < MAX_PROMPT_SIZE;
80+
81+
if (!isValidRequest) {
82+
return {
83+
promise: Promise.reject(new Error('Request is too long. Specify a shorter prompt.')),
84+
abort: () => {},
85+
};
86+
}
87+
88+
const controller = new AbortController();
89+
const signal = controller.signal;
90+
91+
if (!prompt.user || !prompt.system) {
92+
throw new Error('Invalid prompt data');
93+
}
94+
95+
const aiPrompt: AIMessage[] = [
96+
{ role: 'system', content: prompt.system },
97+
{ role: 'user', content: prompt.user },
98+
];
99+
100+
const promise = getAIResponseRecursive(aiPrompt, signal, data?.responseSchema);
101+
102+
const result: Response = {
103+
promise,
104+
abort: () => {
105+
controller.abort();
106+
},
107+
};
108+
109+
return result;
110+
},
111+
});
112+
113+
@Injectable()
114+
export class AiService {
115+
getAiIntegration() {
116+
return aiIntegration;
117+
}
118+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
::ng-deep #gridContainer {
2+
max-height: 800px;
3+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<dx-data-grid
2+
id="gridContainer"
3+
keyExpr="Id"
4+
[dataSource]="sales"
5+
[showBorders]="true"
6+
[filterSyncEnabled]="true"
7+
>
8+
<dxo-data-grid-search-panel
9+
[visible]="true"
10+
[width]="240"
11+
placeholder="Search..."
12+
/>
13+
<dxo-data-grid-group-panel [visible]="true" />
14+
<dxo-data-grid-header-filter [visible]="true" />
15+
<dxo-data-grid-filter-row [visible]="true" />
16+
<dxo-data-grid-paging [pageSize]="10" />
17+
<dxo-data-grid-pager
18+
[visible]="true"
19+
[allowedPageSizes]="[10, 25, 50, 100]"
20+
[showPageSizeSelector]="true"
21+
/>
22+
23+
<dxo-data-grid-ai-assistant
24+
[enabled]="true"
25+
[aiIntegration]="aiIntegration"
26+
[chat]="chatConfig"
27+
/>
28+
29+
<dxi-data-grid-column dataField="Product" />
30+
<dxi-data-grid-column
31+
dataField="Amount"
32+
caption="Sale Amount"
33+
dataType="number"
34+
format="currency"
35+
/>
36+
<dxi-data-grid-column dataField="Region" dataType="string" />
37+
<dxi-data-grid-column dataField="Sector" dataType="string" />
38+
<dxi-data-grid-column dataField="SaleDate" dataType="date" />
39+
<dxi-data-grid-column dataField="Customer" dataType="string" />
40+
</dx-data-grid>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { bootstrapApplication } from '@angular/platform-browser';
2+
import { Component, enableProdMode, provideZoneChangeDetection } from '@angular/core';
3+
import { DxDataGridModule } from 'devextreme-angular';
4+
import type { AIIntegration } from 'devextreme-angular/common/ai-integration';
5+
import type { DxChatTypes } from 'devextreme-angular/ui/chat';
6+
import type { DxButtonGroupTypes } from 'devextreme-angular/ui/button-group';
7+
import { Service, type Sale } from './app.service';
8+
import { AiService } from './ai/ai.service';
9+
10+
interface SuggestionItem extends DxButtonGroupTypes.Item {
11+
prompt: string;
12+
}
13+
14+
if (!/localhost/.test(document.location.host)) {
15+
enableProdMode();
16+
}
17+
18+
let modulePrefix = '';
19+
// @ts-ignore
20+
if (window && window.config?.packageConfigPaths) {
21+
modulePrefix = '/app';
22+
}
23+
24+
@Component({
25+
selector: 'demo-app',
26+
templateUrl: `.${modulePrefix}/app.component.html`,
27+
styleUrls: [`.${modulePrefix}/app.component.css`],
28+
providers: [Service, AiService],
29+
imports: [DxDataGridModule],
30+
})
31+
export class AppComponent {
32+
sales: Sale[];
33+
34+
aiIntegration: AIIntegration;
35+
36+
chatConfig: DxChatTypes.Properties;
37+
38+
private chatInstance: DxChatTypes.InitializedEvent['component'] | null = null;
39+
40+
constructor(service: Service, aiService: AiService) {
41+
this.sales = service.getSales();
42+
this.aiIntegration = aiService.getAiIntegration();
43+
44+
this.chatConfig = {
45+
onInitialized: (e: DxChatTypes.InitializedEvent) => {
46+
this.chatInstance = e.component;
47+
},
48+
user: { id: 'user' },
49+
suggestions: {
50+
items: [
51+
{
52+
text: '💡 Help',
53+
prompt: `💡 The DataGrid AI Assistant allows you to control the component using natural language. You can execute commands such as the following:
54+
• Sort records
55+
• Apply a filter
56+
• Search for a specific value
57+
• Group records by a field
58+
• Focus and select rows
59+
• Modify paging settings
60+
• Pin, resize, and reorder columns
61+
• Configure data summaries
62+
• Pick a suggestion or enter a custom request to get started.`,
63+
},
64+
{
65+
text: '🔍 Filter Sector by Health',
66+
prompt: 'Filter Sector by Health',
67+
},
68+
{
69+
text: '↕️ Sort by Region',
70+
prompt: 'Sort by Region',
71+
},
72+
{
73+
text: '🧩 Group by Product',
74+
prompt: 'Group by Product',
75+
width: 170,
76+
},
77+
] as SuggestionItem[],
78+
onItemClick: (e: DxButtonGroupTypes.ItemClickEvent) => {
79+
this.onSuggestionItemClick(e);
80+
},
81+
},
82+
};
83+
}
84+
85+
onSuggestionItemClick(e: DxButtonGroupTypes.ItemClickEvent) {
86+
const { prompt, text } = e.itemData as SuggestionItem;
87+
const userId = text === '💡 Help' ? 'help' : 'user';
88+
89+
const message = {
90+
id: Date.now(),
91+
timestamp: new Date(),
92+
author: { id: userId },
93+
text: prompt,
94+
};
95+
96+
this.chatInstance.getDataSource().store().push([{
97+
type: 'insert',
98+
data: message,
99+
}]);
100+
}
101+
}
102+
103+
bootstrapApplication(AppComponent, {
104+
providers: [
105+
provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }),
106+
],
107+
});

0 commit comments

Comments
 (0)