feat: implement admin knowledge extraction system with document upload and AI pipeline integration

This commit is contained in:
RaymondVerhoef
2026-05-10 11:18:48 +02:00
parent f8d0c68f84
commit 260644b41a
7 changed files with 625 additions and 5 deletions

View File

@@ -0,0 +1,191 @@
import React, { useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
import { storage } from '../../lib/storage';
const KnowledgeGraph = () => {
const svgRef = useRef(null);
const wrapperRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 800, height: 600 });
const [selectedNode, setSelectedNode] = useState(null);
// Load data
const topics = storage.get('kb:topics', []);
const relations = storage.get('kb:relations', []);
useEffect(() => {
if (!wrapperRef.current) return;
const { width, height } = wrapperRef.current.getBoundingClientRect();
setDimensions({ width, height });
const handleResize = () => {
if (wrapperRef.current) {
const { width, height } = wrapperRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
if (!svgRef.current || topics.length === 0) return;
const { width, height } = dimensions;
// Clear previous render
d3.select(svgRef.current).selectAll('*').remove();
// Setup nodes and links
// Copy objects because d3 modifies them
const nodes = topics.map(d => ({ ...d }));
const links = relations.map(d => ({ ...d }));
// Create SVG container with zoom support
const svg = d3.select(svgRef.current)
.attr('viewBox', [0, 0, width, height])
.attr('width', width)
.attr('height', height);
const g = svg.append('g');
const zoom = d3.zoom()
.scaleExtent([0.1, 4])
.on('zoom', (event) => {
g.attr('transform', event.transform);
});
svg.call(zoom);
// Color scale for node types
const color = d3.scaleOrdinal()
.domain(['concept', 'role', 'process', 'fact'])
.range(['#1F5560', '#5C1DD8', '#AFC0FF', '#E2E8F0']);
// Setup force simulation
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(d => d.id).distance(100))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collide', d3.forceCollide().radius(40));
// Draw links
const link = g.append('g')
.attr('stroke', 'var(--color-bg-warm)')
.attr('stroke-opacity', 0.6)
.selectAll('line')
.data(links)
.join('line')
.attr('stroke-width', 2);
// Draw nodes
const node = g.append('g')
.selectAll('g')
.data(nodes)
.join('g')
.call(drag(simulation));
// Add circles to nodes
node.append('circle')
.attr('r', 20)
.attr('fill', d => color(d.type) || '#1F5560')
.attr('stroke', '#fff')
.attr('stroke-width', 2)
.on('click', (event, d) => {
setSelectedNode(d);
});
// Add labels to nodes
node.append('text')
.text(d => d.label)
.attr('x', 25)
.attr('y', 5)
.attr('font-size', '12px')
.attr('fill', 'var(--color-fg)')
.attr('class', 'font-mono select-none pointer-events-none');
simulation.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);
node
.attr('transform', d => `translate(${d.x},${d.y})`);
});
// Drag behavior
function 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);
}
return () => {
simulation.stop();
};
}, [dimensions, topics, relations]);
return (
<div className="relative w-full h-full flex flex-col md:flex-row">
<div ref={wrapperRef} className="flex-1 h-[400px] md:h-full cursor-grab active:cursor-grabbing border-r border-bg-warm">
{topics.length === 0 ? (
<div className="h-full flex items-center justify-center text-fg-muted p-8 text-center">
Nog geen kennisgraaf data. Upload eerst bronnen via het Bronmateriaal tabblad.
</div>
) : (
<svg ref={svgRef} className="w-full h-full" />
)}
</div>
{/* Node Info Panel */}
<div className="w-full md:w-80 bg-paper p-6 overflow-y-auto border-t md:border-t-0 border-bg-warm">
<h3 className="font-bold text-lg mb-4 text-teal">Node Details</h3>
{selectedNode ? (
<div className="space-y-4">
<div>
<p className="text-xs text-fg-muted uppercase tracking-wider mb-1">Label</p>
<p className="font-medium">{selectedNode.label}</p>
</div>
<div>
<p className="text-xs text-fg-muted uppercase tracking-wider mb-1">Type</p>
<span className="inline-block px-2 py-1 bg-bg-warm rounded-[var(--r-pill)] text-xs font-mono">
{selectedNode.type}
</span>
</div>
<div>
<p className="text-xs text-fg-muted uppercase tracking-wider mb-1">Beschrijving</p>
<p className="text-sm leading-relaxed">{selectedNode.description}</p>
</div>
<div>
<p className="text-xs text-fg-muted uppercase tracking-wider mb-1">ID</p>
<p className="text-xs font-mono text-fg-muted break-all">{selectedNode.id}</p>
</div>
</div>
) : (
<p className="text-sm text-fg-muted">Klik op een node in de graaf om details te bekijken.</p>
)}
</div>
</div>
);
};
export default KnowledgeGraph;