/** * InfoCenterPage Component * * Main Info Center page with tenant selector and tabbed sections for: * - Statistics overview * - Site pages (hierarchy and discovered pages with rescan functionality) * - Training data * * @component * @layer Presentation */ import { useState, useEffect, useMemo } from 'react'; import { Database, FileQuestion } from 'lucide-react'; import { PageLayout } from '@/components/shared'; import { StatisticsCards } from './StatisticsCards'; import { SitePagesSection } from './SitePagesSection'; import { PatternDetectionSection } from './PatternDetectionSection'; import { ListingTemplateModal } from './ListingTemplateModal'; import { useInfoCenter } from '../../presentation/features/info-center/hooks'; import { useTenants } from '@/features/tenants/hooks'; import type { Tenant } from '@domain/entities'; import type { PageElement } from '@domain/entities/PageAnalysis'; import { ElementType, AnalysisStatus, PageStatus } from '@domain/entities'; import type { ListingTemplate } from '@archer/domain'; import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'; import { ReliabilityStatusBadge } from '@/components/ui/ReliabilityStatusBadge'; /** * Tab Type */ type TabType = 'overview' | 'pages' | 'patterns'; /** * Tab Configuration */ const TABS: { id: TabType; label: string; description: string }[] = [ { id: 'overview', label: 'Overview', description: 'Statistics and metrics' }, { id: 'pages', label: 'Site Pages', description: 'Hierarchy and discovered pages' }, { id: 'patterns', label: 'Patterns', description: 'AI-detected patterns' }, ]; /** * InfoCenterPage Component */ export function InfoCenterPage() { const [tenants, setTenants] = useState([]); const [selectedTenantId, setSelectedTenantId] = useState(''); const [activeTab, setActiveTab] = useState('overview'); const [isInitialLoading, setIsInitialLoading] = useState(true); // Pagination state const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(50); // Modal state for ListingTemplateModal const [showTemplateModal, setShowTemplateModal] = useState(false); const [selectedTemplate, setSelectedTemplate] = useState(null); // Use hooks for data management const { data: tenantsData } = useTenants(); const loadedTenants = tenantsData?.tenants || []; const { discoveredPages, pageAnalyses, stats, isLoading, scanProgress, pagination, rescanPage, rescanAll, addUrlToScan, updateElement, } = useInfoCenter({ tenantId: selectedTenantId, page, limit: pageSize, enableScanPolling: activeTab === 'pages', // Only poll when on pages tab }); // Initialize tenants and auto-select first tenant useEffect(() => { if (loadedTenants.length > 0 && !selectedTenantId) { setTenants(loadedTenants); setSelectedTenantId(loadedTenants[0].id); setIsInitialLoading(false); } }, [loadedTenants, selectedTenantId]); // Reset to page 1 when tenant changes useEffect(() => { setPage(1); }, [selectedTenantId]); /** * Handle page change */ const handlePageChange = (newPage: number) => { setPage(newPage); }; /** * Handle page size change */ const handlePageSizeChange = (newPageSize: number) => { setPageSize(newPageSize); setPage(1); // Reset to first page when page size changes }; // TanStack Query automatically refetches when selectedTenantId changes. // The hook receives selectedTenantId, and the internal useCrawledPages/usePageAnalyses // hooks have query keys that include tenantId, so they refetch automatically. /** * Convert domain data to legacy format for child components * * This adapter ensures backward compatibility while we migrate components incrementally. */ const legacyStats = useMemo(() => { // Calculate elements by type from analyses const elementsByType = {} as Record; Object.values(ElementType).forEach((type) => { elementsByType[type] = 0; }); pageAnalyses.forEach((analysis) => { analysis.elements.forEach((element) => { elementsByType[element.type] = (elementsByType[element.type] || 0) + 1; }); }); // Calculate pages by status const pagesByStatus = {} as Record; Object.values(PageStatus).forEach((status) => { pagesByStatus[status] = discoveredPages.filter((p) => p.status === status).length; }); // Calculate pages by analysis status const pagesByAnalysisStatus = {} as Record; Object.values(AnalysisStatus).forEach((status) => { pagesByAnalysisStatus[status] = discoveredPages.filter((p) => p.analysisStatus === status).length; }); return { tenantId: selectedTenantId, totalCrawledPages: stats.totalCrawledPages, totalAnalyzedPages: stats.totalAnalyzedPages, totalElements: stats.totalElements, coveragePercentage: stats.coveragePercentage, pendingAnalysis: stats.pendingAnalysis, failedAnalysis: stats.failedAnalysis, elementsByType: stats.elementsByType, lastCrawlDate: stats.lastCrawlDate, lastAnalysisDate: stats.lastAnalysisDate, pagesByStatus, pagesByAnalysisStatus, analysesByReliability: stats.analysesByReliability, withPatterns: stats.withPatterns, }; }, [selectedTenantId, stats, discoveredPages, pageAnalyses]); /** * Pass domain entities directly (no conversion needed) */ const legacyPages = useMemo(() => { return discoveredPages; }, [discoveredPages]); /** * Pass domain entities directly (no conversion needed) */ const legacyAnalyses = useMemo(() => { return pageAnalyses; }, [pageAnalyses]); /** * Handle element update - delegates to hook */ const handleElementUpdate = (analysisId: string, elementId: string, updatedElement: PageElement) => { updateElement(analysisId, elementId, updatedElement); }; /** * Handle rescan all pages - delegates to hook */ const handleRescanAll = async (tenantId: string) => { await rescanAll(tenantId); }; /** * Handle rescan single page - delegates to hook */ const handleRescanPage = async (pageId: string) => { await rescanPage(pageId); }; /** * Handle add URL to scan - delegates to hook */ const handleAddUrl = (tenantId: string, url: string) => { addUrlToScan(tenantId, url); }; /** * Handle view template - opens modal */ const handleViewTemplate = (template: ListingTemplate) => { setSelectedTemplate(template); setShowTemplateModal(true); }; // Initial loading state (tenants) if (isInitialLoading) { return ( } gradient={true} >
); } // No tenants state if (tenants.length === 0) { return ( } gradient={true} >

No Tenants Available

Create a tenant first to view Info Center data

); } return ( } gradient={true} overlay={ { setShowTemplateModal(false); setSelectedTemplate(null); }} /> } > {/* Tenant Selector */}
{stats.totalCrawledPages}{' '} crawled pages •{' '} {stats.totalAnalyzedPages}{' '} analyzed •{' '} {stats.coveragePercentage}%{' '} coverage
{/* Tabs */}
{/* Tab Content */}
{isLoading ? (
) : ( <> {activeTab === 'overview' && (
{/* Pattern Detection Summary Card */} {stats.withPatterns > 0 && ( Pattern Detection Summary AI-detected patterns across your analyzed pages
{stats.withPatterns}
Pages with Patterns
{legacyAnalyses.reduce((acc, a) => acc + (a.getListingCount() || 0), 0)}
Total Listing Templates
{legacyAnalyses.filter(a => a.searchMechanism).length}
Pages with Search
)}
)} {activeTab === 'pages' && ( )} {activeTab === 'patterns' && (
{/* Empty state when no patterns */} {stats.withPatterns === 0 && (

No pattern detection results

Patterns will appear here once AI confirmation completes for analyzed pages.

)} {/* Pattern cards for each analysis with patterns */} {legacyAnalyses .filter((analysis) => analysis.hasPatterns()) .map((analysis) => (
{analysis.url} Detected {analysis.getListingCount()} listing template(s) {analysis.searchMechanism && ' • Search mechanism'} {analysis.paginationMechanism && ' • Pagination mechanism'}
{analysis.reliabilityStatus && ( )}
))}
)} )}
); }