diff --git a/packages/devextreme-scss/scss/widgets/base/gridBase/layout/aiChat/_index.scss b/packages/devextreme-scss/scss/widgets/base/gridBase/layout/aiChat/_index.scss index 33ad964529d0..c458e54ea0c8 100644 --- a/packages/devextreme-scss/scss/widgets/base/gridBase/layout/aiChat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/gridBase/layout/aiChat/_index.scss @@ -66,3 +66,14 @@ bottom: 0; line-height: 0; } + +.dx-ai-chat--disabled { + .dx-chat-messagebox, .dx-ai-chat__message-regenerate-button { + opacity: 0.5; + pointer-events: none; + } + + .dx-ai-chat__message-regenerate-button { + cursor: default; + } +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_controller.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_controller.test.ts index 488743b57431..3e46c2b926bd 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_controller.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_controller.test.ts @@ -82,6 +82,7 @@ describe('AIAssistantController', () => { const timestamp = '2026-04-16T10:00:00.000Z'; const expectedTimestamp = Date.parse(timestamp); + // eslint-disable-next-line @typescript-eslint/no-floating-promises controller.sendRequestToAI({ author: { id: 'user', name: 'User' }, text: 'Generate values', @@ -104,6 +105,7 @@ describe('AIAssistantController', () => { it('should keep message as pending when AI integration is not configured', async () => { const controller = createController(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises controller.sendRequestToAI({ author: { id: 'user', name: 'User' }, text: 'Generate values', @@ -124,6 +126,7 @@ describe('AIAssistantController', () => { 'aiAssistant.aiIntegration': mockAIIntegration, }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises controller.sendRequestToAI({ author: { id: 'user', name: 'User' }, text: 'Generate values', @@ -153,7 +156,7 @@ describe('AIAssistantController', () => { 'aiAssistant.aiIntegration': mockAIIntegration, }); - controller.sendRequestToAI({ + const promise = controller.sendRequestToAI({ author: { id: 'user', name: 'User' }, text: 'Generate values', timestamp: '2026-04-16T10:00:00.000Z', @@ -169,6 +172,8 @@ describe('AIAssistantController', () => { text: 'Network error', }), ]); + + await expect(promise).rejects.toThrow('Network error'); }); it('should fail message when response has no actions', async () => { @@ -176,7 +181,7 @@ describe('AIAssistantController', () => { 'aiAssistant.aiIntegration': mockAIIntegration, }); - controller.sendRequestToAI({ + const promise = controller.sendRequestToAI({ author: { id: 'user', name: 'User' }, text: 'Generate values', timestamp: '2026-04-16T10:00:00.000Z', @@ -196,6 +201,57 @@ describe('AIAssistantController', () => { text: 'Default error message', }), ]); + + await expect(promise).rejects.toThrow('Default error message'); + }); + + it('should resolve promise when command succeeds', async () => { + const controller = createController({ + 'aiAssistant.aiIntegration': mockAIIntegration, + }); + + const promise = controller.sendRequestToAI({ + author: { id: 'user', name: 'User' }, + text: 'Generate values', + timestamp: '2026-04-16T10:00:00.000Z', + } as Message); + + const actions = [{ name: 'sort', args: { column: 'Name' } }]; + sendRequestCallbacks.onComplete?.({ actions }); + + await expect(promise).resolves.toBeUndefined(); + }); + + it('should reject promise when onError is called', async () => { + const controller = createController({ + 'aiAssistant.aiIntegration': mockAIIntegration, + }); + + const promise = controller.sendRequestToAI({ + author: { id: 'user', name: 'User' }, + text: 'Generate values', + timestamp: '2026-04-16T10:00:00.000Z', + } as Message); + + sendRequestCallbacks.onError?.(new Error('Network error')); + + await expect(promise).rejects.toThrow('Network error'); + }); + + it('should reject promise when response has no actions', async () => { + const controller = createController({ + 'aiAssistant.aiIntegration': mockAIIntegration, + }); + + const promise = controller.sendRequestToAI({ + author: { id: 'user', name: 'User' }, + text: 'Generate values', + timestamp: '2026-04-16T10:00:00.000Z', + } as Message); + + sendRequestCallbacks.onComplete?.({} as ExecuteGridAssistantCommandResult); + + await expect(promise).rejects.toThrow('Default error message'); }); }); }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_view.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_view.test.ts index 0f31e20ad09f..032cdbfd4ed0 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_view.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_view.test.ts @@ -283,6 +283,7 @@ describe('AIAssistantView', () => { describe('chat event handlers', () => { describe('onMessageEntered', () => { it('should send request to AI with the entered message', () => { + mockAIAssistantController.sendRequestToAI.mockReturnValue(Promise.resolve()); createAIAssistantView(); const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions; @@ -296,6 +297,82 @@ describe('AIAssistantView', () => { expect(mockAIAssistantController.sendRequestToAI).toHaveBeenCalledTimes(1); expect(mockAIAssistantController.sendRequestToAI).toHaveBeenCalledWith(message); }); + + it('should not send request when chat is disabled', () => { + createAIAssistantView(); + + const aiChatInstance = (AIChat as jest.Mock) + .mock.results[0].value as { isDisabled: jest.Mock; setDisabled: jest.Mock }; + aiChatInstance.isDisabled.mockReturnValue(true); + + const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions; + const message = { + author: { id: 'user', name: 'User' }, + text: 'Generate summary', + }; + + aiChatConfig.chatOptions?.onMessageEntered?.({ message } as any); + + expect(mockAIAssistantController.sendRequestToAI).not.toHaveBeenCalled(); + }); + + it('should call setDisabled(true) before sending request', () => { + mockAIAssistantController.sendRequestToAI.mockReturnValue(Promise.resolve()); + createAIAssistantView(); + + const aiChatInstance = (AIChat as jest.Mock) + .mock.results[0].value as { setDisabled: jest.Mock }; + + const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions; + const message = { + author: { id: 'user', name: 'User' }, + text: 'Generate summary', + }; + + aiChatConfig.chatOptions?.onMessageEntered?.({ message } as any); + + expect(aiChatInstance.setDisabled).toHaveBeenCalledWith(true); + }); + + it('should call setDisabled(false) after request completes successfully', async () => { + mockAIAssistantController.sendRequestToAI.mockReturnValue(Promise.resolve()); + createAIAssistantView(); + + const aiChatInstance = (AIChat as jest.Mock) + .mock.results[0].value as { setDisabled: jest.Mock }; + + const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions; + const message = { + author: { id: 'user', name: 'User' }, + text: 'Generate summary', + }; + + aiChatConfig.chatOptions?.onMessageEntered?.({ message } as any); + await Promise.resolve(); + + expect(aiChatInstance.setDisabled).toHaveBeenLastCalledWith(false); + }); + + it('should call setDisabled(false) after request fails', async () => { + mockAIAssistantController.sendRequestToAI.mockReturnValue( + Promise.reject(new Error('Network error')), + ); + createAIAssistantView(); + + const aiChatInstance = (AIChat as jest.Mock) + .mock.results[0].value as { setDisabled: jest.Mock }; + + const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions; + const message = { + author: { id: 'user', name: 'User' }, + text: 'Generate summary', + }; + + aiChatConfig.chatOptions?.onMessageEntered?.({ message } as any); + await Promise.resolve(); + + expect(aiChatInstance.setDisabled).toHaveBeenLastCalledWith(false); + }); }); }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_controller.ts index b29ff39703bc..8b0917ec3a26 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_controller.ts @@ -102,26 +102,31 @@ export class AIAssistantController extends Controller { }; } - public sendRequestToAI(message: Message): void { + public sendRequestToAI(message: Message): Promise { const aiMessageId = this.createPendingAIMessage(message); - this.aiAssistantIntegrationController?.sendRequest(message.text, { - onComplete: (response: ExecuteGridAssistantCommandResult): void => { - fromPromise(this.processResponse(response)) - .done((commands: CommandResults) => { - this.completeAIMessage(aiMessageId, commands); - }) - .fail((errorMessage) => { - const error = errorMessage instanceof Error - ? errorMessage - : new Error(String(errorMessage)); - - this.failAIMessage(aiMessageId, error); - }); - }, - onError: (error: Error): void => { - this.failAIMessage(aiMessageId, error); - }, + return new Promise((resolve, reject) => { + this.aiAssistantIntegrationController?.sendRequest(message.text, { + onComplete: (response: ExecuteGridAssistantCommandResult): void => { + fromPromise(this.processResponse(response)) + .done((commands: CommandResults) => { + this.completeAIMessage(aiMessageId, commands); + resolve(); + }) + .fail((errorMessage) => { + const error = errorMessage instanceof Error + ? errorMessage + : new Error(String(errorMessage)); + + this.failAIMessage(aiMessageId, error); + reject(error); + }); + }, + onError: (error: Error): void => { + this.failAIMessage(aiMessageId, error); + reject(error); + }, + }); }); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_view.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_view.ts index 4de99df0b311..a198424c276a 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_view.ts @@ -3,6 +3,7 @@ import type { Callback } from '@js/core/utils/callbacks'; import { getHeight } from '@js/core/utils/size'; import type { Properties as ChatProperties } from '@js/ui/chat'; import type { Properties as PopupProperties } from '@js/ui/popup'; +import { fromPromise } from '@ts/core/utils/m_deferred'; import { AI_ASSISTANT_POPUP_OFFSET } from '@ts/grids/grid_core/ai_assistant/const'; import { isChatOptions, @@ -90,7 +91,14 @@ export class AIAssistantView extends View { dataSource: this.aiAssistantController.getMessageDataSource(), reloadOnChange: true, onMessageEntered: (e): void => { - this.aiAssistantController.sendRequestToAI(e.message); + if (this.aiChatInstance?.isDisabled()) { + return; + } + + this.aiChatInstance?.setDisabled(true); + fromPromise(this.aiAssistantController.sendRequestToAI(e.message)).always(() => { + this.aiChatInstance?.setDisabled(false); + }); }, ...this.option('aiAssistant.chat'), }; diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.test.ts index 9a107750799f..cc44fc2f04a6 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.test.ts @@ -20,21 +20,48 @@ import { } from './const'; import type { AIChatOptions, CommandResults } from './types'; +const mockWidgetInstance = { + option: jest.fn(), +}; + +jest.mock('../m_utils', () => ({ + __esModule: true, + default: { + getWidgetInstance: jest.fn(() => mockWidgetInstance), + }, +})); + const mockPopupInstance = { toggle: jest.fn<() => Promise>().mockResolvedValue(true), hide: jest.fn<() => Promise>().mockResolvedValue(true), option: jest.fn<(name: string) => unknown>().mockReturnValue(false), }; +const mockChatElement = $('
'); + const mockChatInstance = { option: jest.fn(), + $element: jest.fn(() => mockChatElement), +}; + +const mockClearChatButtonInstance = { + option: jest.fn(), }; const createComponentMock = jest.fn(( _el: any, Widget: any, + options?: any, ): any => { if (Widget === Popup) { + const toolbarItems = options?.toolbarItems; + + if (toolbarItems) { + toolbarItems.forEach((item: any) => { + item.options?.onInitialized?.({ component: mockClearChatButtonInstance }); + }); + } + return mockPopupInstance; } if (Widget === Chat) { @@ -88,6 +115,10 @@ const getChatConfig = (): any => { const beforeTest = (): void => { jest.clearAllMocks(); + mockChatElement.removeClass(CLASSES.disabled); + mockChatElement.empty(); + mockWidgetInstance.option.mockClear(); + mockClearChatButtonInstance.option.mockClear(); }; const afterTest = (): void => { @@ -662,4 +693,221 @@ describe('AIChat', () => { }).not.toThrow(); }); }); + + describe('disabled state', () => { + describe('setDisabled', () => { + it('should add disabled class to chat element', () => { + const { aiChat } = createAIChat(); + triggerContentTemplate(); + + aiChat.setDisabled(true); + + expect(mockChatElement.hasClass(CLASSES.disabled)).toBe(true); + }); + + it('should remove disabled class from chat element when set to false', () => { + const { aiChat } = createAIChat(); + triggerContentTemplate(); + + aiChat.setDisabled(true); + aiChat.setDisabled(false); + + expect(mockChatElement.hasClass(CLASSES.disabled)).toBe(false); + }); + + it('should disable text area widget', () => { + const { aiChat } = createAIChat(); + triggerContentTemplate(); + mockChatElement.append($('
').addClass('dx-textarea')); + + aiChat.setDisabled(true); + + expect(mockWidgetInstance.option).toHaveBeenCalledWith('disabled', true); + }); + + it('should enable text area widget when set to false', () => { + const { aiChat } = createAIChat(); + triggerContentTemplate(); + mockChatElement.append($('
').addClass('dx-textarea')); + + aiChat.setDisabled(true); + aiChat.setDisabled(false); + + expect(mockWidgetInstance.option).toHaveBeenCalledWith('disabled', false); + }); + + it('should disable speech-to-text widget', () => { + const { aiChat } = createAIChat(); + triggerContentTemplate(); + mockChatElement.append($('
').addClass('dx-speech-to-text')); + + aiChat.setDisabled(true); + + expect(mockWidgetInstance.option).toHaveBeenCalledWith('disabled', true); + }); + + it('should enable speech-to-text widget when set to false', () => { + const { aiChat } = createAIChat(); + triggerContentTemplate(); + mockChatElement.append($('
').addClass('dx-speech-to-text')); + + aiChat.setDisabled(true); + aiChat.setDisabled(false); + + expect(mockWidgetInstance.option).toHaveBeenCalledWith('disabled', false); + }); + + it('should disable clear button via popup toolbarItems option', () => { + const onChatCleared = jest.fn(); + const { aiChat } = createAIChat({ onChatCleared }); + triggerContentTemplate(); + + aiChat.setDisabled(true); + + expect(mockClearChatButtonInstance.option).toHaveBeenCalledWith('disabled', true); + }); + + it('should enable clear button via popup toolbarItems option', () => { + const onChatCleared = jest.fn(); + const { aiChat } = createAIChat({ onChatCleared }); + triggerContentTemplate(); + + aiChat.setDisabled(true); + aiChat.setDisabled(false); + + expect(mockClearChatButtonInstance.option).toHaveBeenCalledWith('disabled', false); + }); + + it('should not update popup toolbarItems when onChatCleared is not provided', () => { + const { aiChat } = createAIChat(); + triggerContentTemplate(); + + aiChat.setDisabled(true); + + expect(mockClearChatButtonInstance.option).not.toHaveBeenCalledWith( + 'disabled', + expect.anything(), + ); + }); + + it('should not update when setting same disabled value', () => { + const { aiChat } = createAIChat(); + triggerContentTemplate(); + + aiChat.setDisabled(true); + mockClearChatButtonInstance.option.mockClear(); + mockWidgetInstance.option.mockClear(); + + aiChat.setDisabled(true); + + expect(mockClearChatButtonInstance.option).not.toHaveBeenCalled(); + expect(mockWidgetInstance.option).not.toHaveBeenCalled(); + }); + + it('should not throw when chatInstance is not created', () => { + const { aiChat } = createAIChat(); + + expect(() => { + aiChat.setDisabled(true); + }).not.toThrow(); + }); + }); + + describe('isDisabled', () => { + it('should return false by default', () => { + const { aiChat } = createAIChat(); + + expect(aiChat.isDisabled()).toBe(false); + }); + + it('should return true after setDisabled(true)', () => { + const { aiChat } = createAIChat(); + + aiChat.setDisabled(true); + + expect(aiChat.isDisabled()).toBe(true); + }); + + it('should return false after setDisabled(false)', () => { + const { aiChat } = createAIChat(); + + aiChat.setDisabled(true); + aiChat.setDisabled(false); + + expect(aiChat.isDisabled()).toBe(false); + }); + }); + + describe('regenerate button in disabled state', () => { + it('should not call onRegenerate when chat is disabled', () => { + const onRegenerate = jest.fn(); + const { aiChat } = createAIChat({ onRegenerate }); + triggerContentTemplate(); + + aiChat.setDisabled(true); + + const chatConfig = getChatConfig(); + const container = document.createElement('div'); + + chatConfig.messageTemplate({ + message: { + author: { id: AI_ASSISTANT_AUTHOR_ID, name: 'AI Assistant' }, + text: 'Error occurred', + status: 'failure', + }, + }, container); + + const regenerateButton = container.querySelector(`.${CLASSES.messageRegenerateButton}`) as HTMLElement; + regenerateButton.click(); + + expect(onRegenerate).not.toHaveBeenCalled(); + }); + + it('should render regenerate button when chat is disabled', () => { + const onRegenerate = jest.fn(); + const { aiChat } = createAIChat({ onRegenerate }); + triggerContentTemplate(); + + aiChat.setDisabled(true); + + const chatConfig = getChatConfig(); + const container = document.createElement('div'); + + chatConfig.messageTemplate({ + message: { + author: { id: AI_ASSISTANT_AUTHOR_ID, name: 'AI Assistant' }, + text: 'Error occurred', + status: 'failure', + }, + }, container); + + expect(container.querySelector(`.${CLASSES.messageRegenerateButton}`)).not.toBeNull(); + }); + + it('should call onRegenerate when chat is re-enabled', () => { + const onRegenerate = jest.fn(); + const { aiChat } = createAIChat({ onRegenerate }); + triggerContentTemplate(); + + aiChat.setDisabled(true); + aiChat.setDisabled(false); + + const chatConfig = getChatConfig(); + const container = document.createElement('div'); + + chatConfig.messageTemplate({ + message: { + author: { id: AI_ASSISTANT_AUTHOR_ID, name: 'AI Assistant' }, + text: 'Error occurred', + status: 'failure', + }, + }, container); + + const regenerateButton = container.querySelector(`.${CLASSES.messageRegenerateButton}`) as HTMLElement; + regenerateButton.click(); + + expect(onRegenerate).toHaveBeenCalledTimes(1); + }); + }); + }); }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.ts index bd7b8a54c5a6..3b67df64993c 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.ts @@ -7,6 +7,7 @@ import type { Message, Properties as ChatProperties } from '@js/ui/chat'; import Chat from '@js/ui/chat'; import type { Properties as PopupProperties, ToolbarItem } from '@js/ui/popup'; import { MessageStatus } from '@ts/grids/grid_core/ai_assistant/const'; +import type Button from '@ts/ui/button'; import { CHAT_MESSAGELIST_EMPTY_IMAGE_CLASS, CHAT_MESSAGELIST_EMPTY_MESSAGE_CLASS, @@ -15,6 +16,7 @@ import { import ProgressBar from '@ts/ui/m_progress_bar'; import Popup from '@ts/ui/popup/m_popup'; +import gridCoreUtils from '../m_utils'; import { CLASSES, CLEAR_CHAT_ICON, DEFAULT_CHAT_OPTIONS, @@ -36,6 +38,10 @@ export class AIChat { private chatInstance?: Chat; + private clearChatButtonInstance?: Button; + + private disabled = false; + constructor( private options: AIChatOptions, ) { @@ -115,10 +121,14 @@ export class AIChat { widget: 'dxButton', toolbar: 'top', location: 'after', + cssClass: `${CLASSES.clearChatButton}`, options: { icon: CLEAR_CHAT_ICON, hint: messageLocalization.format('dxDataGrid-aiAssistantClearButtonText'), onClick: onChatCleared, + onInitialized: (e): void => { + this.clearChatButtonInstance = e.component; + }, }, }; } @@ -159,7 +169,11 @@ export class AIChat { .addClass(`dx-icon dx-icon-${REGENERATE_ICON} ${CLASSES.messageRegenerateButton}`) .appendTo($row); - eventsEngine.on($button, clickEventName, () => this.options.onRegenerate?.()); + eventsEngine.on($button, clickEventName, () => { + if (!this.disabled) { + this.options.onRegenerate?.(); + } + }); } } @@ -247,6 +261,26 @@ export class AIChat { .appendTo($container); } + private setTextAreaDisabled(disabled: boolean): void { + const $textArea = this.chatInstance?.$element().find(`.${CLASSES.textArea}`); + + if ($textArea?.length) { + gridCoreUtils.getWidgetInstance($textArea)?.option('disabled', disabled); + } + } + + private setSpeechToTextDisabled(disabled: boolean): void { + const $speechToText = this.chatInstance?.$element().find(`.${CLASSES.speechToTextButton}`); + + if ($speechToText?.length) { + gridCoreUtils.getWidgetInstance($speechToText)?.option('disabled', disabled); + } + } + + private setClearChatButtonDisabled(disabled: boolean): void { + this.clearChatButtonInstance?.option('disabled', disabled); + } + public updateOptions(options: AIChatOptions, updatePopup: boolean, updateChat: boolean): void { this.options = options; @@ -271,6 +305,23 @@ export class AIChat { return !!this.popupInstance?.option('visible'); } + public setDisabled(disabled: boolean): void { + if (this.disabled === disabled) { + return; + } + + this.disabled = disabled; + this.chatInstance?.$element().toggleClass(CLASSES.disabled, disabled); + + this.setTextAreaDisabled(disabled); + this.setSpeechToTextDisabled(disabled); + this.setClearChatButtonDisabled(disabled); + } + + public isDisabled(): boolean { + return this.disabled; + } + public renderAIMessage(message: Message, container: HTMLElement): void { const $message = $('
') .addClass(`${CLASSES.message} ${getMessageStateClass(message.status)}`) diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_chat/const.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/const.ts index 9aa1b824acb7..145447e3e691 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_chat/const.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/const.ts @@ -38,6 +38,10 @@ export const CLASSES = { messageHeaderRow: 'dx-ai-chat__message-header-row', messageRegenerateButton: 'dx-ai-chat__message-regenerate-button', messageProgressBar: 'dx-ai-chat__message-progressbar', + clearChatButton: 'dx-ai-chat__clear-button', + disabled: 'dx-ai-chat--disabled', + textArea: 'dx-textarea', + speechToTextButton: 'dx-speech-to-text', }; export const CLEAR_CHAT_ICON = 'clearhistory'; diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index e61cbd284ab3..dd65fc71de8e 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -127,9 +127,9 @@ const getSummaryText = function (summaryItem, summaryTexts) { }; const getWidgetInstance = function ($element) { - const editorData = $element.data && $element.data(); - const dxComponents = editorData && editorData.dxComponents; - const widgetName = dxComponents && dxComponents[0]; + const editorData = $element?.data(); + const dxComponents = editorData?.dxComponents; + const widgetName = dxComponents?.[0]; return widgetName && editorData[widgetName]; };