/** * PageTrainingDataModal Component * * Modal displaying training data (page analysis and elements) for a specific page. * Shows element details, selector strategies, and allows editing. * * @component * @layer Presentation */ import { useState } from 'react'; import { Edit, Plus, Trash2, Save, Search } from 'lucide-react'; import type { PageAnalysis, PageElement, SelectorStrategy } from '@domain/entities'; import { ElementType, SelectorType } from '@domain/entities'; import { ELEMENT_TYPE_CONFIG } from '@domain/constants'; import { Modal } from '@/components/shared'; import { PatternDetectionSection } from './PatternDetectionSection'; interface PageTrainingDataModalProps { pageUrl: string; analysis: PageAnalysis | null; onClose: () => void; onElementUpdate: (analysisId: string, elementId: string, updatedElement: PageElement) => void; } /** * Element Type Badge */ function ElementTypeBadge({ type }: { type: ElementType }) { const config = ELEMENT_TYPE_CONFIG[type]; const colorClasses: Record = { blue: 'bg-blue-100 text-blue-800 border-blue-200', green: 'bg-green-100 text-green-800 border-green-200', yellow: 'bg-yellow-100 text-yellow-800 border-yellow-200', purple: 'bg-purple-100 text-purple-800 border-purple-200', orange: 'bg-orange-100 text-orange-800 border-orange-200', red: 'bg-red-100 text-red-800 border-red-200', gray: 'bg-gray-100 text-gray-700 border-gray-200', }; return ( {config.label} ); } /** * Selector Strategy Badge */ function SelectorBadge({ selector }: { selector: SelectorStrategy }) { const typeColors: Record = { [SelectorType.ARIA]: 'bg-blue-50 border-blue-200 text-blue-700', [SelectorType.TEXT]: 'bg-green-50 border-green-200 text-green-700', [SelectorType.CSS]: 'bg-purple-50 border-purple-200 text-purple-700', [SelectorType.XPATH]: 'bg-orange-50 border-orange-200 text-orange-700', [SelectorType.POSITION]: 'bg-gray-50 border-gray-200 text-gray-700', }; const scoreColor = selector.score >= 0.8 ? 'text-green-600' : selector.score >= 0.5 ? 'text-yellow-600' : 'text-red-600'; return (
{selector.type} {selector.value} {(selector.score * 100).toFixed(0)}%
); } /** * Element Edit Modal */ function ElementEditModal({ element, onSave, onCancel, }: { element: PageElement; onSave: (updatedElement: PageElement) => void; onCancel: () => void; }) { const [formData, setFormData] = useState(JSON.parse(JSON.stringify(element))); const [newSelectorType, setNewSelectorType] = useState(SelectorType.CSS); const [newSelectorValue, setNewSelectorValue] = useState(''); const [newSelectorScore, setNewSelectorScore] = useState(0.5); const handleAddSelector = () => { if (!newSelectorValue.trim()) return; const newSelector: SelectorStrategy = { type: newSelectorType, value: newSelectorValue.trim(), score: newSelectorScore, }; setFormData({ ...formData, selectors: [...formData.selectors, newSelector].sort((a, b) => b.score - a.score), }); setNewSelectorValue(''); setNewSelectorScore(0.5); }; const handleRemoveSelector = (index: number) => { setFormData({ ...formData, selectors: formData.selectors.filter((_, i) => i !== index), }); }; const handleSave = () => { onSave(formData); }; return ( } >
{/* Element Type */}
{/* Semantic Description */}
setFormData({ ...formData, semanticDescription: e.target.value })} className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-cyan-500" placeholder="e.g., Main navigation menu" />
{/* Text Content */}
setFormData({ ...formData, text: e.target.value })} className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-cyan-500" placeholder="Element text content" />
{/* Visibility */}
setFormData({ ...formData, isVisible: e.target.checked })} className="w-3.5 h-3.5 text-cyan-600 border-gray-300 rounded focus:ring-cyan-500" />
{/* Selectors */}
{formData.selectors.map((selector, index) => (
))}
{/* Add New Selector */}
Add New Selector
setNewSelectorValue(e.target.value)} placeholder="Selector value" className="px-1.5 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-cyan-500" /> setNewSelectorScore(parseFloat(e.target.value))} placeholder="Score" className="px-1.5 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-cyan-500" />
{/* Position (if available) */} {formData.position && (
setFormData({ ...formData, position: { ...formData.position!, x: parseInt(e.target.value) }, }) } placeholder="X" className="px-1.5 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-cyan-500" /> setFormData({ ...formData, position: { ...formData.position!, y: parseInt(e.target.value) }, }) } placeholder="Y" className="px-1.5 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-cyan-500" /> setFormData({ ...formData, position: { ...formData.position!, width: parseInt(e.target.value) }, }) } placeholder="Width" className="px-1.5 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-cyan-500" /> setFormData({ ...formData, position: { ...formData.position!, height: parseInt(e.target.value) }, }) } placeholder="Height" className="px-1.5 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-cyan-500" />
)}
); } /** * PageTrainingDataModal Component */ export function PageTrainingDataModal({ pageUrl, analysis, onClose, onElementUpdate, }: PageTrainingDataModalProps) { const [editingElement, setEditingElement] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [activeTab, setActiveTab] = useState<'elements' | 'patterns'>('elements'); const handleEditElement = (element: PageElement) => { setEditingElement(element); }; const handleSaveElement = (updatedElement: PageElement) => { if (analysis) { onElementUpdate(analysis.id, updatedElement.id, updatedElement); } setEditingElement(null); }; const handleCancelEdit = () => { setEditingElement(null); }; // Filter elements by search query const filteredElements = analysis?.elements.filter((element) => { if (!searchQuery) return true; const query = searchQuery.toLowerCase(); return ( element.semanticDescription.toLowerCase().includes(query) || element.text?.toLowerCase().includes(query) || element.type.toLowerCase().includes(query) || element.selectors.some((s) => s.value.toLowerCase().includes(query)) ); }) || []; // Group filtered elements by type const elementsByType = filteredElements.reduce((acc, element) => { if (!acc[element.type]) { acc[element.type] = []; } acc[element.type].push(element); return acc; }, {} as Record); // Dynamic modal width based on active tab const modalWidth = activeTab === 'patterns' ? 'max-w-4xl' : 'max-w-xl'; return ( <> Close } > {analysis ? (
{/* Tab Navigation */}
{/* Elements Tab */} {activeTab === 'elements' && ( <> {/* Search Bar */}
setSearchQuery(e.target.value)} placeholder="Search elements by description, text, type, or selector..." className="w-full pl-10 pr-4 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent" /> {searchQuery && ( )}
{/* Analysis Summary - Ultra Compact */}
Total: {analysis.elements.length}
Showing: {filteredElements.length}
Version: {analysis.version}
Analyzed: {new Date(analysis.analyzedAt).toLocaleDateString()}
{/* Elements - Compact List */}
{Object.keys(elementsByType).length === 0 ? (
No elements match your search
) : ( (Object.entries(elementsByType) as [ElementType, PageElement[]][]).map(([type, elements]) => (
({elements.length})
{elements.map((element) => (
{element.semanticDescription} {element.isVisible && ( )}
{element.text && (
"{element.text}"
)}
{element.selectors.slice(0, 3).map((selector, idx) => ( {selector.type} ))} {element.selectors.length > 3 && ( +{element.selectors.length - 3} )}
))}
)) )}
)} {/* Patterns Tab */} {activeTab === 'patterns' && analysis.hasPatterns() && ( )}
) : (
No Training Data Available

This page has not been analyzed yet.

)}
{/* Element Edit Modal */} {editingElement && ( )} ); }