Skip to content

Commit ea5abe2

Browse files
AIChat: popup and chat default configuration (#33225)
1 parent 2dc64f9 commit ea5abe2

File tree

20 files changed

+751
-85
lines changed

20 files changed

+751
-85
lines changed

packages/devextreme-scss/scss/widgets/base/gridBase/_index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
@use "../mixins" as *;
33
@use 'variables' as *;
44
@use 'layout/cell' as *;
5+
@use "layout/ai-chat" as *;
56

67
// adduse
78

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@forward 'layout/ai-chat/mixins';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@use "../../../icon_fonts" as *;
2+
3+
.dx-ai-chat .dx-popup-content {
4+
padding: 0;
5+
}
6+
7+
.dx-ai-chat__content {
8+
border: none;
9+
min-height: auto;
10+
}
11+
12+
.dx-ai-chat-messagelist-empty-image {
13+
@include dx-icon(sparkle);
14+
15+
border-radius: 999em;
16+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@mixin ai-chat-messagelist-empty(
2+
$messagelist-empty-icon-color,
3+
$messagelist-empty-icon-background-color,
4+
) {
5+
.dx-ai-chat-messagelist-empty-image {
6+
color: $messagelist-empty-icon-color;
7+
background-color: $messagelist-empty-icon-background-color;
8+
}
9+
}

packages/devextreme-scss/scss/widgets/fluent/gridBase/_index.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
@use "../button/colors" as *;
2222
@use "../validation/colors" as *;
2323
@use "../typography/sizes" as *;
24+
@use "../../base/gridBase/mixins" as *;
2425

2526
// adduse
2627
@use "../scrollable";
@@ -1055,4 +1056,9 @@ $fluent-grid-base-group-panel-message-line-height: $fluent-button-text-line-heig
10551056
height: $fluent-grid-base-ai-prompt-editor-progressbar-height;
10561057
border-radius: 0;
10571058
}
1059+
1060+
@include ai-chat-messagelist-empty(
1061+
$button-default-bg,
1062+
$button-default-outlined-hover-bg,
1063+
);
10581064
}

packages/devextreme-scss/scss/widgets/generic/gridBase/_index.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
@use "../scrollable";
2525
@use "../overlay";
2626
@use "../pagination";
27+
@use "../../base/gridBase/mixins" as *;
2728

2829
$generic-grid-base-border-hidden: 1px solid transparent;
2930
$generic-grid-base-row-border: 1px solid transparent;
@@ -898,4 +899,9 @@ $generic-grid-base-cell-input-height: math.round($generic-base-line-height * $ge
898899
border: none;
899900
border-radius: 0;
900901
}
902+
903+
@include ai-chat-messagelist-empty(
904+
$button-default-bg,
905+
$button-default-outlined-bg-hover,
906+
);
901907
}

packages/devextreme-scss/scss/widgets/material/gridBase/_index.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
@use "../button/sizes" as *;
2222
@use "../validation/colors" as *;
2323
@use "../typography/sizes" as *;
24+
@use "../../base/gridBase/mixins" as *;
2425

2526
// adduse
2627
@use "variables" as *;
@@ -1032,4 +1033,9 @@ $material-grid-base-group-panel-message-line-height: $material-button-text-line-
10321033
height: $material-grid-base-ai-prompt-editor-progressbar-height;
10331034
border-radius: 0;
10341035
}
1036+
1037+
@include ai-chat-messagelist-empty(
1038+
$button-default-bg,
1039+
$button-default-outlined-hover-bg,
1040+
);
10351041
}

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_view.test.ts

Lines changed: 161 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,30 @@ const createAIAssistantView = ({
5555

5656
return undefined;
5757
});
58+
const $columnHeadersElement = $('<div>').appendTo($container);
59+
const $rowsViewElement = $('<div>').css('height', '400px').appendTo($container);
60+
61+
const mockColumnHeadersView = {
62+
getHeight: jest.fn().mockReturnValue(50),
63+
element: jest.fn().mockReturnValue($columnHeadersElement),
64+
};
65+
const mockRowsView = {
66+
element: jest.fn().mockReturnValue($rowsViewElement),
67+
};
68+
5869
const mockComponent = {
5970
element: (): any => $container.get(0),
6071
_createComponent: createComponentMock,
6172
_controllers: {},
73+
_views: {
74+
columnHeadersView: mockColumnHeadersView,
75+
rowsView: mockRowsView,
76+
},
6277
option: optionMock,
6378
};
6479

6580
const aiAssistantView = new AIAssistantView(mockComponent);
81+
aiAssistantView.init();
6682
if (render) {
6783
aiAssistantView.render($container);
6884
}
@@ -114,13 +130,16 @@ describe('AIAssistantView', () => {
114130
expect(AIChat).toHaveBeenCalledTimes(1);
115131
});
116132

117-
it('should pass container and createComponent to AIChat', () => {
133+
it('should pass container, createComponent, popupOptions, chatOptions, and onChatCleared to AIChat', () => {
118134
const { aiAssistantView } = createAIAssistantView();
119135

120136
expect(AIChat).toHaveBeenCalledWith(
121137
expect.objectContaining({
122138
container: aiAssistantView.element(),
123139
createComponent: expect.any(Function),
140+
popupOptions: expect.any(Object),
141+
chatOptions: expect.any(Object),
142+
onChatCleared: expect.any(Function),
124143
}),
125144
);
126145
});
@@ -213,20 +232,158 @@ describe('AIAssistantView', () => {
213232
});
214233

215234
describe('visibilityChanged', () => {
216-
it('should fire visibilityChanged callback when popup visibility changes', () => {
235+
it('should fire visibilityChanged callback with true when popup onShowing is triggered', () => {
217236
const { aiAssistantView } = createAIAssistantView();
218237
const callback = jest.fn();
219238

220239
aiAssistantView.visibilityChanged?.add(callback);
221240

222241
const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
223-
aiChatConfig.onVisibilityChanged?.(true);
242+
aiChatConfig.popupOptions?.onShowing?.({} as any);
224243

225244
expect(callback).toHaveBeenCalledWith(true);
245+
});
226246

227-
aiChatConfig.onVisibilityChanged?.(false);
247+
it('should fire visibilityChanged callback with false when popup onHidden is triggered', () => {
248+
const { aiAssistantView } = createAIAssistantView();
249+
const callback = jest.fn();
250+
251+
aiAssistantView.visibilityChanged?.add(callback);
252+
253+
const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
254+
aiChatConfig.popupOptions?.onHidden?.({} as any);
228255

229256
expect(callback).toHaveBeenCalledWith(false);
230257
});
231258
});
259+
260+
describe('optionChanged', () => {
261+
it('should set handled to true for aiAssistant options', () => {
262+
const { aiAssistantView } = createAIAssistantView();
263+
264+
const args = {
265+
name: 'aiAssistant' as const,
266+
fullName: 'aiAssistant.title' as const,
267+
value: 'New Title',
268+
previousValue: 'Old Title',
269+
handled: false,
270+
};
271+
272+
aiAssistantView.optionChanged(args);
273+
274+
expect(args.handled).toBe(true);
275+
});
276+
277+
it('should call _invalidate when aiAssistant.enabled changes to true', () => {
278+
const { aiAssistantView } = createAIAssistantView();
279+
const invalidateSpy = jest.spyOn(aiAssistantView, '_invalidate' as any);
280+
281+
aiAssistantView.optionChanged({
282+
name: 'aiAssistant' as const,
283+
fullName: 'aiAssistant.enabled' as const,
284+
value: true,
285+
previousValue: false,
286+
handled: false,
287+
});
288+
289+
expect(invalidateSpy).toHaveBeenCalledTimes(1);
290+
});
291+
292+
it('should call hide when aiAssistant.enabled changes to false', () => {
293+
const { aiAssistantView, setEnabled } = createAIAssistantView();
294+
const hideSpy = jest.spyOn(aiAssistantView, 'hide');
295+
296+
setEnabled(false);
297+
298+
aiAssistantView.optionChanged({
299+
name: 'aiAssistant' as const,
300+
fullName: 'aiAssistant.enabled' as const,
301+
value: false,
302+
previousValue: true,
303+
handled: false,
304+
});
305+
306+
expect(hideSpy).toHaveBeenCalledTimes(1);
307+
});
308+
309+
it('should call updateOptions on aiChatInstance for title change', () => {
310+
const { aiAssistantView } = createAIAssistantView();
311+
312+
const aiChatInstance = (AIChat as jest.Mock)
313+
.mock.results[0].value as { updateOptions: jest.Mock };
314+
315+
aiAssistantView.optionChanged({
316+
name: 'aiAssistant' as const,
317+
fullName: 'aiAssistant.title' as const,
318+
value: 'New Title',
319+
previousValue: 'Old Title',
320+
handled: false,
321+
});
322+
323+
expect(aiChatInstance.updateOptions).toHaveBeenCalledTimes(1);
324+
expect(aiChatInstance.updateOptions).toHaveBeenCalledWith(
325+
expect.any(Object),
326+
true,
327+
false,
328+
);
329+
});
330+
331+
it('should call updateOptions on aiChatInstance for chat options change', () => {
332+
const { aiAssistantView } = createAIAssistantView();
333+
334+
const aiChatInstance = (AIChat as jest.Mock)
335+
.mock.results[0].value as { updateOptions: jest.Mock };
336+
337+
aiAssistantView.optionChanged({
338+
name: 'aiAssistant' as const,
339+
fullName: 'aiAssistant.chat' as const,
340+
value: { speechToTextEnabled: false },
341+
previousValue: {},
342+
handled: false,
343+
});
344+
345+
expect(aiChatInstance.updateOptions).toHaveBeenCalledTimes(1);
346+
expect(aiChatInstance.updateOptions).toHaveBeenCalledWith(
347+
expect.any(Object),
348+
false,
349+
true,
350+
);
351+
});
352+
353+
it('should call updateOptions with both flags when object value contains title and chat', () => {
354+
const { aiAssistantView } = createAIAssistantView();
355+
356+
const aiChatInstance = (AIChat as jest.Mock)
357+
.mock.results[0].value as { updateOptions: jest.Mock };
358+
359+
aiAssistantView.optionChanged({
360+
name: 'aiAssistant' as const,
361+
fullName: 'aiAssistant' as const,
362+
value: { title: 'New title', chat: { speechToTextEnabled: false } },
363+
previousValue: { title: 'Old title' },
364+
handled: false,
365+
});
366+
367+
expect(aiChatInstance.updateOptions).toHaveBeenCalledTimes(1);
368+
expect(aiChatInstance.updateOptions).toHaveBeenCalledWith(
369+
expect.any(Object),
370+
true,
371+
true,
372+
);
373+
});
374+
375+
it('should not throw when aiChatInstance is not created for non-enabled sub-options', () => {
376+
const { aiAssistantView } = createAIAssistantView({ render: false });
377+
378+
expect(() => {
379+
aiAssistantView.optionChanged({
380+
name: 'aiAssistant' as const,
381+
fullName: 'aiAssistant.title' as const,
382+
value: 'New Title',
383+
previousValue: 'Old Title',
384+
handled: false,
385+
});
386+
}).not.toThrow();
387+
});
388+
});
232389
});

0 commit comments

Comments
 (0)