import { addEpisode } from "~utils/api" import { isExtensionContextValid } from "~utils/context" import { createRoot, type Root } from "react-dom/client" import { createElement } from "react" import TextSelectionPopup from "~components/TextSelectionPopup" let selectionDot: HTMLDivElement | null = null let selectionPopup: HTMLDivElement | null = null let selectionRoot: Root | null = null let autoHideTimer: NodeJS.Timeout | null = null let isHoveringElements = false // Logo dot for text selection function createSelectionDot(rect: DOMRect) { removeSelectionElements() selectionDot = document.createElement("div") selectionDot.style.cssText = ` position: absolute; width: 54px; height: 20px; cursor: pointer; z-index: 10001; opacity: 0; transition: opacity 0.2s ease; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2)); background: #c15e50; border-radius: 6px; padding: 4px; display: flex; flex-direction: column; align-items: center; justify-content: center; ` // Add the logo SVG with white color and Core text selectionDot.innerHTML = `
Core
` const top = rect.bottom + window.scrollY + 5 const left = Math.min( rect.right + window.scrollX - 16, window.innerWidth - 40 ) selectionDot.style.top = `${top}px` selectionDot.style.left = `${left}px` selectionDot.addEventListener("click", showSelectionPopup) selectionDot.addEventListener("mouseenter", showSelectionPopup) document.body.appendChild(selectionDot) setTimeout(() => { if (selectionDot) { selectionDot.style.opacity = "1" } }, 10) // Start auto-hide timer startAutoHideTimer(8000) // Longer initial timer // Handle hover events selectionDot.addEventListener("mouseenter", () => { isHoveringElements = true clearAutoHideTimer() }) selectionDot.addEventListener("mouseleave", () => { isHoveringElements = false if (!selectionPopup) { startAutoHideTimer(3000) // Shorter timer when leaving dot } }) } // Timer management functions function startAutoHideTimer(delay: number) { clearAutoHideTimer() autoHideTimer = setTimeout(() => { if (!isHoveringElements) { removeSelectionElements() } }, delay) } function clearAutoHideTimer() { if (autoHideTimer) { clearTimeout(autoHideTimer) autoHideTimer = null } } // Popup for text selection with React function showSelectionPopup() { const selectedText = window.getSelection()?.toString().trim() if (!selectedText || !selectionDot) return removeSelectionPopup() // Create popup container selectionPopup = document.createElement("div") selectionPopup.style.cssText = ` position: fixed; z-index: 10002; opacity: 0; transition: opacity 0.2s ease; ` // Position popup const dotRect = selectionDot.getBoundingClientRect() let top = dotRect.bottom + 5 let left = dotRect.left // Keep popup within viewport bounds if (left + 350 > window.innerWidth) { left = window.innerWidth - 360 } if (left < 10) { left = 10 } // If popup would go below viewport, show above the dot if (top + 250 > window.innerHeight) { top = dotRect.top - 255 } // Ensure popup doesn't go above viewport if (top < 10) { top = 10 } selectionPopup.style.top = `${top}px` selectionPopup.style.left = `${left}px` document.body.appendChild(selectionPopup) // Handle popup hover events selectionPopup.addEventListener("mouseenter", () => { isHoveringElements = true clearAutoHideTimer() }) selectionPopup.addEventListener("mouseleave", () => { isHoveringElements = false startAutoHideTimer(5000) // Auto-hide after leaving popup }) // Prevent popup from closing when clicking inside it selectionPopup.addEventListener("click", (e) => { e.stopPropagation() }) // Create React root selectionRoot = createRoot(selectionPopup) // State management for the component let saveStatus: 'idle' | 'saving' | 'success' | 'error' | 'empty' = 'idle' let isSaving = false // Handle save const handleSave = async (text: string) => { if (!isExtensionContextValid()) { saveStatus = 'error' renderPopup() return } if (!text.trim()) { saveStatus = 'empty' renderPopup() setTimeout(() => { saveStatus = 'idle' renderPopup() }, 2000) return } isSaving = true saveStatus = 'saving' renderPopup() try { const success = await addEpisode(text) if (success) { saveStatus = 'success' renderPopup() setTimeout(() => removeSelectionElements(), 1500) } else { saveStatus = 'error' renderPopup() setTimeout(() => { saveStatus = 'idle' isSaving = false renderPopup() }, 2000) } } catch (error) { saveStatus = 'error' renderPopup() setTimeout(() => { saveStatus = 'idle' isSaving = false renderPopup() }, 2000) } } // Handle close const handleClose = () => { removeSelectionElements() } // Render function const renderPopup = () => { if (selectionRoot) { selectionRoot.render( createElement(TextSelectionPopup, { selectedText, onSave: handleSave, onClose: handleClose, isSaving, saveStatus }) ) } } // Initial render renderPopup() // Show popup setTimeout(() => { if (selectionPopup) { selectionPopup.style.opacity = "1" } }, 10) } // Text selection handler export function handleTextSelection() { const selection = window.getSelection() const selectedText = selection?.toString().trim() if ( selectedText && selectedText.length > 0 && selection && selection.rangeCount > 0 ) { const range = selection.getRangeAt(0) const rect = range.getBoundingClientRect() createSelectionDot(rect) } else { removeSelectionElements() } } // Cleanup functions export function removeSelectionElements() { clearAutoHideTimer() isHoveringElements = false if (selectionDot) { selectionDot.remove() selectionDot = null } removeSelectionPopup() } function removeSelectionPopup() { // Clean up React root first if (selectionRoot) { selectionRoot.unmount() selectionRoot = null } // Remove DOM element if (selectionPopup) { selectionPopup.remove() selectionPopup = null } } // Check if click is on selection elements export function isSelectionElement(target: HTMLElement): boolean { // Check if clicking on the dot if (selectionDot && selectionDot.contains(target)) { return true } // Check if clicking on the popup (including textarea and buttons) if (selectionPopup && selectionPopup.contains(target)) { return true } // Check if clicking on specific popup elements by ID if (target.id === 'memory-text' || target.id === 'save-btn' || target.id === 'close-btn') { return true } // Check if target is within any element with our popup z-index if (target.closest('[style*="z-index: 10002"]')) { return true } return false } // Close selection popup if clicking elsewhere export function handleSelectionClickOutside(target: HTMLElement) { // Only close if we're not clicking on any selection elements if (selectionPopup && !isSelectionElement(target)) { removeSelectionElements() } }