UNPKG

remix-nlux

Version:

Remix IDE NLUX integration. Remix IDE is the leading IDE for building and deploying smart contracts on Ethereum. NLUX is a JavaScript and React library for building conversational AI experiences.

489 lines (409 loc) 18.9 kB
import {ChatItem} from '@shared/types/conversation'; import {NluxRenderingError} from '@shared/types/error'; import {NLErrorId} from '@shared/types/exceptions/errors'; import {emptyInnerHtml} from '@shared/utils/dom/emptyInnerHtml'; import {getConversationLayout} from '@shared/utils/dom/getConversationLayout'; import {getRootClassNames, getSystemColorScheme} from '@shared/utils/dom/getRootClassNames'; import {warn} from '@shared/utils/warn'; import {CompChatRoom} from '../../sections/chat/chatRoom/chatRoom.model'; import {CompChatRoomProps} from '../../sections/chat/chatRoom/chatRoom.types'; import {CompExceptionsBox} from '../../sections/miscellaneous/exceptionsBox/model'; import {AiChatInternalProps, UpdatableAiChatProps} from '../../types/aiChat/props'; import {ControllerContext} from '../../types/controllerContext'; import {propsToCorePropsInEvents} from '../../utils/propsToCorePropsInEvents'; import {comp} from '../comp/comp'; import {CompRegistry} from '../comp/registry'; import {ComposerOptions} from '../options/composerOptions'; import {ConversationOptions} from '../options/conversationOptions'; import {DisplayOptions} from '../options/displayOptions'; import {MessageOptions} from '../options/messageOptions'; import {PersonaOptions} from '../options/personaOptions'; export class NluxRenderer<AiMsg> { private readonly __context: ControllerContext<AiMsg>; private chatRoom: CompChatRoom<AiMsg> | null = null; private exceptionsBox: CompExceptionsBox<AiMsg> | null = null; private isDestroyed: boolean = false; private isMounted: boolean = false; private rootCompId: string | null = null; private rootElement: HTMLElement | null = null; private rootElementInitialClassName: string | null; private theClassName: string | null = null; private theComposerOptions: ComposerOptions = {}; private theConversationOptions: ConversationOptions = {}; private theDisplayOptions: DisplayOptions = {}; private theInitialConversationContent: ChatItem<AiMsg>[] | null = null; private theMessageOptions: MessageOptions<AiMsg> = {}; private thePersonasOptions: PersonaOptions = {}; constructor( context: ControllerContext<AiMsg>, rootCompId: string, rootElement: HTMLElement, props: AiChatInternalProps<AiMsg> | null = null, ) { if (!rootCompId) { throw new NluxRenderingError({ source: this.constructor.name, message: 'Root component ID is not a valid component name', }); } this.__context = context; this.rootElement = rootElement; this.rootElementInitialClassName = rootElement.className; this.rootCompId = rootCompId; this.chatRoom = null; this.theClassName = props?.className ?? null; this.theDisplayOptions = props?.displayOptions ?? {}; this.theConversationOptions = props?.conversationOptions ?? {}; this.theMessageOptions = props?.messageOptions ?? {}; this.theInitialConversationContent = props?.initialConversation ?? null; this.theComposerOptions = props?.composerOptions ?? {}; this.thePersonasOptions = props?.personaOptions ?? {}; } public get className(): string | undefined { return this.theClassName ?? undefined; } public get colorScheme(): 'light' | 'dark' | 'auto' | undefined { return this.theDisplayOptions.colorScheme; } get context() { return this.__context; } public get mounted() { return this.isMounted; } public get themeId() { return this.theDisplayOptions.themeId; } destroy() { if (this.mounted) { this.unmount(); } if (this.chatRoom) { this.chatRoom.destroy(); } this.rootElement = null; this.rootElementInitialClassName = null; this.rootCompId = null; this.chatRoom = null; this.isMounted = false; this.isDestroyed = true; this.theDisplayOptions = {}; this.theConversationOptions = {}; this.theComposerOptions = {}; } hide() { if (!this.mounted) { throw new NluxRenderingError({ source: this.constructor.name, message: 'Renderer is not mounted and cannot be hidden', }); } this.chatRoom?.hide(); } mount() { if (this.isDestroyed) { throw new NluxRenderingError({ source: this.constructor.name, message: 'Renderer is destroyed and cannot be mounted', }); } if (!this.rootCompId || !this.rootElement) { throw new NluxRenderingError({ source: this.constructor.name, message: 'Root component or root class is not set', }); } let rootComp: CompChatRoom<AiMsg> | null = null; let exceptionAlert: CompExceptionsBox<AiMsg> | null = null; try { // Root component can only be a chat room component. if (this.rootCompId !== 'chatRoom') { throw new NluxRenderingError({ source: this.constructor.name, message: 'Root component is not a chat room', }); } // // IMPORTANT ✨ This is where the CompChatRoom is instantiated! // rootComp = comp(CompChatRoom<AiMsg>) .withContext(this.context) .withProps<CompChatRoomProps<AiMsg>>({ visible: true, conversationLayout: getConversationLayout(this.theConversationOptions.layout), assistantPersona: this.thePersonasOptions?.assistant ?? undefined, userPersona: this.thePersonasOptions?.user ?? undefined, conversationStarters: this.theConversationOptions?.conversationStarters ?? undefined, showGreeting: this.theConversationOptions?.showWelcomeMessage, initialConversationContent: this.theInitialConversationContent ?? undefined, autoScroll: this.theConversationOptions?.autoScroll, syntaxHighlighter: this.context.syntaxHighlighter, htmlSanitizer: this.context.htmlSanitizer, markdownLinkTarget: this.theMessageOptions?.markdownLinkTarget, showCodeBlockCopyButton: this.theMessageOptions?.showCodeBlockCopyButton, streamingAnimationSpeed: this.theMessageOptions?.streamingAnimationSpeed, skipStreamingAnimation: this.theMessageOptions?.skipStreamingAnimation, composer: { placeholder: this.theComposerOptions?.placeholder, autoFocus: this.theComposerOptions?.autoFocus, disableSubmitButton: this.theComposerOptions?.disableSubmitButton, submitShortcut: this.theComposerOptions?.submitShortcut, }, }).create(); const CompExceptionsBoxConstructor = CompRegistry.retrieve( 'exceptionsBox', )?.model as typeof CompExceptionsBox | undefined; if (CompExceptionsBoxConstructor) { exceptionAlert = comp(CompExceptionsBox<AiMsg>) .withContext(this.context) .create(); } else { warn('Exception alert component is not registered! No exceptions will be shown.'); } if (!rootComp) { throw new NluxRenderingError({ source: this.constructor.name, message: 'Root component failed to instantiate', }); } this.setRootElementClassNames(); this.setRoomElementDimensions(this.theDisplayOptions); if (exceptionAlert) { exceptionAlert.render(this.rootElement); } rootComp.render(this.rootElement); if (rootComp && !rootComp.rendered) { this.rootElement.className = this.rootElementInitialClassName || ''; throw new NluxRenderingError({ source: this.constructor.name, message: 'Root component did not render', }); } else { this.chatRoom = rootComp; this.exceptionsBox = exceptionAlert ?? null; this.isMounted = true; } } catch (_error) { this.rootElement.className = this.rootElementInitialClassName || ''; this.renderEx('failed-to-render-content', _error?.toString() ?? 'Unknown error'); } } public renderEx(type: NLErrorId, message: string) { if (!this.mounted) { warn('Renderer is not mounted and cannot render exceptions'); throw new NluxRenderingError({ source: this.constructor.name, message: message, }); } this.exceptionsBox?.showAlert(type, message); } show() { if (!this.mounted) { throw new NluxRenderingError({ source: this.constructor.name, message: 'Renderer is not mounted and cannot be shown', }); } this.chatRoom?.show(); } unmount() { if (this.isDestroyed) { warn('Renderer is destroyed and cannot be unmounted'); return; } if (!this.chatRoom || !this.rootElement) { throw new NluxRenderingError({ source: this.constructor.name, message: 'Root component or root element is not set', }); } // // Trigger pre-destroy event // this.context.emit('preDestroy', { aiChatProps: propsToCorePropsInEvents(this.context.aiChatProps), conversationHistory: this.chatRoom.getConversationContentForAdapter( this.context.aiChatProps.conversationOptions?.historyPayloadSize, ) ?? [], }); // // Destroy UI components // if (this.exceptionsBox) { this.exceptionsBox.destroy(); } this.chatRoom.destroy(); if (this.rootElement) { emptyInnerHtml(this.rootElement); this.rootElement.className = this.rootElementInitialClassName || ''; } this.chatRoom = null; this.exceptionsBox = null; this.isMounted = false; } public updateProps(props: UpdatableAiChatProps<AiMsg>) { if (props.hasOwnProperty('adapter') && props.adapter) { this.context.update({ adapter: props.adapter, }); } let classNameUpdated = false; if (props.hasOwnProperty('className') && props.className !== this.className) { this.theClassName = props.className ?? null; classNameUpdated = true; } let themeIdUpdated = false; let colorSchemeUpdated = false; if (props.hasOwnProperty('displayOptions')) { const newDisplayOptions: Partial<DisplayOptions> = {}; if (props.displayOptions?.themeId !== this.theDisplayOptions.themeId) { newDisplayOptions.themeId = props.displayOptions?.themeId; themeIdUpdated = true; } if (props.displayOptions?.colorScheme !== this.theDisplayOptions.colorScheme) { newDisplayOptions.colorScheme = props.displayOptions?.colorScheme; colorSchemeUpdated = true; } if (props.displayOptions?.height !== this.theDisplayOptions.height) { newDisplayOptions.height = props.displayOptions?.height; } if (props.displayOptions?.width !== this.theDisplayOptions.width) { newDisplayOptions.width = props.displayOptions?.width; } if (Object.keys(newDisplayOptions).length > 0) { const displayOptions: DisplayOptions = { ...this.theDisplayOptions, ...newDisplayOptions, }; this.theDisplayOptions = displayOptions; this.setRoomElementDimensions(displayOptions); } } if (themeIdUpdated || colorSchemeUpdated || classNameUpdated) { this.setRootElementClassNames(); } if (props.hasOwnProperty('conversationOptions')) { const newConversationOptions: Partial<ConversationOptions> = {}; const newProps: Partial<CompChatRoomProps<AiMsg>> = {}; if (props.conversationOptions?.layout !== this.theConversationOptions.layout) { newConversationOptions.layout = props.conversationOptions?.layout; newProps.conversationLayout = getConversationLayout(props.conversationOptions?.layout); } if (props.conversationOptions?.autoScroll !== this.theConversationOptions.autoScroll) { newConversationOptions.autoScroll = props.conversationOptions?.autoScroll; newProps.autoScroll = props.conversationOptions?.autoScroll; } if (props.conversationOptions?.showWelcomeMessage !== this.theConversationOptions.showWelcomeMessage) { newConversationOptions.showWelcomeMessage = props.conversationOptions?.showWelcomeMessage; newProps.showGreeting = 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, ...newConversationOptions, }; this.chatRoom?.setProps(newProps); } } if (props.hasOwnProperty('messageOptions')) { this.theMessageOptions = props.messageOptions ?? {}; this.chatRoom?.setProps({ streamingAnimationSpeed: props.messageOptions?.streamingAnimationSpeed ?? undefined, markdownLinkTarget: props.messageOptions?.markdownLinkTarget ?? undefined, syntaxHighlighter: props.messageOptions?.syntaxHighlighter, htmlSanitizer: props.messageOptions?.htmlSanitizer, }); this.context.update({ syntaxHighlighter: props.messageOptions?.syntaxHighlighter, htmlSanitizer: props.messageOptions?.htmlSanitizer, }); } if (props.hasOwnProperty('composerOptions')) { const changedComposerOptions: Partial<ComposerOptions> = {}; if ( props.composerOptions?.hasOwnProperty('placeholder') && props.composerOptions.placeholder !== this.theComposerOptions.placeholder ) { changedComposerOptions.placeholder = props.composerOptions.placeholder; } if ( props.composerOptions?.hasOwnProperty('autoFocus') && props.composerOptions.autoFocus !== this.theComposerOptions.autoFocus ) { changedComposerOptions.autoFocus = props.composerOptions.autoFocus; } if ( props.composerOptions?.hasOwnProperty('disableSubmitButton') && props.composerOptions.disableSubmitButton !== this.theComposerOptions.disableSubmitButton ) { changedComposerOptions.disableSubmitButton = props.composerOptions.disableSubmitButton; } if ( props.composerOptions?.hasOwnProperty('submitShortcut') && props.composerOptions.submitShortcut !== this.theComposerOptions.submitShortcut ) { changedComposerOptions.submitShortcut = props.composerOptions.submitShortcut; } if (Object.keys(changedComposerOptions).length > 0) { this.theComposerOptions = { ...this.theComposerOptions, ...changedComposerOptions, }; this.chatRoom?.setProps({ composer: this.theComposerOptions, }); } } if (props.hasOwnProperty('personaOptions')) { const changedPersonaProps: Partial<CompChatRoomProps<AiMsg>> = {}; if (props.personaOptions?.assistant !== this.thePersonasOptions?.assistant) { changedPersonaProps.assistantPersona = props.personaOptions?.assistant ?? undefined; } if (props.personaOptions?.user !== this.thePersonasOptions?.user) { changedPersonaProps.userPersona = props.personaOptions?.user ?? undefined; } this.thePersonasOptions = { ...this.thePersonasOptions, ...props.personaOptions, }; this.chatRoom?.setProps(changedPersonaProps); } } private setRoomElementDimensions(newDimensions: { width?: number | string; height?: number | string; }) { if (!this.rootElement) { return; } if (newDimensions.hasOwnProperty('width')) { this.rootElement.style.width = (typeof newDimensions.width === 'number') ? `${newDimensions.width}px` : ( typeof newDimensions.width === 'string' ? newDimensions.width : '' ); } if (newDimensions.hasOwnProperty('height')) { this.rootElement.style.height = (typeof newDimensions.height === 'number') ? `${newDimensions.height}px` : ( typeof newDimensions.height === 'string' ? newDimensions.height : '' ); } } private setRootElementClassNames() { if (!this.rootElement) { return; } const rootClassNames = getRootClassNames({ themeId: this.themeId, className: this.className, }); this.rootElement.className = ''; this.rootElement.classList.add(...rootClassNames); this.rootElement.dataset.colorScheme = (this.colorScheme === 'auto' || !this.colorScheme) ? getSystemColorScheme() : this.colorScheme; } }