diff --git a/ui/src/components/BoundingBoxOverlay.tsx b/ui/src/components/BoundingBoxOverlay.tsx index 44ee961..b5a5130 100644 --- a/ui/src/components/BoundingBoxOverlay.tsx +++ b/ui/src/components/BoundingBoxOverlay.tsx @@ -1,6 +1,22 @@ import { useRef, useState } from 'react'; import classNames from 'classnames'; +// Per-box color so multiple boxes are individually distinguishable (not all one +// color). Hue is spread by the golden-angle so adjacent indices look distinct; +// stable per index so the same box keeps its color across the editor/overlay and +// matches its #n chip in the sidebar. +export function boxColor(index: number): string { + const hue = (index * 137.508) % 360; + return `hsl(${hue.toFixed(1)}, 80%, 60%)`; +} + +// Same hue as boxColor but with an alpha, for tinting backgrounds (e.g. the +// sidebar element cards) so they visually match their box on the image. +export function boxColorAlpha(index: number, alpha: number): string { + const hue = (index * 137.508) % 360; + return `hsla(${hue.toFixed(1)}, 80%, 60%, ${alpha})`; +} + // A single bbox parsed from an Ideogram-style caption/prompt. Stored coords are // normalized 0-1000 in [y1, x1, y2, x2] order (top-left origin). export interface OverlayBox { @@ -227,8 +243,9 @@ export function BoundingBoxEditor({ > {boxes.map(box => { const pos = draft && draft.index === box.elementIndex ? draft.box : box; - const isObj = box.type === 'obj'; const selected = selectedIndex === box.elementIndex; + const showSelected = selected && !drawing; + const color = boxColor(box.elementIndex); return (
{box.label && ( {box.label} @@ -295,36 +309,32 @@ export function BoundingBoxEditor({ export default function BoundingBoxOverlay({ boxes }: { boxes: OverlayBox[] }) { return (
- {boxes.map((b, i) => ( -
- {b.label && ( - - {b.label} - - )} -
- ))} + {boxes.map((b, i) => { + const color = boxColor(i); + return ( +
+ {b.label && ( + + {b.label} + + )} +
+ ); + })}
); } diff --git a/ui/src/components/DatasetImageViewer.tsx b/ui/src/components/DatasetImageViewer.tsx index 1a86561..e7324da 100644 --- a/ui/src/components/DatasetImageViewer.tsx +++ b/ui/src/components/DatasetImageViewer.tsx @@ -11,6 +11,7 @@ import { isVideo, isAudio } from '@/utils/basic'; import AudioPlayer from './AudioPlayer'; import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch'; import { BoundingBoxEditor, parseBoundingBoxes, extractBoxes } from './BoundingBoxOverlay'; +import IdeogramCaptionSidebar, { isIdeogramCaption } from './IdeogramCaptionSidebar'; function safeParse(text: string): any { try { @@ -42,6 +43,8 @@ export default function DatasetImageViewer({ imgPath, imageList, onChange, refre const currentImgPathRef = useRef(null); const captionAbortRef = useRef(null); + const isIdeogram = useMemo(() => isIdeogramCaption(caption), [caption]); + useEffect(() => setMounted(true), []); // Clear box selection / draw mode whenever the image changes. @@ -50,6 +53,11 @@ export default function DatasetImageViewer({ imgPath, imageList, onChange, refre setIsDrawing(false); }, [imgPath]); + // Default to showing the editable boxes when an Ideogram caption is present. + useEffect(() => { + setShowBoxes(isIdeogram); + }, [isIdeogram, imgPath]); + // open/close based on external value useEffect(() => { setIsOpen(Boolean(imgPath)); @@ -269,31 +277,6 @@ export default function DatasetImageViewer({ imgPath, imageList, onChange, refre [editCaption], ); - const handleFieldChange = useCallback( - (field: 'desc' | 'text', value: string) => { - editCaption(els => { - if (selectedBoxIndex != null && els[selectedBoxIndex]) els[selectedBoxIndex][field] = value; - }); - }, - [editCaption, selectedBoxIndex], - ); - - const handleTypeChange = useCallback( - (type: 'obj' | 'text') => { - editCaption(els => { - const el = selectedBoxIndex != null ? els[selectedBoxIndex] : null; - if (!el) return; - el.type = type; - if (type === 'text') { - if (el.text == null) el.text = ''; - } else { - delete el.text; - } - }); - }, - [editCaption, selectedBoxIndex], - ); - // keyboard events while open — skip nav while caption textarea is focused useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -390,12 +373,8 @@ export default function DatasetImageViewer({ imgPath, imageList, onChange, refre const boundingBoxes = useMemo(() => parseBoundingBoxes(caption), [caption]); const canShowBoxes = Boolean(boundingBoxes && imgPath && !isAudio(imgPath) && !isVideo(imgPath)); - // Boxes and the selected element are derived from the (locally edited) caption. + // Boxes are derived from the (locally edited) caption for the image overlay. const editBoxes = useMemo(() => extractBoxes(safeParse(caption)), [caption]); - const selectedElement = useMemo(() => { - if (selectedBoxIndex == null) return null; - return safeParse(caption)?.compositional_deconstruction?.elements?.[selectedBoxIndex] ?? null; - }, [caption, selectedBoxIndex]); if (!mounted) return null; @@ -530,106 +509,36 @@ export default function DatasetImageViewer({ imgPath, imageList, onChange, refre {currentIndex >= 0 ? `${currentIndex + 1} / ${imageList.length}` : ''}
-
-