Skip to content

Commit f3568d3

Browse files
Merge branch '26_1' into issue-4222_26_1
2 parents b01d7af + 582c167 commit f3568d3

11 files changed

Lines changed: 115 additions & 90 deletions

File tree

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
1-
With our v26.1 release, we will extend the AI-powered capabilities of both the DevExtreme [DataGrid](https://js.devexpress.com/Documentation/Guide/UI_Components/DataGrid/Overview/) and [TreeList](https://js.devexpress.com/Documentation/Guide/UI_Components/TreeList/Overview/). We plan to introduce a DevExtreme Chat-based AI assistant that will allow you to use natural language to interact with DataGrid/TreeList. The DataGrid/TreeList AI assistant will be able to complete the following actions:
2-
* Sort, filter, search, and group data.
3-
* Resize, reorder, fix/unfix, and show/hide columns.
4-
* Change pagination settings.
5-
<!--split-->
1+
The AI Assistant for the DevExtreme [DataGrid](/Documentation/Guide/UI_Components/DataGrid/Overview/) allows you to interact with the component using natural language. The Chat also supports [speech-to-text input](/Documentation/ApiReference/UI_Components/dxChat/Configuration/#speechToTextEnabled), ideal for hands-free interactions or entering longer prompts.
2+
3+
You can use/configure the following DataGrid features in the AI Assistant Chat prompts:
64

7-
This feature is particularly well suited for the following usage scenarios:
5+
- [Filtering and Searching](/Documentation/Guide/UI_Components/DataGrid/Filtering_and_Searching/)
6+
- [Sorting](/Documentation/Guide/UI_Components/DataGrid/Sorting/)
7+
- [Grouping](/Documentation/Guide/UI_Components/DataGrid/Grouping/)
8+
- [Paging](/Documentation/Guide/UI_Components/DataGrid/Paging/)
9+
- [Focused Row](/Documentation/Guide/UI_Components/DataGrid/Focused_Row/)
10+
- [Selection](/Documentation/Guide/UI_Components/DataGrid/Selection/)
11+
- [Summaries](/Documentation/Guide/UI_Components/DataGrid/Summaries/Predefined_Aggregate_Functions/)
12+
- [Column Fixing](/Documentation/Guide/UI_Components/DataGrid/Columns/Column_Fixing/), [Resizing](/Documentation/Guide/UI_Components/DataGrid/Columns/Column_Sizing/), and [Reordering](/Documentation/Guide/UI_Components/DataGrid/Columns/Column_Reordering/)
13+
14+
The AI Assistant feature is also available for the DevExtreme [TreeList](/Documentation/ApiReference/UI_Components/dxTreeList/Configuration/#aiAssistant) component.
15+
<!--split-->
816

9-
* **Large Datasets**: Use the power of AI to browse large data sets. Apply complex filter values with natural language.
17+
In this demo, the AI Assistant is enabled for a DataGrid that displays mock sales data with over 1500 records.
1018

19+
[note]
1120

12-
* **Complex Grid Configurations**: Allow users to specify complex display preferences quickly.
21+
AI services used for this demo have been rate and data limited. As such, you may experience performance-related delays when exploring the capabilities of the DataGrid AI Assistant.
1322

23+
When connected to your own AI model/service without rate and data limits, the AI Assistant will perform seamlessly, without artificial delays. Note that DevExtreme does not offer an AI REST API and does not ship any built-in LLMs/SLMs.
1424

15-
* **Accessibility-first Applications**: Allow users to interact with your application in a manner that is most appropriate for each specific situation: while at their desk, or on the go (when their hands are busy and they require voice commands to interact with your app).
25+
[/note]
1626

27+
To enable the AI Assistant, configure the [aiIntegration](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/#aiIntegration) or **aiAssistant**.[aiIntegration](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/aiAssistant/#aiIntegration) object and set the **aiAssistant**.[enabled](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/aiAssistant/#enabled) property to `true`. Once activated, the DataGrid adds a predefined item (*"aiAssistantButton"*) to the toolbar. This button opens the AI Assistant Chat in a draggable pop-up window.
1728

18-
* **Replicable Configurations**: Apply the same prompt to recreate complex grid configurations across sessions. Reduce time spent on tedious layout adjustments.
29+
This demo also configures [suggestions](/Documentation/ApiReference/UI_Components/dxChat/Configuration/#suggestions) for the AI Assistant Chat. These buttons allow you to interact with the assistant in one click using predefined prompts. For additional information about suggestions, refer to the following demo: [DevExtreme Chat - Prompt Suggestions](/Demos/WidgetsGallery/Demo/Chat/PromptSuggestions/).

packages/devextreme/js/__internal/core/ai_integration/templates/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ export const templates: PromptTemplates = {
4242
user: 'User prompt text: {{text}}. Dataset: {{data}}.',
4343
},
4444
executeGridAssistant: {
45-
// TODO: Implement system prompt when grid operations are ready
46-
system: 'You are a helpful AI assistant for a data grid component. The user sends a natural language request describing an operation on the grid (e.g., sorting, filtering, grouping). You receive the user\'s message, a context object describing the current grid state, and a JSON schema describing the available commands and their arguments. Your task is to interpret the user\'s request and return a JSON object with one field: "actions" — an array of command objects, each with "name" (the command name) and "args" (an object of arguments matching the schema). Output must be a valid JSON string, directly parsable by JSON.parse. Do not include any markdown, formatting, or extra text — only the raw JSON object.',
45+
system: 'You are a helpful AI assistant for a data grid component. The user sends a natural language request describing an operation on the grid (e.g., sorting, filtering, grouping). You receive the user\'s message, a context object describing the current grid state, and a JSON schema describing the available commands and their arguments. Your task is to interpret the user\'s request and return a JSON object with one field: "actions" — an array of command objects, each with "name" (the command name) and "args" (an object of arguments matching the schema). Output must be a valid JSON string, directly parsable by JSON.parse. Do not include any markdown, formatting, or extra text — only the raw JSON object.\n\nCRITICAL RULE FOR OPTIONAL ARGUMENTS: If an optional argument is not used, the field MUST be ENTIRELY ABSENT from the JSON object — the key must not appear at all. NEVER emit an optional field with value null, empty string "", empty array [], or any placeholder. This rule overrides any instinct to "complete" the object — omitted IS the value.',
4746
user: 'User request: {{text}}. Grid context: {{context}}.',
4847
},
4948
};

packages/devextreme/js/__internal/grids/data_grid/ai_assistant/commands/__tests__/summary.test.ts

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ describe('summaryCommand', () => {
5757
])('accepts summaryType "%s"', (summaryType) => {
5858
expect(summaryCommand.schema.safeParse({
5959
totalItems: [{ column: 'amount', summaryType }],
60+
groupItems: [],
6061
}).success).toBe(true);
6162
});
6263

@@ -67,41 +68,59 @@ describe('summaryCommand', () => {
6768
}).success).toBe(true);
6869
});
6970

70-
it('accepts empty object (executability layer rejects it)', () => {
71-
expect(summaryCommand.schema.safeParse({}).success).toBe(true);
71+
it('rejects empty object (totalItems and groupItems are required)', () => {
72+
expect(summaryCommand.schema.safeParse({}).success).toBe(false);
73+
});
74+
75+
it('rejects when totalItems is omitted', () => {
76+
expect(summaryCommand.schema.safeParse({
77+
groupItems: [{ column: 'amount', summaryType: 'avg' }],
78+
}).success).toBe(false);
79+
});
80+
81+
it('rejects when groupItems is omitted', () => {
82+
expect(summaryCommand.schema.safeParse({
83+
totalItems: [{ column: 'amount', summaryType: 'sum' }],
84+
}).success).toBe(false);
7285
});
7386

7487
it('rejects an unknown summaryType (including "custom")', () => {
7588
expect(summaryCommand.schema.safeParse({
7689
totalItems: [{ column: 'amount', summaryType: 'custom' }],
90+
groupItems: [],
7791
}).success).toBe(false);
7892
expect(summaryCommand.schema.safeParse({
7993
totalItems: [{ column: 'amount', summaryType: 'median' }],
94+
groupItems: [],
8095
}).success).toBe(false);
8196
});
8297

8398
it('rejects when an item is missing column', () => {
8499
expect(summaryCommand.schema.safeParse({
85100
totalItems: [{ summaryType: 'sum' }],
101+
groupItems: [],
86102
}).success).toBe(false);
87103
});
88104

89105
it('rejects when an item is missing summaryType', () => {
90106
expect(summaryCommand.schema.safeParse({
91107
totalItems: [{ column: 'amount' }],
108+
groupItems: [],
92109
}).success).toBe(false);
93110
});
94111

95112
it('rejects unknown properties on the root', () => {
96113
expect(summaryCommand.schema.safeParse({
97114
totalItems: [{ column: 'amount', summaryType: 'sum' }],
115+
groupItems: [],
98116
extra: 1,
99117
}).success).toBe(false);
100118
});
101119

102120
it('rejects unknown properties on the item', () => {
103121
expect(summaryCommand.schema.safeParse({
104122
totalItems: [{ column: 'amount', summaryType: 'sum', extra: 1 }],
123+
groupItems: [],
105124
}).success).toBe(false);
106125
});
107126

@@ -113,11 +132,13 @@ describe('summaryCommand', () => {
113132
showInColumn: 'name',
114133
displayFormat: 'Sum: {0}',
115134
}],
135+
groupItems: [],
116136
}).success).toBe(true);
117137
});
118138

119139
it('accepts showInColumn, displayFormat, showInGroupFooter, alignByColumn on groupItems', () => {
120140
expect(summaryCommand.schema.safeParse({
141+
totalItems: [],
121142
groupItems: [{
122143
column: 'amount',
123144
summaryType: 'avg',
@@ -136,6 +157,7 @@ describe('summaryCommand', () => {
136157
summaryType: 'sum',
137158
showInGroupFooter: true,
138159
}],
160+
groupItems: [],
139161
}).success).toBe(false);
140162
});
141163

@@ -146,47 +168,41 @@ describe('summaryCommand', () => {
146168
summaryType: 'sum',
147169
alignByColumn: true,
148170
}],
171+
groupItems: [],
149172
}).success).toBe(false);
150173
});
151174

152175
it('rejects when showInColumn is not a string', () => {
153176
expect(summaryCommand.schema.safeParse({
154177
totalItems: [{ column: 'amount', summaryType: 'sum', showInColumn: 1 }],
178+
groupItems: [],
155179
}).success).toBe(false);
156180
});
157181

158182
it('rejects when displayFormat is not a string', () => {
159183
expect(summaryCommand.schema.safeParse({
160184
totalItems: [{ column: 'amount', summaryType: 'sum', displayFormat: 5 }],
185+
groupItems: [],
161186
}).success).toBe(false);
162187
});
163188

164189
it('rejects when showInGroupFooter is not a boolean', () => {
165190
expect(summaryCommand.schema.safeParse({
191+
totalItems: [],
166192
groupItems: [{ column: 'amount', summaryType: 'sum', showInGroupFooter: 'yes' }],
167193
}).success).toBe(false);
168194
});
169195

170196
it('rejects when alignByColumn is not a boolean', () => {
171197
expect(summaryCommand.schema.safeParse({
198+
totalItems: [],
172199
groupItems: [{ column: 'amount', summaryType: 'sum', alignByColumn: 1 }],
173200
}).success).toBe(false);
174201
});
175202
});
176203

177204
describe('execute', () => {
178-
it('returns failure when both totalItems and groupItems are empty/omitted', async () => {
179-
const instance = await createGrid();
180-
const optionSpy = jest.spyOn(instance, 'option');
181-
const callbacks = createCallbacks();
182-
183-
const result = await summaryCommand.execute(instance, callbacks)({});
184-
185-
expect(result.status).toBe('failure');
186-
expect(optionSpy).not.toHaveBeenCalledWith('summary', expect.anything());
187-
});
188-
189-
it('returns failure when both arrays are explicitly empty', async () => {
205+
it('returns failure when both arrays are empty', async () => {
190206
const instance = await createGrid();
191207
const optionSpy = jest.spyOn(instance, 'option');
192208
const callbacks = createCallbacks();
@@ -210,6 +226,7 @@ describe('summaryCommand', () => {
210226
{ column: 'amount', summaryType: 'sum' },
211227
{ column: 'unknown', summaryType: 'avg' },
212228
],
229+
groupItems: [],
213230
});
214231

215232
expect(result.status).toBe('failure');
@@ -222,6 +239,7 @@ describe('summaryCommand', () => {
222239
const callbacks = createCallbacks();
223240

224241
const result = await summaryCommand.execute(instance, callbacks)({
242+
totalItems: [],
225243
groupItems: [{ column: 'unknown', summaryType: 'sum' }],
226244
});
227245

@@ -240,6 +258,7 @@ describe('summaryCommand', () => {
240258
summaryType: 'sum',
241259
showInColumn: 'unknown',
242260
}],
261+
groupItems: [],
243262
});
244263

245264
expect(result.status).toBe('failure');
@@ -263,21 +282,6 @@ describe('summaryCommand', () => {
263282
expect(result.status).toBe('success');
264283
});
265284

266-
it('passes empty arrays when one of the inputs is omitted', async () => {
267-
const instance = await createGrid();
268-
const optionSpy = jest.spyOn(instance, 'option');
269-
const callbacks = createCallbacks();
270-
271-
await summaryCommand.execute(instance, callbacks)({
272-
totalItems: [{ column: 'amount', summaryType: 'sum' }],
273-
});
274-
275-
expect(optionSpy).toHaveBeenCalledWith('summary', {
276-
totalItems: [{ column: 'amount', summaryType: 'sum' }],
277-
groupItems: [],
278-
});
279-
});
280-
281285
it('returns failure when option throws', async () => {
282286
const instance = await createGrid();
283287
const realOption = instance.option.bind(instance);
@@ -291,6 +295,7 @@ describe('summaryCommand', () => {
291295

292296
const result = await summaryCommand.execute(instance, callbacks)({
293297
totalItems: [{ column: 'amount', summaryType: 'sum' }],
298+
groupItems: [],
294299
});
295300

296301
expect(result.status).toBe('failure');
@@ -304,6 +309,7 @@ describe('summaryCommand', () => {
304309

305310
await summaryCommand.execute(instance, callbacks)({
306311
totalItems: [{ column: 'amount', summaryType: 'sum' }],
312+
groupItems: [],
307313
});
308314

309315
expect(callbacks.success).toHaveBeenCalledWith(
@@ -316,6 +322,7 @@ describe('summaryCommand', () => {
316322
const callbacks = createCallbacks();
317323

318324
await summaryCommand.execute(instance, callbacks)({
325+
totalItems: [],
319326
groupItems: [{ column: 'amount', summaryType: 'avg' }],
320327
});
321328

@@ -336,6 +343,7 @@ describe('summaryCommand', () => {
336343

337344
await summaryCommand.execute(instance, callbacks)({
338345
totalItems: [{ column: 'amount', summaryType: summaryType as 'sum' | 'min' | 'max' | 'avg' | 'count' }],
346+
groupItems: [],
339347
});
340348

341349
expect(callbacks.success).toHaveBeenCalledWith(
@@ -349,6 +357,7 @@ describe('summaryCommand', () => {
349357

350358
await summaryCommand.execute(instance, callbacks)({
351359
totalItems: [{ column: 'name', summaryType: 'count' }],
360+
groupItems: [],
352361
});
353362

354363
// 'name' has no explicit caption — DevExtreme auto-derives "Name"
@@ -366,6 +375,7 @@ describe('summaryCommand', () => {
366375
{ column: 'amount', summaryType: 'sum' },
367376
{ column: 'amount', summaryType: 'avg' },
368377
],
378+
groupItems: [],
369379
});
370380

371381
expect(callbacks.success).toHaveBeenCalledWith(
@@ -391,7 +401,10 @@ describe('summaryCommand', () => {
391401
const instance = await createGrid();
392402
const callbacks = createCallbacks();
393403

394-
await summaryCommand.execute(instance, callbacks)({});
404+
await summaryCommand.execute(instance, callbacks)({
405+
totalItems: [],
406+
groupItems: [],
407+
});
395408

396409
expect(callbacks.failure).toHaveBeenCalledWith('Display data summaries.');
397410
});
@@ -403,6 +416,7 @@ describe('summaryCommand', () => {
403416
// Single item with unresolved column → failure path, item-list message
404417
await summaryCommand.execute(instance, callbacks)({
405418
totalItems: [{ column: 'unknown', summaryType: 'sum' }],
419+
groupItems: [],
406420
});
407421

408422
expect(callbacks.failure).toHaveBeenCalledWith(
@@ -427,16 +441,16 @@ describe('clearSummaryCommand', () => {
427441
});
428442

429443
describe('execute', () => {
430-
it('calls component.option("summary", { groupItems: undefined, totalItems: undefined }) on success', async () => {
444+
it('calls component.option("summary", { groupItems: [], totalItems: [] }) on success', async () => {
431445
const instance = await createGrid();
432446
const optionSpy = jest.spyOn(instance, 'option');
433447
const callbacks = createCallbacks();
434448

435449
const result = await clearSummaryCommand.execute(instance, callbacks)();
436450

437451
expect(optionSpy).toHaveBeenCalledWith('summary', {
438-
groupItems: undefined,
439-
totalItems: undefined,
452+
groupItems: [],
453+
totalItems: [],
440454
});
441455
expect(result.status).toBe('success');
442456
});

0 commit comments

Comments
 (0)