Skip to content

Commit

Permalink
Added conversationStarters option and vanilla HTML implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
salmenus committed Jun 2, 2024
1 parent af6b2ee commit 1a8791e
Show file tree
Hide file tree
Showing 31 changed files with 291 additions and 142 deletions.
2 changes: 1 addition & 1 deletion packages/js/core/src/aiChat/options/composerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface ComposerOptions {

/**
* This will override the disabled state of the submit button when the composer is in 'typing' status.
* It will not have any impact in the composer 'submitting' and 'waiting' statuses, as the submit button
* It will not have any impact in the composer 'submitting-prompt' and 'waiting' statuses, as the submit button
* is always disabled in these statuses.
*
* @default: Submit button is only enabled when the message is not empty.
Expand Down
9 changes: 9 additions & 0 deletions packages/js/core/src/aiChat/options/conversationOptions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {ConversationStarter} from '../../types/conversationStarter';

export type HistoryPayloadSize = number | 'max';

export type ConversationLayout = 'bubbles' | 'list';
Expand Down Expand Up @@ -42,4 +44,11 @@ export interface ConversationOptions {
* When no assistant persona is provided, the welcome message will be the NLUX logo.
*/
showWelcomeMessage?: boolean;


/**
* Suggested prompts to display in the UI to help the user start a conversation.
* Conversation starters are only displayed when the conversation is empty, and no conversation history is present.
*/
conversationStarters?: ConversationStarter[];
}
6 changes: 6 additions & 0 deletions packages/js/core/src/aiChat/renderer/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export class NluxRenderer<AiMsg> {
conversationLayout: getConversationLayout(this.theConversationOptions.layout),
assistantPersona: this.thePersonasOptions?.assistant ?? undefined,
userPersona: this.thePersonasOptions?.user ?? undefined,
conversationStarters: this.theConversationOptions?.conversationStarters ?? undefined,
showWelcomeMessage: this.theConversationOptions?.showWelcomeMessage,
initialConversationContent: this.theInitialConversationContent ?? undefined,
autoScroll: this.theConversationOptions?.autoScroll,
Expand Down Expand Up @@ -355,6 +356,11 @@ export class NluxRenderer<AiMsg> {
newProps.showWelcomeMessage = props.conversationOptions?.showWelcomeMessage;
}

if (props.conversationOptions?.conversationStarters !== this.theConversationOptions.conversationStarters) {
newConversationOptions.conversationStarters = props.conversationOptions?.conversationStarters;
newProps.conversationStarters = props.conversationOptions?.conversationStarters;
}

if (Object.keys(newConversationOptions).length > 0) {
this.theConversationOptions = {
...this.theConversationOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const submitPromptFactory = <
try {
// Disable prompt while sending message
const currentComposerProps = composerInstance.getProp('domCompProps');
composerInstance.setDomProps({...currentComposerProps, status: 'submitting'});
composerInstance.setDomProps({...currentComposerProps, status: 'submitting-prompt'});

// Build request and submit prompt
const extras: ChatAdapterExtras<AiMsg> = {
Expand Down
17 changes: 17 additions & 0 deletions packages/js/core/src/sections/chat/chatRoom/chatRoom.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class CompChatRoom<AiMsg> extends BaseComp<
assistantPersona,
userPersona,
showWelcomeMessage,
conversationStarters,
initialConversationContent,
syntaxHighlighter,
htmlSanitizer,
Expand All @@ -54,6 +55,7 @@ export class CompChatRoom<AiMsg> extends BaseComp<
streamingAnimationSpeed,
assistantPersona,
userPersona,
conversationStarters,
showWelcomeMessage,
initialConversationContent,
composer,
Expand Down Expand Up @@ -232,6 +234,7 @@ export class CompChatRoom<AiMsg> extends BaseComp<
streamingAnimationSpeed: this.getProp('streamingAnimationSpeed') as number | undefined,
syntaxHighlighter: this.getProp('syntaxHighlighter') as HighlighterExtension | undefined,
htmlSanitizer: this.getProp('htmlSanitizer') as ((html: string) => string) | undefined,
onConversationStarterClick: this.handleConversationStarterClick,
})
.create();

Expand Down Expand Up @@ -267,6 +270,20 @@ export class CompChatRoom<AiMsg> extends BaseComp<
this.addSubComponent(this.composerInstance.id, this.composerInstance, 'composerContainer');
}

private handleConversationStarterClick = (conversationStarter: ConversationStarter) => {
// Set the prompt
// Disable the composer and submit button
// Set the composer as waiting
// Submit the prompt

this.composerInstance.setDomProps({
status: 'submitting-conversation-starter',
});

this.composerInstance.handleTextChange(conversationStarter.prompt);
this.composerInstance.handleSendButtonClick();
};

private handleComposerSubmit() {
const composerProps: Partial<ComposerProps> | undefined = this.props.composer;
submitPromptFactory({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ export class CompChatSegment<AiMsg> extends BaseComp<
this.setProp(newProp, newValue);
}

@CompEventListener('loader-shown')
onLoaderShown(loader: HTMLElement) {
if (this.renderedDom?.elements) {
this.renderedDom.elements.loaderContainer = loader;
}
}

protected setProp<K extends keyof CompChatSegmentProps>(key: K, value: CompChatSegmentProps[K]) {
super.setProp(key, value);

Expand Down Expand Up @@ -204,11 +211,4 @@ export class CompChatSegment<AiMsg> extends BaseComp<
this.renderedDom.elements.loaderContainer = undefined;
}
}

@CompEventListener('loader-shown')
private onLoaderShown(loader: HTMLElement) {
if (this.renderedDom?.elements) {
this.renderedDom.elements.loaderContainer = loader;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
ChatSegment,
ChatSegmentItem,
ChatSegmentStatus,
} from '@shared/types/chatSegment/chatSegment';
import {ChatSegment, ChatSegmentItem, ChatSegmentStatus} from '@shared/types/chatSegment/chatSegment';
import {ChatItem} from '@shared/types/conversation';
import {chatSegmentsToChatItems} from '@shared/utils/chat/chatSegmentsToChatItems';
import {debug} from '@shared/utils/debug';
Expand All @@ -26,18 +22,23 @@ import {
CompConversationProps,
} from './conversation.types';
import {updateConversation} from './conversation.update';
import {CompConversationStarters} from '../conversationStarters/conversationStarters.model';
import {CompConversationStartersProps} from '../conversationStarters/conversationStarters.types';

@Model('conversation', renderConversation, updateConversation)
export class CompConversation<AiMsg> extends BaseComp<
AiMsg, CompConversationProps<AiMsg>, CompConversationElements, CompConversationEvents, CompConversationActions
> {
private chatSegmentCompIdsByIndex: string[] = [];
private chatSegmentComponentsById: Map<string, CompChatSegment<AiMsg>> = new Map();
private conversationStartersComp: CompConversationStarters<AiMsg> | undefined;

constructor(context: ControllerContext<AiMsg>, props: CompConversationProps<AiMsg>) {
super(context, props);
if (props.messages && props.messages.length > 0) {
this.addChatSegment('complete', props.messages);
} else {
this.setConversationStarters(props.conversationStarters);
}
}

Expand All @@ -56,12 +57,48 @@ export class CompConversation<AiMsg> extends BaseComp<
chatSegment.addChatItem(item);
}

public setConversationStarters(conversationStarters: ConversationStarter[] | undefined) {
if (!conversationStarters && !this.conversationStartersComp) {
return;
}

if (conversationStarters && !this.conversationStartersComp) {
this.conversationStartersComp = comp(CompConversationStarters<AiMsg>)
.withContext(this.context)
.withProps({
conversationStarters,
onConversationStarterClick: this.getProp('onConversationStarterClick') as CompConversationProps<AiMsg>['onConversationStarterClick'],
} satisfies CompConversationStartersProps)
.create();

this.addSubComponent(
this.conversationStartersComp.id,
this.conversationStartersComp,
'conversationStartersContainer',
);

return;
}

if (!conversationStarters && this.conversationStartersComp) {
this.removeSubComponent(this.conversationStartersComp.id);
this.conversationStartersComp = undefined;
} else {
this.conversationStartersComp?.updateConversationStarters(conversationStarters);
}
}

public addChatSegment(
status: ChatSegmentStatus = 'active',
initialConversation?: ChatItem<AiMsg>[],
) {
this.throwIfDestroyed();

if (this.conversationStartersComp) {
this.removeSubComponent(this.conversationStartersComp.id);
this.conversationStartersComp = undefined;
}

const segmentId = uid();
const newChatSegmentComp: CompChatSegment<AiMsg> = comp(CompChatSegment<AiMsg>)
.withContext(this.context)
Expand Down Expand Up @@ -216,6 +253,7 @@ export class CompConversation<AiMsg> extends BaseComp<

if (this.chatSegmentCompIdsByIndex.length === 0) {
this.executeDomAction('resetWelcomeMessage');
this.resetConversationStarters();
}
}

Expand All @@ -235,11 +273,6 @@ export class CompConversation<AiMsg> extends BaseComp<
}
}

public setConversationStarters(conversationStarters: ConversationStarter[] | undefined) {
this.setProp('conversationStarters', conversationStarters);
this.executeDomAction('updateConversationStarters', conversationStarters);
}

public setConversationLayout(layout: ConversationLayout) {
this.setProp('conversationLayout', layout);
this.chatSegmentComponentsById.forEach((comp) => {
Expand Down Expand Up @@ -277,4 +310,9 @@ export class CompConversation<AiMsg> extends BaseComp<
});
}
}

private resetConversationStarters() {
const conversationStarters = this.getProp('conversationStarters') as ConversationStarter[] | undefined;
this.setConversationStarters(conversationStarters);
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import {AnyAiMsg} from '@shared/types/anyAiMsg';
import {NluxRenderingError} from '@shared/types/error';
import {createDefaultWelcomeMessageDom} from '@shared/components/DefaultWelcomeMessage/create';
import {createWelcomeMessageDom} from '@shared/components/WelcomeMessage/create';
import {AssistantPersona, UserPersona} from '../../../aiChat/options/personaOptions';
import {CompRenderer} from '../../../types/comp';
import {ConversationStarter} from '../../../types/conversationStarter';
import {source} from '../../../utils/source';
import {
CompConversationActions,
CompConversationElements,
CompConversationEvents,
CompConversationProps,
} from './conversation.types';
import {createConversationStartersDom} from './utils/createConversationStartersDom';

export const renderConversation: CompRenderer<
CompConversationProps<AnyAiMsg>, CompConversationElements, CompConversationEvents, CompConversationActions
Expand All @@ -29,28 +26,23 @@ export const renderConversation: CompRenderer<
welcomeMessageContainer: HTMLElement | undefined;
conversationStartersContainer: HTMLElement | undefined;
shouldRenderWelcomeMessage: boolean;
shouldRenderConversationStarters: boolean;
} = {
assistantPersona: props.assistantPersona,
userPersona: props.userPersona,
conversationStarters: props.conversationStarters,
welcomeMessageContainer: undefined,
conversationStartersContainer: undefined,
shouldRenderWelcomeMessage: hasNoMessages && props.showWelcomeMessage !== false,
shouldRenderConversationStarters: hasNoMessages && Array.isArray(props.conversationStarters) && props.conversationStarters.length > 0,
};

const segmentsContainer = document.createElement('div');
const segmentsContainer = document.createElement('div') as HTMLElement;
segmentsContainer.classList.add('nlux-chtRm-cnv-sgmts-cntr');

if (!(segmentsContainer instanceof HTMLElement)) {
throw new NluxRenderingError({
source: source('chatRoom', 'render'),
message: 'Conversation component could not be rendered',
});
}
const conversationStartersContainer = document.createElement('div') as HTMLElement;
conversationStartersContainer.classList.add('nlux-comp-convStrts-cntr');

appendToRoot(segmentsContainer);
appendToRoot(conversationStartersContainer);

//
// Create welcome message container
Expand All @@ -71,31 +63,14 @@ export const renderConversation: CompRenderer<
}

if (renderingContext.welcomeMessageContainer) {
segmentsContainer.insertAdjacentElement('beforebegin', renderingContext.welcomeMessageContainer);
segmentsContainer.insertAdjacentElement('afterend', renderingContext.welcomeMessageContainer);
}
}

//
// Create conversation starters container
// and append it to the root if conversation starters are provided
//
if (renderingContext.shouldRenderConversationStarters) {
const conversationStartersContainer = createConversationStartersDom(props.conversationStarters!);
renderingContext.conversationStartersContainer = conversationStartersContainer;
segmentsContainer.insertAdjacentElement('beforebegin', conversationStartersContainer);
}

// Function to remove conversation starters
const removeConversationStarters = () => {
if (renderingContext.conversationStartersContainer) {
renderingContext.conversationStartersContainer.remove();
renderingContext.conversationStartersContainer = undefined;
}
};

return {
elements: {
segmentsContainer,
conversationStartersContainer,
},
actions: {
removeWelcomeMessage: () => {
Expand All @@ -120,7 +95,7 @@ export const renderConversation: CompRenderer<

if (renderingContext.welcomeMessageContainer) {
segmentsContainer.insertAdjacentElement(
'beforebegin',
'afterend',
renderingContext.welcomeMessageContainer,
);
}
Expand All @@ -146,7 +121,7 @@ export const renderConversation: CompRenderer<

if (renderingContext.welcomeMessageContainer) {
segmentsContainer.insertAdjacentElement(
'beforebegin',
'afterend',
renderingContext.welcomeMessageContainer,
);
}
Expand All @@ -155,22 +130,6 @@ export const renderConversation: CompRenderer<
updateUserPersona: (newValue: UserPersona | undefined) => {
renderingContext.userPersona = newValue;
},
updateConversationStarters: (conversationStarters: ConversationStarter[] | undefined) => {
// Reset conversation starters
renderingContext.conversationStarters = conversationStarters;
removeConversationStarters();

if (!conversationStarters || conversationStarters.length === 0) {
renderingContext.shouldRenderConversationStarters = false;
return;
}

const conversationStartersContainer = createConversationStartersDom(conversationStarters);
segmentsContainer.insertAdjacentElement('beforebegin', conversationStartersContainer);

renderingContext.conversationStartersContainer = conversationStartersContainer;
renderingContext.shouldRenderConversationStarters = true;
},
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,17 @@ export type CompConversationProps<AiMsg> = {
showCodeBlockCopyButton?: boolean;
skipStreamingAnimation?: boolean;
streamingAnimationSpeed?: number;
onConversationStarterClick: (conversationStarter: ConversationStarter) => void;
};

export type CompConversationElements = {
segmentsContainer: HTMLElement;
conversationStartersContainer: HTMLElement;
};

export type CompConversationActions = {
removeWelcomeMessage: () => void;
resetWelcomeMessage: () => void;
updateAssistantPersona: (newAssistantPersona: AssistantPersona | undefined) => void;
updateUserPersona: (newUserPersona: UserPersona | undefined) => void;
updateConversationStarters: (conversationStarters?: ConversationStarter[]) => void;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const createConversationStartersDom = (conversationStarters: Conversation
conversationStartersContainer.classList.add('nlux-comp-convStrts');

conversationStarters.forEach((item, index) => {
const conversationStarter = document.createElement('div');
const conversationStarter = document.createElement('button');
conversationStarter.classList.add('nlux-comp-convStrt');
conversationStarter.textContent = item.prompt;
conversationStartersContainer.appendChild(conversationStarter);
Expand Down
Loading

0 comments on commit 1a8791e

Please sign in to comment.