mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-07 08:40:19 +01:00
* fix: remove scaleImage function that stretched vertical images * chore: lint * refactor: Simplify Image Component Usage Across Chat Parts - Removed height and width props from the Image component in various parts (Files, Part, ImageAttachment, LogContent) to streamline image rendering. - Introduced a constant for maximum image height in the Image component for consistent styling. - Updated related components to utilize the new simplified Image component structure, enhancing maintainability and reducing redundancy. * refactor: Simplify LogContent and Enhance Image Component Tests - Removed height and width properties from the ImageAttachment type in LogContent for cleaner code. - Updated the image rendering logic to rely solely on the filepath, improving clarity. - Enhanced the Image component tests with additional assertions for rendering behavior and accessibility. - Introduced new tests for OpenAIImageGen to validate image preloading and progress handling, ensuring robust functionality. --------- Co-authored-by: Danny Avila <danny@librechat.ai>
150 lines
4.6 KiB
TypeScript
150 lines
4.6 KiB
TypeScript
import { memo, useState, useEffect } from 'react';
|
|
import { imageExtRegex, Tools } from 'librechat-data-provider';
|
|
import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider';
|
|
import FileContainer from '~/components/Chat/Input/Files/FileContainer';
|
|
import Image from '~/components/Chat/Messages/Content/Image';
|
|
import { useAttachmentLink } from './LogLink';
|
|
import { cn } from '~/utils';
|
|
|
|
const FileAttachment = memo(({ attachment }: { attachment: Partial<TAttachment> }) => {
|
|
const [isVisible, setIsVisible] = useState(false);
|
|
const file = attachment as TFile & TAttachmentMetadata;
|
|
const { handleDownload } = useAttachmentLink({
|
|
href: attachment.filepath ?? '',
|
|
filename: attachment.filename ?? '',
|
|
file_id: file.file_id,
|
|
user: file.user,
|
|
source: file.source,
|
|
});
|
|
const extension = attachment.filename?.split('.').pop();
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => setIsVisible(true), 50);
|
|
return () => clearTimeout(timer);
|
|
}, []);
|
|
|
|
if (!attachment.filepath) {
|
|
return null;
|
|
}
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'file-attachment-container',
|
|
'transition-all duration-300 ease-out',
|
|
isVisible ? 'translate-y-0 opacity-100' : 'translate-y-2 opacity-0',
|
|
)}
|
|
style={{
|
|
transformOrigin: 'center top',
|
|
willChange: 'opacity, transform',
|
|
WebkitFontSmoothing: 'subpixel-antialiased',
|
|
}}
|
|
>
|
|
<FileContainer
|
|
file={attachment}
|
|
onClick={handleDownload}
|
|
overrideType={extension}
|
|
containerClassName="max-w-fit"
|
|
buttonClassName="bg-surface-secondary hover:cursor-pointer hover:bg-surface-hover active:bg-surface-secondary focus:bg-surface-hover hover:border-border-heavy active:border-border-heavy"
|
|
/>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
const ImageAttachment = memo(({ attachment }: { attachment: TAttachment }) => {
|
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
const { filepath = null } = attachment as TFile & TAttachmentMetadata;
|
|
|
|
useEffect(() => {
|
|
setIsLoaded(false);
|
|
const timer = setTimeout(() => setIsLoaded(true), 100);
|
|
return () => clearTimeout(timer);
|
|
}, [attachment]);
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'image-attachment-container',
|
|
'transition-all duration-500 ease-out',
|
|
isLoaded ? 'scale-100 opacity-100' : 'scale-[0.98] opacity-0',
|
|
)}
|
|
style={{
|
|
transformOrigin: 'center top',
|
|
willChange: 'opacity, transform',
|
|
WebkitFontSmoothing: 'subpixel-antialiased',
|
|
}}
|
|
>
|
|
<Image
|
|
altText={attachment.filename || 'attachment image'}
|
|
imagePath={filepath ?? ''}
|
|
className="mb-4"
|
|
/>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
export default function Attachment({ attachment }: { attachment?: TAttachment }) {
|
|
if (!attachment) {
|
|
return null;
|
|
}
|
|
if (attachment.type === Tools.web_search) {
|
|
return null;
|
|
}
|
|
|
|
const { width, height, filepath = null } = attachment as TFile & TAttachmentMetadata;
|
|
const isImage = attachment.filename
|
|
? imageExtRegex.test(attachment.filename) && width != null && height != null && filepath != null
|
|
: false;
|
|
|
|
if (isImage) {
|
|
return <ImageAttachment attachment={attachment} />;
|
|
} else if (!attachment.filepath) {
|
|
return null;
|
|
}
|
|
return <FileAttachment attachment={attachment} />;
|
|
}
|
|
|
|
export function AttachmentGroup({ attachments }: { attachments?: TAttachment[] }) {
|
|
if (!attachments || attachments.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const fileAttachments: TAttachment[] = [];
|
|
const imageAttachments: TAttachment[] = [];
|
|
|
|
attachments.forEach((attachment) => {
|
|
const { width, height, filepath = null } = attachment as TFile & TAttachmentMetadata;
|
|
const isImage = attachment.filename
|
|
? imageExtRegex.test(attachment.filename) &&
|
|
width != null &&
|
|
height != null &&
|
|
filepath != null
|
|
: false;
|
|
|
|
if (isImage) {
|
|
imageAttachments.push(attachment);
|
|
} else if (attachment.type !== Tools.web_search) {
|
|
fileAttachments.push(attachment);
|
|
}
|
|
});
|
|
|
|
return (
|
|
<>
|
|
{fileAttachments.length > 0 && (
|
|
<div className="my-2 flex flex-wrap items-center gap-2.5">
|
|
{fileAttachments.map((attachment, index) =>
|
|
attachment.filepath ? (
|
|
<FileAttachment attachment={attachment} key={`file-${index}`} />
|
|
) : null,
|
|
)}
|
|
</div>
|
|
)}
|
|
{imageAttachments.length > 0 && (
|
|
<div className="mb-2 flex flex-wrap items-center">
|
|
{imageAttachments.map((attachment, index) => (
|
|
<ImageAttachment attachment={attachment} key={`image-${index}`} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|