disambiguation popup
This commit is contained in:
@@ -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<SVGSVGElement | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
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<HTMLDivElement>(null)
|
||||||
|
const [geom, setGeom] = useState<PopupGeom | null>(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<HTMLButtonElement>('.disambiguation-btn')
|
||||||
|
firstBtn?.focus()
|
||||||
|
}
|
||||||
|
}, [show])
|
||||||
|
|
||||||
|
if (!show || !result || !geom) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={popupRef}
|
||||||
|
className="disambiguation"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: geom.top,
|
||||||
|
left: geom.left,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{result.candidates.slice(0, 3).map((c) => (
|
||||||
|
<Tooltip text={`${Math.round(c.confidence * 100)}%`} key={c.letter}>
|
||||||
|
<button
|
||||||
|
className="disambiguation-btn"
|
||||||
|
onClick={() => acceptCandidate(c.letter)}
|
||||||
|
>
|
||||||
|
{c.letter}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
<Tooltip text={t('common.dismiss')}>
|
||||||
|
<button
|
||||||
|
className="disambiguation-btn dismiss"
|
||||||
|
onClick={dismissDisambiguation}
|
||||||
|
>
|
||||||
|
x
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user