import { useEffect, useState, useCallback } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { apiFetch } from '@core/lib/api' import { useAppPath } from '@core/hooks/useAppPath' import { Card, CardContent, CardHeader, CardTitle } from '@core/components/ui/card' import { Button } from '@core/components/ui/button' import { Badge } from '@core/components/ui/badge' import { Skeleton } from '@core/components/ui/skeleton' import { Input } from '@core/components/ui/input' import { FileCode2, CheckCircle2, AlertTriangle, Clock, ShieldCheck, ShieldAlert, ArrowLeft, RefreshCw, Search, Code2, GitGraph, Layers, ChevronDown, ChevronRight, ExternalLink, } from 'lucide-react' // ─── TYPES ──────────────────────────────────────────────────────────────────── interface CILFile { id: string title: string data: { file_path: string language: string project_id: string ingestion_pass: number chunk_count: number line_count: number stored_hash: string | null is_dead_code: boolean has_existing_markers: boolean existing_marker_count: number git_last_commit: string | null } updated_at: string } interface CILChunk { id: string chunk_name: string chunk_type: string line_start: number line_end: number macro: string doc_confidence: string boundary_confidence: string stored_hash: string | null is_dead_code: boolean stability: string file_path: string project_id: string } interface ValidationSummary { total: number current: number drift_detected: number not_found: number trust_rating: number } interface FileValidation { fileId: string summary: ValidationSummary | null loading: boolean error: string | null } // ─── PASS BADGE ────────────────────────────────────────────────────────────── function PassBadge({ pass }: { pass: number }) { if (!pass || pass < 1) { return Not ingested } if (pass >= 2) { return Pass {pass} complete } return Pass {pass} } // ─── TRUST BADGE ───────────────────────────────────────────────────────────── function TrustBadge({ summary }: { summary: ValidationSummary | null }) { if (!summary) return null const { trust_rating, drift_detected } = summary if (drift_detected > 0) { return ( {trust_rating}% ({drift_detected} drift) ) } return ( {trust_rating}% trusted ) } // ─── PAGE COMPONENT ─────────────────────────────────────────────────────────── export default function CILFileListPage() { const { id: projectId } = useParams<{ id: string }>() const navigate = useNavigate() const appPath = useAppPath() const [project, setProject] = useState<{ id: string; title: string; data: any } | null>(null) const [files, setFiles] = useState([]) const [loading, setLoading] = useState(true) const [search, setSearch] = useState('') const [validations, setValidations] = useState>({}) const [validatingAll, setValidatingAll] = useState(false) const [expandedFiles, setExpandedFiles] = useState>(new Set()) const [fileChunks, setFileChunks] = useState>({}) // ── Fetch project + file list ─────────────────────────────────────────────── const fetchData = useCallback(() => { if (!projectId) return setLoading(true) Promise.all([ apiFetch(`/api/custom_cil-project?action=get&id=${projectId}`).then(r => r.json()), apiFetch(`/api/admin-data?action=list&entity=items&type_slug=cil_file&limit=500`) .then(r => r.json()), ]) .then(([projData, filesData]) => { setProject(projData?.project || projData?.data || null) const allFiles: CILFile[] = filesData?.data || filesData?.items || [] setFiles(allFiles.filter(f => f.data?.project_id === projectId)) }) .catch(() => {}) .finally(() => setLoading(false)) }, [projectId]) useEffect(() => { fetchData() }, [fetchData]) // ── Validate one file ─────────────────────────────────────────────────────── const validateFile = useCallback(async (fileId: string) => { setValidations(prev => ({ ...prev, [fileId]: { fileId, summary: null, loading: true, error: null }, })) try { const res = await apiFetch( `/api/custom_cil-validate?action=validate_file&file_id=${fileId}` ) const data = await res.json() setValidations(prev => ({ ...prev, [fileId]: { fileId, summary: data?.summary || null, loading: false, error: data?.error || null, }, })) } catch (err) { setValidations(prev => ({ ...prev, [fileId]: { fileId, summary: null, loading: false, error: err instanceof Error ? err.message : 'Validation failed', }, })) } }, []) // ── Validate all visible files ────────────────────────────────────────────── const validateAll = useCallback(async () => { const visible = filteredFiles setValidatingAll(true) for (const f of visible) { await validateFile(f.id) } setValidatingAll(false) }, [files, search]) // eslint-disable-line react-hooks/exhaustive-deps // ── Toggle / fetch chunks for a file ───────────────────────────────────── const toggleFileChunks = useCallback(async (fileId: string) => { const alreadyExpanded = expandedFiles.has(fileId) setExpandedFiles(prev => { const next = new Set(prev) alreadyExpanded ? next.delete(fileId) : next.add(fileId) return next }) if (alreadyExpanded) return // collapsing — no need to fetch // Already loaded if (fileChunks[fileId]?.loaded) return setFileChunks(prev => ({ ...prev, [fileId]: { chunks: [], loading: true, loaded: false } })) try { const res = await apiFetch(`/api/custom_cil-graph?action=get_file_chunks&file_id=${fileId}`) const data = await res.json() const chunks: CILChunk[] = data?.chunks || data?.data?.chunks || [] setFileChunks(prev => ({ ...prev, [fileId]: { chunks, loading: false, loaded: true } })) } catch { setFileChunks(prev => ({ ...prev, [fileId]: { chunks: [], loading: false, loaded: true } })) } }, [expandedFiles, fileChunks]) // ── Filter ───────────────────────────────────────────────────────────────── const filteredFiles = files.filter(f => { if (!search) return true const q = search.toLowerCase() return ( (f.data?.file_path || '').toLowerCase().includes(q) || (f.data?.language || '').toLowerCase().includes(q) ) }) // ── Stats ────────────────────────────────────────────────────────────────── const totalChunks = files.reduce((sum, f) => sum + (f.data?.chunk_count || 0), 0) const fullyIngested = files.filter(f => (f.data?.ingestion_pass || 0) >= 2).length const deadCodeFiles = files.filter(f => f.data?.is_dead_code).length const validatedCount = Object.keys(validations).length const driftCount = Object.values(validations).filter(v => (v.summary?.drift_detected || 0) > 0).length if (loading) { return ( {[...Array(6)].map((_, i) => )} ) } return ( {/* Header */} navigate(appPath(`/cil/${projectId}`))} className="flex items-center gap-1 text-muted-foreground" > Back File List {project && ( — {project.title} )} {files.length} files · {totalChunks} total chunks Refresh {validatingAll ? 'Validating…' : `Validate All (${filteredFiles.length})`} {/* Summary stats */} {[ { label: 'Total Files', value: files.length, icon: FileCode2, color: 'text-violet-500' }, { label: 'Fully Ingested', value: fullyIngested, icon: CheckCircle2, color: 'text-emerald-500' }, { label: 'Dead Code', value: deadCodeFiles, icon: AlertTriangle, color: 'text-amber-500' }, { label: 'Files w/ Drift', value: driftCount, icon: ShieldAlert, color: 'text-red-500' }, ].map(stat => ( {stat.label} {stat.value} ))} {/* Search */} setSearch(e.target.value)} /> {/* Validation progress */} {validatedCount > 0 && ( Validated {validatedCount} / {filteredFiles.length} files {driftCount > 0 && ( · {driftCount} with drift )} )} {/* File table */} {filteredFiles.length === 0 ? ( {files.length === 0 ? 'No files ingested yet. Run Pass 1 from the Ingestion tab.' : 'No files match the current filter.'} ) : ( {filteredFiles.map(file => { const d = file.data || {} const filePath: string = d.file_path || file.title const shortName = filePath.split('/').pop() || filePath const dirPath = filePath.includes('/') ? filePath.split('/').slice(0, -1).join('/') : '' const val = validations[file.id] const isValidating = val?.loading === true return ( {/* File info */} {shortName} {dirPath && ( {dirPath}/ )} {d.language || 'unknown'} {d.chunk_count > 0 && ( {d.chunk_count} chunks )} {d.line_count > 0 && ( {d.line_count} lines )} {d.is_dead_code && ( Dead code )} {d.has_existing_markers && d.existing_marker_count > 0 && ( {d.existing_marker_count} prior markers )} {/* Validation result */} {val && !val.loading && val.summary && ( )} {val && !val.loading && val.error && ( Validate error )} {/* Actions */} {/* Expand chunks */} {(d.chunk_count || 0) > 0 && ( toggleFileChunks(file.id)} title={expandedFiles.has(file.id) ? 'Collapse chunks' : 'Show chunks'} > {expandedFiles.has(file.id) ? : } )} validateFile(file.id)} title="Validate file hashes from disk" > {isValidating ? ( ) : ( )} navigate(appPath(`/cil/${projectId}/graph`))} title="View call graph" > {/* Drift details panel */} {val && !val.loading && val.summary && val.summary.drift_detected > 0 && ( {val.summary.drift_detected} chunk{val.summary.drift_detected !== 1 ? 's' : ''} have drifted since last ingestion Run reindex_chunk or re-run Pass 2 with force_reprocess: true to update stored hashes. )} {/* Expandable chunk list */} {expandedFiles.has(file.id) && (() => { const fc = fileChunks[file.id] if (!fc || fc.loading) return ( {[...Array(3)].map((_, i) => ( ))} ) if (!fc.chunks.length) return ( No chunks indexed for this file. ) const CHUNK_TYPE_COLORS: Record = { function: 'bg-indigo-100 text-indigo-700', class: 'bg-violet-100 text-violet-700', interface: 'bg-emerald-100 text-emerald-700', type: 'bg-cyan-100 text-cyan-700', component: 'bg-amber-100 text-amber-700', hook: 'bg-pink-100 text-pink-700', route: 'bg-red-100 text-red-700', } return ( {fc.chunks.map(chunk => { const typeClass = CHUNK_TYPE_COLORS[chunk.chunk_type] || 'bg-slate-100 text-slate-600' return ( {chunk.chunk_name} {chunk.chunk_type} L{chunk.line_start}–{chunk.line_end} {chunk.is_dead_code && ( dead )} ) })} ) })()} ) })} )} ) }
{files.length} files · {totalChunks} total chunks
reindex_chunk
force_reprocess: true