Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
@use "../mixins" as *;
@use 'variables' as *;
@use 'layout/cell' as *;
@use "layout/ai-chat" as *;
@use "layout/aiChat" as *;

// adduse

Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
@use "../button/colors" as *;
@use "../validation/colors" as *;
@use "../typography/sizes" as *;
@use "../../base/gridBase/mixins" as *;

// adduse
@use "../scrollable";
Expand All @@ -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;
Expand Down Expand Up @@ -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,
);
}
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
);
}
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
);
}
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -14,7 +15,8 @@ gridCore.registerModule('aiAssistant', {
};
},
controllers: {
aiAssistant: AIAssistantViewController,
aiAssistant: AIAssistantController,
aiAssistantViewController: AIAssistantViewController,
},
views: {
aiAssistantView: AIAssistantView,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, unknown> = {},
): 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,
}),
]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>('../../ai_chat/ai_chat');
Expand All @@ -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<ProcessedCommands>>()
.mockResolvedValue(mockProcessedCommands),
};

const createAIAssistantView = ({
initialEnabled = true,
render = true,
Expand Down Expand Up @@ -69,7 +79,9 @@ const createAIAssistantView = ({
const mockComponent = {
element: (): any => $container.get(0),
_createComponent: createComponentMock,
_controllers: {},
_controllers: {
aiAssistant: mockAIAssistantController,
},
_views: {
columnHeadersView: mockColumnHeadersView,
rowsView: mockRowsView,
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();
Expand Down
Loading
Loading