// // SPDX-FileCopyrightText: Copyright (c) 1993-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // "use client" import { useState, useEffect, useRef } from "react" import { useDocuments } from "@/contexts/document-context" import type { Triple } from "@/utils/text-processing" import { Pencil, Trash2, Plus, Download, ChevronDown, FileJson, FileText, List, Network, Check, X, Database } from "lucide-react" import { TripleEditor } from "./triple-editor" // Add this new EntityEditor component before the TripleViewer component interface EntityEditorProps { entity: string onSave: (oldEntity: string, newEntity: string) => void onCancel: () => void } function EntityEditor({ entity, onSave, onCancel }: EntityEditorProps) { const [newEntityName, setNewEntityName] = useState(entity) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (newEntityName.trim()) { onSave(entity, newEntityName.trim()) } } return (
setNewEntityName(e.target.value)} className="w-full bg-background border border-border rounded-md p-2 text-sm text-foreground focus:ring-2 focus:ring-primary/50 focus:border-primary" placeholder="Entity" required />
) } export function TripleViewer() { const { documents, addTriple, editTriple, deleteTriple, updateTriples } = useDocuments() const [selectedDocId, setSelectedDocId] = useState(null) const [editingIndex, setEditingIndex] = useState(null) const [isAddingTriple, setIsAddingTriple] = useState(false) const [showExportMenu, setShowExportMenu] = useState(false) const [viewMode, setViewMode] = useState<'triples' | 'entities'>('triples') const [editingEntityIndex, setEditingEntityIndex] = useState(null) const [isAddingEntity, setIsAddingEntity] = useState(false) const [newEntityName, setNewEntityName] = useState('') const [isStoringToDb, setIsStoringToDb] = useState(false) const [storeStatus, setStoreStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle') const [isDropdownOpen, setIsDropdownOpen] = useState(false) const [searchQuery, setSearchQuery] = useState('') const dropdownRef = useRef(null) // Handle click outside to close dropdown useEffect(() => { function handleClickOutside(event: MouseEvent) { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsDropdownOpen(false) } } document.addEventListener("mousedown", handleClickOutside) return () => { document.removeEventListener("mousedown", handleClickOutside) } }, []) const processedDocs = documents.filter((doc) => doc.status === "Processed") const selectedDoc = selectedDocId ? documents.find((doc) => doc.id === selectedDocId) : processedDocs.length > 0 ? processedDocs[0] : null // Filter documents based on search query const filteredDocs = processedDocs.filter(doc => doc.name.toLowerCase().includes(searchQuery.toLowerCase()) ) // Extract unique entities from triples const getUniqueEntities = () => { if (!selectedDoc?.triples) return []; const entitiesSet = new Set(); selectedDoc.triples.forEach(triple => { if (triple.subject && typeof triple.subject === 'string') { entitiesSet.add(triple.subject); } if (triple.object && typeof triple.object === 'string') { entitiesSet.add(triple.object); } }); return Array.from(entitiesSet).sort(); }; const uniqueEntities = getUniqueEntities(); // Helper function to normalize triple text by removing parentheses and quotes const normalizeText = (text: string | null | undefined): string => { if (!text || typeof text !== 'string') return ''; return text.replace(/['"()]/g, '').trim(); }; if (processedDocs.length === 0) { return (

No processed documents available

Upload markdown, CSV, or text files and click "Generate Graph" to extract knowledge triples

) } const handleSaveTriple = (triple: Triple, index?: number) => { if (selectedDoc) { if (index !== undefined) { editTriple(selectedDoc.id, index, triple) } else { addTriple(selectedDoc.id, triple) } } setEditingIndex(null) setIsAddingTriple(false) } const handleDeleteTriple = (index: number) => { if (selectedDoc) { if (confirm("Are you sure you want to delete this triple?")) { deleteTriple(selectedDoc.id, index) } } } const exportTriplesCSV = () => { if (!selectedDoc || !selectedDoc.triples) return const tripleCsv = [ "Subject,Predicate,Object", ...selectedDoc.triples.map((t) => `"${normalizeText(t.subject)}","${normalizeText(t.predicate)}","${normalizeText(t.object)}"`), ].join("\n") const blob = new Blob([tripleCsv], { type: "text/csv" }) const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = `${selectedDoc.name.replace(/\.[^/.]+$/, "")}_triples.csv` document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) setShowExportMenu(false) } const exportTriplesJSON = () => { if (!selectedDoc || !selectedDoc.triples) return // Export the triples in the exact format expected by the graph viewer const triplesJSON = JSON.stringify(selectedDoc.triples, null, 2) const blob = new Blob([triplesJSON], { type: "application/json" }) const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = `${selectedDoc.name.replace(/\.[^/.]+$/, "")}_triples.json` document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) setShowExportMenu(false) } // Export entities list to CSV const exportEntitiesCSV = () => { if (!uniqueEntities.length) return; const entitiesCsv = [ "Entity", ...uniqueEntities.map(entity => `"${normalizeText(entity)}"`), ].join("\n"); const blob = new Blob([entitiesCsv], { type: "text/csv" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${selectedDoc?.name.replace(/\.[^/.]+$/, "") || "graph"}_entities.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); setShowExportMenu(false); } const handleSaveEntity = (oldEntity: string, newEntity: string) => { if (selectedDoc && selectedDoc.triples) { // Create new triples array with updated entity names const updatedTriples = selectedDoc.triples.map(triple => { const updatedTriple = { ...triple }; // Update subject if it matches the old entity name if (updatedTriple.subject === oldEntity) { updatedTriple.subject = newEntity; } // Update object if it matches the old entity name if (updatedTriple.object === oldEntity) { updatedTriple.object = newEntity; } return updatedTriple; }); // Update the document with the new triples updateTriples(selectedDoc.id, updatedTriples); } // Reset editing state setEditingEntityIndex(null); }; const handleAddEntity = () => { if (!newEntityName.trim() || !selectedDoc) return // Add a self-referential triple: "entity" is "entity" // This is a simple way to add an entity to the graph const selfReferentialTriple: Triple = { subject: newEntityName.trim(), predicate: 'is', object: newEntityName.trim() } // Add the new triple to the document addTriple(selectedDoc.id, selfReferentialTriple) // Reset state setNewEntityName('') setIsAddingEntity(false) } const handleDeleteEntity = (entity: string) => { if (!selectedDoc || !selectedDoc.triples) return; if (confirm(`Are you sure you want to delete the entity "${entity}"? This will remove all triples containing this entity.`)) { // Filter out all triples that contain the entity const filteredTriples = selectedDoc.triples.filter(triple => triple.subject !== entity && triple.object !== entity ); // Update the document with the filtered triples updateTriples(selectedDoc.id, filteredTriples); } }; // Function to store triples in the Neo4j database const storeInGraphDb = async () => { if (!selectedDoc || !selectedDoc.triples || selectedDoc.triples.length === 0) { alert("No triples to store in the database"); return; } try { setIsStoringToDb(true); setStoreStatus('loading'); const response = await fetch('/api/graph-db/triples', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ triples: selectedDoc.triples, documentName: selectedDoc.name }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to store triples in the database'); } setStoreStatus('success'); setTimeout(() => setStoreStatus('idle'), 3000); } catch (error) { console.error("Error storing triples in graph database:", error); setStoreStatus('error'); alert(error instanceof Error ? error.message : 'An error occurred while storing triples'); } finally { setIsStoringToDb(false); } }; // Function to store all triples from all documents in the graph database const storeAllTriplesInGraphDb = async () => { // Get all documents with triples const docsWithTriples = documents.filter(doc => doc.triples && doc.triples.length > 0); if (docsWithTriples.length === 0) { alert("No documents with triples to store in the database"); return; } try { setIsStoringToDb(true); setStoreStatus('loading'); // Collect all triples from all documents const allTriples = docsWithTriples.flatMap(doc => doc.triples || []); const response = await fetch('/api/graph-db/triples', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ triples: allTriples, documentName: 'All Documents' }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to store all triples in the database'); } setStoreStatus('success'); setTimeout(() => setStoreStatus('idle'), 3000); } catch (error) { console.error("Error storing all triples in graph database:", error); setStoreStatus('error'); alert(error instanceof Error ? error.message : 'An error occurred while storing all triples'); } finally { setIsStoringToDb(false); } }; return (
{/* Header Section with improved layout */}
{isDropdownOpen && (
setSearchQuery(e.target.value)} onClick={(e) => e.stopPropagation()} />
{filteredDocs.length === 0 ? (
No documents found
) : ( filteredDocs.map((doc) => ( )) )}
)}
{/* Primary Action - Store All Documents */}
{selectedDoc && ( <> {/* Knowledge Graph Stats */}

Document Statistics

{selectedDoc.triples?.length || 0} Triples
{uniqueEntities.length} Entities
{/* Tab Navigation */}
{selectedDoc.chunkCount && selectedDoc.chunkCount > 1 && (
Processed in {selectedDoc.chunkCount} chunks
)}
{viewMode === 'triples' ? ( selectedDoc.triples && selectedDoc.triples.length > 0 ? (
{/* Action Buttons Section */}

Knowledge Triples ({selectedDoc.triples?.length || 0})

{/* Primary Action - Add Triple */} {/* Secondary Actions Group */}
{showExportMenu && (
)}
Subject
Predicate
Object
{isAddingTriple && (
setIsAddingTriple(false)} />
)}
{selectedDoc.triples.map((triple, index) => (
{editingIndex === index ? ( setEditingIndex(null)} /> ) : (
{normalizeText(triple.subject)}
{normalizeText(triple.predicate)}
{normalizeText(triple.object)}
)}
))}
) : (

No triples found in this document

Try regenerating the graph or add triples manually

) ) : ( // Entities View
{/* Entities Action Buttons Section */}

{uniqueEntities.length > 0 ? `Entities (${uniqueEntities.length})` : "No Entities Found"}

{/* Primary Action - Add Entity */} {/* Secondary Action - Export */}
{showExportMenu && (
)}
{isAddingEntity && (
setNewEntityName(e.target.value)} className="flex-1 bg-background border border-border rounded-lg p-3 text-sm text-foreground focus:ring-2 focus:ring-nvidia-green/50 focus:border-nvidia-green transition-colors" placeholder="Enter entity name" />
)} {uniqueEntities.length > 0 ? (
Entity
{uniqueEntities.map((entity, index) => (
{editingEntityIndex === index ? ( setEditingEntityIndex(null)} /> ) : (
{normalizeText(entity)}
)}
))}
) : (

No entities found

Add triples to create entities in the knowledge graph

)}
)} )}
) }