/** * Page.jsx * * This component displays a single page image and highlights a specific circle on it. * - Reads query parameters `image` (page image URL) and `circle` (circle text to highlight) * - Loads the image and calculates scale info for proper overlays * - Sends the image to the backend detection API to get all circles * - Finds the circle that matches the target text and highlights it * - Supports zooming via buttons and mouse wheel */ import { useState, useEffect, useRef } from "react"; import { useSearchParams } from "react-router-dom"; import ZoomControls from "./ZoomControls"; import useZoom from "../hooks/useZoom"; import "../styles/Zoom.css"; function Page() { const [searchParams] = useSearchParams(); const targetCircleText = searchParams.get("circle"); const pageImage = searchParams.get("image"); const [highlightCircle, setHighlightCircle] = useState(null); const [imageInfo, setImageInfo] = useState(null); const imgRef = useRef(null); const wrapperRef = useRef(null); const { zoom, zoomIn, zoomOut, handleWheel } = useZoom({ min: 1, max: 3, step: 0.25 }); const handleImageLoad = () => { if (!imgRef.current) return; const info = { naturalWidth: imgRef.current.naturalWidth, naturalHeight: imgRef.current.naturalHeight, clientWidth: imgRef.current.clientWidth, clientHeight: imgRef.current.clientHeight, scaleX: imgRef.current.clientWidth / imgRef.current.naturalWidth, scaleY: imgRef.current.clientHeight / imgRef.current.naturalHeight, }; setImageInfo(info); }; useEffect(() => { if (!pageImage || !targetCircleText) { setHighlightCircle(null); return; } const detect = async () => { try { const blob = await fetch(pageImage).then((res) => res.blob()); const formData = new FormData(); formData.append("file", blob, "page.png"); const resp = await fetch("/detect/", { method: "POST", body: formData, }); if (!resp.ok) throw new Error(`Detection failed: ${await resp.text()}`); const data = await resp.json(); const circles = data.circles || []; const targetCircle = circles.find( (c) => c.circle_text && c.circle_text.trim().toLowerCase() === targetCircleText.trim().toLowerCase() ); setHighlightCircle(targetCircle || null); } catch (err) { console.error("Detection error:", err); setHighlightCircle(null); } }; detect(); }, [pageImage, targetCircleText]); const getScaledCircle = () => { if (!highlightCircle || !imageInfo) return null; return { cx: highlightCircle.x * imageInfo.scaleX, cy: highlightCircle.y * imageInfo.scaleY, r: highlightCircle.r * Math.min(imageInfo.scaleX, imageInfo.scaleY), }; }; const scaledCircle = getScaledCircle(); return (