Compare commits
	
		
			No commits in common. "bbd2612082c462aee3c1a0d7798e98be5a7189e9" and "6ce069a3ebde61c99504d498fd2f03c4cde356cb" have entirely different histories.
		
	
	
		
			bbd2612082
			...
			6ce069a3eb
		
	
		| 
						 | 
					@ -2,6 +2,9 @@
 | 
				
			||||||
// src/components/KnowledgeGraph.astro
 | 
					// src/components/KnowledgeGraph.astro
 | 
				
			||||||
// Interactive visualization of content connections using Cytoscape.js
 | 
					// Interactive visualization of content connections using Cytoscape.js
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Assuming Cytoscape is loaded via CDN in BaseLayout or globally
 | 
				
			||||||
 | 
					// If not, you might need: import cytoscape from 'cytoscape';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface GraphNode {
 | 
					export interface GraphNode {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
  label: string;
 | 
					  label: string;
 | 
				
			||||||
| 
						 | 
					@ -31,7 +34,7 @@ const { graphData, height = "60vh" } = Astro.props;
 | 
				
			||||||
// Generate colors based on categories for nodes
 | 
					// Generate colors based on categories for nodes
 | 
				
			||||||
const uniqueCategories = [...new Set(graphData.nodes.map(node => node.category || 'Uncategorized'))];
 | 
					const uniqueCategories = [...new Set(graphData.nodes.map(node => node.category || 'Uncategorized'))];
 | 
				
			||||||
const categoryColors = {};
 | 
					const categoryColors = {};
 | 
				
			||||||
const predefinedColors = { 
 | 
					const predefinedColors = { /* Colors from previous step */
 | 
				
			||||||
  'Kubernetes': '#326CE5', 'Docker': '#2496ED', 'DevOps': '#FF6F61',
 | 
					  'Kubernetes': '#326CE5', 'Docker': '#2496ED', 'DevOps': '#FF6F61',
 | 
				
			||||||
  'Homelab': '#06B6D4', 'Networking': '#9333EA', 'Infrastructure': '#10B981',
 | 
					  'Homelab': '#06B6D4', 'Networking': '#9333EA', 'Infrastructure': '#10B981',
 | 
				
			||||||
  'Automation': '#F59E0B', 'Security': '#EF4444', 'Monitoring': '#6366F1',
 | 
					  'Automation': '#F59E0B', 'Security': '#EF4444', 'Monitoring': '#6366F1',
 | 
				
			||||||
| 
						 | 
					@ -64,11 +67,8 @@ graphData.nodes.forEach(node => {
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<!-- Include Cytoscape via CDN with is:inline to ensure it loads before the script runs -->
 | 
					 | 
				
			||||||
<script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js" is:inline></script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div class="graph-wrapper" style={`--graph-height: ${height};`}>
 | 
					<div class="graph-wrapper" style={`--graph-height: ${height};`}>
 | 
				
			||||||
  <!-- Loading Animation -->
 | 
					  {/* Loading Animation */}
 | 
				
			||||||
  <div id="graph-loading" class="graph-loading">
 | 
					  <div id="graph-loading" class="graph-loading">
 | 
				
			||||||
    <div class="loading-spinner">
 | 
					    <div class="loading-spinner">
 | 
				
			||||||
      <div class="spinner-ring"></div>
 | 
					      <div class="spinner-ring"></div>
 | 
				
			||||||
| 
						 | 
					@ -78,10 +78,10 @@ graphData.nodes.forEach(node => {
 | 
				
			||||||
    <div class="loading-text">Initializing Knowledge Graph...</div>
 | 
					    <div class="loading-text">Initializing Knowledge Graph...</div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <!-- Cytoscape Container -->
 | 
					  {/* Cytoscape Container */}
 | 
				
			||||||
  <div id="knowledge-graph" class="graph-container"></div>
 | 
					  <div id="knowledge-graph" class="graph-container"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <!-- Node Details Panel -->
 | 
					  {/* Node Details Panel */}
 | 
				
			||||||
  <div id="node-details" class="node-details">
 | 
					  <div id="node-details" class="node-details">
 | 
				
			||||||
    <div class="node-details-header">
 | 
					    <div class="node-details-header">
 | 
				
			||||||
      <h3 id="node-title" class="node-title">Node Title</h3>
 | 
					      <h3 id="node-title" class="node-title">Node Title</h3>
 | 
				
			||||||
| 
						 | 
					@ -96,19 +96,19 @@ graphData.nodes.forEach(node => {
 | 
				
			||||||
    <div id="node-tags" class="node-tags">
 | 
					    <div id="node-tags" class="node-tags">
 | 
				
			||||||
      <span class="tags-label">Tags:</span>
 | 
					      <span class="tags-label">Tags:</span>
 | 
				
			||||||
      <div class="tags-container">
 | 
					      <div class="tags-container">
 | 
				
			||||||
        <!-- Tags populated by JS -->
 | 
					        {/* Tags populated by JS */}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div id="node-connections" class="node-connections">
 | 
					    <div id="node-connections" class="node-connections">
 | 
				
			||||||
      <span class="connections-label">Connections:</span>
 | 
					      <span class="connections-label">Connections:</span>
 | 
				
			||||||
      <ul class="connections-list">
 | 
					      <ul class="connections-list">
 | 
				
			||||||
        <!-- Connections populated by JS -->
 | 
					        {/* Connections populated by JS */}
 | 
				
			||||||
      </ul>
 | 
					      </ul>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <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 */}
 | 
				
			||||||
  <div class="graph-controls">
 | 
					  <div class="graph-controls">
 | 
				
			||||||
     <div class="graph-filters">
 | 
					     <div class="graph-filters">
 | 
				
			||||||
       <button class="graph-filter active" data-filter="all" style="--filter-color: var(--accent-primary);">All Topics</button>
 | 
					       <button class="graph-filter active" data-filter="all" style="--filter-color: var(--accent-primary);">All Topics</button>
 | 
				
			||||||
| 
						 | 
					@ -129,7 +129,7 @@ graphData.nodes.forEach(node => {
 | 
				
			||||||
     </div>
 | 
					     </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <!-- Graph Legend -->
 | 
					  {/* Graph Legend */}
 | 
				
			||||||
  <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">
 | 
				
			||||||
| 
						 | 
					@ -141,14 +141,18 @@ graphData.nodes.forEach(node => {
 | 
				
			||||||
      ))}
 | 
					      ))}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </details>
 | 
					  </details>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{/* Include Cytoscape via CDN - Ensure this is loaded, perhaps in BaseLayout */}
 | 
				
			||||||
 | 
					{/* <script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js"></script> */}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script define:vars={{ graphData, categoryColors, nodeSizes }}>
 | 
					<script define:vars={{ graphData, categoryColors, nodeSizes }}>
 | 
				
			||||||
  // 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
 | 
				
			||||||
    if (typeof cytoscape === 'undefined') {
 | 
					    if (typeof cytoscape === 'undefined') {
 | 
				
			||||||
      console.error("Cytoscape library not loaded. Make sure it's included via the script tag.");
 | 
					      console.error("Cytoscape library not loaded. Make sure it's included (e.g., via CDN in BaseLayout).");
 | 
				
			||||||
      const loadingEl = document.getElementById('graph-loading');
 | 
					      const loadingEl = document.getElementById('graph-loading');
 | 
				
			||||||
      if(loadingEl) loadingEl.innerHTML = "<p>Error: Cytoscape library not loaded.</p>";
 | 
					      if(loadingEl) loadingEl.innerHTML = "<p>Error: Cytoscape library not loaded.</p>";
 | 
				
			||||||
      return; 
 | 
					      return; 
 | 
				
			||||||
| 
						 | 
					@ -198,7 +202,7 @@ graphData.nodes.forEach(node => {
 | 
				
			||||||
    const cy = cytoscape({
 | 
					    const cy = cytoscape({
 | 
				
			||||||
      container: graphContainer,
 | 
					      container: graphContainer,
 | 
				
			||||||
      elements: elements,
 | 
					      elements: elements,
 | 
				
			||||||
      style: [ 
 | 
					      style: [ /* Styles from your snippet */
 | 
				
			||||||
         { 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 } },
 | 
					         { 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 } },
 | 
				
			||||||
         { 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: '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: '.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: '.highlighted', style: { 'background-color': 'data(color)', 'border-color': '#FFFFFF', 'border-width': '3px', 'color': '#FFFFFF', 'text-background-opacity': 0.9, 'opacity': 1, 'z-index': 20 } },
 | 
				
			||||||
| 
						 | 
					@ -223,7 +227,7 @@ graphData.nodes.forEach(node => {
 | 
				
			||||||
        clearTimeout(hoverTimeout); 
 | 
					        clearTimeout(hoverTimeout); 
 | 
				
			||||||
        node.addClass('highlighted');
 | 
					        node.addClass('highlighted');
 | 
				
			||||||
        node.connectedEdges().addClass('highlighted');
 | 
					        node.connectedEdges().addClass('highlighted');
 | 
				
			||||||
        graphContainer.style.cursor = 'pointer'; 
 | 
					        graphContainer.style.cursor = 'pointer'; // Use graphContainer
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    cy.on('mouseout', 'node', function(e) {
 | 
					    cy.on('mouseout', 'node', function(e) {
 | 
				
			||||||
| 
						 | 
					@ -234,7 +238,7 @@ graphData.nodes.forEach(node => {
 | 
				
			||||||
                node.connectedEdges().removeClass('highlighted');
 | 
					                node.connectedEdges().removeClass('highlighted');
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }, 100);
 | 
					        }, 100);
 | 
				
			||||||
        graphContainer.style.cursor = 'default'; 
 | 
					         graphContainer.style.cursor = 'default'; // Use graphContainer
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    cy.on('tap', 'node', function(e) {
 | 
					    cy.on('tap', 'node', function(e) {
 | 
				
			||||||
| 
						 | 
					@ -278,7 +282,7 @@ graphData.nodes.forEach(node => {
 | 
				
			||||||
                evt.preventDefault();
 | 
					                evt.preventDefault();
 | 
				
			||||||
                cy.$(':selected').unselect(); 
 | 
					                cy.$(':selected').unselect(); 
 | 
				
			||||||
                const targetNode = cy.getElementById(connectedData.id);
 | 
					                const targetNode = cy.getElementById(connectedData.id);
 | 
				
			||||||
                if (targetNode) {
 | 
					                if (targetNode) { // Check if node exists
 | 
				
			||||||
                    targetNode.select(); 
 | 
					                    targetNode.select(); 
 | 
				
			||||||
                    cy.animate({ center: { eles: targetNode }, zoom: cy.zoom() }, { duration: 300 }); 
 | 
					                    cy.animate({ center: { eles: targetNode }, zoom: cy.zoom() }, { duration: 300 }); 
 | 
				
			||||||
                    targetNode.trigger('tap'); 
 | 
					                    targetNode.trigger('tap'); 
 | 
				
			||||||
| 
						 | 
					@ -341,7 +345,7 @@ graphData.nodes.forEach(node => {
 | 
				
			||||||
      item.addEventListener('click', () => {
 | 
					      item.addEventListener('click', () => {
 | 
				
			||||||
        const category = item.dataset.category;
 | 
					        const category = item.dataset.category;
 | 
				
			||||||
        const filterButton = document.querySelector(`.graph-filter[data-filter="${category}"]`);
 | 
					        const filterButton = document.querySelector(`.graph-filter[data-filter="${category}"]`);
 | 
				
			||||||
        if (filterButton) filterButton.click(); 
 | 
					        filterButton?.click(); 
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -351,11 +355,10 @@ graphData.nodes.forEach(node => {
 | 
				
			||||||
    document.getElementById('reset-graph')?.addEventListener('click', () => {
 | 
					    document.getElementById('reset-graph')?.addEventListener('click', () => {
 | 
				
			||||||
        cy.fit(null, 30); 
 | 
					        cy.fit(null, 30); 
 | 
				
			||||||
        cy.elements().removeClass('faded highlighted filtered');
 | 
					        cy.elements().removeClass('faded highlighted filtered');
 | 
				
			||||||
        const allFilterButton = document.querySelector('.graph-filter[data-filter="all"]');
 | 
					        document.querySelector('.graph-filter[data-filter="all"]')?.click(); 
 | 
				
			||||||
        if (allFilterButton) allFilterButton.click(); 
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Add mouse wheel zoom controls
 | 
					    // Add mouse wheel zoom controls (already present in original script)
 | 
				
			||||||
    cy.on('zoom', function() {
 | 
					    cy.on('zoom', function() {
 | 
				
			||||||
      if (cy.zoom() > 1.5) {
 | 
					      if (cy.zoom() > 1.5) {
 | 
				
			||||||
        cy.style().selector('node').style({ 'text-max-width': '150px', 'font-size': '12px' }).update();
 | 
					        cy.style().selector('node').style({ 'text-max-width': '150px', 'font-size': '12px' }).update();
 | 
				
			||||||
| 
						 | 
					@ -433,3 +436,4 @@ graphData.nodes.forEach(node => {
 | 
				
			||||||
    .graph-legend[open] { transform: translateX(0); } 
 | 
					    .graph-legend[open] { transform: translateX(0); } 
 | 
				
			||||||
    .graph-controls { bottom: 10px; }
 | 
					    .graph-controls { bottom: 10px; }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,7 @@ const {
 | 
				
			||||||
    <!-- OpenGraph/Social Media Meta Tags -->
 | 
					    <!-- OpenGraph/Social Media Meta Tags -->
 | 
				
			||||||
    <meta property="og:title" content={title} />
 | 
					    <meta property="og:title" content={title} />
 | 
				
			||||||
    <meta property="og:description" content={description} />
 | 
					    <meta property="og:description" content={description} />
 | 
				
			||||||
    <meta property="og:image" content={Astro.site ? new URL(image, Astro.site).href : image} /> <!-- Use absolute URL -->
 | 
					    <meta property="og:image" content={Astro.site ? new URL(image, Astro.site).href : image} /> {/* Use absolute URL */}
 | 
				
			||||||
    <meta property="og:url" content={Astro.url} />
 | 
					    <meta property="og:url" content={Astro.url} />
 | 
				
			||||||
    <meta property="og:type" content="website" />
 | 
					    <meta property="og:type" content="website" />
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,7 @@ const {
 | 
				
			||||||
    <meta name="twitter:card" content="summary_large_image">
 | 
					    <meta name="twitter:card" content="summary_large_image">
 | 
				
			||||||
    <meta name="twitter:title" content={title}>
 | 
					    <meta name="twitter:title" content={title}>
 | 
				
			||||||
    <meta name="twitter:description" content={description}>
 | 
					    <meta name="twitter:description" content={description}>
 | 
				
			||||||
    <meta name="twitter:image" content={Astro.site ? new URL(image, Astro.site).href : image}> <!-- Use absolute URL -->
 | 
					    <meta name="twitter:image" content={Astro.site ? new URL(image, Astro.site).href : image}> {/* Use absolute URL */}
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    <!-- Fonts -->
 | 
					    <!-- Fonts -->
 | 
				
			||||||
    <link rel="preconnect" href="https://fonts.googleapis.com">
 | 
					    <link rel="preconnect" href="https://fonts.googleapis.com">
 | 
				
			||||||
| 
						 | 
					@ -44,9 +44,6 @@ const {
 | 
				
			||||||
    <!-- Favicon -->
 | 
					    <!-- Favicon -->
 | 
				
			||||||
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
 | 
					    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    <!-- Cytoscape Library for Knowledge Graph -->
 | 
					 | 
				
			||||||
    <script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js" is:inline></script>
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    <!-- Schema.org markup for Google -->
 | 
					    <!-- Schema.org markup for Google -->
 | 
				
			||||||
    <script type="application/ld+json">
 | 
					    <script type="application/ld+json">
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
| 
						 | 
					@ -298,6 +295,10 @@ const {
 | 
				
			||||||
        border-top: 1px solid var(--border-primary);
 | 
					        border-top: 1px solid var(--border-primary);
 | 
				
			||||||
        margin-top: 4rem; /* Add space above footer */
 | 
					        margin-top: 4rem; /* Add space above footer */
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      /* Add other global styles from your external file if needed */
 | 
				
			||||||
 | 
					      /* Or remove conflicting styles from the external file */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    </style>
 | 
					    </style>
 | 
				
			||||||
  </head>
 | 
					  </head>
 | 
				
			||||||
  <body>
 | 
					  <body>
 | 
				
			||||||
| 
						 | 
					@ -311,10 +312,11 @@ const {
 | 
				
			||||||
      <div class="floating-shape shape-3"></div>
 | 
					      <div class="floating-shape shape-3"></div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    {/* Use slots for Header and Footer */}
 | 
				
			||||||
    <slot name="header" />
 | 
					    <slot name="header" />
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    <main>
 | 
					    <main>
 | 
				
			||||||
      <slot /> <!-- Default slot for page content -->
 | 
					      <slot /> {/* Default slot for page content */}
 | 
				
			||||||
    </main>
 | 
					    </main>
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    <slot name="footer" />
 | 
					    <slot name="footer" />
 | 
				
			||||||
| 
						 | 
					@ -343,6 +345,10 @@ const {
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            console.warn("Element with class 'neural-nodes' not found.");
 | 
					            console.warn("Element with class 'neural-nodes' not found.");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Terminal typing effect (if needed globally, otherwise keep in component)
 | 
				
			||||||
 | 
					        // const typingElements = document.querySelectorAll('.terminal-typing');
 | 
				
			||||||
 | 
					        // typingElements.forEach(typingElement => { ... });
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    </script>
 | 
					    </script>
 | 
				
			||||||
  </body>
 | 
					  </body>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue