-
-
-
- SuperBinder
-
+
+
+
+
+
`,
setup() {
@@ -81,9 +80,10 @@ export default {
// Using router path directly
const isLandingPage = Vue.computed(() => router.currentRoute.value.path === '/');
- // Define your menu items once here
+ // Define your menu items with the new knowledge graph page
const menuItems = [
{ label: "Binder", to: "/binder" },
+ { label: "Knowledge Graph", to: "/knowledge-graph" }
];
function triggerFileInput() {
@@ -132,6 +132,16 @@ export default {
menuOpen.value = !menuOpen.value;
}
- return { isLandingPage, download, triggerFileInput, handleFileUpload, fileInput, menuOpen, toggleMenu, projects, menuItems };
+ return {
+ isLandingPage,
+ download,
+ triggerFileInput,
+ handleFileUpload,
+ fileInput,
+ menuOpen,
+ toggleMenu,
+ projects,
+ menuItems
+ };
},
};
\ No newline at end of file
diff --git a/public/components/KnowledgeGraph.js b/public/components/KnowledgeGraph.js
new file mode 100644
index 0000000..646ffc9
--- /dev/null
+++ b/public/components/KnowledgeGraph.js
@@ -0,0 +1,396 @@
+// src/components/KnowledgeGraphComponent.js
+
+import * as d3 from 'd3';
+import { knowledgeGraphService } from '../services/knowledgeGraphService.js';
+
+export default {
+ name: 'KnowledgeGraphComponent',
+ template: `
+
+
+
PDF Knowledge Graph Generator
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ selectedNode.label }}
+
Type: {{ selectedNode.type }}
+
+
Properties:
+
+ -
+ {{ key }}: {{ value }}
+
+
+
+
+
+
+ `,
+ setup() {
+ // State variables
+ const fileInput = Vue.ref(null);
+ const selectedFile = Vue.ref(null);
+ const isProcessing = Vue.ref(false);
+ const status = Vue.ref('');
+ const processingProgress = Vue.ref(0);
+ const graphContainer = Vue.ref(null);
+ const graphData = Vue.ref({ nodes: [], links: [] });
+ const selectedNode = Vue.ref(null);
+ const selectedLayout = Vue.ref('force');
+ const simulation = Vue.ref(null);
+ const svg = Vue.ref(null);
+ const zoom = Vue.ref(null);
+
+ // Function to handle file upload
+ const handleFileUpload = (event) => {
+ const file = event.target.files[0];
+ if (file && file.type === 'application/pdf') {
+ selectedFile.value = file;
+ status.value = `File selected: ${file.name}`;
+ } else {
+ status.value = 'Please select a PDF file.';
+ }
+ };
+
+ // Progress callback for the service
+ const progressCallback = (type, value) => {
+ if (type === 'status' || type === 'extracting' || type === 'storing') {
+ status.value = value;
+ } else if (type === 'progress') {
+ processingProgress.value = value;
+ } else if (type === 'error') {
+ status.value = `Error: ${value}`;
+ }
+ };
+
+ // Main function to process PDF
+ const processPdf = async () => {
+ if (!selectedFile.value) {
+ status.value = 'Please select a PDF file first.';
+ return;
+ }
+
+ try {
+ isProcessing.value = true;
+
+ // Process the PDF using the service
+ const result = await knowledgeGraphService.processPdf(
+ selectedFile.value,
+ progressCallback
+ );
+
+ // Update graph data
+ graphData.value = result;
+
+ // Render the graph
+ renderGraph();
+ } catch (error) {
+ console.error('Error processing PDF:', error);
+ status.value = `Error: ${error.message}`;
+ } finally {
+ isProcessing.value = false;
+ }
+ };
+
+ // Function to render the graph using D3.js
+ const renderGraph = () => {
+ if (!graphContainer.value) return;
+
+ const width = graphContainer.value.clientWidth;
+ const height = graphContainer.value.clientHeight || 600;
+
+ // Clear previous graph
+ d3.select(graphContainer.value).selectAll('*').remove();
+
+ // Create SVG
+ svg.value = d3.select(graphContainer.value)
+ .append('svg')
+ .attr('width', width)
+ .attr('height', height)
+ .attr('viewBox', [0, 0, width, height])
+ .attr('style', 'max-width: 100%; height: auto; font: 12px sans-serif;');
+
+ // Create zoom behavior
+ zoom.value = d3.zoom()
+ .scaleExtent([0.1, 8])
+ .on('zoom', (event) => {
+ container.attr('transform', event.transform);
+ });
+
+ svg.value.call(zoom.value);
+
+ // Container for the graph
+ const container = svg.value.append('g');
+
+ // Reset zoom
+ svg.value.call(zoom.value.transform, d3.zoomIdentity);
+
+ // Create links
+ const link = container.append('g')
+ .selectAll('line')
+ .data(graphData.value.links)
+ .join('line')
+ .attr('stroke', '#999')
+ .attr('stroke-opacity', 0.6)
+ .attr('stroke-width', 1.5);
+
+ // Create link labels
+ const linkLabels = container.append('g')
+ .selectAll('text')
+ .data(graphData.value.links)
+ .join('text')
+ .text(d => d.type)
+ .attr('font-size', '8px')
+ .attr('fill', '#ccc')
+ .attr('text-anchor', 'middle');
+
+ // Create nodes
+ const node = container.append('g')
+ .selectAll('circle')
+ .data(graphData.value.nodes)
+ .join('circle')
+ .attr('r', 8)
+ .attr('fill', d => getNodeColor(d.type))
+ .call(drag(simulation.value))
+ .on('click', (event, d) => {
+ event.stopPropagation();
+ selectedNode.value = d;
+ });
+
+ // Node labels
+ const labels = container.append('g')
+ .selectAll('text')
+ .data(graphData.value.nodes)
+ .join('text')
+ .text(d => d.label)
+ .attr('font-size', '10px')
+ .attr('fill', '#fff')
+ .attr('dx', 12)
+ .attr('dy', 4);
+
+ // Create simulation
+ simulation.value = d3.forceSimulation(graphData.value.nodes)
+ .force('link', d3.forceLink(graphData.value.links)
+ .id(d => d.id)
+ .distance(100))
+ .force('charge', d3.forceManyBody().strength(-200))
+ .force('center', d3.forceCenter(width / 2, height / 2))
+ .on('tick', () => {
+ link
+ .attr('x1', d => d.source.x)
+ .attr('y1', d => d.source.y)
+ .attr('x2', d => d.target.x)
+ .attr('y2', d => d.target.y);
+
+ linkLabels
+ .attr('x', d => (d.source.x + d.target.x) / 2)
+ .attr('y', d => (d.source.y + d.target.y) / 2);
+
+ node
+ .attr('cx', d => d.x)
+ .attr('cy', d => d.y);
+
+ labels
+ .attr('x', d => d.x)
+ .attr('y', d => d.y);
+ });
+
+ // Click on background to deselect node
+ svg.value.on('click', () => {
+ selectedNode.value = null;
+ });
+ };
+
+ // Function to get color based on node type
+ const getNodeColor = (type) => {
+ const colors = {
+ Person: '#FF6B6B',
+ Organization: '#4ECDC4',
+ Location: '#FFE66D',
+ Concept: '#6A0572',
+ Event: '#F7B801',
+ // Add more types as needed
+ };
+
+ return colors[type] || '#999';
+ };
+
+ // Drag functions for d3
+ const drag = (simulation) => {
+ function dragstarted(event) {
+ if (!event.active) simulation.alphaTarget(0.3).restart();
+ event.subject.fx = event.subject.x;
+ event.subject.fy = event.subject.y;
+ }
+
+ function dragged(event) {
+ event.subject.fx = event.x;
+ event.subject.fy = event.y;
+ }
+
+ function dragended(event) {
+ if (!event.active) simulation.alphaTarget(0);
+ event.subject.fx = null;
+ event.subject.fy = null;
+ }
+
+ return d3.drag()
+ .on('start', dragstarted)
+ .on('drag', dragged)
+ .on('end', dragended);
+ };
+
+ // Functions for zoom control
+ const zoomIn = () => {
+ svg.value.transition().call(
+ zoom.value.scaleBy, 1.5
+ );
+ };
+
+ const zoomOut = () => {
+ svg.value.transition().call(
+ zoom.value.scaleBy, 0.75
+ );
+ };
+
+ const resetZoom = () => {
+ svg.value.transition().call(
+ zoom.value.transform, d3.zoomIdentity
+ );
+ };
+
+ // Function to update layout
+ const updateLayout = () => {
+ if (!simulation.value) return;
+
+ simulation.value.stop();
+
+ if (selectedLayout.value === 'force') {
+ simulation.value
+ .force('link', d3.forceLink(graphData.value.links).id(d => d.id).distance(100))
+ .force('charge', d3.forceManyBody().strength(-200))
+ .force('center', d3.forceCenter(
+ graphContainer.value.clientWidth / 2,
+ graphContainer.value.clientHeight / 2
+ ));
+ } else if (selectedLayout.value === 'circular') {
+ simulation.value
+ .force('link', d3.forceLink(graphData.value.links).id(d => d.id).distance(50))
+ .force('charge', d3.forceManyBody().strength(-50))
+ .force('center', d3.forceCenter(
+ graphContainer.value.clientWidth / 2,
+ graphContainer.value.clientHeight / 2
+ ))
+ .force('radial', d3.forceRadial(
+ graphContainer.value.clientWidth / 4,
+ graphContainer.value.clientWidth / 2,
+ graphContainer.value.clientHeight / 2
+ ));
+ } else if (selectedLayout.value === 'hierarchical') {
+ // Apply hierarchical layout
+ const stratify = d3.stratify()
+ .id(d => d.id)
+ .parentId(d => {
+ const parentLink = graphData.value.links.find(link => link.target === d.id);
+ return parentLink ? parentLink.source : null;
+ });
+
+ try {
+ const root = stratify(graphData.value.nodes);
+
+ const treeLayout = d3.tree()
+ .size([
+ graphContainer.value.clientWidth - 100,
+ graphContainer.value.clientHeight - 100
+ ]);
+
+ const nodes = treeLayout(root);
+
+ nodes.each(node => {
+ const originalNode = graphData.value.nodes.find(n => n.id === node.id);
+ if (originalNode) {
+ originalNode.x = node.x + 50;
+ originalNode.y = node.y + 50;
+ }
+ });
+ } catch (error) {
+ console.error('Error applying hierarchical layout:', error);
+ // Fallback to force layout
+ simulation.value
+ .force('link', d3.forceLink(graphData.value.links).id(d => d.id).distance(100))
+ .force('charge', d3.forceManyBody().strength(-200))
+ .force('center', d3.forceCenter(
+ graphContainer.value.clientWidth / 2,
+ graphContainer.value.clientHeight / 2
+ ));
+ }
+ }
+
+ simulation.value.alpha(1).restart();
+ };
+
+ // Function to close the info panel
+ const closeInfoPanel = () => {
+ selectedNode.value = null;
+ };
+
+ // Lifecycle hooks
+ Vue.onMounted(() => {
+ // Initialize container dimensions
+ if (graphContainer.value) {
+ graphContainer.value.style.height = '600px';
+ }
+ });
+
+ Vue.onBeforeUnmount(() => {
+ // Clean up D3 simulation
+ if (simulation.value) {
+ simulation.value.stop();
+ }
+ });
+
+ return {
+ fileInput,
+ selectedFile,
+ isProcessing,
+ status,
+ processingProgress,
+ graphContainer,
+ graphData,
+ selectedNode,
+ selectedLayout,
+ handleFileUpload,
+ processPdf,
+ zoomIn,
+ zoomOut,
+ resetZoom,
+ updateLayout,
+ closeInfoPanel
+ };
+ }
+};
\ No newline at end of file
diff --git a/public/components/KnowledgeGraphView.js b/public/components/KnowledgeGraphView.js
new file mode 100644
index 0000000..6be7bc4
--- /dev/null
+++ b/public/components/KnowledgeGraphView.js
@@ -0,0 +1,21 @@
+// components/KnowledgeGraphView.js
+import KnowledgeGraphComponent from './KnowledgeGraphComponent.js';
+
+export default {
+ name: 'KnowledgeGraphView',
+ template: `
+
+
+
Knowledge Graph Generator
+
+ Upload PDF documents to automatically create an interactive knowledge graph that visualizes the relationships between concepts, entities, and ideas.
+
+
+
+
+
+ `,
+ components: {
+ KnowledgeGraphComponent
+ }
+};
\ No newline at end of file
diff --git a/public/main.js b/public/main.js
index 0acfd8a..e638160 100644
--- a/public/main.js
+++ b/public/main.js
@@ -1,9 +1,13 @@
// Import App and router (which are now simple objects or functions)
import App from './App.js';
import router from './router/index.js';
+import { createApp } from 'vue';
console.log(Vue.version);
+document.body.classList.add('bg-gray-800', 'text-white', 'm-0', 'font-sans');
+
+
// // Create the Vue app and use the router
const app = Vue.createApp(App);
app.use(router);
diff --git a/public/router/index.js b/public/router/index.js
index 98b5f05..b4401ee 100644
--- a/public/router/index.js
+++ b/public/router/index.js
@@ -1,5 +1,6 @@
import Landing from "../components/Landing.js";
import Binder from "../components/Binder.js";
+import KnowledgeGraphView from "../components/KnowledgeGraphView.js";
const routes = [
{
@@ -13,8 +14,14 @@ const routes = [
component: Binder,
name: "binder",
// requiresAuth:true, //Setup your own auth if you want SSO/Logins
+ },
+
+ {
+ path: "/knowledge-graph",
+ component: KnowledgeGraphView,
+ name: "knowledge-graph",
+ // requiresAuth:true, // Uncomment if you want to require authentication
}
-
];
const router = VueRouter.createRouter({
@@ -36,4 +43,4 @@ router.beforeEach((to, from, next) => {
}
});
-export default router;
+export default router;
\ No newline at end of file
diff --git a/public/services/knowledgeGraphService.js b/public/services/knowledgeGraphService.js
new file mode 100644
index 0000000..e369e3f
--- /dev/null
+++ b/public/services/knowledgeGraphService.js
@@ -0,0 +1,263 @@
+// src/services/knowledgeGraphService.js
+
+import axios from 'axios';
+import neo4j from 'neo4j-driver';
+import * as pdfjsLib from 'pdfjs-dist';
+
+/**
+ * Service for creating and managing knowledge graphs from PDF documents
+ */
+export const knowledgeGraphService = {
+ /**
+ * Initialize the Neo4j driver
+ * @returns {object} Neo4j driver instance
+ */
+ initDriver() {
+ return neo4j.driver(
+ process.env.VUE_APP_NEO4J_URI || 'neo4j://localhost:7687',
+ neo4j.auth.basic(
+ process.env.VUE_APP_NEO4J_USER || 'neo4j',
+ process.env.VUE_APP_NEO4J_PASSWORD || 'password'
+ )
+ );
+ },
+
+ /**
+ * Extract text content from a PDF file
+ * @param {File} file - PDF file to process
+ * @param {Function} progressCallback - Callback for progress updates
+ * @returns {Promise
} Extracted text
+ */
+ async extractTextFromPdf(file, progressCallback) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = async (event) => {
+ try {
+ const typedArray = new Uint8Array(event.target.result);
+
+ // Set worker path to pdf.worker.js
+ pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.10.38/build/pdf.worker.min.js';
+
+ const pdf = await pdfjsLib.getDocument({ data: typedArray }).promise;
+ progressCallback?.('extracting', `Extracting text from ${pdf.numPages} pages...`);
+
+ let fullText = '';
+ for (let i = 1; i <= pdf.numPages; i++) {
+ const progress = (i / pdf.numPages) * 30; // First 30% of progress
+ progressCallback?.('progress', progress);
+
+ const page = await pdf.getPage(i);
+ const textContent = await page.getTextContent();
+ const pageText = textContent.items.map(item => item.str).join(' ');
+ fullText += pageText + '\n';
+ }
+
+ resolve(fullText);
+ } catch (error) {
+ reject(error);
+ }
+ };
+ reader.onerror = reject;
+ reader.readAsArrayBuffer(file);
+ });
+ },
+
+ /**
+ * Extract entities and relationships using OpenAI
+ * @param {string} text - Text content to analyze
+ * @param {Function} progressCallback - Callback for progress updates
+ * @returns {Promise