mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
🔍 feat: Filter MultiSelect and SelectDropDown (+variants) + CSS fixes for Scrollbar (#2138)
* Initial implementation of MultiSearch. Added implementation to MultiSelect and SelectDropDown and variants * Update scrollbar styles to prevent breakages on Chrome * Revert changes to vite.config.ts (redundant for now) * chore(New Chat): organize imports * style(scrollbar-transparent): use webkit as standard, expected behavior * chore: useCallback for mouse enter/leave * fix(Footer): resolve map key error * chore: memoize Conversations * style(MultiSearch): improve multisearch styling * style: dark mode search input * fix: react warnings due to unrecognize html props * chore: debounce OpenAI settings inputs * fix(useDebouncedInput): only use event value as newValue if not object --------- Co-authored-by: Flynn <gpg@flyn.ca>
This commit is contained in:
parent
f51ac74e12
commit
382b303963
20 changed files with 305 additions and 83 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React from 'react';
|
||||||
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
|
@ -53,12 +54,13 @@ export default function Footer() {
|
||||||
<div className="relative flex items-center justify-center gap-2 px-2 py-2 text-xs text-gray-600 dark:text-gray-300 md:px-[60px]">
|
<div className="relative flex items-center justify-center gap-2 px-2 py-2 text-xs text-gray-600 dark:text-gray-300 md:px-[60px]">
|
||||||
{footerElements.map((contentRender, index) => {
|
{footerElements.map((contentRender, index) => {
|
||||||
const isLastElement = index === footerElements.length - 1;
|
const isLastElement = index === footerElements.length - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<React.Fragment key={`footer-element-${index}`}>
|
||||||
{contentRender}
|
{contentRender}
|
||||||
{!isLastElement && <div className="h-2 border-r-[1px] border-gray-300" />}
|
{!isLastElement && (
|
||||||
</>
|
<div key={`separator-${index}`} className="h-2 border-r-[1px] border-gray-300" />
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo, memo } from 'react';
|
||||||
import { parseISO, isToday } from 'date-fns';
|
import { parseISO, isToday } from 'date-fns';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { TConversation } from 'librechat-data-provider';
|
import { TConversation } from 'librechat-data-provider';
|
||||||
|
|
@ -6,7 +6,7 @@ import { groupConversationsByDate } from '~/utils';
|
||||||
import Conversation from './Conversation';
|
import Conversation from './Conversation';
|
||||||
import Convo from './Convo';
|
import Convo from './Convo';
|
||||||
|
|
||||||
export default function Conversations({
|
const Conversations = ({
|
||||||
conversations,
|
conversations,
|
||||||
moveToTop,
|
moveToTop,
|
||||||
toggleNav,
|
toggleNav,
|
||||||
|
|
@ -14,7 +14,7 @@ export default function Conversations({
|
||||||
conversations: TConversation[];
|
conversations: TConversation[];
|
||||||
moveToTop: () => void;
|
moveToTop: () => void;
|
||||||
toggleNav: () => void;
|
toggleNav: () => void;
|
||||||
}) {
|
}) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { pathname } = location;
|
const { pathname } = location;
|
||||||
const ConvoItem = pathname.includes('chat') ? Conversation : Convo;
|
const ConvoItem = pathname.includes('chat') ? Conversation : Convo;
|
||||||
|
|
@ -64,4 +64,6 @@ export default function Conversations({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default memo(Conversations);
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,13 @@ import {
|
||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
} from '~/components/ui';
|
} from '~/components/ui';
|
||||||
import { cn, defaultTextProps, optionText, removeFocusOutlines } from '~/utils/';
|
import { cn, defaultTextProps, optionText, removeFocusOutlines } from '~/utils/';
|
||||||
|
import { useLocalize, useDebouncedInput } from '~/hooks';
|
||||||
import type { TModelSelectProps } from '~/common';
|
import type { TModelSelectProps } from '~/common';
|
||||||
import OptionHover from './OptionHover';
|
import OptionHover from './OptionHover';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import { ESide } from '~/common';
|
import { ESide } from '~/common';
|
||||||
|
|
||||||
export default function Settings({ conversation, setOption, models, readonly }: TModelSelectProps) {
|
export default function Settings({ conversation, setOption, models, readonly }: TModelSelectProps) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
if (!conversation) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const {
|
const {
|
||||||
endpoint,
|
endpoint,
|
||||||
endpointType,
|
endpointType,
|
||||||
|
|
@ -33,14 +30,43 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
presence_penalty: presP,
|
presence_penalty: presP,
|
||||||
resendImages,
|
resendImages,
|
||||||
imageDetail,
|
imageDetail,
|
||||||
} = conversation;
|
} = conversation ?? {};
|
||||||
|
const [setChatGptLabel, chatGptLabelValue] = useDebouncedInput({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'chatGptLabel',
|
||||||
|
initialValue: chatGptLabel,
|
||||||
|
});
|
||||||
|
const [setPromptPrefix, promptPrefixValue] = useDebouncedInput({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'promptPrefix',
|
||||||
|
initialValue: promptPrefix,
|
||||||
|
});
|
||||||
|
const [setTemperature, temperatureValue] = useDebouncedInput({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'temperature',
|
||||||
|
initialValue: temperature,
|
||||||
|
});
|
||||||
|
const [setTopP, topPValue] = useDebouncedInput({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'top_p',
|
||||||
|
initialValue: topP,
|
||||||
|
});
|
||||||
|
const [setFreqP, freqPValue] = useDebouncedInput({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'frequency_penalty',
|
||||||
|
initialValue: freqP,
|
||||||
|
});
|
||||||
|
const [setPresP, presPValue] = useDebouncedInput({
|
||||||
|
setOption,
|
||||||
|
optionKey: 'presence_penalty',
|
||||||
|
initialValue: presP,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const setModel = setOption('model');
|
const setModel = setOption('model');
|
||||||
const setChatGptLabel = setOption('chatGptLabel');
|
|
||||||
const setPromptPrefix = setOption('promptPrefix');
|
|
||||||
const setTemperature = setOption('temperature');
|
|
||||||
const setTopP = setOption('top_p');
|
|
||||||
const setFreqP = setOption('frequency_penalty');
|
|
||||||
const setPresP = setOption('presence_penalty');
|
|
||||||
const setResendImages = setOption('resendImages');
|
const setResendImages = setOption('resendImages');
|
||||||
const setImageDetail = setOption('imageDetail');
|
const setImageDetail = setOption('imageDetail');
|
||||||
|
|
||||||
|
|
@ -67,8 +93,8 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
<Input
|
<Input
|
||||||
id="chatGptLabel"
|
id="chatGptLabel"
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={chatGptLabel || ''}
|
value={(chatGptLabelValue as string) || ''}
|
||||||
onChange={(e) => setChatGptLabel(e.target.value ?? null)}
|
onChange={setChatGptLabel}
|
||||||
placeholder={localize('com_endpoint_openai_custom_name_placeholder')}
|
placeholder={localize('com_endpoint_openai_custom_name_placeholder')}
|
||||||
className={cn(
|
className={cn(
|
||||||
defaultTextProps,
|
defaultTextProps,
|
||||||
|
|
@ -86,8 +112,8 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
id="promptPrefix"
|
id="promptPrefix"
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={promptPrefix || ''}
|
value={(promptPrefixValue as string) || ''}
|
||||||
onChange={(e) => setPromptPrefix(e.target.value ?? null)}
|
onChange={setPromptPrefix}
|
||||||
placeholder={localize('com_endpoint_openai_prompt_prefix_placeholder')}
|
placeholder={localize('com_endpoint_openai_prompt_prefix_placeholder')}
|
||||||
className={cn(
|
className={cn(
|
||||||
defaultTextProps,
|
defaultTextProps,
|
||||||
|
|
@ -110,8 +136,8 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
<InputNumber
|
<InputNumber
|
||||||
id="temp-int"
|
id="temp-int"
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={temperature}
|
value={temperatureValue as number}
|
||||||
onChange={(value) => setTemperature(Number(value))}
|
onChange={setTemperature}
|
||||||
max={2}
|
max={2}
|
||||||
min={0}
|
min={0}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
|
|
@ -127,7 +153,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={[temperature ?? 1]}
|
value={[(temperatureValue as number) ?? 1]}
|
||||||
onValueChange={(value) => setTemperature(value[0])}
|
onValueChange={(value) => setTemperature(value[0])}
|
||||||
doubleClickHandler={() => setTemperature(1)}
|
doubleClickHandler={() => setTemperature(1)}
|
||||||
max={2}
|
max={2}
|
||||||
|
|
@ -148,7 +174,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
<InputNumber
|
<InputNumber
|
||||||
id="top-p-int"
|
id="top-p-int"
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={topP}
|
value={topPValue as number}
|
||||||
onChange={(value) => setTopP(Number(value))}
|
onChange={(value) => setTopP(Number(value))}
|
||||||
max={1}
|
max={1}
|
||||||
min={0}
|
min={0}
|
||||||
|
|
@ -165,7 +191,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={[topP ?? 1]}
|
value={[(topPValue as number) ?? 1]}
|
||||||
onValueChange={(value) => setTopP(value[0])}
|
onValueChange={(value) => setTopP(value[0])}
|
||||||
doubleClickHandler={() => setTopP(1)}
|
doubleClickHandler={() => setTopP(1)}
|
||||||
max={1}
|
max={1}
|
||||||
|
|
@ -187,7 +213,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
<InputNumber
|
<InputNumber
|
||||||
id="freq-penalty-int"
|
id="freq-penalty-int"
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={freqP}
|
value={freqPValue as number}
|
||||||
onChange={(value) => setFreqP(Number(value))}
|
onChange={(value) => setFreqP(Number(value))}
|
||||||
max={2}
|
max={2}
|
||||||
min={-2}
|
min={-2}
|
||||||
|
|
@ -204,7 +230,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={[freqP ?? 0]}
|
value={[(freqPValue as number) ?? 0]}
|
||||||
onValueChange={(value) => setFreqP(value[0])}
|
onValueChange={(value) => setFreqP(value[0])}
|
||||||
doubleClickHandler={() => setFreqP(0)}
|
doubleClickHandler={() => setFreqP(0)}
|
||||||
max={2}
|
max={2}
|
||||||
|
|
@ -226,7 +252,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
<InputNumber
|
<InputNumber
|
||||||
id="pres-penalty-int"
|
id="pres-penalty-int"
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={presP}
|
value={presPValue as number}
|
||||||
onChange={(value) => setPresP(Number(value))}
|
onChange={(value) => setPresP(Number(value))}
|
||||||
max={2}
|
max={2}
|
||||||
min={-2}
|
min={-2}
|
||||||
|
|
@ -243,7 +269,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={[presP ?? 0]}
|
value={[(presPValue as number) ?? 0]}
|
||||||
onValueChange={(value) => setPresP(value[0])}
|
onValueChange={(value) => setPresP(value[0])}
|
||||||
doubleClickHandler={() => setPresP(0)}
|
doubleClickHandler={() => setPresP(0)}
|
||||||
max={2}
|
max={2}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,14 @@ const Nav = ({ navVisible, setNavVisible }) => {
|
||||||
const [newUser, setNewUser] = useLocalStorage('newUser', true);
|
const [newUser, setNewUser] = useLocalStorage('newUser', true);
|
||||||
const [isToggleHovering, setIsToggleHovering] = useState(false);
|
const [isToggleHovering, setIsToggleHovering] = useState(false);
|
||||||
|
|
||||||
|
const handleMouseEnter = useCallback(() => {
|
||||||
|
setIsHovering(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMouseLeave = useCallback(() => {
|
||||||
|
setIsHovering(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
setNavWidth('320px');
|
setNavWidth('320px');
|
||||||
|
|
@ -144,8 +152,8 @@ const Nav = ({ navVisible, setNavVisible }) => {
|
||||||
'-mr-2 flex-1 flex-col overflow-y-auto pr-2 transition-opacity duration-500',
|
'-mr-2 flex-1 flex-col overflow-y-auto pr-2 transition-opacity duration-500',
|
||||||
isHovering ? '' : 'scrollbar-transparent',
|
isHovering ? '' : 'scrollbar-transparent',
|
||||||
)}
|
)}
|
||||||
onMouseEnter={() => setIsHovering(true)}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={() => setIsHovering(false)}
|
onMouseLeave={handleMouseLeave}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
<NewChat
|
<NewChat
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { EModelEndpoint } from 'librechat-data-provider';
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||||
|
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
|
||||||
import { useLocalize, useNewConvo, useLocalStorage } from '~/hooks';
|
import { useLocalize, useNewConvo, useLocalStorage } from '~/hooks';
|
||||||
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
|
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
|
||||||
import { NewChatIcon } from '~/components/svg';
|
import { NewChatIcon } from '~/components/svg';
|
||||||
import { getEndpointField } from '~/utils';
|
import { getEndpointField } from '~/utils';
|
||||||
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui/';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
export default function NewChat({
|
export default function NewChat({
|
||||||
toggleNav,
|
toggleNav,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,12 @@ export default function CheckMark({ className = '' }: { className?: string }) {
|
||||||
width="1em"
|
width="1em"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.0633 5.67375C18.5196 5.98487 18.6374 6.607 18.3262 7.06331L10.8262 18.0633C10.6585 18.3093 10.3898 18.4678 10.0934 18.4956C9.79688 18.5234 9.50345 18.4176 9.29289 18.2071L4.79289 13.7071C4.40237 13.3166 4.40237 12.6834 4.79289 12.2929C5.18342 11.9023 5.81658 11.9023 6.20711 12.2929L9.85368 15.9394L16.6738 5.93664C16.9849 5.48033 17.607 5.36263 18.0633 5.67375Z" fill="currentColor">
|
<path
|
||||||
</path></svg>
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M18.0633 5.67375C18.5196 5.98487 18.6374 6.607 18.3262 7.06331L10.8262 18.0633C10.6585 18.3093 10.3898 18.4678 10.0934 18.4956C9.79688 18.5234 9.50345 18.4176 9.29289 18.2071L4.79289 13.7071C4.40237 13.3166 4.40237 12.6834 4.79289 12.2929C5.18342 11.9023 5.81658 11.9023 6.20711 12.2929L9.85368 15.9394L16.6738 5.93664C16.9849 5.48033 17.607 5.36263 18.0633 5.67375Z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,12 @@ export default function Clipboard() {
|
||||||
width="1em"
|
width="1em"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4C10.8954 4 10 4.89543 10 6H14C14 4.89543 13.1046 4 12 4ZM8.53513 4C9.22675 2.8044 10.5194 2 12 2C13.4806 2 14.7733 2.8044 15.4649 4H17C18.6569 4 20 5.34315 20 7V19C20 20.6569 18.6569 22 17 22H7C5.34315 22 4 20.6569 4 19V7C4 5.34315 5.34315 4 7 4H8.53513ZM8 6H7C6.44772 6 6 6.44772 6 7V19C6 19.5523 6.44772 20 7 20H17C17.5523 20 18 19.5523 18 19V7C18 6.44772 17.5523 6 17 6H16C16 7.10457 15.1046 8 14 8H10C8.89543 8 8 7.10457 8 6Z" fill="currentColor">
|
<path
|
||||||
</path></svg>
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M12 4C10.8954 4 10 4.89543 10 6H14C14 4.89543 13.1046 4 12 4ZM8.53513 4C9.22675 2.8044 10.5194 2 12 2C13.4806 2 14.7733 2.8044 15.4649 4H17C18.6569 4 20 5.34315 20 7V19C20 20.6569 18.6569 22 17 22H7C5.34315 22 4 20.6569 4 19V7C4 5.34315 5.34315 4 7 4H8.53513ZM8 6H7C6.44772 6 6 6.44772 6 7V19C6 19.5523 6.44772 20 7 20H17C17.5523 20 18 19.5523 18 19V7C18 6.44772 17.5523 6 17 6H16C16 7.10457 15.1046 8 14 8H10C8.89543 8 8 7.10457 8 6Z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,12 @@ export default function EditIcon() {
|
||||||
width="1em"
|
width="1em"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.2929 4.29291C15.0641 2.52167 17.9359 2.52167 19.7071 4.2929C21.4783 6.06414 21.4783 8.93588 19.7071 10.7071L18.7073 11.7069L11.1603 19.2539C10.7182 19.696 10.1489 19.989 9.53219 20.0918L4.1644 20.9864C3.84584 21.0395 3.52125 20.9355 3.29289 20.7071C3.06453 20.4788 2.96051 20.1542 3.0136 19.8356L3.90824 14.4678C4.01103 13.8511 4.30396 13.2818 4.7461 12.8397L13.2929 4.29291ZM13 7.41422L6.16031 14.2539C6.01293 14.4013 5.91529 14.591 5.88102 14.7966L5.21655 18.7835L9.20339 18.119C9.40898 18.0847 9.59872 17.9871 9.7461 17.8397L16.5858 11L13 7.41422ZM18 9.5858L14.4142 6.00001L14.7071 5.70712C15.6973 4.71693 17.3027 4.71693 18.2929 5.70712C19.2831 6.69731 19.2831 8.30272 18.2929 9.29291L18 9.5858Z" fill="currentColor">
|
<path
|
||||||
</path></svg>
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M13.2929 4.29291C15.0641 2.52167 17.9359 2.52167 19.7071 4.2929C21.4783 6.06414 21.4783 8.93588 19.7071 10.7071L18.7073 11.7069L11.1603 19.2539C10.7182 19.696 10.1489 19.989 9.53219 20.0918L4.1644 20.9864C3.84584 21.0395 3.52125 20.9355 3.29289 20.7071C3.06453 20.4788 2.96051 20.1542 3.0136 19.8356L3.90824 14.4678C4.01103 13.8511 4.30396 13.2818 4.7461 12.8397L13.2929 4.29291ZM13 7.41422L6.16031 14.2539C6.01293 14.4013 5.91529 14.591 5.88102 14.7966L5.21655 18.7835L9.20339 18.119C9.40898 18.0847 9.59872 17.9871 9.7461 17.8397L16.5858 11L13 7.41422ZM18 9.5858L14.4142 6.00001L14.7071 5.70712C15.6973 4.71693 17.3027 4.71693 18.2929 5.70712C19.2831 6.69731 19.2831 8.30272 18.2929 9.29291L18 9.5858Z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,12 @@ export default function RegenerateIcon({ className = '' }: { className?: string
|
||||||
width="1em"
|
width="1em"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 2.5C5.05228 2.5 5.5 2.94772 5.5 3.5V5.07196C7.19872 3.47759 9.48483 2.5 12 2.5C17.2467 2.5 21.5 6.75329 21.5 12C21.5 17.2467 17.2467 21.5 12 21.5C7.1307 21.5 3.11828 17.8375 2.565 13.1164C2.50071 12.5679 2.89327 12.0711 3.4418 12.0068C3.99033 11.9425 4.48712 12.3351 4.5514 12.8836C4.98798 16.6089 8.15708 19.5 12 19.5C16.1421 19.5 19.5 16.1421 19.5 12C19.5 7.85786 16.1421 4.5 12 4.5C9.7796 4.5 7.7836 5.46469 6.40954 7H9C9.55228 7 10 7.44772 10 8C10 8.55228 9.55228 9 9 9H4.5C3.96064 9 3.52101 8.57299 3.50073 8.03859C3.49983 8.01771 3.49958 7.99677 3.5 7.9758V3.5C3.5 2.94772 3.94771 2.5 4.5 2.5Z" fill="currentColor">
|
<path
|
||||||
</path></svg>
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M4.5 2.5C5.05228 2.5 5.5 2.94772 5.5 3.5V5.07196C7.19872 3.47759 9.48483 2.5 12 2.5C17.2467 2.5 21.5 6.75329 21.5 12C21.5 17.2467 17.2467 21.5 12 21.5C7.1307 21.5 3.11828 17.8375 2.565 13.1164C2.50071 12.5679 2.89327 12.0711 3.4418 12.0068C3.99033 11.9425 4.48712 12.3351 4.5514 12.8836C4.98798 16.6089 8.15708 19.5 12 19.5C16.1421 19.5 19.5 16.1421 19.5 12C19.5 7.85786 16.1421 4.5 12 4.5C9.7796 4.5 7.7836 5.46469 6.40954 7H9C9.55228 7 10 7.44772 10 8C10 8.55228 9.55228 9 9 9H4.5C3.96064 9 3.52101 8.57299 3.50073 8.03859C3.49983 8.01771 3.49958 7.99677 3.5 7.9758V3.5C3.5 2.94772 3.94771 2.5 4.5 2.5Z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,12 @@ export default function RenameIcon() {
|
||||||
width="1em"
|
width="1em"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.2929 4.29291C15.0641 2.52167 17.9359 2.52167 19.7071 4.2929C21.4783 6.06414 21.4783 8.93588 19.7071 10.7071L18.7073 11.7069L11.1603 19.2539C10.7182 19.696 10.1489 19.989 9.53219 20.0918L4.1644 20.9864C3.84584 21.0395 3.52125 20.9355 3.29289 20.7071C3.06453 20.4788 2.96051 20.1542 3.0136 19.8356L3.90824 14.4678C4.01103 13.8511 4.30396 13.2818 4.7461 12.8397L13.2929 4.29291ZM13 7.41422L6.16031 14.2539C6.01293 14.4013 5.91529 14.591 5.88102 14.7966L5.21655 18.7835L9.20339 18.119C9.40898 18.0847 9.59872 17.9871 9.7461 17.8397L16.5858 11L13 7.41422ZM18 9.5858L14.4142 6.00001L14.7071 5.70712C15.6973 4.71693 17.3027 4.71693 18.2929 5.70712C19.2831 6.69731 19.2831 8.30272 18.2929 9.29291L18 9.5858Z" fill="currentColor">
|
<path
|
||||||
</path></svg>
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M13.2929 4.29291C15.0641 2.52167 17.9359 2.52167 19.7071 4.2929C21.4783 6.06414 21.4783 8.93588 19.7071 10.7071L18.7073 11.7069L11.1603 19.2539C10.7182 19.696 10.1489 19.989 9.53219 20.0918L4.1644 20.9864C3.84584 21.0395 3.52125 20.9355 3.29289 20.7071C3.06453 20.4788 2.96051 20.1542 3.0136 19.8356L3.90824 14.4678C4.01103 13.8511 4.30396 13.2818 4.7461 12.8397L13.2929 4.29291ZM13 7.41422L6.16031 14.2539C6.01293 14.4013 5.91529 14.591 5.88102 14.7966L5.21655 18.7835L9.20339 18.119C9.40898 18.0847 9.59872 17.9871 9.7461 17.8397L16.5858 11L13 7.41422ZM18 9.5858L14.4142 6.00001L14.7071 5.70712C15.6973 4.71693 17.3027 4.71693 18.2929 5.70712C19.2831 6.69731 19.2831 8.30272 18.2929 9.29291L18 9.5858Z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,12 @@ export default function TrashIcon() {
|
||||||
width="1em"
|
width="1em"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5555 4C10.099 4 9.70052 4.30906 9.58693 4.75114L9.29382 5.8919H14.715L14.4219 4.75114C14.3083 4.30906 13.9098 4 13.4533 4H10.5555ZM16.7799 5.8919L16.3589 4.25342C16.0182 2.92719 14.8226 2 13.4533 2H10.5555C9.18616 2 7.99062 2.92719 7.64985 4.25342L7.22886 5.8919H4C3.44772 5.8919 3 6.33961 3 6.8919C3 7.44418 3.44772 7.8919 4 7.8919H4.10069L5.31544 19.3172C5.47763 20.8427 6.76455 22 8.29863 22H15.7014C17.2354 22 18.5224 20.8427 18.6846 19.3172L19.8993 7.8919H20C20.5523 7.8919 21 7.44418 21 6.8919C21 6.33961 20.5523 5.8919 20 5.8919H16.7799ZM17.888 7.8919H6.11196L7.30423 19.1057C7.3583 19.6142 7.78727 20 8.29863 20H15.7014C16.2127 20 16.6417 19.6142 16.6958 19.1057L17.888 7.8919ZM10 10C10.5523 10 11 10.4477 11 11V16C11 16.5523 10.5523 17 10 17C9.44772 17 9 16.5523 9 16V11C9 10.4477 9.44772 10 10 10ZM14 10C14.5523 10 15 10.4477 15 11V16C15 16.5523 14.5523 17 14 17C13.4477 17 13 16.5523 13 16V11C13 10.4477 13.4477 10 14 10Z" fill="currentColor">
|
<path
|
||||||
</path></svg>
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10.5555 4C10.099 4 9.70052 4.30906 9.58693 4.75114L9.29382 5.8919H14.715L14.4219 4.75114C14.3083 4.30906 13.9098 4 13.4533 4H10.5555ZM16.7799 5.8919L16.3589 4.25342C16.0182 2.92719 14.8226 2 13.4533 2H10.5555C9.18616 2 7.99062 2.92719 7.64985 4.25342L7.22886 5.8919H4C3.44772 5.8919 3 6.33961 3 6.8919C3 7.44418 3.44772 7.8919 4 7.8919H4.10069L5.31544 19.3172C5.47763 20.8427 6.76455 22 8.29863 22H15.7014C17.2354 22 18.5224 20.8427 18.6846 19.3172L19.8993 7.8919H20C20.5523 7.8919 21 7.44418 21 6.8919C21 6.33961 20.5523 5.8919 20 5.8919H16.7799ZM17.888 7.8919H6.11196L7.30423 19.1057C7.3583 19.6142 7.78727 20 8.29863 20H15.7014C16.2127 20 16.6417 19.6142 16.6958 19.1057L17.888 7.8919ZM10 10C10.5523 10 11 10.4477 11 11V16C11 16.5523 10.5523 17 10 17C9.44772 17 9 16.5523 9 16V11C9 10.4477 9.44772 10 10 10ZM14 10C14.5523 10 15 10.4477 15 11V16C15 16.5523 14.5523 17 14 17C13.4477 17 13 16.5523 13 16V11C13 10.4477 13.4477 10 14 10Z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
102
client/src/components/ui/MultiSearch.tsx
Normal file
102
client/src/components/ui/MultiSearch.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { Search, X } from 'lucide-react';
|
||||||
|
import React, { useState, useMemo, useCallback } from 'react';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
|
// This is a generic that can be added to Menu and Select components
|
||||||
|
|
||||||
|
export default function MultiSearch({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
}: {
|
||||||
|
value: string | null;
|
||||||
|
onChange: (filter: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const onChangeHandler: React.ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||||
|
(e) => onChange(e.target.value),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group sticky left-0 top-0 z-10 flex h-12 items-center gap-2 bg-gradient-to-b from-white from-65% to-transparent px-2 px-3 py-2 text-black transition-colors duration-300 focus:bg-gradient-to-b focus:from-white focus:to-white/50 dark:from-gray-800 dark:to-transparent dark:text-white dark:focus:from-white/10 dark:focus:to-white/20">
|
||||||
|
<Search className="h-4 w-4 text-gray-500 transition-colors duration-300 dark:group-focus-within:text-gray-300 dark:group-hover:text-gray-300" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value || ''}
|
||||||
|
onChange={onChangeHandler}
|
||||||
|
placeholder={placeholder || localize('com_ui_select_search_model')}
|
||||||
|
className="flex-1 rounded-md border-none bg-transparent px-2.5 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-gray-700/10 dark:focus:ring-gray-200/10"
|
||||||
|
/>
|
||||||
|
<div className="relative flex h-5 w-5 items-center justify-end text-gray-500">
|
||||||
|
<X
|
||||||
|
className={cn(
|
||||||
|
'text-gray-500 dark:text-gray-300',
|
||||||
|
value?.length ? 'cursor-pointer opacity-100' : 'opacity-0',
|
||||||
|
)}
|
||||||
|
onClick={() => onChange('')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that will take a multiSearch input
|
||||||
|
* @param node
|
||||||
|
*/
|
||||||
|
function defaultGetStringKey(node: unknown): string {
|
||||||
|
if (typeof node === 'string') {
|
||||||
|
return node.toUpperCase();
|
||||||
|
}
|
||||||
|
// This should be a noop, but it's here for redundancy
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for conditionally making a multi-element list component into a sortable component
|
||||||
|
* Returns a RenderNode for search input when search functionality is available
|
||||||
|
* @param availableOptions
|
||||||
|
* @param placeholder
|
||||||
|
* @param getTextKeyOverride
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useMultiSearch<OptionsType extends unknown[]>(
|
||||||
|
availableOptions: OptionsType,
|
||||||
|
placeholder?: string,
|
||||||
|
getTextKeyOverride?: (node: OptionsType[0]) => string,
|
||||||
|
): [OptionsType, React.ReactNode] {
|
||||||
|
const [filterValue, setFilterValue] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// We conditionally show the search when there's more than 10 elements in the menu
|
||||||
|
const shouldShowSearch = availableOptions.length > 10;
|
||||||
|
|
||||||
|
// Define the helper function used to enable search
|
||||||
|
// If this is invalidly described, we will assume developer error - tf. avoid rendering
|
||||||
|
const getTextKeyHelper = getTextKeyOverride || defaultGetStringKey;
|
||||||
|
|
||||||
|
// Iterate said options
|
||||||
|
const filteredOptions = useMemo(() => {
|
||||||
|
if (!shouldShowSearch || !filterValue || !availableOptions.length) {
|
||||||
|
// Don't render if available options aren't present, there's no filter active
|
||||||
|
return availableOptions;
|
||||||
|
}
|
||||||
|
// Filter through the values, using a simple text-based search
|
||||||
|
// nothing too fancy, but we can add a better search algo later if we need
|
||||||
|
const upperFilterValue = filterValue.toUpperCase();
|
||||||
|
|
||||||
|
return availableOptions.filter((value) =>
|
||||||
|
getTextKeyHelper(value).includes(upperFilterValue),
|
||||||
|
) as OptionsType;
|
||||||
|
}, [availableOptions, getTextKeyHelper, filterValue, shouldShowSearch]);
|
||||||
|
|
||||||
|
const onSearchChange = useCallback((nextFilterValue) => setFilterValue(nextFilterValue), []);
|
||||||
|
|
||||||
|
const searchRender = shouldShowSearch ? (
|
||||||
|
<MultiSearch value={filterValue} onChange={onSearchChange} placeholder={placeholder} />
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return [filteredOptions, searchRender];
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import { Listbox, Transition } from '@headlessui/react';
|
||||||
import { Wrench, ArrowRight } from 'lucide-react';
|
import { Wrench, ArrowRight } from 'lucide-react';
|
||||||
import { CheckMark } from '~/components/svg';
|
import { CheckMark } from '~/components/svg';
|
||||||
import useOnClickOutside from '~/hooks/useOnClickOutside';
|
import useOnClickOutside from '~/hooks/useOnClickOutside';
|
||||||
|
import { useMultiSearch } from './MultiSearch';
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
import type { TPlugin } from 'librechat-data-provider';
|
import type { TPlugin } from 'librechat-data-provider';
|
||||||
|
|
||||||
|
|
@ -43,6 +44,13 @@ function MultiSelectDropDown({
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Detemine if we should to convert this component into a searchable select. If we have enough elements, a search
|
||||||
|
// input will appear near the top of the menu, allowing correct filtering of different model menu items. This will
|
||||||
|
// reset once the component is unmounted (as per a normal search)
|
||||||
|
const [filteredValues, searchRender] = useMultiSearch<TPlugin[]>(availableValues);
|
||||||
|
const hasSearchRender = Boolean(searchRender);
|
||||||
|
const options = hasSearchRender ? filteredValues : availableValues;
|
||||||
|
|
||||||
const transitionProps = { className: 'top-full mt-3' };
|
const transitionProps = { className: 'top-full mt-3' };
|
||||||
if (showAbove) {
|
if (showAbove) {
|
||||||
transitionProps.className = 'bottom-full mb-3';
|
transitionProps.className = 'bottom-full mb-3';
|
||||||
|
|
@ -136,9 +144,12 @@ function MultiSelectDropDown({
|
||||||
>
|
>
|
||||||
<Listbox.Options
|
<Listbox.Options
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className="absolute z-50 mt-2 max-h-60 w-full overflow-auto rounded bg-white text-base text-xs ring-1 ring-black/10 focus:outline-none dark:bg-gray-800 dark:ring-white/20 dark:last:border-0 md:w-[100%]"
|
className={cn(
|
||||||
|
'absolute z-50 mt-2 max-h-60 w-full overflow-auto rounded bg-white text-base text-xs ring-1 ring-black/10 focus:outline-none dark:bg-gray-800 dark:ring-white/20 dark:last:border-0 md:w-[100%]',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{availableValues.map((option, i: number) => {
|
{searchRender}
|
||||||
|
{options.map((option, i: number) => {
|
||||||
if (!option) {
|
if (!option) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import React from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { Wrench } from 'lucide-react';
|
import { Wrench } from 'lucide-react';
|
||||||
import { Root, Trigger, Content, Portal } from '@radix-ui/react-popover';
|
import { Root, Trigger, Content, Portal } from '@radix-ui/react-popover';
|
||||||
import type { TPlugin } from 'librechat-data-provider';
|
import type { TPlugin } from 'librechat-data-provider';
|
||||||
import MenuItem from '~/components/Chat/Menus/UI/MenuItem';
|
import MenuItem from '~/components/Chat/Menus/UI/MenuItem';
|
||||||
|
import { useMultiSearch } from './MultiSearch';
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
|
|
||||||
type SelectDropDownProps = {
|
type SelectDropDownProps = {
|
||||||
|
|
@ -35,6 +36,11 @@ function MultiSelectPop({
|
||||||
const title = _title;
|
const title = _title;
|
||||||
const excludeIds = ['select-plugin', 'plugins-label', 'selected-plugins'];
|
const excludeIds = ['select-plugin', 'plugins-label', 'selected-plugins'];
|
||||||
|
|
||||||
|
// Detemine if we should to convert this component into a searchable select
|
||||||
|
const [filteredValues, searchRender] = useMultiSearch<TPlugin[]>(availableValues);
|
||||||
|
const hasSearchRender = Boolean(searchRender);
|
||||||
|
const options = hasSearchRender ? filteredValues : availableValues;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Root>
|
<Root>
|
||||||
<div className={cn('flex items-center justify-center gap-2', containerClassName ?? '')}>
|
<div className={cn('flex items-center justify-center gap-2', containerClassName ?? '')}>
|
||||||
|
|
@ -106,9 +112,13 @@ function MultiSelectPop({
|
||||||
<Content
|
<Content
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="center"
|
align="center"
|
||||||
className="mt-2 max-h-60 min-w-full overflow-hidden overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
className={cn(
|
||||||
|
'mt-2 max-h-60 min-w-full overflow-hidden overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800 dark:text-white',
|
||||||
|
hasSearchRender && 'relative',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{availableValues.map((option) => {
|
{searchRender}
|
||||||
|
{options.map((option) => {
|
||||||
if (!option) {
|
if (!option) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type { Option } from '~/common';
|
||||||
import CheckMark from '../svg/CheckMark';
|
import CheckMark from '../svg/CheckMark';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
|
import { useMultiSearch } from './MultiSearch';
|
||||||
|
|
||||||
type SelectDropDownProps = {
|
type SelectDropDownProps = {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
@ -57,6 +58,13 @@ function SelectDropDown({
|
||||||
title = localize('com_ui_model');
|
title = localize('com_ui_model');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detemine if we should to convert this component into a searchable select. If we have enough elements, a search
|
||||||
|
// input will appear near the top of the menu, allowing correct filtering of different model menu items. This will
|
||||||
|
// reset once the component is unmounted (as per a normal search)
|
||||||
|
const [filteredValues, searchRender] = useMultiSearch<string[] | Option[]>(availableValues);
|
||||||
|
const hasSearchRender = Boolean(searchRender);
|
||||||
|
const options = hasSearchRender ? filteredValues : availableValues;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center justify-center gap-2 ', containerClassName ?? '')}>
|
<div className={cn('flex items-center justify-center gap-2 ', containerClassName ?? '')}>
|
||||||
<div className={cn('relative w-full', subContainerClassName ?? '')}>
|
<div className={cn('relative w-full', subContainerClassName ?? '')}>
|
||||||
|
|
@ -122,7 +130,7 @@ function SelectDropDown({
|
||||||
>
|
>
|
||||||
<Listbox.Options
|
<Listbox.Options
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute z-10 mt-2 max-h-60 w-full overflow-auto rounded border bg-white text-base text-xs ring-black/10 focus:outline-none dark:bg-gray-800 dark:ring-white/20 dark:border-gray-600 md:w-[100%]',
|
'absolute z-10 mt-2 max-h-60 w-full overflow-auto rounded border bg-white text-base text-xs ring-black/10 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:ring-white/20 md:w-[100%]',
|
||||||
optionsListClass ?? '',
|
optionsListClass ?? '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -138,7 +146,8 @@ function SelectDropDown({
|
||||||
{renderOption()}
|
{renderOption()}
|
||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
)}
|
)}
|
||||||
{availableValues.map((option: string | Option, i: number) => {
|
{searchRender}
|
||||||
|
{options.map((option: string | Option, i: number) => {
|
||||||
if (!option) {
|
if (!option) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import MenuItem from '~/components/Chat/Menus/UI/MenuItem';
|
||||||
import type { Option } from '~/common';
|
import type { Option } from '~/common';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
|
import { useMultiSearch } from './MultiSearch';
|
||||||
|
|
||||||
type SelectDropDownProps = {
|
type SelectDropDownProps = {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
@ -42,6 +43,13 @@ function SelectDropDownPop({
|
||||||
title = localize('com_ui_model');
|
title = localize('com_ui_model');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detemine if we should to convert this component into a searchable select. If we have enough elements, a search
|
||||||
|
// input will appear near the top of the menu, allowing correct filtering of different model menu items. This will
|
||||||
|
// reset once the component is unmounted (as per a normal search)
|
||||||
|
const [filteredValues, searchRender] = useMultiSearch<string[] | Option[]>(availableValues);
|
||||||
|
const hasSearchRender = Boolean(searchRender);
|
||||||
|
const options = hasSearchRender ? filteredValues : availableValues;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Root>
|
<Root>
|
||||||
<div className={'flex items-center justify-center gap-2 '}>
|
<div className={'flex items-center justify-center gap-2 '}>
|
||||||
|
|
@ -95,9 +103,13 @@ function SelectDropDownPop({
|
||||||
<Content
|
<Content
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
className="mt-2 max-h-[52vh] min-w-full overflow-hidden overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800 dark:text-white lg:max-h-[52vh]"
|
className={cn(
|
||||||
|
'mt-2 max-h-[52vh] min-w-full overflow-hidden overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800 dark:text-white lg:max-h-[52vh]',
|
||||||
|
hasSearchRender && 'relative',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{availableValues.map((option) => {
|
{searchRender}
|
||||||
|
{options.map((option) => {
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={option}
|
key={option}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ function useDebouncedInput({
|
||||||
initialValue: unknown;
|
initialValue: unknown;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
}): [
|
}): [
|
||||||
React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>,
|
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | unknown) => void,
|
||||||
unknown,
|
unknown,
|
||||||
SetterOrUpdater<string>,
|
SetterOrUpdater<string>,
|
||||||
// (newValue: string) => void,
|
// (newValue: string) => void,
|
||||||
|
|
@ -35,9 +35,12 @@ function useDebouncedInput({
|
||||||
);
|
);
|
||||||
|
|
||||||
/** An onChange handler that updates the local state and the debounced option */
|
/** An onChange handler that updates the local state and the debounced option */
|
||||||
const onChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> = useCallback(
|
const onChange = useCallback(
|
||||||
(e) => {
|
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | unknown) => {
|
||||||
const newValue: unknown = e.target.value;
|
const newValue: unknown =
|
||||||
|
typeof e !== 'object'
|
||||||
|
? e
|
||||||
|
: (e as React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>)?.target.value;
|
||||||
setValue(newValue);
|
setValue(newValue);
|
||||||
setDebouncedOption(newValue);
|
setDebouncedOption(newValue);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ export default {
|
||||||
com_ui_close: 'Close',
|
com_ui_close: 'Close',
|
||||||
com_ui_model: 'Model',
|
com_ui_model: 'Model',
|
||||||
com_ui_select_model: 'Select a model',
|
com_ui_select_model: 'Select a model',
|
||||||
|
com_ui_select_search_model: 'Search model by name',
|
||||||
com_ui_use_prompt: 'Use prompt',
|
com_ui_use_prompt: 'Use prompt',
|
||||||
com_ui_prev: 'Prev',
|
com_ui_prev: 'Prev',
|
||||||
com_ui_next: 'Next',
|
com_ui_next: 'Next',
|
||||||
|
|
|
||||||
|
|
@ -1083,24 +1083,6 @@ button {
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
height: 0.1em;
|
|
||||||
width: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar-trigger:hover ::-webkit-scrollbar-thumb {
|
|
||||||
visibility: hide;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background-color: hsla(0,0%,100%,.1);
|
|
||||||
border-radius: 9999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar-transparent::-webkit-scrollbar-thumb {
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-token-surface-secondary {
|
.bg-token-surface-secondary {
|
||||||
background-color: #f7f7f8;
|
background-color: #f7f7f8;
|
||||||
background-color: var(--surface-secondary);
|
background-color: var(--surface-secondary);
|
||||||
|
|
@ -1112,13 +1094,32 @@ button {
|
||||||
--tw-gradient-stops: var(--tw-gradient-from),var(--tw-gradient-to);
|
--tw-gradient-stops: var(--tw-gradient-from),var(--tw-gradient-to);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Webkit scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
height: 0.1em;
|
||||||
|
width: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-thumb {
|
||||||
|
background-color: hsla(0, 0%, 100%, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: hsla(0,0%,100%,.3);
|
|
||||||
|
|
||||||
|
.scrollbar-transparent::-webkit-scrollbar-thumb {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .scrollbar-transparent::-webkit-scrollbar-thumb {
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
body,
|
body,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
export default function cn(...inputs: string[]) {
|
/**
|
||||||
|
* Merges the tailwind clases (using twMerge). Conditionally removes false values
|
||||||
|
* @param inputs The tailwind classes to merge
|
||||||
|
* @returns className string to apply to an element or HOC
|
||||||
|
*/
|
||||||
|
export default function cn(...inputs: Array<string | boolean>) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue