diff --git a/app/ui_layer/browser/frontend/src/components/ui/AttachmentPreviewModal.module.css b/app/ui_layer/browser/frontend/src/components/ui/AttachmentPreviewModal.module.css index d97049f7..40d2082f 100644 --- a/app/ui_layer/browser/frontend/src/components/ui/AttachmentPreviewModal.module.css +++ b/app/ui_layer/browser/frontend/src/components/ui/AttachmentPreviewModal.module.css @@ -3,6 +3,10 @@ } .metaBar { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); padding: var(--space-2) var(--space-4); font-size: var(--text-xs); color: var(--text-secondary); @@ -10,6 +14,34 @@ flex-shrink: 0; } +.viewToggle { + display: flex; + gap: var(--space-1); + flex-shrink: 0; +} + +.viewToggleBtn, +.viewToggleBtnActive { + padding: var(--space-1) var(--space-3); + border: none; + border-radius: var(--radius-sm); + background: none; + font-size: var(--text-xs); + font-family: inherit; + cursor: pointer; + color: var(--text-secondary); +} + +.viewToggleBtn:hover { + color: var(--text-primary); +} + +.viewToggleBtnActive { + background: var(--bg-primary); + color: var(--text-primary); + font-weight: var(--font-semibold); +} + /* No fixed dimensions: the viewer hugs whichever content branch renders. Centering keeps a small image visually anchored when the modal's min-width forces the box wider than the image's intrinsic size. */ @@ -58,6 +90,15 @@ box-sizing: border-box; } +.markdownPreview { + width: min(1280px, 92vw); + height: calc(92vh - 100px); + padding: var(--space-4); + overflow: auto; + box-sizing: border-box; + background: var(--bg-primary); +} + .message { padding: var(--space-6) var(--space-8); color: var(--text-secondary); diff --git a/app/ui_layer/browser/frontend/src/components/ui/AttachmentPreviewModal.tsx b/app/ui_layer/browser/frontend/src/components/ui/AttachmentPreviewModal.tsx index 0f0fc3f7..7014d153 100644 --- a/app/ui_layer/browser/frontend/src/components/ui/AttachmentPreviewModal.tsx +++ b/app/ui_layer/browser/frontend/src/components/ui/AttachmentPreviewModal.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useState } from 'react' import { Modal } from './Modal' +import { MarkdownContent } from './MarkdownContent' import styles from './AttachmentPreviewModal.module.css' // A single shape that covers both pre-send attachments (base64 in memory) @@ -33,7 +34,8 @@ function classify(att: AttachmentPreviewItem) { || TEXT_MIMES.has(att.type) || TEXT_EXT_RE.test(att.name) ) - return { isImage, isPdf, isText } + const isMarkdown = isText && att.name.toLowerCase().endsWith('.md') + return { isImage, isPdf, isText, isMarkdown } } function formatFileSize(bytes: number): string { @@ -45,7 +47,10 @@ function formatFileSize(bytes: number): string { } export function AttachmentPreviewModal({ isOpen, attachment, onClose }: AttachmentPreviewModalProps) { - const kind = attachment ? classify(attachment) : null + // Memoized: classify() returns a new object each call, and an unstable + // identity here would re-trigger the fetch effect below on every render + // it itself causes (infinite fetch/abort loop). + const kind = useMemo(() => (attachment ? classify(attachment) : null), [attachment]) const imageSrc = useMemo(() => { if (!attachment || !kind?.isImage) return null @@ -74,8 +79,10 @@ export function AttachmentPreviewModal({ isOpen, attachment, onClose }: Attachme const [textContent, setTextContent] = useState(null) const [textLoading, setTextLoading] = useState(false) const [textError, setTextError] = useState(null) + const [mdView, setMdView] = useState<'preview' | 'source'>('preview') useEffect(() => { + setMdView('preview') if (!attachment || !kind?.isText) { setTextContent(null) setTextLoading(false) @@ -125,6 +132,7 @@ export function AttachmentPreviewModal({ isOpen, attachment, onClose }: Attachme {kind.isText && lineCount > 0 && <> ยท {lineCount} line{lineCount !== 1 ? 's' : ''}} ) + const showMarkdownToggle = kind.isMarkdown && textContent != null return ( -
{meta}
+
+ {meta} + {showMarkdownToggle && ( +
+ + +
+ )} +
{kind.isImage && imageSrc && ( {attachment.name} @@ -148,7 +176,11 @@ export function AttachmentPreviewModal({ isOpen, attachment, onClose }: Attachme ) : textError ? (
{textError}
) : textContent != null ? ( -
{textContent}
+ showMarkdownToggle && mdView === 'preview' ? ( + + ) : ( +
{textContent}
+ ) ) : null )} {!kind.isImage && !kind.isPdf && !kind.isText && (