mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-22 03:10: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
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