| import { Rocket } from 'lucide-react'; |
| import debounce from 'lodash/debounce'; |
| import { useRecoilValue } from 'recoil'; |
| import { useForm, FormProvider } from 'react-hook-form'; |
| import { useEffect, useState, useMemo, useCallback, useRef } from 'react'; |
| import { useNavigate, useParams, useOutletContext } from 'react-router-dom'; |
| import { PermissionTypes, Permissions, SystemRoles } from 'librechat-data-provider'; |
| import type { TCreatePrompt } from 'librechat-data-provider'; |
| import { |
| useGetPrompts, |
| useCreatePrompt, |
| useDeletePrompt, |
| useGetPromptGroup, |
| useUpdatePromptGroup, |
| useMakePromptProduction, |
| } from '~/data-provider'; |
| import { useAuthContext, usePromptGroupsNav, useHasAccess } from '~/hooks'; |
| import CategorySelector from './Groups/CategorySelector'; |
| import AlwaysMakeProd from './Groups/AlwaysMakeProd'; |
| import NoPromptGroup from './Groups/NoPromptGroup'; |
| import { Button, Skeleton } from '~/components/ui'; |
| import PromptVariables from './PromptVariables'; |
| import PromptVersions from './PromptVersions'; |
| import DeleteConfirm from './DeleteVersion'; |
| import PromptDetails from './PromptDetails'; |
| import { findPromptGroup } from '~/utils'; |
| import PromptEditor from './PromptEditor'; |
| import SkeletonForm from './SkeletonForm'; |
| import Description from './Description'; |
| import SharePrompt from './SharePrompt'; |
| import PromptName from './PromptName'; |
| import store from '~/store'; |
|
|
| const { PromptsEditorMode, promptsEditorMode } = store; |
|
|
| const PromptForm = () => { |
| const params = useParams(); |
| const navigate = useNavigate(); |
| const { user } = useAuthContext(); |
| const editorMode = useRecoilValue(promptsEditorMode); |
| const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd); |
| const { data: group, isLoading: isLoadingGroup } = useGetPromptGroup(params.promptId || ''); |
| const { data: prompts = [], isLoading: isLoadingPrompts } = useGetPrompts( |
| { groupId: params.promptId ?? '' }, |
| { enabled: !!params.promptId }, |
| ); |
|
|
| const prevIsEditingRef = useRef(false); |
| const [isEditing, setIsEditing] = useState(false); |
| const [initialLoad, setInitialLoad] = useState(true); |
| const [selectionIndex, setSelectionIndex] = useState<number>(0); |
| const isOwner = useMemo(() => user?.id === group?.author, [user, group]); |
| const selectedPrompt = useMemo(() => prompts[selectionIndex], [prompts, selectionIndex]); |
|
|
| const hasShareAccess = useHasAccess({ |
| permissionType: PermissionTypes.PROMPTS, |
| permission: Permissions.SHARED_GLOBAL, |
| }); |
|
|
| const methods = useForm({ |
| defaultValues: { |
| prompt: '', |
| promptName: group?.name || '', |
| category: group?.category || '', |
| }, |
| }); |
|
|
| const { handleSubmit, setValue, reset, watch } = methods; |
| const promptText = watch('prompt'); |
|
|
| const createPromptMutation = useCreatePrompt({ |
| onMutate: (variables) => { |
| reset( |
| { |
| prompt: variables.prompt.prompt, |
| category: variables.group?.category || '', |
| }, |
| { keepDirtyValues: true }, |
| ); |
| }, |
| onSuccess(data) { |
| if (alwaysMakeProd && data.prompt._id && data.prompt.groupId) { |
| makeProductionMutation.mutate( |
| { |
| id: data.prompt._id, |
| groupId: data.prompt.groupId, |
| productionPrompt: { prompt: data.prompt.prompt }, |
| }, |
| { |
| onSuccess: () => setSelectionIndex(0), |
| }, |
| ); |
| } |
|
|
| reset({ |
| prompt: data.prompt.prompt, |
| promptName: data.group?.name || '', |
| category: data.group?.category || '', |
| }); |
|
|
| setSelectionIndex(0); |
| }, |
| }); |
| const updateGroupMutation = useUpdatePromptGroup(); |
| const makeProductionMutation = useMakePromptProduction(); |
| const deletePromptMutation = useDeletePrompt({ |
| onSuccess: (response) => { |
| if (response.promptGroup) { |
| navigate('/d/prompts'); |
| } else { |
| setSelectionIndex(0); |
| } |
| }, |
| }); |
|
|
| const onSave = useCallback( |
| (value: string) => { |
| if (!value) { |
| |
| return; |
| } |
| const tempPrompt: TCreatePrompt = { |
| prompt: { |
| type: selectedPrompt?.type ?? 'text', |
| groupId: selectedPrompt?.groupId ?? '', |
| prompt: value, |
| }, |
| }; |
|
|
| if (value === selectedPrompt?.prompt) { |
| return; |
| } |
|
|
| createPromptMutation.mutate(tempPrompt); |
| }, |
| [selectedPrompt, createPromptMutation], |
| ); |
|
|
| const handleLoadingComplete = useCallback(() => { |
| if (isLoadingGroup || isLoadingPrompts) { |
| return; |
| } |
| setInitialLoad(false); |
| }, [isLoadingGroup, isLoadingPrompts]); |
|
|
| useEffect(() => { |
| if (prevIsEditingRef.current && !isEditing) { |
| handleSubmit((data) => onSave(data.prompt))(); |
| } |
| prevIsEditingRef.current = isEditing; |
| }, [isEditing, onSave, handleSubmit]); |
|
|
| useEffect(() => { |
| if (editorMode === PromptsEditorMode.SIMPLE) { |
| const productionIndex = prompts.findIndex((prompt) => prompt._id === group?.productionId); |
| setSelectionIndex(productionIndex !== -1 ? productionIndex : 0); |
| } |
|
|
| handleLoadingComplete(); |
| }, [params.promptId, editorMode, group?.productionId, prompts, handleLoadingComplete]); |
|
|
| useEffect(() => { |
| setValue('prompt', selectedPrompt?.prompt || '', { shouldDirty: false }); |
| setValue('category', group?.category || '', { shouldDirty: false }); |
| }, [selectedPrompt, group?.category, setValue]); |
|
|
| const debouncedUpdateOneliner = useCallback( |
| debounce((oneliner: string) => { |
| if (!group) { |
| return console.warn('Group not found'); |
| } |
|
|
| updateGroupMutation.mutate({ id: group._id || '', payload: { oneliner } }); |
| }, 950), |
| [updateGroupMutation, group], |
| ); |
|
|
| const { groupsQuery } = useOutletContext<ReturnType<typeof usePromptGroupsNav>>(); |
|
|
| if (initialLoad) { |
| return <SkeletonForm />; |
| } |
|
|
| if (!isOwner && groupsQuery.data && user?.role !== SystemRoles.ADMIN) { |
| const fetchedPrompt = findPromptGroup( |
| groupsQuery.data, |
| (group) => group._id === params.promptId, |
| ); |
| if (!fetchedPrompt) { |
| return <NoPromptGroup />; |
| } |
|
|
| return <PromptDetails group={fetchedPrompt} />; |
| } |
|
|
| if (!group) { |
| return null; |
| } |
|
|
| return ( |
| <FormProvider {...methods}> |
| <form onSubmit={handleSubmit((data) => onSave(data.prompt))}> |
| <div> |
| <div className="flex flex-col items-center justify-between px-4 dark:text-gray-200 sm:flex-row"> |
| {isLoadingGroup ? ( |
| <Skeleton className="mb-1 flex h-10 w-32 flex-row items-center font-bold sm:text-xl md:mb-0 md:h-12 md:text-2xl" /> |
| ) : ( |
| <PromptName |
| name={group?.name} |
| onSave={(value) => { |
| if (!group) { |
| return console.warn('Group not found'); |
| } |
| updateGroupMutation.mutate({ id: group._id || '', payload: { name: value } }); |
| }} |
| /> |
| )} |
| <div className="flex h-10 flex-row gap-x-2"> |
| <CategorySelector |
| className="w-48 md:w-56" |
| currentCategory={group?.category} |
| onValueChange={(value) => |
| updateGroupMutation.mutate({ |
| id: group?._id || '', |
| payload: { name: group?.name || '', category: value }, |
| }) |
| } |
| /> |
| {hasShareAccess && <SharePrompt group={group} disabled={isLoadingGroup} />} |
| {editorMode === PromptsEditorMode.ADVANCED && ( |
| <Button |
| size={'sm'} |
| className="h-10 border border-transparent bg-green-500 transition-all hover:bg-green-600 dark:bg-green-500 dark:hover:bg-green-600" |
| variant={'default'} |
| onClick={() => { |
| const { _id: promptVersionId = '', prompt } = selectedPrompt; |
| makeProductionMutation.mutate( |
| { |
| id: promptVersionId || '', |
| groupId: group?._id || '', |
| productionPrompt: { prompt }, |
| }, |
| { |
| onSuccess: (_data, variables) => { |
| const productionIndex = prompts.findIndex( |
| (prompt) => variables.id === prompt._id, |
| ); |
| setSelectionIndex(productionIndex); |
| }, |
| }, |
| ); |
| }} |
| disabled={ |
| isLoadingGroup || |
| selectedPrompt?._id === group?.productionId || |
| makeProductionMutation.isLoading |
| } |
| > |
| <Rocket className="cursor-pointer text-white" /> |
| </Button> |
| )} |
| <DeleteConfirm |
| name={group.name} |
| disabled={isLoadingGroup} |
| selectHandler={() => { |
| deletePromptMutation.mutate({ |
| _id: selectedPrompt?._id || '', |
| groupId: group?._id || '', |
| }); |
| }} |
| /> |
| </div> |
| </div> |
| {editorMode === PromptsEditorMode.ADVANCED && ( |
| <div className="mt-4 flex items-center justify-center text-text-primary sm:hidden"> |
| <AlwaysMakeProd /> |
| </div> |
| )} |
| <div className="flex h-full w-full flex-col md:flex-row"> |
| {/* Left Section */} |
| <div className="flex-1 overflow-y-auto border-gray-300 p-4 dark:border-gray-600 md:max-h-[calc(100vh-150px)] md:border-r"> |
| {isLoadingPrompts ? ( |
| <Skeleton className="h-96" /> |
| ) : ( |
| <> |
| <PromptEditor name="prompt" isEditing={isEditing} setIsEditing={setIsEditing} /> |
| <PromptVariables promptText={promptText} /> |
| <Description |
| initialValue={group?.oneliner ?? ''} |
| onValueChange={debouncedUpdateOneliner} |
| /> |
| </> |
| )} |
| </div> |
| {/* Right Section */} |
| {editorMode === PromptsEditorMode.ADVANCED && ( |
| <div className="flex-1 overflow-y-auto p-4 md:max-h-[calc(100vh-150px)] md:w-1/4 md:max-w-[35%] lg:max-w-[30%] xl:max-w-[25%]"> |
| {isLoadingPrompts ? ( |
| <Skeleton className="h-96 w-full" /> |
| ) : ( |
| !!prompts.length && ( |
| <PromptVersions |
| group={group} |
| prompts={prompts} |
| selectionIndex={selectionIndex} |
| setSelectionIndex={setSelectionIndex} |
| /> |
| ) |
| )} |
| </div> |
| )} |
| </div> |
| </div> |
| </form> |
| </FormProvider> |
| ); |
| }; |
|
|
| export default PromptForm; |
|
|