feat: Allow multiple welcome message configuration

This commit is contained in:
Joel Hirzel 2025-12-29 12:39:10 +01:00
parent d0835d5222
commit b144a649c0
3 changed files with 89 additions and 34 deletions

View file

@ -1,12 +1,15 @@
import { useMemo, useCallback, useState, useEffect, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { easings } from '@react-spring/web';
import { EModelEndpoint } from 'librechat-data-provider';
import type { TCustomWelcomeConfig } from 'librechat-data-provider';
import { BirthdayIcon, TooltipAnchor, SplitText } from '@librechat/client';
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
import { useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
import ConvoIcon from '~/components/Endpoints/ConvoIcon';
import { useLocalize, useAuthContext } from '~/hooks';
import { getIconEndpoint, getEntity } from '~/utils';
import store from '~/store';
const containerClassName =
'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white dark:bg-presentation dark:text-white text-black dark:after:shadow-none ';
@ -27,6 +30,17 @@ function getTextSizeClass(text: string | undefined | null) {
return 'text-lg sm:text-md';
}
function getMessagesForLanguage(
messages: Record<string, string[]> | undefined,
lang: string,
): string[] {
if (!messages) {
return [];
}
const baseLang = lang?.split('-')[0] ?? 'en';
return messages[lang] || messages[baseLang] || messages['en'] || Object.values(messages)[0] || [];
}
export default function Landing({ centerFormOnLanding }: { centerFormOnLanding: boolean }) {
const { conversation } = useChatContext();
const agentsMap = useAgentsMapContext();
@ -35,11 +49,13 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
const { data: endpointsConfig } = useGetEndpointsQuery();
const { user } = useAuthContext();
const localize = useLocalize();
const lang = useRecoilValue(store.lang);
const [textHasMultipleLines, setTextHasMultipleLines] = useState(false);
const [lineCount, setLineCount] = useState(1);
const [contentHeight, setContentHeight] = useState(0);
const contentRef = useRef<HTMLDivElement>(null);
const greetingIndexRef = useRef<number | null>(null);
const endpointType = useMemo(() => {
let ep = conversation?.endpoint ?? '';
@ -64,42 +80,66 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
const name = entity?.name ?? '';
const description = (entity?.description || conversation?.greeting) ?? '';
const getGreeting = useCallback(() => {
if (typeof startupConfig?.interface?.customWelcome === 'string') {
const customWelcome = startupConfig.interface.customWelcome;
// Replace {{user.name}} with actual user name if available
if (user?.name && customWelcome.includes('{{user.name}}')) {
return customWelcome.replace(/{{user.name}}/g, user.name);
}
return customWelcome;
}
const getTimeGreeting = useCallback(() => {
const now = new Date();
const hours = now.getHours();
const dayOfWeek = now.getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
// Early morning (midnight to 4:59 AM)
let timeGreeting: string;
if (hours >= 0 && hours < 5) {
return localize('com_ui_late_night');
timeGreeting = localize('com_ui_late_night');
} else if (hours < 12) {
timeGreeting = isWeekend
? localize('com_ui_weekend_morning')
: localize('com_ui_good_morning');
} else if (hours < 17) {
timeGreeting = localize('com_ui_good_afternoon');
} else {
timeGreeting = localize('com_ui_good_evening');
}
// Morning (6 AM to 11:59 AM)
else if (hours < 12) {
if (isWeekend) {
return localize('com_ui_weekend_morning');
}
return localize('com_ui_good_morning');
return timeGreeting + (user?.name ? ', ' + user.name : '');
}, [localize, user?.name]);
const replaceTemplateVariables = useCallback(
(message: string): string =>
user?.name ? message.replace(/{{user\.name}}/g, user.name) : message,
[user?.name],
);
const availableGreetings = useMemo(() => {
const customWelcome = startupConfig?.interface?.customWelcome;
if (!customWelcome) {
return [];
}
// Afternoon (12 PM to 4:59 PM)
else if (hours < 17) {
return localize('com_ui_good_afternoon');
if (typeof customWelcome === 'string') {
return [replaceTemplateVariables(customWelcome)];
}
// Evening (5 PM to 8:59 PM)
else {
return localize('com_ui_good_evening');
const config = customWelcome as TCustomWelcomeConfig;
const messages = getMessagesForLanguage(config.messages, lang).map(replaceTemplateVariables);
if (config.includeTimeGreetings) {
messages.push(getTimeGreeting());
}
}, [localize, startupConfig?.interface?.customWelcome, user?.name]);
return messages;
}, [startupConfig?.interface?.customWelcome, lang, replaceTemplateVariables, getTimeGreeting]);
const greetingText = useMemo(() => {
if (availableGreetings.length === 0) {
return getTimeGreeting();
}
if (
greetingIndexRef.current === null ||
greetingIndexRef.current >= availableGreetings.length
) {
greetingIndexRef.current = Math.floor(Math.random() * availableGreetings.length);
}
return availableGreetings[greetingIndexRef.current];
}, [availableGreetings, getTimeGreeting]);
const handleLineCountChange = useCallback((count: number) => {
setTextHasMultipleLines(count > 1);
@ -132,11 +172,6 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
return margin;
}, [lineCount, description, textHasMultipleLines, contentHeight]);
const greetingText =
typeof startupConfig?.interface?.customWelcome === 'string'
? getGreeting()
: getGreeting() + (user?.name ? ', ' + user.name : '');
return (
<div
className={`flex h-full transform-gpu flex-col items-center justify-center pb-16 transition-all duration-200 ${centerFormOnLanding ? 'max-h-full sm:max-h-0' : 'max-h-full'} ${getDynamicMargin}`}
@ -184,7 +219,7 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
</div>
) : (
<SplitText
key={`split-text-${greetingText}${user?.name ? '-user' : ''}`}
key={`split-text-${greetingText}`}
text={greetingText}
className={`${getTextSizeClass(greetingText)} font-medium text-text-primary`}
delay={50}

View file

@ -27,7 +27,18 @@ cache: true
# Custom interface configuration
interface:
customWelcome: 'Welcome to LibreChat! Enjoy your experience.'
customWelcome:
messages:
en:
- 'Welcome, {{user.name}}!'
- 'How can I help you today?'
es:
- 'Bienvenido, {{user.name}}!'
- 'En qué puedo ayudarte hoy?'
de:
- 'Willkommen, {{user.name}}!'
- 'Wie kann ich Ihnen heute helfen?'
includeTimeGreetings: true # Include "Good morning, {{username}}", "Good afternoon, {{username}}", etc.
# Enable/disable file search as a chatarea selection (default: true)
# Note: This setting does not disable the Agents File Search Capability.
# To disable the Agents Capability, see the Agents Endpoint configuration instead.

View file

@ -520,6 +520,15 @@ export type TTermsOfService = z.infer<typeof termsOfServiceSchema>;
const localizedStringSchema = z.union([z.string(), z.record(z.string())]);
export type LocalizedString = z.infer<typeof localizedStringSchema>;
const customWelcomeConfigSchema = z.object({
messages: z.record(z.array(z.string())).optional(),
includeTimeGreetings: z.boolean().optional().default(false),
});
export type TCustomWelcomeConfig = z.infer<typeof customWelcomeConfigSchema>;
// customWelcome can be a string (legacy) or a full config object with language-keyed messages
const customWelcomeSchema = z.union([z.string(), customWelcomeConfigSchema]);
const mcpServersSchema = z
.object({
placeholder: z.string().optional(),
@ -546,7 +555,7 @@ export const interfaceSchema = z
})
.optional(),
termsOfService: termsOfServiceSchema.optional(),
customWelcome: z.string().optional(),
customWelcome: customWelcomeSchema.optional(),
mcpServers: mcpServersSchema.optional(),
endpointsMenu: z.boolean().optional(),
modelSelect: z.boolean().optional(),