diff --git a/client/src/components/Chat/Input/Files/FileContainer.tsx b/client/src/components/Chat/Input/Files/FileContainer.tsx index f146277e051..2386394a132 100644 --- a/client/src/components/Chat/Input/Files/FileContainer.tsx +++ b/client/src/components/Chat/Input/Files/FileContainer.tsx @@ -80,7 +80,7 @@ const FileContainer = ({ 'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-text-primary'; const details = ( -
+
diff --git a/client/src/components/Chat/Messages/Content/Container.tsx b/client/src/components/Chat/Messages/Content/Container.tsx index 945d8ca1d51..db97b17e17a 100644 --- a/client/src/components/Chat/Messages/Content/Container.tsx +++ b/client/src/components/Chat/Messages/Content/Container.tsx @@ -1,18 +1,8 @@ -import { TMessage } from 'librechat-data-provider'; +import type { TMessage } from 'librechat-data-provider'; import MessageQuotes from './MessageQuotes'; import SkillPills from './SkillPills'; -import Files from './Files'; -const Container = ({ - children, - message, - hideFiles = false, -}: { - children: React.ReactNode; - message?: TMessage; - /** The message editor renders the files itself, as chips that can be removed. */ - hideFiles?: boolean; -}) => ( +const Container = ({ children, message }: { children: React.ReactNode; message?: TMessage }) => (
- {!hideFiles && } diff --git a/client/src/components/Chat/Messages/Content/EditMessage.tsx b/client/src/components/Chat/Messages/Content/EditMessage.tsx index 1c9c397f5ea..e46a81fc90e 100644 --- a/client/src/components/Chat/Messages/Content/EditMessage.tsx +++ b/client/src/components/Chat/Messages/Content/EditMessage.tsx @@ -250,7 +250,7 @@ const EditMessage = ({ ); return ( - +
{ setSelectedFile(file)} /> ))} @@ -44,6 +46,7 @@ const Files = ({ message }: { message?: TMessage }) => { ({ jest.mock( '~/components/Chat/Input/Files/FileContainer', () => - ({ onClick }: { onClick: () => void }) => , + ({ onClick, buttonClassName }: { onClick: () => void; buttonClassName?: string }) => ( + + ), +); +jest.mock( + '../Image', + () => + ({ alignRight, className }: { alignRight?: boolean; className?: string }) => ( +
+ image preview +
+ ), ); -jest.mock('../Image', () => ({ alignRight }: { alignRight?: boolean }) => ( -
- image preview -
-)); jest.mock( '../FilePreviewDialog', () => @@ -40,6 +52,16 @@ it('exposes extracted image text without changing ordinary image previews', () = ); expect(screen.getAllByText('image preview')).toHaveLength(1); expect(screen.getByTestId('image-preview')).toHaveAttribute('data-aligned-right', 'true'); + expect(screen.getByText('file action')).toHaveClass( + 'border-surface-user-message', + 'max-w-full', + 'bg-transparent', + ); + expect(screen.getByTestId('image-preview')).toHaveClass( + 'border-surface-user-message', + 'mt-0', + 'shadow-none', + ); fireEvent.click(screen.getByText('file action')); expect(screen.getByText('preview: text')).toBeInTheDocument(); }); diff --git a/client/src/components/Chat/Messages/MessageParts.tsx b/client/src/components/Chat/Messages/MessageParts.tsx index 2c6b0315018..f5a8f782a60 100644 --- a/client/src/components/Chat/Messages/MessageParts.tsx +++ b/client/src/components/Chat/Messages/MessageParts.tsx @@ -116,6 +116,7 @@ function MessageParts(props: TMessageProps) { >
} label={label} timestamp={reveal.revealedAt} diff --git a/client/src/components/Chat/Messages/SearchMessage.tsx b/client/src/components/Chat/Messages/SearchMessage.tsx index 13f0bbf05a7..e81eab67c7c 100644 --- a/client/src/components/Chat/Messages/SearchMessage.tsx +++ b/client/src/components/Chat/Messages/SearchMessage.tsx @@ -2,12 +2,13 @@ import { memo, useMemo } from 'react'; import { useRecoilValue } from 'recoil'; import type { TMessage } from 'librechat-data-provider'; import type { TMessageProps, TMessageIcon } from '~/common'; -import MessageRow, { getMessageRowWidthClass } from '~/components/Chat/Messages/ui/MessageRow'; import AuthorHeader from '~/components/Chat/Messages/Content/Parts/AuthorHeader'; import MinimalHoverButtons from '~/components/Chat/Messages/MinimalHoverButtons'; import { getHeaderModelName } from '~/components/Chat/Messages/ui/HeaderLabel'; +import { getMessageRowWidthClass } from '~/components/Chat/Messages/ui/width'; import { getHeaderPrefixForScreenReader, getMessageAriaLabel } from '~/utils'; import SearchContent, { rendersMarkdownLite } from './Content/SearchContent'; +import MessageRow from '~/components/Chat/Messages/ui/MessageRow'; import Icon from '~/components/Chat/Messages/MessageIcon'; import { useAuthContext, useLocalize } from '~/hooks'; import SearchButtons from './SearchButtons'; @@ -121,6 +122,7 @@ function SearchMessage({ message }: Pick) { className={cn('border-border-light mx-auto border-b px-4 py-4', getMessageRowWidthClass())} > } label={messageLabel} diff --git a/client/src/components/Chat/Messages/ui/MessageRender.tsx b/client/src/components/Chat/Messages/ui/MessageRender.tsx index 077a0724394..f638d393ab6 100644 --- a/client/src/components/Chat/Messages/ui/MessageRender.tsx +++ b/client/src/components/Chat/Messages/ui/MessageRender.tsx @@ -168,6 +168,7 @@ const MessageRender = memo(function MessageRender({ } label={messageLabel ?? ''} diff --git a/client/src/components/Chat/Messages/ui/MessageRow.tsx b/client/src/components/Chat/Messages/ui/MessageRow.tsx index b6885eda2f3..bc825904e8e 100644 --- a/client/src/components/Chat/Messages/ui/MessageRow.tsx +++ b/client/src/components/Chat/Messages/ui/MessageRow.tsx @@ -1,11 +1,16 @@ import { Zap } from 'lucide-react'; +import { ContentTypes } from 'librechat-data-provider'; +import type { TMessage } from 'librechat-data-provider'; import type { ReactNode } from 'react'; import MessageTimestamp from './MessageTimestamp'; +import { getMessageRowWidthClass } from './width'; import HeaderLabel from './HeaderLabel'; +import Files from '../Content/Files'; import { cn } from '~/utils'; type MessageRowProps = { id?: string; + message?: TMessage; label: string; hoverLabel?: string | null; icon: ReactNode; @@ -74,20 +79,9 @@ function renderAuthorHeader({ ); } -export function getMessageRowWidthClass({ - fullWidth = false, - hasParallelContent = false, -}: { - fullWidth?: boolean; - hasParallelContent?: boolean; -} = {}) { - if (fullWidth) return 'w-full max-w-full sm:px-2'; - if (hasParallelContent) return 'w-full sm:px-2 md:max-w-[58rem] xl:max-w-[70rem]'; - return 'w-full sm:px-2 md:max-w-3xl xl:max-w-4xl'; -} - export default function MessageRow({ id, + message, icon, label, hoverLabel, @@ -110,6 +104,24 @@ export default function MessageRow({ const widthClass = getMessageRowWidthClass({ fullWidth, hasParallelContent }); const isSystem = systemLabel != null && systemLabel !== ''; const isUserSide = isCreatedByUser && !isSystem; + const hasFiles = isUserSide && !isEditing && (message?.files?.length ?? 0) > 0; + const hasBody = + !hasFiles || + Boolean( + message?.text?.trim() || + message?.content?.some( + (part) => + part != null && + (part.type !== ContentTypes.TEXT || + (typeof part.text === 'string' ? part.text : part.text?.value)?.trim()), + ) || + message?.privacyRevision || + message?.error || + message?.unfinished || + message?.quotes?.length || + message?.alwaysAppliedSkills?.length || + message?.manualSkills?.length, + ); return (
-
- {children} -
+ {hasFiles && ( +
+ +
+ )} + {hasBody && ( +
+ {children} +
+ )}
{footer}
diff --git a/client/src/components/Chat/Messages/ui/__tests__/MessageRow.spec.tsx b/client/src/components/Chat/Messages/ui/__tests__/MessageRow.spec.tsx index 2c95e2e2b45..bd611dfee26 100644 --- a/client/src/components/Chat/Messages/ui/__tests__/MessageRow.spec.tsx +++ b/client/src/components/Chat/Messages/ui/__tests__/MessageRow.spec.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import type { TMessage } from 'librechat-data-provider'; import MessageRow from '../MessageRow'; jest.mock('../MessageTimestamp', () => ({ @@ -8,10 +9,24 @@ jest.mock('../MessageTimestamp', () => ({ ), })); +jest.mock('../../Content/Files', () => ({ + __esModule: true, + default: () =>