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