/** * TrainingDataSection Component * * Displays page analyses and element training data with: * - Page analyses table * - Element counts and breakdowns * - Selector strategy visualization * - Version history * * @component * @layer Presentation */ import { useState } from 'react'; import { FileText, ChevronDown, ChevronRight, ExternalLink, Edit, Plus, Trash2, Save } 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'; interface TrainingDataSectionProps { analyses: PageAnalysis[]; onElementUpdate: (analysisId: string, elementId: string, updatedElement: PageElement) => void; } /** * Format duration in milliseconds */ function formatDuration(ms?: number): string { if (!ms) return '-'; if (ms < 1000) return `${ms}ms`; return `${(ms / 1000).toFixed(2)}s`; } /** * Format date */ function formatDate(dateInput: string | Date): string { const date = typeof dateInput === 'string' ? new Date(dateInput) : dateInput; return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } /** * Element Type Badge */ function ElementTypeBadge({ type }: { type: ElementType }) { const config = ELEMENT_TYPE_CONFIG[type]; return ( {config.label} ); } /** * Selector Type Badge */ function SelectorTypeBadge({ type, score }: { type: SelectorType; score: number }) { const colorMap = { [SelectorType.ARIA]: 'green', [SelectorType.TEXT]: 'blue', [SelectorType.CSS]: 'purple', [SelectorType.XPATH]: 'orange', [SelectorType.POSITION]: 'gray', }; const color = colorMap[type]; const percentage = Math.round(score * 100); return (
{type} {percentage}%
); } /** * Element Edit Modal Component */ 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 ( } onClose={onCancel} maxWidth="max-w-4xl" footer={ <> } >
{/* Element Type */}
{/* Semantic Description */}
setFormData({ ...formData, semanticDescription: e.target.value })} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500" placeholder="e.g., Submit form button" />
{/* Text Content */}
setFormData({ ...formData, text: e.target.value })} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500" placeholder="e.g., Add to Cart" />
{/* Visibility */}
setFormData({ ...formData, isVisible: e.target.checked })} className="w-4 h-4 text-cyan-600 border-gray-300 rounded focus:ring-cyan-500" />
{/* Selectors */}
{/* Existing Selectors */}
{formData.selectors.map((selector, index) => (
{selector.type} {selector.value} {Math.round(selector.score * 100)}%
))}
{/* Add New Selector */}
Add New Selector
setNewSelectorValue(e.target.value)} placeholder="Selector value" className="col-span-6 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500 text-sm" /> setNewSelectorScore(parseFloat(e.target.value))} className="col-span-2 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500 text-sm" placeholder="Score" />
Score: 0.0 (low reliability) to 1.0 (high reliability)
{/* Position */} {formData.position && (
setFormData({ ...formData, position: { ...formData.position!, x: parseInt(e.target.value) }, }) } className="w-full px-2 py-1 border border-gray-300 rounded text-sm" />
setFormData({ ...formData, position: { ...formData.position!, y: parseInt(e.target.value) }, }) } className="w-full px-2 py-1 border border-gray-300 rounded text-sm" />
setFormData({ ...formData, position: { ...formData.position!, width: parseInt(e.target.value) }, }) } className="w-full px-2 py-1 border border-gray-300 rounded text-sm" />
setFormData({ ...formData, position: { ...formData.position!, height: parseInt(e.target.value) }, }) } className="w-full px-2 py-1 border border-gray-300 rounded text-sm" />
)}
); } /** * Page Analysis Row Component (Expandable) */ function PageAnalysisRow({ analysis, onElementUpdate, }: { analysis: PageAnalysis; onElementUpdate: (analysisId: string, elementId: string, updatedElement: PageElement) => void; }) { const [isExpanded, setIsExpanded] = useState(false); const [editingElement, setEditingElement] = useState(null); const handleEditElement = (element: PageElement) => { setEditingElement(element); }; const handleSaveElement = (updatedElement: PageElement) => { onElementUpdate(analysis.id, updatedElement.id, updatedElement); setEditingElement(null); }; const handleCancelEdit = () => { setEditingElement(null); }; // Count elements by type const elementsByType: Record = {} as any; Object.values(ElementType).forEach((type) => { elementsByType[type] = 0; }); analysis.elements.forEach((element) => { elementsByType[element.type]++; }); // Top 3 element types const topElementTypes = Object.entries(elementsByType) .filter(([_, count]) => count > 0) .sort(([, a], [, b]) => b - a) .slice(0, 3); // Sample elements (first 5) const sampleElements = analysis.elements.slice(0, 5); return ( <> setIsExpanded(!isExpanded)} >
{isExpanded ? : } e.stopPropagation()} > {analysis.url}
{analysis.title && (
{analysis.title}
)} {analysis.elementCount}
{topElementTypes.map(([type, count]) => ( {ELEMENT_TYPE_CONFIG[type as ElementType].label}: {count} ))}
{formatDate(analysis.analyzedAt)} v{analysis.version} {formatDuration(analysis.analysisDurationMs)} {/* Expanded Details */} {isExpanded && (
{/* Element Type Breakdown */}

Element Type Distribution

{Object.entries(elementsByType) .filter(([_, count]) => count > 0) .sort(([, a], [, b]) => b - a) .map(([type, count]) => (
{count}
))}
{/* Sample Elements */}

Sample Elements ({sampleElements.length} of {analysis.elementCount})

{sampleElements.map((element) => (
{element.isVisible ? ( Visible ) : ( Hidden )}
{element.text && ( "{element.text}" )}
{element.semanticDescription}
{/* Selectors */}
Selector Strategies:
{element.selectors.map((selector, idx) => (
{selector.value}
))}
{/* Position */} {element.position && (
Position: ({element.position.x}, {element.position.y}) • Size:{' '} {element.position.width}×{element.position.height}
)}
))}
)} {/* Edit Element Modal */} {editingElement && ( )} ); } /** * TrainingDataSection Component */ export function TrainingDataSection({ analyses, onElementUpdate }: TrainingDataSectionProps) { return (
{/* Header */}

Page Analyses

Click on a row to view element details and selector strategies

{analyses.length} analyzed pages
{/* Table */}
{analyses.length === 0 ? ( ) : ( analyses.map((analysis) => ( )) )}
Page URL Elements Top Types Analyzed At Version Duration
No page analyses available
); }