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.

425 lines (359 loc) 16.2 kB
import {ChatAdapter} from '@shared/types/adapters/chat/chatAdapter'; import {ChatAdapterBuilder} from '@shared/types/adapters/chat/chatAdapterBuilder'; import {StandardChatAdapter} from '@shared/types/adapters/chat/standardChatAdapter'; import {ChatItem} from '@shared/types/conversation'; import {NluxRenderingError, NluxUsageError, NluxValidationError} from '@shared/types/error'; import {debug} from '@shared/utils/debug'; import {registerAllSections} from '../sections/sections'; import {AiChatStatus, IAiChat} from '../types/aiChat/aiChat'; import {UpdatableAiChatProps} from '../types/aiChat/props'; import {EventCallback, EventName, EventsMap} from '../types/event'; import {NluxController} from './controller/controller'; 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 AiChat<AiMsg = string> implements IAiChat<AiMsg> { protected theAdapter: ChatAdapter<AiMsg> | StandardChatAdapter<AiMsg> | null = null; protected theAdapterBuilder: ChatAdapterBuilder<AiMsg> | null = null; protected theAdapterType: 'builder' | 'instance' | null = null; protected theClassName: string | null = null; protected theComposerOptions: ComposerOptions | null = null; protected theConversationOptions: ConversationOptions | null = null; protected theDisplayOptions: DisplayOptions | null = null; protected theInitialConversation: ChatItem<AiMsg>[] | null = null; protected theMessageOptions: MessageOptions<AiMsg> | null = null; protected thePersonasOptions: PersonaOptions | null = null; // Variable to track if the chat component was unmounted (and thus cannot be used anymore). private aiChatStatus: AiChatStatus = 'idle'; // Controller instance private controller: NluxController<AiMsg> | null = null; // Event listeners provided before the controller is mounted, when the aiChat instance is being built. private unregisteredEventListeners: Map<EventName, Set<EventCallback<AiMsg>>> = new Map(); public get status(): AiChatStatus { return this.aiChatStatus; } private get isIdle() { return this.status === 'idle'; } hide() { if (!this.controller) { throw new NluxRenderingError({ source: this.constructor.name, message: 'Unable to hide. nlux is not mounted.', }); } this.controller.hide(); } public mount(rootElement: HTMLElement) { if (!this.isIdle) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to create nlux instance. nlux is already or was previously mounted. ' + 'You can only mount a nlux instance once, when the status is `idle`.', }); } const adapterToUser: ChatAdapter<AiMsg> | StandardChatAdapter<AiMsg> | null = this.theAdapter && this.theAdapterType === 'instance' ? this.theAdapter : (this.theAdapterType === 'builder' && this.theAdapterBuilder) ? this.theAdapterBuilder.create() : null; if (!adapterToUser) { throw new NluxValidationError({ source: this.constructor.name, message: 'Unable to create nlux instance. ChatAdapter is not properly set. ' + 'You should call `withAdapter(adapter)` method before mounting nlux.', }); } registerAllSections(); const aiChatRoot = document.createElement('div'); rootElement.appendChild(aiChatRoot); const controller = new NluxController( aiChatRoot, { adapter: adapterToUser, className: this.theClassName ?? undefined, initialConversation: this.theInitialConversation ?? undefined, messageOptions: this.theMessageOptions ?? {}, displayOptions: this.theDisplayOptions ?? {}, conversationOptions: this.theConversationOptions ?? {}, composerOptions: this.theComposerOptions ?? {}, personaOptions: this.thePersonasOptions ?? {}, }, ); // Register all unregistered event listeners for (const [eventName, eventListeners] of this.unregisteredEventListeners.entries()) { for (const eventCallback of eventListeners) { controller.on(eventName, eventCallback); } } controller.mount(); if (controller.mounted) { this.aiChatStatus = 'mounted'; this.controller = controller; this.unregisteredEventListeners.clear(); } else { throw new NluxRenderingError({ source: this.constructor.name, message: 'AiChat root component did not render.', }); } }; on(event: EventName, callback: EventsMap<AiMsg>[EventName]) { if (this.status === 'unmounted') { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to add event listener. nlux was previously unmounted.', }); } if (this.controller) { this.controller.on(event, callback); // No need to keep track of event callbacks if the controller is already mounted. return this; } if (!this.unregisteredEventListeners.has(event)) { this.unregisteredEventListeners.set(event, new Set()); } this.unregisteredEventListeners.get(event)?.add(callback); return this; } removeAllEventListeners(event: EventName) { // When an event is provided, remove all event listeners for that specific event. this.controller?.removeAllEventListeners(event); this.unregisteredEventListeners.get(event)?.clear(); } removeEventListener(event: EventName, callback: EventCallback<AiMsg>) { this.controller?.removeEventListener(event, callback); this.unregisteredEventListeners.get(event)?.delete(callback); } show() { if (!this.controller) { throw new NluxRenderingError({ source: this.constructor.name, message: 'Unable to show. nlux is not mounted.', }); } this.controller.show(); } public unmount() { debug('Unmounting nlux.'); if (!this.controller) { debug('Invalid call to aiChat.unmount() on an already unmounted nlux instance!'); return; } this.controller.unmount(); if (this.controller.mounted) { throw new NluxRenderingError({ source: this.constructor.name, message: 'Unable to unmount. Root component did not unmount.', }); } this.controller = null; this.unregisteredEventListeners.clear(); this.aiChatStatus = 'unmounted'; } public updateProps(props: UpdatableAiChatProps<AiMsg>) { if (!this.controller) { throw new NluxRenderingError({ source: this.constructor.name, message: 'Unable to update props. nlux is not mounted.', }); } if (props.hasOwnProperty('adapter')) { this.theAdapter = props.adapter ?? null; } if (props.hasOwnProperty('events')) { // Re-register all event listeners this.clearEventListeners(); for (const [eventName, eventCallback] of Object.entries(props.events ?? {})) { this.on(eventName as EventName, eventCallback as EventCallback<AiMsg>); } } if (props.hasOwnProperty('className')) { this.theClassName = props.className ?? null; } if (props.hasOwnProperty('displayOptions')) { this.theDisplayOptions = props.displayOptions ?? null; } if (props.hasOwnProperty('composerOptions')) { this.theComposerOptions = props.composerOptions ?? null; } if (props.hasOwnProperty('personaOptions')) { this.thePersonasOptions = props.personaOptions ?? null; } if (props.hasOwnProperty('conversationOptions')) { this.theConversationOptions = props.conversationOptions ?? null; } if (props.hasOwnProperty('messageOptions')) { this.theMessageOptions = props.messageOptions ?? null; } this.controller.updateProps(props); } public withAdapter(adapter: ChatAdapter<AiMsg> | StandardChatAdapter<AiMsg> | ChatAdapterBuilder<AiMsg>) { if (!this.isIdle) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to set adapter. nlux is already or was previously mounted. ' + 'You can only set the adapter once, when the status is `idle`.', }); } if (this.theAdapterBuilder || this.theAdapter) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to change config. A adapter or adapter builder was already set.', }); } if (typeof adapter !== 'object' || adapter === null) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to set adapter. Invalid adapter or adapter-builder type.', }); } const anAdapterOrAdapterBuilder = adapter as Record<string, unknown>; if (typeof anAdapterOrAdapterBuilder.create === 'function') { this.theAdapterType = 'builder'; this.theAdapterBuilder = anAdapterOrAdapterBuilder as unknown as ChatAdapterBuilder<AiMsg>; return this; } if ( (typeof anAdapterOrAdapterBuilder.batchText === 'function') || (typeof anAdapterOrAdapterBuilder.streamText === 'function') ) { this.theAdapterType = 'instance'; this.theAdapter = anAdapterOrAdapterBuilder as ChatAdapter<AiMsg>; return this; } throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to set adapter. Invalid adapter or adapter-builder implementation! ' + 'When an `ChatAdapterBuilder` is provided, it must implement either `create()` method that returns an ' + 'ChatAdapter instance. When an ChatAdapter instance is provided, must implement `batchText()` and/or ' + '`streamText()` methods. None of the above were found.', }); }; public withClassName(className: string) { if (!this.isIdle) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to set class name. nlux is already or was previously mounted. ' + 'You can only set the class name once, when the status is `idle`.', }); } if (this.theClassName) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to change config. A class name was already set.', }); } this.theClassName = className; return this; } public withComposerOptions(composerOptions: ComposerOptions) { if (!this.isIdle) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to set composer options. nlux is already or was previously mounted. ' + 'You can only set the composer options once, when the status is `idle`.', }); } if (this.theComposerOptions) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to change config. Composer options were already set.', }); } this.theComposerOptions = {...composerOptions}; return this; } public withConversationOptions(conversationOptions: ConversationOptions) { if (!this.isIdle) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to set conversation options. nlux is already or was previously mounted. ' + 'You can only set the conversation options once, when the status is `idle`.', }); } if (this.theConversationOptions) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to change config. Conversation options were already set.', }); } this.theConversationOptions = {...conversationOptions}; return this; } public withDisplayOptions(displayOptions: DisplayOptions) { if (!this.isIdle) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to set display options. nlux is already or was previously mounted. ' + 'You can only set the display options once, when the status is `idle`.', }); } if (this.theDisplayOptions) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to change config. Display options were already set.', }); } this.theDisplayOptions = {...displayOptions}; return this; } public withInitialConversation(initialConversation: ChatItem<AiMsg>[]) { if (!this.isIdle) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to set initial conversation. nlux is already or was previously mounted. ' + 'You can only set the initial conversation once, when the status is `idle`.', }); } if (this.theInitialConversation) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to change config. Conversation history was already set.', }); } this.theInitialConversation = [...initialConversation]; return this; } public withMessageOptions(messageOptions: MessageOptions<AiMsg>) { if (!this.isIdle) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to set message options. nlux is already or was previously mounted. ' + 'You can only set the message options once, when the status is `idle`.', }); } if (this.theMessageOptions) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to change config. Message options were already set.', }); } this.theMessageOptions = {...messageOptions}; return this; } public withPersonaOptions(personaOptions: PersonaOptions) { if (!this.isIdle) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to set persona options. nlux is already or was previously mounted. ' + 'You can only set the persona options once, when the status is `idle`.', }); } if (this.thePersonasOptions) { throw new NluxUsageError({ source: this.constructor.name, message: 'Unable to change config. Personas were already set.', }); } this.thePersonasOptions = {...personaOptions}; return this; } private clearEventListeners() { this.controller?.removeAllEventListenersForAllEvent(); this.unregisteredEventListeners.clear(); return; } }