Skip to content

Commit

Permalink
Added ready and pre-destroy events
Browse files Browse the repository at this point in the history
  • Loading branch information
salmenus committed Feb 11, 2024
1 parent 8b7271d commit 5a93524
Show file tree
Hide file tree
Showing 15 changed files with 409 additions and 30 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,11 @@ cross platforms, with a focus on performance and usability.

## Community & Support 🙏

---

* **Star The Repo** 🌟 ― If you like NLUX, please star the repo to show your support.

---

* **[GitHub Discussions](https://github.com/nluxai/nlux/discussions)** ― Ask questions, report issues, and share your
ideas with the community.
Expand Down
26 changes: 12 additions & 14 deletions packages/js/core/src/components/chat/chat-room/chat-room.model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {BaseComp} from '../../../core/comp/base';
import {comp} from '../../../core/comp/comp';
import {CompEventListener, Model} from '../../../core/comp/decorators';
import {HistoryPayloadSize} from '../../../core/options/conversationOptions';
import {BotPersona, UserPersona} from '../../../core/options/personaOptions';
import {NluxContext} from '../../../types/context';
import {ConversationItem} from '../../../types/conversation';
Expand Down Expand Up @@ -69,6 +70,10 @@ export class CompChatRoom extends BaseComp<
}
}

public getConversationContentForAdapter(historyPayloadSize: HistoryPayloadSize = 'max') {
return this.conversation.getConversationContentForAdapter(historyPayloadSize);
}

public hide() {
this.setProp('visible', false);
}
Expand All @@ -78,6 +83,13 @@ export class CompChatRoom extends BaseComp<
this.promptBoxInstance?.focusTextInput();
}

@CompEventListener('chat-room-ready')
onChatRoomReady(event: MouseEvent) {
this.context.emit('ready', {
aiChatProps: this.context.aiChatProps,
});
}

public setProps(props: Partial<CompChatRoomProps>) {
if (props.hasOwnProperty('containerMaxHeight')) {
this.setProp('containerMaxHeight', props.containerMaxHeight ?? undefined);
Expand Down Expand Up @@ -118,20 +130,6 @@ export class CompChatRoom extends BaseComp<
this.setProp('visible', true);
}

@CompEventListener('show-chat-room-clicked')
showChatRoom(event: MouseEvent) {
if (event.stopPropagation) {
event.stopPropagation();
}

if (event.preventDefault) {
event.preventDefault();
}

this.setProp('visible', true);
this.promptBoxInstance?.focusTextInput();
}

private addConversation(
scrollWhenGenerating: boolean,
streamingAnimationSpeed: number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const renderChatRoom: CompRenderer<

const promptBoxElement = getElement(chatRoomElement, `:scope > .${__('prompt-box-container')}`);
appendToRoot(chatRoomElement);
compEvent('chat-room-ready')();

return {
elements: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {BotPersona, UserPersona} from '../../../core/options/personaOptions';
import {ConversationItem} from '../../../types/conversation';

export type CompChatRoomEvents = 'close-chat-room-clicked'
| 'show-chat-room-clicked'
export type CompChatRoomEvents = 'chat-room-ready'
| 'messages-container-clicked';

export type CompChatRoomProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,6 @@ export const updateChatRoom: CompUpdater<CompChatRoomProps, CompChatRoomElements
newValue,
dom: {elements, actions},
}) => {
if (propName === 'visible') {
if (elements?.chatRoomContainer) {
elements.chatRoomContainer.style.display = newValue ? '' : 'none';
}
return;
}

if (propName === 'containerMaxHeight' && elements?.chatRoomContainer) {
elements.chatRoomContainer.style.maxHeight = typeof newValue === 'number'
? `${newValue}px`
Expand Down
13 changes: 13 additions & 0 deletions packages/js/core/src/core/renderer/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,19 @@ export class NluxRenderer<InboundPayload, OutboundPayload> {
});
}

//
// Trigger pre-destroy event
//
this.context.emit('preDestroy', {
aiChatProps: this.context.aiChatProps,
conversationHistory: this.chatRoom.getConversationContentForAdapter(
this.context.aiChatProps.conversationOptions?.historyPayloadSize,
) ?? [],
});

//
// Destroy UI components
//
if (this.exceptionsBox) {
this.exceptionsBox.destroy();
}
Expand Down
6 changes: 5 additions & 1 deletion packages/js/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ export type {AiChatInternalProps} from './types/props';
export type {
EventName,
EventCallback,
EventsMap,
ErrorCallback,
ErrorEventDetails,
EventsMap,
ReadyCallback,
ReadyEventDetails,
PreDestroyCallback,
PreDestroyEventDetails,
MessageSentCallback,
MessageReceivedCallback,
} from './types/event';
Expand Down
19 changes: 16 additions & 3 deletions packages/js/core/src/types/event.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
import {ExceptionId} from '../exceptions/exceptions';
import {ConversationItem} from './conversation';
import {AiChatProps} from './props';

export type ErrorEventDetails = {
errorId: ExceptionId;
message: string;
};

export type ErrorCallback = (errorDetails: ErrorEventDetails) => void;
export type ReadyEventDetails = {
aiChatProps: AiChatProps;
}

export type MessageReceivedCallback = (message: string) => void;
export type PreDestroyEventDetails = {
aiChatProps: AiChatProps;
conversationHistory: Readonly<ConversationItem[]>;
}

export type ErrorCallback = (errorDetails: ErrorEventDetails) => void;
export type MessageReceivedCallback = (message: string) => void;
export type MessageSentCallback = (message: string) => void;
export type ReadyCallback = (readyDetails: ReadyEventDetails) => void;
export type PreDestroyCallback = (preDestroyDetails: PreDestroyEventDetails) => void;

export type EventsMap = {
error: ErrorCallback;
ready: ReadyCallback;
preDestroy: PreDestroyCallback;
messageSent: MessageSentCallback;
messageReceived: MessageReceivedCallback;
error: ErrorCallback;
};

export type EventName = keyof EventsMap;
Expand Down
6 changes: 5 additions & 1 deletion packages/react/core/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ export type {
export type {
EventName,
EventCallback,
EventsMap,
ErrorCallback,
ErrorEventDetails,
EventsMap,
ReadyCallback,
ReadyEventDetails,
PreDestroyCallback,
PreDestroyEventDetails,
MessageSentCallback,
MessageReceivedCallback,
} from '@nlux/core';
Expand Down
2 changes: 1 addition & 1 deletion pipeline/npm/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ from [`@nlux/themes`](https://www.npmjs.com/package/@nlux/themes) or use the
CDN hosted version from below:

```jsx
<link rel="stylesheet" href="https://themes.nlux.ai/v0.11.0/nova.css"/>
<link rel="stylesheet" href="https://themes.nlux.ai/v0.11.2/nova.css"/>
```

This CDN is provided for demo purposes only and it's not scalable.
Expand Down
2 changes: 1 addition & 1 deletion pipeline/npm/versions.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"inherit": true,
"nlux": "0.11.0",
"nlux": "0.11.2",
"peerDependencies": {
"react": "18.2.0",
"react-dom": "18.2.0"
Expand Down
128 changes: 128 additions & 0 deletions specs/specs/core/events/js/preDestroy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {AiChat, createAiChat} from '@nlux-dev/core/src';
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {adapterBuilder} from '../../../../utils/adapterBuilder';
import {AdapterController} from '../../../../utils/adapters';
import {submit, type} from '../../../../utils/userInteractions';
import {waitForRenderCycle} from '../../../../utils/wait';

describe('When pre-destroy event handler is used with a Vanilla JS Component', () => {
let rootElement: HTMLElement;
let adapterController: AdapterController | undefined = undefined;
let aiChat: AiChat | undefined;

beforeEach(() => {
rootElement = document.createElement('div');
document.body.append(rootElement);
});

afterEach(() => {
adapterController = undefined;
aiChat?.unmount();
rootElement?.remove();
aiChat = undefined;
});

it('should be called when the component is about to get destroyed', async () => {
const preDestroyCallback = vi.fn();

adapterController = adapterBuilder().withFetchText().create();
aiChat = createAiChat()
.withAdapter(adapterController.adapter)
.on('preDestroy', preDestroyCallback);

await waitForRenderCycle();
expect(preDestroyCallback).not.toHaveBeenCalled();

aiChat.mount(rootElement);
await waitForRenderCycle();
expect(preDestroyCallback).not.toHaveBeenCalled();

aiChat.unmount();
await waitForRenderCycle();
expect(preDestroyCallback).toHaveBeenCalledOnce();

expect(preDestroyCallback).toHaveBeenCalledWith({
aiChatProps: expect.objectContaining({
adapter: adapterController.adapter,
}),
conversationHistory: [],
});
});

it('should be called with conversation history and options', async () => {
const preDestroyCallback = vi.fn();

adapterController = adapterBuilder().withFetchText().create();
aiChat = createAiChat()
.withAdapter(adapterController.adapter)
.withClassName('my-class')
.withConversationOptions({
scrollWhenGenerating: false,
})
.withInitialConversation([
{
role: 'user',
message: 'Hello',
},
{
role: 'ai',
message: 'Hi',
},
])
.on('preDestroy', preDestroyCallback);


aiChat.mount(rootElement);
await waitForRenderCycle();

await type('Tell me a joke');
await waitForRenderCycle();

await submit();
await waitForRenderCycle();

adapterController.resolve('Why did the chicken cross the road? To get to the other side.');
await waitForRenderCycle();

aiChat.unmount();
await waitForRenderCycle();

expect(preDestroyCallback).toHaveBeenCalledWith(expect.objectContaining({
aiChatProps: expect.objectContaining({
adapter: adapterController.adapter,
className: 'my-class',
conversationOptions: {
scrollWhenGenerating: false,
},
initialConversation: [
{
role: 'user',
message: 'Hello',
},
{
role: 'ai',
message: 'Hi',
},
],
}),
conversationHistory: [
{
role: 'user',
message: 'Hello',
},
{
role: 'ai',
message: 'Hi',
},
{
role: 'user',
message: 'Tell me a joke',
},
{
role: 'ai',
message: 'Why did the chicken cross the road? To get to the other side.',
},
],
}));
});
});
62 changes: 62 additions & 0 deletions specs/specs/core/events/js/ready.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {AiChat, createAiChat} from '@nlux-dev/core/src';
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {adapterBuilder} from '../../../../utils/adapterBuilder';
import {AdapterController} from '../../../../utils/adapters';
import {waitForRenderCycle} from '../../../../utils/wait';

describe('When ready event handler is used with a Vanilla JS Component', () => {
let rootElement: HTMLElement;
let adapterController: AdapterController | undefined = undefined;
let aiChat: AiChat | undefined;

beforeEach(() => {
rootElement = document.createElement('div');
document.body.append(rootElement);
});

afterEach(() => {
adapterController = undefined;
aiChat?.unmount();
rootElement?.remove();
aiChat = undefined;
});

it('should be called when the component is ready', async () => {
const readyCallback = vi.fn();

adapterController = adapterBuilder().withFetchText().create();
aiChat = createAiChat()
.withAdapter(adapterController.adapter)
.on('ready', readyCallback);

await waitForRenderCycle();
expect(readyCallback).not.toHaveBeenCalled();

aiChat.mount(rootElement);
await waitForRenderCycle();
expect(readyCallback).toHaveBeenCalledOnce();
});

it('should be called with initialisation options', async () => {
const readyCallback = vi.fn();

adapterController = adapterBuilder().withFetchText().create();
aiChat = createAiChat()
.withAdapter(adapterController.adapter)
.on('ready', readyCallback)
.withClassName('test-class');

await waitForRenderCycle();
expect(readyCallback).not.toHaveBeenCalled();

aiChat.mount(rootElement);
await waitForRenderCycle();

expect(readyCallback).toHaveBeenCalledWith(expect.objectContaining({
aiChatProps: {
adapter: adapterController.adapter,
className: 'test-class',
},
}));
});
});
Loading

0 comments on commit 5a93524

Please sign in to comment.