diff --git a/frontend/src/components/chat/MessageComposer/Input.tsx b/frontend/src/components/chat/MessageComposer/Input.tsx index 33f4a5d53f..a850b1510c 100644 --- a/frontend/src/components/chat/MessageComposer/Input.tsx +++ b/frontend/src/components/chat/MessageComposer/Input.tsx @@ -8,7 +8,7 @@ import React, { } from 'react'; import { useRecoilValue } from 'recoil'; -import { ICommand, commandsState } from '@chainlit/react-client'; +import { ICommand, commandsState, useChatData } from '@chainlit/react-client'; import AutoResizeTextarea from '@/components/AutoResizeTextarea'; import Icon from '@/components/Icon'; @@ -28,6 +28,8 @@ interface Props { placeholder?: string; selectedCommand?: ICommand; setSelectedCommand: (command: ICommand | undefined) => void; + selectedSetting?: any; + setSelectedSetting: (setting: any | undefined) => void; onChange: (value: string) => void; onPaste?: (event: any) => void; onEnter?: () => void; @@ -46,6 +48,7 @@ const Input = forwardRef( autoFocus, selectedCommand, setSelectedCommand, + setSelectedSetting, onChange, onEnter, onPaste @@ -53,9 +56,16 @@ const Input = forwardRef( ref ) => { const commands = useRecoilValue(commandsState); + const { chatSettingsInputs } = useChatData(); const [isComposing, setIsComposing] = useState(false); const [showCommands, setShowCommands] = useState(false); + const [showSettings, setShowSettings] = useState(false); + const [showSettingValues, setShowSettingValues] = useState(false); const [commandInput, setCommandInput] = useState(''); + const [settingInput, setSettingInput] = useState(''); + const [settingValueInput, setSettingValueInput] = useState(''); + const [tempSelectedSetting, setTempSelectedSetting] = + useState(undefined); const [value, setValue] = useState(''); const textareaRef = useRef(null); @@ -69,11 +79,51 @@ const Input = forwardRef( return indexA - indexB; }); + const normalizedSettingInput = settingInput.toLowerCase().slice(1); + + const filteredSettings = chatSettingsInputs + .filter((setting: any) => { + // Only show settings that have items/values (like Select) + const hasValues = setting.items && setting.items.length > 0; + const matchesSearch = setting.id + .toLowerCase() + .includes(normalizedSettingInput); + + return hasValues && matchesSearch; + }) + .sort((a: any, b: any) => { + const indexA = a.id.toLowerCase().indexOf(normalizedSettingInput); + const indexB = b.id.toLowerCase().indexOf(normalizedSettingInput); + return indexA - indexB; + }); + + const normalizedSettingValueInput = settingValueInput + .toLowerCase() + .slice(1); + + const filteredSettingValues = tempSelectedSetting?.items + ? tempSelectedSetting.items + .filter( + (item: any) => + item.label.toLowerCase().includes(normalizedSettingValueInput) || + item.value.toLowerCase().includes(normalizedSettingValueInput) + ) + .sort((a: any, b: any) => { + const indexA = a.label + .toLowerCase() + .indexOf(normalizedSettingValueInput); + const indexB = b.label + .toLowerCase() + .indexOf(normalizedSettingValueInput); + return indexA - indexB; + }) + : []; + const { - selectedIndex, - handleMouseMove, - handleMouseLeave, - handleKeyDown: navigationKeyDown + selectedIndex: commandSelectedIndex, + handleMouseMove: commandHandleMouseMove, + handleMouseLeave: commandHandleMouseLeave, + handleKeyDown: commandNavigationKeyDown } = useCommandNavigation({ items: filteredCommands, isOpen: showCommands, @@ -86,13 +136,54 @@ const Input = forwardRef( } }); + const { + selectedIndex: settingSelectedIndex, + handleMouseMove: settingHandleMouseMove, + handleMouseLeave: settingHandleMouseLeave, + handleKeyDown: settingNavigationKeyDown + } = useCommandNavigation({ + items: filteredSettings, + isOpen: showSettings, + onSelect: (setting) => { + handleSettingSelect(setting); + }, + onClose: () => { + setShowSettings(false); + setSettingInput(''); + } + }); + + const { + selectedIndex: settingValueSelectedIndex, + handleMouseMove: settingValueHandleMouseMove, + handleMouseLeave: settingValueHandleMouseLeave, + handleKeyDown: settingValueNavigationKeyDown + } = useCommandNavigation({ + items: filteredSettingValues, + isOpen: showSettingValues, + onSelect: (valueItem) => { + handleSettingValueSelect(valueItem); + }, + onClose: () => { + setShowSettingValues(false); + setSettingValueInput(''); + setTempSelectedSetting(undefined); + } + }); + const reset = () => { setValue(''); if (!selectedCommand?.persistent) { setSelectedCommand(undefined); } + setSelectedSetting(undefined); + setTempSelectedSetting(undefined); setCommandInput(''); + setSettingInput(''); + setSettingValueInput(''); setShowCommands(false); + setShowSettings(false); + setShowSettingValues(false); onChange(''); }; @@ -113,12 +204,71 @@ const Input = forwardRef( // Command detection for dropdown const words = newValue.split(' '); - if (words.length === 1 && words[0].startsWith('/')) { + const firstWord = words[0]; + + if (words.length === 1 && firstWord.startsWith('/')) { setShowCommands(true); - setCommandInput(words[0]); + setShowSettings(false); + setShowSettingValues(false); + setCommandInput(firstWord); + setSettingInput(''); + setSettingValueInput(''); + } else if (words.length === 1 && firstWord.startsWith('@')) { + // Check if it's @setting/value pattern + const parts = firstWord.split('/'); + + if (parts.length === 1) { + // Just @setting - check how many settings have values + const settingsWithValues = chatSettingsInputs.filter( + (setting: any) => setting.items && setting.items.length > 0 + ); + + if (settingsWithValues.length === 1) { + // Only one setting with values - skip to showing values directly + const singleSetting = settingsWithValues[0]; + setTempSelectedSetting(singleSetting); + setShowSettingValues(true); + setShowSettings(false); + setShowCommands(false); + setSettingInput(''); + setCommandInput(''); + setSettingValueInput(firstWord); + } else { + // Multiple settings - show settings dropdown + setShowSettings(true); + setShowCommands(false); + setShowSettingValues(false); + setSettingInput(firstWord); + setCommandInput(''); + setSettingValueInput(''); + setTempSelectedSetting(undefined); + } + } else if (parts.length === 2) { + // @setting/value - show values dropdown + const settingPart = parts[0]; + const valuePart = parts[1]; + const setting = chatSettingsInputs.find( + (s: any) => + s.id.toLowerCase() === settingPart.slice(1).toLowerCase() + ); + + if (setting && setting.items && setting.items.length > 0) { + setTempSelectedSetting(setting); + setShowSettingValues(true); + setShowSettings(false); + setShowCommands(false); + setSettingInput(''); + setCommandInput(''); + setSettingValueInput('/' + valuePart); + } + } } else { setShowCommands(false); + setShowSettings(false); + setShowSettingValues(false); setCommandInput(''); + setSettingInput(''); + setSettingValueInput(''); } }; @@ -139,23 +289,102 @@ const Input = forwardRef( }, 0); }; + const handleSettingSelect = (setting: any) => { + setShowSettings(false); + + // Check if setting has values/items to select from + if (setting.items && setting.items.length > 0) { + // Setting has values - show value selection by adding / + setTempSelectedSetting(setting); + const newValue = `@${setting.id}/`; + setValue(newValue); + onChange(newValue); + setSettingInput(''); + setSettingValueInput('/'); + setShowSettingValues(true); + + // Focus back on textarea + setTimeout(() => { + textareaRef.current?.focus(); + }, 0); + } else { + // Setting has no values - use default/initial value + setSelectedSetting({ + ...setting, + selectedValue: setting.initial + }); + + // Remove the setting text from the input + const newValue = value.replace(settingInput, '').trimStart(); + setValue(newValue); + onChange(newValue); + + setSettingInput(''); + + // Focus back on textarea + setTimeout(() => { + textareaRef.current?.focus(); + }, 0); + } + }; + + const handleSettingValueSelect = (valueItem: any) => { + if (!tempSelectedSetting) return; + + setShowSettingValues(false); + setSelectedSetting({ + ...tempSelectedSetting, + selectedValue: valueItem.value, + selectedLabel: valueItem.label + }); + + setValue(''); + onChange(''); + + setSettingValueInput(''); + setTempSelectedSetting(undefined); + + // Focus back on textarea + setTimeout(() => { + textareaRef.current?.focus(); + }, 0); + }; + const handleKeyDown = (e: React.KeyboardEvent) => { // Handle command selection - check this FIRST before other key handling if (showCommands && filteredCommands.length > 0) { - navigationKeyDown(e); + commandNavigationKeyDown(e); // If the navigation handled the key, don't process further if (e.defaultPrevented) { return; } } - // Handle regular enter only if command menu is not showing + // Handle setting selection + if (showSettings && filteredSettings.length > 0) { + settingNavigationKeyDown(e); + if (e.defaultPrevented) { + return; + } + } + + // Handle setting value selection + if (showSettingValues && filteredSettingValues.length > 0) { + settingValueNavigationKeyDown(e); + if (e.defaultPrevented) { + return; + } + } + + // Handle regular enter only if no menu is showing if ( e.key === 'Enter' && !e.shiftKey && onEnter && !isComposing && - !showCommands + !showCommands && + !showSettings && + !showSettingValues ) { e.preventDefault(); onEnter(); @@ -185,7 +414,7 @@ const Input = forwardRef( {showCommands && filteredCommands.length > 0 && (
@@ -194,8 +423,8 @@ const Input = forwardRef( handleMouseMove(index)} + isSelected={index === commandSelectedIndex} + onMouseMove={() => commandHandleMouseMove(index)} onSelect={() => handleCommandSelect(command)} className="command-item space-x-2" > @@ -203,7 +432,7 @@ const Input = forwardRef( name={command.icon} className={cn( '!size-5 text-muted-foreground transition-transform duration-150', - index === selectedIndex && 'scale-110' + index === commandSelectedIndex && 'scale-110' )} />
@@ -219,6 +448,99 @@ const Input = forwardRef(
)} + + {showSettings && filteredSettings.length > 0 && ( +
+ + + + {filteredSettings.map((setting: any, index: number) => ( + settingHandleMouseMove(index)} + onSelect={() => handleSettingSelect(setting)} + className="command-item space-x-2" + > + +
+
+ {setting.label || setting.id} +
+
+ {setting.description || + setting.tooltip || + 'Chat setting'} + {setting.items && setting.items.length > 0 && ( + + ({setting.items.length} values) + + )} +
+
+
+ ))} +
+
+
+
+ )} + + {showSettingValues && filteredSettingValues.length > 0 && ( +
+ + + +
+ Select value for:{' '} + + {tempSelectedSetting?.label || tempSelectedSetting?.id} + +
+ {filteredSettingValues.map((item: any, index: number) => ( + settingValueHandleMouseMove(index)} + onSelect={() => handleSettingValueSelect(item)} + className="command-item space-x-2" + > + +
+
{item.label}
+ {item.label !== item.value && ( +
+ {item.value} +
+ )} +
+
+ ))} +
+
+
+
+ )}
); } diff --git a/frontend/src/components/chat/MessageComposer/index.tsx b/frontend/src/components/chat/MessageComposer/index.tsx index 9d796bc693..a9cf1e1229 100644 --- a/frontend/src/components/chat/MessageComposer/index.tsx +++ b/frontend/src/components/chat/MessageComposer/index.tsx @@ -49,12 +49,13 @@ export default function MessageComposer({ const [selectedCommand, setSelectedCommand] = useRecoilState( persistentCommandState ); + const [selectedSetting, setSelectedSetting] = useState(undefined); const setChatSettingsOpen = useSetRecoilState(chatSettingsOpenState); const [attachments, setAttachments] = useRecoilState(attachmentsState); const { t } = useTranslation(); const { user } = useAuth(); - const { sendMessage, replyMessage } = useChatInteract(); + const { sendMessage, replyMessage, updateChatSettings } = useChatInteract(); const { askUser, chatSettingsInputs, disabled: _disabled } = useChatData(); const disabled = _disabled || !!attachments.find((a) => !a.uploaded); @@ -84,8 +85,20 @@ export default function MessageComposer({ async ( msg: string, attachments?: IAttachment[], - selectedCommand?: string + selectedCommand?: string, + selectedSetting?: any ) => { + // Apply chat setting if selected + if (selectedSetting) { + const settingValue = { + [selectedSetting.id]: + selectedSetting.selectedValue !== undefined + ? selectedSetting.selectedValue + : selectedSetting.initial + }; + updateChatSettings(settingValue); + } + const message: IStep = { threadId: '', command: selectedCommand, @@ -106,7 +119,7 @@ export default function MessageComposer({ } sendMessage(message, fileReferences); }, - [user, sendMessage, autoScrollRef] + [user, sendMessage, autoScrollRef, updateChatSettings] ); const onReply = useCallback( @@ -132,7 +145,10 @@ export default function MessageComposer({ const submit = useCallback(() => { if ( disabled || - (value.trim() === '' && attachments.length === 0 && !selectedCommand) + (value.trim() === '' && + attachments.length === 0 && + !selectedCommand && + !selectedSetting) ) { return; } @@ -140,10 +156,11 @@ export default function MessageComposer({ if (askUser) { onReply(value); } else { - onSubmit(value, attachments, selectedCommand?.id); + onSubmit(value, attachments, selectedCommand?.id, selectedSetting); } setAttachments([]); + setSelectedSetting(undefined); setValue(''); // Clear the value state inputRef.current?.reset(); }, [ @@ -152,6 +169,7 @@ export default function MessageComposer({ askUser, attachments, selectedCommand, + selectedSetting, setAttachments, onSubmit, onReply @@ -167,12 +185,35 @@ export default function MessageComposer({ ) : null} + {selectedSetting && ( +
+ + + @{selectedSetting.label || selectedSetting.id} + {selectedSetting.selectedValue !== undefined && + selectedSetting.selectedLabel && ( + + /{selectedSetting.selectedLabel} + + )} + + + +
+ )}