forked from danny-avila/LibreChat
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
📦 feat: Model & Assistants Combobox for Side Panel (danny-avila#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
- Loading branch information
1 parent
52afc9f
commit 1ebca83
Showing
33 changed files
with
2,847 additions
and
459 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { useEffect, useMemo } from 'react'; | ||
import { Combobox } from '~/components/ui'; | ||
import { EModelEndpoint, defaultOrderQuery } from 'librechat-data-provider'; | ||
import type { SwitcherProps } from '~/common'; | ||
import { useSetIndexOptions, useSelectAssistant, useLocalize } from '~/hooks'; | ||
import { useChatContext, useAssistantsMapContext } from '~/Providers'; | ||
import { useListAssistantsQuery } from '~/data-provider'; | ||
import Icon from '~/components/Endpoints/Icon'; | ||
|
||
export default function AssistantSwitcher({ 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 ?? '']; | ||
|
||
const assistantOptions = useMemo(() => { | ||
return assistants.map((assistant) => { | ||
return { | ||
label: assistant.name ?? '', | ||
value: assistant.id, | ||
icon: ( | ||
<Icon | ||
isCreatedByUser={false} | ||
endpoint={EModelEndpoint.assistants} | ||
assistantName={assistant.name ?? ''} | ||
iconURL={(assistant.metadata?.avatar as string) ?? ''} | ||
/> | ||
), | ||
}; | ||
}); | ||
}, [assistants]); | ||
|
||
return ( | ||
<Combobox | ||
selectedValue={currentAssistant?.id ?? ''} | ||
displayValue={ | ||
assistants.find((assistant) => assistant.id === selectedAssistant)?.name ?? | ||
localize('com_sidepanel_select_assistant') | ||
} | ||
selectPlaceholder={localize('com_sidepanel_select_assistant')} | ||
searchPlaceholder={localize('com_assistants_search_name')} | ||
isCollapsed={isCollapsed} | ||
ariaLabel={'assistant'} | ||
setValue={onSelect} | ||
items={assistantOptions} | ||
SelectIcon={ | ||
<Icon | ||
isCreatedByUser={false} | ||
endpoint={EModelEndpoint.assistants} | ||
assistantName={currentAssistant?.name ?? ''} | ||
iconURL={(currentAssistant?.metadata?.avatar as string) ?? ''} | ||
/> | ||
} | ||
/> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { useMemo, useRef, useCallback } from 'react'; | ||
import { useGetModelsQuery } from 'librechat-data-provider/react-query'; | ||
import MinimalIcon from '~/components/Endpoints/MinimalIcon'; | ||
import { useSetIndexOptions, useLocalize } from '~/hooks'; | ||
import type { SwitcherProps } from '~/common'; | ||
import { useChatContext } from '~/Providers'; | ||
import { Combobox } from '~/components/ui'; | ||
import { mainTextareaId } from '~/common'; | ||
|
||
export default function ModelSwitcher({ isCollapsed }: SwitcherProps) { | ||
const localize = useLocalize(); | ||
const modelsQuery = useGetModelsQuery(); | ||
const { conversation } = useChatContext(); | ||
const { setOption } = useSetIndexOptions(); | ||
const timeoutIdRef = useRef<NodeJS.Timeout>(); | ||
|
||
const { endpoint, model = null } = conversation ?? {}; | ||
const models = useMemo(() => { | ||
return modelsQuery?.data?.[endpoint ?? ''] ?? []; | ||
}, [modelsQuery, endpoint]); | ||
|
||
const setModel = useCallback( | ||
(model: string) => { | ||
setOption('model')(model); | ||
clearTimeout(timeoutIdRef.current); | ||
timeoutIdRef.current = setTimeout(() => { | ||
const textarea = document.getElementById(mainTextareaId); | ||
if (textarea) { | ||
textarea.focus(); | ||
} | ||
}, 150); | ||
}, | ||
[setOption], | ||
); | ||
|
||
return ( | ||
<Combobox | ||
selectPlaceholder={localize('com_ui_select_model')} | ||
searchPlaceholder={localize('com_ui_select_search_model')} | ||
isCollapsed={isCollapsed} | ||
ariaLabel={'model'} | ||
selectedValue={model ?? ''} | ||
setValue={setModel} | ||
items={models} | ||
SelectIcon={ | ||
<MinimalIcon | ||
isCreatedByUser={false} | ||
endpoint={endpoint} | ||
// iconURL={} // for future preset icons | ||
/> | ||
} | ||
/> | ||
); | ||
} |
97 changes: 97 additions & 0 deletions
97
client/src/components/SidePanel/Parameters/DynamicCheckbox.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
// client/src/components/SidePanel/Parameters/DynamicCheckbox.tsx | ||
import { useMemo, useState } from 'react'; | ||
import { OptionTypes } from 'librechat-data-provider'; | ||
import type { DynamicSettingProps } from 'librechat-data-provider'; | ||
import { Label, Checkbox, HoverCard, HoverCardTrigger } from '~/components/ui'; | ||
import { useLocalize, useParameterEffects } from '~/hooks'; | ||
import { useChatContext } from '~/Providers'; | ||
import OptionHover from './OptionHover'; | ||
import { ESide } from '~/common'; | ||
|
||
function DynamicCheckbox({ | ||
label, | ||
settingKey, | ||
defaultValue, | ||
description, | ||
columnSpan, | ||
setOption, | ||
optionType, | ||
readonly = false, | ||
showDefault = true, | ||
labelCode, | ||
descriptionCode, | ||
}: DynamicSettingProps) { | ||
const localize = useLocalize(); | ||
const { conversation = { conversationId: null }, preset } = useChatContext(); | ||
const [inputValue, setInputValue] = useState<boolean>(!!(defaultValue as boolean | undefined)); | ||
|
||
const selectedValue = useMemo(() => { | ||
if (optionType === OptionTypes.Custom) { | ||
// TODO: custom logic, add to payload but not to conversation | ||
return inputValue; | ||
} | ||
|
||
return conversation?.[settingKey] ?? defaultValue; | ||
}, [conversation, defaultValue, optionType, settingKey, inputValue]); | ||
|
||
const handleCheckedChange = (checked: boolean) => { | ||
if (optionType === OptionTypes.Custom) { | ||
// TODO: custom logic, add to payload but not to conversation | ||
setInputValue(checked); | ||
return; | ||
} | ||
setOption(settingKey)(checked); | ||
}; | ||
|
||
useParameterEffects({ | ||
preset, | ||
settingKey, | ||
defaultValue, | ||
conversation, | ||
inputValue, | ||
setInputValue, | ||
preventDelayedUpdate: true, | ||
}); | ||
|
||
return ( | ||
<div | ||
className={`flex flex-col items-center justify-start gap-6 ${ | ||
columnSpan ? `col-span-${columnSpan}` : 'col-span-full' | ||
}`} | ||
> | ||
<HoverCard openDelay={300}> | ||
<HoverCardTrigger className="grid w-full items-center"> | ||
<div className="flex justify-start gap-4"> | ||
<Label | ||
htmlFor={`${settingKey}-dynamic-checkbox`} | ||
className="text-left text-sm font-medium" | ||
> | ||
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '} | ||
{showDefault && ( | ||
<small className="opacity-40"> | ||
({localize('com_endpoint_default')}:{' '} | ||
{defaultValue ? localize('com_ui_yes') : localize('com_ui_no')}) | ||
</small> | ||
)} | ||
</Label> | ||
<Checkbox | ||
id={`${settingKey}-dynamic-checkbox`} | ||
disabled={readonly} | ||
checked={selectedValue} | ||
onCheckedChange={handleCheckedChange} | ||
className="mt-[2px] focus:ring-opacity-20 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:focus:ring-gray-600 dark:focus:ring-opacity-50 dark:focus:ring-offset-0" | ||
/> | ||
</div> | ||
</HoverCardTrigger> | ||
{description && ( | ||
<OptionHover | ||
description={descriptionCode ? localize(description) || description : description} | ||
side={ESide.Left} | ||
/> | ||
)} | ||
</HoverCard> | ||
</div> | ||
); | ||
} | ||
|
||
export default DynamicCheckbox; |
Oops, something went wrong.