diff --git a/api/models/Assistant.js b/api/models/Assistant.js index ae4c8a768..2c98287a8 100644 --- a/api/models/Assistant.js +++ b/api/models/Assistant.js @@ -12,7 +12,7 @@ const Assistant = mongoose.model('assistant', assistantSchema); * @param {string} searchParams.user - The user ID of the assistant's author. * @param {Object} updateData - An object containing the properties to update. * @param {mongoose.ClientSession} [session] - The transaction session to use (optional). - * @returns {Promise} The updated or newly created assistant document as a plain object. + * @returns {Promise} The updated or newly created assistant document as a plain object. */ const updateAssistantDoc = async (searchParams, updateData, session = null) => { const options = { new: true, upsert: true, session }; @@ -25,7 +25,7 @@ const updateAssistantDoc = async (searchParams, updateData, session = null) => { * @param {Object} searchParams - The search parameters to find the assistant to update. * @param {string} searchParams.assistant_id - The ID of the assistant to update. * @param {string} searchParams.user - The user ID of the assistant's author. - * @returns {Promise} The assistant document as a plain object, or null if not found. + * @returns {Promise} The assistant document as a plain object, or null if not found. */ const getAssistant = async (searchParams) => await Assistant.findOne(searchParams).lean(); @@ -33,10 +33,17 @@ const getAssistant = async (searchParams) => await Assistant.findOne(searchParam * Retrieves all assistants that match the given search parameters. * * @param {Object} searchParams - The search parameters to find matching assistants. - * @returns {Promise>} A promise that resolves to an array of action documents as plain objects. + * @param {Object} [select] - Optional. Specifies which document fields to include or exclude. + * @returns {Promise>} A promise that resolves to an array of assistant documents as plain objects. */ -const getAssistants = async (searchParams) => { - return await Assistant.find(searchParams).lean(); +const getAssistants = async (searchParams, select = null) => { + let query = Assistant.find(searchParams); + + if (select) { + query = query.select(select); + } + + return await query.lean(); }; /** diff --git a/api/models/schema/assistant.js b/api/models/schema/assistant.js index 67eb8e8e7..4260b8a43 100644 --- a/api/models/schema/assistant.js +++ b/api/models/schema/assistant.js @@ -19,6 +19,10 @@ const assistantSchema = mongoose.Schema( }, default: undefined, }, + conversation_starters: { + type: [String], + default: [], + }, access_level: { type: Number, }, diff --git a/api/server/controllers/assistants/helpers.js b/api/server/controllers/assistants/helpers.js index 715bb02ed..f2bf9a1e2 100644 --- a/api/server/controllers/assistants/helpers.js +++ b/api/server/controllers/assistants/helpers.js @@ -64,7 +64,7 @@ const _listAssistants = async ({ req, res, version, query }) => { * @param {object} params.res - The response object, used for initializing the client. * @param {string} params.version - The API version to use. * @param {Omit} params.query - The query parameters to list assistants (e.g., limit, order). - * @returns {Promise} A promise that resolves to the response from the `openai.beta.assistants.list` method call. + * @returns {Promise>} A promise that resolves to the response from the `openai.beta.assistants.list` method call. */ const listAllAssistants = async ({ req, res, version, query }) => { /** @type {{ openai: OpenAIClient }} */ diff --git a/api/server/controllers/assistants/v1.js b/api/server/controllers/assistants/v1.js index 96c974529..5a922cec6 100644 --- a/api/server/controllers/assistants/v1.js +++ b/api/server/controllers/assistants/v1.js @@ -18,7 +18,9 @@ const createAssistant = async (req, res) => { try { const { openai } = await getOpenAIClient({ req, res }); - const { tools = [], endpoint, ...assistantData } = req.body; + const { tools = [], endpoint, conversation_starters, ...assistantData } = req.body; + delete assistantData.conversation_starters; + assistantData.tools = tools .map((tool) => { if (typeof tool !== 'string') { @@ -41,11 +43,22 @@ const createAssistant = async (req, res) => { }; const assistant = await openai.beta.assistants.create(assistantData); - const promise = updateAssistantDoc({ assistant_id: assistant.id }, { user: req.user.id }); + + const createData = { user: req.user.id }; + if (conversation_starters) { + createData.conversation_starters = conversation_starters; + } + + const document = await updateAssistantDoc({ assistant_id: assistant.id }, createData); + if (azureModelIdentifier) { assistant.model = azureModelIdentifier; } - await promise; + + if (document.conversation_starters) { + assistant.conversation_starters = document.conversation_starters; + } + logger.debug('/assistants/', assistant); res.status(201).json(assistant); } catch (error) { @@ -88,7 +101,7 @@ const patchAssistant = async (req, res) => { await validateAuthor({ req, openai }); const assistant_id = req.params.id; - const { endpoint: _e, ...updateData } = req.body; + const { endpoint: _e, conversation_starters, ...updateData } = req.body; updateData.tools = (updateData.tools ?? []) .map((tool) => { if (typeof tool !== 'string') { @@ -104,6 +117,15 @@ const patchAssistant = async (req, res) => { } const updatedAssistant = await openai.beta.assistants.update(assistant_id, updateData); + + if (conversation_starters !== undefined) { + const conversationStartersUpdate = await updateAssistantDoc( + { assistant_id }, + { conversation_starters }, + ); + updatedAssistant.conversation_starters = conversationStartersUpdate.conversation_starters; + } + res.json(updatedAssistant); } catch (error) { logger.error('[/assistants/:id] Error updating assistant', error); @@ -153,6 +175,32 @@ const listAssistants = async (req, res) => { } }; +/** + * Filter assistants based on configuration. + * + * @param {object} params - The parameters object. + * @param {string} params.userId - The user ID to filter private assistants. + * @param {AssistantDocument[]} params.assistants - The list of assistants to filter. + * @param {Partial} [params.assistantsConfig] - The assistant configuration. + * @returns {AssistantDocument[]} - The filtered list of assistants. + */ +function filterAssistantDocs({ documents, userId, assistantsConfig = {} }) { + const { supportedIds, excludedIds, privateAssistants } = assistantsConfig; + const removeUserId = (doc) => { + const { user: _u, ...document } = doc; + return document; + }; + + if (privateAssistants) { + return documents.filter((doc) => userId === doc.user.toString()).map(removeUserId); + } else if (supportedIds?.length) { + return documents.filter((doc) => supportedIds.includes(doc.assistant_id)).map(removeUserId); + } else if (excludedIds?.length) { + return documents.filter((doc) => !excludedIds.includes(doc.assistant_id)).map(removeUserId); + } + return documents.map(removeUserId); +} + /** * Returns a list of the user's assistant documents (metadata saved to database). * @route GET /assistants/documents @@ -160,7 +208,25 @@ const listAssistants = async (req, res) => { */ const getAssistantDocuments = async (req, res) => { try { - res.json(await getAssistants({ user: req.user.id })); + const endpoint = req.query; + const assistantsConfig = req.app.locals[endpoint]; + const documents = await getAssistants( + {}, + { + user: 1, + assistant_id: 1, + conversation_starters: 1, + createdAt: 1, + updatedAt: 1, + }, + ); + + const docs = filterAssistantDocs({ + documents, + userId: req.user.id, + assistantsConfig, + }); + res.json(docs); } catch (error) { logger.error('[/assistants/documents] Error listing assistant documents', error); res.status(500).json({ error: error.message }); diff --git a/api/server/controllers/assistants/v2.js b/api/server/controllers/assistants/v2.js index 82608e428..0e1b4a641 100644 --- a/api/server/controllers/assistants/v2.js +++ b/api/server/controllers/assistants/v2.js @@ -16,7 +16,9 @@ const createAssistant = async (req, res) => { /** @type {{ openai: OpenAIClient }} */ const { openai } = await getOpenAIClient({ req, res }); - const { tools = [], endpoint, ...assistantData } = req.body; + const { tools = [], endpoint, conversation_starters, ...assistantData } = req.body; + delete assistantData.conversation_starters; + assistantData.tools = tools .map((tool) => { if (typeof tool !== 'string') { @@ -39,11 +41,22 @@ const createAssistant = async (req, res) => { }; const assistant = await openai.beta.assistants.create(assistantData); - const promise = updateAssistantDoc({ assistant_id: assistant.id }, { user: req.user.id }); + + const createData = { user: req.user.id }; + if (conversation_starters) { + createData.conversation_starters = conversation_starters; + } + + const document = await updateAssistantDoc({ assistant_id: assistant.id }, createData); + if (azureModelIdentifier) { assistant.model = azureModelIdentifier; } - await promise; + + if (document.conversation_starters) { + assistant.conversation_starters = document.conversation_starters; + } + logger.debug('/assistants/', assistant); res.status(201).json(assistant); } catch (error) { @@ -64,6 +77,17 @@ const createAssistant = async (req, res) => { const updateAssistant = async ({ req, openai, assistant_id, updateData }) => { await validateAuthor({ req, openai }); const tools = []; + let conversation_starters = null; + + if (updateData?.conversation_starters) { + const conversationStartersUpdate = await updateAssistantDoc( + { assistant_id: assistant_id }, + { conversation_starters: updateData.conversation_starters }, + ); + conversation_starters = conversationStartersUpdate.conversation_starters; + + delete updateData.conversation_starters; + } let hasFileSearch = false; for (const tool of updateData.tools ?? []) { @@ -108,7 +132,13 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => { updateData.model = openai.locals.azureOptions.azureOpenAIApiDeploymentName; } - return await openai.beta.assistants.update(assistant_id, updateData); + const assistant = await openai.beta.assistants.update(assistant_id, updateData); + + if (conversation_starters) { + assistant.conversation_starters = conversation_starters; + } + + return assistant; }; /** diff --git a/api/server/routes/assistants/documents.js b/api/server/routes/assistants/documents.js new file mode 100644 index 000000000..72a81d8b4 --- /dev/null +++ b/api/server/routes/assistants/documents.js @@ -0,0 +1,13 @@ +const express = require('express'); +const controllers = require('~/server/controllers/assistants/v1'); + +const router = express.Router(); + +/** + * Returns a list of the user's assistant documents (metadata saved to database). + * @route GET /assistants/documents + * @returns {AssistantDocument[]} 200 - success response - application/json + */ +router.get('/', controllers.getAssistantDocuments); + +module.exports = router; diff --git a/api/server/routes/assistants/v1.js b/api/server/routes/assistants/v1.js index 184450887..8314c91d1 100644 --- a/api/server/routes/assistants/v1.js +++ b/api/server/routes/assistants/v1.js @@ -1,6 +1,7 @@ const multer = require('multer'); const express = require('express'); const controllers = require('~/server/controllers/assistants/v1'); +const documents = require('./documents'); const actions = require('./actions'); const tools = require('./tools'); @@ -20,6 +21,13 @@ router.use('/actions', actions); */ router.use('/tools', tools); +/** + * Create an assistant. + * @route GET /assistants/documents + * @returns {AssistantDocument[]} 200 - application/json + */ +router.use('/documents', documents); + /** * Create an assistant. * @route POST /assistants @@ -61,13 +69,6 @@ router.delete('/:id', controllers.deleteAssistant); */ router.get('/', controllers.listAssistants); -/** - * Returns a list of the user's assistant documents (metadata saved to database). - * @route GET /assistants/documents - * @returns {AssistantDocument[]} 200 - success response - application/json - */ -router.get('/documents', controllers.getAssistantDocuments); - /** * Uploads and updates an avatar for a specific assistant. * @route POST /avatar/:assistant_id diff --git a/api/server/routes/assistants/v2.js b/api/server/routes/assistants/v2.js index 3c70c623a..230bcc287 100644 --- a/api/server/routes/assistants/v2.js +++ b/api/server/routes/assistants/v2.js @@ -2,6 +2,7 @@ const multer = require('multer'); const express = require('express'); const v1 = require('~/server/controllers/assistants/v1'); const v2 = require('~/server/controllers/assistants/v2'); +const documents = require('./documents'); const actions = require('./actions'); const tools = require('./tools'); @@ -21,6 +22,13 @@ router.use('/actions', actions); */ router.use('/tools', tools); +/** + * Create an assistant. + * @route GET /assistants/documents + * @returns {AssistantDocument[]} 200 - application/json + */ +router.use('/documents', documents); + /** * Create an assistant. * @route POST /assistants @@ -62,13 +70,6 @@ router.delete('/:id', v1.deleteAssistant); */ router.get('/', v1.listAssistants); -/** - * Returns a list of the user's assistant documents (metadata saved to database). - * @route GET /assistants/documents - * @returns {AssistantDocument[]} 200 - success response - application/json - */ -router.get('/documents', v1.getAssistantDocuments); - /** * Uploads and updates an avatar for a specific assistant. * @route POST /avatar/:assistant_id diff --git a/client/src/common/assistants-types.ts b/client/src/common/assistants-types.ts index e4edf025e..a4c077d96 100644 --- a/client/src/common/assistants-types.ts +++ b/client/src/common/assistants-types.ts @@ -22,6 +22,7 @@ export type AssistantForm = { name: string | null; description: string | null; instructions: string | null; + conversation_starters: string[]; model: string; functions: string[]; } & Actions; diff --git a/client/src/common/types.ts b/client/src/common/types.ts index 5e00aad82..33d24b5fb 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -18,6 +18,7 @@ import type { TConversation, TStartupConfig, EModelEndpoint, + ActionMetadata, AssistantsEndpoint, TMessageContentParts, AuthorizationTypeEnum, @@ -146,9 +147,13 @@ export type ActionAuthForm = { token_exchange_method: TokenExchangeMethodEnum; }; +export type ActionWithNullableMetadata = Omit & { + metadata: ActionMetadata | null; +}; + export type AssistantPanelProps = { index?: number; - action?: Action; + action?: ActionWithNullableMetadata; actions?: Action[]; assistant_id?: string; activePanel?: string; diff --git a/client/src/components/Bookmarks/DeleteBookmarkButton.tsx b/client/src/components/Bookmarks/DeleteBookmarkButton.tsx index e401f8884..1e422659a 100644 --- a/client/src/components/Bookmarks/DeleteBookmarkButton.tsx +++ b/client/src/components/Bookmarks/DeleteBookmarkButton.tsx @@ -46,7 +46,7 @@ const DeleteBookmarkButton: FC<{ } confirm={confirmDelete} - className="transition-color flex size-7 items-center justify-center rounded-lg duration-200 hover:bg-surface-hover" + className="transition-colors flex size-7 items-center justify-center rounded-lg duration-200 hover:bg-surface-hover" icon={} tabIndex={tabIndex} onFocus={onFocus} diff --git a/client/src/components/Bookmarks/EditBookmarkButton.tsx b/client/src/components/Bookmarks/EditBookmarkButton.tsx index 4955bf557..78a3a96a7 100644 --- a/client/src/components/Bookmarks/EditBookmarkButton.tsx +++ b/client/src/components/Bookmarks/EditBookmarkButton.tsx @@ -25,7 +25,7 @@ const EditBookmarkButton: FC<{ /> + ); +} diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index b89281d27..719d9681f 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -136,7 +136,7 @@ const ChatForm = ({ index = 0 }) => { newConversation={generateConversation} textAreaRef={textAreaRef} commandChar="+" - placeholder="com_ui_add" + placeholder="com_ui_add_model_preset" includeAssistants={false} /> )} diff --git a/client/src/components/Chat/Landing.tsx b/client/src/components/Chat/Landing.tsx index 0faa6d88d..4b6fe03bf 100644 --- a/client/src/components/Chat/Landing.tsx +++ b/client/src/components/Chat/Landing.tsx @@ -1,12 +1,15 @@ -import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider'; +import { useMemo } from 'react'; +import { EModelEndpoint, isAssistantsEndpoint, Constants } from 'librechat-data-provider'; import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query'; import type { ReactNode } from 'react'; import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui'; import { useChatContext, useAssistantsMapContext } from '~/Providers'; +import { useGetAssistantDocsQuery } from '~/data-provider'; import ConvoIcon from '~/components/Endpoints/ConvoIcon'; +import { useLocalize, useSubmitMessage } from '~/hooks'; import { BirthdayIcon } from '~/components/svg'; import { getIconEndpoint, cn } from '~/utils'; -import { useLocalize } from '~/hooks'; +import ConvoStarter from './ConvoStarter'; export default function Landing({ Header }: { Header?: ReactNode }) { const { conversation } = useChatContext(); @@ -29,21 +32,36 @@ export default function Landing({ Header }: { Header?: ReactNode }) { const iconURL = conversation?.iconURL; endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint }); + const { data: documentsMap = new Map() } = useGetAssistantDocsQuery(endpoint, { + select: (data) => new Map(data.map((dbA) => [dbA.assistant_id, dbA])), + }); const isAssistant = isAssistantsEndpoint(endpoint); const assistant = isAssistant ? assistantMap?.[endpoint][assistant_id ?? ''] : undefined; - const assistantName = assistant && assistant.name; - const assistantDesc = assistant && assistant.description; - const avatar = assistant && (assistant.metadata?.avatar as string); + const assistantName = assistant?.name ?? ''; + const assistantDesc = assistant?.description ?? ''; + const avatar = assistant?.metadata?.avatar ?? ''; + const conversation_starters = useMemo(() => { + /* The user made updates, use client-side cache, */ + if (assistant?.conversation_starters) { + return assistant.conversation_starters; + } + /* If none in cache, we use the latest assistant docs */ + const assistantDocs = documentsMap.get(assistant_id ?? ''); + return assistantDocs?.conversation_starters ?? []; + }, [documentsMap, assistant_id, assistant?.conversation_starters]); const containerClassName = 'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black'; + const { submitMessage } = useSubmitMessage(); + const sendConversationStarter = (text: string) => submitMessage({ text }); + return (
-
{Header && Header}
+
{Header != null ? Header : null}
- {!!startupConfig?.showBirthdayIcon && ( + {startupConfig?.showBirthdayIcon === true ? (
@@ -64,14 +82,14 @@ export default function Landing({ Header }: { Header?: ReactNode }) { {localize('com_ui_happy_birthday')}
- )} + ) : null}
{assistantName ? (
{assistantName}
-
+
{assistantDesc ? assistantDesc : localize('com_nav_welcome_message')}
{/*
@@ -85,6 +103,18 @@ export default function Landing({ Header }: { Header?: ReactNode }) { : conversation?.greeting ?? localize('com_nav_welcome_message')} )} +
+ {conversation_starters.length > 0 && + conversation_starters + .slice(0, Constants.MAX_CONVO_STARTERS) + .map((text, index) => ( + sendConversationStarter(text)} + /> + ))} +
diff --git a/client/src/components/Chat/PromptCard.tsx b/client/src/components/Chat/PromptCard.tsx index cdacc45f9..e348b24d2 100644 --- a/client/src/components/Chat/PromptCard.tsx +++ b/client/src/components/Chat/PromptCard.tsx @@ -3,7 +3,7 @@ import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon'; export default function PromptCard({ promptGroup }: { promptGroup: TPromptGroup }) { return ( -
+
diff --git a/client/src/components/Chat/PromptLanding.tsx b/client/src/components/Chat/PromptLanding.tsx deleted file mode 100644 index 6e78cdd39..000000000 --- a/client/src/components/Chat/PromptLanding.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; -import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider'; -import type { ReactNode } from 'react'; -import { useChatContext, useAssistantsMapContext } from '~/Providers'; -import { TooltipProvider, Tooltip } from '~/components/ui'; -import ConvoIcon from '~/components/Endpoints/ConvoIcon'; -import { getIconEndpoint, cn } from '~/utils'; -import Prompts from './Prompts'; - -export default function Landing({ Header }: { Header?: ReactNode }) { - const { conversation } = useChatContext(); - const assistantMap = useAssistantsMapContext(); - const { data: endpointsConfig } = useGetEndpointsQuery(); - - let { endpoint = '' } = conversation ?? {}; - const { assistant_id = null } = conversation ?? {}; - - if ( - endpoint === EModelEndpoint.chatGPTBrowser || - endpoint === EModelEndpoint.azureOpenAI || - endpoint === EModelEndpoint.gptPlugins - ) { - endpoint = EModelEndpoint.openAI; - } - - const iconURL = conversation?.iconURL; - endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint }); - - const isAssistant = isAssistantsEndpoint(endpoint); - const assistant = isAssistant && assistantMap?.[endpoint]?.[assistant_id ?? '']; - const assistantName = (assistant && assistant?.name) || ''; - const avatar = (assistant && (assistant?.metadata?.avatar as string)) || ''; - - const containerClassName = - 'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black'; - - return ( - - -
-
{Header && Header}
-
-
- -
-
- -
-
-
-
-
- ); -} diff --git a/client/src/components/Conversations/Convo.tsx b/client/src/components/Conversations/Convo.tsx index 8cb51c7f6..2dd608bc9 100644 --- a/client/src/components/Conversations/Convo.tsx +++ b/client/src/components/Conversations/Convo.tsx @@ -149,10 +149,10 @@ export default function Conversation({ />
diff --git a/client/src/components/SidePanel/Builder/ActionsInput.tsx b/client/src/components/SidePanel/Builder/ActionsInput.tsx index 741fa68de..1690ac049 100644 --- a/client/src/components/SidePanel/Builder/ActionsInput.tsx +++ b/client/src/components/SidePanel/Builder/ActionsInput.tsx @@ -13,7 +13,7 @@ import type { ValidationResult, AssistantsEndpoint, } from 'librechat-data-provider'; -import type { ActionAuthForm } from '~/common'; +import type { ActionAuthForm, ActionWithNullableMetadata } from '~/common'; import type { Spec } from './ActionsTable'; import { useAssistantsMapContext, useToastContext } from '~/Providers'; import { ActionsTable, columns } from './ActionsTable'; @@ -37,7 +37,7 @@ export default function ActionsInput({ version, setAction, }: { - action?: Action; + action?: ActionWithNullableMetadata; assistant_id?: string; endpoint: AssistantsEndpoint; version: number | string; @@ -62,12 +62,13 @@ export default function ActionsInput({ const [functions, setFunctions] = useState(null); useEffect(() => { - if (!action?.metadata.raw_spec) { + const rawSpec = action?.metadata?.raw_spec ?? ''; + if (!rawSpec) { return; } - setInputValue(action.metadata.raw_spec); - debouncedValidation(action.metadata.raw_spec, handleResult); - }, [action?.metadata.raw_spec]); + setInputValue(rawSpec); + debouncedValidation(rawSpec, handleResult); + }, [action?.metadata?.raw_spec]); useEffect(() => { if (!validationResult || !validationResult.status || !validationResult.spec) { @@ -100,7 +101,8 @@ export default function ActionsInput({ }, onError(error) { showToast({ - message: (error as Error).message ?? localize('com_assistants_update_actions_error'), + message: + (error as Error | undefined)?.message ?? localize('com_assistants_update_actions_error'), status: 'error', }); }, @@ -108,7 +110,8 @@ export default function ActionsInput({ const saveAction = handleSubmit((authFormData) => { console.log('authFormData', authFormData); - if (!assistant_id) { + const currentAssistantId = assistant_id ?? ''; + if (!currentAssistantId) { // alert user? return; } @@ -121,7 +124,10 @@ export default function ActionsInput({ return; } - let { metadata = {} } = action ?? {}; + let { metadata } = action ?? {}; + if (!metadata) { + metadata = {}; + } const action_id = action?.action_id; metadata.raw_spec = inputValue; const parsedUrl = new URL(data[0].domain); @@ -177,10 +183,10 @@ export default function ActionsInput({ action_id, metadata, functions, - assistant_id, + assistant_id: currentAssistantId, endpoint, version, - model: assistantMap?.[endpoint][assistant_id].model ?? '', + model: assistantMap?.[endpoint][currentAssistantId].model ?? '', }); }); diff --git a/client/src/components/SidePanel/Builder/ActionsPanel.tsx b/client/src/components/SidePanel/Builder/ActionsPanel.tsx index 9c4df3e2f..b7a6d8fcd 100644 --- a/client/src/components/SidePanel/Builder/ActionsPanel.tsx +++ b/client/src/components/SidePanel/Builder/ActionsPanel.tsx @@ -40,7 +40,8 @@ export default function ActionsPanel({ }, onError(error) { showToast({ - message: (error as Error)?.message ?? localize('com_assistants_delete_actions_error'), + message: + (error as Error | undefined)?.message ?? localize('com_assistants_delete_actions_error'), status: 'error', }); }, @@ -127,7 +128,7 @@ export default function ActionsPanel({
diff --git a/client/src/components/SidePanel/Builder/AssistantConversationStarters.tsx b/client/src/components/SidePanel/Builder/AssistantConversationStarters.tsx new file mode 100644 index 000000000..9d673044a --- /dev/null +++ b/client/src/components/SidePanel/Builder/AssistantConversationStarters.tsx @@ -0,0 +1,169 @@ +import React, { useRef, useState } from 'react'; +import { Plus, X } from 'lucide-react'; +import { Transition } from 'react-transition-group'; +import { Constants } from 'librechat-data-provider'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '~/components/ui'; +import { useLocalize } from '~/hooks'; + +interface AssistantConversationStartersProps { + field: { + value: string[]; + onChange: (value: string[]) => void; + }; + inputClass: string; + labelClass: string; +} + +const AssistantConversationStarters: React.FC = ({ + field, + inputClass, + labelClass, +}) => { + const localize = useLocalize(); + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + const nodeRef = useRef(null); + const [newStarter, setNewStarter] = useState(''); + + const handleAddStarter = () => { + if (newStarter.trim() && field.value.length < Constants.MAX_CONVO_STARTERS) { + const newValues = [newStarter, ...field.value]; + field.onChange(newValues); + setNewStarter(''); + } + }; + + const handleDeleteStarter = (index: number) => { + const newValues = field.value.filter((_, i) => i !== index); + field.onChange(newValues); + }; + const defaultStyle = { + transition: 'opacity 200ms ease-in-out', + opacity: 0, + }; + + const triggerShake = (element: HTMLElement) => { + element.classList.remove('shake'); + void element.offsetWidth; + element.classList.add('shake'); + setTimeout(() => { + element.classList.remove('shake'); + }, 200); + }; + + const transitionStyles = { + entering: { opacity: 1 }, + entered: { opacity: 1 }, + exiting: { opacity: 0 }, + exited: { opacity: 0 }, + }; + + const hasReachedMax = field.value.length >= Constants.MAX_CONVO_STARTERS; + + return ( +
+ +
+ {/* Persistent starter, used for creating only */} +
+ (inputRefs.current[0] = el)} + value={newStarter} + maxLength={64} + className={`${inputClass} pr-10`} + type="text" + placeholder={ + hasReachedMax + ? localize('com_assistants_max_starters_reached') + : localize('com_assistants_conversation_starters_placeholder') + } + onChange={(e) => setNewStarter(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + if (hasReachedMax) { + triggerShake(e.currentTarget); + } else { + handleAddStarter(); + } + } + }} + /> + + {(state: string) => ( +
+ + + + + + + {hasReachedMax + ? localize('com_assistants_max_starters_reached') + : localize('com_ui_add')} + + + +
+ )} +
+
+ {field.value.map((starter, index) => ( +
+ (inputRefs.current[index + 1] = el)} + value={starter} + onChange={(e) => { + const newValue = [...field.value]; + newValue[index] = e.target.value; + field.onChange(newValue); + }} + className={`${inputClass} pr-10`} + type="text" + maxLength={64} + /> + + + + + + + {localize('com_ui_delete')} + + + +
+ ))} +
+
+ ); +}; + +export default AssistantConversationStarters; diff --git a/client/src/components/SidePanel/Builder/AssistantPanel.tsx b/client/src/components/SidePanel/Builder/AssistantPanel.tsx index 879c69d17..9d4fe8096 100644 --- a/client/src/components/SidePanel/Builder/AssistantPanel.tsx +++ b/client/src/components/SidePanel/Builder/AssistantPanel.tsx @@ -14,6 +14,7 @@ import type { FunctionTool, TConfig, TPlugin } from 'librechat-data-provider'; import type { AssistantForm, AssistantPanelProps } from '~/common'; import { useCreateAssistantMutation, useUpdateAssistantMutation } from '~/data-provider'; import { cn, cardStyle, defaultTextProps, removeFocusOutlines } from '~/utils'; +import AssistantConversationStarters from './AssistantConversationStarters'; import { useAssistantsMapContext, useToastContext } from '~/Providers'; import { useSelectAssistant, useLocalize } from '~/hooks'; import { ToolSelectDialog } from '~/components/Tools'; @@ -31,7 +32,7 @@ import { Panel } from '~/common'; const labelClass = 'mb-2 text-token-text-primary block font-medium'; const inputClass = cn( defaultTextProps, - 'flex w-full px-3 py-2 dark:border-gray-800 dark:bg-gray-800', + 'flex w-full px-3 py-2 dark:border-gray-800 dark:bg-gray-800 rounded-xl mb-2', removeFocusOutlines, ); @@ -106,6 +107,7 @@ export default function AssistantPanel({ }); }, }); + const create = useCreateAssistantMutation({ onSuccess: (data) => { setCurrentAssistantId(data.id); @@ -139,7 +141,7 @@ export default function AssistantPanel({ return functionName; } else { const assistant = assistantMap?.[endpoint]?.[assistant_id]; - const tool = assistant?.tools.find((tool) => tool.function?.name === functionName); + const tool = assistant?.tools?.find((tool) => tool.function?.name === functionName); if (assistant && tool) { return tool; } @@ -148,7 +150,6 @@ export default function AssistantPanel({ return functionName; }); - console.log(data); if (data.code_interpreter) { tools.push({ type: Tools.code_interpreter }); } @@ -163,6 +164,7 @@ export default function AssistantPanel({ name, description, instructions, + conversation_starters: starters, model, // file_ids, // TODO: add file handling here } = data; @@ -174,6 +176,7 @@ export default function AssistantPanel({ name, description, instructions, + conversation_starters: starters.filter((starter) => starter.trim() !== ''), model, tools, endpoint, @@ -186,6 +189,7 @@ export default function AssistantPanel({ name, description, instructions, + conversation_starters: starters.filter((starter) => starter.trim() !== ''), model, tools, endpoint, @@ -239,12 +243,12 @@ export default function AssistantPanel({ )}
-
+
{/* Avatar & Name */}
( -

{field.value ?? ''}

+

{field.value}

)} />
@@ -318,6 +322,23 @@ export default function AssistantPanel({ )} />
+ + {/* Conversation Starters */} +
+ {/* the label of conversation starters is in the component */} + ( + + )} + /> +
{/* Model */}