341 lines
9.2 KiB
Plaintext
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> |