Skip to content

Commit cd8e2d2

Browse files
committed
AIChat: popup and chat default configuration
1 parent 756db92 commit cd8e2d2

File tree

8 files changed

+213
-30
lines changed

8 files changed

+213
-30
lines changed

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

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,134 @@
1+
import type { PositionConfig } from '@js/common/core/animation';
2+
import { ArrayStore } from '@js/common/data';
13
import type { Callback } from '@js/core/utils/callbacks';
4+
import { getHeight } from '@js/core/utils/size';
5+
import { isString } from '@js/core/utils/type';
6+
import type { Message, Properties as ChatProperties } from '@js/ui/chat';
7+
import type { Properties as PopupProperties } from '@js/ui/popup';
8+
import { AI_ASSISTANT_POPUP_OFFSET } from '@ts/grids/grid_core/ai_assistant/const';
9+
import type { ColumnHeadersView } from '@ts/grids/grid_core/column_headers/m_column_headers';
10+
import type { OptionChanged } from '@ts/grids/grid_core/m_types';
11+
import type { RowsView } from '@ts/grids/grid_core/views/m_rows_view';
212

313
import { AIChat } from '../ai_chat/ai_chat';
414
import type { AIChatOptions } from '../ai_chat/types';
515
import { View } from '../m_modules';
616

717
export class AIAssistantView extends View {
8-
private aiChatInstance!: AIChat;
18+
private aiChatInstance?: AIChat;
19+
20+
private columnHeadersView!: ColumnHeadersView;
21+
22+
private rowsView!: RowsView;
923

1024
public visibilityChanged?: Callback;
1125

26+
private messageStore?: ArrayStore<Message, string>;
27+
28+
public init(): void {
29+
this.columnHeadersView = this.getView('columnHeadersView');
30+
this.rowsView = this.getView('rowsView');
31+
32+
this.messageStore = new ArrayStore<Message, string>({
33+
key: 'id',
34+
});
35+
}
36+
1237
private getAIChatConfig(): AIChatOptions {
38+
const popupOptions = this.getAIChatPopupOptions();
39+
const chatOptions = this.getAIChatOptions();
40+
1341
return {
1442
container: this.element(),
1543
createComponent: this._createComponent.bind(this),
16-
onVisibilityChanged: (visible: boolean): void => {
17-
this.visibilityChanged?.fire(visible);
44+
onChatCleared: (): void => {},
45+
popupOptions,
46+
chatOptions,
47+
};
48+
}
49+
50+
private getPopupHeight(): number {
51+
const headersHeight = this.columnHeadersView.getHeight();
52+
const rowsViewHeight = getHeight(this.rowsView.element());
53+
54+
return headersHeight + rowsViewHeight - AI_ASSISTANT_POPUP_OFFSET * 2;
55+
}
56+
57+
private getAIChatPopupOptions(): PopupProperties {
58+
const position: PositionConfig = {
59+
my: 'right top',
60+
at: 'right top',
61+
of: this.columnHeadersView.element(),
62+
collision: 'fit',
63+
offset: `${-AI_ASSISTANT_POPUP_OFFSET} ${AI_ASSISTANT_POPUP_OFFSET}`,
64+
boundaryOffset: `${AI_ASSISTANT_POPUP_OFFSET} ${AI_ASSISTANT_POPUP_OFFSET}`,
65+
};
66+
67+
// @ts-ignore
68+
return {
69+
title: this.option('aiAssistant.title') ?? '',
70+
position,
71+
// NOTE: DevExtreme Popup supports function-valued height at runtime
72+
// (re-evaluated automatically on show and window resize).
73+
// @ts-expect-error type declaration
74+
height: () => this.getPopupHeight(),
75+
onShowing: (): void => {
76+
this.visibilityChanged?.fire(true);
1877
},
78+
onHidden: (): void => {
79+
this.visibilityChanged?.fire(false);
80+
},
81+
...this.option('aiAssistant.popup'),
1982
};
2083
}
2184

22-
protected _renderCore(): void {
23-
const config = this.getAIChatConfig();
85+
private getAIChatOptions(): ChatProperties {
86+
return {
87+
dataSource: this.messageStore,
88+
onMessageEntered: (e): void => {
89+
const parsedTimestamp = isString(e.message.timestamp)
90+
? Date.parse(e.message.timestamp)
91+
: e.message.timestamp?.toString() ?? '';
92+
93+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
94+
this.messageStore?.insert({
95+
...e.message,
96+
id: `${e.message.author?.id}-${parsedTimestamp}`,
97+
});
98+
},
99+
...this.option('aiAssistant.chat'),
100+
};
101+
}
24102

103+
protected _renderCore(): void {
25104
if (!this.aiChatInstance) {
105+
const config = this.getAIChatConfig();
106+
26107
this.aiChatInstance = new AIChat(config);
27108
}
28109
}
29110

111+
public optionChanged(args: OptionChanged): void {
112+
if (args.name === 'aiAssistant') {
113+
const [, secondLevel] = args.fullName.split('.');
114+
switch (secondLevel) {
115+
case 'enabled':
116+
if (args.value) {
117+
this?._invalidate();
118+
} else {
119+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
120+
this?.hide();
121+
}
122+
break;
123+
default:
124+
this.aiChatInstance?.updateOptions(this.getAIChatConfig());
125+
}
126+
args.handled = true;
127+
} else {
128+
super.optionChanged(args);
129+
}
130+
}
131+
30132
protected callbackNames(): string[] {
31133
return ['visibilityChanged'];
32134
}

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_view_controller.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class AIAssistantViewController extends ViewController {
2727
this.aiAssistantView = this.getView('aiAssistantView');
2828
this.headerPanel = this.getView('headerPanel');
2929

30+
// todo add remove before add
3031
this.aiAssistantView.visibilityChanged?.add((visible: boolean): void => {
3132
this.getAiAssistantButton()?.toggleClass(ACTIVE_STATE_CLASS, visible);
3233
});
@@ -42,8 +43,10 @@ export class AIAssistantViewController extends ViewController {
4243

4344
public optionChanged(args: OptionChanged): void {
4445
if (args.name === 'aiAssistant') {
45-
this.syncAiAssistantItem();
46-
args.handled = true;
46+
if (args.fullName === 'aiAssistant.enabled' || args.fullName === 'aiAssistant.title') {
47+
this.syncAiAssistantItem();
48+
args.handled = true;
49+
}
4750
} else {
4851
super.optionChanged(args);
4952
}
@@ -60,11 +63,8 @@ export class AIAssistantViewController extends ViewController {
6063
const aiAssistantToolbarItem = this.createAiAssistantToolbarItem();
6164

6265
this.headerPanel?.applyToolbarItem(AI_ASSISTANT_BUTTON_NAME, aiAssistantToolbarItem);
63-
this.aiAssistantView?._invalidate();
6466
} else {
6567
this.headerPanel?.removeToolbarItem(AI_ASSISTANT_BUTTON_NAME);
66-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
67-
this.aiAssistantView?.hide();
6868
}
6969
}
7070

@@ -73,9 +73,12 @@ export class AIAssistantViewController extends ViewController {
7373

7474
const hintText = this.option('aiAssistant.title'); // TODO clarify option name
7575

76+
const isShown = this.aiAssistantView?.isShown();
77+
7678
const aiAssistantToolbarItemClass = this.headerPanel?.getToolbarButtonClass(
7779
this.addWidgetPrefix(CLASSES.aiAssistantButton),
7880
);
81+
const aiAssistantToolbarItemStateClass = isShown ? ACTIVE_STATE_CLASS : '';
7982

8083
return {
8184
widget: 'dxButton',
@@ -87,7 +90,7 @@ export class AIAssistantViewController extends ViewController {
8790
text: hintText,
8891
elementAttr: {
8992
'aria-haspopup': 'dialog',
90-
class: aiAssistantToolbarItemClass,
93+
class: `${aiAssistantToolbarItemClass} ${aiAssistantToolbarItemStateClass}`,
9194
},
9295
},
9396
showText: 'inMenu',

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/const.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ export const AI_ASSISTANT_ICON_NAME = 'chatsparkleoutline';
44
export const CLASSES = {
55
aiAssistantButton: 'ai-assistant-button',
66
};
7+
8+
export const AI_ASSISTANT_POPUP_OFFSET = 12;
Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
1+
import messageLocalization from '@js/common/core/localization/message';
12
import $ from '@js/core/renderer';
23
import type { Properties as ChatProperties } from '@js/ui/chat';
34
import Chat from '@js/ui/chat';
4-
import type { Properties as PopupProperties } from '@js/ui/popup';
5+
import type { Properties as PopupProperties, ToolbarItem } from '@js/ui/popup';
6+
import {
7+
CHAT_MESSAGELIST_EMPTY_IMAGE_CLASS,
8+
CHAT_MESSAGELIST_EMPTY_MESSAGE_CLASS,
9+
CHAT_MESSAGELIST_EMPTY_PROMPT_CLASS,
10+
} from '@ts/ui/chat/messagelist';
511
import Popup from '@ts/ui/popup/m_popup';
612

7-
import { CLASSES, DEFAULT_POPUP_OPTIONS } from './const';
13+
import {
14+
CLASSES, CLEAR_CHAT_ICON,
15+
DEFAULT_CHAT_OPTIONS,
16+
DEFAULT_POPUP_OPTIONS,
17+
} from './const';
818
import type { AIChatOptions } from './types';
919

1020
export class AIChat {
1121
private readonly popupInstance: Popup;
1222

13-
private chatInstance!: Chat;
23+
private chatInstance?: Chat;
1424

1525
constructor(
16-
private readonly options: AIChatOptions,
26+
private options: AIChatOptions,
1727
) {
1828
const { container, createComponent } = options;
1929

@@ -22,19 +32,42 @@ export class AIChat {
2232
}
2333

2434
private getChatConfig(): ChatProperties {
25-
return {};
35+
return {
36+
...DEFAULT_CHAT_OPTIONS,
37+
// onMessageEntered: (e): void => {
38+
// this.options.onMessageEntered?.(e);
39+
// },
40+
emptyViewTemplate: (_data, container): void => {
41+
const $image = $('<div>')
42+
.addClass(CHAT_MESSAGELIST_EMPTY_IMAGE_CLASS);
43+
const $message = $('<div>')
44+
.addClass(CHAT_MESSAGELIST_EMPTY_MESSAGE_CLASS)
45+
.text(messageLocalization.format('dxDataGrid-aiAChatEmptyViewMessage'));
46+
const $prompt = $('<div>')
47+
.addClass(CHAT_MESSAGELIST_EMPTY_PROMPT_CLASS)
48+
.text(messageLocalization.format('dxDataGrid-aiChatEmptyViewPrompt'));
49+
50+
$(container)
51+
.append($image)
52+
.append($message)
53+
.append($prompt);
54+
},
55+
...this.options.chatOptions,
56+
};
2657
}
2758

2859
private getPopupConfig(): PopupProperties {
60+
const clearChatButton = this.getClearChatButton();
61+
2962
return {
3063
...DEFAULT_POPUP_OPTIONS,
3164
wrapperAttr: { class: `${CLASSES.aiChat} ${CLASSES.aiDialog}` },
32-
onShowing: (): void => {
33-
this.options.onVisibilityChanged?.(true);
34-
},
35-
onHidden: (): void => {
36-
this.options.onVisibilityChanged?.(false);
37-
},
65+
// onShowing: (): void => {
66+
// this.options.onVisibilityChanged?.(true);
67+
// },
68+
// onHidden: (): void => {
69+
// this.options.onVisibilityChanged?.(false);
70+
// },
3871
contentTemplate: ($container): void => {
3972
const $editorContainer = $('<div>')
4073
.addClass(CLASSES.aiChatContent)
@@ -46,9 +79,37 @@ export class AIChat {
4679
this.getChatConfig(),
4780
);
4881
},
82+
toolbarItems: clearChatButton ? [clearChatButton] : undefined,
83+
...this.options.popupOptions,
4984
};
5085
}
5186

87+
private getClearChatButton(): ToolbarItem | undefined {
88+
const { onChatCleared } = this.options;
89+
90+
if (!onChatCleared) {
91+
return undefined;
92+
}
93+
94+
return {
95+
widget: 'dxButton',
96+
toolbar: 'top',
97+
location: 'after',
98+
options: {
99+
icon: CLEAR_CHAT_ICON,
100+
hint: messageLocalization.format('dxDataGrid-aiAssistantClearButtonText'),
101+
onClick: onChatCleared,
102+
},
103+
};
104+
}
105+
106+
public updateOptions(options: AIChatOptions): void {
107+
this.options = options;
108+
109+
this.popupInstance.option(this.options.popupOptions);
110+
this.chatInstance?.option(this.options.chatOptions ?? {});
111+
}
112+
52113
public toggle(): Promise<boolean> {
53114
return this.popupInstance.toggle();
54115
}
@@ -58,6 +119,6 @@ export class AIChat {
58119
}
59120

60121
public isShown(): boolean {
61-
return !!this.popupInstance.option('visible');
122+
return !!this.popupInstance?.option('visible');
62123
}
63124
}
Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
export const DEFAULT_POPUP_OPTIONS = {
2-
width: 360,
3-
height: 'auto',
2+
width: 400,
3+
minWidth: 400,
4+
minHeight: 480,
45
visible: false,
56
shading: false,
67
showCloseButton: true,
78
};
89

10+
export const DEFAULT_CHAT_OPTIONS = {
11+
showAvatar: false,
12+
showDayHeaders: false,
13+
showMessageText: false,
14+
showUserAvatar: false,
15+
speechToTextEnabled: true,
16+
};
17+
918
export const CLASSES = {
1019
aiChat: 'dx-ai-chat',
1120
aiDialog: 'dx-aidialog',
1221
aiChatContent: 'dx-ai-chat__content',
1322
};
23+
24+
export const CLEAR_CHAT_ICON = 'chatdismiss';

packages/devextreme/js/__internal/grids/grid_core/ai_chat/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { dxElementWrapper } from '@js/core/renderer';
2-
import type { Properties as ChatProperties } from '@js/ui/chat';
2+
import type { MessageEnteredEvent, Properties as ChatProperties } from '@js/ui/chat';
33
import type { Properties as PopupProperties } from '@js/ui/popup';
44

55
import type { CreateComponent } from '../m_types';
@@ -8,7 +8,7 @@ export interface AIChatOptions {
88
container: dxElementWrapper;
99
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1010
createComponent: CreateComponent<any>;
11-
onMessageEntered?: () => void;
11+
onMessageEntered?: (e: MessageEnteredEvent) => void;
1212
onChatCleared?: () => void;
1313
onRegenerate?: () => void;
1414
onVisibilityChanged?: (visible: boolean) => void;

packages/devextreme/js/__internal/grids/grid_core/m_types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import type { GridBase, GridBaseOptions, SelectionBase } from '@js/common/grids'
44
import type { Component } from '@js/core/component';
55
import type { PropertyType } from '@js/core/index';
66
import type { dxElementWrapper } from '@js/core/renderer';
7+
import type { Properties as ChatOptions } from '@js/ui/chat';
78
import type { Properties as DataGridOptions } from '@js/ui/data_grid';
9+
import type { Properties as PopupOptions } from '@js/ui/popup';
810
import type { Properties as TreeListdOptions } from '@js/ui/tree_list';
911
import type Widget from '@js/ui/widget/ui.widget';
1012

@@ -132,6 +134,8 @@ export interface InternalGridOptions extends GridBaseOptions<InternalGrid, unkno
132134
aiAssistant?: {
133135
enabled?: boolean;
134136
title?: string;
137+
popup?: PopupOptions,
138+
chat?: ChatOptions,
135139
};
136140
}
137141

0 commit comments

Comments
 (0)