@@ -172,13 +232,24 @@ const nodeTypeCounts = {
console.error("Cytoscape library not loaded. Make sure it's included via the script tag.");
const loadingEl = document.getElementById('graph-loading');
if(loadingEl) loadingEl.innerHTML = "
Error: Cytoscape library not loaded.
";
- return;
+ return;
}
const loadingEl = document.getElementById('graph-loading');
const nodeDetailsEl = document.getElementById('node-details');
const closeDetailsBtn = document.getElementById('close-details');
const graphContainer = document.getElementById('knowledge-graph');
+ const graphWrapper = document.querySelector('.graph-container-wrapper');
+ const fullscreenToggle = document.getElementById('fullscreen-toggle');
+ const fullscreenEnterIcon = document.getElementById('fullscreen-enter-icon');
+ const fullscreenExitIcon = document.getElementById('fullscreen-exit-icon');
+ const fullPostContent = document.getElementById('full-post-content');
+ const closeFullPostBtn = document.getElementById('close-full-post');
+ const fullPostTitle = document.getElementById('full-post-title');
+ const fullPostContainer = document.getElementById('full-post-container');
+ const fullPostCategory = document.getElementById('full-post-category');
+ const fullPostTags = document.getElementById('full-post-tags');
+ const fullPostLink = document.getElementById('full-post-link');
if (!graphContainer) {
console.error("Knowledge graph container not found!");
@@ -192,21 +263,24 @@ const nodeTypeCounts = {
// Format data for Cytoscape
const elements = [];
-
+
+ // State variables
+ let isFullscreen = false;
+
// Add nodes with appropriate styling based on type
graphData.nodes.forEach(node => {
let nodeColor;
-
+
if (node.type === 'post') {
// Posts get category color if available
- nodeColor = node.category && predefinedColors[node.category]
- ? predefinedColors[node.category]
+ nodeColor = node.category && predefinedColors[node.category]
+ ? predefinedColors[node.category]
: nodeTypeColors['post'];
} else {
// Tags and categories get their type color
nodeColor = nodeTypeColors[node.type];
}
-
+
elements.push({
data: {
id: node.id,
@@ -214,23 +288,24 @@ const nodeTypeCounts = {
type: node.type,
category: node.category || '',
tags: node.tags || [],
- size: nodeSizes[node.id] || 25,
- color: nodeColor,
- url: node.url || '#'
+ size: nodeSizes[node.id] || 25,
+ color: nodeColor,
+ url: node.url || '#',
+ content: node.content || '' // Include content data for post nodes
}
});
});
-
+
// Add edges
graphData.edges.forEach((edge, index) => {
if (graphData.nodes.some(n => n.id === edge.source) && graphData.nodes.some(n => n.id === edge.target)) {
elements.push({
- data: {
- id: `e${index}`,
- source: edge.source,
- target: edge.target,
+ data: {
+ id: `e${index}`,
+ source: edge.source,
+ target: edge.target,
type: edge.type || 'post-tag',
- weight: edge.strength || 1
+ weight: edge.strength || 1
}
});
} else {
@@ -242,31 +317,31 @@ const nodeTypeCounts = {
const cy = cytoscape({
container: graphContainer,
elements: elements,
- style: [
+ style: [
// Base node styles
- { selector: 'node', style: {
- 'background-color': 'data(color)',
- 'label': 'data(label)',
- 'width': 'data(size)',
- 'height': 'data(size)',
- 'font-size': '10px',
- 'color': '#E2E8F0',
- 'text-valign': 'bottom',
- 'text-halign': 'center',
- 'text-margin-y': '7px',
- 'text-background-opacity': 0.7,
- 'text-background-color': '#0F1219',
- 'text-background-padding': '3px',
- 'text-background-shape': 'roundrectangle',
- 'text-max-width': '120px',
- 'text-wrap': 'ellipsis',
- 'text-overflow-wrap': 'anywhere',
- 'border-width': '2px',
- 'border-color': '#0F1219',
- 'border-opacity': 0.8,
+ { selector: 'node', style: {
+ 'background-color': 'data(color)',
+ 'label': 'data(label)',
+ 'width': 'data(size)',
+ 'height': 'data(size)',
+ 'font-size': '10px',
+ 'color': '#E2E8F0',
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'text-margin-y': '7px',
+ 'text-background-opacity': 0.7,
+ 'text-background-color': '#0F1219',
+ 'text-background-padding': '3px',
+ 'text-background-shape': 'roundrectangle',
+ 'text-max-width': '120px',
+ 'text-wrap': 'ellipsis',
+ 'text-overflow-wrap': 'anywhere',
+ 'border-width': '2px',
+ 'border-color': '#0F1219',
+ 'border-opacity': 0.8,
'z-index': 10,
- 'text-outline-width': 1,
- 'text-outline-color': '#000',
+ 'text-outline-width': 1,
+ 'text-outline-color': '#000',
'text-outline-opacity': 0.5
}},
// Post node specific styles
@@ -287,12 +362,12 @@ const nodeTypeCounts = {
'border-width': '1px'
}},
// Edge styles
- { selector: 'edge', style: {
- 'width': 'mapData(weight, 1, 10, 1, 3)',
- 'line-color': 'rgba(226, 232, 240, 0.2)',
- 'curve-style': 'bezier',
- 'opacity': 0.6,
- 'z-index': 1
+ { selector: 'edge', style: {
+ 'width': 'mapData(weight, 1, 10, 1, 3)',
+ 'line-color': 'rgba(226, 232, 240, 0.2)',
+ 'curve-style': 'bezier',
+ 'opacity': 0.6,
+ 'z-index': 1
}},
// Post-tag edge specific styles
{ selector: 'edge[type="post-tag"]', style: {
@@ -307,45 +382,87 @@ const nodeTypeCounts = {
// Selection and hover styles
{ selector: '.highlighted', style: { 'background-color': 'data(color)', 'border-color': '#FFFFFF', 'border-width': '3px', 'color': '#FFFFFF', 'text-background-opacity': 0.9, 'opacity': 1, 'z-index': 20 } },
{ selector: '.filtered', style: { 'background-color': 'data(color)', 'border-color': '#FFFFFF', 'border-width': '2px', 'color': '#FFFFFF', 'text-background-opacity': 0.8, 'opacity': 0.8, 'z-index': 15 } },
- { selector: '.faded', style: { 'opacity': 0.15, 'text-opacity': 0.3, 'background-opacity': 0.3, 'z-index': 1 } },
+ { selector: '.faded', style: { 'opacity': 0.15, 'text-opacity': 0.3, 'background-opacity': 0.3, 'z-index': 1 } },
{ selector: 'node:selected', style: { 'border-width': '4px', 'border-color': '#FFFFFF', 'border-opacity': 1, 'background-color': 'data(color)', 'text-opacity': 1, 'color': '#FFFFFF', 'z-index': 30 } },
{ selector: 'edge:selected', style: { 'width': 'mapData(weight, 1, 10, 2, 6)', 'line-color': '#FFFFFF', 'opacity': 1, 'z-index': 30 } }
],
// Update layout for better visualization of post-tag connections
- layout: {
- name: 'cose',
- idealEdgeLength: 80,
- nodeOverlap: 20,
- refresh: 20,
- fit: true,
- padding: 30,
- randomize: false,
- componentSpacing: 100,
- nodeRepulsion: 450000,
- edgeElasticity: 100,
- nestingFactor: 5,
- gravity: 80,
- numIter: 1000,
- initialTemp: 200,
- coolingFactor: 0.95,
- minTemp: 1.0
+ layout: {
+ name: 'cose',
+ idealEdgeLength: 80,
+ nodeOverlap: 20,
+ refresh: 20,
+ fit: true,
+ padding: 30,
+ randomize: false,
+ componentSpacing: 100,
+ nodeRepulsion: 450000,
+ edgeElasticity: 100,
+ nestingFactor: 5,
+ gravity: 80,
+ numIter: 1000,
+ initialTemp: 200,
+ coolingFactor: 0.95,
+ minTemp: 1.0
},
zoom: 1, minZoom: 0.1, maxZoom: 3, zoomingEnabled: true, userZoomingEnabled: true, panningEnabled: true, userPanningEnabled: true, boxSelectionEnabled: false,
});
// Hide loading screen
if (loadingEl) {
- setTimeout(() => { loadingEl.classList.add('hidden'); }, 500);
+ setTimeout(() => { loadingEl.classList.add('hidden'); }, 500);
+ }
+
+ // --- Fullscreen Toggle Functionality ---
+ if (fullscreenToggle) {
+ fullscreenToggle.addEventListener('click', toggleFullscreen);
+ }
+
+ // Toggle fullscreen function
+ function toggleFullscreen() {
+ isFullscreen = !isFullscreen;
+
+ if (isFullscreen) {
+ // Enable fullscreen mode
+ graphWrapper.classList.add('fullscreen');
+ fullscreenEnterIcon.classList.add('hidden');
+ fullscreenExitIcon.classList.remove('hidden');
+
+ // Hide the node details panel if it's visible
+ if (nodeDetailsEl) nodeDetailsEl.classList.remove('active');
+
+ // In fullscreen, we adjust the cytoscape layout to fit
+ setTimeout(() => {
+ cy.resize();
+ cy.fit(null, 30);
+ }, 300); // Wait for transition to complete
+ } else {
+ // Disable fullscreen mode
+ graphWrapper.classList.remove('fullscreen');
+ fullscreenEnterIcon.classList.remove('hidden');
+ fullscreenExitIcon.classList.add('hidden');
+
+ // Hide the full post content panel
+ if (fullPostContent) {
+ fullPostContent.classList.remove('active');
+ }
+
+ // Reset the cytoscape layout
+ setTimeout(() => {
+ cy.resize();
+ cy.fit(null, 30);
+ }, 300); // Wait for transition to complete
+ }
}
// --- Interactions ---
let hoverTimeout;
cy.on('mouseover', 'node', function(e) {
const node = e.target;
- clearTimeout(hoverTimeout);
+ clearTimeout(hoverTimeout);
node.addClass('highlighted');
node.connectedEdges().addClass('highlighted');
- graphContainer.style.cursor = 'pointer';
+ graphContainer.style.cursor = 'pointer';
});
cy.on('mouseout', 'node', function(e) {
@@ -356,23 +473,40 @@ const nodeTypeCounts = {
node.connectedEdges().removeClass('highlighted');
}
}, 100);
- graphContainer.style.cursor = 'default';
+ graphContainer.style.cursor = 'default';
});
+ // Node click handler - dispatches to appropriate function based on fullscreen mode
cy.on('tap', 'node', function(e) {
const node = e.target;
+
+ if (isFullscreen) {
+ handleNodeClickFullscreen(node);
+ } else {
+ handleNodeClickNormal(node);
+ }
+
+ // Common highlight functionality for both modes
+ cy.elements().removeClass('highlighted').removeClass('faded');
+ node.addClass('highlighted');
+ node.neighborhood().addClass('highlighted');
+ cy.elements().difference(node.neighborhood().union(node)).addClass('faded');
+ });
+
+ // Handle node click in normal mode - shows metadata in sidebar
+ function handleNodeClickNormal(node) {
const nodeData = node.data();
-
+
if (nodeDetailsEl) {
// Set node title
document.getElementById('node-title').textContent = nodeData.label;
-
+
// Set node type
- const nodeTypeEl = document.getElementById('node-type').querySelector('.type-value') ||
+ const nodeTypeEl = document.getElementById('node-type').querySelector('.type-value') ||
document.getElementById('node-type');
nodeTypeEl.textContent = nodeData.type.charAt(0).toUpperCase() + nodeData.type.slice(1);
nodeTypeEl.className = `type-value ${nodeData.type}-type`;
-
+
// Set category if it's a post
const categorySection = document.getElementById('node-category');
if (nodeData.type === 'post' && nodeData.category) {
@@ -380,17 +514,17 @@ const nodeTypeCounts = {
const categoryEl = categorySection.querySelector('.category-value');
categoryEl.textContent = nodeData.category;
// Use category colors
- const catColor = predefinedColors[nodeData.category] || 'var(--text-secondary)';
+ const catColor = predefinedColors[nodeData.category] || 'var(--text-secondary)';
categoryEl.style.backgroundColor = `${catColor}33`; // Add alpha
categoryEl.style.color = catColor;
} else {
categorySection.style.display = 'none';
}
-
+
// Set tags if it's a post
const tagsSection = document.getElementById('node-tags');
const tagsContainer = tagsSection.querySelector('.tags-container');
-
+
if (nodeData.type === 'post' && nodeData.tags && nodeData.tags.length > 0) {
tagsSection.style.display = 'block';
tagsContainer.innerHTML = '';
@@ -404,13 +538,13 @@ const nodeTypeCounts = {
if (tagNode.length > 0) {
tagNode.trigger('tap');
}
-
+
// Try to find and click tag filter button
try {
const tagFilterBtn = Array.from(
document.querySelectorAll('.tag-filter-btn')
).find(btn => btn.dataset.tag === tag);
-
+
if (tagFilterBtn) {
tagFilterBtn.click();
}
@@ -423,7 +557,7 @@ const nodeTypeCounts = {
} else {
tagsSection.style.display = 'none';
}
-
+
// Set connections
const connectionsList = document.getElementById('node-connections').querySelector('.connections-list');
connectionsList.innerHTML = '';
@@ -433,17 +567,17 @@ const nodeTypeCounts = {
const connectedData = connectedNode.data();
const listItem = document.createElement('li');
const link = document.createElement('a');
- link.href = '#';
+ link.href = '#';
link.textContent = connectedData.label;
link.dataset.id = connectedData.id;
link.addEventListener('click', (evt) => {
evt.preventDefault();
- cy.$(':selected').unselect();
+ cy.$(':selected').unselect();
const targetNode = cy.getElementById(connectedData.id);
if (targetNode) {
- targetNode.select();
- cy.animate({ center: { eles: targetNode }, zoom: cy.zoom() }, { duration: 300 });
- targetNode.trigger('tap');
+ targetNode.select();
+ cy.animate({ center: { eles: targetNode }, zoom: cy.zoom() }, { duration: 300 });
+ targetNode.trigger('tap');
}
});
listItem.appendChild(link);
@@ -452,7 +586,7 @@ const nodeTypeCounts = {
} else {
connectionsList.innerHTML = '
No connections ';
}
-
+
// Set link text and URL based on node type
const nodeLink = document.getElementById('node-link');
if (nodeData.type === 'post') {
@@ -463,29 +597,23 @@ const nodeTypeCounts = {
nodeLink.textContent = 'View Content';
}
nodeLink.href = nodeData.url;
-
+
nodeDetailsEl.classList.add('active');
}
-
- // Highlight the node and its connections, fade everything else
- cy.elements().removeClass('highlighted').removeClass('faded');
- node.addClass('highlighted');
- node.neighborhood().addClass('highlighted');
- cy.elements().difference(node.neighborhood().union(node)).addClass('faded');
-
+
// If it's a tag node, try to trigger the corresponding tag filter
if (nodeData.type === 'tag') {
- const tagName = nodeData.label;
+ const tagName = nodeData.label.replace(/^#/, ''); // Remove # prefix if present
try {
// Find and click the corresponding tag filter button
const tagFilterBtn = Array.from(
document.querySelectorAll('.tag-filter-btn')
).find(btn => btn.dataset.tag === tagName);
-
+
if (tagFilterBtn) {
tagFilterBtn.click();
}
-
+
// Scroll to the blog section
const blogSection = document.querySelector('.blog-posts-section');
if (blogSection) {
@@ -495,11 +623,148 @@ const nodeTypeCounts = {
console.log('Tag filter button not found or blog section not found');
}
}
- });
+ }
+ // Handle node click in fullscreen mode - shows full content in side panel
+ function handleNodeClickFullscreen(node) {
+ const nodeData = node.data();
+ console.log('Node clicked in fullscreen mode:', nodeData); // Debug log
+
+ // Only show content for post nodes
+ if (nodeData.type === 'post') {
+ if (fullPostContent) {
+ // Set post title
+ fullPostTitle.textContent = nodeData.label;
+
+ // Set post category if available
+ if (nodeData.category) {
+ const catColor = predefinedColors[nodeData.category] || 'var(--accent-primary)';
+ fullPostCategory.textContent = nodeData.category;
+ fullPostCategory.style.backgroundColor = `${catColor}33`;
+ fullPostCategory.style.color = catColor;
+ fullPostCategory.parentElement.style.display = 'flex';
+ } else {
+ fullPostCategory.parentElement.style.display = 'none';
+ }
+
+ // Set post tags if available
+ if (nodeData.tags && nodeData.tags.length > 0) {
+ fullPostTags.innerHTML = '';
+ nodeData.tags.forEach(tag => {
+ const tagEl = document.createElement('span');
+ tagEl.className = 'post-tag';
+ tagEl.textContent = tag;
+ fullPostTags.appendChild(tagEl);
+ });
+ fullPostTags.parentElement.style.display = 'flex';
+ } else {
+ fullPostTags.parentElement.style.display = 'none';
+ }
+
+ // Check if content is available
+ console.log('Content available:', !!nodeData.content); // Debug log
+
+ // Display the content or a placeholder
+ if (nodeData.content) {
+ fullPostContainer.innerHTML = `
+
+ ${nodeData.content}
+
+ `;
+ } else {
+ // Try to fetch the content if not available
+ console.log('Attempting to fetch content from:', nodeData.url);
+ fullPostContainer.innerHTML = `
+
+ `;
+
+ // Try to load the content from the post URL
+ fetch(nodeData.url)
+ .then(response => response.text())
+ .then(html => {
+ // Extract the post content
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ const articleContent = doc.querySelector('article') ||
+ doc.querySelector('.content') ||
+ doc.querySelector('main');
+
+ if (articleContent) {
+ fullPostContainer.innerHTML = `
+
+ ${articleContent.innerHTML}
+
+ `;
+ } else {
+ // If we can't extract content, show the placeholder
+ showContentPlaceholder("Could not load content. Click 'Read Full Article' to view on the blog.");
+ }
+ })
+ .catch(error => {
+ console.error('Error fetching content:', error);
+ showContentPlaceholder("Error loading content. Click 'Read Full Article' to view on the blog.");
+ });
+ }
+
+ // Set the article link
+ fullPostLink.href = nodeData.url || '#';
+
+ // Make the content panel visible
+ fullPostContent.classList.add('active');
+ fullPostContent.style.display = 'flex';
+ }
+ } else {
+ // Hide the content panel for non-post nodes
+ if (fullPostContent) {
+ fullPostContent.classList.remove('active');
+ fullPostContent.style.display = 'none';
+ }
+ }
+ }
+
+ // Helper function to show content placeholder
+ function showContentPlaceholder() {
+ fullPostContainer.innerHTML = `
+
+
+
+
+
+
+
+
+
Full content preview not available.
+
+ `;
+ }
+
+ // Helper function to fetch post content if not provided
+ // async function fetchPostContent(url) {
+ // if (!url || url === '#') return null;
+ // try {
+ // const response = await fetch(url);
+ // if (!response.ok) return null;
+ // const html = await response.text();
+ // const parser = new DOMParser();
+ // const doc = parser.parseFromString(html, 'text/html');
+ // const articleContent = doc.querySelector('article') || doc.querySelector('.content') || doc.querySelector('main');
+ // return articleContent ? articleContent.innerHTML : null;
+ // } catch (error) {
+ // console.error('Error fetching post content:', error);
+ // return null;
+ // }
+ // }
+
+ // Modify background click handler to account for fullscreen mode
cy.on('tap', function(e) {
- if (e.target === cy) {
+ if (e.target === cy) {
if (nodeDetailsEl) nodeDetailsEl.classList.remove('active');
+ if (isFullscreen && fullPostContent) {
+ fullPostContent.classList.remove('active');
+ }
cy.elements().removeClass('selected highlighted faded');
}
});
@@ -512,6 +777,17 @@ const nodeTypeCounts = {
});
}
+ // Close full post panel button
+ if (closeFullPostBtn) {
+ closeFullPostBtn.addEventListener('click', () => {
+ if (fullPostContent) {
+ fullPostContent.classList.remove('active');
+ }
+ cy.$(':selected').unselect();
+ cy.elements().removeClass('highlighted faded');
+ });
+ }
+
// Filter graph by node type
const filterButtons = document.querySelectorAll('.graph-filter');
filterButtons.forEach(button => {
@@ -519,7 +795,7 @@ const nodeTypeCounts = {
filterButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
const filter = button.dataset.filter;
-
+
if (filter === 'all') {
cy.elements().removeClass('faded highlighted filtered');
} else if (filter === 'posts') {
@@ -538,10 +814,10 @@ const nodeTypeCounts = {
document.getElementById('zoom-in')?.addEventListener('click', () => cy.zoom(cy.zoom() * 1.2));
document.getElementById('zoom-out')?.addEventListener('click', () => cy.zoom(cy.zoom() / 1.2));
document.getElementById('reset-graph')?.addEventListener('click', () => {
- cy.fit(null, 30);
+ cy.fit(null, 30);
cy.elements().removeClass('faded highlighted filtered');
const allFilterButton = document.querySelector('.graph-filter[data-filter="all"]');
- if (allFilterButton) allFilterButton.click();
+ if (allFilterButton) allFilterButton.click();
});
// Add mouse wheel zoom controls
@@ -552,7 +828,7 @@ const nodeTypeCounts = {
cy.style().selector('node').style({ 'text-max-width': '120px', 'font-size': '10px' }).update();
}
});
-
+
// Apply initial filter if specified
if (initialFilter && initialFilter !== 'all') {
const filterButton = document.querySelector(`.graph-filter[data-filter="${initialFilter}"]`);
@@ -560,13 +836,13 @@ const nodeTypeCounts = {
setTimeout(() => filterButton.click(), 500);
}
}
-
+
// Connect search input if it exists
const searchInput = document.getElementById('search-input');
if (searchInput) {
searchInput.addEventListener('input', e => {
const term = e.target.value.toLowerCase();
-
+
if (!term) {
// Reset graph view if search is cleared
cy.elements().removeClass('highlighted faded filtered');
@@ -574,62 +850,165 @@ const nodeTypeCounts = {
if (allFilterBtn) allFilterBtn.click();
return;
}
-
+
// Reset previous filters
cy.elements().addClass('faded').removeClass('highlighted filtered');
-
+
// Find nodes that match the search term
const matchingNodes = cy.nodes().filter(node => {
const label = node.data('label').toLowerCase();
return label.includes(term);
});
-
+
if (matchingNodes.length) {
matchingNodes.removeClass('faded').addClass('highlighted');
matchingNodes.connectedEdges().removeClass('faded');
}
});
}
-
+
// Connect tag filter buttons if they exist
const tagFilterButtons = document.querySelectorAll('.tag-filter-btn');
if (tagFilterButtons.length) {
tagFilterButtons.forEach(btn => {
btn.addEventListener('click', () => {
const tagName = btn.dataset.tag;
-
+
if (tagName === 'all') {
// Reset graph view if "All" is selected
cy.elements().removeClass('highlighted faded filtered');
const allFilterBtn = document.querySelector('.graph-filter[data-filter="all"]');
+// Add event listener to prevent redirect in fullscreen mode
+ if (fullPostLink) {
+ fullPostLink.addEventListener('click', (e) => {
+ if (isFullscreen) {
+ // If in fullscreen, prevent default behavior to keep the user in the graph view
+ e.preventDefault();
+
+ // Instead, display a message to exit fullscreen to visit the full article
+ const message = document.createElement('div');
+ message.className = 'fullscreen-message';
+ message.textContent = 'Exit fullscreen to visit the full article page';
+ message.style.position = 'absolute';
+ message.style.bottom = '70px'; // Adjust as needed
+ message.style.left = '50%';
+ message.style.transform = 'translateX(-50%)';
+ message.style.background = 'rgba(0, 0, 0, 0.75)';
+ message.style.color = 'white';
+ message.style.padding = '8px 16px';
+ message.style.borderRadius = '4px';
+ message.style.zIndex = '1000';
+ message.style.transition = 'opacity 0.3s ease';
+
+ fullPostContent.appendChild(message);
+
+ // Remove the message after 3 seconds
+ setTimeout(() => {
+ message.style.opacity = '0';
+ setTimeout(() => {
+ message.remove();
+ }, 300);
+ }, 3000);
+ }
+ });
+ }
+
+ // Listen for ESC key to exit fullscreen
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && isFullscreen) {
+ toggleFullscreen();
+ }
+ });
+
+ // Update window resize handler to preserve layout in fullscreen mode
+ window.addEventListener('resize', () => {
+ if (cy) {
+ cy.resize();
+ if (!isFullscreen) {
+ cy.fit(null, 30);
+ }
+ }
+ });
if (allFilterBtn) allFilterBtn.click();
return;
}
-
+
// Find the tag node
- const tagNode = cy.nodes().filter(node =>
- node.data('type') === 'tag' &&
+ const tagNode = cy.nodes().filter(node =>
+ node.data('type') === 'tag' &&
node.data('label') === tagName
);
-
+
if (tagNode.length) {
// Highlight this tag and connected posts in the graph
cy.elements().addClass('faded').removeClass('highlighted filtered');
tagNode.removeClass('faded').addClass('highlighted');
-
+
// Get connected posts
const connectedPosts = tagNode.neighborhood('node[type="post"]');
connectedPosts.removeClass('faded').addClass('filtered');
-
+
// Highlight connecting edges
tagNode.connectedEdges().removeClass('faded').addClass('highlighted');
}
});
});
}
-
+
// Dispatch graphReady event for external listeners
document.dispatchEvent(new CustomEvent('graphReady', { detail: { cy } }));
+
+ // Add event listener to prevent redirect in fullscreen mode
+ if (fullPostLink) {
+ fullPostLink.addEventListener('click', (e) => {
+ if (isFullscreen) {
+ // If in fullscreen, prevent default behavior to keep the user in the graph view
+ e.preventDefault();
+
+ // Instead, display a message to exit fullscreen to visit the full article
+ const message = document.createElement('div');
+ message.className = 'fullscreen-message';
+ message.textContent = 'Exit fullscreen to visit the full article page';
+ message.style.position = 'absolute';
+ message.style.bottom = '70px'; // Adjust as needed
+ message.style.left = '50%';
+ message.style.transform = 'translateX(-50%)';
+ message.style.background = 'rgba(0, 0, 0, 0.75)';
+ message.style.color = 'white';
+ message.style.padding = '8px 16px';
+ message.style.borderRadius = '4px';
+ message.style.zIndex = '1000';
+ message.style.transition = 'opacity 0.3s ease';
+
+ fullPostContent.appendChild(message);
+
+ // Remove the message after 3 seconds
+ setTimeout(() => {
+ message.style.opacity = '0';
+ setTimeout(() => {
+ message.remove();
+ }, 300);
+ }, 3000);
+ }
+ });
+ }
+
+ // Listen for ESC key to exit fullscreen
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && isFullscreen) {
+ toggleFullscreen();
+ }
+ });
+
+ // Update window resize handler to preserve layout in fullscreen mode
+ window.addEventListener('resize', () => {
+ if (cy) {
+ cy.resize();
+ if (!isFullscreen) {
+ cy.fit(null, 30);
+ }
+ }
+ });
}
// Initialize graph on DOMContentLoaded or if already loaded
@@ -641,397 +1020,95 @@ const nodeTypeCounts = {
\ No newline at end of file
diff --git a/src/components/MiniGraph.astro b/src/components/MiniGraph.astro
new file mode 100644
index 0000000..28eaa7a
--- /dev/null
+++ b/src/components/MiniGraph.astro
@@ -0,0 +1,222 @@
+---
+// MiniGraph.astro - A standalone mini knowledge graph component
+// This component is designed to work independently from the blog structure
+
+// Define props interface
+interface Props {
+ slug: string; // Current post slug
+ title: string; // Current post title
+ tags?: string[]; // Current post tags
+ category?: string; // Current post category
+}
+
+// Extract props with defaults
+const {
+ slug,
+ title,
+ tags = [],
+ category = "Uncategorized"
+} = Astro.props;
+
+// Generate unique ID for the graph container
+const graphId = `graph-${Math.random().toString(36).substring(2, 8)}`;
+
+// Prepare simple graph data for just the post and its tags
+const nodes = [
+ // Current post node
+ {
+ id: slug,
+ label: title,
+ type: "post"
+ },
+ // Tag nodes
+ ...tags.map(tag => ({
+ id: `tag-${tag}`,
+ label: tag,
+ type: "tag"
+ }))
+];
+
+// Create edges connecting post to tags
+const edges = tags.map(tag => ({
+ source: slug,
+ target: `tag-${tag}`,
+ type: "post-tag"
+}));
+
+// Prepare graph data object
+const graphData = { nodes, edges };
+---
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/MiniKnowledgeGraph.astro b/src/components/MiniKnowledgeGraph.astro
new file mode 100644
index 0000000..bc49e1f
--- /dev/null
+++ b/src/components/MiniKnowledgeGraph.astro
@@ -0,0 +1,341 @@
+---
+// 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
();
+const addedPostIds = new Set();
+
+// 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 };
+---
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/Terminal.astro b/src/components/Terminal.astro
index a0b99d2..5980720 100644
--- a/src/components/Terminal.astro
+++ b/src/components/Terminal.astro
@@ -2,37 +2,32 @@
// Terminal.astro
// A component that displays terminal-like interface with animated commands and outputs
-interface Command {
- prompt: string;
- command: string;
- output?: string[];
- delay?: number;
-}
-
-interface Props {
- commands: Command[];
+export interface Props {
title?: string;
- theme?: 'dark' | 'light';
- interactive?: boolean;
+ height?: string;
showTitleBar?: boolean;
+ showPrompt?: boolean;
+ commands?: {
+ prompt: string;
+ command: string;
+ output?: string[];
+ delay?: number;
+ }[];
}
-const {
- commands,
- title = "argobox:~/homelab",
- theme = "dark",
- interactive = false,
- showTitleBar = true
+const {
+ title = "terminal",
+ height = "auto",
+ showTitleBar = true,
+ showPrompt = true,
+ commands = []
} = Astro.props;
// Make the last command have the typing effect
const lastIndex = commands.length - 1;
-
-// Conditionally add classes based on props
-const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : ''}`;
---
-