chat / client /src /components /SidePanel /Builder /AssistantConversationStarters.tsx
helloya20's picture
Upload 2345 files
f0743f4 verified
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;