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.

359 lines (308 loc) 14.9 kB
import {ComposerOptions, EventsMap} from '@nlux/core'; import {ComposerStatus} from '@shared/components/Composer/props'; import {submitPrompt} from '@shared/services/submitPrompt/submitPromptImpl'; import {ChatAdapter as CoreChatAdapter} from '@shared/types/adapters/chat/chatAdapter'; import {ChatAdapterExtras} from '@shared/types/adapters/chat/chatAdapterExtras'; import {ServerComponentChatAdapter} from '@shared/types/adapters/chat/serverComponentChatAdapter'; import {isStandardChatAdapter, StandardChatAdapter} from '@shared/types/adapters/chat/standardChatAdapter'; import {ChatSegment} from '@shared/types/chatSegment/chatSegment'; import {ChatSegmentAiMessage} from '@shared/types/chatSegment/chatSegmentAiMessage'; import {ChatSegmentUserMessage} from '@shared/types/chatSegment/chatSegmentUserMessage'; import {NLErrors} from '@shared/types/exceptions/errors'; import {warn} from '@shared/utils/warn'; import {MutableRefObject, useCallback, useEffect, useMemo, useRef} from 'react'; import {ImperativeConversationCompProps} from '../../sections/Conversation/props'; import {ChatAdapter} from '../../types/chatAdapter'; import {AiChatProps} from '../props'; import {useAdapterExtras} from './useAdapterExtras'; type SubmitPromptHandlerProps<AiMsg> = { aiChatProps: AiChatProps<AiMsg>; adapterToUse?: ChatAdapter<AiMsg> | StandardChatAdapter<AiMsg>; prompt: string; composerOptions?: ComposerOptions; initialSegment?: ChatSegment<AiMsg>; cancelledSegmentIds: Array<string>; cancelledMessageIds: Array<string>; newSegments: ChatSegment<AiMsg>[]; showException: (message: string) => void; setChatSegments: (segments: ChatSegment<AiMsg>[]) => void; setComposerStatus: (status: ComposerStatus) => void; setPrompt: (prompt: string) => void; conversationRef: MutableRefObject<ImperativeConversationCompProps<AiMsg> | null> }; export const useSubmitPromptHandler = <AiMsg>(props: SubmitPromptHandlerProps<AiMsg>) => { const { aiChatProps, adapterToUse, prompt: promptTyped, composerOptions, showException, initialSegment, newSegments, cancelledSegmentIds, cancelledMessageIds, setChatSegments, setComposerStatus, setPrompt, conversationRef, } = props; const hasValidInput = useMemo(() => promptTyped.length > 0, [promptTyped]); // The prompt typed will be read by the submitPrompt function, but it will not be used as a // dependency for the submitPrompt function (only the promptToSubmit is a dependency to useCallback). // Hence, the use of useRef to store the value and access it within the submitPrompt function, without // causing the memoized function to be re-created. const promptTypedRef = useRef(promptTyped); promptTypedRef.current = promptTyped; // React functions and state that can be accessed by non-React DOM update code const domToReactRef = useRef({ newSegments, cancelledSegmentIds, cancelledMessageIds, setChatSegments, setComposerStatus, showException, setPrompt, }); // Callback events can be used by the non-React DOM update code const callbackEvents = useRef<Partial<EventsMap<AiMsg>>>({}); useEffect(() => { domToReactRef.current = { newSegments, cancelledSegmentIds, cancelledMessageIds, setChatSegments, setComposerStatus, showException, setPrompt, }; }, [ newSegments, cancelledSegmentIds, cancelledMessageIds, setChatSegments, setComposerStatus, showException, setPrompt, ]); const adapterExtras: ChatAdapterExtras<AiMsg> = useAdapterExtras( aiChatProps, initialSegment ? [initialSegment, ...newSegments] : newSegments, aiChatProps.conversationOptions?.historyPayloadSize, ); useEffect(() => { callbackEvents.current = aiChatProps.events || {}; }, [aiChatProps.events]); return useCallback( () => { if (!adapterToUse) { warn('No valid adapter was provided to AiChat component'); return; } if (!hasValidInput) { return; } if (composerOptions?.disableSubmitButton) { return; } setComposerStatus('submitting-prompt'); const promptToSubmit = promptTyped; const streamedMessageIds: Set<string> = new Set(); const adapterBridge: CoreChatAdapter<AiMsg> | ServerComponentChatAdapter<AiMsg> | StandardChatAdapter<AiMsg> = isStandardChatAdapter( adapterToUse) ? adapterToUse as StandardChatAdapter<AiMsg> : ( (adapterToUse as ChatAdapter<AiMsg>).streamServerComponent ? { streamServerComponent: (adapterToUse as ChatAdapter<AiMsg>).streamServerComponent!, } satisfies ServerComponentChatAdapter<AiMsg> : { batchText: (adapterToUse as CoreChatAdapter<AiMsg>).batchText, streamText: (adapterToUse as CoreChatAdapter<AiMsg>).streamText, } satisfies CoreChatAdapter<AiMsg> ); // // ⭐️ Important // This is where the prompt is submitted to the API. // const { segment: chatSegment, observable: chatSegmentObservable, } = submitPrompt<AiMsg>( promptToSubmit, adapterBridge, adapterExtras, ); if (chatSegment.status === 'error') { warn('Error occurred while submitting prompt'); showException('Error occurred while submitting prompt'); setComposerStatus('typing'); // Reset the prompt if the composer is empty if (promptTypedRef.current === '') { setPrompt(promptToSubmit); } return; } // THE FOLLOWING CODE IS USED TO TRIGGER AN UPDATE OF THE REACT STATE. // The 'on' event listeners are implemented by @nlux/core non-React prompt handler. // On 'complete' and 'update' events, the chat segment is updated, but in order // to trigger a check and potentially re-render the React component, we need to change // the reference of the parts array by creating a new array. const handleSegmentItemReceived = (item: ChatSegmentAiMessage<AiMsg> | ChatSegmentUserMessage) => { const currentChatSegments = domToReactRef.current.newSegments; const newChatSegments: ChatSegment<AiMsg>[] = currentChatSegments.map( (currentChatSegment) => { if (currentChatSegment.uid !== chatSegmentObservable.segmentId) { return currentChatSegment; } return { ...currentChatSegment, items: [ ...currentChatSegment.items, {...item}, ], }; }, ); domToReactRef.current.setChatSegments(newChatSegments); }; chatSegmentObservable.on('userMessageReceived', (userMessage) => { if (domToReactRef.current?.cancelledMessageIds.includes(userMessage.uid)) { return; } handleSegmentItemReceived(userMessage); if (callbackEvents.current?.messageSent) { callbackEvents.current.messageSent({ uid: userMessage.uid, message: userMessage.content, }); } }); chatSegmentObservable.on('aiMessageStreamStarted', (aiStreamedMessage) => { if (domToReactRef.current?.cancelledMessageIds.includes(aiStreamedMessage.uid)) { return; } handleSegmentItemReceived(aiStreamedMessage); domToReactRef.current.setComposerStatus('waiting'); if (promptTypedRef.current === promptToSubmit) { domToReactRef.current.setPrompt(''); } streamedMessageIds.add(aiStreamedMessage.uid); if (callbackEvents.current?.messageStreamStarted) { callbackEvents.current.messageStreamStarted({uid: aiStreamedMessage.uid}); } }); chatSegmentObservable.on('aiServerComponentStreamStarted', (aiServerComponentMessage) => { if (domToReactRef.current?.cancelledMessageIds.includes(aiServerComponentMessage.uid)) { return; } handleSegmentItemReceived(aiServerComponentMessage); domToReactRef.current.setComposerStatus('waiting'); if (promptTypedRef.current === promptToSubmit) { domToReactRef.current.setPrompt(''); } streamedMessageIds.add(aiServerComponentMessage.uid); if (callbackEvents.current?.serverComponentStreamStarted) { callbackEvents.current?.serverComponentStreamStarted({uid: aiServerComponentMessage.uid}); } }); chatSegmentObservable.on('aiServerComponentStreamed', (streamedServerComponent) => { if (domToReactRef.current?.cancelledMessageIds.includes(streamedServerComponent.uid)) { return; } if (callbackEvents.current?.serverComponentRendered && !domToReactRef.current.cancelledMessageIds.includes(streamedServerComponent.uid)) { callbackEvents.current?.serverComponentRendered({uid: streamedServerComponent.uid}); } }); chatSegmentObservable.on('aiMessageReceived', (aiMessage) => { if (domToReactRef.current?.cancelledMessageIds.includes(aiMessage.uid)) { return; } const currentChatSegments = domToReactRef.current.newSegments; const newChatSegments: ChatSegment<AiMsg>[] = currentChatSegments.map( (currentChatSegment) => { if (currentChatSegment.uid !== chatSegmentObservable.segmentId) { return currentChatSegment; } return {...currentChatSegment, items: [...currentChatSegment.items, {...aiMessage}]}; }, ); domToReactRef.current.setChatSegments(newChatSegments); if (callbackEvents.current?.messageReceived) { callbackEvents.current.messageReceived({ uid: aiMessage.uid, message: aiMessage.content, }); } }); chatSegmentObservable.on('complete', (completeChatSegment) => { if (domToReactRef.current?.cancelledMessageIds.includes(completeChatSegment.uid)) { return; } domToReactRef.current.setComposerStatus('typing'); const currentChatSegments = domToReactRef.current.newSegments; const newChatSegments: ChatSegment<AiMsg>[] = currentChatSegments.map( (currentChatSegment) => { if (currentChatSegment.uid !== chatSegmentObservable.segmentId) { return currentChatSegment; } return {...completeChatSegment}; }, ); domToReactRef.current.setChatSegments(newChatSegments); if (promptTypedRef.current === promptToSubmit) { setPrompt(''); } if (streamedMessageIds.size > 0) { streamedMessageIds.forEach((messageId) => { requestAnimationFrame(() => { conversationRef.current?.completeStream(chatSegmentObservable.segmentId, messageId); }); }); streamedMessageIds.clear(); } }); chatSegmentObservable.on('aiChunkReceived', ({messageId, chunk}) => { if (domToReactRef.current?.cancelledMessageIds.includes(messageId)) { return; } conversationRef.current?.streamChunk(chatSegment.uid, messageId, chunk); }); chatSegmentObservable.on('aiMessageStreamed', (streamedMessage) => { if (domToReactRef.current?.cancelledMessageIds.includes(streamedMessage.uid)) { return; } if (callbackEvents.current?.messageReceived) { callbackEvents.current?.messageReceived({ uid: streamedMessage.uid, // In streamed messages, the AiMsg is always a string message: streamedMessage.content as AiMsg, }); } }); chatSegmentObservable.on('error', (errorId, errorObject) => { const parts = domToReactRef.current.newSegments; const newParts = parts.filter((part) => part.uid !== chatSegment.uid); const errorMessage = NLErrors[errorId]; domToReactRef.current.setChatSegments(newParts); domToReactRef.current.setComposerStatus('typing'); domToReactRef.current.showException(errorMessage); if (promptTypedRef.current === '') { setPrompt(promptToSubmit); } if (callbackEvents.current?.error) { callbackEvents.current.error({ errorId, message: errorMessage, errorObject, }); } }); domToReactRef.current.setChatSegments([ ...domToReactRef.current.newSegments, chatSegment, ]); }, [ promptTyped, adapterToUse, adapterExtras, showException, domToReactRef, composerOptions?.disableSubmitButton, ], ); };