diff --git a/ui/web/src/components/chat/media-gallery.tsx b/ui/web/src/components/chat/media-gallery.tsx
index 029d90a4..084984b5 100644
--- a/ui/web/src/components/chat/media-gallery.tsx
+++ b/ui/web/src/components/chat/media-gallery.tsx
@@ -1,6 +1,14 @@
-import { useState } from "react";
-import { File, FileText, FileCode, Music, Film } from "lucide-react";
+import { useState, useCallback } from "react";
+import { Download, FileText, FileCode, Music, Film, File } from "lucide-react";
import { ImageLightbox } from "@/components/shared/image-lightbox";
+import { MarkdownRenderer } from "@/components/shared/markdown-renderer";
+import { formatSize } from "@/lib/file-helpers";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
import type { MediaItem } from "@/types/chat";
interface MediaGalleryProps {
@@ -17,8 +25,42 @@ function fileIcon(kind: MediaItem["kind"]) {
}
}
+function isMarkdownExt(name: string): boolean {
+ return /\.(md|mdx|markdown)$/i.test(name);
+}
+
+function isMediaKind(kind: string): "image" | "audio" | "video" | null {
+ if (kind === "image" || kind === "audio" || kind === "video") return kind;
+ return null;
+}
+
export function MediaGallery({ items }: MediaGalleryProps) {
const [lightboxIdx, setLightboxIdx] = useState
(null);
+ const [preview, setPreview] = useState<{
+ name: string;
+ href: string;
+ content: string;
+ mediaType?: "image" | "audio" | "video";
+ } | null>(null);
+ const [loading, setLoading] = useState(false);
+
+ const handleFileClick = useCallback((item: MediaItem) => {
+ const media = isMediaKind(item.kind);
+ if (media) {
+ setPreview({ name: item.fileName ?? "file", href: item.path, content: "", mediaType: media });
+ return;
+ }
+ // Text/code/document files: fetch content for preview
+ setLoading(true);
+ fetch(item.path)
+ .then((res) => {
+ if (!res.ok) throw new Error(res.statusText);
+ return res.text();
+ })
+ .then((text) => setPreview({ name: item.fileName ?? "file", href: item.path, content: text }))
+ .catch(() => { /* fetch failed — file may not exist yet, ignore */ })
+ .finally(() => setLoading(false));
+ }, []);
if (items.length === 0) return null;
@@ -50,16 +92,27 @@ export function MediaGallery({ items }: MediaGalleryProps) {
{files.length > 0 && (
)}
@@ -71,6 +124,43 @@ export function MediaGallery({ items }: MediaGalleryProps) {
onClose={() => setLightboxIdx(null)}
/>
)}
+
+ {loading && (
+
+ )}
+
+
);
}
diff --git a/ui/web/src/components/chat/message-bubble.tsx b/ui/web/src/components/chat/message-bubble.tsx
index aef2b449..24fe6a27 100644
--- a/ui/web/src/components/chat/message-bubble.tsx
+++ b/ui/web/src/components/chat/message-bubble.tsx
@@ -3,6 +3,7 @@ import { MessageContent } from "./message-content";
import { ThinkingBlock } from "./thinking-block";
import { ToolCallCard } from "./tool-call-card";
import { BlockReplyBubble } from "./block-reply-bubble";
+import { MediaGallery } from "./media-gallery";
import type { ChatMessage } from "@/types/chat";
interface MessageBubbleProps {
@@ -47,11 +48,11 @@ export function MessageBubble({ message }: MessageBubbleProps) {
))}