From 42d4b016fb2813367ef9a98fb8916011de0f08bf Mon Sep 17 00:00:00 2001 From: lashman Date: Tue, 10 Mar 2026 23:59:21 +0200 Subject: [PATCH] disambiguation popup --- .../src/components/DisambiguationPopup.tsx | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 frontend/src/components/DisambiguationPopup.tsx diff --git a/frontend/src/components/DisambiguationPopup.tsx b/frontend/src/components/DisambiguationPopup.tsx new file mode 100644 index 0000000..e3e9bdd --- /dev/null +++ b/frontend/src/components/DisambiguationPopup.tsx @@ -0,0 +1,80 @@ +import { useEffect, useLayoutEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useStore } from '../store' +import Tooltip from './ui/Tooltip' + +interface Props { + svgRef: React.RefObject +} + +interface PopupGeom { + top: number + left: number +} + +export default function DisambiguationPopup({ svgRef }: Props) { + const { t } = useTranslation() + const show = useStore((s) => s.showDisambiguation) + const result = useStore((s) => s.recognitionResult) + const puzzle = useStore((s) => s.puzzle) + const acceptCandidate = useStore((s) => s.acceptCandidate) + const dismissDisambiguation = useStore((s) => s.dismissDisambiguation) + + const popupRef = useRef(null) + const [geom, setGeom] = useState(null) + + useLayoutEffect(() => { + if (show && result && puzzle && svgRef.current) { + const rect = svgRef.current.getBoundingClientRect() + const cellW = rect.width / puzzle.width + const cellH = rect.height / puzzle.height + const cellX = result.cell.col * cellW + const cellY = result.cell.row * cellH + // position above the cell, or below if near the top + const above = result.cell.row > 1 + setGeom({ top: above ? cellY - 40 : cellY + cellH + 4, left: Math.max(0, cellX - 20) }) + } else { + setGeom(null) + } + }, [show, result, puzzle, svgRef]) + + useEffect(() => { + if (show && popupRef.current) { + const firstBtn = popupRef.current.querySelector('.disambiguation-btn') + firstBtn?.focus() + } + }, [show]) + + if (!show || !result || !geom) return null + + return ( +
+ {result.candidates.slice(0, 3).map((c) => ( + + + + ))} + + + +
+ ) +}