Skip to content

Commit bac54f9

Browse files
author
Raushen
committed
Add onAIColumnRequestCreating implementation
1 parent a814864 commit bac54f9

6 files changed

Lines changed: 319 additions & 100 deletions

File tree

packages/devextreme/js/__internal/grids/grid_core/ai_column/ai_column.integration.test.ts

Lines changed: 211 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,47 @@ describe('API Methods', () => {
10811081
expect(abortSpy).toHaveBeenCalledTimes(1);
10821082
});
10831083

1084+
it('should not send a request if there are no data rows', async () => {
1085+
const aiIntegrationResult = (): RequestResult => ({
1086+
promise: new Promise<string>((resolve) => {
1087+
columnSendRequestResolved();
1088+
resolve('1');
1089+
}),
1090+
abort: (): void => {
1091+
abortSpy();
1092+
},
1093+
});
1094+
1095+
const columnAiIntegration = new AIIntegration({
1096+
sendRequest(): RequestResult {
1097+
return aiIntegrationResult();
1098+
},
1099+
});
1100+
const { instance } = await createDataGrid({
1101+
dataSource: [],
1102+
keyExpr: 'id',
1103+
columns: [
1104+
{ dataField: 'id', caption: 'ID' },
1105+
{ dataField: 'name', caption: 'Name' },
1106+
{ dataField: 'value', caption: 'Value' },
1107+
{
1108+
type: 'ai',
1109+
caption: 'AI Column',
1110+
name: 'myColumn',
1111+
ai: {
1112+
aiIntegration: columnAiIntegration,
1113+
mode: 'manual',
1114+
prompt: 'Test prompt',
1115+
},
1116+
},
1117+
],
1118+
});
1119+
1120+
instance.sendAIColumnRequest('myColumn');
1121+
expect(columnSendRequestResolved).toHaveBeenCalledTimes(0);
1122+
expect(abortSpy).toHaveBeenCalledTimes(0);
1123+
});
1124+
10841125
it('should abort the previous request of the same column', async () => {
10851126
const aiIntegrationResult = (): RequestResult => ({
10861127
promise: new Promise<string>((resolve) => {
@@ -1259,6 +1300,47 @@ describe('API Methods', () => {
12591300
expect(abortSpy).toHaveBeenCalledTimes(1);
12601301
});
12611302

1303+
it('should not send a request if there are no data rows', async () => {
1304+
const aiIntegrationResult = (): RequestResult => ({
1305+
promise: new Promise<string>((resolve) => {
1306+
columnSendRequestResolved();
1307+
resolve('1');
1308+
}),
1309+
abort: (): void => {
1310+
abortSpy();
1311+
},
1312+
});
1313+
1314+
const columnAiIntegration = new AIIntegration({
1315+
sendRequest(): RequestResult {
1316+
return aiIntegrationResult();
1317+
},
1318+
});
1319+
const { instance } = await createDataGrid({
1320+
dataSource: [],
1321+
keyExpr: 'id',
1322+
columns: [
1323+
{ dataField: 'id', caption: 'ID' },
1324+
{ dataField: 'name', caption: 'Name' },
1325+
{ dataField: 'value', caption: 'Value' },
1326+
{
1327+
type: 'ai',
1328+
caption: 'AI Column',
1329+
name: 'myColumn',
1330+
ai: {
1331+
aiIntegration: columnAiIntegration,
1332+
mode: 'manual',
1333+
prompt: 'Test prompt',
1334+
},
1335+
},
1336+
],
1337+
});
1338+
1339+
instance.refreshAIColumn('myColumn');
1340+
expect(columnSendRequestResolved).toHaveBeenCalledTimes(0);
1341+
expect(abortSpy).toHaveBeenCalledTimes(0);
1342+
});
1343+
12621344
it('should abort the previous request of the same column', async () => {
12631345
const aiIntegrationResult = (): RequestResult => ({
12641346
promise: new Promise<string>((resolve) => {
@@ -1458,18 +1540,20 @@ describe('API Methods', () => {
14581540
describe('API Handlers', () => {
14591541
const columnSendRequestStarted = jest.fn();
14601542
const columnSendRequestResolved = jest.fn();
1543+
const sendRequestSpy = jest.fn();
14611544
const abortSpy = jest.fn();
14621545

14631546
beforeEach(() => {
14641547
beforeTest();
14651548
columnSendRequestStarted.mockClear();
14661549
columnSendRequestResolved.mockClear();
1550+
sendRequestSpy.mockClear();
14671551
abortSpy.mockClear();
14681552
});
14691553

14701554
afterEach(afterTest);
14711555

1472-
describe('onAIRequestStarted', () => {
1556+
describe('onAIColumnRequestCreating', () => {
14731557
const aiIntegrationResult = (): RequestResult => ({
14741558
promise: new Promise<string>((resolve) => {
14751559
columnSendRequestStarted();
@@ -1484,11 +1568,12 @@ describe('API Handlers', () => {
14841568
},
14851569
});
14861570
const columnAiIntegration = new AIIntegration({
1487-
sendRequest(): RequestResult {
1571+
sendRequest({ prompt }): RequestResult {
1572+
sendRequestSpy(prompt);
14881573
return aiIntegrationResult();
14891574
},
14901575
});
1491-
it('should call onAIColumnRequestCreating handler', async () => {
1576+
it('should be called by default', async () => {
14921577
const onAIColumnRequestCreating = jest.fn();
14931578
const { instance } = await createDataGrid({
14941579
dataSource: [
@@ -1519,8 +1604,15 @@ describe('API Handlers', () => {
15191604
expect(onAIColumnRequestCreating).toHaveBeenCalledTimes(1);
15201605
expect(onAIColumnRequestCreating).toHaveBeenCalledWith(
15211606
expect.objectContaining({
1522-
column: expect.objectContaining({ name: 'myColumn' }),
1523-
prompt: expect.stringContaining('Test prompt'),
1607+
component: expect.objectContaining({ NAME: 'dxDataGrid' }),
1608+
element: expect.objectContaining({ id: GRID_CONTAINER_ID }),
1609+
column: expect.objectContaining({
1610+
name: 'myColumn',
1611+
ai: expect.objectContaining({
1612+
mode: 'manual',
1613+
prompt: 'Test prompt',
1614+
}),
1615+
}),
15241616
data: expect.arrayContaining([
15251617
{ id: 1, name: 'Name 1', value: 10 },
15261618
{ id: 2, name: 'Name 2', value: 20 },
@@ -1535,6 +1627,120 @@ describe('API Handlers', () => {
15351627
jest.advanceTimersByTime(10000);
15361628
expect(columnSendRequestResolved).toHaveBeenCalledTimes(1);
15371629
});
1630+
1631+
it('should cancel the request if e.cancel is true', async () => {
1632+
const { instance } = await createDataGrid({
1633+
dataSource: [
1634+
{ id: 1, name: 'Name 1', value: 10 },
1635+
{ id: 2, name: 'Name 2', value: 20 },
1636+
],
1637+
keyExpr: 'id',
1638+
columns: [
1639+
{ dataField: 'id', caption: 'ID' },
1640+
{ dataField: 'name', caption: 'Name' },
1641+
{ dataField: 'value', caption: 'Value' },
1642+
{
1643+
type: 'ai',
1644+
caption: 'AI Column',
1645+
name: 'myColumn',
1646+
ai: {
1647+
aiIntegration: columnAiIntegration,
1648+
mode: 'manual',
1649+
prompt: 'Test prompt',
1650+
},
1651+
},
1652+
],
1653+
onAIColumnRequestCreating: (e) => { e.cancel = true; },
1654+
});
1655+
1656+
instance.sendAIColumnRequest('myColumn');
1657+
// There is enough time to resolve a promise
1658+
jest.advanceTimersByTime(10000);
1659+
expect(columnSendRequestStarted).toHaveBeenCalledTimes(0);
1660+
expect(abortSpy).toHaveBeenCalledTimes(0);
1661+
expect(columnSendRequestResolved).toHaveBeenCalledTimes(0);
1662+
});
1663+
1664+
it('should take into account reduced data', async () => {
1665+
const { instance } = await createDataGrid({
1666+
dataSource: [
1667+
{ id: 1, name: 'Name 1', value: 10 },
1668+
{ id: 2, name: 'Name 2', value: 20 },
1669+
],
1670+
keyExpr: 'id',
1671+
columns: [
1672+
{ dataField: 'id', caption: 'ID' },
1673+
{ dataField: 'name', caption: 'Name' },
1674+
{ dataField: 'value', caption: 'Value' },
1675+
{
1676+
type: 'ai',
1677+
caption: 'AI Column',
1678+
name: 'myColumn',
1679+
ai: {
1680+
aiIntegration: columnAiIntegration,
1681+
mode: 'manual',
1682+
prompt: 'Test prompt',
1683+
},
1684+
},
1685+
],
1686+
onAIColumnRequestCreating: (e) => {
1687+
const filtered = e.data.filter((item) => item.id === 2);
1688+
e.data.splice(0, e.data.length, ...filtered);
1689+
},
1690+
});
1691+
1692+
instance.sendAIColumnRequest('myColumn');
1693+
// There is enough time to resolve a promise
1694+
jest.advanceTimersByTime(10000);
1695+
expect(columnSendRequestStarted).toHaveBeenCalledTimes(1);
1696+
expect(columnSendRequestResolved).toHaveBeenCalledTimes(1);
1697+
expect(sendRequestSpy).toHaveBeenCalledWith(expect.objectContaining({
1698+
user: expect.stringContaining('Data: {"2":{"id":2,"name":"Name 2","value":20}}'),
1699+
}));
1700+
1701+
await Promise.resolve();
1702+
expect(abortSpy).toHaveBeenCalledTimes(1);
1703+
});
1704+
1705+
it('should pass additional info to the AI request', async () => {
1706+
const { instance } = await createDataGrid({
1707+
dataSource: [
1708+
{ id: 1, name: 'Name 1', value: 10 },
1709+
{ id: 2, name: 'Name 2', value: 20 },
1710+
],
1711+
keyExpr: 'id',
1712+
columns: [
1713+
{ dataField: 'id', caption: 'ID' },
1714+
{ dataField: 'name', caption: 'Name' },
1715+
{ dataField: 'value', caption: 'Value' },
1716+
{
1717+
type: 'ai',
1718+
caption: 'AI Column',
1719+
name: 'myColumn',
1720+
ai: {
1721+
aiIntegration: columnAiIntegration,
1722+
mode: 'manual',
1723+
prompt: 'Test prompt',
1724+
},
1725+
},
1726+
],
1727+
onAIColumnRequestCreating: (e) => {
1728+
e.additionalInfo = { customData: 'My custom data' };
1729+
},
1730+
});
1731+
1732+
instance.sendAIColumnRequest('myColumn');
1733+
// There is enough time to resolve a promise
1734+
jest.advanceTimersByTime(10000);
1735+
expect(columnSendRequestStarted).toHaveBeenCalledTimes(1);
1736+
expect(columnSendRequestResolved).toHaveBeenCalledTimes(1);
1737+
expect(sendRequestSpy).toHaveBeenCalledWith(expect.objectContaining({
1738+
user: expect.stringContaining('Data: {"2":{"id":2,"name":"Name 2","value":20}}'),
1739+
}));
1740+
1741+
await Promise.resolve();
1742+
expect(abortSpy).toHaveBeenCalledTimes(1);
1743+
});
15381744
});
15391745

15401746
// describe('onAIRequestCompleted', () => {

packages/devextreme/js/__internal/grids/grid_core/ai_column/m_ai_column_controller.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,6 @@ export class AiColumnController extends Controller {
2424
this.dataController.changed.add(this.dataChangedHandler);
2525
}
2626

27-
private createAIColumnRequest() {
28-
const args = {};
29-
this.executeAction('onAIColumnRequestCreating', args);
30-
}
31-
32-
private receiveAIColumnResponse() {
33-
const options = {};
34-
this.executeAction('onAIColumnResponseReceived', options);
35-
}
36-
3727
private refreshAIColumnInternal(columnName: string): void {
3828
this.aiColumnIntegrationController.refreshAIColumn(columnName);
3929
}

packages/devextreme/js/__internal/grids/grid_core/ai_column/m_ai_column_integration_controller.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { DataController } from '../data_controller/m_data_controller';
1111
import type { ErrorHandlingController } from '../error_handling/m_error_handling';
1212
import { Controller } from '../m_modules';
1313
import { AiColumnCacheController } from './m_ai_column_cache_controller';
14-
import { getData, reduceDataCachedKeys } from './utils';
14+
import { getDataFromRowItems, reduceDataCachedKeys } from './utils';
1515

1616
export class AiColumnIntegrationController extends Controller {
1717
private aborts: Record<string, (() => void) | undefined> = { };
@@ -31,6 +31,9 @@ export class AiColumnIntegrationController extends Controller {
3131

3232
this.aiColumnCacheController = new AiColumnCacheController(this.component);
3333
this.aiColumnCacheController.init();
34+
35+
this.createAction('onAIColumnRequestCreating');
36+
this.createAction('onAIColumnResponseReceived');
3437
}
3538

3639
public sendAIColumnRequest(columnName: string): void {
@@ -41,12 +44,13 @@ export class AiColumnIntegrationController extends Controller {
4144
this.sendRequest(columnName, false);
4245
}
4346

44-
private sendRequest(columnName: string, force: boolean): void {
47+
private sendRequest(columnName: string, useCache: boolean): void {
4548
const aiIntegration = this.getAiIntegration(columnName);
4649
if (!aiIntegration) {
4750
return;
4851
}
49-
const prompt = this.columnsController.columnOption(columnName, 'ai.prompt');
52+
const column = this.columnsController.getColumns().find((col) => col.name === columnName);
53+
const { prompt } = column.ai;
5054
if (!prompt) {
5155
return;
5256
}
@@ -55,17 +59,38 @@ export class AiColumnIntegrationController extends Controller {
5559
this.abortRequest(columnName);
5660
}
5761

58-
let data = getData(this.dataController.items());
62+
const rowItems = this.dataController.items();
63+
const data = getDataFromRowItems(rowItems);
64+
const args = {
65+
column,
66+
useCache,
67+
cancel: false,
68+
additionalInfo: { },
69+
data,
70+
};
71+
this.executeAction('onAIColumnRequestCreating', args);
72+
73+
if (args.cancel) {
74+
return;
75+
}
76+
5977
const keys = Object.keys(data);
60-
const cachedResponse: Record<PropertyKey, string> = force
78+
const cachedResponse: Record<PropertyKey, string> = useCache
6179
? {}
6280
: this.aiColumnCacheController.getCachedResponse(columnName, keys);
63-
data = reduceDataCachedKeys(data, cachedResponse);
81+
const keyField = this.dataController.key();
82+
const reducedData = reduceDataCachedKeys(data, cachedResponse, keyField);
83+
const areAllDataCached = Object.keys(reducedData).length === 0;
84+
if (areAllDataCached) {
85+
this.showResult(columnName, '', cachedResponse);
86+
return;
87+
}
6488

6589
const abort = aiIntegration.generateGridColumn(
6690
{
6791
text: prompt,
68-
data,
92+
data: reducedData,
93+
params: args.additionalInfo,
6994
},
7095
this.getAICommandCallbacks<GenerateGridColumnCommandResult>(columnName, cachedResponse),
7196
);
@@ -92,6 +117,7 @@ export class AiColumnIntegrationController extends Controller {
92117
onComplete: (finalResponse: T): void => {
93118
if (this.isRequestAwaitingCompletion(columnName)) {
94119
this.showResult(columnName, String(finalResponse), cachedResponse);
120+
this.executeAction('onAIColumnResponseReceived', {});
95121
this.processCommandCompletion(columnName);
96122
}
97123
},

0 commit comments

Comments
 (0)