LibreChat/client/src/components/Chat/Landing.tsx
Danny Avila 550c7cc68a
🧭 refactor: Modernize Nav/Header (#7094)
* refactor: streamline model preset handling in conversation setup

* refactor: integrate navigation and location hooks in chat functions and event handlers, prevent cache from fetching on final event handling

* fix: prevent adding code interpreter non-image output to file list on message attachment event, fix all unhandled edge cases when this is done (treating the file download as an image attachment, undefined fields, message tokenCount issues, use of `startsWith` on undefined "text") although it is now prevent altogether

* chore: remove unused jailbreak prop from MinimalIcon component in EndpointIcon

* feat: add new SVG icons (MobileSidebar, Sidebar, XAIcon), fix: xAI styling in dark vs. light modes, adjust styling of Landing icons

* fix: open conversation in new tab on navigation with ctrl/meta key

* refactor: update Nav & Header to use close/open sidebar buttons, as well as redesign "New Chat"/"Bookmarks" buttons to the top of the Nav, matching the latest design of ChatGPT for simplicity and to free up space

* chore: remove unused isToggleHovering state and simplify opacity logic in Nav component

* style: match mobile nav to mobile header
2025-04-27 14:03:25 -04:00

214 lines
7.4 KiB
TypeScript

import { useMemo, useCallback, useState, useEffect, useRef } from 'react';
import { easings } from '@react-spring/web';
import { EModelEndpoint } from 'librechat-data-provider';
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
import { useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
import { BirthdayIcon, TooltipAnchor, SplitText } from '~/components';
import ConvoIcon from '~/components/Endpoints/ConvoIcon';
import { useLocalize, useAuthContext } from '~/hooks';
import { getIconEndpoint, getEntity } from '~/utils';
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 ';
function getTextSizeClass(text: string | undefined | null) {
if (!text) {
return 'text-xl sm:text-2xl';
}
if (text.length < 40) {
return 'text-2xl sm:text-4xl';
}
if (text.length < 70) {
return 'text-xl sm:text-2xl';
}
return 'text-lg sm:text-md';
}
export default function Landing({ centerFormOnLanding }: { centerFormOnLanding: boolean }) {
const { conversation } = useChatContext();
const agentsMap = useAgentsMapContext();
const assistantMap = useAssistantsMapContext();
const { data: startupConfig } = useGetStartupConfig();
const { data: endpointsConfig } = useGetEndpointsQuery();
const { user } = useAuthContext();
const localize = useLocalize();
const [textHasMultipleLines, setTextHasMultipleLines] = useState(false);
const [lineCount, setLineCount] = useState(1);
const [contentHeight, setContentHeight] = useState(0);
const contentRef = useRef<HTMLDivElement>(null);
const endpointType = useMemo(() => {
let ep = conversation?.endpoint ?? '';
if (
[
EModelEndpoint.chatGPTBrowser,
EModelEndpoint.azureOpenAI,
EModelEndpoint.gptPlugins,
].includes(ep as EModelEndpoint)
) {
ep = EModelEndpoint.openAI;
}
return getIconEndpoint({
endpointsConfig,
iconURL: conversation?.iconURL,
endpoint: ep,
});
}, [conversation?.endpoint, conversation?.iconURL, endpointsConfig]);
const { entity, isAgent, isAssistant } = getEntity({
endpoint: endpointType,
agentsMap,
assistantMap,
agent_id: conversation?.agent_id,
assistant_id: conversation?.assistant_id,
});
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 now = new Date();
const hours = now.getHours();
const dayOfWeek = now.getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
// Early morning (midnight to 4:59 AM)
if (hours >= 0 && hours < 5) {
return localize('com_ui_late_night');
}
// 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');
}
// Afternoon (12 PM to 4:59 PM)
else if (hours < 17) {
return localize('com_ui_good_afternoon');
}
// Evening (5 PM to 8:59 PM)
else {
return localize('com_ui_good_evening');
}
}, [localize, startupConfig?.interface?.customWelcome, user?.name]);
const handleLineCountChange = useCallback((count: number) => {
setTextHasMultipleLines(count > 1);
setLineCount(count);
}, []);
useEffect(() => {
if (contentRef.current) {
setContentHeight(contentRef.current.offsetHeight);
}
}, [lineCount, description]);
const getDynamicMargin = useMemo(() => {
let margin = 'mb-0';
if (lineCount > 2 || (description && description.length > 100)) {
margin = 'mb-10';
} else if (lineCount > 1 || (description && description.length > 0)) {
margin = 'mb-6';
} else if (textHasMultipleLines) {
margin = 'mb-4';
}
if (contentHeight > 200) {
margin = 'mb-16';
} else if (contentHeight > 150) {
margin = 'mb-12';
}
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}`}
>
<div ref={contentRef} className="flex flex-col items-center gap-0 p-2">
<div
className={`flex ${textHasMultipleLines ? 'flex-col' : 'flex-col md:flex-row'} items-center justify-center gap-2`}
>
<div className={`relative size-10 justify-center ${textHasMultipleLines ? 'mb-2' : ''}`}>
<ConvoIcon
agentsMap={agentsMap}
assistantMap={assistantMap}
conversation={conversation}
endpointsConfig={endpointsConfig}
containerClassName={containerClassName}
context="landing"
className="h-2/3 w-2/3 text-black dark:text-white"
size={41}
/>
{startupConfig?.showBirthdayIcon && (
<TooltipAnchor
className="absolute bottom-[27px] right-2"
description={localize('com_ui_happy_birthday')}
>
<BirthdayIcon />
</TooltipAnchor>
)}
</div>
{((isAgent || isAssistant) && name) || name ? (
<div className="flex flex-col items-center gap-0 p-2">
<SplitText
key={`split-text-${name}`}
text={name}
className={`${getTextSizeClass(name)} font-medium text-text-primary`}
delay={50}
textAlign="center"
animationFrom={{ opacity: 0, transform: 'translate3d(0,50px,0)' }}
animationTo={{ opacity: 1, transform: 'translate3d(0,0,0)' }}
easing={easings.easeOutCubic}
threshold={0}
rootMargin="0px"
onLineCountChange={handleLineCountChange}
/>
</div>
) : (
<SplitText
key={`split-text-${greetingText}${user?.name ? '-user' : ''}`}
text={greetingText}
className={`${getTextSizeClass(greetingText)} font-medium text-text-primary`}
delay={50}
textAlign="center"
animationFrom={{ opacity: 0, transform: 'translate3d(0,50px,0)' }}
animationTo={{ opacity: 1, transform: 'translate3d(0,0,0)' }}
easing={easings.easeOutCubic}
threshold={0}
rootMargin="0px"
onLineCountChange={handleLineCountChange}
/>
)}
</div>
{description && (
<div className="animate-fadeIn mt-4 max-w-md text-center text-sm font-normal text-text-primary">
{description}
</div>
)}
</div>
</div>
);
}