feat: add KnowledgeGraph component for visualizing and managing learning topics and relationships
This commit is contained in:
@@ -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>
|
||||||
|
<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">{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>
|
||||||
|
|||||||
Reference in New Issue
Block a user