mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +01:00
🗨️ feat: Prompt Slash Commands (#3219)
* chore: Update prompt description placeholder text * fix: promptsPathPattern to not include new * feat: command input and styling change for prompt views * fix: intended validation * feat: prompts slash command * chore: localizations and fix add command during creation * refactor(PromptsCommand): better label * feat: update `allPrompGroups` cache on all promptGroups mutations * refactor: ensure assistants builder is first within sidepanel * refactor: allow defining emailVerified via create-user script
This commit is contained in:
parent
b8f2bee3fc
commit
83619de158
33 changed files with 764 additions and 80 deletions
|
|
@ -23,6 +23,7 @@ import { TextareaAutosize } from '~/components/ui';
|
|||
import { useGetFileConfig } from '~/data-provider';
|
||||
import { cn, removeFocusRings } from '~/utils';
|
||||
import TextareaHeader from './TextareaHeader';
|
||||
import PromptsCommand from './PromptsCommand';
|
||||
import AttachFile from './Files/AttachFile';
|
||||
import AudioRecorder from './AudioRecorder';
|
||||
import { mainTextareaId } from '~/common';
|
||||
|
|
@ -48,7 +49,12 @@ const ChatForm = ({ index = 0 }) => {
|
|||
);
|
||||
|
||||
const { requiresKey } = useRequiresKey();
|
||||
const handleKeyUp = useHandleKeyUp({ textAreaRef, setShowPlusPopover, setShowMentionPopover });
|
||||
const handleKeyUp = useHandleKeyUp({
|
||||
index,
|
||||
textAreaRef,
|
||||
setShowPlusPopover,
|
||||
setShowMentionPopover,
|
||||
});
|
||||
const { handlePaste, handleKeyDown, handleCompositionStart, handleCompositionEnd } = useTextarea({
|
||||
textAreaRef,
|
||||
submitButtonRef,
|
||||
|
|
@ -83,7 +89,7 @@ const ChatForm = ({ index = 0 }) => {
|
|||
});
|
||||
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
const { submitMessage } = useSubmitMessage({ clearDraft });
|
||||
const { submitMessage, submitPrompt } = useSubmitMessage({ clearDraft });
|
||||
|
||||
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
|
||||
const endpoint = endpointType ?? _endpoint;
|
||||
|
|
@ -136,6 +142,7 @@ const ChatForm = ({ index = 0 }) => {
|
|||
textAreaRef={textAreaRef}
|
||||
/>
|
||||
)}
|
||||
<PromptsCommand index={index} textAreaRef={textAreaRef} submitPrompt={submitPrompt} />
|
||||
<div className="bg-token-main-surface-primary relative flex w-full flex-grow flex-col overflow-hidden rounded-2xl border dark:border-gray-600 dark:text-white [&:has(textarea:focus)]:border-gray-300 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)] dark:[&:has(textarea:focus)]:border-gray-500">
|
||||
<TextareaHeader addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
|
||||
<FileRow
|
||||
|
|
|
|||
231
client/src/components/Chat/Input/PromptsCommand.tsx
Normal file
231
client/src/components/Chat/Input/PromptsCommand.tsx
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { useState, useRef, useEffect, useMemo, memo, useCallback } from 'react';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import type { PromptOption } from '~/common';
|
||||
import { removeCharIfLast, mapPromptGroups, detectVariables } from '~/utils';
|
||||
import VariableDialog from '~/components/Prompts/Groups/VariableDialog';
|
||||
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
||||
import { useGetAllPromptGroups } from '~/data-provider';
|
||||
import { useLocalize, useCombobox } from '~/hooks';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import MentionItem from './MentionItem';
|
||||
import store from '~/store';
|
||||
|
||||
const commandChar = '/';
|
||||
|
||||
const PopoverContainer = memo(
|
||||
({
|
||||
index,
|
||||
children,
|
||||
isVariableDialogOpen,
|
||||
variableGroup,
|
||||
setVariableDialogOpen,
|
||||
}: {
|
||||
index: number;
|
||||
children: React.ReactNode;
|
||||
isVariableDialogOpen: boolean;
|
||||
variableGroup: TPromptGroup | null;
|
||||
setVariableDialogOpen: (isOpen: boolean) => void;
|
||||
}) => {
|
||||
const showPromptsPopover = useRecoilValue(store.showPromptsPopoverFamily(index));
|
||||
return (
|
||||
<>
|
||||
{showPromptsPopover ? children : null}
|
||||
<VariableDialog
|
||||
open={isVariableDialogOpen}
|
||||
onClose={() => setVariableDialogOpen(false)}
|
||||
group={variableGroup}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function PromptsCommand({
|
||||
index,
|
||||
textAreaRef,
|
||||
submitPrompt,
|
||||
}: {
|
||||
index: number;
|
||||
textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>;
|
||||
submitPrompt: (textPrompt: string) => void;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
|
||||
const { data, isLoading } = useGetAllPromptGroups(undefined, {
|
||||
select: (data) => {
|
||||
const mappedArray = data.map((group) => ({
|
||||
id: group._id,
|
||||
value: group.command ?? group.name,
|
||||
label: `${group.command ? `/${group.command} - ` : ''}${group.name}: ${
|
||||
group.oneliner?.length ? group.oneliner : group.productionPrompt?.prompt ?? ''
|
||||
}`,
|
||||
icon: <CategoryIcon category={group.category ?? ''} />,
|
||||
}));
|
||||
|
||||
const promptsMap = mapPromptGroups(data);
|
||||
|
||||
return {
|
||||
promptsMap,
|
||||
promptGroups: mappedArray,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [isVariableDialogOpen, setVariableDialogOpen] = useState(false);
|
||||
const [variableGroup, setVariableGroup] = useState<TPromptGroup | null>(null);
|
||||
const setShowPromptsPopover = useSetRecoilState(store.showPromptsPopoverFamily(index));
|
||||
|
||||
const prompts = useMemo(() => data?.promptGroups ?? [], [data]);
|
||||
const promptsMap = useMemo(() => data?.promptsMap ?? {}, [data]);
|
||||
|
||||
const { open, setOpen, searchValue, setSearchValue, matches } = useCombobox({
|
||||
value: '',
|
||||
options: prompts,
|
||||
});
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(mention?: PromptOption, e?: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (!mention) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSearchValue('');
|
||||
setOpen(false);
|
||||
setShowPromptsPopover(false);
|
||||
|
||||
if (textAreaRef.current) {
|
||||
removeCharIfLast(textAreaRef.current, commandChar);
|
||||
}
|
||||
|
||||
const isValidPrompt = mention && promptsMap && promptsMap[mention.id];
|
||||
|
||||
if (!isValidPrompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const group = promptsMap[mention.id];
|
||||
const hasVariables = detectVariables(group?.productionPrompt?.prompt ?? '');
|
||||
if (group && hasVariables) {
|
||||
if (e && e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
}
|
||||
setVariableGroup(group);
|
||||
setVariableDialogOpen(true);
|
||||
return;
|
||||
} else if (group) {
|
||||
submitPrompt(group.productionPrompt?.prompt ?? '');
|
||||
}
|
||||
},
|
||||
[setSearchValue, setOpen, setShowPromptsPopover, textAreaRef, promptsMap, submitPrompt],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setActiveIndex(0);
|
||||
} else {
|
||||
setVariableGroup(null);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const currentActiveItem = document.getElementById(`prompt-item-${activeIndex}`);
|
||||
currentActiveItem?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
||||
}, [activeIndex]);
|
||||
|
||||
return (
|
||||
<PopoverContainer
|
||||
index={index}
|
||||
isVariableDialogOpen={isVariableDialogOpen}
|
||||
variableGroup={variableGroup}
|
||||
setVariableDialogOpen={setVariableDialogOpen}
|
||||
>
|
||||
<div className="absolute bottom-16 z-10 w-full space-y-2">
|
||||
<div className="popover border-token-border-light rounded-2xl border bg-surface-tertiary-alt p-2 shadow-lg">
|
||||
<input
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
placeholder={localize('com_ui_command_usage_placeholder')}
|
||||
className="mb-1 w-full border-0 bg-surface-tertiary-alt p-2 text-sm focus:outline-none dark:text-gray-200"
|
||||
autoComplete="off"
|
||||
value={searchValue}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
setOpen(false);
|
||||
setShowPromptsPopover(false);
|
||||
textAreaRef.current?.focus();
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
setActiveIndex((prevIndex) => (prevIndex + 1) % matches.length);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
setActiveIndex((prevIndex) => (prevIndex - 1 + matches.length) % matches.length);
|
||||
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
}
|
||||
handleSelect(matches[activeIndex] as PromptOption | undefined, e);
|
||||
} else if (e.key === 'Backspace' && searchValue === '') {
|
||||
setOpen(false);
|
||||
setShowPromptsPopover(false);
|
||||
textAreaRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
onFocus={() => setOpen(true)}
|
||||
onBlur={() => {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setOpen(false);
|
||||
setShowPromptsPopover(false);
|
||||
}, 150);
|
||||
}}
|
||||
/>
|
||||
<div className="max-h-40 overflow-y-auto">
|
||||
{(() => {
|
||||
if (isLoading && open) {
|
||||
return (
|
||||
<div className="flex h-32 items-center justify-center text-text-primary">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoading && open) {
|
||||
return (matches as PromptOption[]).map((mention, index) => (
|
||||
<MentionItem
|
||||
index={index}
|
||||
key={`${mention.value}-${index}`}
|
||||
onClick={() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = null;
|
||||
handleSelect(mention);
|
||||
}}
|
||||
name={mention.label ?? ''}
|
||||
icon={mention.icon}
|
||||
description={mention.description}
|
||||
isActive={index === activeIndex}
|
||||
/>
|
||||
));
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(PromptsCommand);
|
||||
66
client/src/components/Prompts/Command.tsx
Normal file
66
client/src/components/Prompts/Command.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { SquareSlash } from 'lucide-react';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const Command = ({
|
||||
initialValue,
|
||||
onValueChange,
|
||||
disabled,
|
||||
tabIndex,
|
||||
}: {
|
||||
initialValue?: string;
|
||||
onValueChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
const [command, setCommand] = useState(initialValue || '');
|
||||
const [charCount, setCharCount] = useState(initialValue?.length || 0);
|
||||
|
||||
useEffect(() => {
|
||||
setCommand(initialValue || '');
|
||||
setCharCount(initialValue?.length || 0);
|
||||
}, [initialValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setCharCount(command.length);
|
||||
}, [command]);
|
||||
|
||||
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
let newValue = e.target.value.toLowerCase();
|
||||
|
||||
newValue = newValue.replace(/\s/g, '-').replace(/[^a-z0-9-]/g, '');
|
||||
|
||||
if (newValue.length <= Constants.COMMANDS_MAX_LENGTH) {
|
||||
setCommand(newValue);
|
||||
onValueChange?.(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
if (disabled && !command) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border-medium">
|
||||
<h3 className="flex h-10 items-center gap-2 pl-4 text-sm text-text-secondary">
|
||||
<SquareSlash className="icon-sm" />
|
||||
<input
|
||||
type="text"
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
placeholder={localize('com_ui_command_placeholder')}
|
||||
value={command}
|
||||
onChange={handleInputChange}
|
||||
className="w-full rounded-lg border-none bg-surface-tertiary p-1 text-text-primary placeholder:text-text-secondary-alt focus:bg-surface-tertiary focus:outline-none focus:ring-0 md:w-96"
|
||||
/>
|
||||
{!disabled && (
|
||||
<span className="mr-1 w-10 text-xs text-text-tertiary md:text-sm">{`${charCount}/${Constants.COMMANDS_MAX_LENGTH}`}</span>
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Command;
|
||||
|
|
@ -6,8 +6,9 @@ import CategorySelector from '~/components/Prompts/Groups/CategorySelector';
|
|||
import PromptVariables from '~/components/Prompts/PromptVariables';
|
||||
import { Button, TextareaAutosize, Input } from '~/components/ui';
|
||||
import Description from '~/components/Prompts/Description';
|
||||
import { useCreatePrompt } from '~/data-provider';
|
||||
import { useLocalize, useHasAccess } from '~/hooks';
|
||||
import Command from '~/components/Prompts/Command';
|
||||
import { useCreatePrompt } from '~/data-provider';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
type CreateFormValues = {
|
||||
|
|
@ -16,6 +17,7 @@ type CreateFormValues = {
|
|||
type: 'text' | 'chat';
|
||||
category: string;
|
||||
oneliner?: string;
|
||||
command?: string;
|
||||
};
|
||||
|
||||
const defaultPrompt: CreateFormValues = {
|
||||
|
|
@ -24,6 +26,7 @@ const defaultPrompt: CreateFormValues = {
|
|||
type: 'text',
|
||||
category: '',
|
||||
oneliner: undefined,
|
||||
command: undefined,
|
||||
};
|
||||
|
||||
const CreatePromptForm = ({
|
||||
|
|
@ -73,14 +76,17 @@ const CreatePromptForm = ({
|
|||
const promptText = watch('prompt');
|
||||
|
||||
const onSubmit = (data: CreateFormValues) => {
|
||||
const { name, category, oneliner, ...rest } = data;
|
||||
const { name, category, oneliner, command, ...rest } = data;
|
||||
const groupData = { name, category } as Pick<
|
||||
CreateFormValues,
|
||||
'name' | 'category' | 'oneliner'
|
||||
'name' | 'category' | 'oneliner' | 'command'
|
||||
>;
|
||||
if ((oneliner?.length || 0) > 0) {
|
||||
groupData.oneliner = oneliner;
|
||||
}
|
||||
if ((command?.length || 0) > 0) {
|
||||
groupData.command = command;
|
||||
}
|
||||
createPromptMutation.mutate({
|
||||
prompt: rest,
|
||||
group: groupData,
|
||||
|
|
@ -121,15 +127,15 @@ const CreatePromptForm = ({
|
|||
</div>
|
||||
)}
|
||||
/>
|
||||
<CategorySelector tabIndex={4} />
|
||||
<CategorySelector tabIndex={5} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full md:mt-[1.075rem]">
|
||||
<div className="flex w-full flex-col gap-4 md:mt-[1.075rem]">
|
||||
<div>
|
||||
<h2 className="flex items-center justify-between rounded-t-lg border border-gray-300 py-2 pl-4 pr-1 text-base font-semibold dark:border-gray-600 dark:text-gray-200">
|
||||
{localize('com_ui_prompt_text')}*
|
||||
</h2>
|
||||
<div className="mb-4 min-h-32 rounded-b-lg border border-gray-300 p-4 transition-all duration-150 dark:border-gray-600">
|
||||
<div className="min-h-32 rounded-b-lg border border-gray-300 p-4 transition-all duration-150 dark:border-gray-600">
|
||||
<Controller
|
||||
name="prompt"
|
||||
control={control}
|
||||
|
|
@ -159,9 +165,10 @@ const CreatePromptForm = ({
|
|||
onValueChange={(value) => methods.setValue('oneliner', value)}
|
||||
tabIndex={3}
|
||||
/>
|
||||
<Command onValueChange={(value) => methods.setValue('command', value)} tabIndex={4} />
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button
|
||||
tabIndex={5}
|
||||
tabIndex={6}
|
||||
type="submit"
|
||||
variant="default"
|
||||
disabled={!isDirty || isSubmitting || !isValid}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import VariableForm from './VariableForm';
|
|||
|
||||
interface VariableDialogProps extends Omit<DialogPrimitive.DialogProps, 'onOpenChange'> {
|
||||
onClose: () => void;
|
||||
group: TPromptGroup;
|
||||
group: TPromptGroup | null;
|
||||
}
|
||||
|
||||
const VariableDialog: React.FC<VariableDialogProps> = ({ open, onClose, group }) => {
|
||||
|
|
@ -18,9 +18,13 @@ const VariableDialog: React.FC<VariableDialogProps> = ({ open, onClose, group })
|
|||
};
|
||||
|
||||
const hasVariables = useMemo(
|
||||
() => detectVariables(group.productionPrompt?.prompt ?? ''),
|
||||
[group.productionPrompt?.prompt],
|
||||
() => detectVariables(group?.productionPrompt?.prompt ?? ''),
|
||||
[group?.productionPrompt?.prompt],
|
||||
);
|
||||
if (!group) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!hasVariables) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import CategoryIcon from './Groups/CategoryIcon';
|
|||
import PromptVariables from './PromptVariables';
|
||||
import Description from './Description';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import Command from './Command';
|
||||
|
||||
const PromptDetails = ({ group }: { group: TPromptGroup }) => {
|
||||
const localize = useLocalize();
|
||||
|
|
@ -27,17 +28,18 @@ const PromptDetails = ({ group }: { group: TPromptGroup }) => {
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex h-full w-full flex-col md:flex-row">
|
||||
<div className="flex-1 overflow-y-auto border-gray-300 p-0 dark:border-gray-600 md:max-h-[calc(100vh-150px)] md:p-4">
|
||||
<div className="flex flex-1 flex-col gap-4 overflow-y-auto border-gray-300 p-0 dark:border-gray-600 md:max-h-[calc(100vh-150px)] md:p-4">
|
||||
<div>
|
||||
<h2 className="flex items-center justify-between rounded-t-lg border border-gray-300 py-2 pl-4 text-base font-semibold dark:border-gray-600 dark:text-gray-200">
|
||||
{localize('com_ui_prompt_text')}
|
||||
</h2>
|
||||
<div className="group relative mb-4 min-h-32 rounded-b-lg border border-gray-300 p-4 transition-all duration-150 dark:border-gray-600">
|
||||
<div className="group relative min-h-32 rounded-b-lg border border-gray-300 p-4 transition-all duration-150 dark:border-gray-600">
|
||||
<span className="block break-words px-2 py-1 dark:text-gray-200">{promptText}</span>
|
||||
</div>
|
||||
</div>
|
||||
<PromptVariables promptText={promptText} />
|
||||
<Description initialValue={group.oneliner} disabled={true} />
|
||||
<Command initialValue={group.command} disabled={true} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
</h2>
|
||||
<div
|
||||
className={cn(
|
||||
'group relative mb-4 min-h-32 rounded-b-lg border border-gray-300 p-4 transition-all duration-150 hover:opacity-90 dark:border-gray-600',
|
||||
'group relative min-h-32 rounded-b-lg border border-gray-300 p-4 transition-all duration-150 hover:opacity-90 dark:border-gray-600',
|
||||
{ 'cursor-pointer hover:bg-gray-100/50 dark:hover:bg-gray-100/10': !isEditing },
|
||||
)}
|
||||
onClick={() => !isEditing && setIsEditing(true)}
|
||||
|
|
|
|||
|
|
@ -14,12 +14,13 @@ import {
|
|||
useUpdatePromptGroup,
|
||||
useMakePromptProduction,
|
||||
} from '~/data-provider';
|
||||
import { useAuthContext, usePromptGroupsNav, useHasAccess } from '~/hooks';
|
||||
import { useAuthContext, usePromptGroupsNav, useHasAccess, useLocalize } 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 { useToastContext } from '~/Providers';
|
||||
import PromptVersions from './PromptVersions';
|
||||
import DeleteConfirm from './DeleteVersion';
|
||||
import PromptDetails from './PromptDetails';
|
||||
|
|
@ -29,6 +30,7 @@ import SkeletonForm from './SkeletonForm';
|
|||
import Description from './Description';
|
||||
import SharePrompt from './SharePrompt';
|
||||
import PromptName from './PromptName';
|
||||
import Command from './Command';
|
||||
import store from '~/store';
|
||||
|
||||
const { PromptsEditorMode, promptsEditorMode } = store;
|
||||
|
|
@ -36,7 +38,10 @@ const { PromptsEditorMode, promptsEditorMode } = store;
|
|||
const PromptForm = () => {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const localize = useLocalize();
|
||||
|
||||
const { user } = useAuthContext();
|
||||
const { showToast } = useToastContext();
|
||||
const editorMode = useRecoilValue(promptsEditorMode);
|
||||
const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd);
|
||||
const { data: group, isLoading: isLoadingGroup } = useGetPromptGroup(params.promptId || '');
|
||||
|
|
@ -101,7 +106,14 @@ const PromptForm = () => {
|
|||
setSelectionIndex(0);
|
||||
},
|
||||
});
|
||||
const updateGroupMutation = useUpdatePromptGroup();
|
||||
const updateGroupMutation = useUpdatePromptGroup({
|
||||
onError: () => {
|
||||
showToast({
|
||||
status: 'error',
|
||||
message: localize('com_ui_prompt_update_error'),
|
||||
});
|
||||
},
|
||||
});
|
||||
const makeProductionMutation = useMakePromptProduction();
|
||||
const deletePromptMutation = useDeletePrompt({
|
||||
onSuccess: (response) => {
|
||||
|
|
@ -175,6 +187,17 @@ const PromptForm = () => {
|
|||
[updateGroupMutation, group],
|
||||
);
|
||||
|
||||
const debouncedUpdateCommand = useCallback(
|
||||
debounce((command: string) => {
|
||||
if (!group) {
|
||||
return console.warn('Group not found');
|
||||
}
|
||||
|
||||
updateGroupMutation.mutate({ id: group._id || '', payload: { command } });
|
||||
}, 950),
|
||||
[updateGroupMutation, group],
|
||||
);
|
||||
|
||||
const { groupsQuery } = useOutletContext<ReturnType<typeof usePromptGroupsNav>>();
|
||||
|
||||
if (initialLoad) {
|
||||
|
|
@ -282,14 +305,18 @@ const PromptForm = () => {
|
|||
{isLoadingPrompts ? (
|
||||
<Skeleton className="h-96" />
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col gap-4">
|
||||
<PromptEditor name="prompt" isEditing={isEditing} setIsEditing={setIsEditing} />
|
||||
<PromptVariables promptText={promptText} />
|
||||
<Description
|
||||
initialValue={group?.oneliner ?? ''}
|
||||
onValueChange={debouncedUpdateOneliner}
|
||||
/>
|
||||
</>
|
||||
<Command
|
||||
initialValue={group?.command ?? ''}
|
||||
onValueChange={debouncedUpdateCommand}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Right Section */}
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@ const PromptVariables = ({ promptText }: { promptText: string }) => {
|
|||
}, [promptText]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h3 className="flex items-center gap-2 rounded-t-lg border border-border-medium py-2 pl-4 text-base font-semibold text-text-secondary">
|
||||
<Variable className="icon-sm" />
|
||||
{localize('com_ui_variables')}
|
||||
</h3>
|
||||
<div className="mb-4 flex w-full flex-row flex-wrap rounded-b-lg border border-border-medium p-4 md:min-h-16">
|
||||
<div className="flex w-full flex-row flex-wrap rounded-b-lg border border-border-medium p-4 md:min-h-16">
|
||||
{variables.length ? (
|
||||
<div className="flex h-7 items-center">
|
||||
{variables.map((variable, index) => (
|
||||
|
|
@ -52,7 +52,7 @@ const PromptVariables = ({ promptText }: { promptText: string }) => {
|
|||
{localize('com_ui_special_variables')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue