feat(ui): improve ux on TI autcomplete

- cursor reinserts at the end of the trigger
- `enter` closes the select
- popover styling
This commit is contained in:
psychedelicious
2023-07-06 14:56:37 +10:00
parent 2415dc1235
commit fbd6b25b4d
3 changed files with 79 additions and 31 deletions

View File

@ -68,17 +68,26 @@ const ParamEmbeddingPopover = (props: Props) => {
return ( return (
<Popover <Popover
initialFocusRef={inputRef} initialFocusRef={inputRef}
returnFocusOnClose={true}
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
placement="bottom" placement="bottom"
openDelay={0} openDelay={0}
closeDelay={0} closeDelay={0}
closeOnBlur={true}
returnFocusOnClose={true}
> >
<PopoverTrigger>{children}</PopoverTrigger> <PopoverTrigger>{children}</PopoverTrigger>
<PopoverContent sx={{ p: 0, top: -1, shadow: 'dark-lg' }}> <PopoverContent
sx={{
p: 0,
top: -1,
shadow: 'dark-lg',
bg: 'accent.300',
_dark: { bg: 'accent.400' },
}}
>
<PopoverBody <PopoverBody
sx={{ p: 1, w: `calc(${PARAMETERS_PANEL_WIDTH} - 2rem )` }} sx={{ p: 0.5, w: `calc(${PARAMETERS_PANEL_WIDTH} - 2rem )` }}
> >
<IAIMantineMultiSelect <IAIMantineMultiSelect
inputRef={inputRef} inputRef={inputRef}

View File

@ -6,6 +6,7 @@ import AddEmbeddingButton from 'features/embedding/components/AddEmbeddingButton
import ParamEmbeddingPopover from 'features/embedding/components/ParamEmbeddingPopover'; import ParamEmbeddingPopover from 'features/embedding/components/ParamEmbeddingPopover';
import { setNegativePrompt } from 'features/parameters/store/generationSlice'; import { setNegativePrompt } from 'features/parameters/store/generationSlice';
import { ChangeEvent, KeyboardEvent, useCallback, useRef } from 'react'; import { ChangeEvent, KeyboardEvent, useCallback, useRef } from 'react';
import { flushSync } from 'react-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const ParamNegativeConditioning = () => { const ParamNegativeConditioning = () => {
@ -32,9 +33,14 @@ const ParamNegativeConditioning = () => {
[onOpen] [onOpen]
); );
const handleSelect = useCallback( const handleSelectEmbedding = useCallback(
(v: string) => { (v: string) => {
const caret = promptRef.current?.selectionStart; if (!promptRef.current) {
return;
}
// this is where we insert the TI trigger
const caret = promptRef.current.selectionStart;
if (caret === undefined) { if (caret === undefined) {
return; return;
@ -47,11 +53,22 @@ const ParamNegativeConditioning = () => {
} }
newPrompt += `${v}>`; newPrompt += `${v}>`;
// we insert the cursor after the `>`
const finalCaretPos = newPrompt.length;
newPrompt += negativePrompt.slice(caret); newPrompt += negativePrompt.slice(caret);
dispatch(setNegativePrompt(newPrompt)); // must flush dom updates else selection gets reset
flushSync(() => {
dispatch(setNegativePrompt(newPrompt));
});
// set the caret position to just after the TI trigger promptRef.current.selectionStart = finalCaretPos;
promptRef.current.selectionEnd = finalCaretPos;
onClose();
}, },
[dispatch, negativePrompt] [dispatch, onClose, negativePrompt]
); );
return ( return (
@ -59,7 +76,7 @@ const ParamNegativeConditioning = () => {
<ParamEmbeddingPopover <ParamEmbeddingPopover
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
onSelect={handleSelect} onSelect={handleSelectEmbedding}
> >
<IAITextarea <IAITextarea
id="negativePrompt" id="negativePrompt"

View File

@ -17,6 +17,7 @@ import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke';
import AddEmbeddingButton from 'features/embedding/components/AddEmbeddingButton'; import AddEmbeddingButton from 'features/embedding/components/AddEmbeddingButton';
import ParamEmbeddingPopover from 'features/embedding/components/ParamEmbeddingPopover'; import ParamEmbeddingPopover from 'features/embedding/components/ParamEmbeddingPopover';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { flushSync } from 'react-dom';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -45,7 +46,6 @@ const ParamPositiveConditioning = () => {
const promptRef = useRef<HTMLTextAreaElement>(null); const promptRef = useRef<HTMLTextAreaElement>(null);
const { isOpen, onClose, onOpen } = useDisclosure(); const { isOpen, onClose, onOpen } = useDisclosure();
const { t } = useTranslation(); const { t } = useTranslation();
const handleChangePrompt = useCallback( const handleChangePrompt = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => { (e: ChangeEvent<HTMLTextAreaElement>) => {
dispatch(setPositivePrompt(e.target.value)); dispatch(setPositivePrompt(e.target.value));
@ -61,6 +61,45 @@ const ParamPositiveConditioning = () => {
[] []
); );
const handleSelectEmbedding = useCallback(
(v: string) => {
if (!promptRef.current) {
return;
}
// this is where we insert the TI trigger
const caret = promptRef.current.selectionStart;
if (caret === undefined) {
return;
}
let newPrompt = prompt.slice(0, caret);
if (newPrompt[newPrompt.length - 1] !== '<') {
newPrompt += '<';
}
newPrompt += `${v}>`;
// we insert the cursor after the `>`
const finalCaretPos = newPrompt.length;
newPrompt += prompt.slice(caret);
// must flush dom updates else selection gets reset
flushSync(() => {
dispatch(setPositivePrompt(newPrompt));
});
// set the caret position to just after the TI trigger
promptRef.current.selectionStart = finalCaretPos;
promptRef.current.selectionEnd = finalCaretPos;
onClose();
},
[dispatch, onClose, prompt]
);
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLTextAreaElement>) => { (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && e.shiftKey === false && isReady) { if (e.key === 'Enter' && e.shiftKey === false && isReady) {
@ -75,27 +114,10 @@ const ParamPositiveConditioning = () => {
[isReady, dispatch, activeTabName, onOpen] [isReady, dispatch, activeTabName, onOpen]
); );
const handleSelect = useCallback( // const handleSelect = (e: MouseEvent<HTMLTextAreaElement>) => {
(v: string) => { // const target = e.target as HTMLTextAreaElement;
const caret = promptRef.current?.selectionStart; // setCaret({ start: target.selectionStart, end: target.selectionEnd });
// };
if (caret === undefined) {
return;
}
let newPrompt = prompt.slice(0, caret);
if (newPrompt[newPrompt.length - 1] !== '<') {
newPrompt += '<';
}
newPrompt += `${v}>`;
newPrompt += prompt.slice(caret);
dispatch(setPositivePrompt(newPrompt));
},
[dispatch, prompt]
);
return ( return (
<Box> <Box>
@ -103,7 +125,7 @@ const ParamPositiveConditioning = () => {
<ParamEmbeddingPopover <ParamEmbeddingPopover
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
onSelect={handleSelect} onSelect={handleSelectEmbedding}
> >
<IAITextarea <IAITextarea
id="prompt" id="prompt"