From 852976113449a63d318c3dcb5c3dab024103d8f4 Mon Sep 17 00:00:00 2001 From: Joseph Izang Date: Wed, 7 May 2025 13:23:00 +0100 Subject: [PATCH 1/6] working on customizations --- .tool-versions | 2 +- packages/react/core/src/exports/AiChat.tsx | 27 +- packages/react/core/src/exports/AiChatUI.tsx | 14 +- .../core/src/exports/elements/Composer.tsx | 9 + packages/react/core/src/exports/props.tsx | 20 +- .../src/sections/Composer/RemixComposer.tsx | 151 ++ .../react/core/src/sections/Composer/props.ts | 53 +- pipeline/npm/versions.json | 2 +- yarn.lock | 1653 ++++++++++++++++- 9 files changed, 1877 insertions(+), 54 deletions(-) create mode 100644 packages/react/core/src/exports/elements/Composer.tsx create mode 100644 packages/react/core/src/sections/Composer/RemixComposer.tsx diff --git a/.tool-versions b/.tool-versions index 584fa7a3..dff218a2 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -nodejs 20.17.0 +nodejs 20.18.1 yarn 1.22.22 diff --git a/packages/react/core/src/exports/AiChat.tsx b/packages/react/core/src/exports/AiChat.tsx index 18d50e64..1acacb35 100644 --- a/packages/react/core/src/exports/AiChat.tsx +++ b/packages/react/core/src/exports/AiChat.tsx @@ -26,6 +26,7 @@ import {useSubmitPromptHandler} from './hooks/useSubmitPromptHandler'; import {useUiOverrides} from './hooks/useUiOverrides'; import {usMarkdownContainers} from './hooks/usMarkdownContainers'; import {AiChatProps} from './props'; +import { RemixComposerComp } from '../sections/Composer/RemixComposer'; export const AiChat: ( props: AiChatProps, @@ -228,19 +229,19 @@ export const AiChat: ( />
- + {uiOverrides.Composer ? ( + uiOverrides.Composer + ) : ( + + )}
diff --git a/packages/react/core/src/exports/AiChatUI.tsx b/packages/react/core/src/exports/AiChatUI.tsx index d4759edf..ff473527 100644 --- a/packages/react/core/src/exports/AiChatUI.tsx +++ b/packages/react/core/src/exports/AiChatUI.tsx @@ -1,10 +1,11 @@ -import {ReactElement} from 'react'; -import {Greeting} from './elements/Greeting'; -import {Loader} from './elements/Loader'; - +import {ReactElement} from 'react' +import {Greeting} from './elements/Greeting' +import {Loader} from './elements/Loader' +import {Composer} from './elements/Composer' export type AiChatUIOverrides = { - Loader: ReactElement; - Greeting?: ReactElement; + Loader: ReactElement + Greeting?: ReactElement + Composer?: ReactElement } /** @@ -13,4 +14,5 @@ export type AiChatUIOverrides = { export const AiChatUI = { Loader, Greeting, + Composer, }; diff --git a/packages/react/core/src/exports/elements/Composer.tsx b/packages/react/core/src/exports/elements/Composer.tsx new file mode 100644 index 00000000..528193b0 --- /dev/null +++ b/packages/react/core/src/exports/elements/Composer.tsx @@ -0,0 +1,9 @@ +import {FunctionComponent, PropsWithChildren} from 'react' + +/** + * A wrapper around a user-provided Composer component. + * When used, it will override the default Composer component. + */ +export const Composer: FunctionComponent = (props) => { + return <>{props.children} +}; diff --git a/packages/react/core/src/exports/props.tsx b/packages/react/core/src/exports/props.tsx index 5b2f9829..babedf68 100644 --- a/packages/react/core/src/exports/props.tsx +++ b/packages/react/core/src/exports/props.tsx @@ -1,12 +1,12 @@ -import {ComposerOptions, DisplayOptions, EventsConfig, StandardChatAdapter} from '@nlux/core'; -import {ChatAdapterBuilder} from '@shared/types/adapters/chat/chatAdapterBuilder'; -import {ChatItem} from '@shared/types/conversation'; -import {ReactNode} from 'react'; -import {ChatAdapter} from '../types/chatAdapter'; -import {ConversationOptions} from '../types/conversationOptions'; -import {AiChatApi} from './hooks/useAiChatApi'; -import {MessageOptions} from './messageOptions'; -import {PersonaOptions} from './personaOptions'; +import {ComposerOptions, DisplayOptions, EventsConfig, RemixComposerOptions, StandardChatAdapter} from '@nlux/core' +import {ChatAdapterBuilder} from '@shared/types/adapters/chat/chatAdapterBuilder' +import {ChatItem} from '@shared/types/conversation' +import {ReactNode} from 'react' +import {ChatAdapter} from '../types/chatAdapter' +import {ConversationOptions} from '../types/conversationOptions' +import {AiChatApi} from './hooks/useAiChatApi' +import {MessageOptions} from './messageOptions' +import {PersonaOptions} from './personaOptions' /** * Props for the AiChat React component. @@ -58,7 +58,7 @@ export type AiChatProps = { /** * Options for the composer. */ - composerOptions?: ComposerOptions; + composerOptions?: RemixComposerOptions /** * Options for the persona. diff --git a/packages/react/core/src/sections/Composer/RemixComposer.tsx b/packages/react/core/src/sections/Composer/RemixComposer.tsx new file mode 100644 index 00000000..c195c9b3 --- /dev/null +++ b/packages/react/core/src/sections/Composer/RemixComposer.tsx @@ -0,0 +1,151 @@ +import {className as compComposerClassName} from '@shared/components/Composer/create' +import {ComposerStatus} from '@shared/components/Composer/props' +import { + statusClassName as compComposerStatusClassName, +} from '@shared/components/Composer/utils/applyNewStatusClassName' +import {isSubmitShortcutKey} from '@shared/utils/isSubmitShortcutKey' +import {ChangeEvent, KeyboardEvent, useEffect, useMemo, useRef, useState} from 'react' +import {CancelIconComp} from '../../components/CancelIcon/CancelIconComp' +import {SendIconComp} from '../../components/SendIcon/SendIconComp' +import {RemixComposerProps} from './props' + +const submittingPromptStatuses: Array = [ + 'submitting-prompt', + 'submitting-edit', + 'submitting-conversation-starter', + 'submitting-external-message', +] + +export const RemixComposerComp = (props: RemixComposerProps) => { + const compClassNameFromStats = compComposerStatusClassName[props.status] || '' + const className = `${compComposerStatusClassName} ${compClassNameFromStats}` + const [selectContext, setSelectContext] = useState(false) + const disableTextarea = submittingPromptStatuses.includes(props.status); + const disableButton = !props.hasValidInput || props.status === 'waiting' || submittingPromptStatuses.includes( + props.status); + const showSendIcon = props.status === 'typing' || props.status === 'waiting'; + const hideCancelButton = props.hideStopButton === true; + const showCancelButton = !hideCancelButton && (submittingPromptStatuses.includes(props.status) || props.status + === 'waiting'); + + const textareaRef = useRef(null); + useEffect(() => { + if (props.status === 'typing' && props.autoFocus && textareaRef.current) { + textareaRef.current.focus(); + } + }, [props.status, props.autoFocus, textareaRef.current]); + + const handleChange = useMemo(() => (e: ChangeEvent) => { + props.onChange?.(e.target.value); + }, [props.onChange]); + + const handleSubmit = useMemo(() => () => { + props.onSubmit?.(); + }, [props.onSubmit]); + + const handleKeyDown = useMemo(() => (e: KeyboardEvent) => { + if (isSubmitShortcutKey(e, props.submitShortcut)) { + e.preventDefault(); + handleSubmit(); + } + }, [handleSubmit, props.submitShortcut]); + + useEffect(() => { + if (!textareaRef.current) { + return; + } + const adjustHeight = () => { + const textarea = textareaRef.current; + if (textarea) { + textarea.style.height = 'auto'; // Reset height + textarea.style.height = `${textarea.scrollHeight}px`; // Set new height based on content + } + }; + textareaRef.current.addEventListener('input', adjustHeight); + return () => { + textareaRef.current?.removeEventListener('input', adjustHeight); + }; + + }, [textareaRef.current]); + + return ( +
+ {selectContext &&
+
Add context files
+
    +
  • +
    + {}} /> + +
    +
  • +
  • +
    + {}} /> + +
    +
  • +
  • +
    + {}} /> + +
    +
  • +
+
} +
+
+ +
+