mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30: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 { useDeletePrompt } from '~/data-provider';
|
||||
import { Button, OGDialog, OGDialogTrigger, Label, OGDialogTemplate } from '@librechat/client';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const DeleteVersion = ({
|
||||
const DeleteConfirmDialog = ({
|
||||
name,
|
||||
disabled,
|
||||
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 { Dropdown } from '@librechat/client';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
import { DropdownPopup } from '@librechat/client';
|
||||
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 { useCategories } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface CategorySelectorProps {
|
||||
currentCategory?: string;
|
||||
|
|
@ -20,10 +23,11 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
|||
const { t } = useTranslation();
|
||||
const formContext = useFormContext();
|
||||
const { categories, emptyCategory } = useCategories();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const control = formContext.control;
|
||||
const watch = formContext.watch;
|
||||
const setValue = formContext.setValue;
|
||||
const control = formContext?.control;
|
||||
const watch = formContext?.watch;
|
||||
const setValue = formContext?.setValue;
|
||||
|
||||
const watchedCategory = watch ? watch('category') : currentCategory;
|
||||
|
||||
|
|
@ -46,53 +50,71 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
|||
return categoryOption;
|
||||
}, [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 ? (
|
||||
<Controller
|
||||
name="category"
|
||||
control={control}
|
||||
render={() => (
|
||||
<Dropdown
|
||||
value={displayCategory.value ?? ''}
|
||||
label={displayCategory.value ? undefined : t('com_ui_category')}
|
||||
onChange={(value: string) => {
|
||||
setValue('category', value, { shouldDirty: false });
|
||||
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
|
||||
onValueChange?.(value);
|
||||
}}
|
||||
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>
|
||||
)}
|
||||
<DropdownPopup
|
||||
trigger={trigger}
|
||||
items={menuItems}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
menuId="category-selector-menu"
|
||||
className="mt-2"
|
||||
portal={true}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Dropdown
|
||||
value={currentCategory ?? ''}
|
||||
onChange={(value: string) => {
|
||||
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
|
||||
onValueChange?.(value);
|
||||
}}
|
||||
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>
|
||||
)}
|
||||
<DropdownPopup
|
||||
trigger={trigger}
|
||||
items={menuItems}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
menuId="category-selector-menu"
|
||||
className="mt-2"
|
||||
portal={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
||||
import React from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
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 { Button, Skeleton, useToastContext } from '@librechat/client';
|
||||
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 {
|
||||
useCreatePrompt,
|
||||
useGetPrompts,
|
||||
useCreatePrompt,
|
||||
useGetPromptGroup,
|
||||
useUpdatePromptGroup,
|
||||
useMakePromptProduction,
|
||||
useDeletePrompt,
|
||||
} from '~/data-provider';
|
||||
import { useAuthContext, usePromptGroupsNav, useHasAccess, useLocalize } from '~/hooks';
|
||||
import CategorySelector from './Groups/CategorySelector';
|
||||
|
|
@ -22,7 +22,7 @@ import PromptVariables from './PromptVariables';
|
|||
import { cn, findPromptGroup } from '~/utils';
|
||||
import PromptVersions from './PromptVersions';
|
||||
import { PromptsEditorMode } from '~/common';
|
||||
import DeleteConfirm from './DeleteVersion';
|
||||
import DeleteVersion from './DeleteVersion';
|
||||
import PromptDetails from './PromptDetails';
|
||||
import PromptEditor from './PromptEditor';
|
||||
import SkeletonForm from './SkeletonForm';
|
||||
|
|
@ -32,16 +32,136 @@ import PromptName from './PromptName';
|
|||
import Command from './Command';
|
||||
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 params = useParams();
|
||||
const localize = useLocalize();
|
||||
const { user } = useAuthContext();
|
||||
const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd);
|
||||
const { showToast } = useToastContext();
|
||||
const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd);
|
||||
const promptId = params.promptId || '';
|
||||
|
||||
const [selectionIndex, setSelectionIndex] = useState<number>(0);
|
||||
const editorMode = useRecoilValue(store.promptsEditorMode);
|
||||
const [selectionIndex, setSelectionIndex] = useState<number>(0);
|
||||
|
||||
const prevIsEditingRef = useRef(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [initialLoad, setInitialLoad] = useState(true);
|
||||
|
|
@ -72,11 +192,9 @@ const PromptForm = () => {
|
|||
[prompts, selectionIndex],
|
||||
);
|
||||
|
||||
const selectedPromptId = useMemo(() => selectedPrompt?._id, [selectedPrompt?._id]);
|
||||
|
||||
const { groupsQuery } = useOutletContext<ReturnType<typeof usePromptGroupsNav>>();
|
||||
const hasShareAccess = useHasAccess({
|
||||
permissionType: PermissionTypes.PROMPTS,
|
||||
permission: Permissions.SHARED_GLOBAL,
|
||||
});
|
||||
|
||||
const updateGroupMutation = useUpdatePromptGroup({
|
||||
onError: () => {
|
||||
|
|
@ -88,7 +206,6 @@ const PromptForm = () => {
|
|||
});
|
||||
|
||||
const makeProductionMutation = useMakePromptProduction();
|
||||
const deletePromptMutation = useDeletePrompt();
|
||||
|
||||
const createPromptMutation = useCreatePrompt({
|
||||
onMutate: (variables) => {
|
||||
|
|
@ -177,24 +294,40 @@ const PromptForm = () => {
|
|||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const debouncedUpdateOneliner = useCallback(
|
||||
debounce((oneliner: string) => {
|
||||
if (!group || !group._id) {
|
||||
return console.warn('Group not found');
|
||||
}
|
||||
updateGroupMutation.mutate({ id: group._id, payload: { oneliner } });
|
||||
}, 950),
|
||||
[updateGroupMutation, group],
|
||||
const debouncedUpdateOneliner = useMemo(
|
||||
() =>
|
||||
debounce((groupId: string, oneliner: string, mutate: any) => {
|
||||
mutate({ id: groupId, payload: { oneliner } });
|
||||
}, 950),
|
||||
[],
|
||||
);
|
||||
|
||||
const debouncedUpdateCommand = useCallback(
|
||||
debounce((command: string) => {
|
||||
const debouncedUpdateCommand = useMemo(
|
||||
() =>
|
||||
debounce((groupId: string, command: string, mutate: any) => {
|
||||
mutate({ id: groupId, payload: { command } });
|
||||
}, 950),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleUpdateOneliner = useCallback(
|
||||
(oneliner: string) => {
|
||||
if (!group || !group._id) {
|
||||
return console.warn('Group not found');
|
||||
}
|
||||
updateGroupMutation.mutate({ id: group._id, payload: { command } });
|
||||
}, 950),
|
||||
[updateGroupMutation, group],
|
||||
debouncedUpdateOneliner(group._id, oneliner, updateGroupMutation.mutate);
|
||||
},
|
||||
[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) {
|
||||
|
|
@ -217,89 +350,7 @@ const PromptForm = () => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const groupId = group._id;
|
||||
|
||||
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 (
|
||||
<FormProvider {...methods}>
|
||||
|
|
@ -339,7 +390,17 @@ const PromptForm = () => {
|
|||
<Menu className="size-5" />
|
||||
</Button>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -352,11 +413,11 @@ const PromptForm = () => {
|
|||
<PromptVariables promptText={promptText} />
|
||||
<Description
|
||||
initialValue={group.oneliner ?? ''}
|
||||
onValueChange={debouncedUpdateOneliner}
|
||||
onValueChange={handleUpdateOneliner}
|
||||
/>
|
||||
<Command
|
||||
initialValue={group.command ?? ''}
|
||||
onValueChange={debouncedUpdateCommand}
|
||||
onValueChange={handleUpdateCommand}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -364,7 +425,15 @@ const PromptForm = () => {
|
|||
|
||||
{editorMode === PromptsEditorMode.ADVANCED && (
|
||||
<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>
|
||||
|
|
@ -395,7 +464,15 @@ const PromptForm = () => {
|
|||
>
|
||||
<div className="h-full">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -4,18 +4,18 @@ import { Plus } from 'lucide-react';
|
|||
import { matchSorter } from 'match-sorter';
|
||||
import { SystemRoles, PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import {
|
||||
Spinner,
|
||||
EditIcon,
|
||||
TrashIcon,
|
||||
Table,
|
||||
Input,
|
||||
Label,
|
||||
Button,
|
||||
Switch,
|
||||
Spinner,
|
||||
TableRow,
|
||||
OGDialog,
|
||||
EditIcon,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TrashIcon,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TooltipAnchor,
|
||||
|
|
@ -25,10 +25,10 @@ import {
|
|||
} from '@librechat/client';
|
||||
import type { TUserMemory } from 'librechat-data-provider';
|
||||
import {
|
||||
useGetUserQuery,
|
||||
useMemoriesQuery,
|
||||
useDeleteMemoryMutation,
|
||||
useUpdateMemoryPreferencesMutation,
|
||||
useDeleteMemoryMutation,
|
||||
useMemoriesQuery,
|
||||
useGetUserQuery,
|
||||
} from '~/data-provider';
|
||||
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
|
||||
import MemoryCreateDialog from './MemoryCreateDialog';
|
||||
|
|
@ -36,18 +36,114 @@ import MemoryEditDialog from './MemoryEditDialog';
|
|||
import AdminSettings from './AdminSettings';
|
||||
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() {
|
||||
const localize = useLocalize();
|
||||
const { user } = useAuthContext();
|
||||
const { data: userData } = useGetUserQuery();
|
||||
const { data: memData, isLoading } = useMemoriesQuery();
|
||||
const { mutate: deleteMemory } = useDeleteMemoryMutation();
|
||||
const { showToast } = useToastContext();
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const pageSize = 10;
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const [deletingKey, setDeletingKey] = useState<string | null>(null);
|
||||
const [referenceSavedMemories, setReferenceSavedMemories] = useState(true);
|
||||
|
||||
const updateMemoryPreferencesMutation = useUpdateMemoryPreferencesMutation({
|
||||
|
|
@ -119,108 +215,6 @@ export default function MemoryViewer() {
|
|||
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) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center p-4">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue