mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-27 13:48:51 +01:00
refactor: update CheckboxButton to support controlled state and enhance ToolsDropdown with permission-based toggles for web search and code interpreter
This commit is contained in:
parent
727d4a8a77
commit
9eb62370a4
5 changed files with 84 additions and 51 deletions
|
|
@ -29,7 +29,7 @@ function CodeInterpreter() {
|
|||
<CheckboxButton
|
||||
ref={triggerRef}
|
||||
className="max-w-fit"
|
||||
defaultChecked={runCode}
|
||||
checked={runCode}
|
||||
setValue={debouncedChange}
|
||||
label={localize('com_assistants_code_interpreter')}
|
||||
isCheckedClassName="border-purple-600/40 bg-purple-500/10 hover:bg-purple-700/10"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
import { Settings2, Search, ImageIcon, Globe, PenTool } from 'lucide-react';
|
||||
import { Settings2, Globe, TerminalSquareIcon } from 'lucide-react';
|
||||
import { Permissions, PermissionTypes } from 'librechat-data-provider';
|
||||
import { TooltipAnchor, DropdownPopup } from '~/components';
|
||||
import { useBadgeRowContext } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { useLocalize, useHasAccess } from '~/hooks';
|
||||
import type { MenuItemProps } from '~/common';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface ToolsDropdownProps {
|
||||
|
|
@ -12,12 +14,32 @@ interface ToolsDropdownProps {
|
|||
|
||||
const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
||||
const localize = useLocalize();
|
||||
const { conversationId } = useBadgeRowContext();
|
||||
const { webSearch, codeInterpreter } = useBadgeRowContext();
|
||||
const isDisabled = disabled ?? false;
|
||||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||
|
||||
const canUseWebSearch = useHasAccess({
|
||||
permissionType: PermissionTypes.WEB_SEARCH,
|
||||
permission: Permissions.USE,
|
||||
});
|
||||
|
||||
const canRunCode = useHasAccess({
|
||||
permissionType: PermissionTypes.RUN_CODE,
|
||||
permission: Permissions.USE,
|
||||
});
|
||||
|
||||
const handleWebSearchToggle = useCallback(() => {
|
||||
const newValue = !webSearch.toggleState;
|
||||
webSearch.debouncedChange({ isChecked: newValue });
|
||||
}, [webSearch]);
|
||||
|
||||
const handleCodeInterpreterToggle = useCallback(() => {
|
||||
const newValue = !codeInterpreter.toggleState;
|
||||
codeInterpreter.debouncedChange({ isChecked: newValue });
|
||||
}, [codeInterpreter]);
|
||||
|
||||
const dropdownItems = useMemo(() => {
|
||||
return [
|
||||
const items: MenuItemProps[] = [
|
||||
{
|
||||
render: () => (
|
||||
<div className="px-3 py-2 text-xs font-semibold text-text-secondary">
|
||||
|
|
@ -26,41 +48,40 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
|||
),
|
||||
hideOnClick: false,
|
||||
},
|
||||
{
|
||||
label: 'Search connectors',
|
||||
onClick: () => {
|
||||
// TODO: Implement search connectors functionality
|
||||
console.log('Search connectors clicked');
|
||||
},
|
||||
icon: <Search className="icon-md" />,
|
||||
badge: 'NEW',
|
||||
},
|
||||
{
|
||||
label: 'Create an image',
|
||||
onClick: () => {
|
||||
// TODO: Implement create image functionality
|
||||
console.log('Create an image clicked');
|
||||
},
|
||||
icon: <ImageIcon className="icon-md" />,
|
||||
},
|
||||
{
|
||||
label: 'Search the web',
|
||||
onClick: () => {
|
||||
// TODO: Implement web search functionality
|
||||
console.log('Search the web clicked');
|
||||
},
|
||||
icon: <Globe className="icon-md" />,
|
||||
},
|
||||
{
|
||||
label: 'Write or code',
|
||||
onClick: () => {
|
||||
// TODO: Implement write or code functionality
|
||||
console.log('Write or code clicked');
|
||||
},
|
||||
icon: <PenTool className="icon-md" />,
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
||||
if (canUseWebSearch) {
|
||||
items.push({
|
||||
onClick: handleWebSearchToggle,
|
||||
hideOnClick: true,
|
||||
render: (props) => (
|
||||
<div className="flex w-full cursor-pointer items-center justify-between" {...props}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="icon-md" />
|
||||
<span>{localize('com_ui_web_search')}</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (canRunCode) {
|
||||
items.push({
|
||||
onClick: handleCodeInterpreterToggle,
|
||||
hideOnClick: true,
|
||||
render: (props) => (
|
||||
<div className="flex w-full cursor-pointer items-center justify-between" {...props}>
|
||||
<div className="flex items-center gap-2">
|
||||
<TerminalSquareIcon className="icon-md" />
|
||||
<span>{localize('com_assistants_code_interpreter')}</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [canUseWebSearch, canRunCode, localize, handleWebSearchToggle, handleCodeInterpreterToggle]);
|
||||
|
||||
const menuTrigger = (
|
||||
<TooltipAnchor
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ function WebSearch() {
|
|||
<CheckboxButton
|
||||
ref={triggerRef}
|
||||
className="max-w-fit"
|
||||
defaultChecked={webSearch}
|
||||
checked={webSearch}
|
||||
setValue={debouncedChange}
|
||||
label={localize('com_ui_search')}
|
||||
isCheckedClassName="border-blue-600/40 bg-blue-500/10 hover:bg-blue-700/10"
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@ const CheckboxButton = React.forwardRef<
|
|||
icon?: React.ReactNode;
|
||||
label: string;
|
||||
className?: string;
|
||||
checked?: boolean;
|
||||
defaultChecked?: boolean;
|
||||
isCheckedClassName?: string;
|
||||
setValue?: (e: React.ChangeEvent<HTMLInputElement>, isChecked: boolean) => void;
|
||||
setValue?: (values: { e?: React.ChangeEvent<HTMLInputElement>; isChecked: boolean }) => void;
|
||||
}
|
||||
>(({ icon, label, setValue, className, defaultChecked, isCheckedClassName }, ref) => {
|
||||
>(({ icon, label, setValue, className, checked, defaultChecked, isCheckedClassName }, ref) => {
|
||||
const checkbox = useCheckboxStore();
|
||||
const isChecked = useStoreState(checkbox, (state) => state?.value);
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
|
@ -21,20 +22,28 @@ const CheckboxButton = React.forwardRef<
|
|||
if (typeof isChecked !== 'boolean') {
|
||||
return;
|
||||
}
|
||||
setValue?.(e, !isChecked);
|
||||
setValue?.({ e, isChecked: !isChecked });
|
||||
};
|
||||
|
||||
// Sync with controlled checked prop
|
||||
useEffect(() => {
|
||||
if (defaultChecked) {
|
||||
if (checked !== undefined) {
|
||||
checkbox.setValue(checked);
|
||||
}
|
||||
}, [checked, checkbox]);
|
||||
|
||||
// Set initial value from defaultChecked
|
||||
useEffect(() => {
|
||||
if (defaultChecked !== undefined && checked === undefined) {
|
||||
checkbox.setValue(defaultChecked);
|
||||
}
|
||||
}, [defaultChecked, checkbox]);
|
||||
}, [defaultChecked, checked, checkbox]);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
ref={ref}
|
||||
store={checkbox}
|
||||
onChange={onChange}
|
||||
defaultChecked={defaultChecked}
|
||||
className={cn(
|
||||
// Base styling from MultiSelect's selectClassName
|
||||
'group relative inline-flex items-center justify-center gap-1.5',
|
||||
|
|
|
|||
|
|
@ -54,8 +54,11 @@ export function useToolToggle({
|
|||
},
|
||||
);
|
||||
|
||||
const isAuthenticated =
|
||||
externalIsAuthenticated ?? (authConfig ? (authQuery?.data?.authenticated ?? false) : false);
|
||||
const isAuthenticated = useMemo(
|
||||
() =>
|
||||
externalIsAuthenticated ?? (authConfig ? (authQuery?.data?.authenticated ?? false) : false),
|
||||
[externalIsAuthenticated, authConfig, authQuery.data?.authenticated],
|
||||
);
|
||||
|
||||
const isToolEnabled = useMemo(() => {
|
||||
return ephemeralAgent?.[toolKey] ?? false;
|
||||
|
|
@ -72,10 +75,10 @@ export function useToolToggle({
|
|||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>, isChecked: boolean) => {
|
||||
({ e, isChecked }: { e?: React.ChangeEvent<HTMLInputElement>; isChecked: boolean }) => {
|
||||
if (isAuthenticated !== undefined && !isAuthenticated && setIsDialogOpen) {
|
||||
setIsDialogOpen(true);
|
||||
e.preventDefault();
|
||||
e?.preventDefault?.();
|
||||
return;
|
||||
}
|
||||
setToggleState(isChecked);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue