import { useState, useEffect } from 'react'; import { Card } from './ui/card'; import { Input } from './ui/input'; import { Button } from './ui/button'; import { Badge } from './ui/badge'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'; import { Database, Search, ChevronLeft, ChevronRight, Settings, Cpu, LineChart, Sparkles } from 'lucide-react'; import { API_BASE_URL } from '../config'; interface ModelMetrics { modelName: string; datasetSize: number; numClasses: number; accuracy: number; macroF1: number; weightedF1: number; weightedPrecision: number; weightedRecall: number; vocabularySize: number; wordFeatureCount: number; charFeatureCount: number; ngramRange: [number, number]; charNgramRange: [number, number]; maxIterations: number; splitMethod: string; groupOverlap: number; calibrated: boolean; } export function ModelExplorer() { // Search & Filter State const [searchQuery, setSearchQuery] = useState(''); const [selectedCategory, setSelectedCategory] = useState(''); const [currentPage, setCurrentPage] = useState(0); const [totalRecords, setTotalRecords] = useState(0); const [records, setRecords] = useState([]); const [categoryCounts, setCategoryCounts] = useState>({}); const [metrics, setMetrics] = useState(null); // loading state const [isLoading, setIsLoading] = useState(true); const [isTableLoading, setIsTableLoading] = useState(false); const [error, setError] = useState(null); const PAGE_LIMIT = 10; // Load category counts initially useEffect(() => { async function loadStats() { setIsLoading(true); try { const [datasetResponse, metricsResponse] = await Promise.all([ fetch(`${API_BASE_URL}/api/dataset?limit=1`), fetch(`${API_BASE_URL}/api/metrics`), ]); if (!datasetResponse.ok || !metricsResponse.ok) { throw new Error('Failed to load model statistics'); } const [data, metricsData] = await Promise.all([ datasetResponse.json(), metricsResponse.json(), ]); if (data.status === 'success') { setCategoryCounts(data.categoryCounts); } setMetrics(metricsData); } catch (err: any) { console.error(err); setError(err.message || 'Failed to connect to dataset API.'); } finally { setIsLoading(false); } } loadStats(); }, []); // Query records when page/search/category changes useEffect(() => { async function fetchRecords() { setIsTableLoading(true); const offset = currentPage * PAGE_LIMIT; const url = `${API_BASE_URL}/api/dataset?q=${encodeURIComponent(searchQuery)}&category=${encodeURIComponent(selectedCategory)}&limit=${PAGE_LIMIT}&offset=${offset}`; try { const res = await fetch(url); if (!res.ok) throw new Error('Failed to fetch records'); const data = await res.json(); if (data.status === 'success') { setRecords(data.records); setTotalRecords(data.total); } } catch (err: any) { console.error(err); } finally { setIsTableLoading(false); } } // Debounce search input const delayDebounceFn = setTimeout(() => { fetchRecords(); }, searchQuery ? 300 : 0); return () => clearTimeout(delayDebounceFn); }, [searchQuery, selectedCategory, currentPage]); const handleSearchChange = (e: React.ChangeEvent) => { setSearchQuery(e.target.value); setCurrentPage(0); // Reset page on new query }; const handleCategoryChange = (category: string) => { setSelectedCategory(category); setCurrentPage(0); }; // Convert category stats to Recharts format const chartData = Object.entries(categoryCounts) .map(([name, value]) => ({ name, count: value })) .sort((a, b) => b.count - a.count); const getSeverityBadgeClass = (category: string) => { switch (category.toLowerCase()) { case 'critical': case 'forced action': case 'sneaking': case 'obstruction': return 'bg-rose-500/10 text-rose-400 border border-rose-500/20'; case 'high': case 'urgency': return 'bg-orange-500/10 text-orange-400 border border-orange-500/20'; case 'medium': case 'scarcity': return 'bg-amber-500/10 text-amber-400 border border-amber-500/20'; case 'low': case 'social proof': case 'misdirection': return 'bg-indigo-500/10 text-indigo-400 border border-indigo-500/20'; default: return 'bg-slate-500/15 text-slate-400 border border-white/5'; } }; const totalDatasetCount = Object.values(categoryCounts).reduce((a, b) => a + b, 0); const cleanCopyCount = categoryCounts['Not Dark Pattern'] || 0; const deceptiveClassCount = metrics ? Math.max(metrics.numClasses - 1, 0) : 0; return (
{/* Model Performance Metrics Card */}
{/* Model Architecture */}

Model Architecture

Pipeline: TF-IDF + LogReg
Word / Character Features: {metrics ? `${metrics.wordFeatureCount.toLocaleString()} / ${metrics.charFeatureCount.toLocaleString()}` : '...'}
Word / Character N-grams: {metrics ? `${metrics.ngramRange.join('-')} / ${metrics.charNgramRange.join('-')}` : '...'}
Auto-retrained on Python backend startup.
{/* Model Statistics */}

Model Performance

Classification Accuracy: {metrics ? `${(metrics.accuracy * 100).toFixed(1)}%` : '...'}
F1 Score (Weighted): {metrics ? `${(metrics.weightedF1 * 100).toFixed(1)}%` : '...'}
Precision / Recall: {metrics ? `${(metrics.weightedPrecision * 100).toFixed(1)}% / ${(metrics.weightedRecall * 100).toFixed(1)}%` : '...'}
{metrics ? `${metrics.splitMethod}; page overlap: ${metrics.groupOverlap}; confidence calibrated: ${metrics.calibrated ? 'yes' : 'no'}.` : 'Loading validation method...'}
{/* Dataset Stats */}

Training Corpus

Total Training Examples: {metrics?.datasetSize || totalDatasetCount || '...'} rows
Deceptive Classes: {metrics ? deceptiveClassCount : '...'} Categories
Clean Interface Copy: {cleanCopyCount ? cleanCopyCount.toLocaleString() : '...'} rows
Counts are loaded directly from the labeled NLP training corpus.
{/* Dataset Chart Distributions */}

Pattern Category Distribution (Training Corpus)

{isLoading ? (
Loading distribution analytics...
) : (
)}
{/* Searchable Training Grid */}

Training Records Browser

{/* Filters */}
{/* Category selection */} {/* Search Input */}
{/* Data Table */}
{isTableLoading ? ( ) : records.length > 0 ? ( records.map((record) => ( )) ) : ( )}
Row ID Category Training Text Copy Label
Querying model datasets...
#{record.page_id} {record['Pattern Category']} {record.text} {record.label}
No matching training samples found in dataset.
{/* Pagination controls */} {totalRecords > PAGE_LIMIT && (
Showing {currentPage * PAGE_LIMIT + 1} - {Math.min((currentPage + 1) * PAGE_LIMIT, totalRecords)} of {totalRecords} records
{currentPage + 1} / {Math.ceil(totalRecords / PAGE_LIMIT)}
)}
); }