import React, { useEffect, useRef, useState, useCallback } from 'react'; import { Copy, Sparkles, CheckCircle, Search } from 'lucide-react'; import { useAIStore } from '../../stores/aiStore'; import { useReaderStore, useUIStore } from '../../stores/appStore'; import { useSearchStore } from '../../stores/searchStore'; import { motion, AnimatePresence } from 'framer-motion'; interface PopupPos { x: number; y: number } const SelectionPopup: React.FC = () => { const [pos, setPos] = useState(null); const [selected, setSelected] = useState(''); const [copied, setCopied] = useState(false); const popupRef = useRef(null); const { askAI } = useAIStore(); const { currentVolume, currentPage, volumeTitle } = useReaderStore(); const { setNavOpen, setActiveTab } = useUIStore(); const { setQuery, performSearch } = useSearchStore(); const hide = useCallback(() => { setPos(null); setSelected(''); setCopied(false); }, []); useEffect(() => { const onPointerUp = (e: MouseEvent | TouchEvent) => { // Don't interfere if the pointer was released inside the popup — // clicking the copy button clears the selection, which would // trigger hide() and prevent the CheckCircle confirmation from showing. if (popupRef.current?.contains(e.target as Node)) return; // Small delay — let browser finalise selection rect setTimeout(() => { const sel = window.getSelection(); const text = sel?.toString().trim() ?? ''; if (!text || text.length < 2) { hide(); return; } // Only fire inside .reader-content elements const anchor = sel?.anchorNode?.parentElement; if (!anchor?.closest('.reader-content')) { hide(); return; } const range = sel!.getRangeAt(0); const rect = range.getBoundingClientRect(); setSelected(text); // getBoundingClientRect() = viewport-relative → position:fixed also viewport-relative // No window.scrollX/Y needed since we're using fixed positioning setPos({ x: rect.left + rect.width / 2, y: rect.top - 8, // 8px gap above selection }); setCopied(false); }, 10); }; // Click outside popup → close const onMouseDown = (e: MouseEvent) => { if (popupRef.current && !popupRef.current.contains(e.target as Node)) { hide(); } }; document.addEventListener('mouseup', onPointerUp); document.addEventListener('touchend', onPointerUp); document.addEventListener('mousedown', onMouseDown); return () => { document.removeEventListener('mouseup', onPointerUp); document.removeEventListener('touchend', onPointerUp); document.removeEventListener('mousedown', onMouseDown); }; }, [hide]); const handleCopy = async () => { try { await navigator.clipboard.writeText(selected); } catch { // Safari fallback const ta = document.createElement('textarea'); ta.value = selected; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } setCopied(true); setTimeout(hide, 900); }; const handleAsk = () => { // askAI(question, context) — sets isOpen: true automatically // Include volume/page metadata so the LLM can cite the actual source const title = volumeTitle ? ` (${volumeTitle})` : ''; const context = `เล่มที่ ${currentVolume} หน้าที่ ${currentPage}${title}\n\n${selected}`; askAI( `วิเคราะห์ข้อความต่อไปนี้จากพระไตรปิฎก:\n\n"${selected}"`, context, ); hide(); }; const handleSearch = () => { setQuery(selected); setActiveTab('search'); setNavOpen(true); setTimeout(() => { performSearch(); }, 50); hide(); }; return ( {pos && ( e.preventDefault()} className="flex items-center gap-1 px-2 py-1.5 rounded-xl bg-[#1a1a2e] border border-[#3a3a5e] shadow-2xl select-none" > {/* ── Copy ─────────────────────────────────────────────── */} {/* divider */}
{/* ── Search ────────────────────────────────────────────── */} {/* divider */}
{/* ── AI Analyse ───────────────────────────────────────── */} {/* tail arrow pointing down */}
)} ); }; export default SelectionPopup;