mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
👷 fix: Minor Fixes and Refactors (#2388)
* refactor(useTextarea): set Textarea disabled message due to key higher in priority * fix(SidePanel): intended behavior for non-user provided keys * fix: generate specs * style: update combobox styling as before, with better dynamic height * chore: remove unused import
This commit is contained in:
parent
0fe47cf1f8
commit
c19dfddd0f
6 changed files with 136 additions and 75 deletions
|
|
@ -335,6 +335,7 @@ export interface ExtendedFile {
|
|||
export type ContextType = { navVisible: boolean; setNavVisible: (visible: boolean) => void };
|
||||
|
||||
export interface SwitcherProps {
|
||||
endpoint?: EModelEndpoint | null;
|
||||
endpointKeyProvided: boolean;
|
||||
isCollapsed: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ const SidePanel = ({
|
|||
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
|
||||
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
|
||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(EModelEndpoint.assistants);
|
||||
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
||||
const { conversation } = useChatContext();
|
||||
const { endpoint } = conversation ?? {};
|
||||
const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(endpoint ?? '');
|
||||
|
||||
const panelRef = useRef<ImperativePanelHandle>(null);
|
||||
|
||||
|
|
@ -187,7 +187,11 @@ const SidePanel = ({
|
|||
isCollapsed ? 'h-[52px]' : 'px-2',
|
||||
)}
|
||||
>
|
||||
<Switcher isCollapsed={isCollapsed} endpointKeyProvided={keyProvided} />
|
||||
<Switcher
|
||||
isCollapsed={isCollapsed}
|
||||
endpointKeyProvided={keyProvided}
|
||||
endpoint={endpoint}
|
||||
/>
|
||||
<Separator className="bg-gray-100/50 dark:bg-gray-600" />
|
||||
</div>
|
||||
<Nav
|
||||
|
|
|
|||
|
|
@ -1,19 +1,13 @@
|
|||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { SwitcherProps } from '~/common';
|
||||
import AssistantSwitcher from './AssistantSwitcher';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import ModelSwitcher from './ModelSwitcher';
|
||||
|
||||
export default function Switcher(props: SwitcherProps) {
|
||||
const { conversation } = useChatContext();
|
||||
const { endpoint } = conversation ?? {};
|
||||
|
||||
if (!props.endpointKeyProvided) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (endpoint === EModelEndpoint.assistants) {
|
||||
if (props.endpoint === EModelEndpoint.assistants && props.endpointKeyProvided) {
|
||||
return <AssistantSwitcher {...props} />;
|
||||
} else if (props.endpoint === EModelEndpoint.assistants) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <ModelSwitcher {...props} />;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { startTransition, useMemo } from 'react';
|
||||
import * as RadixSelect from '@radix-ui/react-select';
|
||||
import { Search as SearchIcon } from 'lucide-react';
|
||||
import * as RadixSelect from '@radix-ui/react-select';
|
||||
import { CheckIcon, ChevronDownIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Combobox,
|
||||
|
|
@ -10,7 +10,7 @@ import {
|
|||
ComboboxCancel,
|
||||
} from '@ariakit/react';
|
||||
import type { OptionWithIcon } from '~/common';
|
||||
import { SelectTrigger, SelectValue } from './Select';
|
||||
import { SelectTrigger, SelectValue, SelectScrollDownButton } from './Select';
|
||||
import useCombobox from '~/hooks/Input/useCombobox';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
|
|
@ -101,65 +101,66 @@ export default function ComboboxComponent({
|
|||
role="dialog"
|
||||
aria-label={ariaLabel + 's'}
|
||||
position="popper"
|
||||
sideOffset={4}
|
||||
alignOffset={-16}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-600',
|
||||
'bg-popover text-popover-foreground relative z-50 max-h-[52vh] min-w-[8rem] overflow-hidden rounded-md border border-gray-200 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-600',
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
'bg-white dark:bg-gray-700',
|
||||
)}
|
||||
>
|
||||
<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-700 dark:to-transparent dark:text-white dark:focus:from-white/10 dark:focus:to-white/20">
|
||||
<SearchIcon 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" />
|
||||
<Combobox
|
||||
autoSelect
|
||||
placeholder={searchPlaceholder}
|
||||
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"
|
||||
// Ariakit's Combobox manually triggers a blur event on virtually
|
||||
// blurred items, making them work as if they had actual DOM
|
||||
// focus. These blur events might happen after the corresponding
|
||||
// focus events in the capture phase, leading Radix Select to
|
||||
// close the popover. This happens because Radix Select relies on
|
||||
// the order of these captured events to discern if the focus was
|
||||
// outside the element. Since we don't have access to the
|
||||
// onInteractOutside prop in the Radix SelectContent component to
|
||||
// stop this behavior, we can turn off Ariakit's behavior here.
|
||||
onBlurCapture={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
<ComboboxCancel
|
||||
hideWhenEmpty={true}
|
||||
className="relative flex h-5 w-5 items-center justify-end text-gray-500 transition-colors duration-300 dark:group-focus-within:text-gray-300 dark:group-hover:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<ComboboxList className="overflow-y-auto p-1 py-2">
|
||||
{matches.map(({ label, value, icon }) => (
|
||||
<RadixSelect.Item key={value} value={`${value ?? ''}`} asChild>
|
||||
<ComboboxItem
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'rounded-lg hover:bg-gray-100/50 hover:bg-gray-50 dark:text-white dark:hover:bg-gray-600',
|
||||
)}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<RadixSelect.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</RadixSelect.ItemIndicator>
|
||||
</span>
|
||||
<RadixSelect.ItemText>
|
||||
<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 && icon}
|
||||
<RadixSelect.Viewport className="mb-5 h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]">
|
||||
<div className="group sticky left-0 top-0 z-10 flex h-12 items-center gap-2 bg-white px-2 px-3 py-2 text-black duration-300 dark:bg-gray-700 dark:text-white">
|
||||
<SearchIcon 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" />
|
||||
<Combobox
|
||||
autoSelect
|
||||
placeholder={searchPlaceholder}
|
||||
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"
|
||||
// Ariakit's Combobox manually triggers a blur event on virtually
|
||||
// blurred items, making them work as if they had actual DOM
|
||||
// focus. These blur events might happen after the corresponding
|
||||
// focus events in the capture phase, leading Radix Select to
|
||||
// close the popover. This happens because Radix Select relies on
|
||||
// the order of these captured events to discern if the focus was
|
||||
// outside the element. Since we don't have access to the
|
||||
// onInteractOutside prop in the Radix SelectContent component to
|
||||
// stop this behavior, we can turn off Ariakit's behavior here.
|
||||
onBlurCapture={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
<ComboboxCancel
|
||||
hideWhenEmpty={true}
|
||||
className="relative flex h-5 w-5 items-center justify-end text-gray-500 transition-colors duration-300 dark:group-focus-within:text-gray-300 dark:group-hover:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<ComboboxList className="overflow-y-auto p-1 py-2">
|
||||
{matches.map(({ label, value, icon }) => (
|
||||
<RadixSelect.Item key={value} value={`${value ?? ''}`} asChild>
|
||||
<ComboboxItem
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'rounded-lg hover:bg-gray-100/50 hover:bg-gray-50 dark:text-white dark:hover:bg-gray-600',
|
||||
)}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<RadixSelect.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</RadixSelect.ItemIndicator>
|
||||
</span>
|
||||
<RadixSelect.ItemText>
|
||||
<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 && icon}
|
||||
</div>
|
||||
{label}
|
||||
</div>
|
||||
{label}
|
||||
</div>
|
||||
</RadixSelect.ItemText>
|
||||
</ComboboxItem>
|
||||
</RadixSelect.Item>
|
||||
))}
|
||||
</ComboboxList>
|
||||
</RadixSelect.ItemText>
|
||||
</ComboboxItem>
|
||||
</RadixSelect.Item>
|
||||
))}
|
||||
</ComboboxList>
|
||||
</RadixSelect.Viewport>
|
||||
<SelectScrollDownButton className="absolute bottom-0 left-0 right-0" />
|
||||
</RadixSelect.Content>
|
||||
</RadixSelect.Portal>
|
||||
</ComboboxProvider>
|
||||
|
|
|
|||
|
|
@ -81,15 +81,15 @@ export default function useTextarea({
|
|||
}
|
||||
|
||||
const getPlaceholderText = () => {
|
||||
if (disabled) {
|
||||
return localize('com_endpoint_config_placeholder');
|
||||
}
|
||||
if (
|
||||
conversation?.endpoint === EModelEndpoint.assistants &&
|
||||
(!conversation?.assistant_id || !assistantMap?.[conversation?.assistant_id ?? ''])
|
||||
) {
|
||||
return localize('com_endpoint_assistant_placeholder');
|
||||
}
|
||||
if (disabled) {
|
||||
return localize('com_endpoint_config_placeholder');
|
||||
}
|
||||
|
||||
if (isNotAppendable) {
|
||||
return localize('com_endpoint_message_not_appendable');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable jest/no-conditional-expect */
|
||||
import { ZodError, z } from 'zod';
|
||||
import { generateDynamicSchema, validateSettingDefinitions } from '../src/generate';
|
||||
import { generateDynamicSchema, validateSettingDefinitions, OptionTypes } from '../src/generate';
|
||||
import type { SettingsConfiguration } from '../src/generate';
|
||||
|
||||
describe('generateDynamicSchema', () => {
|
||||
|
|
@ -117,7 +117,40 @@ describe('generateDynamicSchema', () => {
|
|||
});
|
||||
|
||||
describe('validateSettingDefinitions', () => {
|
||||
// Test for valid setting configurations
|
||||
test('should throw error for Conversation optionType', () => {
|
||||
const validSettings: SettingsConfiguration = [
|
||||
{
|
||||
key: 'themeColor',
|
||||
component: 'input',
|
||||
type: 'string',
|
||||
default: '#ffffff',
|
||||
label: 'Theme Color',
|
||||
columns: 2,
|
||||
columnSpan: 1,
|
||||
optionType: OptionTypes.Conversation,
|
||||
},
|
||||
];
|
||||
|
||||
expect(() => validateSettingDefinitions(validSettings)).toThrow();
|
||||
});
|
||||
|
||||
test('should throw error for Model optionType', () => {
|
||||
const validSettings: SettingsConfiguration = [
|
||||
{
|
||||
key: 'themeColor',
|
||||
component: 'input',
|
||||
type: 'string',
|
||||
default: '#ffffff',
|
||||
label: 'Theme Color',
|
||||
columns: 2,
|
||||
columnSpan: 1,
|
||||
optionType: OptionTypes.Model,
|
||||
},
|
||||
];
|
||||
|
||||
expect(() => validateSettingDefinitions(validSettings)).toThrow();
|
||||
});
|
||||
|
||||
test('should not throw error for valid settings', () => {
|
||||
const validSettings: SettingsConfiguration = [
|
||||
{
|
||||
|
|
@ -128,7 +161,7 @@ describe('validateSettingDefinitions', () => {
|
|||
label: 'Theme Color',
|
||||
columns: 2,
|
||||
columnSpan: 1,
|
||||
optionType: 'model',
|
||||
optionType: OptionTypes.Custom,
|
||||
},
|
||||
{
|
||||
key: 'fontSize',
|
||||
|
|
@ -137,6 +170,7 @@ describe('validateSettingDefinitions', () => {
|
|||
range: { min: 8, max: 36 },
|
||||
default: 14,
|
||||
columnSpan: 2,
|
||||
optionType: OptionTypes.Custom,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -166,6 +200,7 @@ describe('validateSettingDefinitions', () => {
|
|||
columns: 4,
|
||||
range: { min: 8, max: 14 },
|
||||
default: 11,
|
||||
optionType: OptionTypes.Custom,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -175,7 +210,13 @@ describe('validateSettingDefinitions', () => {
|
|||
// Test for label defaulting to key if not provided
|
||||
test('label should default to key if not explicitly set', () => {
|
||||
const settingsWithDefaultLabel: SettingsConfiguration = [
|
||||
{ key: 'fontWeight', component: 'dropdown', type: 'string', options: ['normal', 'bold'] },
|
||||
{
|
||||
key: 'fontWeight',
|
||||
component: 'dropdown',
|
||||
type: 'string',
|
||||
options: ['normal', 'bold'],
|
||||
optionType: OptionTypes.Custom,
|
||||
},
|
||||
];
|
||||
|
||||
expect(() => validateSettingDefinitions(settingsWithDefaultLabel)).not.toThrow();
|
||||
|
|
@ -211,6 +252,7 @@ describe('validateSettingDefinitions', () => {
|
|||
component: 'slider',
|
||||
range: { min: 10, max: 20 },
|
||||
columns: 4,
|
||||
optionType: OptionTypes.Custom,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -297,7 +339,12 @@ describe('validateSettingDefinitions', () => {
|
|||
// Validate correct handling of boolean settings with default values
|
||||
test('correct handling of boolean settings with defaults', () => {
|
||||
const settings: SettingsConfiguration = [
|
||||
{ key: 'enableFeatureX', type: 'boolean', component: 'switch' }, // Missing default, should default to false
|
||||
{
|
||||
key: 'enableFeatureX',
|
||||
type: 'boolean',
|
||||
component: 'switch',
|
||||
optionType: OptionTypes.Custom,
|
||||
}, // Missing default, should default to false
|
||||
];
|
||||
|
||||
validateSettingDefinitions(settings); // This would populate default values where missing
|
||||
|
|
@ -308,7 +355,13 @@ describe('validateSettingDefinitions', () => {
|
|||
// Validate that number slider without default uses middle of range
|
||||
test('number slider without default uses middle of range', () => {
|
||||
const settings: SettingsConfiguration = [
|
||||
{ key: 'brightness', type: 'number', component: 'slider', range: { min: 0, max: 100 } }, // Missing default
|
||||
{
|
||||
key: 'brightness',
|
||||
type: 'number',
|
||||
component: 'slider',
|
||||
range: { min: 0, max: 100 },
|
||||
optionType: OptionTypes.Custom,
|
||||
}, // Missing default
|
||||
];
|
||||
|
||||
validateSettingDefinitions(settings); // This would populate default values where missing
|
||||
|
|
@ -330,6 +383,7 @@ const settingsConfiguration: SettingsConfiguration = [
|
|||
step: 0.01,
|
||||
},
|
||||
component: 'slider',
|
||||
optionType: OptionTypes.Custom,
|
||||
},
|
||||
{
|
||||
key: 'top_p',
|
||||
|
|
@ -343,6 +397,7 @@ const settingsConfiguration: SettingsConfiguration = [
|
|||
step: 0.01,
|
||||
},
|
||||
component: 'slider',
|
||||
optionType: OptionTypes.Custom,
|
||||
},
|
||||
{
|
||||
key: 'presence_penalty',
|
||||
|
|
@ -356,6 +411,7 @@ const settingsConfiguration: SettingsConfiguration = [
|
|||
step: 0.01,
|
||||
},
|
||||
component: 'slider',
|
||||
optionType: OptionTypes.Custom,
|
||||
},
|
||||
{
|
||||
key: 'frequency_penalty',
|
||||
|
|
@ -369,6 +425,7 @@ const settingsConfiguration: SettingsConfiguration = [
|
|||
step: 0.01,
|
||||
},
|
||||
component: 'slider',
|
||||
optionType: OptionTypes.Custom,
|
||||
},
|
||||
{
|
||||
key: 'resendFiles',
|
||||
|
|
@ -377,6 +434,7 @@ const settingsConfiguration: SettingsConfiguration = [
|
|||
type: 'boolean',
|
||||
default: true,
|
||||
component: 'switch',
|
||||
optionType: OptionTypes.Custom,
|
||||
},
|
||||
{
|
||||
key: 'imageDetail',
|
||||
|
|
@ -386,12 +444,14 @@ const settingsConfiguration: SettingsConfiguration = [
|
|||
default: 'auto',
|
||||
options: ['low', 'high', 'auto'],
|
||||
component: 'slider',
|
||||
optionType: OptionTypes.Custom,
|
||||
},
|
||||
{
|
||||
key: 'promptPrefix',
|
||||
type: 'string',
|
||||
default: '',
|
||||
component: 'input',
|
||||
optionType: OptionTypes.Custom,
|
||||
placeholder: 'Set custom instructions to include in System Message. Default: none',
|
||||
},
|
||||
{
|
||||
|
|
@ -399,6 +459,7 @@ const settingsConfiguration: SettingsConfiguration = [
|
|||
type: 'string',
|
||||
default: '',
|
||||
component: 'input',
|
||||
optionType: OptionTypes.Custom,
|
||||
placeholder: 'Set a custom name for your AI',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue