mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-13 05:58:51 +01:00
feat: Implement Default Preset Selection for Conversations 📌 (#1275)
* fix: type issues with icons * refactor: use react query for presets, show toasts on preset crud, refactor mutations, remove presetsQuery from Root (breaking change) * refactor: change preset titling * refactor: update preset schemas and methods for necessary new properties `order` and `defaultPreset` * feat: add `defaultPreset` Recoil value * refactor(getPresetTitle): make logic cleaner and more concise * feat: complete UI portion of defaultPreset feature, with animations added to preset items * chore: remove console.logs() * feat: complete default preset handling * refactor: remove user sensitive values on logout * fix: allow endpoint selection without default preset overwriting
This commit is contained in:
parent
fdb65366d7
commit
ca64efec1b
32 changed files with 681 additions and 270 deletions
|
|
@ -1,10 +1,6 @@
|
|||
import axios from 'axios';
|
||||
import filenamify from 'filenamify';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import exportFromJSON from 'export-from-json';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useGetEndpointsQuery } from 'librechat-data-provider';
|
||||
import type { TEditPresetProps } from '~/common';
|
||||
import { cn, defaultTextProps, removeFocusOutlines, cleanupPreset, mapEndpoints } from '~/utils';
|
||||
import { cn, defaultTextProps, removeFocusOutlines, mapEndpoints } from '~/utils';
|
||||
import { Input, Label, Dropdown, Dialog, DialogClose, DialogButton } from '~/components/';
|
||||
import PopoverButtons from '~/components/Endpoints/PopoverButtons';
|
||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
||||
|
|
@ -13,42 +9,21 @@ import { EndpointSettings } from '~/components/Endpoints';
|
|||
import { useChatContext } from '~/Providers';
|
||||
import store from '~/store';
|
||||
|
||||
const EditPresetDialog = ({ open, onOpenChange, title }: Omit<TEditPresetProps, 'preset'>) => {
|
||||
const EditPresetDialog = ({
|
||||
exportPreset,
|
||||
submitPreset,
|
||||
}: {
|
||||
exportPreset: () => void;
|
||||
submitPreset: () => void;
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
const { preset } = useChatContext();
|
||||
const { setOption } = useSetIndexOptions(preset);
|
||||
const [presetModalVisible, setPresetModalVisible] = useRecoilState(store.presetModalVisible);
|
||||
|
||||
// TODO: use React Query for presets data
|
||||
const setPresets = useSetRecoilState(store.presets);
|
||||
const { data: availableEndpoints = [] } = useGetEndpointsQuery({
|
||||
select: mapEndpoints,
|
||||
});
|
||||
const { setOption } = useSetIndexOptions(preset);
|
||||
const localize = useLocalize();
|
||||
|
||||
const submitPreset = () => {
|
||||
if (!preset) {
|
||||
return;
|
||||
}
|
||||
axios({
|
||||
method: 'post',
|
||||
url: '/api/presets',
|
||||
data: cleanupPreset({ preset }),
|
||||
withCredentials: true,
|
||||
}).then((res) => {
|
||||
setPresets(res?.data);
|
||||
});
|
||||
};
|
||||
|
||||
const exportPreset = () => {
|
||||
if (!preset) {
|
||||
return;
|
||||
}
|
||||
const fileName = filenamify(preset?.title || 'preset');
|
||||
exportFromJSON({
|
||||
data: cleanupPreset({ preset }),
|
||||
fileName,
|
||||
exportType: exportFromJSON.types.json,
|
||||
});
|
||||
};
|
||||
|
||||
const { endpoint } = preset || {};
|
||||
if (!endpoint) {
|
||||
|
|
@ -56,9 +31,9 @@ const EditPresetDialog = ({ open, onOpenChange, title }: Omit<TEditPresetProps,
|
|||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog open={presetModalVisible} onOpenChange={setPresetModalVisible}>
|
||||
<DialogTemplate
|
||||
title={`${title || localize('com_ui_edit') + ' ' + localize('com_endpoint_preset')} - ${
|
||||
title={`${localize('com_ui_edit') + ' ' + localize('com_endpoint_preset')} - ${
|
||||
preset?.title
|
||||
}`}
|
||||
className="h-full max-w-full overflow-y-auto pb-4 sm:w-[680px] sm:pb-0 md:h-[720px] md:w-[750px] md:overflow-y-hidden lg:w-[950px] xl:h-[720px]"
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
import type { FC } from 'react';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Close } from '@radix-ui/react-popover';
|
||||
import { Flipper, Flipped } from 'react-flip-toolkit';
|
||||
import type { FC } from 'react';
|
||||
import type { TPreset } from 'librechat-data-provider';
|
||||
import FileUpload from '~/components/Input/EndpointMenu/FileUpload';
|
||||
import { PinIcon, EditIcon, TrashIcon } from '~/components/svg';
|
||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
||||
import { Dialog, DialogTrigger } from '~/components/ui/';
|
||||
import { EditIcon, TrashIcon } from '~/components/svg';
|
||||
import { MenuSeparator, MenuItem } from '../UI';
|
||||
import { icons } from '../Endpoints/Icons';
|
||||
import { getPresetTitle } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
const PresetItems: FC<{
|
||||
presets: TPreset[];
|
||||
onSetDefaultPreset: (preset: TPreset, remove?: boolean) => void;
|
||||
onSelectPreset: (preset: TPreset) => void;
|
||||
onChangePreset: (preset: TPreset) => void;
|
||||
onDeletePreset: (preset: TPreset) => void;
|
||||
|
|
@ -20,12 +24,14 @@ const PresetItems: FC<{
|
|||
onFileSelected: (jsonData: Record<string, unknown>) => void;
|
||||
}> = ({
|
||||
presets,
|
||||
onSetDefaultPreset,
|
||||
onSelectPreset,
|
||||
onChangePreset,
|
||||
onDeletePreset,
|
||||
clearAllPresets,
|
||||
onFileSelected,
|
||||
}) => {
|
||||
const defaultPreset = useRecoilValue(store.defaultPreset);
|
||||
const localize = useLocalize();
|
||||
return (
|
||||
<>
|
||||
|
|
@ -35,11 +41,19 @@ const PresetItems: FC<{
|
|||
tabIndex={-1}
|
||||
>
|
||||
<div className="flex h-full grow items-center justify-end gap-2">
|
||||
<label
|
||||
htmlFor="default-preset"
|
||||
className="w-40 truncate rounded bg-transparent px-2 py-1 text-xs font-medium font-normal text-gray-600 transition-colors dark:bg-transparent dark:text-gray-300 sm:w-72"
|
||||
>
|
||||
{defaultPreset
|
||||
? `${localize('com_endpoint_preset_default_item')} ${defaultPreset.title}`
|
||||
: localize('com_endpoint_preset_default_none')}
|
||||
</label>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<label
|
||||
htmlFor="file-upload"
|
||||
className="mr-1 flex h-[32px] h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium font-normal text-gray-600 transition-colors hover:bg-slate-200 hover:text-red-700 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-green-500"
|
||||
className="mr-1 flex h-[32px] cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium font-normal text-gray-600 transition-colors hover:bg-slate-200 hover:text-red-700 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-green-500"
|
||||
>
|
||||
<Trash2 className="mr-1 flex w-[22px] items-center stroke-1" />
|
||||
{localize('com_ui_clear')} {localize('com_ui_all')}
|
||||
|
|
@ -71,56 +85,70 @@ const PresetItems: FC<{
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{presets &&
|
||||
presets.length > 0 &&
|
||||
presets.map((preset, i) => {
|
||||
if (!preset) {
|
||||
return null;
|
||||
}
|
||||
<Flipper flipKey={presets.map(({ presetId }) => presetId).join('.')}>
|
||||
{presets &&
|
||||
presets.length > 0 &&
|
||||
presets.map((preset, i) => {
|
||||
if (!preset || !preset.presetId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Close asChild key={`preset-${preset.presetId}`}>
|
||||
<div key={`preset-${preset.presetId}`}>
|
||||
<MenuItem
|
||||
key={`preset-item-${preset.presetId}`}
|
||||
textClassName="text-xs max-w-[200px] truncate md:max-w-full "
|
||||
title={getPresetTitle(preset)}
|
||||
disableHover={true}
|
||||
onClick={() => onSelectPreset(preset)}
|
||||
icon={icons[preset.endpoint ?? 'unknown']({ className: 'icon-md mr-1 ' })}
|
||||
// value={preset.presetId}
|
||||
selected={false}
|
||||
data-testid={`preset-item-${preset}`}
|
||||
// description="With DALL·E, browsing and analysis"
|
||||
>
|
||||
<div className="flex h-full items-center justify-end gap-1">
|
||||
<button
|
||||
className="m-0 h-full rounded-md p-2 px-4 text-gray-400 hover:text-gray-700 dark:bg-gray-700 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:group-hover:visible"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onChangePreset(preset);
|
||||
}}
|
||||
return (
|
||||
<Close asChild key={`preset-${preset.presetId}`}>
|
||||
<div key={`preset-${preset.presetId}`}>
|
||||
<Flipped flipId={preset.presetId}>
|
||||
<MenuItem
|
||||
key={`preset-item-${preset.presetId}`}
|
||||
textClassName="text-xs max-w-[150px] sm:max-w-[200px] truncate md:max-w-full "
|
||||
title={getPresetTitle(preset)}
|
||||
disableHover={true}
|
||||
onClick={() => onSelectPreset(preset)}
|
||||
icon={icons[preset.endpoint ?? 'unknown']({
|
||||
className: 'icon-md mr-1 dark:text-white',
|
||||
})}
|
||||
selected={false}
|
||||
data-testid={`preset-item-${preset}`}
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
<button
|
||||
className="m-0 h-full rounded-md p-2 px-4 text-gray-400 hover:text-gray-700 dark:bg-gray-700 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:group-hover:visible"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDeletePreset(preset);
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</div>
|
||||
</MenuItem>
|
||||
{i !== presets.length - 1 && <MenuSeparator />}
|
||||
</div>
|
||||
</Close>
|
||||
);
|
||||
})}
|
||||
<div className="flex h-full items-center justify-end gap-1">
|
||||
<button
|
||||
className="m-0 h-full rounded-md p-2 px-4 text-gray-400 hover:text-gray-700 dark:bg-gray-700 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:group-hover:visible"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onSetDefaultPreset(preset, defaultPreset?.presetId === preset.presetId);
|
||||
}}
|
||||
>
|
||||
<PinIcon unpin={defaultPreset?.presetId === preset.presetId} />
|
||||
</button>
|
||||
<button
|
||||
className="m-0 h-full rounded-md p-2 px-4 text-gray-400 hover:text-gray-700 dark:bg-gray-700 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:group-hover:visible"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onChangePreset(preset);
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
<button
|
||||
className="m-0 h-full rounded-md p-2 px-4 text-gray-400 hover:text-gray-700 dark:bg-gray-700 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:group-hover:visible"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDeletePreset(preset);
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</Flipped>
|
||||
{i !== presets.length - 1 && <MenuSeparator />}
|
||||
</div>
|
||||
</Close>
|
||||
);
|
||||
})}
|
||||
</Flipper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue