Skip to content

Commit 438e46a

Browse files
committed
feat: Implement base64 image preview with hover tooltip and expand functionality in JsonDisplay.
1 parent 66e774c commit 438e46a

File tree

2 files changed

+97
-1
lines changed

2 files changed

+97
-1
lines changed

frontend/components/JsonDisplay.tsx

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

22
import React, { useState, useRef, useEffect, useCallback } from 'react';
3-
import { CheckIcon, ChevronDownIcon, ClipboardIcon, SearchIcon, XIcon, ArrowUpwardIcon, ArrowDownwardIcon, CaseSensitiveIcon, WholeWordIcon, RegexIcon } from './icons/material-icons-imports';
3+
import { CheckIcon, ChevronDownIcon, ClipboardIcon, SearchIcon, XIcon, ArrowUpwardIcon, ArrowDownwardIcon, CaseSensitiveIcon, WholeWordIcon, RegexIcon, ImageIcon } from './icons/material-icons-imports';
44

55
// Search context for highlighting and navigation
66
interface SearchState {
@@ -201,6 +201,96 @@ const ObjectIdDisplay: React.FC<{
201201
);
202202
};
203203

204+
const Base64ImagePreview: React.FC<{
205+
base64String: string;
206+
searchRegex: RegExp | null;
207+
}> = ({ base64String, searchRegex }) => {
208+
const [isExpanded, setIsExpanded] = useState(false);
209+
const [previewPos, setPreviewPos] = useState<{ x: number; y: number } | null>(null);
210+
const iconRef = useRef<HTMLSpanElement>(null);
211+
212+
const handleMouseEnter = () => {
213+
if (iconRef.current) {
214+
const rect = iconRef.current.getBoundingClientRect();
215+
const previewHeight = 220; // Approx max height
216+
const previewWidth = 220; // Approx max width
217+
218+
// Default to showing above
219+
let top = rect.top - previewHeight - 8;
220+
let left = rect.left;
221+
222+
// If not enough space on top, show below
223+
if (top < 10) {
224+
top = rect.bottom + 8;
225+
}
226+
227+
// Prevent right overflow
228+
if (left + previewWidth > window.innerWidth) {
229+
left = window.innerWidth - previewWidth - 10;
230+
}
231+
232+
// Prevent left overflow
233+
if (left < 10) left = 10;
234+
235+
setPreviewPos({ x: left, y: top });
236+
}
237+
};
238+
239+
const handleMouseLeave = () => {
240+
setPreviewPos(null);
241+
};
242+
243+
if (isExpanded) {
244+
return (
245+
<span
246+
className="cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-700 rounded px-1 -ml-1 transition-colors"
247+
onClick={() => setIsExpanded(false)}
248+
title="Click to collapse back to icon"
249+
>
250+
<span className="text-emerald-700 dark:text-emerald-300">"
251+
{searchRegex ? highlightText(base64String, searchRegex) : base64String}"
252+
</span>
253+
</span>
254+
);
255+
}
256+
257+
return (
258+
<span
259+
className="inline-flex items-center gap-2 group relative select-none"
260+
onMouseEnter={handleMouseEnter}
261+
onMouseLeave={handleMouseLeave}
262+
>
263+
<span
264+
ref={iconRef}
265+
className="cursor-pointer"
266+
onClick={() => {
267+
setIsExpanded(true);
268+
setPreviewPos(null);
269+
}}
270+
>
271+
<ImageIcon className="w-5 h-5 text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-300 transition-colors" />
272+
</span>
273+
274+
{/* Tooltip Image Preview - Uses fixed positioning to escape container clipping */}
275+
{previewPos && (
276+
<div
277+
className="fixed z-[9999] bg-white dark:bg-slate-800 p-2 rounded-lg shadow-xl border border-slate-200 dark:border-slate-600 pointer-events-none"
278+
style={{
279+
left: `${previewPos.x}px`,
280+
top: `${previewPos.y}px`
281+
}}
282+
>
283+
<img
284+
src={base64String}
285+
alt="Preview"
286+
className="max-w-[200px] max-h-[200px] object-contain rounded bg-slate-100 dark:bg-slate-900"
287+
/>
288+
</div>
289+
)}
290+
</span>
291+
);
292+
};
293+
204294
// A single, recursive component to render all parts of the JSON object.
205295
const JsonNode: React.FC<{
206296
nodeValue: any;
@@ -384,6 +474,11 @@ const JsonNode: React.FC<{
384474
</span>
385475
);
386476
}
477+
478+
if (nodeValue.startsWith("data:image/png;base64,")) {
479+
return <Base64ImagePreview base64String={nodeValue} searchRegex={searchRegex} />;
480+
}
481+
387482
return <span className="text-emerald-700 dark:text-emerald-300">"
388483
{searchRegex ? highlightText(nodeValue, searchRegex) : nodeValue}"
389484
</span>;

frontend/components/icons/material-icons-imports.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,4 @@ export { default as CaseSensitiveIcon } from '@mui/icons-material/Abc';
6565
export { default as WholeWordIcon } from '@mui/icons-material/ShortText';
6666
export { default as RegexIcon } from '@mui/icons-material/Code';
6767
export { default as DeleteIcon } from '@mui/icons-material/Delete';
68+
export { default as ImageIcon } from '@mui/icons-material/Image';

0 commit comments

Comments
 (0)