From e57cf0f921629f0917c8fb741905d9e6ae54851c Mon Sep 17 00:00:00 2001 From: Aleksei Aleshin Date: Tue, 17 Feb 2026 10:58:04 +0300 Subject: [PATCH 1/2] add image preview --- .../chat/MessageComposer/Attachment.tsx | 42 ++++++++++++++++++- .../chat/MessageComposer/Attachments.tsx | 1 + frontend/src/components/chat/index.tsx | 1 + frontend/src/state/chat.ts | 1 + 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/chat/MessageComposer/Attachment.tsx b/frontend/src/components/chat/MessageComposer/Attachment.tsx index 5bdb336e3d..7f4447f2f4 100644 --- a/frontend/src/components/chat/MessageComposer/Attachment.tsx +++ b/frontend/src/components/chat/MessageComposer/Attachment.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { DefaultExtensionType, FileIcon, defaultStyles } from 'react-file-icon'; import { Card } from '@/components/ui/card'; @@ -13,9 +13,23 @@ interface AttachmentProps { name: string; mime: string; children?: React.ReactNode; + file?: File; } -const Attachment: React.FC = ({ name, mime, children }) => { +const Attachment: React.FC = ({ + name, + mime, + children, + file +}) => { + const isImage = useMemo(() => mime.startsWith('image/'), [mime]); + const imageUrl = useMemo(() => { + if (isImage && file) { + return URL.createObjectURL(file); + } + return undefined; + }, [isImage, file]); + let extension: DefaultExtensionType; if (name.includes('.')) { extension = name.split('.').pop()!.toLowerCase() as DefaultExtensionType; @@ -25,6 +39,30 @@ const Attachment: React.FC = ({ name, mime, children }) => { : ('txt' as DefaultExtensionType); } + if (isImage && imageUrl) { + return ( + + + +
+ {children} + + {name} + +
+
+ +

{name}

+
+
+
+ ); + } + return ( diff --git a/frontend/src/components/chat/MessageComposer/Attachments.tsx b/frontend/src/components/chat/MessageComposer/Attachments.tsx index ef96d6f1d6..a9418d8005 100644 --- a/frontend/src/components/chat/MessageComposer/Attachments.tsx +++ b/frontend/src/components/chat/MessageComposer/Attachments.tsx @@ -129,6 +129,7 @@ const Attachments = () => { key={attachment.id} name={attachment.name} mime={attachment.type} + file={attachment.file} > {progress} {remove} diff --git a/frontend/src/components/chat/index.tsx b/frontend/src/components/chat/index.tsx index d53575b71c..12a8f6cb7a 100644 --- a/frontend/src/components/chat/index.tsx +++ b/frontend/src/components/chat/index.tsx @@ -137,6 +137,7 @@ const Chat = () => { name: file.name, size: file.size, uploadProgress: 0, + file, cancel: () => { toast.info(`${t('chat.fileUpload.errors.cancelled')} ${file.name}`); xhr.abort(); diff --git a/frontend/src/state/chat.ts b/frontend/src/state/chat.ts index 6acd8cd513..0510db82f9 100644 --- a/frontend/src/state/chat.ts +++ b/frontend/src/state/chat.ts @@ -12,6 +12,7 @@ export interface IAttachment { uploaded?: boolean; cancel?: () => void; remove?: () => void; + file?: File; } export const attachmentsState = atom({ From e72e504c780c49aa8f5184e9efbaf2caea99d8ae Mon Sep 17 00:00:00 2001 From: Aleksei Aleshin Date: Tue, 17 Feb 2026 11:14:14 +0300 Subject: [PATCH 2/2] add Cleanup Object URL --- .../components/chat/MessageComposer/Attachment.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/chat/MessageComposer/Attachment.tsx b/frontend/src/components/chat/MessageComposer/Attachment.tsx index 7f4447f2f4..d5087044e2 100644 --- a/frontend/src/components/chat/MessageComposer/Attachment.tsx +++ b/frontend/src/components/chat/MessageComposer/Attachment.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { DefaultExtensionType, FileIcon, defaultStyles } from 'react-file-icon'; import { Card } from '@/components/ui/card'; @@ -30,6 +30,15 @@ const Attachment: React.FC = ({ return undefined; }, [isImage, file]); + // Cleanup Object URL on unmount or when imageUrl changes + useEffect(() => { + return () => { + if (imageUrl) { + URL.revokeObjectURL(imageUrl); + } + }; + }, [imageUrl]); + let extension: DefaultExtensionType; if (name.includes('.')) { extension = name.split('.').pop()!.toLowerCase() as DefaultExtensionType;