Add inline preview for markdown and plain-text file attachments

Clicking a .md/.txt attachment now opens a modal instead of downloading,
with an explicit download button still available inside it. Markdown
renders through the same XSS-safe renderer used for chat messages;
plain text renders as literal escaped content via <pre>.

No backend change needed: the preview content is read via fetch(), which
is unaffected by the Content-Disposition: attachment header the file-serve
endpoint always sends (that header only steers the browser's own
navigation/embed rendering, not a script-initiated body read) -- so the
existing download-forcing security behavior from #13 stays intact.
Non-previewable types (PDF, etc.) are unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ksmithandClaude Sonnet 5 committed 2026-08-15 21:30:29 -06:00
1 parent 14dc340174
commit d7e777cbd8
4 files changed
+273 -36

No files matched your search

+15 -16
View File
@@ -46,20 +46,19 @@ function preserveLineBreaks(text: string): string {
.join('\n')
}
export function MessageContent({ content }: MessageContentProps) {
return (
<Markdown
options={{
// The core XSS mitigation: raw HTML in message content is escaped
// and printed literally instead of being parsed into elements.
disableParsingRawHTML: true,
overrides: {
a: { props: { target: '_blank', rel: 'noopener noreferrer' } },
img: { component: MarkdownImageLink },
},
}}
>
{preserveLineBreaks(content)}
</Markdown>
)
// Shared with FilePreviewModal so both render paths carry the exact same
// XSS mitigation (disableParsingRawHTML) -- duplicating this object would
// risk the two drifting out of sync if one gets edited later.
export const MARKDOWN_OPTIONS = {
// The core XSS mitigation: raw HTML in message content is escaped
// and printed literally instead of being parsed into elements.
disableParsingRawHTML: true,
overrides: {
a: { props: { target: '_blank', rel: 'noopener noreferrer' } },
img: { component: MarkdownImageLink },
},
}
export function MessageContent({ content }: MessageContentProps) {
return <Markdown options={MARKDOWN_OPTIONS}>{preserveLineBreaks(content)}</Markdown>
}