🗨️ 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:
Danny Avila 2024-06-27 17:34:48 -04:00 committed by GitHub
parent b8f2bee3fc
commit 83619de158
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 764 additions and 80 deletions

View 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;

View file

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

View file

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

View file

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

View file

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

View file

@ -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 */}

View file

@ -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>
);
};