diff --git a/client/src/components/Chat/Input/BadgeRow.tsx b/client/src/components/Chat/Input/BadgeRow.tsx index c217d749ae..2d07658229 100644 --- a/client/src/components/Chat/Input/BadgeRow.tsx +++ b/client/src/components/Chat/Input/BadgeRow.tsx @@ -17,11 +17,13 @@ import store from '~/store'; interface BadgeRowProps { onChange: (badges: Pick[]) => 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( - ({ 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'} > 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([]); const [dragState, dispatch] = useReducer(dragReducer, { draggedBadge: null, @@ -301,17 +306,20 @@ export function BadgeRow({ onChange, onToggle }: BadgeRowProps) { {dragState.draggedBadge && dragState.insertIndex === index && ghostBadge && (
)} )} @@ -343,10 +353,12 @@ export function BadgeRow({ onChange, onToggle }: BadgeRowProps) { }} > diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 3bdbde785e..4f083903b3 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -278,7 +278,12 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
- setBadges(newBadges)} /> + setBadges(newBadges)} + isInChat={ + Array.isArray(conversation?.messages) && conversation.messages.length >= 1 + } + />
{SpeechToText && ( (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 (
-
-
-
+
+
+
) : ( @@ -134,6 +173,7 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding: easing={easings.easeOutCubic} threshold={0} rootMargin="0px" + onLineCountChange={handleLineCountChange} /> )}
diff --git a/client/src/components/Chat/Menus/Endpoints/CustomMenu.tsx b/client/src/components/Chat/Menus/Endpoints/CustomMenu.tsx index 24eb0559d6..a80b4d4fb9 100644 --- a/client/src/components/Chat/Menus/Endpoints/CustomMenu.tsx +++ b/client/src/components/Chat/Menus/Endpoints/CustomMenu.tsx @@ -77,8 +77,9 @@ export const CustomMenu = React.forwardRef(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', )} />
diff --git a/client/src/components/Chat/Menus/Endpoints/components/ModelSpecItem.tsx b/client/src/components/Chat/Menus/Endpoints/components/ModelSpecItem.tsx index ea2e26b54c..a7465a0280 100644 --- a/client/src/components/Chat/Menus/Endpoints/components/ModelSpecItem.tsx +++ b/client/src/components/Chat/Menus/Endpoints/components/ModelSpecItem.tsx @@ -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', )} >
{isSelected && ( -
+
= ({ currentSpec, endpointsConfig }) => if (!iconURL.includes('http')) { Icon = (icons[iconKey] ?? icons.unknown) as IconType; } else if (iconURL) { - return ; + return ( + + ); } else { Icon = (icons[endpoint ?? ''] ?? icons.unknown) as IconType; } diff --git a/client/src/components/Endpoints/URLIcon.tsx b/client/src/components/Endpoints/URLIcon.tsx index 520bf4679e..ab22635809 100644 --- a/client/src/components/Endpoints/URLIcon.tsx +++ b/client/src/components/Endpoints/URLIcon.tsx @@ -1,14 +1,14 @@ import React, { memo, useState } from 'react'; +import { AlertCircle } from 'lucide-react'; import { icons } from '~/hooks/Endpoint/Icons'; -import { AlertCircle } from 'lucide-react'; // Assuming you have lucide-react for icons export const URLIcon = memo( ({ iconURL, altName, - containerStyle = { width: '20', height: '20' }, + containerStyle = { width: 20, height: 20 }, imageStyle = { width: '100%', height: '100%' }, - className = 'icon-xl mr-1 shrink-0 overflow-hidden rounded-full', + className = 'icon-md mr-1 shrink-0 overflow-hidden rounded-full', endpoint, }: { iconURL: string; @@ -26,11 +26,12 @@ export const URLIcon = memo( const DefaultIcon: React.ElementType = endpoint && icons[endpoint] ? icons[endpoint]! : icons.unknown!; + if (imageError || !iconURL) { return (
- +
{imageError && iconURL && (
{altName
); diff --git a/client/src/components/SidePanel/Nav.tsx b/client/src/components/SidePanel/Nav.tsx index 44ef52b39d..d901d6b47a 100644 --- a/client/src/components/SidePanel/Nav.tsx +++ b/client/src/components/SidePanel/Nav.tsx @@ -25,7 +25,7 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
-
+
{links.map((link, index) => { const variant = getVariant(link); return isCollapsed ? ( diff --git a/client/src/components/ui/Badge.tsx b/client/src/components/ui/Badge.tsx index b29a953d29..e100f170f4 100644 --- a/client/src/components/ui/Badge.tsx +++ b/client/src/components/ui/Badge.tsx @@ -1,3 +1,5 @@ +import type React from 'react'; + import { X, Plus } from 'lucide-react'; import { motion } from 'framer-motion'; import type { ButtonHTMLAttributes } from 'react'; @@ -7,10 +9,12 @@ import { cn } from '~/utils'; interface BadgeProps extends ButtonHTMLAttributes { icon?: LucideIcon; label: string; + id?: string; isActive?: boolean; isEditing?: boolean; isDragging?: boolean; isAvailable: boolean; + isInChat?: boolean; onBadgeAction?: () => void; onToggle?: () => void; } @@ -18,18 +22,27 @@ interface BadgeProps extends ButtonHTMLAttributes { export default function Badge({ icon: Icon, label, + id, isActive = false, isEditing = false, isDragging = false, isAvailable = true, + isInChat = false, onBadgeAction, onToggle, className, ...props }: BadgeProps) { const isMoveable = isEditing && isAvailable; + const isDisabled = id === '1' && isInChat; const handleClick: React.MouseEventHandler = (e) => { + if (isDisabled) { + e.preventDefault(); + e.stopPropagation(); + return; + } + if (!isEditing && onToggle) { if (typeof window !== 'undefined' && window.innerWidth >= 768) { e.preventDefault(); @@ -44,27 +57,30 @@ export default function Badge({ onClick={handleClick} className={cn( 'group relative inline-flex items-center gap-1.5 rounded-full px-4 py-1.5', - 'border border-border-medium text-sm font-medium transition-shadow', + 'border border-border-medium text-sm font-medium transition-shadow md:w-full', + 'size-9 p-2 md:p-3', isActive ? 'bg-surface-active shadow-md' : 'bg-surface-chat shadow-sm hover:bg-surface-hover hover:shadow-md', + 'active:scale-95 active:shadow-inner', isMoveable && 'cursor-move', + isDisabled && 'cursor-not-allowed opacity-50 hover:shadow-sm', className, )} animate={{ scale: isDragging ? 1.1 : 1, boxShadow: isDragging ? '0 10px 25px rgba(0,0,0,0.1)' : undefined, }} - whileTap={{ scale: isDragging ? 1.1 : 0.97 }} + whileTap={{ scale: isDragging ? 1.1 : isDisabled ? 1 : 0.97 }} transition={{ type: 'tween', duration: 0.1, ease: 'easeOut' }} {...props} > - {Icon && } + {Icon && } {label} {isEditing && !isDragging && ( void; + onLineCountChange?: (lineCount: number) => void; } const SplitText: React.FC = ({ @@ -25,6 +26,7 @@ const SplitText: React.FC = ({ rootMargin = '-100px', textAlign = 'center', onLetterAnimationComplete, + onLineCountChange, }) => { const words = text.split(' ').map((word) => word.split('')); const letters = words.flat(); @@ -32,6 +34,24 @@ const SplitText: React.FC = ({ const ref = useRef(null); const animatedCount = useRef(0); + const springs = useSprings( + letters.length, + letters.map((_, i) => ({ + from: animationFrom, + to: inView + ? async (next: (props: any) => Promise) => { + await next(animationTo); + animatedCount.current += 1; + if (animatedCount.current === letters.length && onLetterAnimationComplete) { + onLetterAnimationComplete(); + } + } + : animationFrom, + delay: i * delay, + config: { easing }, + })), + ); + useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { @@ -52,23 +72,22 @@ const SplitText: React.FC = ({ return () => observer.disconnect(); }, [threshold, rootMargin]); - const springs = useSprings( - letters.length, - letters.map((_, i) => ({ - from: animationFrom, - to: inView - ? async (next: (props: any) => Promise) => { - await next(animationTo); - animatedCount.current += 1; - if (animatedCount.current === letters.length && onLetterAnimationComplete) { - onLetterAnimationComplete(); - } + useEffect(() => { + if (ref.current && inView) { + const element = ref.current; + setTimeout(() => { + const lineHeight = + parseInt(getComputedStyle(element).lineHeight) || + parseInt(getComputedStyle(element).fontSize) * 1.2; + const height = element.offsetHeight; + const lines = Math.round(height / lineHeight); + + if (onLineCountChange) { + onLineCountChange(lines); } - : animationFrom, - delay: i * delay, - config: { easing }, - })), - ); + }, 100); + } + }, [inView, text, onLineCountChange]); return (

= ({ ); })} -   + {wordIndex < words.length - 1 && ( +   + )} ))}