'use client' import { useState, useRef, useEffect, useCallback } from 'react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' interface VisualSelectorProps { url: string onSelect: (selector: string) => void onClose: () => void } /** * Generate an optimal CSS selector for an element */ function generateSelector(element: Element): string { // Try ID first if (element.id) { return `#${element.id}` } // Try unique class combination if (element.classList.length > 0) { const classes = Array.from(element.classList) const classSelector = '.' + classes.join('.') if (document.querySelectorAll(classSelector).length === 1) { return classSelector } } // Build path from parent elements const path: string[] = [] let current: Element | null = element while (current && current !== document.body) { let selector = current.tagName.toLowerCase() if (current.id) { selector = `#${current.id}` path.unshift(selector) break } if (current.classList.length > 0) { const significantClasses = Array.from(current.classList) .filter(c => !c.includes('hover') && !c.includes('active') && !c.includes('focus')) .slice(0, 2) if (significantClasses.length > 0) { selector += '.' + significantClasses.join('.') } } // Add nth-child if needed for uniqueness const parent = current.parentElement if (parent) { const siblings = Array.from(parent.children).filter( c => c.tagName === current!.tagName ) if (siblings.length > 1) { const index = siblings.indexOf(current) + 1 selector += `:nth-child(${index})` } } path.unshift(selector) current = current.parentElement } return path.join(' > ') } export function VisualSelector({ url, onSelect, onClose }: VisualSelectorProps) { const iframeRef = useRef(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [selectedSelector, setSelectedSelector] = useState('') const [testResult, setTestResult] = useState<{ count: number; success: boolean } | null>(null) const [proxyHtml, setProxyHtml] = useState(null) // Fetch page content through proxy useEffect(() => { async function fetchProxyContent() { try { setLoading(true) setError(null) const response = await fetch(`/api/proxy?url=${encodeURIComponent(url)}`) if (!response.ok) { throw new Error('Failed to load page') } const html = await response.text() setProxyHtml(html) setLoading(false) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load page') setLoading(false) } } fetchProxyContent() }, [url]) // Handle clicks within the iframe const handleIframeLoad = useCallback(() => { const iframe = iframeRef.current if (!iframe?.contentDocument) return const doc = iframe.contentDocument // Inject selection styles const style = doc.createElement('style') style.textContent = ` .visual-selector-hover { outline: 2px solid #3b82f6 !important; outline-offset: 2px; cursor: crosshair !important; } .visual-selector-selected { outline: 3px solid #22c55e !important; outline-offset: 2px; background-color: rgba(34, 197, 94, 0.1) !important; } ` doc.head.appendChild(style) // Add event listeners const handleMouseOver = (e: MouseEvent) => { const target = e.target as HTMLElement if (target && target !== doc.body) { target.classList.add('visual-selector-hover') } } const handleMouseOut = (e: MouseEvent) => { const target = e.target as HTMLElement if (target) { target.classList.remove('visual-selector-hover') } } const handleClick = (e: MouseEvent) => { e.preventDefault() e.stopPropagation() const target = e.target as HTMLElement if (!target || target === doc.body) return // Remove previous selection doc.querySelectorAll('.visual-selector-selected').forEach(el => { el.classList.remove('visual-selector-selected') }) // Add selection to current element target.classList.add('visual-selector-selected') // Generate and set selector const selector = generateSelector(target) setSelectedSelector(selector) // Test the selector const matches = doc.querySelectorAll(selector) setTestResult({ count: matches.length, success: matches.length === 1 }) } doc.body.addEventListener('mouseover', handleMouseOver) doc.body.addEventListener('mouseout', handleMouseOut) doc.body.addEventListener('click', handleClick) // Cleanup return () => { doc.body.removeEventListener('mouseover', handleMouseOver) doc.body.removeEventListener('mouseout', handleMouseOut) doc.body.removeEventListener('click', handleClick) } }, []) const handleConfirm = () => { if (selectedSelector) { onSelect(selectedSelector) } } const handleTestSelector = () => { const iframe = iframeRef.current if (!iframe?.contentDocument || !selectedSelector) return try { const matches = iframe.contentDocument.querySelectorAll(selectedSelector) setTestResult({ count: matches.length, success: matches.length === 1 }) // Highlight matches iframe.contentDocument.querySelectorAll('.visual-selector-selected').forEach(el => { el.classList.remove('visual-selector-selected') }) matches.forEach(el => { el.classList.add('visual-selector-selected') }) } catch { setTestResult({ count: 0, success: false }) } } return (
Visual Element Selector

Click on an element to select it. The CSS selector will be generated automatically.

{/* URL display */}
Loading: {url}
{/* Iframe container */}
{loading && (

Loading page...

)} {error && (

Failed to load page

{error}

Note: Some sites may block embedding due to security policies.

)} {proxyHtml && (