| import React, { useRef, useState } from 'react'; |
| import { Plus, X } from 'lucide-react'; |
| import { TooltipAnchor } from '@librechat/client'; |
| import { Transition } from 'react-transition-group'; |
| import { Constants } from 'librechat-data-provider'; |
| import { useLocalize } from '~/hooks'; |
|
|
| interface AssistantConversationStartersProps { |
| field: { |
| value: string[]; |
| onChange: (value: string[]) => void; |
| }; |
| inputClass: string; |
| labelClass: string; |
| } |
|
|
| const AssistantConversationStarters: React.FC<AssistantConversationStartersProps> = ({ |
| field, |
| inputClass, |
| labelClass, |
| }) => { |
| const localize = useLocalize(); |
| const inputRefs = useRef<(HTMLInputElement | null)[]>([]); |
| const nodeRef = useRef(null); |
| const [newStarter, setNewStarter] = useState(''); |
|
|
| const handleAddStarter = () => { |
| if (newStarter.trim() && field.value.length < Constants.MAX_CONVO_STARTERS) { |
| const newValues = [newStarter, ...field.value]; |
| field.onChange(newValues); |
| setNewStarter(''); |
| } |
| }; |
|
|
| const handleDeleteStarter = (index: number) => { |
| const newValues = field.value.filter((_, i) => i !== index); |
| field.onChange(newValues); |
| }; |
| const defaultStyle = { |
| transition: 'opacity 200ms ease-in-out', |
| opacity: 0, |
| }; |
|
|
| const triggerShake = (element: HTMLElement) => { |
| element.classList.remove('shake'); |
| void element.offsetWidth; |
| element.classList.add('shake'); |
| setTimeout(() => { |
| element.classList.remove('shake'); |
| }, 200); |
| }; |
|
|
| const transitionStyles = { |
| entering: { opacity: 1 }, |
| entered: { opacity: 1 }, |
| exiting: { opacity: 0 }, |
| exited: { opacity: 0 }, |
| }; |
|
|
| const hasReachedMax = field.value.length >= Constants.MAX_CONVO_STARTERS; |
|
|
| const addConversationStarterLabel = hasReachedMax |
| ? localize('com_assistants_max_starters_reached') |
| : localize('com_ui_add'); |
|
|
| return ( |
| <div className="relative"> |
| <label className={labelClass} htmlFor="conversation_starters"> |
| {localize('com_assistants_conversation_starters')} |
| </label> |
| <div className="mt-4 space-y-2"> |
| {/* Persistent starter, used for creating only */} |
| <div className="relative"> |
| <input |
| ref={(el) => (inputRefs.current[0] = el)} |
| value={newStarter} |
| maxLength={64} |
| className={`${inputClass} pr-10`} |
| type="text" |
| placeholder={ |
| hasReachedMax |
| ? localize('com_assistants_max_starters_reached') |
| : localize('com_assistants_conversation_starters_placeholder') |
| } |
| onChange={(e) => setNewStarter(e.target.value)} |
| onKeyDown={(e) => { |
| if (e.key === 'Enter') { |
| e.preventDefault(); |
| if (hasReachedMax) { |
| triggerShake(e.currentTarget); |
| } else { |
| handleAddStarter(); |
| } |
| } |
| }} |
| /> |
| <Transition |
| nodeRef={nodeRef} |
| in={field.value.length < Constants.MAX_CONVO_STARTERS} |
| timeout={200} |
| unmountOnExit |
| > |
| {(state: string) => ( |
| <div |
| ref={nodeRef} |
| style={{ |
| ...defaultStyle, |
| ...transitionStyles[state as keyof typeof transitionStyles], |
| transition: state === 'entering' ? 'none' : defaultStyle.transition, |
| }} |
| className="absolute right-1 top-1" |
| > |
| <TooltipAnchor |
| side="top" |
| description={addConversationStarterLabel} |
| aria-label={addConversationStarterLabel} |
| className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover" |
| onClick={handleAddStarter} |
| disabled={hasReachedMax} |
| > |
| <Plus className="size-4" /> |
| </TooltipAnchor> |
| </div> |
| )} |
| </Transition> |
| </div> |
| {field.value.map((starter, index) => ( |
| <div key={index} className="relative"> |
| <input |
| ref={(el) => (inputRefs.current[index + 1] = el)} |
| value={starter} |
| onChange={(e) => { |
| const newValue = [...field.value]; |
| newValue[index] = e.target.value; |
| field.onChange(newValue); |
| }} |
| className={`${inputClass} pr-10`} |
| type="text" |
| maxLength={64} |
| /> |
| <TooltipAnchor |
| side="top" |
| description={localize('com_ui_delete')} |
| aria-label={localize('com_ui_delete')} |
| className="absolute right-1 top-1 flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover" |
| onClick={() => handleDeleteStarter(index)} |
| > |
| <X className="size-4" /> |
| </TooltipAnchor> |
| </div> |
| ))} |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default AssistantConversationStarters; |
|
|