From 7a7f38717d774a881b468b16211941ea8edab3f2 Mon Sep 17 00:00:00 2001 From: Salmen Hichri Date: Wed, 15 May 2024 12:43:11 +0100 Subject: [PATCH] Improved logic related to passing props to custom adapters --- .../themes/src/common/components/Message.css | 142 +++++++++--------- packages/extra/markdown/src/index.ts | 3 +- packages/js/core/src/exports/global.ts | 4 - packages/js/hf/src/createChatAdapter.ts | 4 +- packages/js/langchain/src/index.ts | 4 +- .../src/langserve/adapter/adapter.ts | 50 +++++- .../langserve/builder/createChatAdapter.ts | 2 +- .../src/nlbridge/chatAdapter/adapter.ts | 15 +- .../chatAdapter/builder/createChatAdapter.ts | 4 +- .../src/nlbridge/chatAdapter/fetch.ts | 13 +- .../src/nlbridge/chatAdapter/stream.ts | 104 +++++++------ .../react/core/src/exports/messageOptions.tsx | 72 ++++++++- packages/react/core/src/index.tsx | 2 + .../src/logic/ChatSegment/ChatSegmentComp.tsx | 25 +-- .../react/core/src/logic/ChatSegment/props.ts | 4 +- .../logic/Conversation/ConversationComp.tsx | 6 +- .../hooks/useChatSegmentsController.ts | 4 +- .../core/src/logic/Conversation/props.ts | 4 +- .../logic/MessageRenderer/MessageRenderer.tsx | 51 +++++-- .../StreamContainer/StreamContainerComp.tsx | 23 ++- .../core/src/logic/StreamContainer/props.ts | 4 +- packages/react/core/src/types/AiContext.ts | 6 +- .../core/src/ui/ChatItem/ChatItemComp.tsx | 10 +- packages/react/core/src/ui/ChatItem/props.ts | 10 +- ...nitChatAdapter.ts => getAdapterBuilder.ts} | 8 +- packages/react/hf/src/hooks/useChatAdapter.ts | 12 +- .../langchain/src/hooks/useChatAdapter.ts | 14 +- .../src/hooks/getChatAdapterBuilder.ts | 5 +- .../nlbridge/src/hooks/useChatAdapter.ts | 7 +- .../openai/src/hooks/useUnsafeChatAdapter.ts | 12 +- samples/aiChat/js/src/app.ts | 29 +++- samples/aiChat/react/src/App.tsx | 61 +++++--- .../react/src/comp/AiChatReactExpo.tsx | 34 +++-- .../react/src/comp/ChatItemReactExpo.tsx | 3 +- 34 files changed, 485 insertions(+), 266 deletions(-) rename packages/react/hf/src/hooks/{initChatAdapter.ts => getAdapterBuilder.ts} (86%) diff --git a/packages/css/themes/src/common/components/Message.css b/packages/css/themes/src/common/components/Message.css index ce2a88b1..36764b07 100644 --- a/packages/css/themes/src/common/components/Message.css +++ b/packages/css/themes/src/common/components/Message.css @@ -12,95 +12,95 @@ > .nlux-md-strm-root { width: 100%; + } - > .nlux-md-cntr { - display: flex; - flex-direction: column; - width: 100%; - gap: 0.5em; + .nlux-md-cntr { + display: flex; + flex-direction: column; + width: 100%; + gap: 0.5em; - :is(p, pre, h1, h2, h3, h4, h5, h6, ul, ol, dl, blockquote, table, hr) { - margin: 0; - padding: 0; - } + :is(p, pre, h1, h2, h3, h4, h5, h6, ul, ol, dl, blockquote, table, hr) { + margin: 0; + padding: 0; + } - > .code-block { - font-family: var(--nlux-mono-font-family), monospace; + > .code-block { + font-family: var(--nlux-mono-font-family), monospace; - font-size: var(--nlux-font-size-s); - position: relative; - overflow: scroll; - padding: var(--nlux-padding) 0; + font-size: var(--nlux-font-size-s); + position: relative; + overflow: scroll; + padding: var(--nlux-padding) 0; - color: var(--nlux-code-block-text-color); - border: none; - border-radius: var(--nlux-border-radius-s); - background-color: var(--nlux-code-block-background-color); - box-shadow: var(--nlux-box-shadow); - } + color: var(--nlux-code-block-text-color); + border: none; + border-radius: var(--nlux-border-radius-s); + background-color: var(--nlux-code-block-background-color); + box-shadow: var(--nlux-box-shadow); + } - > .code-block > pre { - width: fit-content; - min-width: 100%; - } + > .code-block > pre { + width: fit-content; + min-width: 100%; + } - > .code-block > pre > div { - padding: 0 var(--nlux-padding-m); - } + > .code-block > pre > div { + padding: 0 var(--nlux-padding-m); + } - > .code-block > pre > div:hover { - background-color: var(--nlux-code-block-hover-background-color); - } + > .code-block > pre > div:hover { + background-color: var(--nlux-code-block-hover-background-color); + } - button.nlux-cpy-btn { - position: relative; - z-index: 999999; - width: 25px; - height: 24px; + button.nlux-cpy-btn { + position: relative; + z-index: 999999; + width: 25px; + height: 24px; - margin-right: 10px; - margin-bottom: -23px; - margin-left: auto; + margin-right: 10px; + margin-bottom: -23px; + margin-left: auto; - padding: 4px; + padding: 4px; - cursor: pointer; - color: var(--nlux-code-block-background-color); - border: 1px solid var(--nlux-code-block-background-color); - border-radius: var(--nlux-border-radius-xs); - background-color: var(--nlux-message-received-background-color); - } + cursor: pointer; + color: var(--nlux-code-block-background-color); + border: 1px solid var(--nlux-code-block-background-color); + border-radius: var(--nlux-border-radius-xs); + background-color: var(--nlux-message-received-background-color); + } - button.nlux-cpy-btn { - &.clicked, - &.clicked:hover { - color: var(--nlux-message-received-background-color); - border-color: var(--nlux-code-block-background-color); - background-color: var(--nlux-code-block-background-color); - } - - &:hover * { - opacity: 0.5; - } - - &.clicked:hover * { - opacity: 1; - } + button.nlux-cpy-btn { + &.clicked, + &.clicked:hover { + color: var(--nlux-message-received-background-color); + border-color: var(--nlux-code-block-background-color); + background-color: var(--nlux-code-block-background-color); } - code { - font-family: var(--nlux-mono-font-family), monospace; - font-size: var(--nlux-font-size-s); - padding: var(--nlux-padding-xs) var(--nlux-padding-s); - - color: var(--nlux-inline-code-text-color); - border-radius: var(--nlux-border-radius-xs); - background-color: var(--nlux-inline-code-background-color); + &:hover * { + opacity: 0.5; } - strong { - font-weight: bold; + &.clicked:hover * { + opacity: 1; } } + + code { + font-family: var(--nlux-mono-font-family), monospace; + font-size: var(--nlux-font-size-s); + padding: var(--nlux-padding-xs) var(--nlux-padding-s); + + color: var(--nlux-inline-code-text-color); + border-radius: var(--nlux-border-radius-xs); + background-color: var(--nlux-inline-code-background-color); + } + + strong { + font-weight: bold; + } } } diff --git a/packages/extra/markdown/src/index.ts b/packages/extra/markdown/src/index.ts index ab9827e7..83f2aac0 100644 --- a/packages/extra/markdown/src/index.ts +++ b/packages/extra/markdown/src/index.ts @@ -1,5 +1,6 @@ import {HighlighterExtension} from '../../../js/core/src'; import {createMdStreamRenderer} from '../../../shared/src/markdown/stream/streamParser'; +import {CallbackFunction} from '../../../shared/src/types/callbackFunction'; export type MarkdownStreamParser = { next(value: string): void; @@ -12,7 +13,7 @@ export type MarkdownStreamParserOptions = { skipStreamingAnimation?: boolean; streamingAnimationSpeed?: number; showCodeBlockCopyButton?: boolean; - onComplete?: Function; + onComplete?: CallbackFunction; }; export const createMarkdownStreamParser = ( diff --git a/packages/js/core/src/exports/global.ts b/packages/js/core/src/exports/global.ts index 6da03c24..6884ae6f 100644 --- a/packages/js/core/src/exports/global.ts +++ b/packages/js/core/src/exports/global.ts @@ -1,5 +1,3 @@ -import {debug} from '../../../../shared/src/utils/debug'; - const globalNlux: { version: string; [key: string]: unknown; @@ -19,7 +17,5 @@ export const getGlobalNlux = (): typeof globalNlux | undefined => { } theWindow.nlux = globalNlux; - debug('globalNlux', globalNlux); - return globalNlux; }; diff --git a/packages/js/hf/src/createChatAdapter.ts b/packages/js/hf/src/createChatAdapter.ts index 428eb1c6..8a4d1412 100644 --- a/packages/js/hf/src/createChatAdapter.ts +++ b/packages/js/hf/src/createChatAdapter.ts @@ -1,4 +1,6 @@ import {ChatAdapterBuilder} from './hf/builder/builder'; import {ChatAdapterBuilderImpl} from './hf/builder/builderImpl'; -export const createChatAdapter = (): ChatAdapterBuilder => new ChatAdapterBuilderImpl(); +export const createChatAdapter = (): ChatAdapterBuilder => { + return new ChatAdapterBuilderImpl(); +}; diff --git a/packages/js/langchain/src/index.ts b/packages/js/langchain/src/index.ts index 5fcf8d93..fd15538d 100644 --- a/packages/js/langchain/src/index.ts +++ b/packages/js/langchain/src/index.ts @@ -13,4 +13,6 @@ export type { export type {LangServeEndpointType} from './langserve/types/langServe'; -export {createChatAdapter} from './langserve/builder/createChatAdapter'; +export { + createChatAdapter, +} from './langserve/builder/createChatAdapter'; diff --git a/packages/js/langchain/src/langserve/adapter/adapter.ts b/packages/js/langchain/src/langserve/adapter/adapter.ts index 55e91a1a..a09639e0 100644 --- a/packages/js/langchain/src/langserve/adapter/adapter.ts +++ b/packages/js/langchain/src/langserve/adapter/adapter.ts @@ -128,12 +128,54 @@ export abstract class LangServeAbstractAdapter implements StandardChatAda } } - preProcessAiStreamedChunk(chunk: string | object | undefined, extras: ChatAdapterExtras): AiMsg | undefined { - throw new Error('Method not implemented.'); + preProcessAiStreamedChunk( + chunk: string | object | undefined, + extras: ChatAdapterExtras, + ): AiMsg | undefined { + if (this.outputPreProcessor) { + return this.outputPreProcessor(chunk); + } + + if (typeof chunk === 'string') { + return chunk as AiMsg; + } + + const content = (chunk as Record)?.content; + if (typeof content === 'string') { + return content as AiMsg; + } + + warn( + 'LangServe adapter is unable to process the chunk from the runnable. Returning empty string. ' + + 'You may want to implement an output pre-processor to handle chunks of custom responses.', + ); + + return undefined; } - preProcessAiUnifiedMessage(message: string | object | undefined, extras: ChatAdapterExtras): AiMsg | undefined { - throw new Error('Method not implemented.'); + preProcessAiUnifiedMessage( + message: string | object | undefined, + extras: ChatAdapterExtras, + ): AiMsg | undefined { + if (this.outputPreProcessor) { + return this.outputPreProcessor(message); + } + + if (typeof message === 'string') { + return message as AiMsg; + } + + const content = (message as Record)?.content; + if (typeof content === 'string') { + return content as AiMsg; + } + + warn( + 'LangServe adapter is unable to process the response from the runnable. Returning empty string. ' + + 'You may want to implement an output pre-processor to handle custom responses.', + ); + + return undefined; } abstract streamText( diff --git a/packages/js/langchain/src/langserve/builder/createChatAdapter.ts b/packages/js/langchain/src/langserve/builder/createChatAdapter.ts index b844bdd7..85ed639a 100644 --- a/packages/js/langchain/src/langserve/builder/createChatAdapter.ts +++ b/packages/js/langchain/src/langserve/builder/createChatAdapter.ts @@ -1,6 +1,6 @@ import {ChatAdapterBuilder} from './builder'; import {LangServeAdapterBuilderImpl} from './builderImpl'; -export const createChatAdapter = function (): ChatAdapterBuilder { +export const createChatAdapter = (): ChatAdapterBuilder => { return new LangServeAdapterBuilderImpl(); }; diff --git a/packages/js/nlbridge/src/nlbridge/chatAdapter/adapter.ts b/packages/js/nlbridge/src/nlbridge/chatAdapter/adapter.ts index 0f3f9b0a..27e67f12 100644 --- a/packages/js/nlbridge/src/nlbridge/chatAdapter/adapter.ts +++ b/packages/js/nlbridge/src/nlbridge/chatAdapter/adapter.ts @@ -7,6 +7,7 @@ import { StreamingAdapterObserver, } from '@nlux/core'; import {uid} from '../../../../../shared/src/utils/uid'; +import {warn} from '../../../../../shared/src/utils/warn'; import {ChatAdapterOptions, ChatAdapterUsageMode} from '../types/chatAdapterOptions'; export abstract class NLBridgeAbstractAdapter implements StandardChatAdapter { @@ -71,11 +72,21 @@ export abstract class NLBridgeAbstractAdapter implements StandardChatAdap ): Promise; preProcessAiStreamedChunk(chunk: string | object | undefined, extras: ChatAdapterExtras): AiMsg | undefined { - throw new Error('Method not implemented.'); + if (typeof chunk === 'string') { + return chunk as AiMsg; + } + + warn('NLBridge adapter received a non-string chunk from the server. Returning empty string.'); + return '' as AiMsg; } preProcessAiUnifiedMessage(message: string | object | undefined, extras: ChatAdapterExtras): AiMsg | undefined { - throw new Error('Method not implemented.'); + if (typeof message === 'string') { + return message as AiMsg; + } + + warn('NLBridge adapter received a non-string message from the server. Returning empty string.'); + return '' as AiMsg; } abstract streamText( diff --git a/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/createChatAdapter.ts b/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/createChatAdapter.ts index 470b2814..df679cf2 100644 --- a/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/createChatAdapter.ts +++ b/packages/js/nlbridge/src/nlbridge/chatAdapter/builder/createChatAdapter.ts @@ -1,4 +1,6 @@ import {ChatAdapterBuilder} from './builder'; import {ChatAdapterBuilderImpl} from './builderImpl'; -export const createChatAdapter = (): ChatAdapterBuilder => new ChatAdapterBuilderImpl(); +export const createChatAdapter = (): ChatAdapterBuilder => { + return new ChatAdapterBuilderImpl(); +}; diff --git a/packages/js/nlbridge/src/nlbridge/chatAdapter/fetch.ts b/packages/js/nlbridge/src/nlbridge/chatAdapter/fetch.ts index 91e04972..53418e8f 100644 --- a/packages/js/nlbridge/src/nlbridge/chatAdapter/fetch.ts +++ b/packages/js/nlbridge/src/nlbridge/chatAdapter/fetch.ts @@ -8,7 +8,7 @@ export class NLBridgeFetchAdapter extends NLBridgeAbstractAdapte super(options); } - async fetchText(message: string, extras: ChatAdapterExtras): Promise { + async fetchText(message: string, extras: ChatAdapterExtras): Promise { if (this.context && this.context.contextId) { await this.context.flush(); } @@ -43,10 +43,7 @@ export class NLBridgeFetchAdapter extends NLBridgeAbstractAdapte typeof body.result === 'object' && body.result !== null && typeof body.result.response === 'string' ) { - const { - response, - task, - } = body.result; + const {response, task} = body.result; if ( this.context && task @@ -65,7 +62,11 @@ export class NLBridgeFetchAdapter extends NLBridgeAbstractAdapte } } - streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void { + streamText( + message: string, + observer: StreamingAdapterObserver, + extras: ChatAdapterExtras, + ): void { throw new NluxUsageError({ source: this.constructor.name, message: 'Cannot stream text from the fetch adapter!', diff --git a/packages/js/nlbridge/src/nlbridge/chatAdapter/stream.ts b/packages/js/nlbridge/src/nlbridge/chatAdapter/stream.ts index 196a77cd..b3af5782 100644 --- a/packages/js/nlbridge/src/nlbridge/chatAdapter/stream.ts +++ b/packages/js/nlbridge/src/nlbridge/chatAdapter/stream.ts @@ -9,72 +9,84 @@ export class NLBridgeStreamAdapter extends NLBridgeAbstractAdapter super(options); } - async fetchText(message: string, extras: ChatAdapterExtras): Promise { + async fetchText( + message: string, + extras: ChatAdapterExtras, + ): Promise { throw new NluxUsageError({ source: this.constructor.name, message: 'Cannot fetch text using the stream adapter!', }); } - streamText(message: string, observer: StreamingAdapterObserver, extras: ChatAdapterExtras): void { - const submitPrompt = () => fetch(this.endpointUrl, { - method: 'POST', - headers: { - ...this.headers, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - action: 'chat-stream', - payload: { - message, - conversationHistory: extras.conversationHistory, - contextId: this.context?.contextId, + streamText( + message: string, + observer: StreamingAdapterObserver, + extras: ChatAdapterExtras, + ): void { + const submitPrompt = () => { + fetch(this.endpointUrl, { + method: 'POST', + headers: { + ...this.headers, + 'Content-Type': 'application/json', }, - }), - }).then(async (response) => { - if (!response.ok) { - throw new Error(`NLBridge adapter returned status code: ${response.status}`); - } + body: JSON.stringify({ + action: 'chat-stream', + payload: { + message, + conversationHistory: extras.conversationHistory, + contextId: this.context?.contextId, + }, + }), + }).then(async (response) => { + if (!response.ok) { + throw new Error(`NLBridge adapter returned status code: ${response.status}`); + } - if (!response.body) { - throw new Error(`NLBridge adapter returned status code: ${response.status}`); - } + if (!response.body) { + throw new Error(`NLBridge adapter returned status code: ${response.status}`); + } - // Read a stream of server-sent events - // and feed them to the observer as they are being generated - const reader = response.body.getReader(); - const textDecoder = new TextDecoder(); - let doneReading = false; + // Read a stream of server-sent events + // and feed them to the observer as they are being generated + const reader = response.body.getReader(); + const textDecoder = new TextDecoder(); + let doneReading = false; - while (!doneReading) { - const {value, done} = await reader.read(); - if (done) { - doneReading = true; - continue; - } + while (!doneReading) { + const {value, done} = await reader.read(); + if (done) { + doneReading = true; + continue; + } - try { - const chunk = textDecoder.decode(value); - observer.next(chunk); - } catch (err) { - warn(`Error parsing chunk by NLBridgeStreamAdapter: ${err}`); + try { + const chunk = textDecoder.decode(value); + observer.next(chunk); + } catch (err) { + warn(`Error parsing chunk by NLBridgeStreamAdapter: ${err}`); + } } - } - observer.complete(); - }); + observer.complete(); + }); + }; + // + // When a valid context is available, flush it before submitting the prompt + // if (this.context && this.context.contextId) { - this.context.flush().then(() => { - submitPrompt(); - }).catch(() => { + this.context + .flush() + .then(() => submitPrompt()) // Submit prompt even when flushing fails - submitPrompt(); - }); + .catch(() => submitPrompt()); return; } + // Submit prompt when no context is available submitPrompt(); } } diff --git a/packages/react/core/src/exports/messageOptions.tsx b/packages/react/core/src/exports/messageOptions.tsx index 269cd86e..e6beadfe 100644 --- a/packages/react/core/src/exports/messageOptions.tsx +++ b/packages/react/core/src/exports/messageOptions.tsx @@ -1,21 +1,83 @@ import {MessageOptions as JavascriptMessageOptions} from '@nlux/core'; -import {FunctionComponent, RefObject} from 'react'; +import {FC, RefObject} from 'react'; -export type ResponseComponentProps = { +/** + * Props for the custom React component that renders a message received from the AI. + * @template AiMsg The type of the message received from the AI. Defaults to string for standard NLUX adapters. + * + * @property {string} uid The unique identifier of the message. + * @property {'stream' | 'fetch'} dataTransferMode The data transfer mode used by the adapter. + * + * @property {'streaming' | 'complete'} status The status of the message. When the data transfer mode is 'fetch', + * the status is always 'complete'. When the data transfer mode is 'stream', the status is 'streaming' until the + * message is complete. The status is 'complete' when the message is fully received. + * + * @property {AiMsg | AiMsg[]} [content] The content of the message. When the data transfer mode is 'fetch', + * the content is always a single message. When the data transfer mode is 'stream', the content is an array of + * messages. The content is undefined when the status is 'streaming'. + * + * @property {unknown | unknown[]} [serverResponse] The raw server response. When the data transfer mode is 'fetch', + * the server response is always a single object or string representing the response received from the server. + * When the data transfer mode is 'stream', the server response is an array of objects or strings representing each + * chunk of the response received from the server. The server response is undefined when the status is 'streaming'. + */ + +/** + * Props for the custom React component that renders a message sent by the server in streaming mode. + * @template AiMsg The type of the message received from the AI. Defaults to string for standard NLUX adapters. + * + * @property {string} uid The unique identifier of the message. + * @property {'stream'} dataTransferMode The data transfer mode used by the adapter. + * @property {'streaming' | 'complete'} status The status of the message. + * + * @property {AiMsg[]} content The content of the message. The content is an array of messages. The content is undefined + * when the status is 'streaming'. The content is an array of messages when the status is 'complete'. + * + * @property {unknown[]} serverResponse The raw server response. The server response is an array of objects or strings + * representing each raw chunk of the response received from the server. The server response is undefined when the + * status is 'streaming'. The server response is an array of objects or strings when the status is 'complete'. + * + * @property {RefObject} containerRef A reference to the HTML element that contains the message to be streamed. + */ +export type StreamResponseComponentProps = { uid: string; + dataTransferMode: 'stream'; status: 'streaming' | 'complete'; - response?: AiMsg; + content?: AiMsg[]; + serverResponse?: unknown[]; containerRef: RefObject; }; -export type ResponseComponent = FunctionComponent>; +/** + * Props for the custom React component that renders a message sent by the server in fetch mode. + * @template AiMsg The type of the message received from the AI. Defaults to string for standard NLUX adapters. + * + * @property {string} uid The unique identifier of the message. + * @property {'fetch'} dataTransferMode The data transfer mode used by the adapter. + * @property {'complete'} status The status of the message. + * + * @property {AiMsg} content The content of the message. The content is a single message. + * @property {unknown} serverResponse The raw server response. The server response is a single object or string + * representing the raw response received from the server. + */ +export type FetchResponseComponentProps = { + uid: string; + dataTransferMode: 'fetch'; + status: 'complete'; + content: AiMsg; + serverResponse: unknown; +}; + +export type ResponseComponentProps = StreamResponseComponentProps | FetchResponseComponentProps; + +export type ResponseComponent = FC>; export type PromptComponentProps = { uid: string; prompt: string; }; -export type PromptComponent = FunctionComponent; +export type PromptComponent = FC; export type ReactSpecificMessageOptions = { /** diff --git a/packages/react/core/src/index.tsx b/packages/react/core/src/index.tsx index b2892121..ac76481b 100644 --- a/packages/react/core/src/index.tsx +++ b/packages/react/core/src/index.tsx @@ -69,6 +69,8 @@ export type { } from './exports/props'; export type { + FetchResponseComponentProps, + StreamResponseComponentProps, ResponseComponentProps, ResponseComponent, PromptComponentProps, diff --git a/packages/react/core/src/logic/ChatSegment/ChatSegmentComp.tsx b/packages/react/core/src/logic/ChatSegment/ChatSegmentComp.tsx index 3b63155f..aa353b1b 100644 --- a/packages/react/core/src/logic/ChatSegment/ChatSegmentComp.tsx +++ b/packages/react/core/src/logic/ChatSegment/ChatSegmentComp.tsx @@ -12,18 +12,18 @@ import {pictureFromMessageAndPersona} from './utils/pictureFromMessageAndPersona export const ChatSegmentComp: ( props: ChatSegmentProps, - ref: Ref, + ref: Ref>, ) => ReactNode = function ( props: ChatSegmentProps, - ref: Ref, + ref: Ref>, ): ReactNode { const {chatSegment, containerRef} = props; const chatItemsRef = useMemo( - () => new Map>(), [], + () => new Map>>(), [], ); const chatItemsStreamingBuffer = useMemo( - () => new Map>(), [], + () => new Map>(), [], ); useEffect(() => { @@ -56,7 +56,7 @@ export const ChatSegmentComp: ( const rootClassName = useMemo(() => getChatSegmentClassName(chatSegment.status), [chatSegment.status]); useImperativeHandle(ref, () => ({ - streamChunk: (chatItemId: string, chunk: string) => { + streamChunk: (chatItemId: string, chunk: AiMsg) => { const chatItemCompRef = chatItemsRef.get(chatItemId); if (chatItemCompRef?.current) { chatItemCompRef.current.streamChunk(chunk); @@ -107,9 +107,9 @@ export const ChatSegmentComp: ( return (
{chatItems.map((chatItem, index) => { - let ref: RefObject | undefined = chatItemsRef.get(chatItem.uid); + let ref: RefObject> | undefined = chatItemsRef.get(chatItem.uid); if (!ref) { - ref = createRef(); + ref = createRef>(); chatItemsRef.set(chatItem.uid, ref); } @@ -144,7 +144,8 @@ export const ChatSegmentComp: ( uid={chatItem.uid} status={'complete'} direction={'outgoing'} - message={chatItem.content} + dataTransferMode={'fetch'} // User chat items are always in fetch mode. + fetchedContent={chatItem.content as AiMsg} // Same comp is used for user and AI chat items. name={nameFromMessageAndPersona(chatItem.participantRole, props.personaOptions)} picture={pictureFromMessageAndPersona(chatItem.participantRole, props.personaOptions)} syntaxHighlighter={props.syntaxHighlighter} @@ -175,7 +176,8 @@ export const ChatSegmentComp: ( uid={chatItem.uid} status={'streaming'} direction={'incoming'} - message={chatItem.content} + dataTransferMode={chatItem.dataTransferMode} + streamedContent={chatItem.content} responseRenderer={props.responseRenderer} name={nameFromMessageAndPersona(chatItem.participantRole, props.personaOptions)} picture={pictureFromMessageAndPersona(chatItem.participantRole, @@ -212,7 +214,9 @@ export const ChatSegmentComp: ( uid={chatItem.uid} status={'complete'} direction={'incoming'} - message={chatItem.content} + dataTransferMode={chatItem.dataTransferMode} + fetchedContent={chatItem.content} + fetchedServerResponse={chatItem.serverResponse} responseRenderer={props.responseRenderer} name={nameFromMessageAndPersona(chatItem.participantRole, props.personaOptions)} picture={pictureFromMessageAndPersona(chatItem.participantRole, @@ -239,6 +243,7 @@ export const ChatSegmentComp: ( uid={chatItem.uid} status={'streaming'} direction={'incoming'} + dataTransferMode={chatItem.dataTransferMode} responseRenderer={props.responseRenderer} name={nameFromMessageAndPersona(chatItem.participantRole, props.personaOptions)} picture={pictureFromMessageAndPersona(chatItem.participantRole, diff --git a/packages/react/core/src/logic/ChatSegment/props.ts b/packages/react/core/src/logic/ChatSegment/props.ts index a188ff5f..2f7a3fe6 100644 --- a/packages/react/core/src/logic/ChatSegment/props.ts +++ b/packages/react/core/src/logic/ChatSegment/props.ts @@ -17,7 +17,7 @@ export type ChatSegmentProps = { containerRef?: RefObject; }; -export type ChatSegmentImperativeProps = { - streamChunk: (messageId: string, chunk: string) => void; +export type ChatSegmentImperativeProps = { + streamChunk: (messageId: string, chunk: AiMsg) => void; completeStream: (messageId: string) => void; }; diff --git a/packages/react/core/src/logic/Conversation/ConversationComp.tsx b/packages/react/core/src/logic/Conversation/ConversationComp.tsx index e43e0010..23a3667a 100644 --- a/packages/react/core/src/logic/Conversation/ConversationComp.tsx +++ b/packages/react/core/src/logic/Conversation/ConversationComp.tsx @@ -7,12 +7,12 @@ import {ConversationCompProps, ImperativeConversationCompProps} from './props'; export type ConversationCompType = ( props: ConversationCompProps, - ref: Ref, + ref: Ref>, ) => ReactNode; export const ConversationComp: ConversationCompType = function ( props: ConversationCompProps, - ref: Ref, + ref: Ref>, ): ReactNode { const { segments, @@ -32,7 +32,7 @@ export const ConversationComp: ConversationCompType = function ( const segmentsController = useChatSegmentsController(segments); useImperativeHandle(ref, () => ({ - streamChunk: (segmentId: string, messageId: string, chunk: string) => { + streamChunk: (segmentId: string, messageId: string, chunk: AiMsg) => { const chatSegment = segmentsController.get(segmentId); chatSegment?.streamChunk(messageId, chunk); }, diff --git a/packages/react/core/src/logic/Conversation/hooks/useChatSegmentsController.ts b/packages/react/core/src/logic/Conversation/hooks/useChatSegmentsController.ts index b0dd27c5..a45c0988 100644 --- a/packages/react/core/src/logic/Conversation/hooks/useChatSegmentsController.ts +++ b/packages/react/core/src/logic/Conversation/hooks/useChatSegmentsController.ts @@ -6,7 +6,7 @@ export const useChatSegmentsController = function ( segments: ChatSegment[], ) { const chatSegmentsRef = useMemo( - () => new Map>(), [], + () => new Map>>(), [], ); useEffect(() => { @@ -27,7 +27,7 @@ export const useChatSegmentsController = function ( return { get: (uid: string) => chatSegmentsRef.get(uid)?.current, getRef: (uid: string) => chatSegmentsRef.get(uid), - set: (uid: string, ref: RefObject) => { + set: (uid: string, ref: RefObject>) => { chatSegmentsRef.set(uid, ref); }, remove: (uid: string) => { diff --git a/packages/react/core/src/logic/Conversation/props.ts b/packages/react/core/src/logic/Conversation/props.ts index a14a5afb..b5de981d 100644 --- a/packages/react/core/src/logic/Conversation/props.ts +++ b/packages/react/core/src/logic/Conversation/props.ts @@ -21,7 +21,7 @@ export type ConversationCompProps = { } | undefined) => void; }; -export type ImperativeConversationCompProps = { - streamChunk: (segmentId: string, messageId: string, chunk: AiChat) => void; +export type ImperativeConversationCompProps = { + streamChunk: (segmentId: string, messageId: string, chunk: AiMsg) => void; completeStream: (segmentId: string, messageId: string) => void; }; diff --git a/packages/react/core/src/logic/MessageRenderer/MessageRenderer.tsx b/packages/react/core/src/logic/MessageRenderer/MessageRenderer.tsx index 5a1d89d0..5c874a53 100644 --- a/packages/react/core/src/logic/MessageRenderer/MessageRenderer.tsx +++ b/packages/react/core/src/logic/MessageRenderer/MessageRenderer.tsx @@ -1,4 +1,5 @@ import {FC, RefObject} from 'react'; +import {FetchResponseComponentProps, StreamResponseComponentProps} from '../../exports/messageOptions'; import {ChatItemProps} from '../../ui/ChatItem/props'; import {MarkdownSnapshotRenderer} from './MarkdownSnapshotRenderer'; @@ -11,8 +12,10 @@ export const createMessageRenderer: ( ) { const { uid, + dataTransferMode, status, - message, + fetchedContent, + fetchedServerResponse, direction, responseRenderer, syntaxHighlighter, @@ -25,31 +28,49 @@ export const createMessageRenderer: ( // When the dataTransferMode is 'fetch', the message is defined and the containerRef is not needed. const containerRefToUse = containerRef ?? {current: null} satisfies RefObject; + // + // A custom response renderer is provided by the user. + // if (responseRenderer !== undefined) { - if (message === undefined) { - return () => null; - } + // + // Streaming into a custom renderer. + // + if (dataTransferMode === 'stream') { + const props: StreamResponseComponentProps = { + uid, + status, + dataTransferMode, + containerRef: containerRefToUse as RefObject, + }; + + return () => responseRenderer(props); + } else { + // + // Fetching data and displaying it in a custom renderer. + // + const props: FetchResponseComponentProps = { + uid, + status: 'complete', + content: fetchedContent as AiMsg, + serverResponse: fetchedServerResponse, + dataTransferMode, + }; - return () => responseRenderer({ - uid, - status, - // We only pass the response to custom renderer when the status is 'complete'. - response: status === 'complete' ? message as AiMsg : undefined, - containerRef: containerRefToUse as RefObject, - }); + return () => responseRenderer(props); + } } if (direction === 'outgoing') { - if (typeof message === 'string') { - const messageToRender: string = message; + if (typeof fetchedContent === 'string') { + const messageToRender: string = fetchedContent; return () => <>{messageToRender}; } return () => ''; } - if (typeof message === 'string') { - const messageToRender: string = message; + if (typeof fetchedContent === 'string') { + const messageToRender: string = fetchedContent; return () => ( ( - props: StreamContainerProps, - ref: Ref, +export const StreamContainerComp = function ( + props: StreamContainerProps, + ref: Ref>, ) { const { uid, @@ -83,8 +84,10 @@ export const StreamContainerComp = function ( } + content={undefined} + serverResponse={undefined} + dataTransferMode={'stream'} /> ); } else { @@ -102,7 +105,13 @@ export const StreamContainerComp = function ( }, []); useImperativeHandle(ref, () => ({ - streamChunk: (chunk: string) => mdStreamParserRef.current?.next(chunk), + streamChunk: (chunk: AiMsg) => { + if (typeof chunk === 'string') { + mdStreamParserRef.current?.next(chunk); + } else { + warn('When using a markdown stream renderer, the chunk must be a string.'); + } + }, completeStream: () => mdStreamParserRef.current?.complete(), }), []); diff --git a/packages/react/core/src/logic/StreamContainer/props.ts b/packages/react/core/src/logic/StreamContainer/props.ts index a240e84f..909fdd3e 100644 --- a/packages/react/core/src/logic/StreamContainer/props.ts +++ b/packages/react/core/src/logic/StreamContainer/props.ts @@ -17,7 +17,7 @@ export type StreamContainerProps = { } }; -export type StreamContainerImperativeProps = { - streamChunk: (chunk: string) => void; +export type StreamContainerImperativeProps = { + streamChunk: (chunk: AiMsg) => void; completeStream: () => void; }; diff --git a/packages/react/core/src/types/AiContext.ts b/packages/react/core/src/types/AiContext.ts index a58c2185..10918b86 100644 --- a/packages/react/core/src/types/AiContext.ts +++ b/packages/react/core/src/types/AiContext.ts @@ -1,10 +1,10 @@ import {AiContext as CoreAiContext, ContextItems} from '@nlux/core'; -import {ComponentClass, Context, FunctionComponent, ReactNode} from 'react'; +import {ComponentClass, Context, FC, ReactNode} from 'react'; export type AiContextProviderProps = { initialItems?: ContextItems; - errorComponent?: FunctionComponent<{error?: string}> | ComponentClass<{error?: string}> - loadingComponent?: FunctionComponent | ComponentClass; + errorComponent?: FC<{error?: string}> | ComponentClass<{error?: string}> + loadingComponent?: FC | ComponentClass; children: ReactNode; }; diff --git a/packages/react/core/src/ui/ChatItem/ChatItemComp.tsx b/packages/react/core/src/ui/ChatItem/ChatItemComp.tsx index 4b490a3b..e7136603 100644 --- a/packages/react/core/src/ui/ChatItem/ChatItemComp.tsx +++ b/packages/react/core/src/ui/ChatItem/ChatItemComp.tsx @@ -12,10 +12,10 @@ import {ChatItemImperativeProps, ChatItemProps} from './props'; export const ChatItemComp: ( props: ChatItemProps, - ref: Ref, + ref: Ref>, ) => ReactElement = function ( props: ChatItemProps, - ref: Ref, + ref: Ref>, ): ReactElement { const picture = useMemo(() => { if (props.picture === undefined && props.name === undefined) { @@ -26,10 +26,10 @@ export const ChatItemComp: ( }, [props?.picture, props?.name]); const isStreaming = useMemo(() => props.status === 'streaming', [props.status]); - const streamContainer = useRef(null); + const streamContainer = useRef | null>(null); useImperativeHandle(ref, () => ({ - streamChunk: (chunk: string) => streamContainer?.current?.streamChunk(chunk), + streamChunk: (chunk: AiMsg) => streamContainer?.current?.streamChunk(chunk), completeStream: () => streamContainer?.current?.completeStream(), }), []); @@ -42,7 +42,7 @@ export const ChatItemComp: ( return isStreaming ? () => '' : createMessageRenderer(props); }, [ isStreaming, - props.uid, props.status, props.message, props.direction, + props.uid, props.status, props.fetchedContent, props.streamedContent, props.direction, props.responseRenderer, props.syntaxHighlighter, props.markdownLinkTarget, ]); diff --git a/packages/react/core/src/ui/ChatItem/props.ts b/packages/react/core/src/ui/ChatItem/props.ts index 4006f660..b26b221c 100644 --- a/packages/react/core/src/ui/ChatItem/props.ts +++ b/packages/react/core/src/ui/ChatItem/props.ts @@ -6,8 +6,12 @@ import {ResponseComponent} from '../../exports/messageOptions'; export type ChatItemProps = { uid: string; direction: MessageDirection; + dataTransferMode: 'fetch' | 'stream'; status: 'streaming' | 'complete'; - message?: AiMsg | string; + fetchedContent?: AiMsg; + fetchedServerResponse?: unknown; + streamedContent?: AiMsg[]; + streamedServerResponse?: Array; responseRenderer?: ResponseComponent; name?: string; picture?: string | ReactElement; @@ -18,7 +22,7 @@ export type ChatItemProps = { streamingAnimationSpeed?: number; }; -export type ChatItemImperativeProps = { - streamChunk: (chunk: string) => void; +export type ChatItemImperativeProps = { + streamChunk: (chunk: AiMsg) => void; completeStream: () => void; }; diff --git a/packages/react/hf/src/hooks/initChatAdapter.ts b/packages/react/hf/src/hooks/getAdapterBuilder.ts similarity index 86% rename from packages/react/hf/src/hooks/initChatAdapter.ts rename to packages/react/hf/src/hooks/getAdapterBuilder.ts index 2434d418..9e04ac33 100644 --- a/packages/react/hf/src/hooks/initChatAdapter.ts +++ b/packages/react/hf/src/hooks/getAdapterBuilder.ts @@ -1,9 +1,11 @@ -import {ChatAdapterOptions, createChatAdapter} from '@nlux/hf'; +import {ChatAdapterBuilder, ChatAdapterOptions, createChatAdapter} from '@nlux/hf'; import {NluxUsageError} from '../../../../shared/src/types/error'; -const source = 'hooks/initChatAdapter'; +const source = 'hooks/getAdapterBuilder'; -export const initChatAdapter = (options: ChatAdapterOptions) => { +export const getAdapterBuilder = ( + options: ChatAdapterOptions, +): ChatAdapterBuilder => { const { model, authToken, diff --git a/packages/react/hf/src/hooks/useChatAdapter.ts b/packages/react/hf/src/hooks/useChatAdapter.ts index b6cdb39f..70900116 100644 --- a/packages/react/hf/src/hooks/useChatAdapter.ts +++ b/packages/react/hf/src/hooks/useChatAdapter.ts @@ -1,18 +1,20 @@ -import {ChatAdapterBuilder, ChatAdapterOptions} from '@nlux/hf'; +import {ChatAdapterOptions, StandardChatAdapter} from '@nlux/hf'; import {useEffect, useState} from 'react'; import {debug} from '../../../../shared/src/utils/debug'; -import {initChatAdapter} from './initChatAdapter'; +import {getAdapterBuilder} from './getAdapterBuilder'; const source = 'hooks/useChatAdapter'; -export const useChatAdapter = (options: ChatAdapterOptions) => { +export const useChatAdapter = ( + options: ChatAdapterOptions, +): StandardChatAdapter => { if (!options.model) { throw new Error('You must provide either a model or an endpoint to use Hugging Face Inference API.'); } const [isInitialized, setIsInitialized] = useState(false); - const [adapter] = useState>( - initChatAdapter(options), + const [adapter] = useState>( + getAdapterBuilder(options).create(), ); const { diff --git a/packages/react/langchain/src/hooks/useChatAdapter.ts b/packages/react/langchain/src/hooks/useChatAdapter.ts index 2a3f681e..b9f93c10 100644 --- a/packages/react/langchain/src/hooks/useChatAdapter.ts +++ b/packages/react/langchain/src/hooks/useChatAdapter.ts @@ -1,12 +1,16 @@ -import {ChatAdapterBuilder, ChatAdapterOptions} from '@nlux/langchain'; +import {ChatAdapterOptions, StandardChatAdapter} from '@nlux/langchain'; import {useDeepCompareEffect} from '@nlux/react'; import {useState} from 'react'; import {getAdapterBuilder} from './getAdapterBuilder'; -export const useChatAdapter = (options: ChatAdapterOptions) => { +export const useChatAdapter = ( + options: ChatAdapterOptions, +): StandardChatAdapter => { const [isInitialized, setIsInitialized] = useState(false); - const [adapter, setAdapter] = useState>( - getAdapterBuilder(options), + const [adapter, setAdapter] = useState< + StandardChatAdapter + >( + getAdapterBuilder(options).create(), ); const { @@ -24,7 +28,7 @@ export const useChatAdapter = (options: ChatAdapterOptions(options: ChatAdapterOptions): ChatAdapter => { +export const getChatAdapterBuilder = (options: ChatAdapterOptions): StandardChatAdapter => { const { url, mode, diff --git a/packages/react/nlbridge/src/hooks/useChatAdapter.ts b/packages/react/nlbridge/src/hooks/useChatAdapter.ts index dd9896a4..bc5eeb6c 100644 --- a/packages/react/nlbridge/src/hooks/useChatAdapter.ts +++ b/packages/react/nlbridge/src/hooks/useChatAdapter.ts @@ -1,5 +1,4 @@ -import {ChatAdapter} from '@nlux/nlbridge'; -import {AiContext as ReactAiContext} from '@nlux/react'; +import {AiContext as ReactAiContext, StandardChatAdapter} from '@nlux/react'; import {useContext, useEffect, useState} from 'react'; import {getChatAdapterBuilder} from './getChatAdapterBuilder'; @@ -10,7 +9,7 @@ export type ReactChatAdapterOptions = { headers?: Record; }; -export const useChatAdapter = (options: ReactChatAdapterOptions): ChatAdapter => { +export const useChatAdapter = (options: ReactChatAdapterOptions): StandardChatAdapter => { const { context, url, @@ -51,7 +50,7 @@ export const useChatAdapter = (options: ReactChatAdapterOptions) }, [headers]); const coreContext = context?.ref ? useContext(context.ref) : undefined; - const [adapter, setAdapter] = useState>( + const [adapter, setAdapter] = useState>( getChatAdapterBuilder({ url, mode, diff --git a/packages/react/openai/src/hooks/useUnsafeChatAdapter.ts b/packages/react/openai/src/hooks/useUnsafeChatAdapter.ts index 163b495a..f98230fb 100644 --- a/packages/react/openai/src/hooks/useUnsafeChatAdapter.ts +++ b/packages/react/openai/src/hooks/useUnsafeChatAdapter.ts @@ -1,11 +1,13 @@ -import {ChatAdapterBuilder, ChatAdapterOptions} from '@nlux/openai'; +import {ChatAdapterOptions, StandardChatAdapter} from '@nlux/openai'; import {useEffect, useState} from 'react'; import {getAdapterBuilder} from './getAdapterBuilder'; -export const useUnsafeChatAdapter = (options: ChatAdapterOptions) => { +export const useUnsafeChatAdapter = ( + options: ChatAdapterOptions, +): StandardChatAdapter => { const [isInitialized, setIsInitialized] = useState(false); - const [adapter, setAdapter] = useState>( - getAdapterBuilder(options), + const [adapter, setAdapter] = useState>( + getAdapterBuilder(options).create(), ); const { @@ -21,7 +23,7 @@ export const useUnsafeChatAdapter = (options: ChatAdapterOptions) => { return; } - const newAdapter = getAdapterBuilder(options); + const newAdapter = getAdapterBuilder(options).create(); setAdapter(newAdapter); }, [ apiKey, diff --git a/samples/aiChat/js/src/app.ts b/samples/aiChat/js/src/app.ts index d0cbbb5d..54c5f2d5 100644 --- a/samples/aiChat/js/src/app.ts +++ b/samples/aiChat/js/src/app.ts @@ -1,17 +1,30 @@ -import './style.css'; import '@nlux-dev/themes/src/luna/theme.css'; import {ChatItem, createAiChat} from '@nlux-dev/core/src'; -import '@nlux-dev/highlighter/src/themes/stackoverflow/dark.css'; -import {createChatAdapter} from '@nlux-dev/nlbridge/src'; -import {createUnsafeChatAdapter} from '@nlux-dev/openai/src'; +// import {highlighter} from '@nlux-dev/highlighter/src'; +// import '@nlux-dev/highlighter/src/themes/stackoverflow/dark.css'; +import {createChatAdapter as createHuggingFaceChatAdapter} from '@nlux-dev/hf/src'; +import {createChatAdapter as createLangChainChatAdapter} from '@nlux-dev/langchain/src'; +import {createChatAdapter as createNlbridgeChatAdapter} from '@nlux-dev/nlbridge/src'; +import {createUnsafeChatAdapter as createOpenAiChatAdapter} from '@nlux-dev/openai/src'; +import './style.css'; document.addEventListener('DOMContentLoaded', () => { const parent = document.getElementById('root')!; - const adapter = createChatAdapter() + const nlBridgeAdapter = createNlbridgeChatAdapter() .withUrl('http://localhost:8899/'); - const openAiAdapter = createUnsafeChatAdapter() + const langChainAdapter = createLangChainChatAdapter() + .withUrl('https://pynlux.api.nlux.ai/einbot') + .withDataTransferMode('stream') + .withInputSchema(true); + + const huggingFaceAdapter = createHuggingFaceChatAdapter() + .withModel('gpt4') + .withDataTransferMode('fetch') + .withAuthToken('N/A'); + + const openAiAdapter = createOpenAiChatAdapter() .withApiKey(localStorage.getItem('openai-api-key') || 'N/A') .withDataTransferMode('stream'); @@ -52,7 +65,9 @@ document.addEventListener('DOMContentLoaded', () => { ]; const aiChat = createAiChat() - .withAdapter(openAiAdapter) + // .withAdapter(nlBridgeAdapter) + // .withAdapter(openAiAdapter) + .withAdapter(langChainAdapter) // .withInitialConversation(initialConversation) .withPromptBoxOptions({ placeholder: 'Type your prompt here', diff --git a/samples/aiChat/react/src/App.tsx b/samples/aiChat/react/src/App.tsx index bf89ed62..14daa464 100644 --- a/samples/aiChat/react/src/App.tsx +++ b/samples/aiChat/react/src/App.tsx @@ -1,26 +1,33 @@ import '@nlux-dev/themes/src/luna/theme.css'; -import '@nlux-dev/highlighter/src/themes/stackoverflow/dark.css'; -import {highlighter} from '@nlux-dev/highlighter/src'; -import {useChatAdapter as useChatLangChainAdapter} from '@nlux-dev/langchain-react/src'; -import {useChatAdapter} from '@nlux-dev/nlbridge-react/src'; -import {createUnsafeChatAdapter} from '@nlux-dev/openai/src'; -import {AiChat, ChatItem} from '@nlux-dev/react/src'; +// import {highlighter} from '@nlux-dev/highlighter/src'; +// import '@nlux-dev/highlighter/src/themes/stackoverflow/dark.css'; +import {useChatAdapter as useHfChatAdapter} from '@nlux-dev/hf-react/src'; +import {useChatAdapter as useChatLangChainChatAdapter} from '@nlux-dev/langchain-react/src'; +import {useChatAdapter as useNlbridgeChatAdapter} from '@nlux-dev/nlbridge-react/src'; +// import {createUnsafeChatAdapter as useOpenAiChatAdapter} from '@nlux-dev/openai/src'; +import {AiChat, ChatItem, FetchResponseComponentProps, StreamResponseComponentProps} from '@nlux-dev/react/src'; import './App.css'; function App() { - const nlBridge = useChatAdapter({ + const nlBridgeAdapter = useNlbridgeChatAdapter({ url: 'http://localhost:8899/', }); - const langChainAdapter = useChatLangChainAdapter({ + const langChainAdapter = useChatLangChainChatAdapter({ url: 'https://pynlux.api.nlux.ai/einbot', - dataTransferMode: 'fetch', + dataTransferMode: 'stream', useInputSchema: true, }); - const openAiAdapter = createUnsafeChatAdapter() - .withApiKey(localStorage.getItem('openai-api-key') || 'N/A') - .withDataTransferMode('fetch'); + const hfAdapter = useHfChatAdapter({ + dataTransferMode: 'fetch', + model: 'gpt4', + authToken: 'N/A', + }); + + // const openAiAdapter = useOpenAiChatAdapter() + // .withApiKey(localStorage.getItem('openai-api-key') || 'N/A') + // .withDataTransferMode('fetch'); const longMessage = 'Hello, [how can I help you](http://questions.com)? This is going to be a very long greeting ' + 'It is so long that it will be split into multiple lines. It will also showcase that no ' @@ -60,9 +67,10 @@ function App() { return ( { @@ -97,15 +105,18 @@ function App() {
); }, - responseComponent: ({ - response, - status, - containerRef, - }) => { - console.log(status); + responseComponent: (props) => { + const {dataTransferMode} = props; + const propsForFetch = props as FetchResponseComponentProps; + const propsForStream = props as StreamResponseComponentProps; + + console.log('Response Component Props'); + console.dir(props); + return ( -
- {response ?
{response}
:
} + <> + {(dataTransferMode === 'fetch') &&
{propsForFetch.content}
} + {(dataTransferMode === 'stream') &&
}
Custom Response Component
-
+ ); }, }} diff --git a/samples/components/react/src/comp/AiChatReactExpo.tsx b/samples/components/react/src/comp/AiChatReactExpo.tsx index 66d9b3c2..aa679157 100644 --- a/samples/components/react/src/comp/AiChatReactExpo.tsx +++ b/samples/components/react/src/comp/AiChatReactExpo.tsx @@ -1,9 +1,14 @@ -import {DataTransferMode, PersonaOptions, ResponseComponentProps} from '@nlux-dev/react/src'; -import {AiChat} from '@nlux-dev/react/src/exports/AiChat.tsx'; -import {AiChatProps} from '@nlux-dev/react/src/exports/props.tsx'; +import { + AiChat, + AiChatProps, + DataTransferMode, + FetchResponseComponentProps, + PersonaOptions, + ResponseComponentProps, +} from '@nlux-dev/react/src'; import {ChatItem} from '@nlux/core'; import {useChatAdapter} from '@nlux/langchain-react'; -import {FunctionComponent, useMemo, useState} from 'react'; +import {FC, useMemo, useState} from 'react'; import '@nlux-dev/themes/src/luna/theme.css'; type MessageObjectType = {txt: string, color: string, bg: string}; @@ -11,18 +16,23 @@ type MessageObjectType = {txt: string, color: string, bg: string}; const possibleColors = ['red', 'green', 'blue', 'yellow', 'purple']; const possibleBackgrounds = ['white', 'black', 'gray', 'lightgray', 'darkgray']; -const CustomMessageComponent: FunctionComponent< +const CustomMessageComponent: FC< ResponseComponentProps -> = ( - {response}, -) => { +> = (props) => { const color = useMemo(() => possibleColors[Math.floor(Math.random() * possibleColors.length)], []); const bg = useMemo(() => possibleBackgrounds[Math.floor(Math.random() * possibleBackgrounds.length)], []); - if (typeof response === 'object' && response?.txt !== undefined) { + if (props.dataTransferMode === 'stream') { + // This custom component does not support streaming mode + return null; + } + + const {content} = props as FetchResponseComponentProps; + + if (typeof content === 'object' && content?.txt !== undefined) { return ( -
- {response.txt} +
+ {content.txt}
); } @@ -32,7 +42,7 @@ const CustomMessageComponent: FunctionComponent< color, backgroundColor: bg, }}> - {`${response}`} + {`${content}`}
); }; diff --git a/samples/components/react/src/comp/ChatItemReactExpo.tsx b/samples/components/react/src/comp/ChatItemReactExpo.tsx index e77719f4..9b26e0ed 100644 --- a/samples/components/react/src/comp/ChatItemReactExpo.tsx +++ b/samples/components/react/src/comp/ChatItemReactExpo.tsx @@ -58,9 +58,10 @@ export const ChatItemReactExpo = () => {