Skip to content

Commit ecbce8e

Browse files
authored
DataGrid - AI Column: Implement the AI data rendering (#31590)
Co-authored-by: Alyar <>
1 parent e8ddaf0 commit ecbce8e

File tree

20 files changed

+1122
-151
lines changed

20 files changed

+1122
-151
lines changed

apps/react-storybook/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,20 @@
1414
"dependencies": {
1515
"devextreme": "workspace:*",
1616
"devextreme-react": "workspace:*",
17+
"html-react-parser": "^5.2.2",
1718
"inferno": "catalog:",
18-
"html-react-parser": "^5.2.2"
19+
"openai": "4.73.1"
1920
},
2021
"devDependencies": {
21-
"@types/react": "18.0.0",
22-
"@types/react-dom": "18.0.0",
2322
"@storybook/addon-essentials": "7.6.19",
2423
"@storybook/addon-interactions": "7.6.19",
2524
"@storybook/addon-links": "7.6.19",
2625
"@storybook/blocks": "7.6.19",
2726
"@storybook/react": "7.6.19",
2827
"@storybook/react-webpack5": "7.6.19",
2928
"@storybook/test": "7.6.19",
29+
"@types/react": "18.0.0",
30+
"@types/react-dom": "18.0.0",
3031
"http-server": "14.1.1",
3132
"prop-types": "15.8.1",
3233
"react": "18.0.0",

apps/react-storybook/stories/examples/datagrid/DataGrid.stories.tsx

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import DataGrid, {
1414
import DiscountCell from "./DiscountCell";
1515
import ODataStore from "devextreme/data/odata/store";
1616
import { AIIntegration } from 'devextreme-react/common/ai-integration';
17+
import { AzureOpenAI } from 'openai';
1718

1819
const columnOptions = {
1920
regularColumns: [
@@ -273,65 +274,82 @@ args: {
273274
}
274275
}
275276

277+
const deployment = "gpt-4o-mini";
278+
const apiVersion = "2024-02-01";
279+
const endpoint = "https://public-api.devexpress.com/demo-openai";
280+
const apiKey = "DEMO";
281+
282+
const aiService = new AzureOpenAI({
283+
dangerouslyAllowBrowser: true,
284+
deployment,
285+
endpoint,
286+
apiVersion,
287+
apiKey
288+
});
289+
290+
async function getAIResponse(messages, signal): Promise<string> {
291+
const params = {
292+
messages,
293+
model: deployment,
294+
max_tokens: 1000,
295+
temperature: 0.7,
296+
};
297+
298+
const response = await aiService.chat.completions.create(params, { signal });
299+
const result = response.choices[0].message?.content;
300+
301+
return result ?? '';
302+
}
303+
304+
const aiIntegration = new AIIntegration({
305+
sendRequest({ prompt }) {
306+
const controller = new AbortController();
307+
const signal = controller.signal;
308+
309+
const aiPrompt = [
310+
{ role: 'system', content: prompt.system, },
311+
{ role: 'user', content: prompt.user, },
312+
];
313+
314+
const promise = getAIResponse(aiPrompt, signal);
315+
316+
const result = {
317+
promise,
318+
abort: () => {
319+
controller.abort();
320+
},
321+
};
322+
323+
return result;
324+
},
325+
});
326+
327+
276328
export const AiColumn: Story = {
277329
args: {
278330
dataSource: countries,
331+
aiIntegration,
332+
keyExpr: 'ID',
279333
columns: [
280334
{
281335
caption: 'AI Column 1',
282336
type: 'ai',
283-
name: 'test',
337+
name: 'test1',
284338
ai: {
285-
aiIntegration: new AIIntegration({
286-
sendRequest() {
287-
return {
288-
promise: new Promise((resolve) => {
289-
setTimeout(() => {
290-
resolve('{"text":"Test response from AI Column 1"}');
291-
}, 5000);
292-
}),
293-
abort: () => {
294-
},
295-
};
296-
},
297-
}),
339+
prompt: 'Country currency',
298340
},
299341
},
300-
'Country', 'Area', 'Population_Urban', 'Population_Rural',
301342
{
302343
caption: 'AI Column 2',
303344
type: 'ai',
304-
name: 'test',
345+
name: 'test2',
305346
ai: {
306-
aiIntegration: new AIIntegration({
307-
sendRequest() {
308-
return {
309-
promise: new Promise((resolve) => {
310-
setTimeout(() => {
311-
resolve('{"text":"Test response from AI Column 2"}');
312-
}, 5000);
313-
}),
314-
abort: () => {
315-
},
316-
};
317-
},
318-
}),
347+
prompt: 'Emoji flag of the country',
319348
},
320-
}
349+
},
350+
'Country', 'Area', 'Population_Urban', 'Population_Rural',
321351
],
322352
allowColumnResizing: true,
323353
allowColumnReordering: true,
324-
onContextMenuPreparing: (e) => {
325-
if (e.target === 'header' && e.column?.type === 'ai') {
326-
e.items = e.items || [];
327-
e.items.push({
328-
text: 'Show AI Prompt Editor',
329-
onItemClick: () => {
330-
// @ts-expect-error
331-
e.component.getView('aiColumnView').showPromptEditor(e.event.target, e.column);
332-
},
333-
});
334-
}
335-
}
336-
}
354+
},
337355
};

packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ const DATAGRID_DEPRECATED_TEMPLATE_WARNING = 'Specifying grid templates with the
2626
gridCore.registerModulesOrder([
2727
'stateStoring',
2828
'columns',
29-
'aiColumn',
3029
'selection',
3130
'editorFactory',
3231
'columnChooser',
@@ -40,6 +39,7 @@ gridCore.registerModulesOrder([
4039
'adaptivity',
4140
'data',
4241
'virtualScrolling',
42+
'aiColumn',
4343
'columnHeaders',
4444
'filterRow',
4545
'headerPanel',
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export class DataCellModel {
2+
constructor(protected readonly root: HTMLElement | null) {}
3+
4+
public getElement(): HTMLElement | null {
5+
return this.root;
6+
}
7+
8+
public getText(): string {
9+
return this.root?.textContent ?? '';
10+
}
11+
}

packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import type { GridBase } from '@js/common/grids';
44
import type { dxElementWrapper } from '@js/core/renderer';
55
import $ from '@js/core/renderer';
6+
import { LoadPanelModel } from '@ts/ui/__tests__/__mock__/model/load_panel';
67
import { ToastModel } from '@ts/ui/__tests__/__mock__/model/toast';
78

89
import { AIPromptEditorModel } from './ai_prompt_editor';
910
import { AIHeaderCellModel } from './cell/ai_header_cell';
11+
import { DataCellModel } from './cell/data_cell';
1012
import { HeaderCellModel } from './cell/header_cell';
1113

1214
const SELECTORS = {
@@ -16,13 +18,18 @@ const SELECTORS = {
1618
aiDialog: 'dx-aidialog',
1719
aiPromptEditor: 'dx-ai-prompt-editor',
1820
toast: 'dx-toast',
21+
loadPanel: 'dx-loadpanel',
1922
};
2023

2124
export abstract class GridCoreModel<TInstance extends GridBase = GridBase> {
2225
protected abstract NAME: string;
2326

2427
constructor(protected readonly root: HTMLElement) {}
2528

29+
private getPromptEditorContainer(): HTMLElement {
30+
return this.root.querySelector(`.${SELECTORS.aiPromptEditor}`) as HTMLElement;
31+
}
32+
2633
public getHeaderCells(): NodeListOf<HTMLElement> {
2734
return this.root.querySelectorAll(`.${SELECTORS.headerRowClass} > td`);
2835
}
@@ -38,24 +45,20 @@ export abstract class GridCoreModel<TInstance extends GridBase = GridBase> {
3845
);
3946
}
4047

41-
public getCellElement(rowIndex: number, columnIndex: number): HTMLElement {
42-
return this.root.querySelectorAll(`.${SELECTORS.dataRowClass}`)[rowIndex]?.querySelectorAll('td')[columnIndex] as HTMLElement;
48+
public getDataRows(): NodeListOf<HTMLElement> {
49+
return this.root.querySelectorAll(`.${SELECTORS.dataRowClass}`);
4350
}
4451

45-
public getGroupRows(): NodeListOf<HTMLElement> {
46-
return this.root.querySelectorAll(`.${SELECTORS.groupRowClass}`);
52+
public getDataCells(rowIndex: number): NodeListOf<HTMLElement> {
53+
return this.root.querySelectorAll(`.${SELECTORS.dataRowClass}:nth-child(${rowIndex + 1}) > td`);
4754
}
4855

49-
public apiColumnOption(id: string, name?: string, value?: any): any {
50-
switch (arguments.length) {
51-
case 1:
52-
return this.getInstance().columnOption(id);
53-
case 2:
54-
return this.getInstance().columnOption(id, name);
55-
default:
56-
this.getInstance().columnOption(id, name as string, value);
57-
return undefined;
58-
}
56+
public getDataCell(rowIndex: number, columnIndex: number): DataCellModel {
57+
return new DataCellModel(this.getDataCells(rowIndex)[columnIndex]);
58+
}
59+
60+
public getGroupRows(): NodeListOf<HTMLElement> {
61+
return this.root.querySelectorAll(`.${SELECTORS.groupRowClass}`);
5962
}
6063

6164
public getHeaderByText(text: string): dxElementWrapper {
@@ -66,10 +69,6 @@ export abstract class GridCoreModel<TInstance extends GridBase = GridBase> {
6669
return document.body.querySelector(`.${SELECTORS.aiDialog}`) as HTMLElement;
6770
}
6871

69-
private getPromptEditorContainer(): HTMLElement {
70-
return this.root.querySelector(`.${SELECTORS.aiPromptEditor}`) as HTMLElement;
71-
}
72-
7372
public getAIPromptEditor(): AIPromptEditorModel {
7473
return new AIPromptEditorModel(this.getPromptEditorContainer());
7574
}
@@ -88,5 +87,29 @@ export abstract class GridCoreModel<TInstance extends GridBase = GridBase> {
8887
return `dx-${componentName.slice(2).toLowerCase()}${classNames ? `-${classNames}` : ''}`;
8988
}
9089

90+
public apiColumnOption(id: string, name?: string, value?: any): any {
91+
switch (arguments.length) {
92+
case 1:
93+
return this.getInstance().columnOption(id);
94+
case 2:
95+
return this.getInstance().columnOption(id, name);
96+
default:
97+
this.getInstance().columnOption(id, name as string, value);
98+
return undefined;
99+
}
100+
}
101+
102+
public async apiRefresh(): Promise<void> {
103+
await this.getInstance().refresh();
104+
}
105+
106+
public apiAbortAIColumnRequest(columnName: string): void {
107+
this.getInstance().abortAIColumnRequest(columnName);
108+
}
109+
110+
public getLoadPanel(): LoadPanelModel {
111+
return new LoadPanelModel(document.body.querySelector(`.${SELECTORS.loadPanel}`));
112+
}
113+
91114
public abstract getInstance(): TInstance;
92115
}

0 commit comments

Comments
 (0)