From 551d767991b6eee581d8cbd5a5193e46b7437ba7 Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Fri, 14 Feb 2025 11:17:00 +0100 Subject: [PATCH 1/3] feat: Custom Welcome Message (#2967) --- .env.example | 1 + api/server/routes/config.js | 4 + client/src/components/Chat/Landing.tsx | 106 +++++++++++++------------ packages/data-provider/src/config.ts | 1 + 4 files changed, 61 insertions(+), 51 deletions(-) diff --git a/.env.example b/.env.example index d87021ea4b3..9597c0d8d90 100644 --- a/.env.example +++ b/.env.example @@ -487,6 +487,7 @@ ALLOW_SHARED_LINKS_PUBLIC=true #===================================================# APP_TITLE=LibreChat +# CUSTOM_WELCOME_MESSAGE="My custom welcome message" # CUSTOM_FOOTER="My custom footer" HELP_AND_FAQ_URL=https://librechat.ai diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 705a1d3cb1c..bfab77d3239 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -90,6 +90,10 @@ router.get('/', async function (req, res) { payload.customFooter = process.env.CUSTOM_FOOTER; } + if (typeof process.env.CUSTOM_WELCOME_MESSAGE === 'string') { + payload.customWelcomeMessage = process.env.CUSTOM_WELCOME_MESSAGE; + } + await cache.set(CacheKeys.STARTUP_CONFIG, payload); return res.status(200).send(payload); } catch (err) { diff --git a/client/src/components/Chat/Landing.tsx b/client/src/components/Chat/Landing.tsx index bfb1a34b692..aab3aec726c 100644 --- a/client/src/components/Chat/Landing.tsx +++ b/client/src/components/Chat/Landing.tsx @@ -21,25 +21,35 @@ export default function Landing({ Header }: { Header?: ReactNode }) { const assistantMap = useAssistantsMapContext(); const { data: startupConfig } = useGetStartupConfig(); const { data: endpointsConfig } = useGetEndpointsQuery(); - const localize = useLocalize(); + const { submitMessage } = useSubmitMessage(); - let { endpoint = '' } = conversation ?? {}; + // Normalize the endpoint (override certain endpoints to use openAI) + const rawEndpoint = conversation?.endpoint || ''; + const normalizedEndpoint = useMemo(() => { + if ( + rawEndpoint === EModelEndpoint.chatGPTBrowser || + rawEndpoint === EModelEndpoint.azureOpenAI || + rawEndpoint === EModelEndpoint.gptPlugins + ) { + return EModelEndpoint.openAI; + } + return rawEndpoint; + }, [rawEndpoint]); - if ( - endpoint === EModelEndpoint.chatGPTBrowser || - endpoint === EModelEndpoint.azureOpenAI || - endpoint === EModelEndpoint.gptPlugins - ) { - endpoint = EModelEndpoint.openAI; - } + // Compute the endpoint with the icon considerations + const endpoint = getIconEndpoint({ + endpointsConfig, + iconURL: conversation?.iconURL, + endpoint: normalizedEndpoint, + }); - const iconURL = conversation?.iconURL; - endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint }); + // Fetch assistant documents mapped by assistant_id const { data: documentsMap = new Map() } = useGetAssistantDocsQuery(endpoint, { select: (data) => new Map(data.map((dbA) => [dbA.assistant_id, dbA])), }); + // Determine the active entity (Agent/Assistant) based on the conversation const { entity, isAgent, isAssistant } = getEntity({ endpoint, agentsMap, @@ -48,51 +58,41 @@ export default function Landing({ Header }: { Header?: ReactNode }) { assistant_id: conversation?.assistant_id, }); - const name = entity?.name ?? ''; - const description = entity?.description ?? ''; - const avatar = isAgent - ? (entity as t.Agent | undefined)?.avatar?.filepath ?? '' - : ((entity as t.Assistant | undefined)?.metadata?.avatar as string | undefined) ?? ''; - const conversation_starters = useMemo(() => { - /* The user made updates, use client-side cache, or they exist in an Agent */ - if (entity && (entity.conversation_starters?.length ?? 0) > 0) { - return entity.conversation_starters; - } - if (isAgent) { - return entity?.conversation_starters ?? []; - } + const name = entity?.name || ''; + const description = entity?.description || ''; + const avatar = + isAgent + ? (entity as t.Agent | undefined)?.avatar?.filepath ?? '' + : ((entity as t.Assistant | undefined)?.metadata?.avatar as string | undefined) ?? ''; - /* If none in cache, we use the latest assistant docs */ - const entityDocs = documentsMap.get(entity?.id ?? ''); - return entityDocs?.conversation_starters ?? []; + // Compute conversation starters + const conversationStarters = useMemo(() => { + if (entity) { + if (entity.conversation_starters?.length) {return entity.conversation_starters;} + if (isAgent) {return entity.conversation_starters ?? [];} + const entityDocs = documentsMap.get(entity?.id ?? ''); + return entityDocs?.conversation_starters ?? []; + } + return []; }, [documentsMap, isAgent, entity]); 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 }); const getWelcomeMessage = () => { - const greeting = conversation?.greeting ?? ''; - if (greeting) { - return greeting; - } - - if (isAssistant) { - return localize('com_nav_welcome_assistant'); - } - - if (isAgent) { - return localize('com_nav_welcome_agent'); - } - - return localize('com_nav_welcome_message'); + if (conversation?.greeting) {return conversation.greeting;} + if (isAssistant) {return localize('com_nav_welcome_assistant');} + if (isAgent) {return localize('com_nav_welcome_agent');} + return typeof startupConfig?.customWelcomeMessage === 'string' + ? startupConfig.customWelcomeMessage + : localize('com_nav_welcome_message'); }; return (
-
{Header != null ? Header : null}
+
{Header || null}
- {startupConfig?.showBirthdayIcon === true ? ( + {startupConfig?.showBirthdayIcon === true && ( - ) : null} + )}
{name ? (
{name}
-
- {description ? description : localize('com_nav_welcome_message')} +
+ {description || + (typeof startupConfig?.customWelcomeMessage === 'string' + ? startupConfig.customWelcomeMessage + : localize('com_nav_welcome_message'))}
{/*
By Daniel Avila
@@ -129,9 +132,9 @@ export default function Landing({ Header }: { Header?: ReactNode }) { {getWelcomeMessage()} )} -
- {conversation_starters.length > 0 && - conversation_starters + {conversationStarters.length > 0 && ( +
+ {conversationStarters .slice(0, Constants.MAX_CONVO_STARTERS) .map((text: string, index: number) => ( sendConversationStarter(text)} /> ))} -
+
+ )}
); diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 82137e61576..860d08e5636 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -501,6 +501,7 @@ export type TStartupConfig = { showBirthdayIcon: boolean; helpAndFaqURL: string; customFooter?: string; + customWelcomeMessage?: string; modelSpecs?: TSpecsConfig; sharedLinksEnabled: boolean; publicSharedLinksEnabled: boolean; From df872767a65f8b793491ca1284bbb8c63588b825 Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Fri, 14 Feb 2025 17:14:47 +0100 Subject: [PATCH 2/3] don't think I'm on the right path? --- .env.example | 1 - client/src/components/Chat/Landing.tsx | 19 +++++++++++++------ librechat.example.yaml | 8 +++++--- packages/data-provider/src/config.ts | 2 +- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index 9597c0d8d90..d87021ea4b3 100644 --- a/.env.example +++ b/.env.example @@ -487,7 +487,6 @@ ALLOW_SHARED_LINKS_PUBLIC=true #===================================================# APP_TITLE=LibreChat -# CUSTOM_WELCOME_MESSAGE="My custom welcome message" # CUSTOM_FOOTER="My custom footer" HELP_AND_FAQ_URL=https://librechat.ai diff --git a/client/src/components/Chat/Landing.tsx b/client/src/components/Chat/Landing.tsx index aab3aec726c..ad332ee2425 100644 --- a/client/src/components/Chat/Landing.tsx +++ b/client/src/components/Chat/Landing.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { EModelEndpoint, Constants } from 'librechat-data-provider'; +import { EModelEndpoint, Constants, getConfigDefaults } from 'librechat-data-provider'; import type * as t from 'librechat-data-provider'; import type { ReactNode } from 'react'; import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers'; @@ -15,11 +15,17 @@ import { TooltipAnchor } from '~/components/ui'; import { BirthdayIcon } from '~/components/svg'; import ConvoStarter from './ConvoStarter'; +const defaultInterface = getConfigDefaults().interface; + export default function Landing({ Header }: { Header?: ReactNode }) { const { conversation } = useChatContext(); const agentsMap = useAgentsMapContext(); const assistantMap = useAssistantsMapContext(); const { data: startupConfig } = useGetStartupConfig(); + const interfaceConfig = useMemo( + () => startupConfig?.interface ?? defaultInterface, + [startupConfig], + ); const { data: endpointsConfig } = useGetEndpointsQuery(); const localize = useLocalize(); const { submitMessage } = useSubmitMessage(); @@ -85,9 +91,10 @@ export default function Landing({ Header }: { Header?: ReactNode }) { if (conversation?.greeting) {return conversation.greeting;} if (isAssistant) {return localize('com_nav_welcome_assistant');} if (isAgent) {return localize('com_nav_welcome_agent');} - return typeof startupConfig?.customWelcomeMessage === 'string' - ? startupConfig.customWelcomeMessage - : localize('com_nav_welcome_message'); + return interfaceConfig.customWelcome; + // return typeof interfaceConfig?.customWelcome === 'string' + // ? interfaceConfig.customWelcome + // : localize('com_nav_welcome_message'); }; return ( @@ -119,8 +126,8 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
{name}
{description || - (typeof startupConfig?.customWelcomeMessage === 'string' - ? startupConfig.customWelcomeMessage + (typeof interfaceConfig?.customWelcome === 'string' + ? interfaceConfig.customWelcome : localize('com_nav_welcome_message'))}
{/*
diff --git a/librechat.example.yaml b/librechat.example.yaml index e49f9b37b3d..ca7a8473123 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -57,6 +57,8 @@ interface: By using the Website, you acknowledge that you have read these Terms of Service and agree to be bound by them. + customWelcome: "Welcome to LibreChat! Enjoy your experience." + endpointsMenu: true modelSelect: true parameters: true @@ -81,7 +83,7 @@ registration: # model: '' # voices: [''] -# +# # stt: # openai: # url: '' @@ -234,10 +236,10 @@ endpoints: # Recommended: Drop the stop parameter from the request as Openrouter models use a variety of stop tokens. dropParams: ['stop'] modelDisplayLabel: 'OpenRouter' - + # Portkey AI Example - name: "Portkey" - apiKey: "dummy" + apiKey: "dummy" baseURL: 'https://api.portkey.ai/v1' headers: x-portkey-api-key: '${PORTKEY_API_KEY}' diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 860d08e5636..f412cada727 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -446,6 +446,7 @@ export const intefaceSchema = z }) .optional(), termsOfService: termsOfServiceSchema.optional(), + customWelcome: z.string().optional(), endpointsMenu: z.boolean().optional(), modelSelect: z.boolean().optional(), parameters: z.boolean().optional(), @@ -501,7 +502,6 @@ export type TStartupConfig = { showBirthdayIcon: boolean; helpAndFaqURL: string; customFooter?: string; - customWelcomeMessage?: string; modelSpecs?: TSpecsConfig; sharedLinksEnabled: boolean; publicSharedLinksEnabled: boolean; From 75236d0015ceb679a27337954a60765af2f5322e Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Sat, 22 Feb 2025 15:26:09 +0100 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=A8=20feat:=20Implement=20custom=20we?= =?UTF-8?q?lcome=20message=20configuration=20in=20interface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server/routes/config.js | 4 - api/server/services/start/interface.js | 1 + client/src/components/Chat/Landing.tsx | 118 ++++++++++++------------- librechat.example.yaml | 3 +- 4 files changed, 58 insertions(+), 68 deletions(-) diff --git a/api/server/routes/config.js b/api/server/routes/config.js index bfab77d3239..705a1d3cb1c 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -90,10 +90,6 @@ router.get('/', async function (req, res) { payload.customFooter = process.env.CUSTOM_FOOTER; } - if (typeof process.env.CUSTOM_WELCOME_MESSAGE === 'string') { - payload.customWelcomeMessage = process.env.CUSTOM_WELCOME_MESSAGE; - } - await cache.set(CacheKeys.STARTUP_CONFIG, payload); return res.status(200).send(payload); } catch (err) { diff --git a/api/server/services/start/interface.js b/api/server/services/start/interface.js index 98bcb6858ef..bdb151ec349 100644 --- a/api/server/services/start/interface.js +++ b/api/server/services/start/interface.js @@ -34,6 +34,7 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol multiConvo: interfaceConfig?.multiConvo ?? defaults.multiConvo, agents: interfaceConfig?.agents ?? defaults.agents, temporaryChat: interfaceConfig?.temporaryChat ?? defaults.temporaryChat, + customWelcome: interfaceConfig?.customWelcome ?? defaults.customWelcome, }); await updateAccessPermissions(roleName, { diff --git a/client/src/components/Chat/Landing.tsx b/client/src/components/Chat/Landing.tsx index ad332ee2425..5e3ac9de35a 100644 --- a/client/src/components/Chat/Landing.tsx +++ b/client/src/components/Chat/Landing.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { EModelEndpoint, Constants, getConfigDefaults } from 'librechat-data-provider'; +import { EModelEndpoint, Constants } from 'librechat-data-provider'; import type * as t from 'librechat-data-provider'; import type { ReactNode } from 'react'; import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers'; @@ -15,47 +15,31 @@ import { TooltipAnchor } from '~/components/ui'; import { BirthdayIcon } from '~/components/svg'; import ConvoStarter from './ConvoStarter'; -const defaultInterface = getConfigDefaults().interface; - export default function Landing({ Header }: { Header?: ReactNode }) { const { conversation } = useChatContext(); const agentsMap = useAgentsMapContext(); const assistantMap = useAssistantsMapContext(); const { data: startupConfig } = useGetStartupConfig(); - const interfaceConfig = useMemo( - () => startupConfig?.interface ?? defaultInterface, - [startupConfig], - ); const { data: endpointsConfig } = useGetEndpointsQuery(); + const localize = useLocalize(); - const { submitMessage } = useSubmitMessage(); - // Normalize the endpoint (override certain endpoints to use openAI) - const rawEndpoint = conversation?.endpoint || ''; - const normalizedEndpoint = useMemo(() => { - if ( - rawEndpoint === EModelEndpoint.chatGPTBrowser || - rawEndpoint === EModelEndpoint.azureOpenAI || - rawEndpoint === EModelEndpoint.gptPlugins - ) { - return EModelEndpoint.openAI; - } - return rawEndpoint; - }, [rawEndpoint]); + let { endpoint = '' } = conversation ?? {}; - // Compute the endpoint with the icon considerations - const endpoint = getIconEndpoint({ - endpointsConfig, - iconURL: conversation?.iconURL, - endpoint: normalizedEndpoint, - }); + if ( + endpoint === EModelEndpoint.chatGPTBrowser || + endpoint === EModelEndpoint.azureOpenAI || + endpoint === EModelEndpoint.gptPlugins + ) { + endpoint = EModelEndpoint.openAI; + } - // Fetch assistant documents mapped by assistant_id + 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])), }); - // Determine the active entity (Agent/Assistant) based on the conversation const { entity, isAgent, isAssistant } = getEntity({ endpoint, agentsMap, @@ -64,42 +48,53 @@ export default function Landing({ Header }: { Header?: ReactNode }) { assistant_id: conversation?.assistant_id, }); - const name = entity?.name || ''; - const description = entity?.description || ''; - const avatar = - isAgent - ? (entity as t.Agent | undefined)?.avatar?.filepath ?? '' - : ((entity as t.Assistant | undefined)?.metadata?.avatar as string | undefined) ?? ''; - - // Compute conversation starters - const conversationStarters = useMemo(() => { - if (entity) { - if (entity.conversation_starters?.length) {return entity.conversation_starters;} - if (isAgent) {return entity.conversation_starters ?? [];} - const entityDocs = documentsMap.get(entity?.id ?? ''); - return entityDocs?.conversation_starters ?? []; + const name = entity?.name ?? ''; + const description = entity?.description ?? ''; + const avatar = isAgent + ? (entity as t.Agent | undefined)?.avatar?.filepath ?? '' + : ((entity as t.Assistant | undefined)?.metadata?.avatar as string | undefined) ?? ''; + const conversation_starters = useMemo(() => { + /* The user made updates, use client-side cache, or they exist in an Agent */ + if (entity && (entity.conversation_starters?.length ?? 0) > 0) { + return entity.conversation_starters; + } + if (isAgent) { + return entity?.conversation_starters ?? []; } - return []; + + /* If none in cache, we use the latest assistant docs */ + const entityDocs = documentsMap.get(entity?.id ?? ''); + return entityDocs?.conversation_starters ?? []; }, [documentsMap, isAgent, entity]); 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 }); const getWelcomeMessage = () => { - if (conversation?.greeting) {return conversation.greeting;} - if (isAssistant) {return localize('com_nav_welcome_assistant');} - if (isAgent) {return localize('com_nav_welcome_agent');} - return interfaceConfig.customWelcome; - // return typeof interfaceConfig?.customWelcome === 'string' - // ? interfaceConfig.customWelcome - // : localize('com_nav_welcome_message'); + const greeting = conversation?.greeting ?? ''; + if (greeting) { + return greeting; + } + + if (isAssistant) { + return localize('com_nav_welcome_assistant'); + } + + if (isAgent) { + return localize('com_nav_welcome_agent'); + } + + return typeof startupConfig?.interface?.customWelcome === 'string' + ? startupConfig?.interface?.customWelcome + : localize('com_nav_welcome_message'); }; return (
-
{Header || null}
+
{Header != null ? Header : null}
- {startupConfig?.showBirthdayIcon === true && ( + {startupConfig?.showBirthdayIcon === true ? ( - )} + ) : null}
{name ? (
{name}
-
+
{description || - (typeof interfaceConfig?.customWelcome === 'string' - ? interfaceConfig.customWelcome + (typeof startupConfig?.interface?.customWelcome === 'string' + ? startupConfig?.interface?.customWelcome : localize('com_nav_welcome_message'))}
{/*
-
By Daniel Avila
+
By Daniel Avila
*/}
) : ( @@ -139,9 +134,9 @@ export default function Landing({ Header }: { Header?: ReactNode }) { {getWelcomeMessage()} )} - {conversationStarters.length > 0 && ( -
- {conversationStarters +
+ {conversation_starters.length > 0 && + conversation_starters .slice(0, Constants.MAX_CONVO_STARTERS) .map((text: string, index: number) => ( sendConversationStarter(text)} /> ))} -
- )} +
); diff --git a/librechat.example.yaml b/librechat.example.yaml index ca7a8473123..637e7a5219b 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -9,6 +9,7 @@ cache: true # Custom interface configuration interface: + customWelcome: "Welcome to LibreChat! Enjoy your experience." # Privacy policy settings privacyPolicy: externalUrl: 'https://librechat.ai/privacy-policy' @@ -57,8 +58,6 @@ interface: By using the Website, you acknowledge that you have read these Terms of Service and agree to be bound by them. - customWelcome: "Welcome to LibreChat! Enjoy your experience." - endpointsMenu: true modelSelect: true parameters: true