From 406998c27f23eb20640ae68e6b318005ec133b61 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Tue, 14 Apr 2026 14:18:48 +0400 Subject: [PATCH 1/4] Add AIAssistantController --- .../module_not_extended/ai_assistant.ts | 4 ++- .../ai_assistant/ai_assistant_controller.ts | 35 +++++++++++++++++++ .../js/__internal/grids/grid_core/m_types.ts | 3 +- .../module_not_extended/ai_assistant.ts | 4 ++- 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_controller.ts diff --git a/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/ai_assistant.ts b/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/ai_assistant.ts index 38c5e84f8c21..a90e3f77231c 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/ai_assistant.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/ai_assistant.ts @@ -1,4 +1,5 @@ import messageLocalization from '@js/common/core/localization/message'; +import { AIAssistantController } from '@ts/grids/grid_core/ai_assistant/ai_assistant_controller'; import { AIAssistantView } from '@ts/grids/grid_core/ai_assistant/ai_assistant_view'; import { AIAssistantViewController } from '@ts/grids/grid_core/ai_assistant/ai_assistant_view_controller'; @@ -14,7 +15,8 @@ gridCore.registerModule('aiAssistant', { }; }, controllers: { - aiAssistant: AIAssistantViewController, + aiAssistant: AIAssistantController, + aiAssistantViewController: AIAssistantViewController, }, views: { aiAssistantView: AIAssistantView, 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 new file mode 100644 index 000000000000..caf0ea9e267f --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_controller.ts @@ -0,0 +1,35 @@ +import type { ColumnsController } from '../columns_controller/m_columns_controller'; +import type { DataController } from '../data_controller/m_data_controller'; +import { Controller } from '../m_modules'; + +interface MockAIIntegration { + sendRequest: () => Promise, +} + +export class AIAssistantController extends Controller { + private readonly dataController!: DataController; + + private readonly columnsController!: ColumnsController; + + // TODO: need to specify type AIIntegration | null after creating AIIntegration command + private getAIIntegration(): MockAIIntegration | null { + const gridAIIntegration = this.option('aiAssistant.aiIntegration'); + + if (gridAIIntegration) { + return gridAIIntegration as MockAIIntegration; + } + + return null; + } + + public sendRequestToAI(): Promise { + const aiIntegration = this.getAIIntegration(); + + if (!aiIntegration) { + return Promise.reject(new Error('AI integration is not configured')); + } + + // TODO: need to create new aiIntegration command + return aiIntegration.sendRequest(); + } +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts index d925ef21d3bd..4d2a2602eb07 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts @@ -216,7 +216,8 @@ export interface Controllers { toastViewController: import('./toast/m_toast_controller').ToastViewController; aiColumn: import('./ai_column/controllers/m_ai_column_controller').AIColumnController; aiPromptEditor: import('./ai_column/controllers/m_ai_prompt_editor_view_controller').AIPromptEditorViewController; - aiAssistant: import('./ai_assistant/ai_assistant_view_controller').AIAssistantViewController; + aiAssistant: import('./ai_assistant/ai_assistant_controller').AIAssistantController; + aiAssistantViewController: import('./ai_assistant/ai_assistant_view_controller').AIAssistantViewController; } type ControllerTypes = { diff --git a/packages/devextreme/js/__internal/grids/tree_list/module_not_extended/ai_assistant.ts b/packages/devextreme/js/__internal/grids/tree_list/module_not_extended/ai_assistant.ts index 38c5e84f8c21..a90e3f77231c 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/module_not_extended/ai_assistant.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/module_not_extended/ai_assistant.ts @@ -1,4 +1,5 @@ import messageLocalization from '@js/common/core/localization/message'; +import { AIAssistantController } from '@ts/grids/grid_core/ai_assistant/ai_assistant_controller'; import { AIAssistantView } from '@ts/grids/grid_core/ai_assistant/ai_assistant_view'; import { AIAssistantViewController } from '@ts/grids/grid_core/ai_assistant/ai_assistant_view_controller'; @@ -14,7 +15,8 @@ gridCore.registerModule('aiAssistant', { }; }, controllers: { - aiAssistant: AIAssistantViewController, + aiAssistant: AIAssistantController, + aiAssistantViewController: AIAssistantViewController, }, views: { aiAssistantView: AIAssistantView, From 29d39f791b63c81c3653ec64093c5ee25c7ff846 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Tue, 14 Apr 2026 18:24:48 +0400 Subject: [PATCH 2/4] AIAssistant: add grid commands and request processing --- .../ai_assistant/ai_assistant_controller.ts | 45 +++++++++++++++---- .../grid_core/ai_assistant/grid_commands.ts | 19 ++++++++ .../grids/grid_core/ai_assistant/types.ts | 21 +++++++++ 3 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/ai_assistant/grid_commands.ts create mode 100644 packages/devextreme/js/__internal/grids/grid_core/ai_assistant/types.ts 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 caf0ea9e267f..1e13ef4bc44a 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 @@ -1,15 +1,15 @@ -import type { ColumnsController } from '../columns_controller/m_columns_controller'; -import type { DataController } from '../data_controller/m_data_controller'; import { Controller } from '../m_modules'; +import { GridCommands } from './grid_commands'; +import type { + CommandResponse, InternalRequestCallbacks, ProcessedCommands, +} from './types'; interface MockAIIntegration { - sendRequest: () => Promise, + sendRequest: ({ callbacks }: { callbacks: InternalRequestCallbacks }) => void, } export class AIAssistantController extends Controller { - private readonly dataController!: DataController; - - private readonly columnsController!: ColumnsController; + private gridCommands!: GridCommands; // TODO: need to specify type AIIntegration | null after creating AIIntegration command private getAIIntegration(): MockAIIntegration | null { @@ -22,7 +22,11 @@ export class AIAssistantController extends Controller { return null; } - public sendRequestToAI(): Promise { + public init(): void { + this.gridCommands = new GridCommands(this.component); + } + + public sendRequestToAI(): Promise { const aiIntegration = this.getAIIntegration(); if (!aiIntegration) { @@ -30,6 +34,31 @@ export class AIAssistantController extends Controller { } // TODO: need to create new aiIntegration command - return aiIntegration.sendRequest(); + return new Promise((resolve, reject) => { + aiIntegration.sendRequest({ + callbacks: { + onComplete: (response: CommandResponse): void => { + this.processResponse(response).then(resolve, reject); + }, + onError: (error: Error): void => { + reject(error); + }, + }, + }); + }); + } + + private processResponse(response: CommandResponse): Promise { + if (!response?.commands || response?.explanation) { + // TODO: need to localize default error message when there are no commands + return Promise.reject(new Error(response.explanation || 'Default error message')); + } + + if (!this.gridCommands.validate(response.commands)) { + // TODO: need to localize error message on validation fail + return Promise.reject(new Error('Received invalid commands')); + } + + return this.gridCommands.executeCommands(response.commands); } } diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/grid_commands.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/grid_commands.ts new file mode 100644 index 000000000000..f3f56930445e --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/grid_commands.ts @@ -0,0 +1,19 @@ +import type { InternalGrid } from '../m_types'; +import type { Command, ProcessedCommands } from './types'; + +export class GridCommands { + constructor(private readonly gridInstance: InternalGrid) { + } + + // TODO: need to implement real validation logic + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public validate(commands: Command[]): boolean { + return true; + } + + // TODO: need to implement real command execution logic + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public executeCommands(commands: Command[]): Promise { + return Promise.resolve([{ status: 'success', message: 'Command executed successfully' }]); + } +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/types.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/types.ts new file mode 100644 index 000000000000..48879de255a7 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/types.ts @@ -0,0 +1,21 @@ +export interface CommandResponse { + commands: Command[]; + explanation: string; +} + +export interface Command { + command: string; + args: Record; +} + +export interface InternalRequestCallbacks { + onComplete?: (finalResponse: CommandResponse) => void; + onError?: (error: Error) => void; +} + +export interface ProcessedCommand { + status: 'success' | 'error'; + message: string; +} + +export type ProcessedCommands = ProcessedCommand[]; From b1de362770540b152d4691eb3e1de837d185050d Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Tue, 14 Apr 2026 18:53:22 +0400 Subject: [PATCH 3/4] AIAssistant: move messageStore and message processing to controller --- .../ai_assistant/ai_assistant_controller.ts | 42 +++++++++++++++++++ .../ai_assistant/ai_assistant_view.ts | 31 +++++--------- 2 files changed, 53 insertions(+), 20 deletions(-) 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 1e13ef4bc44a..26bb4a1ba13c 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 @@ -1,3 +1,7 @@ +import { ArrayStore } from '@js/common/data'; +import { isString } from '@js/core/utils/type'; +import type { Message } from '@js/ui/chat'; + import { Controller } from '../m_modules'; import { GridCommands } from './grid_commands'; import type { @@ -11,6 +15,8 @@ interface MockAIIntegration { export class AIAssistantController extends Controller { private gridCommands!: GridCommands; + private messageStore!: ArrayStore; + // TODO: need to specify type AIIntegration | null after creating AIIntegration command private getAIIntegration(): MockAIIntegration | null { const gridAIIntegration = this.option('aiAssistant.aiIntegration'); @@ -24,6 +30,42 @@ export class AIAssistantController extends Controller { public init(): void { this.gridCommands = new GridCommands(this.component); + this.messageStore = new ArrayStore({ + key: 'id', + }); + } + + public getMessageStore(): ArrayStore { + return this.messageStore; + } + + public processUserMessage(message: Message): void { + const parsedTimestamp = isString(message.timestamp) + ? Date.parse(message.timestamp) + : message.timestamp?.toString() ?? ''; + + this.messageStore.push([ + { + type: 'insert', + data: { + ...message, + id: `${message.author?.id}-${parsedTimestamp}`, + }, + }, + { + type: 'insert', + data: { + id: Date.now(), + timestamp: parsedTimestamp, + // TODO: need to localize author name and move it to constants or options + author: { id: 'assistant', name: 'AI Assistant' }, + text: message.text, + }, + }, + ]); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.sendRequestToAI(); } public sendRequestToAI(): Promise { 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 b47bc9ba38e7..2883961f87da 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 @@ -1,9 +1,7 @@ import type { PositionConfig } from '@js/common/core/animation'; -import { ArrayStore } from '@js/common/data'; import type { Callback } from '@js/core/utils/callbacks'; import { getHeight } from '@js/core/utils/size'; -import { isString } from '@js/core/utils/type'; -import type { Message, Properties as ChatProperties } from '@js/ui/chat'; +import type { Properties as ChatProperties } from '@js/ui/chat'; import type { Properties as PopupProperties } from '@js/ui/popup'; import { AI_ASSISTANT_POPUP_OFFSET } from '@ts/grids/grid_core/ai_assistant/const'; import { @@ -19,9 +17,12 @@ import type { RowsView } from '@ts/grids/grid_core/views/m_rows_view'; import { AIChat } from '../ai_chat/ai_chat'; import type { AIChatOptions } from '../ai_chat/types'; import { View } from '../m_modules'; +import type { AIAssistantController } from './ai_assistant_controller'; export class AIAssistantView extends View { - private aiChatInstance?: AIChat; + private aiChatInstance!: AIChat; + + private aiAssistantController?: AIAssistantController; private columnHeadersView!: ColumnHeadersView; @@ -29,15 +30,10 @@ export class AIAssistantView extends View { public visibilityChanged?: Callback; - private messageStore?: ArrayStore; - public init(): void { this.columnHeadersView = this.getView('columnHeadersView'); this.rowsView = this.getView('rowsView'); - - this.messageStore = new ArrayStore({ - key: 'id', - }); + this.aiAssistantController = this.getController('aiAssistant'); } private getAIChatConfig(): AIChatOptions { @@ -90,17 +86,12 @@ export class AIAssistantView extends View { private getAIChatOptions(): ChatProperties { return { - dataSource: this.messageStore, + dataSource: { + store: this.aiAssistantController?.getMessageStore(), + reshapeOnPush: true, + }, onMessageEntered: (e): void => { - const parsedTimestamp = isString(e.message.timestamp) - ? Date.parse(e.message.timestamp) - : e.message.timestamp?.toString() ?? ''; - - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.messageStore?.insert({ - ...e.message, - id: `${e.message.author?.id}-${parsedTimestamp}`, - }); + this.aiAssistantController?.processUserMessage(e.message); }, ...this.option('aiAssistant.chat'), }; From 7f425ee29c6895b5271e7385664c4e5723fabec6 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Thu, 16 Apr 2026 04:33:10 +0400 Subject: [PATCH 4/4] AIAssistant: implement message template --- .../scss/widgets/base/gridBase/_index.scss | 2 +- .../scss/widgets/base/gridBase/_mixins.scss | 1 - .../base/gridBase/layout/ai-chat/_index.scss | 16 --- .../base/gridBase/layout/ai-chat/_mixins.scss | 9 -- .../base/gridBase/layout/aiChat/_index.scss | 40 +++++++ .../base/gridBase/layout/aiChat/_mixins.scss | 19 ++++ .../scss/widgets/fluent/gridBase/_index.scss | 7 +- .../fluent/gridBase/layout/aiChat/_index.scss | 7 ++ .../scss/widgets/generic/gridBase/_index.scss | 7 +- .../gridBase/layout/aiChat/_index.scss | 7 ++ .../widgets/material/gridBase/_index.scss | 7 +- .../gridBase/layout/aiChat/_index.scss | 7 ++ .../__tests__/ai_assistant_controller.test.ts | 72 +++++++++++++ .../__tests__/ai_assistant_view.test.ts | 47 +++++++- .../ai_assistant/ai_assistant_controller.ts | 47 ++++---- .../ai_assistant/ai_assistant_view.ts | 22 ++-- .../grids/grid_core/ai_assistant/const.ts | 10 ++ .../grids/grid_core/ai_chat/ai_chat.test.ts | 102 +++++++++++++++--- .../grids/grid_core/ai_chat/ai_chat.ts | 84 ++++++++++++++- .../grids/grid_core/ai_chat/const.ts | 11 +- .../grids/grid_core/ai_chat/types.ts | 2 + 21 files changed, 428 insertions(+), 98 deletions(-) delete mode 100644 packages/devextreme-scss/scss/widgets/base/gridBase/_mixins.scss delete mode 100644 packages/devextreme-scss/scss/widgets/base/gridBase/layout/ai-chat/_index.scss delete mode 100644 packages/devextreme-scss/scss/widgets/base/gridBase/layout/ai-chat/_mixins.scss create mode 100644 packages/devextreme-scss/scss/widgets/base/gridBase/layout/aiChat/_index.scss create mode 100644 packages/devextreme-scss/scss/widgets/base/gridBase/layout/aiChat/_mixins.scss create mode 100644 packages/devextreme-scss/scss/widgets/fluent/gridBase/layout/aiChat/_index.scss create mode 100644 packages/devextreme-scss/scss/widgets/generic/gridBase/layout/aiChat/_index.scss create mode 100644 packages/devextreme-scss/scss/widgets/material/gridBase/layout/aiChat/_index.scss create mode 100644 packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_controller.test.ts diff --git a/packages/devextreme-scss/scss/widgets/base/gridBase/_index.scss b/packages/devextreme-scss/scss/widgets/base/gridBase/_index.scss index 32f07305bb14..3c47c93296c4 100644 --- a/packages/devextreme-scss/scss/widgets/base/gridBase/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/gridBase/_index.scss @@ -2,7 +2,7 @@ @use "../mixins" as *; @use 'variables' as *; @use 'layout/cell' as *; -@use "layout/ai-chat" as *; +@use "layout/aiChat" as *; // adduse diff --git a/packages/devextreme-scss/scss/widgets/base/gridBase/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/gridBase/_mixins.scss deleted file mode 100644 index 9d0662a25a46..000000000000 --- a/packages/devextreme-scss/scss/widgets/base/gridBase/_mixins.scss +++ /dev/null @@ -1 +0,0 @@ -@forward 'layout/ai-chat/mixins'; diff --git a/packages/devextreme-scss/scss/widgets/base/gridBase/layout/ai-chat/_index.scss b/packages/devextreme-scss/scss/widgets/base/gridBase/layout/ai-chat/_index.scss deleted file mode 100644 index 8af0c61d1429..000000000000 --- a/packages/devextreme-scss/scss/widgets/base/gridBase/layout/ai-chat/_index.scss +++ /dev/null @@ -1,16 +0,0 @@ -@use "../../../icon_fonts" as *; - -.dx-ai-chat .dx-popup-content { - padding: 0; -} - -.dx-ai-chat__content { - border: none; - min-height: auto; -} - -.dx-ai-chat-messagelist-empty-image { - @include dx-icon(sparkle); - - border-radius: 999em; -} diff --git a/packages/devextreme-scss/scss/widgets/base/gridBase/layout/ai-chat/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/gridBase/layout/ai-chat/_mixins.scss deleted file mode 100644 index f760924c5bc9..000000000000 --- a/packages/devextreme-scss/scss/widgets/base/gridBase/layout/ai-chat/_mixins.scss +++ /dev/null @@ -1,9 +0,0 @@ -@mixin ai-chat-messagelist-empty( - $messagelist-empty-icon-color, - $messagelist-empty-icon-background-color, -) { - .dx-ai-chat-messagelist-empty-image { - color: $messagelist-empty-icon-color; - background-color: $messagelist-empty-icon-background-color; - } -} 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 new file mode 100644 index 000000000000..69eb92e0c1ca --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/gridBase/layout/aiChat/_index.scss @@ -0,0 +1,40 @@ +@use "../../../icon_fonts" as *; + +.dx-ai-chat .dx-popup-content { + padding: 0; +} + +.dx-ai-chat__content { + border: none; + min-height: auto; +} + +.dx-ai-chat__empty-image { + @include dx-icon(sparkle); + + border-radius: 999em; +} + +.dx-ai-chat__content .dx-chat-messagebubble { + position: relative; + overflow: hidden; +} + +.dx-ai-chat__message { + display: flex; + gap: 8px; +} + +.dx-ai-chat__message-content { + display: flex; + flex-flow: column; + gap: 4px; +} + +.dx-ai-chat__message-progressbar { + position: absolute; + left: 0; + right: 0; + bottom: 0; + line-height: 0; +} diff --git a/packages/devextreme-scss/scss/widgets/base/gridBase/layout/aiChat/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/gridBase/layout/aiChat/_mixins.scss new file mode 100644 index 000000000000..6a5c5b13e43a --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/gridBase/layout/aiChat/_mixins.scss @@ -0,0 +1,19 @@ +@mixin ai-chat-messagelist-empty( + $messagelist-empty-icon-color, + $messagelist-empty-icon-background-color, +) { + .dx-ai-chat__empty-image { + color: $messagelist-empty-icon-color; + background-color: $messagelist-empty-icon-background-color; + } +} + +@mixin ai-chat-message-pending( + $pending-message-color, +) { + .dx-ai-chat__message--pending { + .dx-ai-chat__message-icon, .dx-ai-chat__message-group-operations { + color: $pending-message-color; + } + } +} diff --git a/packages/devextreme-scss/scss/widgets/fluent/gridBase/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/gridBase/_index.scss index abac73c44eb4..346f4c8517f1 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/gridBase/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/gridBase/_index.scss @@ -21,7 +21,6 @@ @use "../button/colors" as *; @use "../validation/colors" as *; @use "../typography/sizes" as *; -@use "../../base/gridBase/mixins" as *; // adduse @use "../scrollable"; @@ -30,6 +29,7 @@ @use 'variables' as *; @forward 'variables'; @use 'layout/cell' as *; +@use "layout/aiChat" as *; $fluent-grid-base-border-hidden: 1px solid transparent; $fluent-grid-base-row-border: 1px solid transparent; @@ -1056,9 +1056,4 @@ $fluent-grid-base-group-panel-message-line-height: $fluent-button-text-line-heig height: $fluent-grid-base-ai-prompt-editor-progressbar-height; border-radius: 0; } - - @include ai-chat-messagelist-empty( - $button-default-bg, - $button-default-outlined-hover-bg, - ); } diff --git a/packages/devextreme-scss/scss/widgets/fluent/gridBase/layout/aiChat/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/gridBase/layout/aiChat/_index.scss new file mode 100644 index 000000000000..425502c567b0 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/fluent/gridBase/layout/aiChat/_index.scss @@ -0,0 +1,7 @@ +@use '../../../../base/gridBase/layout/aiChat/mixins' as *; +@use "../../../button/colors" as *; +@include ai-chat-messagelist-empty( + $button-default-bg, + $button-default-outlined-hover-bg, +); +@include ai-chat-message-pending($button-default-bg); diff --git a/packages/devextreme-scss/scss/widgets/generic/gridBase/_index.scss b/packages/devextreme-scss/scss/widgets/generic/gridBase/_index.scss index 7aa81ec3e5a6..394c65320cfe 100644 --- a/packages/devextreme-scss/scss/widgets/generic/gridBase/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/gridBase/_index.scss @@ -21,10 +21,10 @@ @use "variables" as *; @forward "variables"; @use 'layout/cell' as *; +@use "layout/aiChat" as *; @use "../scrollable"; @use "../overlay"; @use "../pagination"; -@use "../../base/gridBase/mixins" as *; $generic-grid-base-border-hidden: 1px solid transparent; $generic-grid-base-row-border: 1px solid transparent; @@ -899,9 +899,4 @@ $generic-grid-base-cell-input-height: math.round($generic-base-line-height * $ge border: none; border-radius: 0; } - - @include ai-chat-messagelist-empty( - $button-default-bg, - $button-default-outlined-bg-hover, - ); } diff --git a/packages/devextreme-scss/scss/widgets/generic/gridBase/layout/aiChat/_index.scss b/packages/devextreme-scss/scss/widgets/generic/gridBase/layout/aiChat/_index.scss new file mode 100644 index 000000000000..24614760d2ea --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/generic/gridBase/layout/aiChat/_index.scss @@ -0,0 +1,7 @@ +@use '../../../../base/gridBase/layout/aiChat/mixins' as *; +@use "../../../button/colors" as *; +@include ai-chat-messagelist-empty( + $button-default-bg, + $button-default-outlined-bg-hover, +); +@include ai-chat-message-pending($button-default-bg); diff --git a/packages/devextreme-scss/scss/widgets/material/gridBase/_index.scss b/packages/devextreme-scss/scss/widgets/material/gridBase/_index.scss index 2c77b333900d..d8c67b2d36b5 100644 --- a/packages/devextreme-scss/scss/widgets/material/gridBase/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/gridBase/_index.scss @@ -21,12 +21,12 @@ @use "../button/sizes" as *; @use "../validation/colors" as *; @use "../typography/sizes" as *; -@use "../../base/gridBase/mixins" as *; // adduse @use "variables" as *; @forward "variables"; @use 'layout/cell' as *; +@use "layout/aiChat" as *; @use "../scrollable"; @use "../overlay"; @use "../pagination"; @@ -1033,9 +1033,4 @@ $material-grid-base-group-panel-message-line-height: $material-button-text-line- height: $material-grid-base-ai-prompt-editor-progressbar-height; border-radius: 0; } - - @include ai-chat-messagelist-empty( - $button-default-bg, - $button-default-outlined-hover-bg, - ); } diff --git a/packages/devextreme-scss/scss/widgets/material/gridBase/layout/aiChat/_index.scss b/packages/devextreme-scss/scss/widgets/material/gridBase/layout/aiChat/_index.scss new file mode 100644 index 000000000000..425502c567b0 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/material/gridBase/layout/aiChat/_index.scss @@ -0,0 +1,7 @@ +@use '../../../../base/gridBase/layout/aiChat/mixins' as *; +@use "../../../button/colors" as *; +@include ai-chat-messagelist-empty( + $button-default-bg, + $button-default-outlined-hover-bg, +); +@include ai-chat-message-pending($button-default-bg); 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 new file mode 100644 index 000000000000..5030e9630bc4 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_controller.test.ts @@ -0,0 +1,72 @@ +import { + beforeEach, + describe, + expect, + it, + jest, +} from '@jest/globals'; +import type { Message } from '@js/ui/chat'; + +import type { InternalGrid } from '../../m_types'; +import { AIAssistantController } from '../ai_assistant_controller'; +import { + AI_ASSISTANT_AUTHOR, + AI_ASSISTANT_AUTHOR_ID, + MessageStatus, +} from '../const'; + +const createController = ( + options: Record = {}, +): AIAssistantController => { + const mockComponent = { + _optionCache: {}, + _controllers: {}, + option: jest.fn((name?: string) => { + if (name === undefined) { + return options; + } + + return options[name]; + }), + }; + + const controller = new AIAssistantController(mockComponent as unknown as InternalGrid); + controller.init(); + + return controller; +}; + +describe('AIAssistantController', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createPendingAIMessage', () => { + it('should create pending assistant message in store and return its id', async () => { + const controller = createController(); + const timestamp = '2026-04-16T10:00:00.000Z'; + const expectedTimestamp = Date.parse(timestamp); + + const message: Message = { + author: { id: 'user', name: 'User' }, + text: 'Generate values', + timestamp, + } as Message; + + const messageId = controller.createPendingAIMessage(message); + + const messages = await controller.getMessageStore().load(); + + expect(messageId).toBe(`${AI_ASSISTANT_AUTHOR_ID}-${expectedTimestamp}`); + expect(messages).toEqual([ + expect.objectContaining({ + id: `${AI_ASSISTANT_AUTHOR_ID}-${expectedTimestamp}`, + timestamp: expectedTimestamp, + author: AI_ASSISTANT_AUTHOR, + text: 'Generate values', + status: MessageStatus.Pending, + }), + ]); + }); + }); +}); 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 a8514389639c..ca34b282330f 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 @@ -15,6 +15,7 @@ import wrapInstanceWithMocks from '@ts/grids/grid_core/__tests__/__mock__/helper import { AIChat } from '../../ai_chat/ai_chat'; import type { AIChatOptions } from '../../ai_chat/types'; import { AIAssistantView } from '../ai_assistant_view'; +import type { ProcessedCommands } from '../types'; jest.mock('../../ai_chat/ai_chat', (): any => { const original = jest.requireActual('../../ai_chat/ai_chat'); @@ -34,6 +35,15 @@ const createComponentMock = jest.fn(( options: any, ): any => new Widget(el, options)); +const mockMessageStore = {}; +const mockProcessedCommands: ProcessedCommands = []; +const mockAIAssistantController = { + getMessageStore: jest.fn().mockReturnValue(mockMessageStore), + createPendingAIMessage: jest.fn().mockReturnValue('assistant-1'), + sendRequestToAI: jest.fn<() => Promise>() + .mockResolvedValue(mockProcessedCommands), +}; + const createAIAssistantView = ({ initialEnabled = true, render = true, @@ -69,7 +79,9 @@ const createAIAssistantView = ({ const mockComponent = { element: (): any => $container.get(0), _createComponent: createComponentMock, - _controllers: {}, + _controllers: { + aiAssistant: mockAIAssistantController, + }, _views: { columnHeadersView: mockColumnHeadersView, rowsView: mockRowsView, @@ -144,6 +156,19 @@ describe('AIAssistantView', () => { ); }); + it('should configure chatOptions with controller message store and reloadOnChange', () => { + createAIAssistantView(); + + const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions; + + expect(mockAIAssistantController.getMessageStore).toHaveBeenCalledTimes(1); + expect(aiChatConfig.chatOptions).toEqual(expect.objectContaining({ + dataSource: mockMessageStore, + reloadOnChange: true, + onMessageEntered: expect.any(Function), + })); + }); + it('should not create a new AIChat instance on subsequent renders', () => { const { $container, aiAssistantView } = createAIAssistantView(); @@ -257,6 +282,26 @@ describe('AIAssistantView', () => { }); }); + describe('chat event handlers', () => { + describe('onMessageEntered', () => { + it('should create pending message and send request to AI', () => { + createAIAssistantView(); + + 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.createPendingAIMessage).toHaveBeenCalledTimes(1); + expect(mockAIAssistantController.createPendingAIMessage).toHaveBeenCalledWith(message); + expect(mockAIAssistantController.sendRequestToAI).toHaveBeenCalledTimes(1); + }); + }); + }); + describe('optionChanged', () => { it('should set handled to true for aiAssistant options', () => { const { aiAssistantView } = createAIAssistantView(); 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 26bb4a1ba13c..0532a4fc3c04 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 @@ -3,6 +3,7 @@ import { isString } from '@js/core/utils/type'; import type { Message } from '@js/ui/chat'; import { Controller } from '../m_modules'; +import { AI_ASSISTANT_AUTHOR, AI_ASSISTANT_AUTHOR_ID, MessageStatus } from './const'; import { GridCommands } from './grid_commands'; import type { CommandResponse, InternalRequestCallbacks, ProcessedCommands, @@ -28,6 +29,20 @@ export class AIAssistantController extends Controller { return null; } + private processResponse(response: CommandResponse): Promise { + if (!response?.commands || response?.explanation) { + // TODO: need to localize default error message when there are no commands + return Promise.reject(new Error(response.explanation || 'Default error message')); + } + + if (!this.gridCommands.validate(response.commands)) { + // TODO: need to localize error message on validation fail + return Promise.reject(new Error('Received invalid commands')); + } + + return this.gridCommands.executeCommands(response.commands); + } + public init(): void { this.gridCommands = new GridCommands(this.component); this.messageStore = new ArrayStore({ @@ -39,33 +54,27 @@ export class AIAssistantController extends Controller { return this.messageStore; } - public processUserMessage(message: Message): void { + public createPendingAIMessage(message: Message): string { const parsedTimestamp = isString(message.timestamp) ? Date.parse(message.timestamp) : message.timestamp?.toString() ?? ''; + const aiMessageId = `${AI_ASSISTANT_AUTHOR_ID}-${parsedTimestamp}`; this.messageStore.push([ { type: 'insert', data: { - ...message, - id: `${message.author?.id}-${parsedTimestamp}`, - }, - }, - { - type: 'insert', - data: { - id: Date.now(), + id: aiMessageId, timestamp: parsedTimestamp, // TODO: need to localize author name and move it to constants or options - author: { id: 'assistant', name: 'AI Assistant' }, + author: AI_ASSISTANT_AUTHOR, text: message.text, + status: MessageStatus.Pending, }, }, ]); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.sendRequestToAI(); + return aiMessageId; } public sendRequestToAI(): Promise { @@ -89,18 +98,4 @@ export class AIAssistantController extends Controller { }); }); } - - private processResponse(response: CommandResponse): Promise { - if (!response?.commands || response?.explanation) { - // TODO: need to localize default error message when there are no commands - return Promise.reject(new Error(response.explanation || 'Default error message')); - } - - if (!this.gridCommands.validate(response.commands)) { - // TODO: need to localize error message on validation fail - return Promise.reject(new Error('Received invalid commands')); - } - - return this.gridCommands.executeCommands(response.commands); - } } 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 2883961f87da..6290ee6b1d51 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, @@ -22,7 +23,7 @@ import type { AIAssistantController } from './ai_assistant_controller'; export class AIAssistantView extends View { private aiChatInstance!: AIChat; - private aiAssistantController?: AIAssistantController; + private aiAssistantController!: AIAssistantController; private columnHeadersView!: ColumnHeadersView; @@ -86,12 +87,21 @@ export class AIAssistantView extends View { private getAIChatOptions(): ChatProperties { return { - dataSource: { - store: this.aiAssistantController?.getMessageStore(), - reshapeOnPush: true, - }, + dataSource: this.aiAssistantController.getMessageStore(), + reloadOnChange: true, onMessageEntered: (e): void => { - this.aiAssistantController?.processUserMessage(e.message); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const aiMessageId = this.aiAssistantController.createPendingAIMessage(e.message); + + fromPromise(this.aiAssistantController.sendRequestToAI()) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .done((response) => { + + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .fail((error) => { + + }); }, ...this.option('aiAssistant.chat'), }; diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/const.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/const.ts index 4e52bee28f6c..a4ceac3bba71 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/const.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/const.ts @@ -6,3 +6,13 @@ export const CLASSES = { }; export const AI_ASSISTANT_POPUP_OFFSET = 12; + +export const AI_ASSISTANT_AUTHOR_ID = 'assistant'; +export const AI_ASSISTANT_AUTHOR_NAME = 'AI Assistant'; +export const AI_ASSISTANT_AUTHOR = { id: AI_ASSISTANT_AUTHOR_ID, name: AI_ASSISTANT_AUTHOR_NAME }; + +export enum MessageStatus { + Pending = 'pending', + Success = 'success', + Error = 'error', +} 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 aee8541d3881..aae4b4ab40ee 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 @@ -9,6 +9,8 @@ import { } from '@jest/globals'; import $ from '@js/core/renderer'; import Chat from '@js/ui/chat'; +import { AI_ASSISTANT_AUTHOR_ID } from '@ts/grids/grid_core/ai_assistant/const'; +import ProgressBar from '@ts/ui/m_progress_bar'; import Popup from '@ts/ui/popup/m_popup'; import { AIChat } from './ai_chat'; @@ -55,6 +57,32 @@ const createAIChat = (optionsOverride: Partial = {}): { return { $container, aiChat }; }; +const getPopupConfig = (): any => { + const call = createComponentMock.mock.calls.find( + ([, Widget]) => Widget === Popup, + ); + + expect(call).toBeDefined(); + + return (call as any)[2]; +}; + +const triggerContentTemplate = (): void => { + const popupConfig = getPopupConfig(); + + popupConfig.contentTemplate($('
')); +}; + +const getChatConfig = (): any => { + const call = createComponentMock.mock.calls.find( + ([, Widget]) => Widget === Chat, + ); + + expect(call).toBeDefined(); + + return (call as any)[2]; +}; + const beforeTest = (): void => { jest.clearAllMocks(); }; @@ -130,16 +158,6 @@ describe('AIChat', () => { }); describe('clearChatButton', () => { - const getPopupConfig = (): any => { - const call = createComponentMock.mock.calls.find( - ([, Widget]) => Widget === Popup, - ); - - expect(call).toBeDefined(); - - return (call as any)[2]; - }; - it('should include toolbarItems with clear chat button when onChatCleared is provided', () => { const onChatCleared = jest.fn(); createAIChat({ onChatCleared }); @@ -168,16 +186,66 @@ describe('AIChat', () => { }); }); - describe('updateOptions', () => { - const triggerContentTemplate = (): void => { - const call = createComponentMock.mock.calls.find( - ([, Widget]) => Widget === Popup, + describe('messageTemplate', () => { + it('should render assistant pending message with custom markup and progress bar', () => { + createAIChat(); + triggerContentTemplate(); + + const chatConfig = getChatConfig(); + const container = document.createElement('div'); + + chatConfig.messageTemplate({ + message: { + author: { id: AI_ASSISTANT_AUTHOR_ID, name: 'AI Assistant' }, + text: 'Build summary', + status: 'pending', + }, + }, container); + + expect(container.querySelector(`.${CLASSES.message}`)).not.toBeNull(); + expect(container.querySelector(`.${CLASSES.message}`)?.classList.contains(CLASSES.messagePending)).toBe(true); + expect(container.querySelector(`.${CLASSES.messageIcon}`)).not.toBeNull(); + expect(container.querySelector(`.${CLASSES.messageHeader}`)?.textContent).toBe('Build summary'); + expect(container.querySelector(`.${CLASSES.messageGroupOperations}`)?.textContent).toBe('Processing...'); + expect(container.querySelector(`.${CLASSES.messageProgressBar}`)).not.toBeNull(); + expect(createComponentMock).toHaveBeenCalledWith( + expect.any(Object), + ProgressBar, + { + value: false, + visible: true, + showStatus: false, + width: '100%', + }, ); - const popupConfig = (call as any)[2]; + }); - popupConfig.contentTemplate($('
')); - }; + it('should render plain text for non-assistant message', () => { + createAIChat(); + triggerContentTemplate(); + const chatConfig = getChatConfig(); + const container = document.createElement('div'); + + chatConfig.messageTemplate({ + message: { + author: { id: 'user', name: 'User' }, + text: 'User message', + }, + }, container); + + expect(container.textContent).toBe('User message'); + expect(container.querySelector(`.${CLASSES.message}`)).toBeNull(); + const hasProgressBarCreation = createComponentMock + .mock + .calls + .some(([, Widget]) => Widget === ProgressBar); + + expect(hasProgressBarCreation).toBe(false); + }); + }); + + describe('updateOptions', () => { it('should call popupInstance.option with new popupOptions when updatePopup is true', () => { const { aiChat, $container } = createAIChat(); const newPopupOptions = { title: 'Updated' }; 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 6fb2342ffcc2..4c35bd6a9855 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 @@ -1,13 +1,16 @@ import messageLocalization from '@js/common/core/localization/message'; +import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; -import type { Properties as ChatProperties } from '@js/ui/chat'; +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 { AI_ASSISTANT_AUTHOR_ID } from '@ts/grids/grid_core/ai_assistant/const'; import { CHAT_MESSAGELIST_EMPTY_IMAGE_CLASS, CHAT_MESSAGELIST_EMPTY_MESSAGE_CLASS, CHAT_MESSAGELIST_EMPTY_PROMPT_CLASS, } from '@ts/ui/chat/messagelist'; +import ProgressBar from '@ts/ui/m_progress_bar'; import Popup from '@ts/ui/popup/m_popup'; import { @@ -15,7 +18,7 @@ import { DEFAULT_CHAT_OPTIONS, DEFAULT_POPUP_OPTIONS, } from './const'; -import type { AIChatOptions } from './types'; +import type { AIChatOptions, MessageStatus } from './types'; export class AIChat { private readonly popupInstance: Popup; @@ -50,6 +53,15 @@ export class AIChat { .append($message) .append($prompt); }, + messageTemplate: (data, container): void => { + const { message } = data; + + if (message?.author?.id === AI_ASSISTANT_AUTHOR_ID) { + this.renderMessage(message, container); + } else { + $(container).text(message?.text); + } + }, ...this.options.chatOptions, }; } @@ -95,6 +107,59 @@ export class AIChat { }; } + private renderMessageIcon($parent: dxElementWrapper): void { + $('') + .addClass(`dx-icon dx-icon-sparkle ${CLASSES.messageIcon}`) + .appendTo($parent); + } + + private renderMessageHeader($parent: dxElementWrapper, text: string): void { + $('') + .addClass(CLASSES.messageHeader) + .text(text) + .appendTo($parent); + } + + private renderMessageGroupOperations($parent: dxElementWrapper, status: MessageStatus): void { + switch (status) { + case 'pending': + this.renderPendingOperation($parent); + break; + case 'success': + this.renderSuccessOperations($parent); + break; + case 'error': + this.renderErrorOperations($parent); + break; + default: + break; + } + } + + private renderPendingOperation($parent: dxElementWrapper): void { + $('
') + .addClass(CLASSES.messageGroupOperations) + .text('Processing...') + .appendTo($parent); + + const $progressBar = $('
') + .addClass(CLASSES.messageProgressBar) + .appendTo($parent); + + this.options.createComponent($progressBar, ProgressBar, { + value: false, + visible: true, + showStatus: false, + width: '100%', + }); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private renderSuccessOperations($parent: dxElementWrapper): void {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private renderErrorOperations($parent: dxElementWrapper): void {} + public updateOptions(options: AIChatOptions, updatePopup: boolean, updateChat: boolean): void { this.options = options; @@ -118,4 +183,19 @@ export class AIChat { public isShown(): boolean { return !!this.popupInstance?.option('visible'); } + + public renderMessage(message: Message, container: HTMLElement): void { + const $message = $('
') + .addClass(`${CLASSES.message} ${CLASSES.messagePending}`) + .appendTo(container); + + this.renderMessageIcon($message); + + const $content = $('
') + .addClass(CLASSES.messageContent) + .appendTo($message); + + this.renderMessageHeader($content, message.text); + this.renderMessageGroupOperations($content, 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 c2ba5ace1a2a..d911c83cc66e 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 @@ -19,7 +19,16 @@ export const CLASSES = { aiChat: 'dx-ai-chat', aiDialog: 'dx-aidialog', aiChatContent: 'dx-ai-chat__content', - aiChatEmptyImage: 'dx-ai-chat-messagelist-empty-image', + aiChatEmptyImage: 'dx-ai-chat__empty-image', + message: 'dx-ai-chat__message', + messagePending: 'dx-ai-chat__message--pending', + messageSuccess: 'dx-ai-chat__message--success', + messageError: 'dx-ai-chat__message--error', + messageIcon: 'dx-ai-chat__message-icon', + messageContent: 'dx-ai-chat__message-content', + messageHeader: 'dx-ai-chat__message-header', + messageGroupOperations: 'dx-ai-chat__message-group-operations', + messageProgressBar: 'dx-ai-chat__message-progressbar', }; export const CLEAR_CHAT_ICON = 'clearhistory'; diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_chat/types.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/types.ts index ed3c8bb6aa06..dc7c0ac00779 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_chat/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/types.ts @@ -13,3 +13,5 @@ export interface AIChatOptions { onChatCleared?: () => void; onRegenerate?: () => void; } + +export type MessageStatus = 'pending' | 'success' | 'error';