diff --git a/web/src/components/Chat/MessageBubble.tsx b/web/src/components/Chat/MessageBubble.tsx index 510f0f0..38a02bf 100644 --- a/web/src/components/Chat/MessageBubble.tsx +++ b/web/src/components/Chat/MessageBubble.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react' -import { User, Bot, Wrench, CheckCircle, AlertCircle, Terminal, File, Image, FileText, Music, Video, Download, ChevronDown, ChevronRight, Copy, Check, Loader2, XCircle, Clock, Loader } from 'lucide-react' +import { useState, useEffect } from 'react' +import { User, Bot, Wrench, CheckCircle, AlertCircle, Terminal, File, Image, FileText, Music, Video, Download, ChevronDown, ChevronRight, Copy, Check, Loader2, XCircle, Clock, Loader, X } from 'lucide-react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import type { ChatMessage, Attachment, TaskToolResult } from '../../types/protocol' @@ -144,6 +144,68 @@ function AttachmentCard({ attachment }: { attachment: Attachment }) { ) } +function ImageLightbox({ src, mimeType, fileName, onClose }: { src: string; mimeType: string; fileName?: string; onClose: () => void }) { + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [onClose]) + + const handleDownload = (e: React.MouseEvent) => { + e.stopPropagation() + const byteChars = atob(src) + const byteNums = new Array(byteChars.length) + for (let i = 0; i < byteChars.length; i++) { + byteNums[i] = byteChars.charCodeAt(i) + } + const byteArr = new Uint8Array(byteNums) + const blob = new Blob([byteArr], { type: mimeType }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = fileName || `image.${mimeType.split('/')[1] || 'png'}` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + + return ( +