mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-21 21:50:49 +02: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
|
@ -51,6 +51,94 @@ const createGroupPipeline = (query, skip, limit) => {
|
|||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a pipeline for the aggregation to get all prompt groups
|
||||
* @param {Object} query
|
||||
* @param {Partial<MongoPromptGroup>} $project
|
||||
* @returns {[Object]} - The pipeline for the aggregation
|
||||
*/
|
||||
const createAllGroupsPipeline = (
|
||||
query,
|
||||
$project = {
|
||||
name: 1,
|
||||
oneliner: 1,
|
||||
category: 1,
|
||||
author: 1,
|
||||
authorName: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
command: 1,
|
||||
'productionPrompt.prompt': 1,
|
||||
},
|
||||
) => {
|
||||
return [
|
||||
{ $match: query },
|
||||
{ $sort: { createdAt: -1 } },
|
||||
{
|
||||
$lookup: {
|
||||
from: 'prompts',
|
||||
localField: 'productionId',
|
||||
foreignField: '_id',
|
||||
as: 'productionPrompt',
|
||||
},
|
||||
},
|
||||
{ $unwind: { path: '$productionPrompt', preserveNullAndEmptyArrays: true } },
|
||||
{
|
||||
$project,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all prompt groups with filters
|
||||
* @param {Object} req
|
||||
* @param {TPromptGroupsWithFilterRequest} filter
|
||||
* @returns {Promise<PromptGroupListResponse>}
|
||||
*/
|
||||
const getAllPromptGroups = async (req, filter) => {
|
||||
try {
|
||||
const { name, ...query } = filter;
|
||||
|
||||
if (!query.author) {
|
||||
throw new Error('Author is required');
|
||||
}
|
||||
|
||||
let searchShared = true;
|
||||
let searchSharedOnly = false;
|
||||
if (name) {
|
||||
query.name = new RegExp(name, 'i');
|
||||
}
|
||||
if (!query.category) {
|
||||
delete query.category;
|
||||
} else if (query.category === SystemCategories.MY_PROMPTS) {
|
||||
searchShared = false;
|
||||
delete query.category;
|
||||
} else if (query.category === SystemCategories.NO_CATEGORY) {
|
||||
query.category = '';
|
||||
} else if (query.category === SystemCategories.SHARED_PROMPTS) {
|
||||
searchSharedOnly = true;
|
||||
delete query.category;
|
||||
}
|
||||
|
||||
let combinedQuery = query;
|
||||
|
||||
if (searchShared) {
|
||||
const project = await getProjectByName('instance', 'promptGroupIds');
|
||||
if (project && project.promptGroupIds.length > 0) {
|
||||
const projectQuery = { _id: { $in: project.promptGroupIds }, ...query };
|
||||
delete projectQuery.author;
|
||||
combinedQuery = searchSharedOnly ? projectQuery : { $or: [projectQuery, query] };
|
||||
}
|
||||
}
|
||||
|
||||
const promptGroupsPipeline = createAllGroupsPipeline(combinedQuery);
|
||||
return await PromptGroup.aggregate(promptGroupsPipeline).exec();
|
||||
} catch (error) {
|
||||
console.error('Error getting all prompt groups', error);
|
||||
return { message: 'Error getting all prompt groups' };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get prompt groups with filters
|
||||
* @param {Object} req
|
||||
|
@ -126,6 +214,7 @@ const getPromptGroups = async (req, filter) => {
|
|||
|
||||
module.exports = {
|
||||
getPromptGroups,
|
||||
getAllPromptGroups,
|
||||
/**
|
||||
* Create a prompt and its respective group
|
||||
* @param {TCreatePromptRecord} saveData
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const mongoose = require('mongoose');
|
||||
const { Constants } = require('librechat-data-provider');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
/**
|
||||
|
@ -12,6 +13,7 @@ const Schema = mongoose.Schema;
|
|||
* @property {number} [numberOfGenerations=0] - Number of generations the prompt group has
|
||||
* @property {string} [oneliner=''] - Oneliner description of the prompt group
|
||||
* @property {string} [category=''] - Category of the prompt group
|
||||
* @property {string} [command] - Command for the prompt group
|
||||
* @property {Date} [createdAt] - Date when the prompt group was created (added by timestamps)
|
||||
* @property {Date} [updatedAt] - Date when the prompt group was last updated (added by timestamps)
|
||||
*/
|
||||
|
@ -57,6 +59,21 @@ const promptGroupSchema = new Schema(
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
command: {
|
||||
type: String,
|
||||
index: true,
|
||||
validate: {
|
||||
validator: function (v) {
|
||||
return v === undefined || v === null || v === '' || /^[a-z0-9-]+$/.test(v);
|
||||
},
|
||||
message: (props) =>
|
||||
`${props.value} is not a valid command. Only lowercase alphanumeric characters and highfins (') are allowed.`,
|
||||
},
|
||||
maxlength: [
|
||||
Constants.COMMANDS_MAX_LENGTH,
|
||||
`Command cannot be longer than ${Constants.COMMANDS_MAX_LENGTH} characters`,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
|
|
@ -10,6 +10,7 @@ const {
|
|||
updatePromptGroup,
|
||||
deletePromptGroup,
|
||||
createPromptGroup,
|
||||
getAllPromptGroups,
|
||||
// updatePromptLabels,
|
||||
makePromptProduction,
|
||||
} = require('~/models/Prompt');
|
||||
|
@ -65,6 +66,22 @@ router.get('/groups/:groupId', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Route to fetch all prompt groups
|
||||
* GET /groups
|
||||
*/
|
||||
router.get('/all', async (req, res) => {
|
||||
try {
|
||||
const groups = await getAllPromptGroups(req, {
|
||||
author: req.user._id,
|
||||
});
|
||||
res.status(200).send(groups);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
res.status(500).send({ error: 'Error getting prompt groups' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Route to fetch paginated prompt groups with filters
|
||||
* GET /groups
|
||||
|
|
|
@ -62,7 +62,9 @@ const sendVerificationEmail = async (user) => {
|
|||
let verifyToken = crypto.randomBytes(32).toString('hex');
|
||||
const hash = bcrypt.hashSync(verifyToken, 10);
|
||||
|
||||
const verificationLink = `${domains.client}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`;
|
||||
const verificationLink = `${
|
||||
domains.client
|
||||
}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`;
|
||||
await sendEmail({
|
||||
email: user.email,
|
||||
subject: 'Verify your email',
|
||||
|
@ -119,9 +121,10 @@ const verifyEmail = async (req) => {
|
|||
/**
|
||||
* Register a new user.
|
||||
* @param {MongoUser} user <email, password, name, username>
|
||||
* @param {Partial<MongoUser>} [additionalData={}]
|
||||
* @returns {Promise<{status: number, message: string, user?: MongoUser}>}
|
||||
*/
|
||||
const registerUser = async (user) => {
|
||||
const registerUser = async (user, additionalData = {}) => {
|
||||
const { error } = registerSchema.safeParse(user);
|
||||
if (error) {
|
||||
const errorMessage = errorsToString(error.errors);
|
||||
|
@ -171,11 +174,13 @@ const registerUser = async (user) => {
|
|||
avatar: null,
|
||||
role: isFirstRegisteredUser ? SystemRoles.ADMIN : SystemRoles.USER,
|
||||
password: bcrypt.hashSync(password, salt),
|
||||
...additionalData,
|
||||
};
|
||||
|
||||
const emailEnabled = checkEmailConfig();
|
||||
newUserId = await createUser(newUserData, false);
|
||||
if (emailEnabled) {
|
||||
const newUser = await createUser(newUserData, false, true);
|
||||
newUserId = newUser._id;
|
||||
if (emailEnabled && !newUser.emailVerified) {
|
||||
await sendVerificationEmail({
|
||||
_id: newUserId,
|
||||
email,
|
||||
|
@ -363,7 +368,9 @@ const resendVerificationEmail = async (req) => {
|
|||
let verifyToken = crypto.randomBytes(32).toString('hex');
|
||||
const hash = bcrypt.hashSync(verifyToken, 10);
|
||||
|
||||
const verificationLink = `${domains.client}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`;
|
||||
const verificationLink = `${
|
||||
domains.client
|
||||
}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`;
|
||||
|
||||
await sendEmail({
|
||||
email: user.email,
|
||||
|
|
|
@ -375,6 +375,9 @@ export type MentionOption = OptionWithIcon & {
|
|||
value: string;
|
||||
description?: string;
|
||||
};
|
||||
export type PromptOption = MentionOption & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TOptionSettings = {
|
||||
showExamples?: boolean;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -5,9 +5,12 @@ import type { UseMutationResult } from '@tanstack/react-query';
|
|||
import type t from 'librechat-data-provider';
|
||||
import {
|
||||
/* Prompts */
|
||||
addGroupToAll,
|
||||
addPromptGroup,
|
||||
updateGroupInAll,
|
||||
updateGroupFields,
|
||||
deletePromptGroup,
|
||||
removeGroupFromAll,
|
||||
} from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
|
@ -83,7 +86,12 @@ export const useUpdatePromptGroup = (
|
|||
onError(err, variables, context);
|
||||
}
|
||||
},
|
||||
onSuccess,
|
||||
onSuccess: (response, variables, context) => {
|
||||
updateGroupInAll(queryClient, { _id: variables.id, ...response });
|
||||
if (onSuccess) {
|
||||
onSuccess(response, variables, context);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -118,6 +126,8 @@ export const useCreatePrompt = (
|
|||
return addPromptGroup(data, group);
|
||||
},
|
||||
);
|
||||
|
||||
addGroupToAll(queryClient, group);
|
||||
}
|
||||
|
||||
if (onSuccess) {
|
||||
|
@ -151,6 +161,8 @@ export const useDeletePrompt = (
|
|||
return deletePromptGroup(data, promptGroupId);
|
||||
},
|
||||
);
|
||||
|
||||
removeGroupFromAll(queryClient, promptGroupId);
|
||||
} else {
|
||||
queryClient.setQueryData<t.TPrompt[]>(
|
||||
[QueryKeys.prompts, variables.groupId],
|
||||
|
@ -208,6 +220,8 @@ export const useDeletePromptGroup = (
|
|||
return deletePromptGroup(data, variables.id);
|
||||
},
|
||||
);
|
||||
|
||||
removeGroupFromAll(queryClient, variables.id);
|
||||
if (onSuccess) {
|
||||
onSuccess(response, variables, context);
|
||||
}
|
||||
|
@ -299,6 +313,15 @@ export const useMakePromptProduction = (options?: t.MakePromptProductionOptions)
|
|||
onError(err, variables, context);
|
||||
}
|
||||
},
|
||||
onSuccess,
|
||||
onSuccess: (response, variables, context) => {
|
||||
updateGroupInAll(queryClient, {
|
||||
_id: variables.groupId,
|
||||
productionId: variables.id,
|
||||
productionPrompt: variables.productionPrompt,
|
||||
});
|
||||
if (onSuccess) {
|
||||
onSuccess(response, variables, context);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -488,6 +488,23 @@ export const useGetPrompts = (
|
|||
);
|
||||
};
|
||||
|
||||
export const useGetAllPromptGroups = <TData = t.AllPromptGroupsResponse>(
|
||||
filter?: t.AllPromptGroupsFilterRequest,
|
||||
config?: UseQueryOptions<t.AllPromptGroupsResponse, unknown, TData>,
|
||||
): QueryObserverResult<TData> => {
|
||||
return useQuery<t.AllPromptGroupsResponse, unknown, TData>(
|
||||
[QueryKeys.allPromptGroups],
|
||||
() => dataService.getAllPromptGroups(),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
retry: false,
|
||||
...config,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const useGetCategories = <TData = t.TGetCategoriesResponse>(
|
||||
config?: UseQueryOptions<t.TGetCategoriesResponse, unknown, TData>,
|
||||
): QueryObserverResult<TData> => {
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { useSetRecoilState } from 'recoil';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import type { SetterOrUpdater } from 'recoil';
|
||||
import useHasAccess from '~/hooks/Roles/useHasAccess';
|
||||
import store from '~/store';
|
||||
|
||||
/** Event Keys that shouldn't trigger a command */
|
||||
const invalidKeys = {
|
||||
|
@ -36,14 +40,21 @@ const shouldTriggerCommand = (
|
|||
* Custom hook for handling key up events with command triggers.
|
||||
*/
|
||||
const useHandleKeyUp = ({
|
||||
index,
|
||||
textAreaRef,
|
||||
setShowPlusPopover,
|
||||
setShowMentionPopover,
|
||||
}: {
|
||||
index: number;
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
setShowPlusPopover: SetterOrUpdater<boolean>;
|
||||
setShowMentionPopover: SetterOrUpdater<boolean>;
|
||||
}) => {
|
||||
const hasAccess = useHasAccess({
|
||||
permissionType: PermissionTypes.PROMPTS,
|
||||
permission: Permissions.USE,
|
||||
});
|
||||
const setShowPromptsPopover = useSetRecoilState(store.showPromptsPopoverFamily(index));
|
||||
const handleAtCommand = useCallback(() => {
|
||||
if (shouldTriggerCommand(textAreaRef, '@')) {
|
||||
setShowMentionPopover(true);
|
||||
|
@ -56,12 +67,22 @@ const useHandleKeyUp = ({
|
|||
}
|
||||
}, [textAreaRef, setShowPlusPopover]);
|
||||
|
||||
const handlePromptsCommand = useCallback(() => {
|
||||
if (!hasAccess) {
|
||||
return;
|
||||
}
|
||||
if (shouldTriggerCommand(textAreaRef, '/')) {
|
||||
setShowPromptsPopover(true);
|
||||
}
|
||||
}, [textAreaRef, hasAccess, setShowPromptsPopover]);
|
||||
|
||||
const commandHandlers = useMemo(
|
||||
() => ({
|
||||
'@': handleAtCommand,
|
||||
'+': handlePlusCommand,
|
||||
'/': handlePromptsCommand,
|
||||
}),
|
||||
[handleAtCommand, handlePlusCommand],
|
||||
[handleAtCommand, handlePlusCommand, handlePromptsCommand],
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
@ -32,22 +32,13 @@ export default function useSideNavLinks({
|
|||
endpoint?: EModelEndpoint | null;
|
||||
interfaceConfig: Partial<TInterfaceConfig>;
|
||||
}) {
|
||||
const hasAccess = useHasAccess({
|
||||
const hasAccessToPrompts = useHasAccess({
|
||||
permissionType: PermissionTypes.PROMPTS,
|
||||
permission: Permissions.USE,
|
||||
});
|
||||
|
||||
const Links = useMemo(() => {
|
||||
const links: NavLink[] = [];
|
||||
if (hasAccess) {
|
||||
links.push({
|
||||
title: 'com_ui_prompts',
|
||||
label: '',
|
||||
icon: MessageSquareQuote,
|
||||
id: 'prompts',
|
||||
Component: PromptsAccordion,
|
||||
});
|
||||
}
|
||||
if (
|
||||
isAssistantsEndpoint(endpoint) &&
|
||||
assistants &&
|
||||
|
@ -64,6 +55,16 @@ export default function useSideNavLinks({
|
|||
});
|
||||
}
|
||||
|
||||
if (hasAccessToPrompts) {
|
||||
links.push({
|
||||
title: 'com_ui_prompts',
|
||||
label: '',
|
||||
icon: MessageSquareQuote,
|
||||
id: 'prompts',
|
||||
Component: PromptsAccordion,
|
||||
});
|
||||
}
|
||||
|
||||
links.push({
|
||||
title: 'com_sidepanel_attach_files',
|
||||
label: '',
|
||||
|
@ -81,7 +82,14 @@ export default function useSideNavLinks({
|
|||
});
|
||||
|
||||
return links;
|
||||
}, [assistants, keyProvided, hidePanel, endpoint, interfaceConfig.parameters, hasAccess]);
|
||||
}, [
|
||||
assistants,
|
||||
keyProvided,
|
||||
hidePanel,
|
||||
endpoint,
|
||||
interfaceConfig.parameters,
|
||||
hasAccessToPrompts,
|
||||
]);
|
||||
|
||||
return Links;
|
||||
}
|
||||
|
|
|
@ -7,8 +7,7 @@ export default {
|
|||
'It appears that the content submitted has been flagged by our moderation system for not aligning with our community guidelines. We\'re unable to proceed with this specific topic. If you have any other questions or topics you\'d like to explore, please edit your message, or create a new conversation.',
|
||||
com_error_no_user_key: 'No key found. Please provide a key and try again.',
|
||||
com_error_no_base_url: 'No base URL found. Please provide one and try again.',
|
||||
com_error_invalid_user_key:
|
||||
'Invalid key provided. Please provide a valid key and try again.',
|
||||
com_error_invalid_user_key: 'Invalid key provided. Please provide a valid key and try again.',
|
||||
com_error_expired_user_key:
|
||||
'Provided key for {0} expired at {1}. Please provide a new key and try again.',
|
||||
com_files_no_results: 'No results.',
|
||||
|
@ -247,8 +246,11 @@ export default {
|
|||
com_ui_prompts_allow_create: 'Allow creating Prompts',
|
||||
com_ui_prompts_allow_share_global: 'Allow sharing Prompts to all users',
|
||||
com_ui_prompt_shared_to_all: 'This prompt is shared to all users',
|
||||
com_ui_prompt_update_error: 'There was an error updating the prompt',
|
||||
com_ui_prompt_already_shared_to_all: 'This prompt is already shared to all users',
|
||||
com_ui_description_placeholder: 'Optional: Enter a description to display in the prompt',
|
||||
com_ui_description_placeholder: 'Optional: Enter a description to display for the prompt',
|
||||
com_ui_command_placeholder: 'Optional: Enter a command for the prompt or name will be used.',
|
||||
com_ui_command_usage_placeholder: 'Select a Prompt by command or name',
|
||||
com_ui_no_prompt_description: 'No description found.',
|
||||
com_ui_share_link_to_chat: 'Share link to chat',
|
||||
com_ui_share_error: 'There was an error sharing the chat link',
|
||||
|
@ -641,7 +643,8 @@ export default {
|
|||
'When enabled, the text and attachments you enter in the chat form will be automatically saved locally as drafts. These drafts will be available even if you reload the page or switch to a different conversation. Drafts are stored locally on your device and are deleted once the message is sent.',
|
||||
com_nav_info_fork_change_default:
|
||||
'`Visible messages only` includes just the direct path to the selected message. `Include related branches` adds branches along the path. `Include all to/from here` includes all connected messages and branches.',
|
||||
com_nav_info_fork_split_target_setting: 'When enabled, forking will commence from the target message to the latest message in the conversation, according to the behavior selected.',
|
||||
com_nav_info_fork_split_target_setting:
|
||||
'When enabled, forking will commence from the target message to the latest message in the conversation, according to the behavior selected.',
|
||||
com_nav_info_user_name_display:
|
||||
'When enabled, the username of the sender will be shown above each message you send. When disabled, you will only see "You" above your messages.',
|
||||
com_nav_info_latex_parsing:
|
||||
|
|
|
@ -21,7 +21,7 @@ import AdminSettings from '~/components/Prompts/AdminSettings';
|
|||
import { useDashboardContext } from '~/Providers';
|
||||
import store from '~/store';
|
||||
|
||||
const promptsPathPattern = /prompts\/.*/;
|
||||
const promptsPathPattern = /prompts\/(?!new(?:\/|$)).*$/;
|
||||
|
||||
const getConversationId = (prevLocationPath: string) => {
|
||||
if (!prevLocationPath || prevLocationPath.includes('/d/')) {
|
||||
|
|
|
@ -173,6 +173,11 @@ const showPlusPopoverFamily = atomFamily<boolean, string | number | null>({
|
|||
default: false,
|
||||
});
|
||||
|
||||
const showPromptsPopoverFamily = atomFamily<boolean, string | number | null>({
|
||||
key: 'showPromptsPopoverByIndex',
|
||||
default: false,
|
||||
});
|
||||
|
||||
const globalAudioURLFamily = atomFamily<string | null, string | number | null>({
|
||||
key: 'globalAudioURLByIndex',
|
||||
default: null,
|
||||
|
@ -326,4 +331,5 @@ export default {
|
|||
activePromptByIndex,
|
||||
useClearSubmissionState,
|
||||
useClearLatestMessages,
|
||||
showPromptsPopoverFamily,
|
||||
};
|
||||
|
|
|
@ -32,6 +32,7 @@ html {
|
|||
--surface-primary-contrast:var(--gray-100);
|
||||
--surface-secondary:var(--gray-50);
|
||||
--surface-tertiary:var(--gray-100);
|
||||
--surface-tertiary-alt:var(--white);
|
||||
--border-light:var(--gray-100);
|
||||
--border-medium-alt:var(--gray-300);
|
||||
--border-medium:var(--gray-200);
|
||||
|
@ -48,6 +49,7 @@ html {
|
|||
--surface-primary-contrast:var(--gray-850);
|
||||
--surface-secondary:var(--gray-800);
|
||||
--surface-tertiary:var(--gray-700);
|
||||
--surface-tertiary-alt:var(--gray-700);
|
||||
--border-light:var(--gray-700);
|
||||
--border-medium-alt:var(--gray-600);
|
||||
--border-medium:var(--gray-600);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { InfiniteData } from '@tanstack/react-query';
|
||||
import { InfiniteData, QueryClient } from '@tanstack/react-query';
|
||||
|
||||
export const addData = <TCollection, TData>(
|
||||
data: InfiniteData<TCollection>,
|
||||
|
@ -165,3 +165,56 @@ export const updateFields = <TCollection, TData>(
|
|||
|
||||
return newData;
|
||||
};
|
||||
|
||||
type UpdateCacheListOptions<TData> = {
|
||||
queryClient: QueryClient;
|
||||
queryKey: unknown[];
|
||||
searchProperty: keyof TData;
|
||||
updateData: Partial<TData>;
|
||||
searchValue: unknown;
|
||||
};
|
||||
|
||||
export function updateCacheList<TData>({
|
||||
queryClient,
|
||||
queryKey,
|
||||
searchProperty,
|
||||
updateData,
|
||||
searchValue,
|
||||
}: UpdateCacheListOptions<TData>) {
|
||||
queryClient.setQueryData<TData[]>(queryKey, (oldData) => {
|
||||
if (!oldData) {
|
||||
return oldData;
|
||||
}
|
||||
|
||||
return oldData.map((item) =>
|
||||
item[searchProperty] === searchValue ? { ...item, ...updateData } : item,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function addToCacheList<TData>(
|
||||
queryClient: QueryClient,
|
||||
queryKey: unknown[],
|
||||
newItem: TData,
|
||||
) {
|
||||
queryClient.setQueryData<TData[]>(queryKey, (oldData) => {
|
||||
if (!oldData) {
|
||||
return [newItem];
|
||||
}
|
||||
return [...oldData, newItem];
|
||||
});
|
||||
}
|
||||
|
||||
export function removeFromCacheList<TData>(
|
||||
queryClient: QueryClient,
|
||||
queryKey: unknown[],
|
||||
searchProperty: keyof TData,
|
||||
searchValue: unknown,
|
||||
) {
|
||||
queryClient.setQueryData<TData[]>(queryKey, (oldData) => {
|
||||
if (!oldData) {
|
||||
return oldData;
|
||||
}
|
||||
return oldData.filter((item) => item[searchProperty] !== searchValue);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
import { InfiniteCollections } from 'librechat-data-provider';
|
||||
import { InfiniteCollections, QueryKeys } from 'librechat-data-provider';
|
||||
import type { InfiniteData, QueryClient } from '@tanstack/react-query';
|
||||
import type {
|
||||
PromptGroupListResponse,
|
||||
PromptGroupListData,
|
||||
TPromptGroup,
|
||||
} from 'librechat-data-provider';
|
||||
import { addData, deleteData, updateData, updateFields, getRecordByProperty } from './collection';
|
||||
import { InfiniteData } from '@tanstack/react-query';
|
||||
import {
|
||||
addData,
|
||||
deleteData,
|
||||
updateData,
|
||||
updateFields,
|
||||
addToCacheList,
|
||||
updateCacheList,
|
||||
removeFromCacheList,
|
||||
getRecordByProperty,
|
||||
} from './collection';
|
||||
|
||||
export const addPromptGroup = (
|
||||
data: InfiniteData<PromptGroupListResponse>,
|
||||
|
@ -70,3 +79,24 @@ export const findPromptGroup = (
|
|||
findProperty,
|
||||
);
|
||||
};
|
||||
|
||||
export const addGroupToAll = (queryClient: QueryClient, newGroup: TPromptGroup) => {
|
||||
addToCacheList<TPromptGroup>(queryClient, [QueryKeys.allPromptGroups], newGroup);
|
||||
};
|
||||
|
||||
export const updateGroupInAll = (
|
||||
queryClient: QueryClient,
|
||||
updatedGroup: Partial<TPromptGroup> & { _id: string },
|
||||
) => {
|
||||
updateCacheList<TPromptGroup>({
|
||||
queryClient,
|
||||
queryKey: [QueryKeys.allPromptGroups],
|
||||
searchProperty: '_id',
|
||||
updateData: updatedGroup,
|
||||
searchValue: updatedGroup._id,
|
||||
});
|
||||
};
|
||||
|
||||
export const removeGroupFromAll = (queryClient: QueryClient, groupId: string) => {
|
||||
removeFromCacheList<TPromptGroup>(queryClient, [QueryKeys.allPromptGroups], '_id', groupId);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { format } from 'date-fns';
|
||||
import type { TUser } from 'librechat-data-provider';
|
||||
import type { TUser, TPromptGroup } from 'librechat-data-provider';
|
||||
|
||||
export function replaceSpecialVars({ text, user }: { text: string; user?: TUser }) {
|
||||
if (!text) {
|
||||
|
@ -92,3 +92,13 @@ export function formatDateTime(dateTimeString: string) {
|
|||
|
||||
return `${formattedDate}, ${formattedTime}`;
|
||||
}
|
||||
|
||||
export const mapPromptGroups = (groups: TPromptGroup[]): Record<string, TPromptGroup> => {
|
||||
return groups.reduce((acc, group) => {
|
||||
if (!group._id) {
|
||||
return acc;
|
||||
}
|
||||
acc[group._id] = group;
|
||||
return acc;
|
||||
}, {} as Record<string, TPromptGroup>);
|
||||
};
|
||||
|
|
|
@ -73,6 +73,7 @@ module.exports = {
|
|||
'surface-primary-contrast': 'var(--surface-primary-contrast)',
|
||||
'surface-secondary': 'var(--surface-secondary)',
|
||||
'surface-tertiary': 'var(--surface-tertiary)',
|
||||
'surface-tertiary-alt': 'var(--surface-tertiary-alt)',
|
||||
'border-light': 'var(--border-light)',
|
||||
'border-medium': 'var(--border-medium)',
|
||||
'border-medium-alt': 'var(--border-medium-alt)',
|
||||
|
|
|
@ -8,50 +8,48 @@ const connect = require('./connect');
|
|||
(async () => {
|
||||
await connect();
|
||||
|
||||
/**
|
||||
* Show the welcome / help menu
|
||||
*/
|
||||
console.purple('--------------------------');
|
||||
console.purple('Create a new user account!');
|
||||
console.purple('--------------------------');
|
||||
// If we don't have enough arguments, show the help menu
|
||||
|
||||
if (process.argv.length < 5) {
|
||||
console.orange('Usage: npm run create-user <email> <name> <username>');
|
||||
console.orange('Usage: npm run create-user <email> <name> <username> [--email-verified=false]');
|
||||
console.orange('Note: if you do not pass in the arguments, you will be prompted for them.');
|
||||
console.orange(
|
||||
'If you really need to pass in the password, you can do so as the 4th argument (not recommended for security).',
|
||||
);
|
||||
console.orange('Use --email-verified=false to set emailVerified to false. Default is true.');
|
||||
console.purple('--------------------------');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the variables we need and get the arguments if they were passed in
|
||||
*/
|
||||
let email = '';
|
||||
let password = '';
|
||||
let name = '';
|
||||
let username = '';
|
||||
// If we have the right number of arguments, lets use them
|
||||
if (process.argv.length >= 4) {
|
||||
email = process.argv[2];
|
||||
name = process.argv[3];
|
||||
let emailVerified = true;
|
||||
|
||||
if (process.argv.length >= 5) {
|
||||
username = process.argv[4];
|
||||
// Parse command line arguments
|
||||
for (let i = 2; i < process.argv.length; i++) {
|
||||
if (process.argv[i].startsWith('--email-verified=')) {
|
||||
emailVerified = process.argv[i].split('=')[1].toLowerCase() !== 'false';
|
||||
continue;
|
||||
}
|
||||
if (process.argv.length >= 6) {
|
||||
|
||||
if (!email) {
|
||||
email = process.argv[i];
|
||||
} else if (!name) {
|
||||
name = process.argv[i];
|
||||
} else if (!username) {
|
||||
username = process.argv[i];
|
||||
} else if (!password) {
|
||||
console.red('Warning: password passed in as argument, this is not secure!');
|
||||
password = process.argv[5];
|
||||
password = process.argv[i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we don't have the right number of arguments, lets prompt the user for them
|
||||
*/
|
||||
if (!email) {
|
||||
email = await askQuestion('Email:');
|
||||
}
|
||||
// Validate the email
|
||||
if (!email.includes('@')) {
|
||||
console.red('Error: Invalid email address!');
|
||||
silentExit(1);
|
||||
|
@ -73,41 +71,51 @@ const connect = require('./connect');
|
|||
if (!password) {
|
||||
password = await askQuestion('Password: (leave blank, to generate one)');
|
||||
if (!password) {
|
||||
// Make it a random password, length 18
|
||||
password = Math.random().toString(36).slice(-18);
|
||||
console.orange('Your password is: ' + password);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the user doesn't already exist
|
||||
// Only prompt for emailVerified if it wasn't set via CLI
|
||||
if (!process.argv.some((arg) => arg.startsWith('--email-verified='))) {
|
||||
const emailVerifiedInput = await askQuestion(`Email verified? (Y/n, default is Y):
|
||||
|
||||
If \`y\`, the user's email will be considered verified.
|
||||
|
||||
If \`n\`, and email service is configured, the user will be sent a verification email.
|
||||
|
||||
If \`n\`, and email service is not configured, you must have the \`ALLOW_UNVERIFIED_EMAIL_LOGIN\` .env variable set to true,
|
||||
or the user will need to attempt logging in to have a verification link sent to them.`);
|
||||
|
||||
if (emailVerifiedInput.toLowerCase() === 'n') {
|
||||
emailVerified = false;
|
||||
}
|
||||
}
|
||||
|
||||
const userExists = await User.findOne({ $or: [{ email }, { username }] });
|
||||
if (userExists) {
|
||||
console.red('Error: A user with that email or username already exists!');
|
||||
silentExit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Now that we have all the variables we need, lets create the user
|
||||
*/
|
||||
const user = { email, password, name, username, confirm_password: password };
|
||||
let result;
|
||||
try {
|
||||
result = await registerUser(user);
|
||||
result = await registerUser(user, { emailVerified });
|
||||
} catch (error) {
|
||||
console.red('Error: ' + error.message);
|
||||
silentExit(1);
|
||||
}
|
||||
|
||||
// Check the result
|
||||
if (result.status !== 200) {
|
||||
console.red('Error: ' + result.message);
|
||||
silentExit(1);
|
||||
}
|
||||
|
||||
// Done!
|
||||
const userCreated = await User.findOne({ $or: [{ email }, { username }] });
|
||||
if (userCreated) {
|
||||
console.green('User created successfully!');
|
||||
console.green(`Email verified: ${userCreated.emailVerified}`);
|
||||
silentExit(0);
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -177,6 +177,8 @@ export const deletePrompt = ({ _id, groupId }: { _id: string; groupId: string })
|
|||
|
||||
export const getCategories = () => '/api/categories';
|
||||
|
||||
export const getAllPromptGroups = () => `${prompts()}/all`;
|
||||
|
||||
/* Roles */
|
||||
export const roles = () => '/api/roles';
|
||||
export const getRole = (roleName: string) => `${roles()}/${roleName.toLowerCase()}`;
|
||||
|
|
|
@ -822,6 +822,8 @@ export enum Constants {
|
|||
CURRENT_MODEL = 'current_model',
|
||||
/** Common divider for text values */
|
||||
COMMON_DIVIDER = '__',
|
||||
/** Max length for commands */
|
||||
COMMANDS_MAX_LENGTH = 56,
|
||||
}
|
||||
|
||||
export enum LocalStorageKeys {
|
||||
|
|
|
@ -475,6 +475,10 @@ export function getPrompts(filter: t.TPromptsWithFilterRequest): Promise<t.TProm
|
|||
return request.get(endpoints.getPromptsWithFilters(filter));
|
||||
}
|
||||
|
||||
export function getAllPromptGroups(): Promise<q.AllPromptGroupsResponse> {
|
||||
return request.get(endpoints.getAllPromptGroups());
|
||||
}
|
||||
|
||||
export function getPromptGroups(
|
||||
filter: t.TPromptGroupsWithFilterRequest,
|
||||
): Promise<t.PromptGroupListResponse> {
|
||||
|
|
|
@ -30,6 +30,7 @@ export enum QueryKeys {
|
|||
prompts = 'prompts',
|
||||
prompt = 'prompt',
|
||||
promptGroups = 'promptGroups',
|
||||
allPromptGroups = 'allPromptGroups',
|
||||
promptGroup = 'promptGroup',
|
||||
categories = 'categories',
|
||||
randomPrompts = 'randomPrompts',
|
||||
|
|
|
@ -351,6 +351,7 @@ export type TPrompt = {
|
|||
export type TPromptGroup = {
|
||||
name: string;
|
||||
numberOfGenerations?: number;
|
||||
command?: string;
|
||||
oneliner?: string;
|
||||
category?: string;
|
||||
projectIds?: string[];
|
||||
|
@ -365,7 +366,7 @@ export type TPromptGroup = {
|
|||
|
||||
export type TCreatePrompt = {
|
||||
prompt: Pick<TPrompt, 'prompt' | 'type'> & { groupId?: string };
|
||||
group?: { name: string; category?: string; oneliner?: string };
|
||||
group?: { name: string; category?: string; oneliner?: string; command?: string };
|
||||
};
|
||||
|
||||
export type TCreatePromptRecord = TCreatePrompt & Pick<TPromptGroup, 'author' | 'authorName'>;
|
||||
|
@ -385,6 +386,7 @@ export type TPromptGroupsWithFilterRequest = {
|
|||
after?: string | null;
|
||||
order?: 'asc' | 'desc';
|
||||
name?: string;
|
||||
author?: string;
|
||||
};
|
||||
|
||||
export type PromptGroupListResponse = {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { InfiniteData } from '@tanstack/react-query';
|
||||
import type { TMessage, TConversation, TSharedLink } from '../schemas';
|
||||
import type * as t from '../types';
|
||||
export type Conversation = {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
|
@ -54,3 +55,16 @@ export type SharedLinkListResponse = {
|
|||
};
|
||||
|
||||
export type SharedLinkListData = InfiniteData<SharedLinkListResponse>;
|
||||
|
||||
export type AllPromptGroupsFilterRequest = {
|
||||
category: string;
|
||||
pageNumber: string;
|
||||
pageSize: string | number;
|
||||
before?: string | null;
|
||||
after?: string | null;
|
||||
order?: 'asc' | 'desc';
|
||||
name?: string;
|
||||
author?: string;
|
||||
};
|
||||
|
||||
export type AllPromptGroupsResponse = t.TPromptGroup[];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue