feat: add KnowledgeGraph component for visualizing and managing learning topics and relationships
All checks were successful
On Push to Main / test (push) Successful in 30s
On Push to Main / publish (push) Successful in 57s
On Push to Main / deploy-dev (push) Successful in 1m37s

This commit is contained in:
RaymondVerhoef
2026-05-19 12:33:12 +02:00
parent 8529def748
commit 08aaed591f

View File

@@ -26,6 +26,7 @@ const KnowledgeGraph = () => {
const [topics, setTopics] = useState([]); const [topics, setTopics] = useState([]);
const [relations, setRelations] = useState([]); const [relations, setRelations] = useState([]);
const [showExcludeNodes, setShowExcludeNodes] = useState(false);
const reloadKb = useCallback(() => { const reloadKb = useCallback(() => {
Promise.all([db.getTopics(), db.getRelations()]).then(([allTopics, allRelations]) => { Promise.all([db.getTopics(), db.getRelations()]).then(([allTopics, allRelations]) => {
@@ -71,8 +72,15 @@ const KnowledgeGraph = () => {
const { width, height } = dimensions; const { width, height } = dimensions;
d3.select(svgRef.current).selectAll('*').remove(); d3.select(svgRef.current).selectAll('*').remove();
const nodes = topics.map(d => ({ ...d })); const filteredTopics = showExcludeNodes ? topics : topics.filter(t => t.learning_relevance !== 'exclude');
const links = relations.map(d => ({ ...d })); const filteredTopicIds = new Set(filteredTopics.map(t => t.id));
const filteredRelations = relations.filter(r =>
filteredTopicIds.has(r.source?.id || r.source) &&
filteredTopicIds.has(r.target?.id || r.target)
);
const nodes = filteredTopics.map(d => ({ ...d }));
const links = filteredRelations.map(d => ({ ...d }));
const svg = d3.select(svgRef.current) const svg = d3.select(svgRef.current)
.attr('viewBox', [0, 0, width, height]) .attr('viewBox', [0, 0, width, height])
@@ -116,6 +124,12 @@ const KnowledgeGraph = () => {
.attr('fill', d => color(d.type) || '#1F5560') .attr('fill', d => color(d.type) || '#1F5560')
.attr('stroke', '#fff') .attr('stroke', '#fff')
.attr('stroke-width', 2) .attr('stroke-width', 2)
.attr('stroke-dasharray', d => d.learning_relevance === 'exclude' ? '4,4' : 'none')
.attr('opacity', d => {
if (d.learning_relevance === 'exclude') return 0.3;
if (d.learning_relevance === 'peripheral') return 0.5;
return 1;
})
.on('click', (event, d) => { .on('click', (event, d) => {
setSelectedNode(d); setSelectedNode(d);
setIsEditing(false); setIsEditing(false);
@@ -392,6 +406,15 @@ Analyze this graph and return ONLY the optimized JSON object with this EXACT str
<div className="w-full md:w-80 bg-paper p-6 overflow-y-auto border-t md:border-t-0 border-bg-warm flex flex-col"> <div className="w-full md:w-80 bg-paper p-6 overflow-y-auto border-t md:border-t-0 border-bg-warm flex flex-col">
<div className="mb-6 pb-4 border-b border-bg-warm space-y-3"> <div className="mb-6 pb-4 border-b border-bg-warm space-y-3">
<label className="flex items-center gap-2 text-sm text-fg-muted cursor-pointer mb-3">
<input
type="checkbox"
checked={showExcludeNodes}
onChange={e => setShowExcludeNodes(e.target.checked)}
className="rounded bg-bg-warm border-transparent focus:ring-0 text-teal"
/>
Show Excluded Nodes (Reference Material)
</label>
<div> <div>
<Button <Button
onClick={analyzeGraph} onClick={analyzeGraph}
@@ -486,6 +509,19 @@ Analyze this graph and return ONLY the optimized JSON object with this EXACT str
<option value="process">Process</option> <option value="process">Process</option>
</select> </select>
</div> </div>
<div>
<label className="text-xs text-fg-muted uppercase tracking-wider mb-1 block">Learning Relevance</label>
<select
className="w-full border border-bg-warm rounded-[var(--r-sm)] px-3 py-1.5 text-sm bg-bg"
value={editData.learning_relevance || 'standard'}
onChange={e => setEditData({...editData, learning_relevance: e.target.value})}
>
<option value="core">Core</option>
<option value="standard">Standard</option>
<option value="peripheral">Peripheral</option>
<option value="exclude">Exclude</option>
</select>
</div>
<div> <div>
<label className="text-xs text-fg-muted uppercase tracking-wider mb-1 block">Description</label> <label className="text-xs text-fg-muted uppercase tracking-wider mb-1 block">Description</label>
<textarea <textarea
@@ -506,8 +542,11 @@ Analyze this graph and return ONLY the optimized JSON object with this EXACT str
<p className="font-medium">{selectedNode.label}</p> <p className="font-medium">{selectedNode.label}</p>
</div> </div>
<div> <div>
<p className="text-xs text-fg-muted uppercase tracking-wider mb-1">Type</p> <p className="text-xs text-fg-muted uppercase tracking-wider mb-1">Type & Relevance</p>
<span className="inline-block px-2 py-1 bg-bg-warm rounded-[var(--r-pill)] text-xs font-mono">{selectedNode.type}</span> <div className="flex gap-2">
<span className="inline-block px-2 py-1 bg-bg-warm rounded-[var(--r-pill)] text-xs font-mono">{selectedNode.type}</span>
<span className="inline-block px-2 py-1 bg-bg-warm rounded-[var(--r-pill)] text-xs font-mono opacity-80">{selectedNode.learning_relevance || 'standard'}</span>
</div>
</div> </div>
<div> <div>
<p className="text-xs text-fg-muted uppercase tracking-wider mb-1">Description</p> <p className="text-xs text-fg-muted uppercase tracking-wider mb-1">Description</p>