| import React from 'react'; |
| import { MODES } from '../lib/modes.js'; |
| import { NoteBody } from './MarginNote.jsx'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export default function FigurePopover({ fig, n, pdfHash, pos, commentary, generating, onGenerate, onClose }) { |
| const ref = React.useRef(null); |
| const [mode, setMode] = React.useState('history'); |
| const [imgError, setImgError] = React.useState(false); |
|
|
| const key = `${fig.id}:${mode}`; |
| const entry = commentary[key]; |
| const isGenerating = !!generating[key]; |
|
|
| React.useLayoutEffect(() => { |
| const el = ref.current; if (!el) return; |
| const r = el.getBoundingClientRect(); |
| let x = pos.x, y = pos.y + 10; |
| if (x + r.width > window.innerWidth - 16) x = window.innerWidth - r.width - 16; |
| if (y + r.height > window.innerHeight - 16) y = pos.y - r.height - 10; |
| el.style.left = Math.max(8, x) + 'px'; |
| el.style.top = Math.max(8, y) + 'px'; |
| el.style.opacity = 1; |
| }, [fig, pos]); |
|
|
| React.useEffect(() => { |
| const onKey = (e) => { if (e.key === 'Escape') onClose(); }; |
| window.addEventListener('keydown', onKey); |
| return () => window.removeEventListener('keydown', onKey); |
| }, [onClose]); |
|
|
| function handleDotClick(modeId) { |
| setMode(modeId); |
| if (!commentary[`${fig.id}:${modeId}`]) { |
| onGenerate(fig.id, modeId); |
| } |
| } |
|
|
| function handleRegenerate() { |
| onGenerate(fig.id, mode); |
| } |
|
|
| const imgSrc = `/api/figures/${pdfHash}/${fig.id}.png`; |
|
|
| return ( |
| <div> |
| <div className="ap-pop-backdrop" onClick={onClose}></div> |
| <div className="ap-pop ap-figure-popover" ref={ref} style={{ opacity: 0 }}> |
| <div className="ap-pop-rank">plate · figure {n}</div> |
| {fig.hasImage && !imgError && ( |
| <img |
| src={imgSrc} |
| alt={fig.label} |
| style={{ maxWidth: '100%', display: 'block', marginBottom: '10px' }} |
| onError={() => setImgError(true)} |
| /> |
| )} |
| <div className="ap-pop-abstract">{fig.caption}</div> |
| <div className="ap-pop-links" style={{ borderTop: '1px solid color-mix(in oklab, var(--ink) 12%, var(--paper))', marginTop: '12px', paddingTop: '10px' }}></div> |
| <div className="ap-note-head" style={{ marginTop: '2px' }}> |
| <span className="ap-note-mode" style={{ '--mode-c': 'var(--mc-' + mode + ')' }}> |
| {MODES.find((m) => m.id === mode)?.label} |
| </span> |
| {entry && entry.source === 'llm' && !isGenerating && ( |
| <span className="ap-note-gen" title="generated by the annotator">✦</span> |
| )} |
| {entry && !isGenerating && ( |
| <button className="ap-note-regen" title="regenerate this note" onClick={handleRegenerate}>↻</button> |
| )} |
| </div> |
| <NoteBody entry={entry} generating={isGenerating} onRegenerate={handleRegenerate} /> |
| <div className="ap-note-modes" role="group" aria-label="commentary mode" style={{ marginTop: '9px' }}> |
| {MODES.map((mm) => ( |
| <button |
| key={mm.id} |
| className={'ap-mode-dot' + (mm.id === mode ? ' is-active' : '')} |
| style={{ '--mode-c': 'var(--mc-' + mm.id + ')' }} |
| title={mm.label} |
| onClick={() => handleDotClick(mm.id)} |
| >{mm.glyph}</button> |
| ))} |
| </div> |
| <button className="ap-pop-close" onClick={onClose} title="close">×</button> |
| </div> |
| </div> |
| ); |
| } |
|
|