📦 feat: Model & Assistants Combobox for Side Panel (#2380)

* WIP: dynamic settings

* WIP: update tests and validations

* refactor(SidePanel): use hook for Links

* WIP: dynamic settings, slider implemented

* feat(useDebouncedInput): dynamic typing with generic

* refactor(generate): add `custom` optionType to be non-conforming to conversation schema

* feat: DynamicDropdown

* refactor(DynamicSlider): custom optionType handling and useEffect for conversation updates elsewhere

* refactor(Panel): add more test cases

* chore(DynamicSlider): note

* refactor(useDebouncedInput): import defaultDebouncedDelay from ~/common`

* WIP: implement remaining ComponentTypes

* chore: add com_sidepanel_parameters

* refactor: add langCode handling for dynamic settings

* chore(useOriginNavigate): change path to '/c/'

* refactor: explicit textarea focus on new convo, share textarea idea via ~/common

* refactor: useParameterEffects: reset if convo or preset Ids change, share and maintain statefulness in side panel

* wip: combobox

* chore: minor styling for Select components

* wip: combobox select styling for side panel

* feat: complete combobox

* refactor: model select for side panel switcher

* refactor(Combobox): add portal

* chore: comment out dynamic parameters panel for future PR and delete prompt files

* refactor(Combobox): add icon field for options, change hover bg-color, add displayValue

* fix(useNewConvo): proper textarea focus with setTimeout

* refactor(AssistantSwitcher): use Combobox

* refactor(ModelSwitcher): add textarea focus on model switch
This commit is contained in:
Danny Avila 2024-04-10 14:27:22 -04:00 committed by GitHub
parent f64a2cb0b0
commit 8e5f1ad575
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 2850 additions and 462 deletions

View file

@ -1,104 +1,20 @@
import { useEffect } from 'react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '~/components/ui/Select';
import { EModelEndpoint, defaultOrderQuery } from 'librechat-data-provider';
import { useSetIndexOptions, useSelectAssistant, useLocalize } from '~/hooks';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { useListAssistantsQuery } from '~/data-provider';
import Icon from '~/components/Endpoints/Icon';
import { cn } from '~/utils';
import { EModelEndpoint } from 'librechat-data-provider';
import type { SwitcherProps } from '~/common';
import AssistantSwitcher from './AssistantSwitcher';
import { useChatContext } from '~/Providers';
import ModelSwitcher from './ModelSwitcher';
interface SwitcherProps {
isCollapsed: boolean;
}
export default function Switcher({ isCollapsed }: SwitcherProps) {
const localize = useLocalize();
const { setOption } = useSetIndexOptions();
const { index, conversation } = useChatContext();
/* `selectedAssistant` must be defined with `null` to cause re-render on update */
const { assistant_id: selectedAssistant = null, endpoint } = conversation ?? {};
const { data: assistants = [] } = useListAssistantsQuery(defaultOrderQuery, {
select: (res) => res.data.map(({ id, name, metadata }) => ({ id, name, metadata })),
});
const assistantMap = useAssistantsMapContext();
const { onSelect } = useSelectAssistant();
useEffect(() => {
if (!selectedAssistant && assistants && assistants.length && assistantMap) {
const assistant_id =
localStorage.getItem(`assistant_id__${index}`) ?? assistants[0]?.id ?? '';
const assistant = assistantMap?.[assistant_id];
if (!assistant) {
return;
}
if (endpoint !== EModelEndpoint.assistants) {
return;
}
setOption('model')(assistant.model);
setOption('assistant_id')(assistant_id);
}
}, [index, assistants, selectedAssistant, assistantMap, endpoint, setOption]);
const currentAssistant = assistantMap?.[selectedAssistant ?? ''];
return (
<Select value={selectedAssistant as string | undefined} onValueChange={onSelect}>
<SelectTrigger
className={cn(
'flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0',
isCollapsed
? 'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'
: '',
'bg-white text-black hover:bg-gray-50 dark:bg-gray-850 dark:text-white',
)}
aria-label={localize('com_sidepanel_select_assistant')}
>
<SelectValue placeholder={localize('com_sidepanel_select_assistant')}>
<div className="assistant-item flex items-center justify-center overflow-hidden rounded-full">
<Icon
isCreatedByUser={false}
endpoint={EModelEndpoint.assistants}
assistantName={currentAssistant?.name ?? ''}
iconURL={(currentAssistant?.metadata?.avatar as string) ?? ''}
/>
</div>
<span className={cn('ml-2', isCollapsed ? 'hidden' : '')} style={{ userSelect: 'none' }}>
{assistants.find((assistant) => assistant.id === selectedAssistant)?.name ??
localize('com_sidepanel_select_assistant')}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent className="bg-white dark:bg-gray-800">
{assistants.map((assistant) => (
<SelectItem
key={assistant.id}
value={assistant.id}
className="hover:bg-gray-50 dark:text-white"
>
<div className="[&_svg]:text-foreground flex items-center justify-center gap-3 dark:text-white [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0">
<div className="assistant-item overflow-hidden rounded-full ">
<Icon
isCreatedByUser={false}
endpoint={EModelEndpoint.assistants}
assistantName={assistant.name ?? ''}
iconURL={(assistant.metadata?.avatar as string) ?? ''}
/>
</div>
{assistant.name}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
);
export default function Switcher(props: SwitcherProps) {
const { conversation } = useChatContext();
const { endpoint } = conversation ?? {};
if (!props.endpointKeyProvided) {
return null;
}
if (endpoint === EModelEndpoint.assistants) {
return <AssistantSwitcher {...props} />;
}
return <ModelSwitcher {...props} />;
}