mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-07 08:40:19 +01:00
* refactor: Update Image Component to Remove Lazy Loading and Enhance Rendering - Removed the react-lazy-load-image-component dependency from the Image component, simplifying the image loading process. - Updated the Image component to use a standard <img> tag with async decoding for improved performance and user experience. - Adjusted related tests to reflect changes in image rendering behavior and ensure proper functionality without lazy loading. * refactor: Enhance Image Handling and Caching Across Components - Introduced a new previewCache utility for managing local blob preview URLs, improving image loading efficiency. - Updated the Image component and related parts (FileRow, Files, Part, ImageAttachment, LogContent) to utilize cached previews, enhancing rendering performance and user experience. - Added width and height properties to the Image component for better layout management and consistency across different usages. - Improved file handling logic in useFileHandling to cache previews during file uploads, ensuring quick access to image data. - Enhanced overall code clarity and maintainability by streamlining image rendering logic and reducing redundancy. * refactor: Enhance OpenAIImageGen Component with Image Dimensions - Added width and height properties to the OpenAIImageGen component for improved image rendering and layout management. - Updated the Image component usage within OpenAIImageGen to utilize the new dimensions, enhancing visual consistency and performance. - Improved code clarity by destructuring additional properties from the attachment object, streamlining the component's logic. * refactor: Implement Image Size Caching in DialogImage Component - Introduced an imageSizeCache to store and retrieve image sizes, enhancing performance by reducing redundant fetch requests. - Updated the getImageSize function to first check the cache before making network requests, improving efficiency in image handling. - Added decoding attribute to the image element for optimized rendering behavior. * refactor: Enhance UserAvatar Component with Avatar Caching and Error Handling - Introduced avatar caching logic to optimize avatar resolution based on user ID and avatar source, improving performance and reducing redundant image loads. - Implemented error handling for failed image loads, allowing for fallback to a default avatar when necessary. - Updated UserAvatar props to streamline the interface by removing the user object and directly accepting avatar-related properties. - Enhanced overall code clarity and maintainability by refactoring the component structure and logic. * fix: Layout Shift in Message and Placeholder Components for Consistent Height Management - Adjusted the height of the PlaceholderRow and related message components to ensure consistent rendering with a minimum height of 31px. - Updated the MessageParts and ContentRender components to utilize a minimum height for better layout stability. - Enhanced overall code clarity by standardizing the structure of message-related components. * tests: Update FileRow Component to Prefer Cached Previews for Image Rendering - Modified the image URL selection logic in the FileRow component to prioritize cached previews over file paths when uploads are complete, enhancing rendering performance and user experience. - Updated related tests to reflect changes in image URL handling, ensuring accurate assertions for both preview and file path scenarios. - Introduced a fallback mechanism to use file paths when no preview exists, improving robustness in file handling. * fix: Image cache lifecycle and dialog decoding - Add deletePreview/clearPreviewCache to previewCache.ts for blob URL cleanup - Wire deletePreview into useFileDeletion to revoke blobs on file delete - Move dimensionCache.set into useMemo to avoid side effects during render - Extract IMAGE_MAX_W_PX constant (512) to document coupling with max-w-lg - Export _resetImageCaches for test isolation - Change DialogImage decoding from "sync" to "async" to avoid blocking main thread * fix: Avatar cache invalidation and cleanup - Include avatarSrc in cache invalidation to prevent stale avatars - Remove unused username parameter from resolveAvatar - Skip caching when userId is empty to prevent cache key collisions * test: Fix test isolation and type safety - Reset module-level dimensionCache/paintedUrls in beforeEach via _resetImageCaches - Replace any[] with typed mock signature in cn mock for both test files * chore: Code quality improvements from review - Use barrel imports for previewCache in Files.tsx and Part.tsx - Single Map.get with truthy check instead of has+get in useEventHandlers - Add JSDoc comments explaining EmptyText margin removal and PlaceholderRow height - Fix FileRow toast showing "Deleting file" when file isn't actually deleted (progress < 1) * fix: Address remaining review findings (R1-R3) - Add deletePreview calls to deleteFiles batch path to prevent blob URL leaks - Change useFileDeletion import from deep path to barrel (~/utils) - Change useMemo to useEffect for dimensionCache.set (side effect, not derived value) * fix: Address audit comments 2, 5, and 7 - Fix files preservation to distinguish null (missing) from [] (empty) in finalHandler - Add auto-revoke on overwrite in cachePreview to prevent leaked blobs - Add removePreviewEntry for key transfer without revoke - Clean up stale temp_file_id cache entry after promotion to permanent file_id
196 lines
6.1 KiB
TypeScript
196 lines
6.1 KiB
TypeScript
import { useCallback, useMemo, memo } from 'react';
|
|
import { useAtomValue } from 'jotai';
|
|
import { useRecoilValue } from 'recoil';
|
|
import type { TMessage, TMessageContentParts } from 'librechat-data-provider';
|
|
import type { TMessageProps, TMessageIcon } from '~/common';
|
|
import { useAttachments, useLocalize, useMessageActions, useContentMetadata } from '~/hooks';
|
|
import ContentParts from '~/components/Chat/Messages/Content/ContentParts';
|
|
import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow';
|
|
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
|
|
import HoverButtons from '~/components/Chat/Messages/HoverButtons';
|
|
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
|
|
import SubRow from '~/components/Chat/Messages/SubRow';
|
|
import { cn, getMessageAriaLabel } from '~/utils';
|
|
import { fontSizeAtom } from '~/store/fontSize';
|
|
import store from '~/store';
|
|
|
|
type ContentRenderProps = {
|
|
message?: TMessage;
|
|
isSubmitting?: boolean;
|
|
} & Pick<
|
|
TMessageProps,
|
|
'currentEditId' | 'setCurrentEditId' | 'siblingIdx' | 'setSiblingIdx' | 'siblingCount'
|
|
>;
|
|
|
|
const ContentRender = memo(function ContentRender({
|
|
message: msg,
|
|
siblingIdx,
|
|
siblingCount,
|
|
setSiblingIdx,
|
|
currentEditId,
|
|
setCurrentEditId,
|
|
isSubmitting = false,
|
|
}: ContentRenderProps) {
|
|
const localize = useLocalize();
|
|
const { attachments, searchResults } = useAttachments({
|
|
messageId: msg?.messageId,
|
|
attachments: msg?.attachments,
|
|
});
|
|
const {
|
|
edit,
|
|
index,
|
|
agent,
|
|
assistant,
|
|
enterEdit,
|
|
conversation,
|
|
messageLabel,
|
|
handleContinue,
|
|
handleFeedback,
|
|
latestMessageId,
|
|
copyToClipboard,
|
|
regenerateMessage,
|
|
latestMessageDepth,
|
|
} = useMessageActions({
|
|
message: msg,
|
|
searchResults,
|
|
currentEditId,
|
|
setCurrentEditId,
|
|
});
|
|
const fontSize = useAtomValue(fontSizeAtom);
|
|
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
|
|
|
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
|
|
const isLast = useMemo(
|
|
() => !(msg?.children?.length ?? 0) && (msg?.depth === latestMessageDepth || msg?.depth === -1),
|
|
[msg?.children, msg?.depth, latestMessageDepth],
|
|
);
|
|
const hasNoChildren = !(msg?.children?.length ?? 0);
|
|
const isLatestMessage = msg?.messageId === latestMessageId;
|
|
/** Only pass isSubmitting to the latest message to prevent unnecessary re-renders */
|
|
const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false;
|
|
|
|
const iconData: TMessageIcon = useMemo(
|
|
() => ({
|
|
endpoint: msg?.endpoint ?? conversation?.endpoint,
|
|
model: msg?.model ?? conversation?.model,
|
|
iconURL: msg?.iconURL,
|
|
modelLabel: messageLabel,
|
|
isCreatedByUser: msg?.isCreatedByUser,
|
|
}),
|
|
[
|
|
messageLabel,
|
|
conversation?.endpoint,
|
|
conversation?.model,
|
|
msg?.model,
|
|
msg?.iconURL,
|
|
msg?.endpoint,
|
|
msg?.isCreatedByUser,
|
|
],
|
|
);
|
|
|
|
const { hasParallelContent } = useContentMetadata(msg);
|
|
|
|
if (!msg) {
|
|
return null;
|
|
}
|
|
|
|
const getChatWidthClass = () => {
|
|
if (maximizeChatSpace) {
|
|
return 'w-full max-w-full md:px-5 lg:px-1 xl:px-5';
|
|
}
|
|
if (hasParallelContent) {
|
|
return 'md:max-w-[58rem] xl:max-w-[70rem]';
|
|
}
|
|
return 'md:max-w-[47rem] xl:max-w-[55rem]';
|
|
};
|
|
|
|
const baseClasses = {
|
|
common: 'group mx-auto flex flex-1 gap-3 transition-all duration-300 transform-gpu ',
|
|
chat: getChatWidthClass(),
|
|
};
|
|
|
|
const conditionalClasses = {
|
|
focus: 'focus:outline-none focus:ring-2 focus:ring-border-xheavy',
|
|
};
|
|
|
|
return (
|
|
<div
|
|
id={msg.messageId}
|
|
aria-label={getMessageAriaLabel(msg, localize)}
|
|
className={cn(
|
|
baseClasses.common,
|
|
baseClasses.chat,
|
|
conditionalClasses.focus,
|
|
'message-render',
|
|
)}
|
|
>
|
|
{!hasParallelContent && (
|
|
<div className="relative flex flex-shrink-0 flex-col items-center">
|
|
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
|
|
<MessageIcon iconData={iconData} assistant={assistant} agent={agent} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div
|
|
className={cn(
|
|
'relative flex flex-col',
|
|
hasParallelContent ? 'w-full' : 'w-11/12',
|
|
msg.isCreatedByUser ? 'user-turn' : 'agent-turn',
|
|
)}
|
|
>
|
|
{!hasParallelContent && (
|
|
<h2 className={cn('select-none font-semibold', fontSize)}>{messageLabel}</h2>
|
|
)}
|
|
|
|
<div className="flex flex-col gap-1">
|
|
<div className="flex min-h-[20px] max-w-full flex-grow flex-col gap-0">
|
|
<ContentParts
|
|
edit={edit}
|
|
isLast={isLast}
|
|
enterEdit={enterEdit}
|
|
siblingIdx={siblingIdx}
|
|
messageId={msg.messageId}
|
|
attachments={attachments}
|
|
searchResults={searchResults}
|
|
setSiblingIdx={setSiblingIdx}
|
|
isLatestMessage={isLatestMessage}
|
|
isSubmitting={effectiveIsSubmitting}
|
|
isCreatedByUser={msg.isCreatedByUser}
|
|
conversationId={conversation?.conversationId}
|
|
content={msg.content as Array<TMessageContentParts | undefined>}
|
|
/>
|
|
</div>
|
|
{hasNoChildren && effectiveIsSubmitting ? (
|
|
<PlaceholderRow />
|
|
) : (
|
|
<SubRow classes="text-xs">
|
|
<SiblingSwitch
|
|
siblingIdx={siblingIdx}
|
|
siblingCount={siblingCount}
|
|
setSiblingIdx={setSiblingIdx}
|
|
/>
|
|
<HoverButtons
|
|
index={index}
|
|
message={msg}
|
|
isEditing={edit}
|
|
enterEdit={enterEdit}
|
|
isSubmitting={isSubmitting}
|
|
conversation={conversation ?? null}
|
|
regenerate={handleRegenerateMessage}
|
|
copyToClipboard={copyToClipboard}
|
|
handleContinue={handleContinue}
|
|
latestMessageId={latestMessageId}
|
|
handleFeedback={handleFeedback}
|
|
isLast={isLast}
|
|
/>
|
|
</SubRow>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
ContentRender.displayName = 'ContentRender';
|
|
|
|
export default ContentRender;
|