laforceit-blog/src/components/MiniKnowledgeGraph.astro

341 lines
9.2 KiB
Plaintext

---
// MiniKnowledgeGraph.astro - Inline version that replaces the Tags section
// Designed to work within the existing sidebar structure
export interface GraphNode {
id: string;
label: string;
type: 'post' | 'tag' | 'category';
url?: string;
}
export interface GraphEdge {
source: string;
target: string;
type: 'post-tag' | 'post-post';
}
interface Props {
currentPost: any;
relatedPosts?: any[];
}
const { currentPost, relatedPosts = [] } = Astro.props;
// Generate unique ID for the graph container
const graphId = `mini-cy-${Math.random().toString(36).substring(2, 9)}`;
// Ensure currentPost has necessary properties
const safeCurrentPost = {
id: currentPost.slug || 'current-post',
title: currentPost.data?.title || 'Current Post',
tags: currentPost.data?.tags || [],
category: currentPost.data?.category || 'Uncategorized',
};
// Prepare graph data
const nodes: GraphNode[] = [];
const edges: GraphEdge[] = [];
const addedTagIds = new Set<string>();
const addedPostIds = new Set<string>();
// Add current post node
nodes.push({
id: safeCurrentPost.id,
label: safeCurrentPost.title,
type: 'post',
url: `/posts/${safeCurrentPost.id}/`
});
addedPostIds.add(safeCurrentPost.id);
// Add tags from current post
safeCurrentPost.tags.forEach((tag: string) => {
const tagId = `tag-${tag}`;
// Only add if not already added
if (!addedTagIds.has(tagId)) {
nodes.push({
id: tagId,
label: tag,
type: 'tag',
url: `/tag/${tag}/`
});
addedTagIds.add(tagId);
}
// Add edge from current post to tag
edges.push({
source: safeCurrentPost.id,
target: tagId,
type: 'post-tag'
});
});
// Add related posts and their connections
if (relatedPosts && relatedPosts.length > 0) {
relatedPosts.forEach(post => {
if (!post) return;
const postId = post.slug || `post-${Math.random().toString(36).substring(2, 9)}`;
// Skip if already added or is the current post
if (addedPostIds.has(postId) || postId === safeCurrentPost.id) {
return;
}
// Add related post node
nodes.push({
id: postId,
label: post.data?.title || 'Related Post',
type: 'post',
url: `/posts/${postId}/`
});
addedPostIds.add(postId);
// Add edge from current post to related post
edges.push({
source: safeCurrentPost.id,
target: postId,
type: 'post-post'
});
// Add shared tags and their connections
const postTags = post.data?.tags || [];
postTags.forEach((tag: string) => {
// Only add connections for tags that the current post also has
if (safeCurrentPost.tags.includes(tag)) {
const tagId = `tag-${tag}`;
// Add edge from related post to shared tag
edges.push({
source: postId,
target: tagId,
type: 'post-tag'
});
}
});
});
}
// Generate graph data
const graphData = { nodes, edges };
---
<div class="sidebar-card knowledge-graph-card">
<h3 class="sidebar-title">Post Connections</h3>
<div class="mini-knowledge-graph">
<div id={graphId} class="mini-cy"></div>
</div>
</div>
<style>
.knowledge-graph-card {
margin-bottom: 1.5rem;
}
.sidebar-title {
margin-bottom: 1rem;
font-size: 1.1rem;
color: var(--text-primary);
}
.mini-knowledge-graph {
position: relative;
width: 100%;
height: 200px;
}
.mini-cy {
width: 100%;
height: 100%;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--card-border, rgba(56, 189, 248, 0.2));
background: rgba(15, 23, 42, 0.2);
}
</style>
<script define:vars={{ graphId, graphData }}>
// Initialize the miniature knowledge graph
function initializeMiniGraph() {
// Ensure Cytoscape is available
if (typeof cytoscape === 'undefined') {
console.error('[MiniKnowledgeGraph] Cytoscape library not loaded.');
return;
}
// Find the container
const container = document.getElementById(graphId);
if (!container) {
console.error(`[MiniKnowledgeGraph] Container #${graphId} not found.`);
return;
}
try {
// Check if we have any nodes to display
if (!graphData.nodes || graphData.nodes.length === 0) {
console.warn('[MiniKnowledgeGraph] No nodes to display.');
container.innerHTML = '<div style="display:flex;height:100%;align-items:center;justify-content:center;color:var(--text-secondary);">No connections available</div>';
return;
}
// Initialize Cytoscape with improved layout parameters for small space
const cy = cytoscape({
container,
elements: [
...graphData.nodes.map(node => ({
data: {
id: node.id,
label: node.label,
type: node.type,
url: node.url
}
})),
...graphData.edges.map((edge, index) => ({
data: {
id: `e${index}`,
source: edge.source,
target: edge.target,
type: edge.type
}
}))
],
style: [
// Node styling
{
selector: 'node',
style: {
'background-color': '#3B82F6', // Default blue for posts
'label': 'data(label)',
'width': 15,
'height': 15,
'font-size': '8px',
'color': '#E2E8F0',
'text-valign': 'bottom',
'text-halign': 'center',
'text-margin-y': 4,
'text-wrap': 'ellipsis',
'text-max-width': '60px',
'border-width': 1,
'border-color': '#0F1219',
'border-opacity': 0.8
}
},
// Post node specific styles
{
selector: 'node[type="post"]',
style: {
'background-color': '#3B82F6', // Blue for posts
'shape': 'ellipse',
'width': 18,
'height': 18
}
},
// Current post node (first in the nodes array)
{
selector: `#${graphData.nodes[0]?.id}`,
style: {
'background-color': '#06B6D4', // Cyan for current post
'width': 25,
'height': 25,
'border-width': 2,
'border-color': '#E2E8F0'
}
},
// Tag node specific styles
{
selector: 'node[type="tag"]',
style: {
'background-color': '#10B981', // Green for tags
'shape': 'diamond',
'width': 15,
'height': 15
}
},
// Edge styles
{
selector: 'edge',
style: {
'width': 1,
'line-color': 'rgba(226, 232, 240, 0.4)',
'curve-style': 'bezier',
'opacity': 0.6
}
},
// Post-tag edge specific styles
{
selector: 'edge[type="post-tag"]',
style: {
'line-color': 'rgba(16, 185, 129, 0.6)', // Green
'line-style': 'dashed'
}
},
// Post-post edge specific styles
{
selector: 'edge[type="post-post"]',
style: {
'line-color': 'rgba(59, 130, 246, 0.6)', // Blue
'line-style': 'solid',
'width': 1.5
}
},
// Hover styles
{
selector: 'node:hover',
style: {
'background-color': '#F59E0B', // Amber on hover
'border-color': '#FFFFFF',
'border-width': 2,
'cursor': 'pointer'
}
}
],
// Use a compact layout for sidebar
layout: {
name: 'cose',
animate: false,
fit: true,
padding: 5,
nodeRepulsion: function(node) {
return 10000; // Stronger repulsion to prevent overlap in small space
},
idealEdgeLength: 50,
edgeElasticity: 0.45,
nestingFactor: 0.1,
gravity: 0.25,
numIter: 1500,
initialTemp: 1000,
coolingFactor: 0.99,
minTemp: 1.0
}
});
// Add click event for nodes
cy.on('tap', 'node', function(evt) {
const node = evt.target;
const url = node.data('url');
if (url) {
window.location.href = url;
}
});
// Center the graph
cy.fit(undefined, 10);
} catch (error) {
console.error('[MiniKnowledgeGraph] Error initializing Cytoscape:', error);
container.innerHTML = '<div style="padding:10px;color:var(--text-secondary);">Error loading graph</div>';
}
}
// Wait for DOM to be ready and ensure proper initialization
document.addEventListener('DOMContentLoaded', function() {
// Delay initialization slightly to ensure container has dimensions
setTimeout(initializeMiniGraph, 100);
});
// Also handle the case where the script loads after DOMContentLoaded
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setTimeout(initializeMiniGraph, 100);
}
</script>