🔀 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:
Danny Avila 2025-07-28 15:14:37 -04:00 committed by GitHub
parent 8e6eef04ab
commit a4ca4b7d9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 401 additions and 268 deletions

View file

@ -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>