Fixed Source viewer issue with edge nodes

This commit is contained in:
abhigyantrumio 2025-08-04 03:38:03 +05:30
parent 614abfc1d2
commit d92b1370d3
4 changed files with 1322 additions and 1885 deletions

View file

@ -0,0 +1,199 @@
import { GitHubService } from './github.ts';
import { ZipService } from './zip.ts';
import { getIngestionWorker, type IngestionProgress } from '../lib/workerUtils.ts';
import type { KnowledgeGraph } from '../core/graph/types.ts';
export interface IngestionOptions {
directoryFilter?: string;
fileExtensions?: string;
onProgress?: (message: string) => void;
}
export interface IngestionResult {
graph: KnowledgeGraph;
fileContents: Map<string, string>;
}
export class IngestionService {
private githubService: GitHubService;
private zipService: ZipService;
constructor(githubToken?: string) {
this.githubService = new GitHubService(githubToken);
this.zipService = new ZipService();
}
async processGitHubRepo(
githubUrl: string,
options: IngestionOptions = {}
): Promise<IngestionResult> {
const { directoryFilter, fileExtensions, onProgress } = options;
// Parse GitHub URL
const match = githubUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)(?:\/.*)?$/);
if (!match) {
throw new Error('Invalid GitHub repository URL');
}
const [, owner, repo] = match;
onProgress?.('Fetching repository structure...');
// Get all files from the repository
const allFiles = await this.githubService.getAllFilesRecursively(owner, repo);
// Filter files based on options
const filteredFiles = this.filterFiles(allFiles, directoryFilter, fileExtensions);
onProgress?.(`Found ${filteredFiles.length} files. Downloading content...`);
// Download file contents
const fileContents = new Map<string, string>();
let processedFiles = 0;
for (const file of filteredFiles) {
try {
const content = await this.githubService.getFileContent(owner, repo, file.path);
if (content && content.length <= 1000000) { // Skip files larger than 1MB
fileContents.set(file.path, content);
}
processedFiles++;
if (processedFiles % 10 === 0) {
onProgress?.(`Downloaded ${processedFiles}/${filteredFiles.length} files...`);
}
} catch (error) {
console.warn(`Failed to download ${file.path}:`, error);
}
}
onProgress?.('Processing files with knowledge graph engine...');
// Process with ingestion worker
const graph = await this.processWithWorker(
fileContents,
`${owner}/${repo}`,
Array.from(fileContents.keys()),
onProgress
);
return { graph, fileContents };
}
async processZipFile(
file: File,
options: IngestionOptions = {}
): Promise<IngestionResult> {
const { directoryFilter, fileExtensions, onProgress } = options;
onProgress?.('Extracting ZIP file...');
// Extract ZIP contents
const allFileContents = await this.zipService.extractTextFiles(file);
// Filter files based on options
const filteredFileContents = new Map<string, string>();
const allPaths = Array.from(allFileContents.keys());
const filteredPaths = this.filterPaths(allPaths, directoryFilter, fileExtensions);
filteredPaths.forEach(path => {
const content = allFileContents.get(path);
if (content) {
filteredFileContents.set(path, content);
}
});
onProgress?.(`Extracted ${filteredFileContents.size} files. Processing...`);
// Process with ingestion worker
const projectName = file.name.replace(/\.zip$/i, '');
const graph = await this.processWithWorker(
filteredFileContents,
projectName,
Array.from(filteredFileContents.keys()),
onProgress
);
return { graph, fileContents: filteredFileContents };
}
private async processWithWorker(
fileContents: Map<string, string>,
projectName: string,
filePaths: string[],
onProgress?: (message: string) => void
): Promise<KnowledgeGraph> {
const worker = getIngestionWorker();
try {
await worker.initialize();
// Set up progress callback
await worker.setProgressCallback((progress: IngestionProgress) => {
onProgress?.(progress.message);
});
onProgress?.('Building knowledge graph...');
// Process repository
const result = await worker.processRepository({
projectRoot: '/',
projectName,
filePaths,
fileContents
});
if (!result.success || !result.graph) {
throw new Error(result.error || 'Failed to process repository');
}
return result.graph;
} catch (error) {
throw new Error(`Processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
private filterFiles(files: any[], directoryFilter?: string, fileExtensions?: string): any[] {
let filtered = files;
// Filter by directory
if (directoryFilter?.trim()) {
const dirPatterns = directoryFilter.toLowerCase().split(',').map(p => p.trim());
filtered = filtered.filter(file =>
dirPatterns.some(pattern => file.path.toLowerCase().includes(pattern))
);
}
// Filter by file extensions
if (fileExtensions?.trim()) {
const extensions = fileExtensions.toLowerCase().split(',').map(ext => ext.trim());
filtered = filtered.filter(file =>
extensions.some(ext => file.path.toLowerCase().endsWith(ext))
);
}
return filtered;
}
private filterPaths(paths: string[], directoryFilter?: string, fileExtensions?: string): string[] {
let filtered = paths;
// Filter by directory
if (directoryFilter?.trim()) {
const dirPatterns = directoryFilter.toLowerCase().split(',').map(p => p.trim());
filtered = filtered.filter(path =>
dirPatterns.some(pattern => path.toLowerCase().includes(pattern))
);
}
// Filter by file extensions
if (fileExtensions?.trim()) {
const extensions = fileExtensions.toLowerCase().split(',').map(ext => ext.trim());
filtered = filtered.filter(path =>
extensions.some(ext => path.toLowerCase().endsWith(ext))
);
}
return filtered;
}
}

View file

@ -1,6 +1,5 @@
import React, { useState, useEffect } from 'react';
import GraphVisualization from './Visualization.tsx';
import SourceViewer from './SourceViewer.tsx';
import type { KnowledgeGraph } from '../../../core/graph/types.ts';
interface GraphExplorerProps {
@ -8,89 +7,119 @@ interface GraphExplorerProps {
fileContents?: Map<string, string>;
className?: string;
style?: React.CSSProperties;
onNodeSelect?: (nodeId: string | null) => void;
selectedNodeId?: string | null;
}
const GraphExplorer: React.FC<GraphExplorerProps> = ({
graph,
fileContents,
className = '',
style = {}
style = {},
onNodeSelect,
selectedNodeId
}) => {
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
const [internalSelectedNodeId, setInternalSelectedNodeId] = useState<string | null>(null);
// Debug: Log when component receives graph data
useEffect(() => {
console.log('=== GRAPH EXPLORER RECEIVED GRAPH ===');
console.log('Graph nodes:', graph?.nodes?.length || 0);
console.log('Graph relationships:', graph?.relationships?.length || 0);
console.log('Graph object:', graph);
console.log('=====================================');
}, [graph]);
// Use external selectedNodeId if provided, otherwise use internal state
const currentSelectedNodeId = selectedNodeId !== undefined ? selectedNodeId : internalSelectedNodeId;
const handleNodeSelect = (nodeId: string | null) => {
setSelectedNodeId(nodeId);
if (onNodeSelect) {
onNodeSelect(nodeId);
} else {
setInternalSelectedNodeId(nodeId);
}
};
// Warm tone colors to match the new theme
const colors = {
background: '#FEF9F0', // Slightly warm white
surface: '#FFFFFF',
text: '#451A03', // Dark brown
textSecondary: '#78350F', // Medium brown
textMuted: '#A16207', // Light brown
border: '#FED7AA', // Light orange
borderLight: '#FEF3C7', // Very light orange
primary: '#D97706' // Warm orange
};
const containerStyle: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
gap: '16px',
padding: '16px',
backgroundColor: '#f5f5f5',
borderRadius: '8px',
width: '100%',
height: '100%',
width: '100%',
backgroundColor: colors.background,
overflow: 'hidden',
...style
};
const graphContainerStyle: React.CSSProperties = {
flex: '1 1 60%',
minHeight: '400px'
};
const sourceContainerStyle: React.CSSProperties = {
flex: '1 1 40%',
minHeight: '300px'
};
const headerStyle: React.CSSProperties = {
fontSize: '18px',
fontWeight: '600',
color: '#333',
marginBottom: '8px',
padding: '24px 24px 16px 24px',
backgroundColor: colors.surface,
borderBottom: `1px solid ${colors.borderLight}`
};
const titleStyle: React.CSSProperties = {
fontSize: '20px',
fontWeight: '700',
color: colors.text,
marginBottom: '16px',
display: 'flex',
alignItems: 'center',
gap: '8px'
gap: '12px'
};
const statsStyle: React.CSSProperties = {
const statsContainerStyle: React.CSSProperties = {
display: 'flex',
gap: '24px',
fontSize: '14px',
color: '#666',
marginBottom: '16px',
padding: '12px',
backgroundColor: '#fff',
borderRadius: '4px',
border: '1px solid #ddd'
flexWrap: 'wrap'
};
const statItemStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
gap: '4px'
gap: '8px',
padding: '8px 16px',
backgroundColor: colors.background,
borderRadius: '8px',
border: `1px solid ${colors.borderLight}`,
fontSize: '14px',
fontWeight: '500',
color: colors.textSecondary
};
const graphContainerStyle: React.CSSProperties = {
flex: 1,
overflow: 'hidden',
position: 'relative'
};
const selectedNodeInfoStyle: React.CSSProperties = {
position: 'absolute',
top: '16px',
left: '16px',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(8px)',
padding: '12px 16px',
borderRadius: '8px',
border: `1px solid ${colors.borderLight}`,
fontSize: '14px',
fontWeight: '500',
color: colors.text,
zIndex: 10,
boxShadow: '0 4px 12px rgba(0,0,0,0.1)'
};
// Calculate graph statistics
const nodeStats = graph.nodes.reduce((acc, node) => {
const nodeStats = graph?.nodes?.reduce((acc, node) => {
acc[node.label] = (acc[node.label] || 0) + 1;
return acc;
}, {} as Record<string, number>);
}, {} as Record<string, number>) || {};
function getSelectedNodeName(): string {
if (!selectedNodeId) return '';
const node = graph.nodes.find(n => n.id === selectedNodeId);
if (!currentSelectedNodeId || !graph?.nodes) return '';
const node = graph.nodes.find(n => n.id === currentSelectedNodeId);
return node?.properties.name as string || node?.id || '';
}
@ -110,27 +139,26 @@ const GraphExplorer: React.FC<GraphExplorerProps> = ({
return (
<div className={`graph-explorer ${className}`} style={containerStyle}>
{/* Header */}
<div>
<div style={headerStyle}>
<span>🔍</span>
<span>Code Knowledge Graph Explorer</span>
{/* Header with Statistics */}
<div style={headerStyle}>
<div style={titleStyle}>
<span>🕸️</span>
<span>Knowledge Graph</span>
</div>
{/* Statistics */}
<div style={statsStyle}>
<div style={statsContainerStyle}>
<div style={statItemStyle}>
<span>📊</span>
<span><strong>{graph.nodes.length}</strong> nodes</span>
<span>{graph?.nodes?.length || 0} nodes</span>
</div>
<div style={statItemStyle}>
<span>🔗</span>
<span><strong>{graph.relationships.length}</strong> relationships</span>
<span>{graph?.relationships?.length || 0} relationships</span>
</div>
{Object.entries(nodeStats).map(([type, count]) => (
{Object.entries(nodeStats).slice(0, 4).map(([type, count]) => (
<div key={type} style={statItemStyle}>
<span>{getNodeTypeIcon(type)}</span>
<span><strong>{count}</strong> {type.toLowerCase()}s</span>
<span>{count} {type.toLowerCase()}s</span>
</div>
))}
</div>
@ -138,40 +166,19 @@ const GraphExplorer: React.FC<GraphExplorerProps> = ({
{/* Graph Visualization */}
<div style={graphContainerStyle}>
<div style={{ ...headerStyle, fontSize: '16px', marginBottom: '8px' }}>
<span>🕸️</span>
<span>Interactive Graph</span>
{selectedNodeId && (
<span style={{
fontSize: '14px',
fontWeight: 'normal',
color: '#666',
marginLeft: '8px'
}}>
• Selected: {getSelectedNodeName()}
</span>
)}
</div>
<GraphVisualization
graph={graph}
selectedNodeId={selectedNodeId}
selectedNodeId={currentSelectedNodeId}
onNodeSelect={handleNodeSelect}
style={{ height: '100%' }}
/>
</div>
{/* Source Viewer */}
<div style={sourceContainerStyle}>
<div style={{ ...headerStyle, fontSize: '16px', marginBottom: '8px' }}>
<span>📝</span>
<span>Source Code</span>
</div>
<SourceViewer
graph={graph}
selectedNodeId={selectedNodeId}
fileContents={fileContents}
style={{ height: '100%' }}
/>
{/* Selected Node Info Overlay */}
{currentSelectedNodeId && (
<div style={selectedNodeInfoStyle}>
<strong>Selected:</strong> {getSelectedNodeName()}
</div>
)}
</div>
</div>
);

View file

@ -27,6 +27,100 @@ const SourceViewer: React.FC<SourceViewerProps> = ({
className = '',
style = {}
}) => {
// Warm tone colors to match the new theme
const colors = {
background: '#FEF9F0', // Slightly warm white
surface: '#FFFFFF',
text: '#451A03', // Dark brown
textSecondary: '#78350F', // Medium brown
textMuted: '#A16207', // Light brown
border: '#FED7AA', // Light orange
borderLight: '#FEF3C7', // Very light orange
primary: '#D97706', // Warm orange
codeBackground: '#FDF6E3', // Warm cream for code
lineNumbers: '#92400E' // Dark orange for line numbers
};
// Extract relevant content from a file for a specific function/class/method
const extractRelevantContent = (fileContent: string, targetName: string, nodeType: string): string | null => {
const lines = fileContent.split('\n');
try {
if (nodeType === 'Function') {
// Look for function definition patterns
const patterns = [
new RegExp(`^\\s*def\\s+${targetName}\\s*\\(`), // Python
new RegExp(`^\\s*function\\s+${targetName}\\s*\\(`), // JavaScript
new RegExp(`^\\s*const\\s+${targetName}\\s*=`), // JavaScript const
new RegExp(`^\\s*let\\s+${targetName}\\s*=`), // JavaScript let
new RegExp(`^\\s*export\\s+function\\s+${targetName}\\s*\\(`), // ES6 export
new RegExp(`^\\s*(public|private|protected)?\\s*\\w*\\s*${targetName}\\s*\\(`) // Java/C#
];
for (let i = 0; i < lines.length; i++) {
if (patterns.some(pattern => pattern.test(lines[i]))) {
// Found the function, now extract it with context
const startLine = Math.max(0, i - 2); // Include 2 lines before for context
let endLine = i + 1;
// Find the end of the function (simple heuristic)
let braceCount = 0;
const indentLevel = lines[i].match(/^\s*/)?.[0].length || 0;
for (let j = i + 1; j < lines.length; j++) {
const line = lines[j];
const currentIndent = line.match(/^\s*/)?.[0].length || 0;
// For Python, use indentation
if (lines[i].includes('def ')) {
if (line.trim() && currentIndent <= indentLevel && !line.startsWith(' ')) {
break;
}
endLine = j;
}
// For JavaScript/Java, use braces
else {
braceCount += (line.match(/\{/g) || []).length;
braceCount -= (line.match(/\}/g) || []).length;
endLine = j;
if (braceCount === 0 && j > i) {
break;
}
}
// Safety limit
if (j - i > 100) break;
}
return lines.slice(startLine, endLine + 3).join('\n'); // Include 3 lines after
}
}
}
if (nodeType === 'Class') {
const patterns = [
new RegExp(`^\\s*class\\s+${targetName}\\b`),
new RegExp(`^\\s*(public|private)?\\s*class\\s+${targetName}\\b`)
];
for (let i = 0; i < lines.length; i++) {
if (patterns.some(pattern => pattern.test(lines[i]))) {
const startLine = Math.max(0, i - 2);
const endLine = i + 20; // Show first 20 lines of class
return lines.slice(startLine, Math.min(endLine, lines.length)).join('\n');
}
}
}
// If we can't extract specifically, return null to use full file
return null;
} catch (error) {
console.warn('Error extracting content:', error);
return null;
}
};
// Generate mock source content based on node type
const generateMockContent = (node: GraphNode): string => {
try {
@ -58,390 +152,366 @@ const SourceViewer: React.FC<SourceViewerProps> = ({
"""
Class: ${name}
"""
def __init__(self):
# Constructor implementation
# Constructor
pass`;
case 'Variable':
return `# Variable: ${name}
${name} = None # Initialize variable`;
case 'File': {
const extension = node.properties.extension as string || '';
const language = node.properties.language as string || 'unknown';
const path = node.properties.path as string || name;
const extension = path.split('.').pop()?.toLowerCase() || 'txt';
// Handle different file types
if (extension === '.js' || extension === '.ts' || extension === '.tsx' || extension === '.jsx') {
return `// File: ${name}
// Path: ${node.properties.filePath || node.properties.path || 'unknown'}
// Language: JavaScript/TypeScript
switch (extension) {
case 'py':
return `# File: ${path}
"""
Python module: ${name}
"""
// This is a ${language} file in the project
// Click on specific functions or classes to see their details
def main():
print("Hello from ${name}")
export default function() {
// Implementation here
if __name__ == "__main__":
main()`;
case 'js':
case 'ts':
return `// File: ${path}
/**
* JavaScript/TypeScript module: ${name}
*/
function main() {
console.log("Hello from ${name}");
}
export default main;`;
case 'java':
return `// File: ${path}
/**
* Java class: ${name}
*/
public class ${name.replace(/\.[^/.]+$/, "")} {
public static void main(String[] args) {
System.out.println("Hello from ${name}");
}
}`;
} else {
return `# File: ${name}
# Path: ${node.properties.filePath || node.properties.path || 'unknown'}
# This is a Python file in the project
# Click on specific functions or classes to see their details`;
default:
return `// File: ${path}
// Content of ${name}`;
}
}
case 'Folder':
return `# Folder: ${name}
# Path: ${node.properties.path || 'unknown'}
# This folder contains:
# - Python files (.py)
# - Other project files
#
# Navigate through the graph to explore the contents`;
return `# Directory: ${name}
# This is a folder containing other files and directories`;
case 'Project':
return `# Project: ${name}
# Root directory of the project`;
# This is the root of your project
# Explore the graph to see:
# - Project structure
# - Files and folders
# - Functions and classes
# - Code relationships`;
default: {
// Safe JSON stringify for default case
const safeStringify = (obj: unknown) => {
try {
return JSON.stringify(obj, null, 2);
} catch {
const seen = new WeakSet();
return JSON.stringify(obj, (key, val) => {
if (val != null && typeof val === "object") {
if (seen.has(val)) {
return "[Circular Reference]";
}
seen.add(val);
}
if (typeof val === 'function') return "[Function]";
if (val instanceof Date) return val.toISOString();
if (val instanceof RegExp) return val.toString();
return val;
}, 2);
}
};
default:
return `# ${node.label}: ${name}
# Node ID: ${node.id}
# Properties: ${safeStringify(node.properties)}
# This node represents a ${node.label.toLowerCase()} in your codebase`;
}
# No specific content available`;
}
} catch (error) {
console.error('Error generating mock content:', error);
return `# Error generating content for ${node.label}: ${node.properties.name || node.id}
Error: ${error instanceof Error ? error.message : 'Unknown error'}
Node type: ${node.label}
Node ID: ${node.id}
# This error occurred while trying to generate mock content for display`;
return `# Error generating content for ${node.label}
# ${error instanceof Error ? error.message : 'Unknown error'}`;
}
};
// Get source information for the selected node
// Get source info for selected node
const sourceInfo = useMemo((): SourceInfo | null => {
if (!selectedNodeId) return null;
if (!selectedNodeId || !graph?.nodes) return null;
const selectedNode = graph.nodes.find(n => n.id === selectedNodeId);
if (!selectedNode) return null;
const node = graph.nodes.find(n => n.id === selectedNodeId);
if (!node) return null;
try {
// Get file path from node properties
const filePath = selectedNode.properties.filePath as string || selectedNode.properties.path as string || '/unknown';
// Get file name from path
const pathParts = filePath.split('/');
const fileName = pathParts[pathParts.length - 1];
let content: string = '';
// Try to get real file contents first
if (fileContents && selectedNode.label === 'File') {
const realContent = fileContents.get(filePath);
if (realContent) {
content = realContent;
} else {
// Try alternative path formats
const alternativePaths = [
selectedNode.properties.name as string,
fileName,
filePath.replace(/^\/+/, ''), // Remove leading slashes
filePath.replace(/\\/g, '/'), // Normalize path separators
].filter(Boolean);
let found = false;
for (const altPath of alternativePaths) {
const altContent = fileContents.get(altPath);
if (altContent) {
content = altContent;
found = true;
break;
}
}
if (!found) {
// Fall back to mock content if real content not found
content = generateMockContent(selectedNode);
}
}
} else {
// For non-file nodes or when fileContents not available, use mock content
try {
content = generateMockContent(selectedNode);
} catch (mockError) {
console.error('Error in generateMockContent:', mockError);
// If mock content generation fails, create a basic display
const extension = selectedNode.properties.extension as string || '';
const name = selectedNode.properties.name as string || 'unknown';
if (extension === '.js' || extension === '.ts' || extension === '.tsx' || extension === '.jsx') {
content = `// File: ${name}
// Path: ${filePath}
// Type: JavaScript/TypeScript Build Artifact
// This is a compiled JavaScript file
// It's part of the build output and contains bundled code
console.log('This is a Next.js build chunk');
// Original source files would show actual code here
// Build artifacts like this contain minified/compiled code`;
} else {
content = `# File: ${name}
# Path: ${filePath}
# This file could not be processed for display
# Error: ${mockError instanceof Error ? mockError.message : 'Unknown error'}`;
}
}
}
// Determine language from file extension
const extension = selectedNode.properties.extension as string || '';
let language = selectedNode.properties.language as string;
if (!language) {
if (extension === '.js' || extension === '.jsx') {
language = 'javascript';
} else if (extension === '.ts' || extension === '.tsx') {
language = 'typescript';
} else if (extension === '.py') {
language = 'python';
} else if (extension === '.sol') {
language = 'solidity';
} else {
language = 'text';
}
}
return {
fileName,
filePath,
content,
language,
nodeType: selectedNode.label,
nodeName: selectedNode.properties.name as string || selectedNode.id
};
} catch (error) {
console.error('Error generating source info:', error);
// Safe JSON stringify that handles circular references
const safeStringify = (obj: unknown) => {
try {
return JSON.stringify(obj, null, 2);
} catch {
// Handle circular references and non-serializable values
const seen = new WeakSet();
return JSON.stringify(obj, (key, val) => {
if (val != null && typeof val === "object") {
if (seen.has(val)) {
return "[Circular Reference]";
}
seen.add(val);
}
// Handle functions and other non-serializable types
if (typeof val === 'function') {
return "[Function]";
}
if (val instanceof Date) {
return val.toISOString();
}
if (val instanceof RegExp) {
return val.toString();
}
return val;
}, 2);
}
};
return {
fileName: 'error.txt',
filePath: '/error',
content: `Error loading source for node: ${selectedNode.id}\n\nNode properties: ${safeStringify(selectedNode.properties)}`,
language: 'text',
nodeType: selectedNode.label,
nodeName: selectedNode.properties.name as string || selectedNode.id
};
}
}, [selectedNodeId, graph, fileContents]);
// Format line numbers
const formatLineNumbers = (content: string, startLine?: number): string => {
const lines = content.split('\n');
const lineStart = startLine || 1;
const nodeName = node.properties.name as string || node.id;
return lines
.map((line, index) => {
const lineNumber = (lineStart + index).toString().padStart(3, ' ');
return `${lineNumber}│ ${line}`;
})
.join('\n');
};
// First, try to get the file path from the node's properties
let filePath = node.properties.path as string || node.properties.filePath as string;
// If the node doesn't have a direct file path, find it through graph relationships
if (!filePath || filePath === nodeName) {
console.log('SourceViewer - Finding file through graph relationships for:', nodeName);
// Find the file that CONTAINS this node
const containsRelationship = graph.relationships?.find(rel =>
rel.type === 'CONTAINS' && rel.target === selectedNodeId
);
if (containsRelationship) {
const fileNode = graph.nodes.find(n => n.id === containsRelationship.source);
if (fileNode && fileNode.label === 'File') {
filePath = fileNode.properties.path as string || fileNode.properties.filePath as string;
console.log('SourceViewer - Found file through CONTAINS relationship:', filePath);
}
}
// If still no file path, try reverse lookup by searching for the node name in file contents
if (!filePath && fileContents) {
console.log('SourceViewer - Searching file contents for node:', nodeName);
for (const [path, content] of fileContents) {
const patterns = [
`def ${nodeName}(`, // Python function
`function ${nodeName}(`, // JavaScript function
`const ${nodeName} =`, // JavaScript const
`class ${nodeName}`, // Class definition
`${nodeName}:`, // Object property or TypeScript type
];
if (patterns.some(pattern => content.includes(pattern))) {
filePath = path;
console.log('SourceViewer - Found file through content search:', filePath);
break;
}
}
}
}
const defaultStyle: React.CSSProperties = {
width: '100%',
height: '400px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f8f9fa',
const fileName = filePath ? filePath.split('/').pop() || filePath : nodeName;
console.log('SourceViewer - Final node details:', {
nodeId: selectedNodeId,
nodeName,
filePath,
fileName,
nodeLabel: node.label,
nodeProperties: node.properties,
fileContentsSize: fileContents?.size || 0
});
// Try to get actual file content
let content = '';
if (filePath && fileContents && fileContents.has(filePath)) {
content = fileContents.get(filePath)!;
console.log('SourceViewer - Found file content for:', filePath);
// For function/method/class nodes, try to extract just the relevant part
if (node.label === 'Function' || node.label === 'Method' || node.label === 'Class') {
const extractedContent = extractRelevantContent(content, nodeName, node.label);
if (extractedContent) {
content = extractedContent;
console.log('SourceViewer - Extracted relevant content for:', nodeName);
}
}
} else {
console.log('SourceViewer - No file content found, using mock content');
content = generateMockContent(node);
}
// Detect language from file extension
const extension = fileName.split('.').pop()?.toLowerCase();
let language = 'text';
switch (extension) {
case 'js':
case 'jsx':
language = 'javascript';
break;
case 'ts':
case 'tsx':
language = 'typescript';
break;
case 'py':
language = 'python';
break;
case 'java':
language = 'java';
break;
case 'cpp':
case 'cc':
case 'cxx':
language = 'cpp';
break;
case 'c':
language = 'c';
break;
case 'cs':
language = 'csharp';
break;
case 'php':
language = 'php';
break;
case 'rb':
language = 'ruby';
break;
case 'go':
language = 'go';
break;
case 'rs':
language = 'rust';
break;
case 'swift':
language = 'swift';
break;
case 'kt':
language = 'kotlin';
break;
case 'scala':
language = 'scala';
break;
case 'html':
language = 'html';
break;
case 'css':
language = 'css';
break;
case 'scss':
language = 'scss';
break;
case 'json':
language = 'json';
break;
case 'xml':
language = 'xml';
break;
case 'yaml':
case 'yml':
language = 'yaml';
break;
case 'md':
language = 'markdown';
break;
case 'sh':
language = 'bash';
break;
case 'sql':
language = 'sql';
break;
}
return {
fileName,
filePath: filePath || nodeName,
content,
nodeType: node.label,
nodeName,
language,
startLine: node.properties.startLine as number,
endLine: node.properties.endLine as number
};
}, [selectedNodeId, graph?.nodes, graph?.relationships, fileContents]);
const containerStyle: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
height: '100%',
width: '100%',
backgroundColor: colors.background,
overflow: 'hidden',
...style
};
const headerStyle: React.CSSProperties = {
padding: '12px 16px',
borderBottom: '1px solid #ddd',
backgroundColor: '#fff',
borderRadius: '4px 4px 0 0',
fontSize: '14px',
fontWeight: '600',
color: '#333'
padding: '16px 20px',
backgroundColor: colors.surface,
borderBottom: `1px solid ${colors.borderLight}`,
flexShrink: 0
};
const contentStyle: React.CSSProperties = {
const titleStyle: React.CSSProperties = {
fontSize: '16px',
fontWeight: '600',
color: colors.text,
marginBottom: '8px',
display: 'flex',
alignItems: 'center',
gap: '8px'
};
const infoStyle: React.CSSProperties = {
fontSize: '12px',
color: colors.textMuted,
display: 'flex',
alignItems: 'center',
gap: '16px',
flexWrap: 'wrap'
};
const codeContainerStyle: React.CSSProperties = {
flex: 1,
padding: '16px',
overflow: 'auto',
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace',
backgroundColor: colors.codeBackground,
position: 'relative'
};
const codeStyle: React.CSSProperties = {
fontFamily: "'Fira Code', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
fontSize: '13px',
lineHeight: '1.5',
backgroundColor: '#fafafa',
color: '#333',
whiteSpace: 'pre',
tabSize: 2
padding: '16px',
margin: 0,
backgroundColor: 'transparent',
color: colors.text,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
border: 'none',
outline: 'none',
resize: 'none',
width: '100%',
minHeight: '100%',
boxSizing: 'border-box'
};
const placeholderStyle: React.CSSProperties = {
flex: 1,
const emptyStateStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#666',
flexDirection: 'column',
height: '100%',
color: colors.textMuted,
fontSize: '14px',
fontStyle: 'italic'
gap: '12px',
padding: '32px'
};
const getNodeTypeColor = (nodeType: string): string => {
switch (nodeType.toLowerCase()) {
case 'project': return '#4CAF50';
case 'folder': return '#FF9800';
case 'file': return '#2196F3';
case 'module': return '#9C27B0';
case 'function': return '#00BCD4';
case 'method': return '#00BCD4';
case 'class': return '#E91E63';
case 'variable': return '#607D8B';
default: return '#666';
}
};
const renderEmptyState = () => (
<div style={emptyStateStyle}>
<div style={{ fontSize: '48px', opacity: 0.3 }}>📝</div>
<div style={{ textAlign: 'center' }}>
Select a node in the graph to view its source code
</div>
</div>
);
const getNodeTypeIcon = (nodeType: string): string => {
switch (nodeType.toLowerCase()) {
case 'project': return '📁';
case 'folder': return '📂';
case 'file': return '📄';
case 'module': return '📦';
case 'function': return '⚡';
case 'method': return '🔧';
case 'class': return '🏗️';
case 'variable': return '📊';
default: return '📄';
}
const renderSourceContent = (info: SourceInfo) => {
const lines = info.content.split('\n');
return (
<>
<div style={headerStyle}>
<div style={titleStyle}>
<span>📝</span>
<span>{info.fileName}</span>
</div>
<div style={infoStyle}>
<span>📄 {info.nodeType}</span>
<span>🏷️ {info.nodeName}</span>
{info.language && <span>💻 {info.language}</span>}
<span>📏 {lines.length} lines</span>
</div>
</div>
<div style={codeContainerStyle}>
<pre style={codeStyle}>
{info.content}
</pre>
</div>
</>
);
};
return (
<div className={`source-viewer ${className}`} style={defaultStyle}>
{sourceInfo ? (
<>
<div style={headerStyle}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '16px' }}>
{getNodeTypeIcon(sourceInfo.nodeType)}
</span>
<span
style={{
color: getNodeTypeColor(sourceInfo.nodeType),
fontWeight: 'bold'
}}
>
{sourceInfo.nodeType}
</span>
<span style={{ color: '#666' }}>•</span>
<span>{sourceInfo.nodeName}</span>
</div>
<div style={{
fontSize: '12px',
color: '#666',
marginTop: '4px',
display: 'flex',
alignItems: 'center',
gap: '16px'
}}>
<span>📁 {sourceInfo.fileName}</span>
{sourceInfo.startLine && sourceInfo.endLine && (
<span>📍 Lines {sourceInfo.startLine}-{sourceInfo.endLine}</span>
)}
</div>
</div>
<div style={contentStyle}>
{formatLineNumbers(sourceInfo.content, sourceInfo.startLine)}
</div>
</>
) : (
<>
<div style={headerStyle}>
<span style={{ color: '#666' }}>Source Code Viewer</span>
</div>
<div style={placeholderStyle}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '48px', marginBottom: '16px', opacity: 0.3 }}>
📄
</div>
<div>Select a node to view its source code</div>
<div style={{
fontSize: '12px',
color: '#999',
marginTop: '8px'
}}>
Click on any node in the graph to see its details
</div>
</div>
</div>
</>
)}
<div className={`source-viewer ${className}`} style={containerStyle}>
{sourceInfo ? renderSourceContent(sourceInfo) : renderEmptyState()}
</div>
);
};

File diff suppressed because it is too large Load diff