mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20:15 +01:00
🔀 fix: Rerender Edge Cases After Migration to Shared Package (#8713)
* fix: render issues in PromptForm by decoupling nested dependencies as a result of @librechat/client components * fix: MemoryViewer flicker by moving EditMemoryButton and DeleteMemoryButton outside of rendering * fix: CategorySelector to use DropdownPopup for improved mobile compatibility * chore: imports
This commit is contained in:
parent
8e6eef04ab
commit
a4ca4b7d9d
4 changed files with 401 additions and 268 deletions
|
|
@ -1,8 +1,10 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
import { Trash2 } from 'lucide-react';
|
import { Trash2 } from 'lucide-react';
|
||||||
|
import { useDeletePrompt } from '~/data-provider';
|
||||||
import { Button, OGDialog, OGDialogTrigger, Label, OGDialogTemplate } from '@librechat/client';
|
import { Button, OGDialog, OGDialogTrigger, Label, OGDialogTemplate } from '@librechat/client';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
const DeleteVersion = ({
|
const DeleteConfirmDialog = ({
|
||||||
name,
|
name,
|
||||||
disabled,
|
disabled,
|
||||||
selectHandler,
|
selectHandler,
|
||||||
|
|
@ -58,4 +60,42 @@ const DeleteVersion = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteVersion;
|
interface DeletePromptProps {
|
||||||
|
promptId?: string;
|
||||||
|
groupId: string;
|
||||||
|
promptName: string;
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeletePrompt = React.memo(
|
||||||
|
({ promptId, groupId, promptName, disabled }: DeletePromptProps) => {
|
||||||
|
const deletePromptMutation = useDeletePrompt();
|
||||||
|
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
if (!promptId) {
|
||||||
|
console.warn('No prompt ID provided for deletion');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deletePromptMutation.mutate({
|
||||||
|
_id: promptId,
|
||||||
|
groupId,
|
||||||
|
});
|
||||||
|
}, [promptId, groupId, deletePromptMutation]);
|
||||||
|
|
||||||
|
if (!promptId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DeleteConfirmDialog
|
||||||
|
name={promptName}
|
||||||
|
disabled={disabled || !promptId}
|
||||||
|
selectHandler={handleDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
DeletePrompt.displayName = 'DeletePrompt';
|
||||||
|
|
||||||
|
export default DeletePrompt;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { Dropdown } from '@librechat/client';
|
import * as Ariakit from '@ariakit/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useFormContext, Controller } from 'react-hook-form';
|
import { DropdownPopup } from '@librechat/client';
|
||||||
import { LocalStorageKeys } from 'librechat-data-provider';
|
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||||
|
import { useFormContext, Controller } from 'react-hook-form';
|
||||||
|
import type { MenuItemProps } from '@librechat/client';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useCategories } from '~/hooks';
|
import { useCategories } from '~/hooks';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
interface CategorySelectorProps {
|
interface CategorySelectorProps {
|
||||||
currentCategory?: string;
|
currentCategory?: string;
|
||||||
|
|
@ -20,10 +23,11 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const formContext = useFormContext();
|
const formContext = useFormContext();
|
||||||
const { categories, emptyCategory } = useCategories();
|
const { categories, emptyCategory } = useCategories();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const control = formContext.control;
|
const control = formContext?.control;
|
||||||
const watch = formContext.watch;
|
const watch = formContext?.watch;
|
||||||
const setValue = formContext.setValue;
|
const setValue = formContext?.setValue;
|
||||||
|
|
||||||
const watchedCategory = watch ? watch('category') : currentCategory;
|
const watchedCategory = watch ? watch('category') : currentCategory;
|
||||||
|
|
||||||
|
|
@ -46,53 +50,71 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
||||||
return categoryOption;
|
return categoryOption;
|
||||||
}, [categoryOption, t]);
|
}, [categoryOption, t]);
|
||||||
|
|
||||||
|
const menuItems: MenuItemProps[] = useMemo(() => {
|
||||||
|
if (!categories) return [];
|
||||||
|
|
||||||
|
return categories.map((category) => ({
|
||||||
|
id: category.value,
|
||||||
|
label: category.label,
|
||||||
|
icon: 'icon' in category ? category.icon : undefined,
|
||||||
|
onClick: () => {
|
||||||
|
const value = category.value || '';
|
||||||
|
if (formContext && setValue) {
|
||||||
|
setValue('category', value, { shouldDirty: false });
|
||||||
|
}
|
||||||
|
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
|
||||||
|
onValueChange?.(value);
|
||||||
|
setIsOpen(false);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, [categories, formContext, setValue, onValueChange]);
|
||||||
|
|
||||||
|
const trigger = (
|
||||||
|
<Ariakit.MenuButton
|
||||||
|
className={cn(
|
||||||
|
'focus:ring-offset-ring-offset relative inline-flex items-center justify-between rounded-xl border border-input bg-background px-3 py-2 text-sm text-text-primary transition-all duration-200 ease-in-out hover:bg-accent hover:text-accent-foreground focus:ring-ring-primary',
|
||||||
|
'w-fit gap-2',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
aria-label="Prompt's category selector"
|
||||||
|
aria-labelledby="category-selector-label"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{'icon' in displayCategory && displayCategory.icon != null && (
|
||||||
|
<span>{displayCategory.icon as ReactNode}</span>
|
||||||
|
)}
|
||||||
|
<span>{displayCategory.value ? displayCategory.label : t('com_ui_category')}</span>
|
||||||
|
</div>
|
||||||
|
<Ariakit.MenuButtonArrow />
|
||||||
|
</Ariakit.MenuButton>
|
||||||
|
);
|
||||||
|
|
||||||
return formContext ? (
|
return formContext ? (
|
||||||
<Controller
|
<Controller
|
||||||
name="category"
|
name="category"
|
||||||
control={control}
|
control={control}
|
||||||
render={() => (
|
render={() => (
|
||||||
<Dropdown
|
<DropdownPopup
|
||||||
value={displayCategory.value ?? ''}
|
trigger={trigger}
|
||||||
label={displayCategory.value ? undefined : t('com_ui_category')}
|
items={menuItems}
|
||||||
onChange={(value: string) => {
|
isOpen={isOpen}
|
||||||
setValue('category', value, { shouldDirty: false });
|
setIsOpen={setIsOpen}
|
||||||
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
|
menuId="category-selector-menu"
|
||||||
onValueChange?.(value);
|
className="mt-2"
|
||||||
}}
|
portal={true}
|
||||||
aria-labelledby="category-selector-label"
|
|
||||||
ariaLabel="Prompt's category selector"
|
|
||||||
className={className}
|
|
||||||
options={categories || []}
|
|
||||||
renderValue={() => (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
{'icon' in displayCategory && displayCategory.icon != null && (
|
|
||||||
<span>{displayCategory.icon as ReactNode}</span>
|
|
||||||
)}
|
|
||||||
<span>{displayCategory.label}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Dropdown
|
<DropdownPopup
|
||||||
value={currentCategory ?? ''}
|
trigger={trigger}
|
||||||
onChange={(value: string) => {
|
items={menuItems}
|
||||||
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
|
isOpen={isOpen}
|
||||||
onValueChange?.(value);
|
setIsOpen={setIsOpen}
|
||||||
}}
|
menuId="category-selector-menu"
|
||||||
aria-labelledby="category-selector-label"
|
className="mt-2"
|
||||||
ariaLabel="Prompt's category selector"
|
portal={true}
|
||||||
className={className}
|
|
||||||
options={categories || []}
|
|
||||||
renderValue={() => (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
{'icon' in displayCategory && displayCategory.icon != null && (
|
|
||||||
<span>{displayCategory.icon as ReactNode}</span>
|
|
||||||
)}
|
|
||||||
<span>{displayCategory.label}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
import { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
||||||
|
import React from 'react';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Menu, Rocket } from 'lucide-react';
|
import { Menu, Rocket } from 'lucide-react';
|
||||||
|
|
@ -6,14 +7,13 @@ import { useForm, FormProvider } from 'react-hook-form';
|
||||||
import { useParams, useOutletContext } from 'react-router-dom';
|
import { useParams, useOutletContext } from 'react-router-dom';
|
||||||
import { Button, Skeleton, useToastContext } from '@librechat/client';
|
import { Button, Skeleton, useToastContext } from '@librechat/client';
|
||||||
import { SystemRoles, PermissionTypes, Permissions } from 'librechat-data-provider';
|
import { SystemRoles, PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||||
import type { TCreatePrompt } from 'librechat-data-provider';
|
import type { TCreatePrompt, TPrompt, TPromptGroup } from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
useCreatePrompt,
|
|
||||||
useGetPrompts,
|
useGetPrompts,
|
||||||
|
useCreatePrompt,
|
||||||
useGetPromptGroup,
|
useGetPromptGroup,
|
||||||
useUpdatePromptGroup,
|
useUpdatePromptGroup,
|
||||||
useMakePromptProduction,
|
useMakePromptProduction,
|
||||||
useDeletePrompt,
|
|
||||||
} from '~/data-provider';
|
} from '~/data-provider';
|
||||||
import { useAuthContext, usePromptGroupsNav, useHasAccess, useLocalize } from '~/hooks';
|
import { useAuthContext, usePromptGroupsNav, useHasAccess, useLocalize } from '~/hooks';
|
||||||
import CategorySelector from './Groups/CategorySelector';
|
import CategorySelector from './Groups/CategorySelector';
|
||||||
|
|
@ -22,7 +22,7 @@ import PromptVariables from './PromptVariables';
|
||||||
import { cn, findPromptGroup } from '~/utils';
|
import { cn, findPromptGroup } from '~/utils';
|
||||||
import PromptVersions from './PromptVersions';
|
import PromptVersions from './PromptVersions';
|
||||||
import { PromptsEditorMode } from '~/common';
|
import { PromptsEditorMode } from '~/common';
|
||||||
import DeleteConfirm from './DeleteVersion';
|
import DeleteVersion from './DeleteVersion';
|
||||||
import PromptDetails from './PromptDetails';
|
import PromptDetails from './PromptDetails';
|
||||||
import PromptEditor from './PromptEditor';
|
import PromptEditor from './PromptEditor';
|
||||||
import SkeletonForm from './SkeletonForm';
|
import SkeletonForm from './SkeletonForm';
|
||||||
|
|
@ -32,16 +32,136 @@ import PromptName from './PromptName';
|
||||||
import Command from './Command';
|
import Command from './Command';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
interface RightPanelProps {
|
||||||
|
group: TPromptGroup;
|
||||||
|
prompts: TPrompt[];
|
||||||
|
selectedPrompt: any;
|
||||||
|
selectionIndex: number;
|
||||||
|
selectedPromptId?: string;
|
||||||
|
isLoadingPrompts: boolean;
|
||||||
|
setSelectionIndex: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RightPanel = React.memo(
|
||||||
|
({
|
||||||
|
group,
|
||||||
|
prompts,
|
||||||
|
selectedPrompt,
|
||||||
|
selectedPromptId,
|
||||||
|
isLoadingPrompts,
|
||||||
|
selectionIndex,
|
||||||
|
setSelectionIndex,
|
||||||
|
}: RightPanelProps) => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const editorMode = useRecoilValue(store.promptsEditorMode);
|
||||||
|
const hasShareAccess = useHasAccess({
|
||||||
|
permissionType: PermissionTypes.PROMPTS,
|
||||||
|
permission: Permissions.SHARED_GLOBAL,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateGroupMutation = useUpdatePromptGroup({
|
||||||
|
onError: () => {
|
||||||
|
showToast({
|
||||||
|
status: 'error',
|
||||||
|
message: localize('com_ui_prompt_update_error'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeProductionMutation = useMakePromptProduction();
|
||||||
|
|
||||||
|
const groupId = group?._id || '';
|
||||||
|
const groupName = group?.name || '';
|
||||||
|
const groupCategory = group?.category || '';
|
||||||
|
const isLoadingGroup = !group;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="h-full w-full overflow-y-auto bg-surface-primary px-4"
|
||||||
|
style={{ maxHeight: 'calc(100vh - 100px)' }}
|
||||||
|
>
|
||||||
|
<div className="mb-2 flex flex-col lg:flex-row lg:items-center lg:justify-center lg:gap-x-2 xl:flex-row xl:space-y-0">
|
||||||
|
<CategorySelector
|
||||||
|
currentCategory={groupCategory}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateGroupMutation.mutate({
|
||||||
|
id: groupId,
|
||||||
|
payload: { name: groupName, category: value },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="mt-2 flex flex-row items-center justify-center gap-x-2 lg:mt-0">
|
||||||
|
{hasShareAccess && <SharePrompt group={group} disabled={isLoadingGroup} />}
|
||||||
|
{editorMode === PromptsEditorMode.ADVANCED && (
|
||||||
|
<Button
|
||||||
|
variant="submit"
|
||||||
|
size="sm"
|
||||||
|
aria-label="Make prompt production"
|
||||||
|
className="h-10 w-10 border border-transparent p-0.5 transition-all"
|
||||||
|
onClick={() => {
|
||||||
|
if (!selectedPrompt) {
|
||||||
|
console.warn('No prompt is selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { _id: promptVersionId = '', prompt } = selectedPrompt;
|
||||||
|
makeProductionMutation.mutate({
|
||||||
|
id: promptVersionId,
|
||||||
|
groupId,
|
||||||
|
productionPrompt: { prompt },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
isLoadingGroup ||
|
||||||
|
!selectedPrompt ||
|
||||||
|
selectedPrompt._id === group?.productionId ||
|
||||||
|
makeProductionMutation.isLoading
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Rocket className="size-5 cursor-pointer text-white" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<DeleteVersion
|
||||||
|
promptId={selectedPromptId}
|
||||||
|
groupId={groupId}
|
||||||
|
promptName={groupName}
|
||||||
|
disabled={isLoadingGroup}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{editorMode === PromptsEditorMode.ADVANCED &&
|
||||||
|
(isLoadingPrompts
|
||||||
|
? Array.from({ length: 6 }).map((_, index: number) => (
|
||||||
|
<div key={index} className="my-2">
|
||||||
|
<Skeleton className="h-[72px] w-full" />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: prompts.length > 0 && (
|
||||||
|
<PromptVersions
|
||||||
|
group={group}
|
||||||
|
prompts={prompts}
|
||||||
|
selectionIndex={selectionIndex}
|
||||||
|
setSelectionIndex={setSelectionIndex}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
RightPanel.displayName = 'RightPanel';
|
||||||
|
|
||||||
const PromptForm = () => {
|
const PromptForm = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { user } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd);
|
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
|
const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd);
|
||||||
const promptId = params.promptId || '';
|
const promptId = params.promptId || '';
|
||||||
|
|
||||||
const [selectionIndex, setSelectionIndex] = useState<number>(0);
|
|
||||||
const editorMode = useRecoilValue(store.promptsEditorMode);
|
const editorMode = useRecoilValue(store.promptsEditorMode);
|
||||||
|
const [selectionIndex, setSelectionIndex] = useState<number>(0);
|
||||||
|
|
||||||
const prevIsEditingRef = useRef(false);
|
const prevIsEditingRef = useRef(false);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [initialLoad, setInitialLoad] = useState(true);
|
const [initialLoad, setInitialLoad] = useState(true);
|
||||||
|
|
@ -72,11 +192,9 @@ const PromptForm = () => {
|
||||||
[prompts, selectionIndex],
|
[prompts, selectionIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const selectedPromptId = useMemo(() => selectedPrompt?._id, [selectedPrompt?._id]);
|
||||||
|
|
||||||
const { groupsQuery } = useOutletContext<ReturnType<typeof usePromptGroupsNav>>();
|
const { groupsQuery } = useOutletContext<ReturnType<typeof usePromptGroupsNav>>();
|
||||||
const hasShareAccess = useHasAccess({
|
|
||||||
permissionType: PermissionTypes.PROMPTS,
|
|
||||||
permission: Permissions.SHARED_GLOBAL,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateGroupMutation = useUpdatePromptGroup({
|
const updateGroupMutation = useUpdatePromptGroup({
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
|
@ -88,7 +206,6 @@ const PromptForm = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeProductionMutation = useMakePromptProduction();
|
const makeProductionMutation = useMakePromptProduction();
|
||||||
const deletePromptMutation = useDeletePrompt();
|
|
||||||
|
|
||||||
const createPromptMutation = useCreatePrompt({
|
const createPromptMutation = useCreatePrompt({
|
||||||
onMutate: (variables) => {
|
onMutate: (variables) => {
|
||||||
|
|
@ -177,24 +294,40 @@ const PromptForm = () => {
|
||||||
return () => window.removeEventListener('resize', handleResize);
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const debouncedUpdateOneliner = useCallback(
|
const debouncedUpdateOneliner = useMemo(
|
||||||
debounce((oneliner: string) => {
|
() =>
|
||||||
if (!group || !group._id) {
|
debounce((groupId: string, oneliner: string, mutate: any) => {
|
||||||
return console.warn('Group not found');
|
mutate({ id: groupId, payload: { oneliner } });
|
||||||
}
|
}, 950),
|
||||||
updateGroupMutation.mutate({ id: group._id, payload: { oneliner } });
|
[],
|
||||||
}, 950),
|
|
||||||
[updateGroupMutation, group],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedUpdateCommand = useCallback(
|
const debouncedUpdateCommand = useMemo(
|
||||||
debounce((command: string) => {
|
() =>
|
||||||
|
debounce((groupId: string, command: string, mutate: any) => {
|
||||||
|
mutate({ id: groupId, payload: { command } });
|
||||||
|
}, 950),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUpdateOneliner = useCallback(
|
||||||
|
(oneliner: string) => {
|
||||||
if (!group || !group._id) {
|
if (!group || !group._id) {
|
||||||
return console.warn('Group not found');
|
return console.warn('Group not found');
|
||||||
}
|
}
|
||||||
updateGroupMutation.mutate({ id: group._id, payload: { command } });
|
debouncedUpdateOneliner(group._id, oneliner, updateGroupMutation.mutate);
|
||||||
}, 950),
|
},
|
||||||
[updateGroupMutation, group],
|
[group, updateGroupMutation.mutate, debouncedUpdateOneliner],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUpdateCommand = useCallback(
|
||||||
|
(command: string) => {
|
||||||
|
if (!group || !group._id) {
|
||||||
|
return console.warn('Group not found');
|
||||||
|
}
|
||||||
|
debouncedUpdateCommand(group._id, command, updateGroupMutation.mutate);
|
||||||
|
},
|
||||||
|
[group, updateGroupMutation.mutate, debouncedUpdateCommand],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (initialLoad) {
|
if (initialLoad) {
|
||||||
|
|
@ -217,89 +350,7 @@ const PromptForm = () => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupId = group._id;
|
|
||||||
|
|
||||||
const groupName = group.name;
|
const groupName = group.name;
|
||||||
const groupCategory = group.category;
|
|
||||||
|
|
||||||
const RightPanel = () => (
|
|
||||||
<div
|
|
||||||
className="h-full w-full overflow-y-auto bg-surface-primary px-4"
|
|
||||||
style={{ maxHeight: 'calc(100vh - 100px)' }}
|
|
||||||
>
|
|
||||||
<div className="mb-2 flex flex-col lg:flex-row lg:items-center lg:justify-center lg:gap-x-2 xl:flex-row xl:space-y-0">
|
|
||||||
<CategorySelector
|
|
||||||
currentCategory={groupCategory}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateGroupMutation.mutate({
|
|
||||||
id: groupId,
|
|
||||||
payload: { name: groupName, category: value },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className="mt-2 flex flex-row items-center justify-center gap-x-2 lg:mt-0">
|
|
||||||
{hasShareAccess && <SharePrompt group={group} disabled={isLoadingGroup} />}
|
|
||||||
{editorMode === PromptsEditorMode.ADVANCED && (
|
|
||||||
<Button
|
|
||||||
variant="submit"
|
|
||||||
size="sm"
|
|
||||||
aria-label="Make prompt production"
|
|
||||||
className="h-10 w-10 border border-transparent p-0.5 transition-all"
|
|
||||||
onClick={() => {
|
|
||||||
if (!selectedPrompt) {
|
|
||||||
console.warn('No prompt is selected');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { _id: promptVersionId = '', prompt } = selectedPrompt;
|
|
||||||
makeProductionMutation.mutate({
|
|
||||||
id: promptVersionId,
|
|
||||||
groupId,
|
|
||||||
productionPrompt: { prompt },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
disabled={
|
|
||||||
isLoadingGroup ||
|
|
||||||
!selectedPrompt ||
|
|
||||||
selectedPrompt._id === group.productionId ||
|
|
||||||
makeProductionMutation.isLoading
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Rocket className="size-5 cursor-pointer text-white" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<DeleteConfirm
|
|
||||||
name={groupName}
|
|
||||||
disabled={isLoadingGroup}
|
|
||||||
selectHandler={() => {
|
|
||||||
if (!selectedPrompt || !selectedPrompt._id) {
|
|
||||||
console.warn('No prompt is selected or prompt _id is missing');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
deletePromptMutation.mutate({
|
|
||||||
_id: selectedPrompt._id,
|
|
||||||
groupId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{editorMode === PromptsEditorMode.ADVANCED &&
|
|
||||||
(isLoadingPrompts
|
|
||||||
? Array.from({ length: 6 }).map((_, index: number) => (
|
|
||||||
<div key={index} className="my-2">
|
|
||||||
<Skeleton className="h-[72px] w-full" />
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
: prompts.length > 0 && (
|
|
||||||
<PromptVersions
|
|
||||||
group={group}
|
|
||||||
prompts={prompts}
|
|
||||||
selectionIndex={selectionIndex}
|
|
||||||
setSelectionIndex={setSelectionIndex}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
|
|
@ -339,7 +390,17 @@ const PromptForm = () => {
|
||||||
<Menu className="size-5" />
|
<Menu className="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
<div className="hidden lg:block">
|
<div className="hidden lg:block">
|
||||||
{editorMode === PromptsEditorMode.SIMPLE && <RightPanel />}
|
{editorMode === PromptsEditorMode.SIMPLE && (
|
||||||
|
<RightPanel
|
||||||
|
group={group}
|
||||||
|
prompts={prompts}
|
||||||
|
selectedPrompt={selectedPrompt}
|
||||||
|
selectionIndex={selectionIndex}
|
||||||
|
selectedPromptId={selectedPromptId}
|
||||||
|
isLoadingPrompts={isLoadingPrompts}
|
||||||
|
setSelectionIndex={setSelectionIndex}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -352,11 +413,11 @@ const PromptForm = () => {
|
||||||
<PromptVariables promptText={promptText} />
|
<PromptVariables promptText={promptText} />
|
||||||
<Description
|
<Description
|
||||||
initialValue={group.oneliner ?? ''}
|
initialValue={group.oneliner ?? ''}
|
||||||
onValueChange={debouncedUpdateOneliner}
|
onValueChange={handleUpdateOneliner}
|
||||||
/>
|
/>
|
||||||
<Command
|
<Command
|
||||||
initialValue={group.command ?? ''}
|
initialValue={group.command ?? ''}
|
||||||
onValueChange={debouncedUpdateCommand}
|
onValueChange={handleUpdateCommand}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -364,7 +425,15 @@ const PromptForm = () => {
|
||||||
|
|
||||||
{editorMode === PromptsEditorMode.ADVANCED && (
|
{editorMode === PromptsEditorMode.ADVANCED && (
|
||||||
<div className="hidden w-1/4 border-l border-border-light lg:block">
|
<div className="hidden w-1/4 border-l border-border-light lg:block">
|
||||||
<RightPanel />
|
<RightPanel
|
||||||
|
group={group}
|
||||||
|
prompts={prompts}
|
||||||
|
selectionIndex={selectionIndex}
|
||||||
|
selectedPrompt={selectedPrompt}
|
||||||
|
selectedPromptId={selectedPromptId}
|
||||||
|
isLoadingPrompts={isLoadingPrompts}
|
||||||
|
setSelectionIndex={setSelectionIndex}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -395,7 +464,15 @@ const PromptForm = () => {
|
||||||
>
|
>
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<div className="h-full overflow-auto">
|
<div className="h-full overflow-auto">
|
||||||
<RightPanel />
|
<RightPanel
|
||||||
|
group={group}
|
||||||
|
prompts={prompts}
|
||||||
|
selectionIndex={selectionIndex}
|
||||||
|
selectedPrompt={selectedPrompt}
|
||||||
|
selectedPromptId={selectedPromptId}
|
||||||
|
isLoadingPrompts={isLoadingPrompts}
|
||||||
|
setSelectionIndex={setSelectionIndex}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,18 @@ import { Plus } from 'lucide-react';
|
||||||
import { matchSorter } from 'match-sorter';
|
import { matchSorter } from 'match-sorter';
|
||||||
import { SystemRoles, PermissionTypes, Permissions } from 'librechat-data-provider';
|
import { SystemRoles, PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
Spinner,
|
|
||||||
EditIcon,
|
|
||||||
TrashIcon,
|
|
||||||
Table,
|
Table,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Button,
|
Button,
|
||||||
Switch,
|
Switch,
|
||||||
|
Spinner,
|
||||||
TableRow,
|
TableRow,
|
||||||
OGDialog,
|
OGDialog,
|
||||||
|
EditIcon,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
TrashIcon,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TooltipAnchor,
|
TooltipAnchor,
|
||||||
|
|
@ -25,10 +25,10 @@ import {
|
||||||
} from '@librechat/client';
|
} from '@librechat/client';
|
||||||
import type { TUserMemory } from 'librechat-data-provider';
|
import type { TUserMemory } from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
useGetUserQuery,
|
|
||||||
useMemoriesQuery,
|
|
||||||
useDeleteMemoryMutation,
|
|
||||||
useUpdateMemoryPreferencesMutation,
|
useUpdateMemoryPreferencesMutation,
|
||||||
|
useDeleteMemoryMutation,
|
||||||
|
useMemoriesQuery,
|
||||||
|
useGetUserQuery,
|
||||||
} from '~/data-provider';
|
} from '~/data-provider';
|
||||||
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
|
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
|
||||||
import MemoryCreateDialog from './MemoryCreateDialog';
|
import MemoryCreateDialog from './MemoryCreateDialog';
|
||||||
|
|
@ -36,18 +36,114 @@ import MemoryEditDialog from './MemoryEditDialog';
|
||||||
import AdminSettings from './AdminSettings';
|
import AdminSettings from './AdminSettings';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
|
const EditMemoryButton = ({ memory }: { memory: TUserMemory }) => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const triggerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MemoryEditDialog
|
||||||
|
open={open}
|
||||||
|
memory={memory}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
triggerRef={triggerRef as React.MutableRefObject<HTMLButtonElement | null>}
|
||||||
|
>
|
||||||
|
<OGDialogTrigger asChild>
|
||||||
|
<TooltipAnchor
|
||||||
|
description={localize('com_ui_edit_memory')}
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
aria-label={localize('com_ui_bookmarks_edit')}
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</OGDialogTrigger>
|
||||||
|
</MemoryEditDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeleteMemoryButton = ({ memory }: { memory: TUserMemory }) => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const { mutate: deleteMemory } = useDeleteMemoryMutation();
|
||||||
|
const [deletingKey, setDeletingKey] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
setDeletingKey(memory.key);
|
||||||
|
deleteMemory(memory.key, {
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_deleted'),
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
onError: () =>
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_error'),
|
||||||
|
status: 'error',
|
||||||
|
}),
|
||||||
|
onSettled: () => setDeletingKey(null),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OGDialog open={open} onOpenChange={setOpen}>
|
||||||
|
<OGDialogTrigger asChild>
|
||||||
|
<TooltipAnchor
|
||||||
|
description={localize('com_ui_delete_memory')}
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
aria-label={localize('com_ui_delete')}
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
{deletingKey === memory.key ? (
|
||||||
|
<Spinner className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<TrashIcon className="size-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</OGDialogTrigger>
|
||||||
|
<OGDialogTemplate
|
||||||
|
showCloseButton={false}
|
||||||
|
title={localize('com_ui_delete_memory')}
|
||||||
|
className="w-11/12 max-w-lg"
|
||||||
|
main={
|
||||||
|
<Label className="text-left text-sm font-medium">
|
||||||
|
{localize('com_ui_delete_confirm')} "{memory.key}"?
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
selection={{
|
||||||
|
selectHandler: confirmDelete,
|
||||||
|
selectClasses:
|
||||||
|
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white',
|
||||||
|
selectText: localize('com_ui_delete'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</OGDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageSize = 10;
|
||||||
export default function MemoryViewer() {
|
export default function MemoryViewer() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { user } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
const { data: userData } = useGetUserQuery();
|
const { data: userData } = useGetUserQuery();
|
||||||
const { data: memData, isLoading } = useMemoriesQuery();
|
const { data: memData, isLoading } = useMemoriesQuery();
|
||||||
const { mutate: deleteMemory } = useDeleteMemoryMutation();
|
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const pageSize = 10;
|
|
||||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||||
const [deletingKey, setDeletingKey] = useState<string | null>(null);
|
|
||||||
const [referenceSavedMemories, setReferenceSavedMemories] = useState(true);
|
const [referenceSavedMemories, setReferenceSavedMemories] = useState(true);
|
||||||
|
|
||||||
const updateMemoryPreferencesMutation = useUpdateMemoryPreferencesMutation({
|
const updateMemoryPreferencesMutation = useUpdateMemoryPreferencesMutation({
|
||||||
|
|
@ -119,108 +215,6 @@ export default function MemoryViewer() {
|
||||||
return 'stroke-green-500';
|
return 'stroke-green-500';
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditMemoryButton = ({ memory }: { memory: TUserMemory }) => {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const triggerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Only show edit button if user has UPDATE permission
|
|
||||||
if (!hasUpdateAccess) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MemoryEditDialog
|
|
||||||
open={open}
|
|
||||||
memory={memory}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
triggerRef={triggerRef as React.MutableRefObject<HTMLButtonElement | null>}
|
|
||||||
>
|
|
||||||
<OGDialogTrigger asChild>
|
|
||||||
<TooltipAnchor
|
|
||||||
description={localize('com_ui_edit_memory')}
|
|
||||||
render={
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
aria-label={localize('com_ui_bookmarks_edit')}
|
|
||||||
onClick={() => setOpen(!open)}
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
>
|
|
||||||
<EditIcon />
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</OGDialogTrigger>
|
|
||||||
</MemoryEditDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const DeleteMemoryButton = ({ memory }: { memory: TUserMemory }) => {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
if (!hasUpdateAccess) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmDelete = async () => {
|
|
||||||
setDeletingKey(memory.key);
|
|
||||||
deleteMemory(memory.key, {
|
|
||||||
onSuccess: () => {
|
|
||||||
showToast({
|
|
||||||
message: localize('com_ui_deleted'),
|
|
||||||
status: 'success',
|
|
||||||
});
|
|
||||||
setOpen(false);
|
|
||||||
},
|
|
||||||
onError: () =>
|
|
||||||
showToast({
|
|
||||||
message: localize('com_ui_error'),
|
|
||||||
status: 'error',
|
|
||||||
}),
|
|
||||||
onSettled: () => setDeletingKey(null),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OGDialog open={open} onOpenChange={setOpen}>
|
|
||||||
<OGDialogTrigger asChild>
|
|
||||||
<TooltipAnchor
|
|
||||||
description={localize('com_ui_delete_memory')}
|
|
||||||
render={
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
aria-label={localize('com_ui_delete')}
|
|
||||||
onClick={() => setOpen(!open)}
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
>
|
|
||||||
{deletingKey === memory.key ? (
|
|
||||||
<Spinner className="size-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<TrashIcon className="size-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</OGDialogTrigger>
|
|
||||||
<OGDialogTemplate
|
|
||||||
showCloseButton={false}
|
|
||||||
title={localize('com_ui_delete_memory')}
|
|
||||||
className="w-11/12 max-w-lg"
|
|
||||||
main={
|
|
||||||
<Label className="text-left text-sm font-medium">
|
|
||||||
{localize('com_ui_delete_confirm')} "{memory.key}"?
|
|
||||||
</Label>
|
|
||||||
}
|
|
||||||
selection={{
|
|
||||||
selectHandler: confirmDelete,
|
|
||||||
selectClasses:
|
|
||||||
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white',
|
|
||||||
selectText: localize('com_ui_delete'),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</OGDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center justify-center p-4">
|
<div className="flex h-full w-full items-center justify-center p-4">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue