"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 (
)
}
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 && (
)}
{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 && (
)}
{uniqueEntities.length > 0 ? (
{uniqueEntities.map((entity, index) => (
{editingEntityIndex === index ? (
setEditingEntityIndex(null)}
/>
) : (
{normalizeText(entity)}
)}
))}
) : (
No entities found
Add triples to create entities in the knowledge graph
)}
)}
>
)}
)
}