fresh-main #6
|
@ -28,26 +28,13 @@ interface Props {
|
||||||
|
|
||||||
const { graphData, height = "60vh" } = Astro.props;
|
const { graphData, height = "60vh" } = Astro.props;
|
||||||
|
|
||||||
// Generate colors based on categories for nodes
|
// Define fixed colors for node types
|
||||||
const uniqueCategories = [...new Set(graphData.nodes.map(node => node.category || 'Uncategorized'))];
|
const nodeTypeColors = {
|
||||||
const categoryColors = {};
|
post: 'var(--accent-secondary)', // Blue for posts
|
||||||
const predefinedColors = {
|
tag: 'var(--accent-primary)' // Cyan for tags
|
||||||
'Kubernetes': '#326CE5', 'Docker': '#2496ED', 'DevOps': '#FF6F61',
|
|
||||||
'Homelab': '#06B6D4', 'Networking': '#9333EA', 'Infrastructure': '#10B981',
|
|
||||||
'Automation': '#F59E0B', 'Security': '#EF4444', 'Monitoring': '#6366F1',
|
|
||||||
'Storage': '#8B5CF6', 'Obsidian': '#7C3AED', 'Tutorial': '#3B82F6',
|
|
||||||
'Uncategorized': '#A0AEC0'
|
|
||||||
};
|
};
|
||||||
uniqueCategories.forEach((category, index) => {
|
|
||||||
if (predefinedColors[category]) {
|
|
||||||
categoryColors[category] = predefinedColors[category];
|
|
||||||
} else {
|
|
||||||
const hue = (index * 137.5) % 360;
|
|
||||||
categoryColors[category] = `hsl(${hue}, 70%, 60%)`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate node sizes
|
// Calculate node sizes (Keep this logic)
|
||||||
const nodeSizes = {};
|
const nodeSizes = {};
|
||||||
const minSize = 20; const maxSize = 40;
|
const minSize = 20; const maxSize = 40;
|
||||||
const degreeMap = new Map();
|
const degreeMap = new Map();
|
||||||
|
@ -108,14 +95,9 @@ graphData.nodes.forEach(node => {
|
||||||
<a href="#" id="node-link" class="node-link" target="_blank" rel="noopener noreferrer">Read Article</a>
|
<a href="#" id="node-link" class="node-link" target="_blank" rel="noopener noreferrer">Read Article</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Graph Controls -->
|
<!-- Graph Controls (Simplified: Only Zoom/Reset) -->
|
||||||
<div class="graph-controls">
|
<div class="graph-controls">
|
||||||
<div class="graph-filters">
|
{/* Removed category filter buttons */}
|
||||||
<button class="graph-filter active" data-filter="all" style="--filter-color: var(--accent-primary);">All Topics</button>
|
|
||||||
{uniqueCategories.map(category => (
|
|
||||||
<button class="graph-filter" data-filter={category} style={`--filter-color: ${categoryColors[category]};`}>{category}</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div class="graph-actions">
|
<div class="graph-actions">
|
||||||
<button id="zoom-in" class="graph-action" aria-label="Zoom In">
|
<button id="zoom-in" class="graph-action" aria-label="Zoom In">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||||||
|
@ -129,21 +111,23 @@ graphData.nodes.forEach(node => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Graph Legend -->
|
<!-- Graph Legend (Simplified for node types) -->
|
||||||
<details class="graph-legend">
|
<details class="graph-legend">
|
||||||
<summary class="legend-title">Legend</summary>
|
<summary class="legend-title">Legend</summary>
|
||||||
<div class="legend-items">
|
<div class="legend-items">
|
||||||
{uniqueCategories.map(category => (
|
<div class="legend-item">
|
||||||
<div class="legend-item" data-category={category}>
|
<span class="legend-color" style={`background-color: ${nodeTypeColors.post};`}></span>
|
||||||
<span class="legend-color" style={`background-color: ${categoryColors[category]};`}></span>
|
<span class="legend-label">Post</span>
|
||||||
<span class="legend-label">{category}</span>
|
</div>
|
||||||
</div>
|
<div class="legend-item">
|
||||||
))}
|
<span class="legend-color" style={`background-color: ${nodeTypeColors.tag};`}></span>
|
||||||
|
<span class="legend-label">Tag</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script define:vars={{ graphData, categoryColors, nodeSizes }}>
|
<script define:vars={{ graphData, nodeTypeColors, nodeSizes }}> {/* Pass nodeTypeColors instead */}
|
||||||
// Initialize the graph when the DOM is ready
|
// Initialize the graph when the DOM is ready
|
||||||
function initializeGraph() {
|
function initializeGraph() {
|
||||||
// Check if Cytoscape is loaded
|
// Check if Cytoscape is loaded
|
||||||
|
@ -176,11 +160,10 @@ graphData.nodes.forEach(node => {
|
||||||
data: {
|
data: {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
label: node.label,
|
label: node.label,
|
||||||
category: node.category || 'Uncategorized',
|
type: node.type, // Pass the type ('post' or 'tag')
|
||||||
tags: node.tags || [],
|
size: nodeSizes[node.id] || (node.type === 'tag' ? 15 : 25), // Make tags slightly smaller?
|
||||||
size: nodeSizes[node.id] || 25,
|
color: nodeTypeColors[node.type] || '#A0AEC0', // Use type for color
|
||||||
color: categoryColors[node.category || 'Uncategorized'] || '#A0AEC0',
|
url: node.url || '#' // Pass URL if available (for posts)
|
||||||
url: `/posts/${node.id}/`
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -198,14 +181,23 @@ graphData.nodes.forEach(node => {
|
||||||
const cy = cytoscape({
|
const cy = cytoscape({
|
||||||
container: graphContainer,
|
container: graphContainer,
|
||||||
elements: elements,
|
elements: elements,
|
||||||
style: [
|
style: [
|
||||||
{ 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-opacity': 0.5 } },
|
// Base node style (common properties)
|
||||||
{ selector: 'edge', style: { 'width': 'mapData(weight, 1, 10, 1, 4)', 'line-color': 'rgba(226, 232, 240, 0.2)', 'curve-style': 'bezier', 'opacity': 0.6, 'z-index': 1 } },
|
{ selector: 'node', style: { '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-opacity': 0.5 } },
|
||||||
{ 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 } },
|
// Post node style
|
||||||
{ 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: 'node[type="post"]', style: { 'background-color': 'data(color)', 'shape': 'ellipse' } },
|
||||||
{ selector: '.faded', style: { 'opacity': 0.15, 'text-opacity': 0.3, 'background-opacity': 0.3, 'z-index': 1 } },
|
// Tag node style
|
||||||
{ 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: 'node[type="tag"]', style: { 'background-color': 'data(color)', 'shape': 'round-rectangle', 'font-size': '9px', 'text-margin-y': '5px' } }, // Slightly smaller font for tags
|
||||||
{ selector: 'edge:selected', style: { 'width': 'mapData(weight, 1, 10, 2, 6)', 'line-color': '#FFFFFF', 'opacity': 1, 'z-index': 30 } }
|
// Base edge style
|
||||||
|
{ selector: 'edge', style: { 'width': '1px', 'line-color': 'rgba(226, 232, 240, 0.2)', 'curve-style': 'bezier', 'opacity': 0.6, 'z-index': 1 } },
|
||||||
|
// Highlighted state (for nodes and edges)
|
||||||
|
{ selector: '.highlighted', style: { 'border-color': '#FFFFFF', 'border-width': '3px', 'color': '#FFFFFF', 'text-background-opacity': 0.9, 'opacity': 1, 'z-index': 20, 'line-color': 'rgba(255, 255, 255, 0.7)', 'width': '2px' } }, // Highlight edges too
|
||||||
|
// Faded state (for nodes and edges)
|
||||||
|
{ selector: '.faded', style: { 'opacity': 0.15, 'text-opacity': 0.3, 'background-opacity': 0.3, 'z-index': 1, 'line-color': 'rgba(226, 232, 240, 0.05)' } },
|
||||||
|
// Selected node state
|
||||||
|
{ selector: 'node:selected', style: { 'border-width': '4px', 'border-color': '#FFFFFF', 'border-opacity': 1, 'text-opacity': 1, 'color': '#FFFFFF', 'z-index': 30 } },
|
||||||
|
// Selected edge state (optional, can remove if not needed)
|
||||||
|
// { selector: 'edge:selected', style: { 'width': '3px', 'line-color': '#FFFFFF', 'opacity': 1, 'z-index': 30 } }
|
||||||
],
|
],
|
||||||
layout: { name: 'cose', idealEdgeLength: 100, nodeOverlap: 20, refresh: 20, fit: true, padding: 30, randomize: false, componentSpacing: 100, nodeRepulsion: 400000, edgeElasticity: 100, nestingFactor: 5, gravity: 80, numIter: 1000, initialTemp: 200, coolingFactor: 0.95, minTemp: 1.0 },
|
layout: { name: 'cose', idealEdgeLength: 100, nodeOverlap: 20, refresh: 20, fit: true, padding: 30, randomize: false, componentSpacing: 100, nodeRepulsion: 400000, edgeElasticity: 100, nestingFactor: 5, gravity: 80, numIter: 1000, initialTemp: 200, coolingFactor: 0.95, minTemp: 1.0 },
|
||||||
zoom: 1, minZoom: 0.1, maxZoom: 4, zoomingEnabled: true, userZoomingEnabled: true, panningEnabled: true, userPanningEnabled: true, boxSelectionEnabled: false,
|
zoom: 1, minZoom: 0.1, maxZoom: 4, zoomingEnabled: true, userZoomingEnabled: true, panningEnabled: true, userPanningEnabled: true, boxSelectionEnabled: false,
|
||||||
|
@ -241,64 +233,19 @@ graphData.nodes.forEach(node => {
|
||||||
const node = e.target;
|
const node = e.target;
|
||||||
const nodeData = node.data();
|
const nodeData = node.data();
|
||||||
|
|
||||||
if (nodeDetailsEl) {
|
// Simplified tap logic: Let index.astro handle filtering/scrolling
|
||||||
document.getElementById('node-title').textContent = nodeData.label;
|
// We can still highlight neighbors on tap if desired, or remove this section
|
||||||
const categoryEl = document.getElementById('node-category').querySelector('.category-value');
|
// For now, just log the tap
|
||||||
categoryEl.textContent = nodeData.category;
|
console.log(`Node tapped in KnowledgeGraph component: ${nodeData.id} (${nodeData.type})`);
|
||||||
// Use categoryColors map correctly
|
|
||||||
const catColor = categoryColors[nodeData.category] || 'var(--text-secondary)';
|
// Optional: Highlight tapped node and neighbors
|
||||||
categoryEl.style.backgroundColor = `${catColor}33`; // Add alpha
|
// cy.elements().removeClass('highlighted faded');
|
||||||
categoryEl.style.color = catColor;
|
// node.addClass('highlighted');
|
||||||
|
// node.neighborhood().addClass('highlighted');
|
||||||
const tagsContainer = document.getElementById('node-tags').querySelector('.tags-container');
|
// cy.elements().difference(node.neighborhood().union(node)).addClass('faded');
|
||||||
tagsContainer.innerHTML = '';
|
|
||||||
if (nodeData.tags && nodeData.tags.length > 0) {
|
// Remove node details panel logic
|
||||||
nodeData.tags.forEach(tag => {
|
// if (nodeDetailsEl) { ... } block removed
|
||||||
const tagEl = document.createElement('span');
|
|
||||||
tagEl.className = 'tag';
|
|
||||||
tagEl.textContent = tag;
|
|
||||||
tagsContainer.appendChild(tagEl);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
tagsContainer.innerHTML = '<span class="no-tags">No tags</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectionsList = document.getElementById('node-connections').querySelector('.connections-list');
|
|
||||||
connectionsList.innerHTML = '';
|
|
||||||
const connectedNodes = node.neighborhood('node');
|
|
||||||
if (connectedNodes.length > 0) {
|
|
||||||
connectedNodes.forEach(connectedNode => {
|
|
||||||
const connectedData = connectedNode.data();
|
|
||||||
const listItem = document.createElement('li');
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = '#';
|
|
||||||
link.textContent = connectedData.label;
|
|
||||||
link.dataset.id = connectedData.id;
|
|
||||||
link.addEventListener('click', (evt) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
listItem.appendChild(link);
|
|
||||||
connectionsList.appendChild(listItem);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
connectionsList.innerHTML = '<li>No connections</li>';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('node-link').href = nodeData.url;
|
|
||||||
nodeDetailsEl.classList.add('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.elements().removeClass('highlighted').removeClass('faded');
|
|
||||||
node.addClass('highlighted');
|
|
||||||
node.neighborhood().addClass('highlighted');
|
|
||||||
cy.elements().difference(node.neighborhood().union(node)).addClass('faded');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.on('tap', function(e) {
|
cy.on('tap', function(e) {
|
||||||
|
@ -316,34 +263,8 @@ graphData.nodes.forEach(node => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category filtering
|
// Removed category filtering logic
|
||||||
const filterButtons = document.querySelectorAll('.graph-filter');
|
// Removed legend interaction logic
|
||||||
filterButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
filterButtons.forEach(btn => btn.classList.remove('active'));
|
|
||||||
button.classList.add('active');
|
|
||||||
const category = button.dataset.filter;
|
|
||||||
|
|
||||||
if (category === 'all') {
|
|
||||||
cy.elements().removeClass('faded highlighted filtered');
|
|
||||||
} else {
|
|
||||||
cy.elements().addClass('faded').removeClass('highlighted filtered');
|
|
||||||
const selectedNodes = cy.nodes().filter(node => node.data('category') === category);
|
|
||||||
selectedNodes.removeClass('faded').addClass('filtered');
|
|
||||||
selectedNodes.connectedEdges().removeClass('faded');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Legend item interactions
|
|
||||||
const legendItems = document.querySelectorAll('.legend-item');
|
|
||||||
legendItems.forEach(item => {
|
|
||||||
item.addEventListener('click', () => {
|
|
||||||
const category = item.dataset.category;
|
|
||||||
const filterButton = document.querySelector(`.graph-filter[data-filter="${category}"]`);
|
|
||||||
if (filterButton) filterButton.click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Zoom controls
|
// Zoom controls
|
||||||
document.getElementById('zoom-in')?.addEventListener('click', () => cy.zoom(cy.zoom() * 1.2));
|
document.getElementById('zoom-in')?.addEventListener('click', () => cy.zoom(cy.zoom() * 1.2));
|
||||||
|
|
|
@ -36,38 +36,44 @@ const postsData = sortedPosts.map(post => ({
|
||||||
isDraft: post.data.draft || false
|
isDraft: post.data.draft || false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Prepare graph data for visualization
|
// Prepare graph data (Obsidian-style: Posts and Tags)
|
||||||
const graphData = {
|
const graphNodes = [];
|
||||||
nodes: sortedPosts
|
const graphEdges = [];
|
||||||
.filter(post => !post.data.draft)
|
const tagNodes = new Map(); // To avoid duplicate tag nodes
|
||||||
.map(post => ({
|
|
||||||
|
// Add post nodes
|
||||||
|
sortedPosts.forEach(post => {
|
||||||
|
if (!post.data.draft) { // Exclude drafts from graph
|
||||||
|
graphNodes.push({
|
||||||
id: post.slug,
|
id: post.slug,
|
||||||
label: post.data.title,
|
label: post.data.title,
|
||||||
category: post.data.category || 'Uncategorized',
|
type: 'post', // Add type for styling/interaction
|
||||||
tags: post.data.tags || []
|
url: `/posts/${post.slug}/` // Add URL for linking
|
||||||
})),
|
});
|
||||||
edges: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create edges between posts based on shared tags
|
// Add tag nodes and edges
|
||||||
for (let i = 0; i < graphData.nodes.length; i++) {
|
(post.data.tags || []).forEach(tag => {
|
||||||
const postA = graphData.nodes[i];
|
const tagId = `tag-${tag}`;
|
||||||
|
// Add tag node only if it doesn't exist
|
||||||
for (let j = i + 1; j < graphData.nodes.length; j++) {
|
if (!tagNodes.has(tagId)) {
|
||||||
const postB = graphData.nodes[j];
|
graphNodes.push({
|
||||||
|
id: tagId,
|
||||||
// Create edge if posts share at least one tag or same category
|
label: `#${tag}`, // Prefix with # for clarity
|
||||||
const sharedTags = postA.tags.filter(tag => postB.tags.includes(tag));
|
type: 'tag' // Add type
|
||||||
|
});
|
||||||
if (sharedTags.length > 0 || postA.category === postB.category) {
|
tagNodes.set(tagId, true);
|
||||||
graphData.edges.push({
|
}
|
||||||
source: postA.id,
|
// Add edge connecting post to tag
|
||||||
target: postB.id,
|
graphEdges.push({
|
||||||
strength: sharedTags.length + (postA.category === postB.category ? 1 : 0)
|
source: post.slug,
|
||||||
|
target: tagId,
|
||||||
|
type: 'tag-connection' // Add type
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
const graphData = { nodes: graphNodes, edges: graphEdges };
|
||||||
|
|
||||||
// Terminal commands for tech effect
|
// Terminal commands for tech effect
|
||||||
const commands = [
|
const commands = [
|
||||||
|
@ -115,29 +121,6 @@ const commands = [
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Knowledge Graph Visualization */}
|
|
||||||
<section class="graph-section">
|
|
||||||
<div class="container">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2 class="section-title">Knowledge Graph</h2>
|
|
||||||
<p class="section-description">
|
|
||||||
Explore connections between articles based on topics and categories
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="graph-container">
|
|
||||||
<KnowledgeGraph graphData={graphData} />
|
|
||||||
|
|
||||||
<div class="graph-controls">
|
|
||||||
<button class="graph-filter active" data-filter="all">All Topics</button>
|
|
||||||
{allCategories.map(category => (
|
|
||||||
<button class="graph-filter" data-filter={category}>{category}</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Blog Posts Section */}
|
{/* Blog Posts Section */}
|
||||||
<section class="blog-posts-section">
|
<section class="blog-posts-section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@ -159,9 +142,15 @@ const commands = [
|
||||||
<button class="tag-filter-btn" data-tag={tag}>{tag}</button>
|
<button class="tag-filter-btn" data-tag={tag}>{tag}</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Integrated Knowledge Graph */}
|
||||||
|
<div class="integrated-graph-container">
|
||||||
|
<KnowledgeGraph graphData={graphData} />
|
||||||
|
{/* We will update graphData generation later */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Blog Grid (will be populated by JS) -->
|
{/* Blog Grid (will be populated by JS) */}
|
||||||
<div class="blog-grid" id="blog-grid">
|
<div class="blog-grid" id="blog-grid">
|
||||||
<div class="loading-indicator">
|
<div class="loading-indicator">
|
||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner"></div>
|
||||||
|
@ -179,12 +168,12 @@ const commands = [
|
||||||
const searchInput = document.getElementById('search-input');
|
const searchInput = document.getElementById('search-input');
|
||||||
const tagButtons = document.querySelectorAll('.tag-filter-btn');
|
const tagButtons = document.querySelectorAll('.tag-filter-btn');
|
||||||
const blogGrid = document.getElementById('blog-grid');
|
const blogGrid = document.getElementById('blog-grid');
|
||||||
const graphFilters = document.querySelectorAll('.graph-filter');
|
// Removed graphFilters as category filtering is removed from graph
|
||||||
|
|
||||||
// State variables
|
// State variables
|
||||||
let currentFilterTag = 'all';
|
let currentFilterTag = 'all';
|
||||||
let currentSearchTerm = '';
|
let currentSearchTerm = '';
|
||||||
let currentGraphFilter = 'all';
|
// Removed currentGraphFilter
|
||||||
let cy; // Cytoscape instance will be set by KnowledgeGraph component
|
let cy; // Cytoscape instance will be set by KnowledgeGraph component
|
||||||
|
|
||||||
// Wait for cytoscape instance to be available
|
// Wait for cytoscape instance to be available
|
||||||
|
@ -193,63 +182,70 @@ const commands = [
|
||||||
setupGraphInteractions();
|
setupGraphInteractions();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup graph filtering and interactions
|
// Setup graph interactions (Post and Tag nodes)
|
||||||
function setupGraphInteractions() {
|
function setupGraphInteractions() {
|
||||||
if (!cy) return;
|
if (!cy) {
|
||||||
|
console.error("Cytoscape instance not ready.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Graph filtering by category
|
// Remove previous category filter logic if any existed
|
||||||
graphFilters.forEach(button => {
|
// graphFilters.forEach(...) logic removed
|
||||||
button.addEventListener('click', () => {
|
|
||||||
// Update active button style
|
// Handle clicks on graph nodes
|
||||||
graphFilters.forEach(btn => btn.classList.remove('active'));
|
|
||||||
button.classList.add('active');
|
|
||||||
|
|
||||||
// Update filter
|
|
||||||
currentGraphFilter = button.dataset.filter;
|
|
||||||
|
|
||||||
// Apply filter to graph
|
|
||||||
if (currentGraphFilter === 'all') {
|
|
||||||
cy.elements().removeClass('faded').removeClass('highlighted');
|
|
||||||
} else {
|
|
||||||
// Fade all nodes/edges
|
|
||||||
cy.elements().addClass('faded');
|
|
||||||
|
|
||||||
// Highlight nodes with matching category and their edges
|
|
||||||
const matchingNodes = cy.nodes().filter(node =>
|
|
||||||
node.data('category') === currentGraphFilter
|
|
||||||
);
|
|
||||||
|
|
||||||
matchingNodes.removeClass('faded').addClass('highlighted');
|
|
||||||
matchingNodes.connectedEdges().removeClass('faded').addClass('highlighted');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click node to filter posts
|
|
||||||
cy.on('tap', 'node', function(evt) {
|
cy.on('tap', 'node', function(evt) {
|
||||||
const node = evt.target;
|
const node = evt.target;
|
||||||
const slug = node.id();
|
const nodeId = node.id();
|
||||||
|
const nodeType = node.data('type'); // Get type ('post' or 'tag')
|
||||||
// Scroll to the post in the blog grid
|
|
||||||
const post = postsData.find(p => p.slug === slug);
|
console.log(`Node clicked: ID=${nodeId}, Type=${nodeType}`); // Debug log
|
||||||
if (post) {
|
|
||||||
// Reset filters
|
if (nodeType === 'post') {
|
||||||
currentFilterTag = 'all';
|
// Handle post node click: Find post, update search, filter grid, scroll
|
||||||
searchInput.value = post.title;
|
const post = postsData.find(p => p.slug === nodeId);
|
||||||
currentSearchTerm = post.title;
|
if (post) {
|
||||||
|
console.log(`Post node clicked: ${post.title}`);
|
||||||
|
// Reset tag filter to 'all' when a specific post is selected via graph
|
||||||
|
currentFilterTag = 'all';
|
||||||
|
tagButtons.forEach(btn => btn.classList.remove('active'));
|
||||||
|
const allButton = document.querySelector('.tag-filter-btn[data-tag="all"]');
|
||||||
|
if (allButton) allButton.classList.add('active');
|
||||||
|
|
||||||
|
// Update search bar and term
|
||||||
|
searchInput.value = post.title; // Show post title in search
|
||||||
|
currentSearchTerm = post.title; // Filter grid by title
|
||||||
|
|
||||||
|
// Update grid to show only this post (or matching search term)
|
||||||
|
updateGrid();
|
||||||
|
|
||||||
|
// Scroll to the blog section smoothly
|
||||||
|
const blogSection = document.querySelector('.blog-posts-section');
|
||||||
|
if (blogSection) {
|
||||||
|
blogSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Post data not found for slug: ${nodeId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (nodeType === 'tag') {
|
||||||
|
// Handle tag node click: Simulate click on corresponding tag filter button
|
||||||
|
const tagName = nodeId.replace(/^tag-/, ''); // Extract tag name (remove 'tag-' prefix)
|
||||||
|
console.log(`Tag node clicked: ${tagName}`);
|
||||||
|
|
||||||
// Update UI
|
const correspondingButton = document.querySelector(`.tag-filter-btn[data-tag="${tagName}"]`);
|
||||||
tagButtons.forEach(btn => btn.classList.remove('active'));
|
|
||||||
tagButtons[0].classList.add('active');
|
|
||||||
|
|
||||||
// Update grid with just this post
|
if (correspondingButton) {
|
||||||
updateGrid();
|
console.log(`Found corresponding button for tag: ${tagName}`);
|
||||||
|
// Simulate click on the button
|
||||||
// Scroll to blog section
|
correspondingButton.click();
|
||||||
document.querySelector('.blog-posts-section').scrollIntoView({
|
// Scroll to blog section smoothly
|
||||||
behavior: 'smooth',
|
const blogSection = document.querySelector('.blog-posts-section');
|
||||||
block: 'start'
|
if (blogSection) {
|
||||||
});
|
blogSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Could not find tag filter button for tag: ${tagName}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -326,26 +322,43 @@ const commands = [
|
||||||
if (filteredPosts.length > 0) {
|
if (filteredPosts.length > 0) {
|
||||||
blogGrid.innerHTML = filteredPosts.map(createPostCardHTML).join('');
|
blogGrid.innerHTML = filteredPosts.map(createPostCardHTML).join('');
|
||||||
|
|
||||||
// If graph is available, highlight matching nodes
|
// If graph is available, highlight post nodes shown in the grid
|
||||||
if (cy) {
|
if (cy) {
|
||||||
const matchingSlugs = filteredPosts.map(post => post.slug);
|
const matchingPostSlugs = filteredPosts.map(post => post.slug);
|
||||||
|
|
||||||
// Reset all nodes
|
// Reset styles on all nodes first
|
||||||
cy.nodes().removeClass('highlighted').removeClass('filtered');
|
cy.nodes().removeClass('highlighted').removeClass('faded');
|
||||||
|
|
||||||
// Highlight matching nodes
|
// Highlight post nodes that are currently visible in the grid
|
||||||
matchingSlugs.forEach(slug => {
|
cy.nodes('[type="post"]').forEach(node => {
|
||||||
cy.getElementById(slug).addClass('highlighted');
|
if (matchingPostSlugs.includes(node.id())) {
|
||||||
|
node.removeClass('faded').addClass('highlighted');
|
||||||
|
} else {
|
||||||
|
node.removeClass('highlighted').addClass('faded'); // Fade non-matching posts
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// If filtering by tag, also highlight connected nodes
|
// Highlight tag nodes connected to visible posts OR the currently selected tag
|
||||||
if (currentFilterTag !== 'all') {
|
cy.nodes('[type="tag"]').forEach(tagNode => {
|
||||||
cy.nodes().forEach(node => {
|
const tagName = tagNode.id().replace(/^tag-/, '');
|
||||||
if (node.data('tags')?.includes(currentFilterTag)) {
|
const isSelectedTag = tagName === currentFilterTag;
|
||||||
node.addClass('filtered');
|
const isConnectedToVisiblePost = tagNode.connectedEdges().sources().some(postNode => matchingPostSlugs.includes(postNode.id()));
|
||||||
|
|
||||||
|
if (isSelectedTag || (currentFilterTag === 'all' && isConnectedToVisiblePost)) {
|
||||||
|
tagNode.removeClass('faded').addClass('highlighted');
|
||||||
|
} else {
|
||||||
|
tagNode.removeClass('highlighted').addClass('faded');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Adjust edge visibility based on connected highlighted nodes
|
||||||
|
cy.edges().forEach(edge => {
|
||||||
|
if (edge.source().hasClass('highlighted') && edge.target().hasClass('highlighted')) {
|
||||||
|
edge.removeClass('faded').addClass('highlighted');
|
||||||
|
} else {
|
||||||
|
edge.removeClass('highlighted').addClass('faded');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blogGrid.innerHTML = '<p class="no-results">No posts found matching your criteria.</p>';
|
blogGrid.innerHTML = '<p class="no-results">No posts found matching your criteria.</p>';
|
||||||
|
@ -603,6 +616,17 @@ const commands = [
|
||||||
border-color: var(--accent-primary);
|
border-color: var(--accent-primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Styles for the integrated graph container */
|
||||||
|
.integrated-graph-container {
|
||||||
|
margin-top: 2rem; /* Add space above the graph */
|
||||||
|
height: 400px; /* Adjust height as needed */
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(15, 23, 42, 0.3); /* Slightly different background */
|
||||||
|
position: relative; /* Needed for Cytoscape */
|
||||||
|
overflow: hidden; /* Hide scrollbars if graph overflows */
|
||||||
|
}
|
||||||
|
|
||||||
.blog-grid {
|
.blog-grid {
|
||||||
margin: 2rem 0 4rem;
|
margin: 2rem 0 4rem;
|
||||||
|
|
Loading…
Reference in New Issue