Skip to content

Commit

Permalink
🗨️ refactor(VariableForm): use InputCombobox, fix Dropdown Variables (
Browse files Browse the repository at this point in the history
#3692)

* feat: Add SimpleCombobox component

* feat: Add labelClassName and add manual focus handling

* feat: Update VariableForm component to use SimpleCombobox

The VariableForm component in the client/src/components/Prompts/Groups/VariableForm.tsx file has been updated to use the SimpleCombobox component instead of the InputWithDropdown component. This change improves the functionality and styling of the form.

* chore: Update VariableForm component placeholder text

* refactor: Improve VariableForm component

The VariableForm component in the client/src/components/Prompts/Groups/VariableForm.tsx file has been refactored to improve its functionality. The `parseFieldConfig` function now trims the `variable` string before processing it. Additionally, the `onSubmit` function now properly escapes potential regex special chars that may cause issues when replacing text

* refactor: Improve VariableForm using ariakit helpers/custom fields, open menu on input focus

* refactor: rename SimpleCombobox to InputCombobox
  • Loading branch information
danny-avila authored Aug 19, 2024
1 parent 8ca1e4f commit 598e2be
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 14 deletions.
40 changes: 27 additions & 13 deletions client/src/components/Prompts/Groups/VariableForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
extractVariableInfo,
} from '~/utils';
import { useAuthContext, useLocalize, useSubmitMessage } from '~/hooks';
import { TextareaAutosize, InputWithDropdown } from '~/components/ui';
import { TextareaAutosize, InputCombobox } from '~/components/ui';
import { code } from '~/components/Chat/Messages/Content/Markdown';

type FieldType = 'text' | 'select';
Expand Down Expand Up @@ -51,11 +51,15 @@ type FormValues = {
*/

const parseFieldConfig = (variable: string): FieldConfig => {
const content = variable;
const content = variable.trim();
if (content.includes(':')) {
const [name, options] = content.split(':');
if (options && options.includes('|')) {
return { variable: name, type: 'select', options: options.split('|') };
return {
variable: name.trim(),
type: 'select',
options: options.split('|').map((opt) => opt.trim()),
};
}
}
return { variable: content, type: 'text' };
Expand Down Expand Up @@ -121,10 +125,13 @@ export default function VariableForm({
const onSubmit = (data: FormValues) => {
let text = mainText;
data.fields.forEach(({ variable, value }) => {
if (value) {
const regex = new RegExp(variable, 'g');
text = text.replace(regex, value);
if (!value) {
return;
}

const escapedVariable = variable.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
const regex = new RegExp(escapedVariable, 'g');
text = text.replace(regex, value);
});

submitPrompt(text);
Expand Down Expand Up @@ -153,22 +160,29 @@ export default function VariableForm({
<Controller
name={`fields.${index}.value`}
control={control}
render={({ field: inputField }) => {
render={({ field: { onChange, onBlur, value, ref } }) => {
if (field.config.type === 'select') {
return (
<InputWithDropdown
{...inputField}
id={`fields.${index}.value`}
className={cn(defaultTextProps, 'focus:bg-surface-tertiary')}
placeholder={localize('com_ui_enter_var', field.config.variable)}
<InputCombobox
options={field.config.options || []}
placeholder={localize('com_ui_enter_var', field.config.variable)}
className={cn(
defaultTextProps,
'rounded px-3 py-2 focus:bg-surface-tertiary',
)}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
);
}

return (
<TextareaAutosize
{...inputField}
ref={ref}
value={value}
onChange={onChange}
onBlur={onBlur}
id={`fields.${index}.value`}
className={cn(
defaultTextProps,
Expand Down
105 changes: 105 additions & 0 deletions client/src/components/ui/InputCombobox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React from 'react';
import * as Ariakit from '@ariakit/react';
import type { OptionWithIcon } from '~/common';
import { cn } from '~/utils';

type ComboboxProps = {
label?: string;
placeholder?: string;
options: OptionWithIcon[] | string[];
className?: string;
labelClassName?: string;
value: string;
onChange: (value: string) => void;
onBlur: () => void;
};

export const InputCombobox: React.FC<ComboboxProps> = ({
label,
labelClassName,
placeholder = 'Select an option',
options,
className,
value,
onChange,
onBlur,
}) => {
const isOptionObject = (option: unknown): option is OptionWithIcon => {
return option != null && typeof option === 'object' && 'value' in option;
};

const [isOpen, setIsOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState(value);
const [isKeyboardFocus, setIsKeyboardFocus] = React.useState(false);

React.useEffect(() => {
setInputValue(value);
}, [value]);

const handleChange = (newValue: string) => {
setInputValue(newValue);
onChange(newValue);
};

return (
<Ariakit.ComboboxProvider value={inputValue} setValue={handleChange}>
{label != null && (
<Ariakit.ComboboxLabel
className={cn('mb-2 block text-sm font-medium text-text-primary', labelClassName ?? '')}
>
{label}
</Ariakit.ComboboxLabel>
)}
<div className={cn('relative', isKeyboardFocus ? 'rounded-md ring-2 ring-ring-primary' : '')}>
<Ariakit.Combobox
placeholder={placeholder}
className={cn(
'h-10 w-full rounded-md border border-border-light bg-surface-primary px-3 py-2 text-sm',
'placeholder-text-secondary hover:bg-surface-hover',
'focus:outline-none',
className,
)}
onChange={(event) => handleChange(event.target.value)}
onBlur={() => {
setIsKeyboardFocus(false);
onBlur();
}}
onFocusVisible={() => {
setIsKeyboardFocus(true);
setIsOpen(true);
}}
onMouseDown={() => {
setIsKeyboardFocus(false);
}}
/>
</div>
<Ariakit.ComboboxPopover
gutter={4}
sameWidth
open={isOpen}
onClose={() => setIsOpen(false)}
className={cn(
'z-50 max-h-60 w-full overflow-auto rounded-md bg-surface-primary p-1 shadow-lg',
'animate-in fade-in-0 zoom-in-95',
)}
>
{options.map((option: string | OptionWithIcon, index: number) => (
<Ariakit.ComboboxItem
key={index}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
'cursor-pointer hover:bg-surface-tertiary hover:text-text-primary',
'data-[active-item]:bg-surface-tertiary data-[active-item]:text-text-primary',
)}
value={isOptionObject(option) ? `${option.value ?? ''}` : option}
>
{isOptionObject(option) && option.icon != null && (
<span className="mr-2 flex-shrink-0">{option.icon}</span>
)}
{isOptionObject(option) ? option.label : option}
</Ariakit.ComboboxItem>
))}
</Ariakit.ComboboxPopover>
</Ariakit.ComboboxProvider>
);
};
3 changes: 2 additions & 1 deletion client/src/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './Prompt';
export * from './QuestionMark';
export * from './Slider';
export * from './Separator';
export * from './InputCombobox';
export * from './Skeleton';
export * from './Switch';
export * from './Table';
Expand All @@ -32,4 +33,4 @@ export { default as SelectDropDown } from './SelectDropDown';
export { default as MultiSelectPop } from './MultiSelectPop';
export { default as InputWithDropdown } from './InputWithDropDown';
export { default as SelectDropDownPop } from './SelectDropDownPop';
export { default as MultiSelectDropDown } from './MultiSelectDropDown';
export { default as MultiSelectDropDown } from './MultiSelectDropDown';

0 comments on commit 598e2be

Please sign in to comment.