mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-03 09:08:52 +01:00
🎨 style: Address Minor UI Refresh Issues (#6552)
* 🎨 style: Adjust isSelected svg layout of ModelSpecItem
* style: fix modelSpec URL image beeing off-center; style: selected svg centered vertically
* style: Update CustomMenu component to use rounded-lg and enhance focus styles
* style: SidePanel top padding same as NewChat
* fix: prevent unnecessary space rendering in SplitText component
* style: Fix class names and enhance layout in Badge components
* feat: disable temporary chat when in chat
* style: handle > 1 lines in title Landing
* feat: enhance dynamic margin calculation based on line count and content height in Landing component
This commit is contained in:
parent
6b58547c63
commit
3ba7c4eb19
10 changed files with 147 additions and 40 deletions
|
|
@ -17,11 +17,13 @@ import store from '~/store';
|
|||
interface BadgeRowProps {
|
||||
onChange: (badges: Pick<BadgeItem, 'id'>[]) => void;
|
||||
onToggle?: (badgeId: string, currentActive: boolean) => void;
|
||||
isInChat: boolean;
|
||||
}
|
||||
|
||||
interface BadgeWrapperProps {
|
||||
badge: BadgeItem;
|
||||
isEditing: boolean;
|
||||
isInChat: boolean;
|
||||
onToggle: (badge: BadgeItem) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onMouseDown: (e: React.MouseEvent, badge: BadgeItem, isActive: boolean) => void;
|
||||
|
|
@ -30,7 +32,7 @@ interface BadgeWrapperProps {
|
|||
|
||||
const BadgeWrapper = React.memo(
|
||||
forwardRef<HTMLDivElement, BadgeWrapperProps>(
|
||||
({ badge, isEditing, onToggle, onDelete, onMouseDown, badgeRefs }, ref) => {
|
||||
({ badge, isEditing, isInChat, onToggle, onDelete, onMouseDown, badgeRefs }, ref) => {
|
||||
const isActive = badge.atom ? useRecoilValue(badge.atom) : false;
|
||||
|
||||
return (
|
||||
|
|
@ -49,11 +51,13 @@ const BadgeWrapper = React.memo(
|
|||
className={isEditing ? 'ios-wiggle badge-icon h-full' : 'badge-icon h-full'}
|
||||
>
|
||||
<Badge
|
||||
id={badge.id}
|
||||
icon={badge.icon as LucideIcon}
|
||||
label={badge.label}
|
||||
isActive={isActive}
|
||||
isEditing={isEditing}
|
||||
isAvailable={badge.isAvailable}
|
||||
isInChat={isInChat}
|
||||
onToggle={() => onToggle(badge)}
|
||||
onBadgeAction={() => onDelete(badge.id)}
|
||||
/>
|
||||
|
|
@ -64,6 +68,7 @@ const BadgeWrapper = React.memo(
|
|||
(prevProps, nextProps) =>
|
||||
prevProps.badge.id === nextProps.badge.id &&
|
||||
prevProps.isEditing === nextProps.isEditing &&
|
||||
prevProps.isInChat === nextProps.isInChat &&
|
||||
prevProps.onToggle === nextProps.onToggle &&
|
||||
prevProps.onDelete === nextProps.onDelete &&
|
||||
prevProps.onMouseDown === nextProps.onMouseDown &&
|
||||
|
|
@ -121,7 +126,7 @@ const dragReducer = (state: DragState, action: DragAction): DragState => {
|
|||
}
|
||||
};
|
||||
|
||||
export function BadgeRow({ onChange, onToggle }: BadgeRowProps) {
|
||||
export function BadgeRow({ onChange, onToggle, isInChat }: BadgeRowProps) {
|
||||
const [orderedBadges, setOrderedBadges] = useState<BadgeItem[]>([]);
|
||||
const [dragState, dispatch] = useReducer(dragReducer, {
|
||||
draggedBadge: null,
|
||||
|
|
@ -301,17 +306,20 @@ export function BadgeRow({ onChange, onToggle }: BadgeRowProps) {
|
|||
{dragState.draggedBadge && dragState.insertIndex === index && ghostBadge && (
|
||||
<div className="badge-icon h-full">
|
||||
<Badge
|
||||
id={ghostBadge.id}
|
||||
icon={ghostBadge.icon as LucideIcon}
|
||||
label={ghostBadge.label}
|
||||
isActive={dragState.draggedBadgeActive}
|
||||
isEditing={isEditing}
|
||||
isAvailable={ghostBadge.isAvailable}
|
||||
isInChat={isInChat}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<BadgeWrapper
|
||||
badge={badge}
|
||||
isEditing={isEditing}
|
||||
isInChat={isInChat}
|
||||
onToggle={handleBadgeToggle}
|
||||
onDelete={handleDelete}
|
||||
onMouseDown={handleMouseDown}
|
||||
|
|
@ -322,11 +330,13 @@ export function BadgeRow({ onChange, onToggle }: BadgeRowProps) {
|
|||
{dragState.draggedBadge && dragState.insertIndex === tempBadges.length && ghostBadge && (
|
||||
<div className="badge-icon h-full">
|
||||
<Badge
|
||||
id={ghostBadge.id}
|
||||
icon={ghostBadge.icon as LucideIcon}
|
||||
label={ghostBadge.label}
|
||||
isActive={dragState.draggedBadgeActive}
|
||||
isEditing={isEditing}
|
||||
isAvailable={ghostBadge.isAvailable}
|
||||
isInChat={isInChat}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -343,10 +353,12 @@ export function BadgeRow({ onChange, onToggle }: BadgeRowProps) {
|
|||
}}
|
||||
>
|
||||
<Badge
|
||||
id={ghostBadge.id}
|
||||
icon={ghostBadge.icon as LucideIcon}
|
||||
label={ghostBadge.label}
|
||||
isActive={dragState.draggedBadgeActive}
|
||||
isAvailable={ghostBadge.isAvailable}
|
||||
isInChat={isInChat}
|
||||
isEditing
|
||||
isDragging
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -278,7 +278,12 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
|||
<div className={`${isRTL ? 'mr-2' : 'ml-2'}`}>
|
||||
<AttachFileChat disableInputs={disableInputs} />
|
||||
</div>
|
||||
<BadgeRow onChange={(newBadges) => setBadges(newBadges)} />
|
||||
<BadgeRow
|
||||
onChange={(newBadges) => setBadges(newBadges)}
|
||||
isInChat={
|
||||
Array.isArray(conversation?.messages) && conversation.messages.length >= 1
|
||||
}
|
||||
/>
|
||||
<div className="mx-auto flex" />
|
||||
{SpeechToText && (
|
||||
<AudioRecorder
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useMemo, useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { easings } from '@react-spring/web';
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
|
||||
import { useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
|
||||
|
|
@ -20,6 +20,11 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
|
|||
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 (
|
||||
|
|
@ -81,13 +86,46 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
|
|||
}
|
||||
}, [localize, startupConfig?.interface?.customWelcome]);
|
||||
|
||||
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]);
|
||||
|
||||
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'}`}
|
||||
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 className="flex flex-col items-center gap-0 p-2">
|
||||
<div className="flex flex-col items-center justify-center gap-4 md:flex-row">
|
||||
<div className="relative size-10 justify-center">
|
||||
<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-4`}
|
||||
>
|
||||
<div className={`relative size-10 justify-center ${textHasMultipleLines ? 'mb-2' : ''}`}>
|
||||
<ConvoIcon
|
||||
agentsMap={agentsMap}
|
||||
assistantMap={assistantMap}
|
||||
|
|
@ -120,6 +158,7 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
|
|||
easing={easings.easeOutCubic}
|
||||
threshold={0}
|
||||
rootMargin="0px"
|
||||
onLineCountChange={handleLineCountChange}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -134,6 +173,7 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
|
|||
easing={easings.easeOutCubic}
|
||||
threshold={0}
|
||||
rootMargin="0px"
|
||||
onLineCountChange={handleLineCountChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -77,8 +77,9 @@ export const CustomMenu = React.forwardRef<HTMLDivElement, CustomMenuProps>(func
|
|||
autoSelect
|
||||
render={combobox}
|
||||
className={cn(
|
||||
'h-10 w-full rounded border-none bg-transparent px-2 text-base',
|
||||
'h-10 w-full rounded-lg border-none bg-transparent px-2 text-base',
|
||||
'sm:h-8 sm:text-sm',
|
||||
'focus:outline-none focus:ring-0 focus-visible:ring-2 focus-visible:ring-white',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ export function ModelSpecItem({ spec, isSelected }: ModelSpecItemProps) {
|
|||
key={spec.name}
|
||||
onClick={() => handleSelectSpec(spec)}
|
||||
className={cn(
|
||||
'flex w-full cursor-pointer justify-between rounded-lg px-2 text-sm',
|
||||
spec.description ? 'items-start' : 'items-center',
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 text-sm',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
|
@ -41,7 +40,7 @@ export function ModelSpecItem({ spec, isSelected }: ModelSpecItemProps) {
|
|||
</div>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className={cn('flex-shrink-0', spec.description ? 'pt-1' : '')}>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
|
|
|
|||
|
|
@ -22,7 +22,15 @@ const SpecIcon: React.FC<SpecIconProps> = ({ currentSpec, endpointsConfig }) =>
|
|||
if (!iconURL.includes('http')) {
|
||||
Icon = (icons[iconKey] ?? icons.unknown) as IconType;
|
||||
} else if (iconURL) {
|
||||
return <URLIcon iconURL={iconURL} altName={currentSpec.name} />;
|
||||
return (
|
||||
<URLIcon
|
||||
iconURL={iconURL}
|
||||
altName={currentSpec.name}
|
||||
containerStyle={{ width: 20, height: 20 }}
|
||||
className="icon-md shrink-0 overflow-hidden rounded-full"
|
||||
endpoint={endpoint || undefined}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
Icon = (icons[endpoint ?? ''] ?? icons.unknown) as IconType;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue