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'; import { useForm, FormProvider } from 'react-hook-form'; import { useParams, useOutletContext } from 'react-router-dom'; import { Button, Skeleton, useToastContext } from '@librechat/client'; import { Permissions, PermissionTypes, PermissionBits } from 'librechat-data-provider'; import type { TCreatePrompt, TPrompt, TPromptGroup } from 'librechat-data-provider'; import { useGetPrompts, useGetPromptGroup, useAddPromptToGroup, useUpdatePromptGroup, useMakePromptProduction, } from '~/data-provider'; import { useResourcePermissions, usePromptGroupsNav, useHasAccess, useLocalize } from '~/hooks'; import CategorySelector from './Groups/CategorySelector'; import NoPromptGroup from './Groups/NoPromptGroup'; import PromptVariables from './PromptVariables'; import { cn, findPromptGroup } from '~/utils'; import PromptVersions from './PromptVersions'; import { PromptsEditorMode } from '~/common'; import DeleteVersion from './DeleteVersion'; import PromptDetails from './PromptDetails'; import PromptEditor from './PromptEditor'; import SkeletonForm from './SkeletonForm'; import Description from './Description'; import SharePrompt from './SharePrompt'; 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; canEdit: boolean; setSelectionIndex: React.Dispatch>; } const RightPanel = React.memo( ({ group, prompts, selectedPrompt, selectedPromptId, isLoadingPrompts, canEdit, 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 (
updateGroupMutation.mutate({ id: groupId, payload: { name: groupName, category: value }, }) : undefined } />
{hasShareAccess && } {editorMode === PromptsEditorMode.ADVANCED && canEdit && ( )}
{editorMode === PromptsEditorMode.ADVANCED && (isLoadingPrompts ? Array.from({ length: 6 }).map((_, index: number) => (
)) : prompts.length > 0 && ( ))}
); }, ); RightPanel.displayName = 'RightPanel'; const PromptForm = () => { const params = useParams(); const localize = useLocalize(); const { showToast } = useToastContext(); const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd); const promptId = params.promptId || ''; const editorMode = useRecoilValue(store.promptsEditorMode); const [selectionIndex, setSelectionIndex] = useState(0); const prevIsEditingRef = useRef(false); const [isEditing, setIsEditing] = useState(false); const [initialLoad, setInitialLoad] = useState(true); const [showSidePanel, setShowSidePanel] = useState(false); const sidePanelWidth = '320px'; // Fetch group early so it is available for later hooks. const { data: group, isLoading: isLoadingGroup } = useGetPromptGroup(promptId); const { data: prompts = [], isLoading: isLoadingPrompts } = useGetPrompts( { groupId: promptId }, { enabled: !!promptId }, ); // Check permissions for the promptGroup const { hasPermission, isLoading: permissionsLoading } = useResourcePermissions( 'promptGroup', group?._id || '', ); const canEdit = hasPermission(PermissionBits.EDIT); const canView = hasPermission(PermissionBits.VIEW); const methods = useForm({ defaultValues: { prompt: '', promptName: group ? group.name : '', category: group ? group.category : '', }, }); const { handleSubmit, setValue, reset, watch } = methods; const promptText = watch('prompt'); const selectedPrompt = useMemo( () => (prompts.length > 0 ? prompts[selectionIndex] : undefined), [prompts, selectionIndex], ); const selectedPromptId = useMemo(() => selectedPrompt?._id, [selectedPrompt?._id]); const { groupsQuery } = useOutletContext>(); const updateGroupMutation = useUpdatePromptGroup({ onError: () => { showToast({ status: 'error', message: localize('com_ui_prompt_update_error'), }); }, }); const makeProductionMutation = useMakePromptProduction(); const addPromptToGroupMutation = useAddPromptToGroup({ onMutate: (variables) => { reset( { prompt: variables.prompt.prompt, category: group?.category || '', }, { keepDirtyValues: true }, ); }, onSuccess(data) { if (alwaysMakeProd && data.prompt._id != null && data.prompt._id && data.prompt.groupId) { makeProductionMutation.mutate({ id: data.prompt._id, groupId: data.prompt.groupId, productionPrompt: { prompt: data.prompt.prompt }, }); } reset({ prompt: data.prompt.prompt, promptName: group?.name || '', category: group?.category || '', }); }, }); const onSave = useCallback( (value: string) => { if (!canEdit) { return; } if (!value) { // TODO: show toast, cannot be empty. return; } if (!selectedPrompt) { return; } const groupId = selectedPrompt.groupId || group?._id; if (!groupId) { console.error('No groupId available'); return; } const tempPrompt: TCreatePrompt = { prompt: { type: selectedPrompt.type ?? 'text', groupId: groupId, prompt: value, }, }; if (value === selectedPrompt.prompt) { return; } // We're adding to an existing group, so use the addPromptToGroup mutation addPromptToGroupMutation.mutate({ ...tempPrompt, groupId }); }, [selectedPrompt, group, addPromptToGroupMutation, canEdit], ); const handleLoadingComplete = useCallback(() => { if (isLoadingGroup || isLoadingPrompts) { return; } setInitialLoad(false); }, [isLoadingGroup, isLoadingPrompts]); useEffect(() => { if (prevIsEditingRef.current && !isEditing && canEdit) { handleSubmit((data) => onSave(data.prompt))(); } prevIsEditingRef.current = isEditing; }, [isEditing, onSave, handleSubmit, canEdit]); useEffect(() => { handleLoadingComplete(); }, [params.promptId, editorMode, group?.productionId, prompts, handleLoadingComplete]); useEffect(() => { setValue('prompt', selectedPrompt ? selectedPrompt.prompt : '', { shouldDirty: false }); setValue('category', group ? group.category : '', { shouldDirty: false }); }, [selectedPrompt, group, setValue]); useEffect(() => { const handleResize = () => { if (window.matchMedia('(min-width: 1022px)').matches) { setShowSidePanel(false); } }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); const debouncedUpdateOneliner = useMemo( () => debounce((groupId: string, oneliner: string, mutate: any) => { mutate({ id: groupId, payload: { oneliner } }); }, 950), [], ); 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'); } 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) { return ; } // Show read-only view if user doesn't have edit permission if (!canEdit && !permissionsLoading && groupsQuery.data) { const fetchedPrompt = findPromptGroup( groupsQuery.data, (group) => group._id === params.promptId, ); if (!fetchedPrompt && !canView) { return ; } if (fetchedPrompt || group) { return ; } } if (!group || group._id == null) { return null; } const groupName = group.name; return (
onSave(data.prompt))}>
{isLoadingGroup ? ( ) : ( <> { if (!canEdit || !group._id) { return; } updateGroupMutation.mutate({ id: group._id, payload: { name: value }, }); }} />
{editorMode === PromptsEditorMode.SIMPLE && ( )}
)}
{isLoadingPrompts ? ( ) : (
canEdit && setIsEditing(value)} />
)}
{editorMode === PromptsEditorMode.ADVANCED && (
)}
); }; export default PromptForm;