import React, { useState, useEffect } from 'react'; import { useAuth } from '../context/AuthContext'; import type { DriftReport, DocumentInfo } from '../types/api'; import { Database, GitMerge, Settings, Sparkles, Save, Info, Zap, AlertTriangle, Check, X, FileText } from 'lucide-react'; const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api'; const Ontology: React.FC = () => { const { token, logout } = useAuth(); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [refining, setRefining] = useState(false); const [deduping, setDeduping] = useState(false); const [enriching, setEnriching] = useState(false); const [detectingDrift, setDetectingDrift] = useState(false); const [driftReports, setDriftReports] = useState([]); const [version, setVersion] = useState(''); const [entityTypes, setEntityTypes] = useState(''); const [relationshipTypes, setRelationshipTypes] = useState(''); const [properties, setProperties] = useState(''); const [feedback, setFeedback] = useState(''); // "Global" baseline so we can restore when switching back to global mode const [globalEntityTypes, setGlobalEntityTypes] = useState(''); const [globalRelationshipTypes, setGlobalRelationshipTypes] = useState(''); const [globalProperties, setGlobalProperties] = useState(''); const [documents, setDocuments] = useState([]); const [selectedDocId, setSelectedDocId] = useState(''); const [stats, setStats] = useState(null); const [docSchemaLoading, setDocSchemaLoading] = useState(false); const [message, setMessage] = useState(''); /* ── Fetch global ontology ─────────────────────────────────────── */ const fetchOntology = async () => { setLoading(true); try { const res = await fetch(`${API_BASE}/ontology`, { headers: { Authorization: `Bearer ${token}` } }); if (res.status === 401) { logout(); return; } if (res.ok) { const data = await res.json(); setVersion(data.version || '1.0'); const ent = data.entity_types?.join(', ') || ''; const rel = data.relationship_types?.join(', ') || ''; const props = JSON.stringify(data.properties || {}, null, 2); setEntityTypes(ent); setRelationshipTypes(rel); setProperties(props); // Save as global baseline setGlobalEntityTypes(ent); setGlobalRelationshipTypes(rel); setGlobalProperties(props); } else { setMessage('No active ontology found. Please upload documents first.'); } } catch (err) { console.error(err); setMessage('FAILED TO LOAD ONTOLOGY API'); } finally { setLoading(false); } }; const fetchDocuments = async () => { try { const res = await fetch(`${API_BASE}/documents`, { headers: { Authorization: `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setDocuments(data.documents); } } catch (err) { console.error('Failed to fetch docs for dropdown', err); } }; const fetchStats = async (docId: string) => { try { const url = new URL(`${API_BASE}/ontology/stats`); if (docId) url.searchParams.append('document_id', docId); const res = await fetch(url.toString(), { headers: { Authorization: `Bearer ${token}` } }); if (res.ok) setStats(await res.json()); else setStats(null); } catch { setStats(null); } }; /* ── Fetch document-specific schema ─────────────────────────────── */ const fetchDocSchema = async (docId: string) => { if (!docId) { // Restore global schema setEntityTypes(globalEntityTypes); setRelationshipTypes(globalRelationshipTypes); setProperties(globalProperties); return; } setDocSchemaLoading(true); try { const url = new URL(`${API_BASE}/ontology/stats`); url.searchParams.append('document_id', docId); const res = await fetch(url.toString(), { headers: { Authorization: `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); // Populate editor with document-specific entity types and relationships const docEntityTypes = (data.entity_stats || []) .map((s: any) => s.type) .filter(Boolean) .join(', '); const docRelTypes = (data.relationship_stats || []) .map((s: any) => s.type) .filter(Boolean) .join(', '); setEntityTypes(docEntityTypes || globalEntityTypes); setRelationshipTypes(docRelTypes || globalRelationshipTypes); // Properties: keep global properties since per-doc isn't tracked separately setProperties(globalProperties); } } catch (err) { console.error('Failed to fetch doc schema', err); } finally { setDocSchemaLoading(false); } }; useEffect(() => { fetchOntology(); fetchDocuments(); fetchStats(''); }, [token]); useEffect(() => { fetchStats(selectedDocId); fetchDocSchema(selectedDocId); }, [selectedDocId]); const handleSave = async (e: React.FormEvent) => { e.preventDefault(); setSaving(true); setMessage(''); let parsedProps = {}; try { parsedProps = JSON.parse(properties); } catch (e) { setMessage('ERROR: PROPERTIES MUST BE VALID JSON'); setSaving(false); return; } try { const res = await fetch(`${API_BASE}/ontology`, { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ entity_types: entityTypes.split(',').map(s => s.trim()).filter(Boolean), relationship_types: relationshipTypes.split(',').map(s => s.trim()).filter(Boolean), properties: parsedProps, approved: true }) }); if (res.status === 401) { logout(); return; } if (res.ok) { setMessage('ONTOLOGY SCHEMA UPDATED'); fetchOntology(); setSelectedDocId(''); // reset to global after save } else { setMessage('FAILED TO SAVE SCHEMA'); } } catch (err) { console.error(err); setMessage('API ERROR DURING SAVE'); } finally { setSaving(false); } }; const handleRefine = async () => { setRefining(true); setMessage('ANALYZING GRAPH FOR UPGRADES... (THIS MAY TAKE 30s+)'); try { const res = await fetch(`${API_BASE}/ontology/refine`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ feedback: feedback || undefined, document_id: selectedDocId || undefined }) }); if (res.status === 401) { logout(); return; } if (res.ok) { const data = await res.json(); setMessage(`SUCCESS: ${data.changes}`); fetchOntology(); } else { setMessage('FAILED TO REFINE SCHEMA'); } } catch (err) { console.error(err); setMessage('API ERROR DURING REFINE'); } finally { setRefining(false); } }; const handleDeduplicate = async () => { if (!window.confirm("Run semantic merging? This cannot be undone.")) return; setDeduping(true); setMessage('SCANNING GRAPH FOR DUPLICATE ENTITIES... (THIS MAY TAKE AWHILE)'); try { const res = await fetch(`${API_BASE}/entities/deduplicate`, { method: 'POST', headers: { Authorization: `Bearer ${token}` } }); if (res.status === 401) { logout(); return; } if (res.ok) { const data = await res.json(); setMessage(`DEDUPLICATION COMPLETE: Merged ${data.merged_count} entities.`); } else { setMessage('FAILED TO DEDUPLICATE ENTITIES'); } } catch (err) { console.error(err); setMessage('API ERROR DURING DEDUPLICATION'); } finally { setDeduping(false); } }; const handleEnrichEntities = async () => { setEnriching(true); setMessage('GENERATING ENTITY PROFILES FROM GRAPH NEIGHBORHOODS...'); try { const res = await fetch(`${API_BASE}/entities/enrich`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ batch_size: 20, min_connections: 1 }) }); if (res.status === 401) { logout(); return; } if (res.ok) { const data = await res.json(); setMessage(`ENRICHMENT COMPLETE: ${data.message || `${data.enriched_count ?? '?'} entities profiled.`}`); } else { setMessage('FAILED TO ENRICH ENTITIES'); } } catch { setMessage('API ERROR DURING ENRICHMENT'); } finally { setEnriching(false); } }; const handleDetectDrift = async () => { setDetectingDrift(true); setMessage('ANALYZING GRAPH DATA FOR SCHEMA DRIFT...'); try { const res = await fetch(`${API_BASE}/ontology/drift/detect`, { method: 'POST', headers: { Authorization: `Bearer ${token}` } }); if (res.status === 401) { logout(); return; } if (res.ok) { const data = await res.json(); setMessage(`DRIFT REPORT CREATED: ID ${data.report_id || data.id || '—'}`); fetchDriftReports(); } else { setMessage('FAILED TO DETECT DRIFT'); } } catch { setMessage('API ERROR DURING DRIFT DETECTION'); } finally { setDetectingDrift(false); } }; const fetchDriftReports = async () => { try { const res = await fetch(`${API_BASE}/ontology/drift`, { headers: { Authorization: `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setDriftReports(data.reports || []); } } catch {} }; const handleDriftAction = async (id: string, action: 'approve' | 'reject') => { const res = await fetch(`${API_BASE}/ontology/drift/${id}/${action}`, { method: 'POST', headers: { Authorization: `Bearer ${token}` } }); if (res.ok) { setDriftReports(d => d.filter(r => r.id !== id)); setMessage(`Drift report ${action}d.`); } }; const selectedDoc = documents.find(d => d.id === selectedDocId); return (

ONTOLOGY MANAGEMENT

SCHEMA CONTROL & GRAPH REFINEMENT

{/* Help info bar */}
ENTITY TYPES define what kinds of nodes exist in your graph. RELATIONSHIP TYPES define how they connect. Use LLM REFINEMENT to auto-suggest schema improvements from your data. Use DRIFT DETECTION to detect when new data doesn't fit the current schema. Use ENTITY ENRICHMENT to synthesize rich profiles for all graph nodes.
{/* Schema Editor */}

EDIT SCHEMA {version ? `v${version}` : ''}

{selectedDocId && (
DOC SCOPE: {selectedDoc?.filename?.slice(0, 22) ?? selectedDocId.slice(0, 12)}…
)}
{/* Document selector in editor header */}
{docSchemaLoading && (
↻ Loading document schema…
)} {selectedDocId && !docSchemaLoading && (
Schema populated from "{selectedDoc?.filename ?? selectedDocId}"
)}
{loading ? (
LOADING SCHEMA...
) : (