Download client/src/components/Prompts/PromptForm.tsx from Zhitomir/chat: direct link, hf CLI and curl.
- Browser
- Download file 11.1 kB
-
https://huggingface.co/spaces/Zhitomir/chat/resolve/main/client/src/components/Prompts/PromptForm.tsx
- Command line
-
hf download hf://spaces/Zhitomir/chat/client/src/components/Prompts/PromptForm.tsx
-
curl -L -o PromptForm.tsx https://huggingface.co/spaces/Zhitomir/chat/resolve/main/client/src/components/Prompts/PromptForm.tsx
11.1 kB
| 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) { | |
| // TODO: show toast, cannot be empty. | |
| 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; | |