Skip to content

Commit

Permalink
Added test for markdown in initialConversation
Browse files Browse the repository at this point in the history
  • Loading branch information
salmenus committed Jun 5, 2024
1 parent 132e0b9 commit 88f3709
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 14 deletions.
8 changes: 7 additions & 1 deletion packages/react/core/src/exports/messageOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ export type ResponseRenderer<AiMsg> = FC<
StreamResponseComponentProps<AiMsg> | BatchResponseComponentProps<AiMsg>
>;

export type BatchResponseRenderer<AiMsg> = FC<BatchResponseComponentProps<AiMsg>>;
export type StreamResponseRenderer<AiMsg> = FC<StreamResponseComponentProps<AiMsg>>;

export type PromptRendererProps = {
uid: string;
prompt: string;
Expand All @@ -62,7 +65,10 @@ export type ReactSpecificMessageOptions<AiMsg> = {
/**
* Custom React component to render the message received from the AI.
*/
responseRenderer?: ResponseRenderer<AiMsg>;
responseRenderer?:
ResponseRenderer<AiMsg> |
FC<BatchResponseComponentProps<AiMsg>> |
FC<StreamResponseComponentProps<AiMsg>>;

/**
* Custom React component to render the message sent by the user.
Expand Down
8 changes: 5 additions & 3 deletions packages/react/core/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,14 @@ export type {
} from './types/conversationOptions';

export type {
MessageOptions,
ResponseRenderer,
BatchResponseRenderer,
BatchResponseComponentProps,
StreamResponseRenderer,
StreamResponseComponentProps,
ResponseRenderer,
PromptRendererProps,
PromptRenderer,
MessageOptions,
PromptRendererProps,
} from './exports/messageOptions';

export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ export const createMessageRenderer: <AiMsg>(
//
// A custom response renderer is provided by the user.
//
if (
messageOptions?.responseRenderer !== undefined) {
if (messageOptions?.responseRenderer !== undefined) {
//
// Streaming into a custom renderer.
//
Expand All @@ -41,7 +40,7 @@ export const createMessageRenderer: <AiMsg>(
containerRef: containerRefToUse as RefObject<never>,
};

return () => (messageOptions?.responseRenderer as FC<StreamResponseComponentProps<AiMsg>>)(props);
return () => (messageOptions.responseRenderer as FC<StreamResponseComponentProps<AiMsg>>)(props);
} else {
//
// Batching data and displaying it in a custom renderer.
Expand All @@ -54,7 +53,7 @@ export const createMessageRenderer: <AiMsg>(
dataTransferMode,
};

return () => (messageOptions?.responseRenderer as FC<BatchResponseComponentProps<AiMsg>>)(props);
return () => (messageOptions.responseRenderer as FC<BatchResponseComponentProps<AiMsg>>)(props);
}
}

Expand All @@ -78,6 +77,7 @@ export const createMessageRenderer: <AiMsg>(
htmlSanitizer: messageOptions?.htmlSanitizer,
markdownLinkTarget: messageOptions?.markdownLinkTarget,
showCodeBlockCopyButton: messageOptions?.showCodeBlockCopyButton,
skipStreamingAnimation: messageOptions?.skipStreamingAnimation,
}}
/>
);
Expand Down
6 changes: 3 additions & 3 deletions packages/react/core/src/sections/StreamContainer/props.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {HighlighterExtension, SanitizerExtension} from '@nlux/core';
import {MessageDirection} from '@shared/components/Message/props';
import {ResponseRenderer} from '../../exports/messageOptions';
import {BatchResponseRenderer, ResponseRenderer, StreamResponseRenderer} from '../../exports/messageOptions';

export type StreamContainerProps<AisMsg> = {
uid: string,
direction: MessageDirection,
status: 'streaming' | 'complete';
initialMarkdownMessage?: string;
responseRenderer?: ResponseRenderer<AisMsg>;
promptRenderer?: ResponseRenderer<AisMsg>;
responseRenderer?: ResponseRenderer<AisMsg> | BatchResponseRenderer<AisMsg> | StreamResponseRenderer<AisMsg>;
promptRenderer?: BatchResponseRenderer<AisMsg>;
markdownOptions?: {
syntaxHighlighter?: HighlighterExtension;
htmlSanitizer?: SanitizerExtension;
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/types/markdown/snapshotParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type SnapshotParserOptions = {
htmlSanitizer?: SanitizerExtension;
markdownLinkTarget?: 'blank' | 'self';
showCodeBlockCopyButton?: boolean;
skipStreamingAnimation?: boolean;
};

export type SnapshotParser = (
Expand All @@ -15,5 +16,6 @@ export type SnapshotParser = (
htmlSanitizer?: SanitizerExtension;
markdownLinkTarget?: 'blank' | 'self',
showCodeBlockCopyButton?: boolean;
skipStreamingAnimation?: boolean;
},
) => string;
4 changes: 4 additions & 0 deletions samples/aiChat/react/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ function App() {
+ 'This is a code block.';

const initialConversation: ChatItem<string>[] = [
{
role: 'assistant',
message: 'Hello, **how can I help you?**\n# AI speaking\nCheers!',
},
{
role: 'assistant',
message: longMessage,
Expand Down
34 changes: 33 additions & 1 deletion specs/specs/aiChat/options/react/initialConversation.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {render} from '@testing-library/react';
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
import {adapterBuilder} from '../../../../utils/adapterBuilder';
import {AdapterController} from '../../../../utils/adapters';
import {waitForMilliseconds, waitForReactRenderCycle} from '../../../../utils/wait';
import {waitForMdStreamToComplete, waitForMilliseconds, waitForReactRenderCycle} from '../../../../utils/wait';

describe('<AiChat /> + initialConversation prop', () => {
let adapterController: AdapterController | undefined;
Expand Down Expand Up @@ -81,6 +81,38 @@ describe('<AiChat /> + initialConversation prop', () => {
expect(receivedMessage.textContent).toEqual(expect.stringContaining('We find connection, hand in hand'));
});

it('The markdown in assistant responses should be parsed', async () => {
// Arrange
const initialConversation: ChatItem<string>[] = [
{
role: 'assistant',
message: 'Hello, **how can I help you?**\n# AI speaking\nCheers!',
},
{
role: 'user', message: 'I need help with my account.',
},
];

const aiChat = (
<AiChat
adapter={adapterController!.adapter}
initialConversation={initialConversation}
/>
);

render(aiChat);
await waitForMdStreamToComplete();

// Act
const aiChatDom = document.querySelector('.nlux-AiChat-root')!;

// Assert
const receivedMessage = aiChatDom.querySelector('.nlux_msg_received')!;
expect(receivedMessage.innerHTML).toEqual(expect.stringContaining(
'<p>Hello, <strong>how can I help you?</strong></p>\n<h1>AI speaking</h1>\n<p>Cheers!</p>',
));
});

it('The scroll should be at the bottom of the chat', async () => {
// Arrange
const initialConversation: ChatItem<string>[] = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {AiChat, BatchResponseComponentProps} from '@nlux-dev/react/src';
import {render} from '@testing-library/react';
import {FC} from 'react';
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
import {adapterBuilder} from '../../../utils/adapterBuilder';
import {AdapterController} from '../../../utils/adapters';
import {waitForReactRenderCycle} from '../../../utils/wait';

describe('<AiChat /> + responseRenderer + initialConversation', () => {
let adapterController: AdapterController | undefined = undefined;

describe('When a response renderer is used with initial conversation', () => {
beforeEach(() => {
adapterController = adapterBuilder()
.withBatchText(true)
.withStreamText(false)
.create();
});

afterEach(() => {
adapterController = undefined;
});

it('Should render the custom component for the initial conversation messages', async () => {
// Arrange
const CustomResponseComponent: FC<BatchResponseComponentProps<string>> = ({content}) => (
<div>The AI response is: {content}</div>
);


// Act
const {container} = render(
<AiChat
adapter={adapterController!.adapter}
initialConversation={[
{role: 'user', message: 'Hello'},
{role: 'assistant', message: 'Hi. How can I help you?'},
]}
messageOptions={{
responseRenderer: CustomResponseComponent,
}}
/>,
);
await waitForReactRenderCycle();

// Assert
const responseElement = container.querySelector('.nlux-comp-chatItem--received');
expect(responseElement!.innerHTML).toEqual(
expect.stringContaining('<div>The AI response is: Hi. How can I help you?</div>'),
);
});
});
});
2 changes: 1 addition & 1 deletion specs/specs/markdown/12-hyperlink.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {createMdStreamRenderer} from '@shared/markdown/stream/streamParser';
import {StandardStreamParserOutput} from '@shared/types/markdown/streamParser';
import {waitForMdStreamToComplete} from '../../utils/wait';

describe('Asterisk Italic Markdowns Parser', () => {
describe('Hyperlinks Markdowns Parser', () => {
let streamRenderer: StandardStreamParserOutput;
let rootElement: HTMLElement;

Expand Down
2 changes: 1 addition & 1 deletion specs/utils/wait.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ export const waitForMilliseconds = (milliseconds: number) => new Promise(resolve
});

export const waitForMdStreamToComplete = (streamLength: number = 20) => new Promise(resolve => {
const duration = streamLength * 5; // 10ms wait per character
const duration = streamLength * 10; // 10ms wait per character
setTimeout(resolve, duration);
});

0 comments on commit 88f3709

Please sign in to comment.