diff --git a/client/src/components/Chat/Messages/Content/Parts/Attachment.tsx b/client/src/components/Chat/Messages/Content/Parts/Attachment.tsx index f5a75ea181..4b27ae1423 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Attachment.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Attachment.tsx @@ -216,6 +216,18 @@ const FileAttachmentGroup = memo(({ attachments }: { attachments: TAttachment[] 0: String(names.length - 2), })}`; }, [visibleAttachments, localize]); + const groupedAttachments = useMemo(() => { + const files: TAttachment[] = []; + const textPreviews: TAttachment[] = []; + for (const attachment of visibleAttachments) { + if (isTextAttachment(attachment)) { + textPreviews.push(attachment); + continue; + } + files.push(attachment); + } + return { files, textPreviews }; + }, [visibleAttachments]); if (count === 0) { return null; @@ -270,11 +282,22 @@ const FileAttachmentGroup = memo(({ attachments }: { attachments: TAttachment[]
-
- {visibleAttachments.map((attachment, index) => ( - + {groupedAttachments.files.length > 0 && ( +
+ {groupedAttachments.files.map((attachment, index) => ( + + ))} +
+ )} + {groupedAttachments.textPreviews.map((attachment, index) => ( + ))}
@@ -285,92 +308,121 @@ const FileAttachmentGroup = memo(({ attachments }: { attachments: TAttachment[] }); FileAttachmentGroup.displayName = 'FileAttachmentGroup'; -const TextAttachment = memo(({ attachment }: { attachment: Partial }) => { - const localize = useLocalize(); - const preId = useId(); - const preRef = useRef(null); - const [isVisible, setIsVisible] = useState(false); - const [expanded, setExpanded] = useState(false); - // Decided once after layout: does the text actually overflow the collapsed - // height? Char count is a poor proxy (a 100-char file with many newlines can - // overflow; 800 chars of dense single-line text may not), so we measure. - const [overflowed, setOverflowed] = 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(); - const text = file.text ?? ''; +const TextAttachment = memo( + ({ + attachment, + showFileChip = true, + }: { + attachment: Partial; + showFileChip?: boolean; + }) => { + const localize = useLocalize(); + const preId = useId(); + const preRef = useRef(null); + const [isVisible, setIsVisible] = useState(false); + const [expanded, setExpanded] = useState(false); + // Decided once after layout: does the text actually overflow the collapsed + // height? Char count is a poor proxy (a 100-char file with many newlines can + // overflow; 800 chars of dense single-line text may not), so we measure. + const [overflowed, setOverflowed] = 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(); + const text = file.text ?? ''; + const visibleFilename = displayFilename(attachment.filename); - useEffect(() => { - const timer = setTimeout(() => setIsVisible(true), 50); - return () => clearTimeout(timer); - }, []); + useEffect(() => { + const timer = setTimeout(() => setIsVisible(true), 50); + return () => clearTimeout(timer); + }, []); - useLayoutEffect(() => { - const el = preRef.current; - if (!el) { - return; - } - setOverflowed(el.scrollHeight > COLLAPSED_MAX_HEIGHT + 1); - }, [text]); + useLayoutEffect(() => { + const el = preRef.current; + if (!el) { + return; + } + setOverflowed(el.scrollHeight > COLLAPSED_MAX_HEIGHT + 1); + }, [text]); - const isClamped = overflowed && !expanded; + const isClamped = overflowed && !expanded; - return ( -
- {attachment.filepath && ( - - )} -
-
-          {text}
-        
- {overflowed && ( - + return ( +
+ {attachment.filepath && showFileChip && ( + + )} +
+ {!showFileChip && ( +
+ + {visibleFilename} + + {attachment.filepath && ( + + )} +
+ )} +
+
+              {text}
+            
+ {overflowed && ( + + )} +
+
-
- ); -}); + ); + }, +); const ImageAttachment = memo(({ attachment }: { attachment: TAttachment }) => { const [isLoaded, setIsLoaded] = useState(false); diff --git a/client/src/components/Chat/Messages/Content/Parts/__tests__/ArtifactRouting.test.tsx b/client/src/components/Chat/Messages/Content/Parts/__tests__/ArtifactRouting.test.tsx index 36eaf2c8a4..70a1661dcb 100644 --- a/client/src/components/Chat/Messages/Content/Parts/__tests__/ArtifactRouting.test.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/__tests__/ArtifactRouting.test.tsx @@ -762,8 +762,15 @@ describe('AttachmentGroup routing', () => { expect(toggle).toHaveAttribute('aria-expanded', 'false'); const panel = document.getElementById(toggle.getAttribute('aria-controls') ?? ''); expect(panel?.firstElementChild).toHaveAttribute('aria-hidden', 'true'); - expect(container.querySelector('pre')).toBeNull(); expect(screen.getByTestId('image')).toBeInTheDocument(); + expect(screen.getAllByTestId('file-container').map((chip) => chip.textContent)).not.toContain( + 'c.json', + ); + + fireEvent.click(toggle); + expect(toggle).toHaveAttribute('aria-expanded', 'true'); + expect(screen.getByText('c.json')).toBeInTheDocument(); + expect(container.querySelector('pre')?.textContent).toBe('{"c":true}'); }); it('passes a non-dotfile filename through to FileContainer unchanged', () => { @@ -869,12 +876,14 @@ describe('AttachmentGroup routing', () => { // Mermaid render expect(screen.getByTestId('mermaid-render')).toBeInTheDocument(); // JSON and plain zip are both downloadable file outputs, so they collapse together. - expect(container.querySelector('pre')).toBeNull(); - expect(screen.getByRole('button', { name: 'com_ui_show_n_files' })).toHaveAttribute( - 'aria-expanded', - 'false', - ); - // FileContainer chips are still mounted inside the collapsed panel. - expect(screen.getAllByTestId('file-container').length).toBeGreaterThanOrEqual(2); + const toggle = screen.getByRole('button', { name: 'com_ui_show_n_files' }); + expect(toggle).toHaveAttribute('aria-expanded', 'false'); + const chipLabels = screen.getAllByTestId('file-container').map((chip) => chip.textContent); + expect(chipLabels).toContain('archive.zip'); + expect(chipLabels).not.toContain('data.json'); + + fireEvent.click(toggle); + expect(screen.getByText('data.json')).toBeInTheDocument(); + expect(container.querySelector('pre')?.textContent).toBe('{"a":1}'); }); });