From d92b1370d3200bd0980ba500db891ca623559317 Mon Sep 17 00:00:00 2001 From: abhigyantrumio Date: Mon, 4 Aug 2025 03:38:03 +0530 Subject: [PATCH] Fixed Source viewer issue with edge nodes --- src/services/ingestion.service.ts | 199 ++ src/ui/components/graph/GraphExplorer.tsx | 177 +- src/ui/components/graph/SourceViewer.tsx | 736 ++++---- src/ui/pages/HomePage.tsx | 2095 ++++++--------------- 4 files changed, 1322 insertions(+), 1885 deletions(-) create mode 100644 src/services/ingestion.service.ts diff --git a/src/services/ingestion.service.ts b/src/services/ingestion.service.ts new file mode 100644 index 000000000..0659d4913 --- /dev/null +++ b/src/services/ingestion.service.ts @@ -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; +} + +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 { + 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(); + 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 { + 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(); + 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, + projectName: string, + filePaths: string[], + onProgress?: (message: string) => void + ): Promise { + 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; + } +} \ No newline at end of file diff --git a/src/ui/components/graph/GraphExplorer.tsx b/src/ui/components/graph/GraphExplorer.tsx index c70b0a46a..08edf1489 100644 --- a/src/ui/components/graph/GraphExplorer.tsx +++ b/src/ui/components/graph/GraphExplorer.tsx @@ -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; className?: string; style?: React.CSSProperties; + onNodeSelect?: (nodeId: string | null) => void; + selectedNodeId?: string | null; } const GraphExplorer: React.FC = ({ graph, fileContents, className = '', - style = {} + style = {}, + onNodeSelect, + selectedNodeId }) => { - const [selectedNodeId, setSelectedNodeId] = useState(null); + const [internalSelectedNodeId, setInternalSelectedNodeId] = useState(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); + }, {} as Record) || {}; 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 = ({ return (
- {/* Header */} -
-
- πŸ” - Code Knowledge Graph Explorer + {/* Header with Statistics */} +
+
+ πŸ•ΈοΈ + Knowledge Graph
- {/* Statistics */} -
+
πŸ“Š - {graph.nodes.length} nodes + {graph?.nodes?.length || 0} nodes
πŸ”— - {graph.relationships.length} relationships + {graph?.relationships?.length || 0} relationships
- {Object.entries(nodeStats).map(([type, count]) => ( + {Object.entries(nodeStats).slice(0, 4).map(([type, count]) => (
{getNodeTypeIcon(type)} - {count} {type.toLowerCase()}s + {count} {type.toLowerCase()}s
))}
@@ -138,40 +166,19 @@ const GraphExplorer: React.FC = ({ {/* Graph Visualization */}
-
- πŸ•ΈοΈ - Interactive Graph - {selectedNodeId && ( - - β€’ Selected: {getSelectedNodeName()} - - )} -
-
- - {/* Source Viewer */} -
-
- πŸ“ - Source Code -
- + + {/* Selected Node Info Overlay */} + {currentSelectedNodeId && ( +
+ Selected: {getSelectedNodeName()} +
+ )}
); diff --git a/src/ui/components/graph/SourceViewer.tsx b/src/ui/components/graph/SourceViewer.tsx index 26a8425b9..618e4a65f 100644 --- a/src/ui/components/graph/SourceViewer.tsx +++ b/src/ui/components/graph/SourceViewer.tsx @@ -27,6 +27,100 @@ const SourceViewer: React.FC = ({ 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 = ({ """ 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 = () => ( +
+
πŸ“
+
+ Select a node in the graph to view its source code +
+
+ ); - 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 ( + <> +
+
+ πŸ“ + {info.fileName} +
+
+ πŸ“„ {info.nodeType} + 🏷️ {info.nodeName} + {info.language && πŸ’» {info.language}} + πŸ“ {lines.length} lines +
+
+ +
+
+            {info.content}
+          
+
+ + ); }; return ( -
- {sourceInfo ? ( - <> -
-
- - {getNodeTypeIcon(sourceInfo.nodeType)} - - - {sourceInfo.nodeType} - - β€’ - {sourceInfo.nodeName} -
-
- πŸ“ {sourceInfo.fileName} - {sourceInfo.startLine && sourceInfo.endLine && ( - πŸ“ Lines {sourceInfo.startLine}-{sourceInfo.endLine} - )} -
-
-
- {formatLineNumbers(sourceInfo.content, sourceInfo.startLine)} -
- - ) : ( - <> -
- Source Code Viewer -
-
-
-
- πŸ“„ -
-
Select a node to view its source code
-
- Click on any node in the graph to see its details -
-
-
- - )} +
+ {sourceInfo ? renderSourceContent(sourceInfo) : renderEmptyState()}
); }; diff --git a/src/ui/pages/HomePage.tsx b/src/ui/pages/HomePage.tsx index 8ea974d6d..4fd1f49cf 100644 --- a/src/ui/pages/HomePage.tsx +++ b/src/ui/pages/HomePage.tsx @@ -1,1528 +1,689 @@ -import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'; -import { GitHubService } from '../../services/github.ts'; -import { ZipService } from '../../services/zip.ts'; -import { getIngestionWorker, type IngestionProgress } from '../../lib/workerUtils.ts'; +import React, { useState, useCallback, useEffect } from 'react'; +import ErrorBoundary from '../components/ErrorBoundary.tsx'; import { GraphExplorer } from '../components/graph/index.ts'; import { ChatInterface } from '../components/chat/index.ts'; -import ErrorBoundary from '../components/ErrorBoundary.tsx'; -import { LLMService, CypherGenerator, LangChainRAGOrchestrator } from '../../ai/index.ts'; -import { exportAndDownloadGraph, calculateExportSize } from '../../lib/export.ts'; +import SourceViewer from '../components/graph/SourceViewer.tsx'; import type { KnowledgeGraph } from '../../core/graph/types.ts'; -import type { LLMProvider, LLMConfig } from '../../ai/llm-service.ts'; - -// Import GitHubFile type -interface GitHubFile { - name: string; - path: string; - sha: string; - size: number; - url: string; - html_url: string; - git_url: string; - download_url: string | null; - type: 'file' | 'dir'; - content?: string; - encoding?: string; -} - -// Global services singleton to survive component remounts -interface Services { - github: GitHubService; - zip: ZipService; - llm: LLMService; - cypher: CypherGenerator; - rag: LangChainRAGOrchestrator; -} - -let globalServices: Services | null = null; - -function getOrCreateServices(githubToken?: string): Services { - if (!globalServices) { - console.log('[GLOBAL] Creating services singleton'); - const llmService = new LLMService(); - const cypherGenerator = new CypherGenerator(llmService); - const ragOrchestrator = new LangChainRAGOrchestrator(llmService, cypherGenerator); - - globalServices = { - github: new GitHubService(githubToken), - zip: new ZipService(), - llm: llmService, - cypher: cypherGenerator, - rag: ragOrchestrator - }; - } - return globalServices; -} +import { IngestionService } from '../../services/ingestion.service.ts'; +import { LLMService, type LLMProvider } from '../../ai/llm-service.ts'; interface AppState { // Data graph: KnowledgeGraph | null; fileContents: Map; - selectedNodeId: string | null; // UI State - isProcessing: boolean; - progress: IngestionProgress | null; - error: string | null; + selectedNodeId: string | null; + showWelcome: boolean; // Input State githubUrl: string; - uploadedFile: File | null; + directoryFilter: string; + fileExtensions: string; + + // Processing State + isProcessing: boolean; + progress: string; + error: string; // Settings - githubToken: string; llmProvider: LLMProvider; llmApiKey: string; - llmModel: string; - - // Performance settings - maxFiles: number; - directoryFilter: string; - filePatternFilter: string; + showSettings: boolean; } -interface ProcessingStats { - totalFiles: number; - processedFiles: number; - duration: number; - nodeCount: number; - relationshipCount: number; - projectName?: string; -} +const initialState: AppState = { + graph: null, + fileContents: new Map(), + selectedNodeId: null, + showWelcome: true, + githubUrl: '', + directoryFilter: 'src,lib,components,pages,utils', + fileExtensions: '.ts,.tsx,.js,.jsx,.py,.java,.cpp,.c,.cs,.php,.rb,.go,.rs,.swift,.kt,.scala,.clj,.hs,.ml,.fs,.elm,.dart,.lua,.r,.m,.sh,.sql,.html,.css,.scss,.less,.vue,.svelte', + isProcessing: false, + progress: '', + error: '', + llmProvider: 'openai', + llmApiKey: localStorage.getItem('llm_api_key') || '', + showSettings: false +}; -interface ConfirmationDialogProps { - isOpen: boolean; - title: string; - message: string; - details?: string[]; - onConfirm: () => void; - onCancel: () => void; - confirmText?: string; - cancelText?: string; -} +const HomePage: React.FC = () => { + const [state, setState] = useState(initialState); + const [services] = useState(() => ({ + ingestion: new IngestionService(), + llm: new LLMService() + })); -const ConfirmationDialog: React.FC = ({ - isOpen, - title, - message, - details, - onConfirm, - onCancel, - confirmText = 'Continue', - cancelText = 'Cancel' -}) => { - if (!isOpen) return null; + const updateState = useCallback((updates: Partial) => { + setState(prev => ({ ...prev, ...updates })); + }, []); - return ( -
{ + if (state.llmApiKey) { + localStorage.setItem('llm_api_key', state.llmApiKey); + } + }, [state.llmApiKey]); + + const handleFileUpload = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file || !file.name.endsWith('.zip')) { + updateState({ error: 'Please select a valid ZIP file' }); + return; + } + + try { + updateState({ + isProcessing: true, + error: '', + progress: 'Reading ZIP file...', + showWelcome: false + }); + + console.log('Starting ZIP processing...', file.name); + + const result = await services.ingestion.processZipFile(file, { + directoryFilter: state.directoryFilter, + fileExtensions: state.fileExtensions + }); + + console.log('ZIP processing completed:', { + nodeCount: result.graph?.nodes?.length || 0, + relationshipCount: result.graph?.relationships?.length || 0, + fileCount: result.fileContents?.size || 0 + }); + + updateState({ + graph: result.graph, + fileContents: result.fileContents, + isProcessing: false, + progress: '', + showWelcome: false // Ensure we stay in main interface + }); + } catch (error) { + console.error('ZIP processing error:', error); + updateState({ + error: error instanceof Error ? error.message : 'Failed to process ZIP file', + isProcessing: false, + progress: '', + showWelcome: true // Return to welcome screen on error + }); + } + }; + + const handleGitHubProcess = async () => { + if (!state.githubUrl.trim()) { + updateState({ error: 'Please enter a GitHub repository URL' }); + return; + } + + try { + updateState({ + isProcessing: true, + error: '', + progress: 'Fetching repository...', + showWelcome: false + }); + + console.log('Starting GitHub processing...', state.githubUrl); + + const result = await services.ingestion.processGitHubRepo(state.githubUrl, { + directoryFilter: state.directoryFilter, + fileExtensions: state.fileExtensions, + onProgress: (progress) => { + console.log('Progress:', progress); + updateState({ progress }); + } + }); + + console.log('GitHub processing completed:', { + nodeCount: result.graph?.nodes?.length || 0, + relationshipCount: result.graph?.relationships?.length || 0, + fileCount: result.fileContents?.size || 0 + }); + + updateState({ + graph: result.graph, + fileContents: result.fileContents, + isProcessing: false, + progress: '', + showWelcome: false // Ensure we stay in main interface + }); + } catch (error) { + console.error('GitHub processing error:', error); + updateState({ + error: error instanceof Error ? error.message : 'Failed to process repository', + isProcessing: false, + progress: '', + showWelcome: true // Return to welcome screen on error + }); + } + }; + + const handleNewProject = () => { + updateState({ + graph: null, + fileContents: new Map(), + selectedNodeId: null, + showWelcome: true, + githubUrl: '', + error: '', + progress: '' + }); + }; + + const handleNodeSelect = (nodeId: string | null) => { + console.log('HomePage - Node selected:', { + nodeId, + fileContentsSize: state.fileContents.size, + fileContentsKeys: Array.from(state.fileContents.keys()).slice(0, 10), + graphNodeCount: state.graph?.nodes?.length || 0 + }); + updateState({ selectedNodeId: nodeId }); + }; + + const isApiKeyValid = services.llm.validateApiKey(state.llmProvider, state.llmApiKey); + const isGraphValid = state.graph && state.graph.nodes && Array.isArray(state.graph.nodes) && state.graph.relationships && Array.isArray(state.graph.relationships); + + // Warm tone color palette + const colors = { + background: '#FDF6E3', // Warm cream + surface: '#FFFFFF', + surfaceWarm: '#FEF9F0', // Slightly warm white + primary: '#D97706', // Warm orange + primaryLight: '#F59E0B', // Light orange + secondary: '#92400E', // Dark orange + accent: '#DC2626', // Warm red + text: '#451A03', // Dark brown + textSecondary: '#78350F', // Medium brown + textMuted: '#A16207', // Light brown + border: '#FED7AA', // Light orange + borderLight: '#FEF3C7', // Very light orange + success: '#059669', // Warm green + warning: '#D97706', // Orange + error: '#DC2626' // Red + }; + + // Modern styles with warm theme + const styles = { + container: { + display: 'flex', + flexDirection: 'column' as const, + height: '100vh', + width: '100vw', + backgroundColor: colors.background, + fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", + overflow: 'hidden' + }, + + // Top navbar (only visible when project is loaded) + navbar: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '12px 24px', + backgroundColor: colors.surface, + borderBottom: `1px solid ${colors.borderLight}`, + boxShadow: '0 1px 3px rgba(0,0,0,0.05)', + position: 'relative' as const + }, + + navbarContent: { + display: 'flex', + alignItems: 'center', + gap: '16px', + fontSize: '14px', + fontWeight: '500', + color: colors.textSecondary + }, + + navbarButton: { + padding: '8px 16px', + backgroundColor: colors.surfaceWarm, + border: `1px solid ${colors.border}`, + borderRadius: '8px', + color: colors.text, + fontSize: '14px', + fontWeight: '500', + cursor: 'pointer', + transition: 'all 0.2s ease', + display: 'flex', + alignItems: 'center', + gap: '8px' + }, + + // Welcome screen (center overlay) + welcomeOverlay: { + position: 'fixed' as const, top: 0, left: 0, right: 0, bottom: 0, - backgroundColor: 'rgba(0,0,0,0.5)', + backgroundColor: colors.background, display: 'flex', alignItems: 'center', justifyContent: 'center', - zIndex: 1001 - }}> -
-

{title}

-

{message}

- - {details && details.length > 0 && ( -
    - {details.map((detail, index) => ( -
  • {detail}
  • - ))} -
+ zIndex: 1000 + }, + + welcomeCard: { + backgroundColor: colors.surface, + borderRadius: '20px', + padding: '48px', + boxShadow: '0 20px 40px rgba(0,0,0,0.1)', + border: `1px solid ${colors.borderLight}`, + maxWidth: '600px', + width: '90%', + textAlign: 'center' as const + }, + + welcomeTitle: { + fontSize: '32px', + fontWeight: '700', + color: colors.text, + marginBottom: '16px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '12px' + }, + + welcomeSubtitle: { + fontSize: '18px', + color: colors.textSecondary, + marginBottom: '40px', + lineHeight: '1.6' + }, + + inputSection: { + display: 'flex', + flexDirection: 'column' as const, + gap: '24px', + marginBottom: '32px' + }, + + inputGroup: { + display: 'flex', + flexDirection: 'column' as const, + gap: '8px', + textAlign: 'left' as const + }, + + label: { + fontSize: '14px', + fontWeight: '600', + color: colors.text + }, + + input: { + padding: '16px', + border: `2px solid ${colors.border}`, + borderRadius: '12px', + fontSize: '16px', + backgroundColor: colors.surfaceWarm, + color: colors.text, + transition: 'all 0.2s ease', + outline: 'none' + }, + + primaryButton: { + padding: '16px 32px', + backgroundColor: colors.primary, + border: 'none', + borderRadius: '12px', + color: 'white', + fontSize: '16px', + fontWeight: '600', + cursor: 'pointer', + transition: 'all 0.2s ease', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '8px' + }, + + secondaryButton: { + padding: '12px 24px', + backgroundColor: colors.surfaceWarm, + border: `2px solid ${colors.border}`, + borderRadius: '12px', + color: colors.text, + fontSize: '14px', + fontWeight: '500', + cursor: 'pointer', + transition: 'all 0.2s ease' + }, + + orDivider: { + display: 'flex', + alignItems: 'center', + gap: '16px', + margin: '20px 0', + color: colors.textMuted, + fontSize: '14px', + fontWeight: '500' + }, + + orLine: { + flex: 1, + height: '1px', + backgroundColor: colors.border + }, + + // Main layout (when project is loaded) + mainLayout: { + display: 'flex', + flex: 1, + overflow: 'hidden' + }, + + // Left side - Knowledge Graph (70% width) + leftPanel: { + flex: '0 0 70%', + backgroundColor: colors.surface, + borderRight: `1px solid ${colors.borderLight}`, + overflow: 'hidden' + }, + + // Right side - Chat and Source (30% width) + rightPanel: { + flex: '0 0 30%', + display: 'flex', + flexDirection: 'column' as const, + backgroundColor: colors.surfaceWarm + }, + + // Right panel sections + chatSection: { + flex: '0 0 60%', + borderBottom: `1px solid ${colors.borderLight}`, + overflow: 'hidden' + }, + + sourceSection: { + flex: '0 0 40%', + overflow: 'hidden' + }, + + // Error and progress styles + errorBanner: { + backgroundColor: '#FEF2F2', + border: `1px solid #FECACA`, + color: colors.error, + padding: '16px', + borderRadius: '12px', + margin: '16px 0', + fontSize: '14px' + }, + + progressBanner: { + backgroundColor: '#FEF3C7', + border: `1px solid ${colors.border}`, + color: colors.secondary, + padding: '16px', + borderRadius: '12px', + margin: '16px 0', + fontSize: '14px', + display: 'flex', + alignItems: 'center', + gap: '12px' + }, + + spinner: { + width: '20px', + height: '20px', + border: `2px solid ${colors.border}`, + borderTop: `2px solid ${colors.primary}`, + borderRadius: '50%', + animation: 'spin 1s linear infinite' + } + }; + + const renderWelcomeScreen = () => ( +
+
+
+ πŸ” + GitNexus +
+
+ Transform your codebase into an interactive knowledge graph +
+ + {state.error && ( +
+ {state.error} +
)} -
- - + {state.isProcessing && ( +
+
+ {state.progress} +
+ )} + +
+
+ + updateState({ githubUrl: e.target.value })} + placeholder="https://github.com/owner/repo" + style={styles.input} + disabled={state.isProcessing} + /> + +
+ +
+
+ OR +
+
+ +
+ + +
+ +
); -}; -const HomePage: React.FC = () => { - // Persistent logging system - make it completely stable - const logToPersistent = useRef((message: string, data?: unknown) => { - const timestamp = new Date().toISOString(); - const logEntry = { - timestamp, - message, - data: data ? JSON.stringify(data, null, 2) : undefined - }; - - // Get existing logs - const existingLogs = localStorage.getItem('gitnexus_debug_logs'); - const logs = existingLogs ? JSON.parse(existingLogs) : []; - - // Add new log - logs.push(logEntry); - - // Keep only last 50 logs - if (logs.length > 50) { - logs.splice(0, logs.length - 50); - } - - // Save back to localStorage - localStorage.setItem('gitnexus_debug_logs', JSON.stringify(logs)); - - // Also log to console - console.log(`[PERSISTENT] ${message}`, data || ''); - }).current; - - // Main application state - const [state, setState] = useState({ - graph: null, - fileContents: new Map(), - selectedNodeId: null, - isProcessing: false, - progress: null, - error: null, - githubUrl: '', - uploadedFile: null, - githubToken: '', - llmProvider: 'openai', - llmApiKey: '', - llmModel: 'gpt-4o-mini', - maxFiles: 100, // Reduced from 500 to prevent memory issues - directoryFilter: '', - filePatternFilter: '' - }); - - const [processingStats, setProcessingStats] = useState(null); - const [showSettings, setShowSettings] = useState(false); - const [showConfirmation, setShowConfirmation] = useState(false); - const [confirmationData, setConfirmationData] = useState<{ - files: GitHubFile[]; - projectName: string; - } | null>(null); - - // Stable reference for GitHub token to prevent service recreation - const githubTokenRef = useRef(state.githubToken); - - // Update the ref when token changes - useEffect(() => { - githubTokenRef.current = state.githubToken; - }, [state.githubToken]); - - // Update state helper - remove logToPersistent dependency - const updateState = useCallback((updates: Partial) => { - logToPersistent('State update', updates); - setState(prev => ({ ...prev, ...updates })); - }, []); // No dependencies! - - // Check for previous logs on component mount - remove logToPersistent dependency - useEffect(() => { - const existingLogs = localStorage.getItem('gitnexus_debug_logs'); - if (existingLogs) { - console.log('=== PREVIOUS DEBUG LOGS FOUND ==='); - const logs = JSON.parse(existingLogs); - logs.forEach((log: { timestamp: string; message: string; data?: string }, index: number) => { - console.log(`Log ${index + 1}:`, log.timestamp, log.message, log.data || ''); - }); - console.log('=== END PREVIOUS LOGS ==='); - } - - logToPersistent('HomePage component mounted'); - - // Cleanup function to detect unmounting - return () => { - logToPersistent('HomePage component UNMOUNTING - this might explain the refresh!'); - }; - }, []); // No dependencies! - - // Get services singleton - const services = getOrCreateServices(state.githubToken); - - // Global error handlers - useEffect(() => { - // Check for previous errors on load - const previousError = localStorage.getItem('gitnexus_last_error'); - if (previousError) { - console.error('=== PREVIOUS ERROR FOUND ==='); - console.error(previousError); - localStorage.removeItem('gitnexus_last_error'); + const renderMainInterface = () => { + // Double-check graph validity before rendering + if (!isGraphValid) { + console.warn('Attempted to render main interface with invalid graph:', state.graph); + return renderWelcomeScreen(); } - const handleError = (event: ErrorEvent) => { - const errorInfo = { - timestamp: new Date().toISOString(), - message: event.message, - filename: event.filename, - lineno: event.lineno, - colno: event.colno, - error: event.error?.toString(), - stack: event.error?.stack - }; - - console.error('=== GLOBAL ERROR CAUGHT ==='); - console.error('Error info:', errorInfo); - - // Store error in localStorage for debugging - localStorage.setItem('gitnexus_last_error', JSON.stringify(errorInfo)); - - // Prevent default behavior (page reload) - event.preventDefault(); - - // Use setState directly to avoid dependency issues - setState(prev => ({ - ...prev, - error: `Unexpected error: ${event.message}`, - isProcessing: false, - progress: null - })); - }; - - const handleUnhandledRejection = (event: PromiseRejectionEvent) => { - const rejectionInfo = { - timestamp: new Date().toISOString(), - reason: event.reason?.toString(), - stack: event.reason?.stack - }; - - console.error('=== UNHANDLED PROMISE REJECTION ==='); - console.error('Rejection info:', rejectionInfo); - - // Store rejection in localStorage for debugging - localStorage.setItem('gitnexus_last_error', JSON.stringify(rejectionInfo)); - - // Prevent default behavior - event.preventDefault(); - - const errorMessage = event.reason instanceof Error - ? event.reason.message - : String(event.reason); - - // Use setState directly to avoid dependency issues - setState(prev => ({ - ...prev, - error: `Unhandled promise rejection: ${errorMessage}`, - isProcessing: false, - progress: null - })); - }; - - // Add global error listeners - window.addEventListener('error', handleError); - window.addEventListener('unhandledrejection', handleUnhandledRejection); - - // Cleanup - return () => { - window.removeEventListener('error', handleError); - window.removeEventListener('unhandledrejection', handleUnhandledRejection); - }; - }, []); // No dependencies! - - // Initialize RAG context when graph changes - useEffect(() => { - logToPersistent('RAG context effect triggered', { - hasGraph: !!state.graph, - fileContentsSize: state.fileContents.size, - hasApiKey: !!state.llmApiKey - }); - - if (state.graph && state.fileContents.size > 0 && state.llmApiKey) { - const llmConfig: LLMConfig = { - provider: state.llmProvider, - apiKey: state.llmApiKey, - model: state.llmModel, - temperature: 0.1, - maxTokens: 4000 - }; - - logToPersistent('Setting RAG context'); - services.rag.setContext({ - graph: state.graph, - fileContents: state.fileContents - }, llmConfig).catch(error => { - logToPersistent('Failed to initialize RAG context', error); - console.error('Failed to initialize RAG context:', error); - }); - } - }, [state.graph, state.fileContents, state.llmApiKey, state.llmProvider, state.llmModel, services.rag]); // Removed logToPersistent dependency - - // Validate GitHub URL - const validateGitHubUrl = useCallback((url: string): { isValid: boolean; error?: string } => { - if (!url.trim()) { - return { isValid: false, error: 'Please enter a GitHub repository URL' }; - } - - const githubUrlPattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)(?:\/.*)?$/; - const match = url.match(githubUrlPattern); - - if (!match) { - return { isValid: false, error: 'Please enter a valid GitHub repository URL (e.g., https://github.com/owner/repo)' }; - } - - return { isValid: true }; - }, []); - - // Extract owner and repo from GitHub URL - const parseGitHubUrl = useCallback((url: string): { owner: string; repo: string } | null => { - const githubUrlPattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)(?:\/.*)?$/; - const match = url.match(githubUrlPattern); - - if (match) { - return { owner: match[1], repo: match[2] }; - } - - return null; - }, []); - - // Filter files based on directory and pattern filters - const filterFiles = useCallback((files: GitHubFile[]): GitHubFile[] => { - let filtered = files; - - // Filter by directory - if (state.directoryFilter.trim()) { - const dirPattern = state.directoryFilter.toLowerCase(); - filtered = filtered.filter(file => - file.path.toLowerCase().includes(dirPattern) - ); - } - - // Filter by file pattern - if (state.filePatternFilter.trim()) { - const patterns = state.filePatternFilter.split(',').map(p => p.trim().toLowerCase()); - filtered = filtered.filter(file => - patterns.some(pattern => { - if (pattern.includes('*')) { - const regex = new RegExp(pattern.replace(/\*/g, '.*')); - return regex.test(file.path.toLowerCase()); - } - return file.path.toLowerCase().includes(pattern); - }) - ); - } - - return filtered; - }, [state.directoryFilter, state.filePatternFilter]); - - // Process GitHub repository - const processGitHubRepository = useCallback(async () => { - const validation = validateGitHubUrl(state.githubUrl); - if (!validation.isValid) { - updateState({ error: validation.error }); - return; - } - - const parsed = parseGitHubUrl(state.githubUrl); - if (!parsed) { - updateState({ error: 'Invalid GitHub URL format' }); - return; - } - - updateState({ - isProcessing: true, - error: null, - progress: { phase: 'structure', message: 'Fetching repository...', progress: 0, timestamp: Date.now() } - }); - - try { - // Fetch repository contents - updateState({ - progress: { phase: 'structure', message: 'Fetching repository structure...', progress: 10, timestamp: Date.now() } - }); - - const allFiles = await services.github.getAllFilesRecursively(parsed.owner, parsed.repo); - const filteredFiles = filterFiles(allFiles); - - // Check if repository exceeds file limit - if (filteredFiles.length > state.maxFiles) { - setConfirmationData({ - files: filteredFiles, - projectName: `${parsed.owner}/${parsed.repo}` - }); - setShowConfirmation(true); - updateState({ isProcessing: false, progress: null }); - return; - } - - await processFilesFromGitHub(filteredFiles, `${parsed.owner}/${parsed.repo}`); - - } catch (error) { - console.error('GitHub processing error:', error); - updateState({ - error: error instanceof Error ? error.message : 'Failed to process GitHub repository', - isProcessing: false, - progress: null - }); - } - }, [state.githubUrl, state.githubToken, state.maxFiles, services.github, validateGitHubUrl, parseGitHubUrl, filterFiles, updateState]); - - // Process with ingestion worker - const processWithWorker = useCallback(async ( - fileContents: Map, - projectName: string, - filePaths: string[] - ) => { - try { - logToPersistent('STARTING WORKER PROCESSING', { - projectName, - fileCount: fileContents.size, - pathCount: filePaths.length - }); - - const worker = getIngestionWorker(); - logToPersistent('Worker obtained, initializing...'); - - await worker.initialize(); - logToPersistent('Worker initialized successfully'); - - // Set up progress callback - await worker.setProgressCallback((progress: IngestionProgress) => { - logToPersistent('Progress update', progress); - updateState({ progress }); - }); - logToPersistent('Progress callback set'); - - updateState({ - progress: { phase: 'parsing', message: 'Starting knowledge graph construction...', progress: 50, timestamp: Date.now() } - }); - - // Process repository - logToPersistent('Calling worker.processRepository...'); - const result = await worker.processRepository({ - projectRoot: '/', - projectName, - filePaths, - fileContents - }); - logToPersistent('Worker processing completed', { - success: result.success, - nodeCount: result.graph?.nodes?.length || 0, - relationshipCount: result.graph?.relationships?.length || 0 - }); - - if (result.success && result.graph) { - logToPersistent('Processing successful, creating stats...'); - - // Calculate stats - const stats: ProcessingStats = { - totalFiles: filePaths.length, - processedFiles: fileContents.size, - duration: result.duration || 0, - nodeCount: result.graph.nodes.length, - relationshipCount: result.graph.relationships.length, - projectName - }; - logToPersistent('Stats created', stats); - - logToPersistent('Updating application state...'); - updateState({ - graph: result.graph, - fileContents, - isProcessing: false, - progress: null, - error: null - }); - logToPersistent('Application state updated'); - - // Debug: Log graph data - console.log('=== GRAPH SET SUCCESSFULLY ==='); - console.log('Graph nodes:', result.graph.nodes.length); - console.log('Graph relationships:', result.graph.relationships.length); - console.log('First few nodes:', result.graph.nodes.slice(0, 3)); - console.log('================================'); - - setProcessingStats(stats); - logToPersistent('Processing stats set'); - - // Show success message briefly - setTimeout(() => { - logToPersistent('Showing success message'); - updateState({ - progress: { - phase: 'complete', - message: `Successfully processed ${stats.nodeCount} nodes and ${stats.relationshipCount} relationships!`, - progress: 100, - timestamp: Date.now() - } - }); - setTimeout(() => { - logToPersistent('Clearing success message'); - updateState({ progress: null }); - }, 3000); - }, 500); - - logToPersistent('WORKER PROCESSING COMPLETED SUCCESSFULLY'); - } else { - logToPersistent('Worker processing failed - no success or no graph', result); - throw new Error(result.error || 'Processing failed'); - } - - } catch (error) { - logToPersistent('WORKER PROCESSING ERROR', { - type: typeof error, - isError: error instanceof Error, - message: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : 'No stack trace' - }); - - updateState({ - error: error instanceof Error ? error.message : 'Failed to process with worker', - isProcessing: false, - progress: null - }); - logToPersistent('ERROR STATE UPDATED'); - } - }, [updateState, logToPersistent]); - - // Process files from GitHub (used by both normal processing and confirmation) - const processFilesFromGitHub = useCallback(async (files: GitHubFile[], projectName: string) => { - const fileContents = new Map(); - - try { - logToPersistent('STARTING GITHUB FILE PROCESSING', { - fileCount: files.length, - projectName - }); - - updateState({ - isProcessing: true, - progress: { phase: 'structure', message: `Found ${files.length} files. Downloading...`, progress: 20, timestamp: Date.now() } - }); - - // Memory optimization: Process files in smaller batches - const DOWNLOAD_BATCH_SIZE = 20; // Download 20 files at a time - let processedFiles = 0; - - for (let i = 0; i < files.length; i += DOWNLOAD_BATCH_SIZE) { - const batch = files.slice(i, i + DOWNLOAD_BATCH_SIZE); - - for (const file of batch) { - try { - logToPersistent(`Processing file ${processedFiles + 1}/${files.length}`, file.path); - - const parsed = parseGitHubUrl(state.githubUrl); - if (!parsed) { - logToPersistent('Failed to parse GitHub URL', state.githubUrl); - break; - } - - logToPersistent(`Downloading content for: ${file.path}`); - const content = await services.github.getFileContent(parsed.owner, parsed.repo, file.path); - if (content) { - // Memory optimization: Skip very large files - if (content.length > 1000000) { // Skip files larger than 1MB - logToPersistent(`Skipping large file (${content.length} chars): ${file.path}`); - } else { - fileContents.set(file.path, content); - logToPersistent(`Successfully downloaded: ${file.path}`, `${content.length} chars`); - } - } else { - logToPersistent(`No content received for: ${file.path}`); - } - processedFiles++; - - const progress = 20 + (processedFiles / files.length) * 30; - updateState({ - progress: { - phase: 'structure', - message: `Downloaded ${processedFiles}/${files.length} files...`, - progress, - timestamp: Date.now() - } - }); - } catch (error) { - logToPersistent(`Failed to download ${file.path}`, { - message: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : 'No stack' - }); - // Continue with other files instead of breaking - } - } - - // Small delay between batches to prevent overwhelming the API - await new Promise(resolve => setTimeout(resolve, 100)); - } - - logToPersistent('GITHUB FILE PROCESSING COMPLETED', { - downloadedFiles: fileContents.size, - totalSize: Array.from(fileContents.values()).reduce((sum, content) => sum + content.length, 0) - }); - - // Process with ingestion worker - logToPersistent('Calling processWithWorker...'); - await processWithWorker(fileContents, projectName, files.map(f => f.path)); - - } catch (error) { - logToPersistent('GITHUB FILE PROCESSING ERROR', { - type: typeof error, - message: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : 'No stack trace' - }); - - updateState({ - error: error instanceof Error ? error.message : 'Failed to process GitHub files', - isProcessing: false, - progress: null - }); - } - }, [state.githubUrl, services.github, parseGitHubUrl, updateState, processWithWorker, logToPersistent]); - - // Handle confirmation dialog - const handleConfirmLargeRepository = useCallback(async () => { - if (!confirmationData) return; - - setShowConfirmation(false); - - // Limit to maxFiles - const limitedFiles = confirmationData.files.slice(0, state.maxFiles); - await processFilesFromGitHub(limitedFiles, confirmationData.projectName); - - setConfirmationData(null); - }, [confirmationData, state.maxFiles, processFilesFromGitHub]); - - const handleCancelLargeRepository = useCallback(() => { - setShowConfirmation(false); - setConfirmationData(null); - updateState({ isProcessing: false, progress: null }); - }, [updateState]); - - // Process ZIP file - const processZipFile = useCallback(async () => { - if (!state.uploadedFile) { - updateState({ error: 'Please select a ZIP file to upload' }); - return; - } - - updateState({ - isProcessing: true, - error: null, - progress: { phase: 'structure', message: 'Extracting ZIP file...', progress: 0, timestamp: Date.now() } - }); - - try { - // Extract ZIP contents - updateState({ - progress: { phase: 'structure', message: 'Reading ZIP file...', progress: 10, timestamp: Date.now() } - }); - - const allFileContents = await services.zip.extractTextFiles(state.uploadedFile); - let fileContents = allFileContents; - let filePaths = Array.from(allFileContents.keys()); - - // Apply filters - if (state.directoryFilter.trim() || state.filePatternFilter.trim()) { - const filteredPaths = filePaths.filter(path => { - let matches = true; - - if (state.directoryFilter.trim()) { - matches = matches && path.toLowerCase().includes(state.directoryFilter.toLowerCase()); - } - - if (state.filePatternFilter.trim()) { - const patterns = state.filePatternFilter.split(',').map(p => p.trim().toLowerCase()); - matches = matches && patterns.some(pattern => { - if (pattern.includes('*')) { - const regex = new RegExp(pattern.replace(/\*/g, '.*')); - return regex.test(path.toLowerCase()); - } - return path.toLowerCase().includes(pattern); - }); - } - - return matches; - }); - - // Create filtered file contents map - fileContents = new Map(); - filteredPaths.forEach(path => { - const content = allFileContents.get(path); - if (content) { - fileContents.set(path, content); - } - }); - filePaths = filteredPaths; - } - - // Check file limit - if (filePaths.length > state.maxFiles) { - const projectName = state.uploadedFile.name.replace(/\.zip$/i, ''); - setConfirmationData({ - files: filePaths.map(path => ({ - name: path.split('/').pop() || path, - path, - sha: '', - size: 0, - url: '', - html_url: '', - git_url: '', - download_url: null, - type: 'file' as const - })), - projectName - }); - setShowConfirmation(true); - updateState({ isProcessing: false, progress: null }); - return; - } - - updateState({ - progress: { - phase: 'structure', - message: `Extracted ${filePaths.length} text files...`, - progress: 30, - timestamp: Date.now() - } - }); - - // Process with ingestion worker - const projectName = state.uploadedFile.name.replace(/\.zip$/i, ''); - await processWithWorker(fileContents, projectName, filePaths); - - } catch (error) { - console.error('ZIP processing error:', error); - updateState({ - error: error instanceof Error ? error.message : 'Failed to process ZIP file', - isProcessing: false, - progress: null - }); - } - }, [state.uploadedFile, state.maxFiles, state.directoryFilter, state.filePatternFilter, services.zip, updateState]); - - // Export graph - const handleExportGraph = useCallback(() => { - if (!state.graph) return; - - try { - exportAndDownloadGraph( - state.graph, - { - projectName: processingStats?.projectName, - includeMetadata: true, - prettyPrint: true - }, - state.fileContents, - processingStats ? { duration: processingStats.duration } : undefined - ); - } catch (error) { - updateState({ - error: error instanceof Error ? error.message : 'Failed to export graph' - }); - } - }, [state.graph, state.fileContents, processingStats, updateState]); - - // Calculate export size - const exportSize = useMemo(() => { - if (!state.graph) return null; - return calculateExportSize(state.graph, true, state.fileContents); - }, [state.graph, state.fileContents]); - - // Handle file upload - const handleFileUpload = useCallback((event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (file) { - if (!file.name.toLowerCase().endsWith('.zip')) { - updateState({ error: 'Please select a ZIP file' }); - return; - } - updateState({ uploadedFile: file, error: null }); - } - }, [updateState]); - - // Clear all data - const clearData = useCallback(() => { - setState(prev => ({ - ...prev, - graph: null, - fileContents: new Map(), - selectedNodeId: null, - isProcessing: false, - progress: null, - error: null, - githubUrl: '', - uploadedFile: null - })); - setProcessingStats(null); - }, []); - - // Get available models for current provider - const getAvailableModels = useCallback(() => { - return services.llm.getAvailableModels(state.llmProvider); - }, [services.llm, state.llmProvider]); - - // Validate API key - const isApiKeyValid = useMemo(() => { - if (!state.llmApiKey.trim()) return false; - return services.llm.validateApiKey(state.llmProvider, state.llmApiKey); - }, [services.llm, state.llmProvider, state.llmApiKey]); - - // Styles - const containerStyle: React.CSSProperties = { - display: 'flex', - flexDirection: 'column', - height: '100vh', - width: '100vw', - backgroundColor: '#f5f5f5', - fontFamily: 'system-ui, -apple-system, sans-serif', - overflow: 'hidden' - }; - - const headerStyle: React.CSSProperties = { - backgroundColor: '#fff', - borderBottom: '1px solid #ddd', - padding: '16px 24px', - boxShadow: '0 2px 4px rgba(0,0,0,0.1)' - }; - - const titleStyle: React.CSSProperties = { - fontSize: '24px', - fontWeight: '600', - color: '#333', - margin: '0 0 16px 0', - display: 'flex', - alignItems: 'center', - gap: '8px' - }; - - const inputRowStyle: React.CSSProperties = { - display: 'flex', - gap: '12px', - alignItems: 'center', - flexWrap: 'wrap' - }; - - const inputGroupStyle: React.CSSProperties = { - display: 'flex', - flexDirection: 'column', - gap: '4px' - }; - - const labelStyle: React.CSSProperties = { - fontSize: '12px', - color: '#666', - fontWeight: '500' - }; - - const inputStyle: React.CSSProperties = { - padding: '8px 12px', - border: '1px solid #ddd', - borderRadius: '4px', - fontSize: '14px', - minWidth: '200px' - }; - - const smallInputStyle: React.CSSProperties = { - ...inputStyle, - minWidth: '150px' - }; - - const buttonStyle = (variant: 'primary' | 'secondary' | 'danger' = 'primary'): React.CSSProperties => ({ - padding: '8px 16px', - border: 'none', - borderRadius: '4px', - fontSize: '14px', - fontWeight: '500', - cursor: 'pointer', - backgroundColor: variant === 'primary' ? '#007bff' : variant === 'danger' ? '#dc3545' : '#6c757d', - color: '#fff', - opacity: state.isProcessing ? 0.6 : 1, - transition: 'all 0.2s ease' - }); - - const mainContentStyle: React.CSSProperties = { - flex: 1, - display: 'flex', - gap: '16px', - padding: '16px', - overflow: 'hidden', - width: '100%', - height: '100%' - }; - - const leftPaneStyle: React.CSSProperties = { - flex: '1 1 60%', - display: 'flex', - flexDirection: 'column', - backgroundColor: '#fff', - borderRadius: '8px', - boxShadow: '0 2px 8px rgba(0,0,0,0.1)', - overflow: 'hidden', - minWidth: 0 - }; - - const rightPaneStyle: React.CSSProperties = { - flex: '1 1 40%', - display: 'flex', - flexDirection: 'column', - gap: '16px', - minWidth: 0 - }; - - const panelStyle: React.CSSProperties = { - backgroundColor: '#fff', - borderRadius: '8px', - boxShadow: '0 2px 8px rgba(0,0,0,0.1)', - overflow: 'hidden' - }; - - const errorStyle: React.CSSProperties = { - backgroundColor: '#f8d7da', - color: '#721c24', - padding: '12px 16px', - borderRadius: '4px', - margin: '8px 0', - fontSize: '14px' - }; - - const progressStyle: React.CSSProperties = { - backgroundColor: '#d1ecf1', - color: '#0c5460', - padding: '12px 16px', - borderRadius: '4px', - margin: '8px 0', - fontSize: '14px' - }; - - const statsStyle: React.CSSProperties = { - display: 'flex', - gap: '16px', - fontSize: '12px', - color: '#666', - marginTop: '8px', - flexWrap: 'wrap' - }; - - const emptyStateStyle: React.CSSProperties = { - flex: 1, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'column', - color: '#666', - fontSize: '16px', - width: '100%', - height: '100%' - }; - - const settingsModalStyle: React.CSSProperties = { - position: 'fixed', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: 'rgba(0,0,0,0.5)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - zIndex: 1000 - }; - - const settingsContentStyle: React.CSSProperties = { - backgroundColor: '#fff', - borderRadius: '8px', - padding: '24px', - maxWidth: '600px', - width: '90%', - maxHeight: '80vh', - overflow: 'auto' - }; - - const settingsGridStyle: React.CSSProperties = { - display: 'grid', - gridTemplateColumns: '1fr 1fr', - gap: '16px', - marginBottom: '16px' + return ( + <> + {/* Top Navbar */} +
+
+ πŸ” GitNexus + β€’ + {state.graph?.nodes.length || 0} nodes + β€’ + {state.graph?.relationships.length || 0} relationships +
+ +
+ + {/* Main Layout */} +
+ {/* Left Panel - Knowledge Graph */} +
+ +
+ + {/* Right Panel - Chat and Source */} +
+ {/* Chat Section */} +
+ {isApiKeyValid ? ( + + ) : ( +
+
πŸ”‘
+
Configure your API key to use the chat interface
+ +
+ )} +
+ + {/* Source Section */} +
+ +
+
+
+ + ); }; return ( -
- {/* Header */} -
-
- πŸ” - GitNexus - Code Knowledge Graph Explorer -
+
+ - {/* Processing Stats */} - {processingStats && ( -
- πŸ“ {processingStats.processedFiles}/{processingStats.totalFiles} files - πŸ”— {processingStats.nodeCount} nodes - πŸ“Š {processingStats.relationshipCount} relationships - ⏱️ {(processingStats.duration / 1000).toFixed(1)}s - {exportSize && πŸ’Ύ Export: {exportSize.sizeFormatted}} -
- )} -
+ {state.showWelcome || !isGraphValid ? renderWelcomeScreen() : renderMainInterface()} - {/* Main Content */} -
- {state.graph ? ( - <> - {/* Left Pane - Graph Visualization */} -
- +
+

Settings

+
+ + updateState({ llmApiKey: e.target.value })} + placeholder="sk-..." + style={styles.input} />
- - {/* Right Pane - Chat Interface */} -
-
- {isApiKeyValid ? ( - - ) : ( -
-
- πŸ”‘ -
-
Configure your API key in settings to use the chat interface
- -
- )} -
-
- - ) : ( - /* Empty State */ -
-
- πŸ” -
-
- Welcome to GitNexus -
-
- Analyze GitHub repositories or upload ZIP files to explore code structure, - relationships, and get AI-powered insights about your codebase. -
-
- Enter a GitHub URL or upload a ZIP file to get started -
-
-
Performance Tips:
-
β€’ Use directory filters to focus on specific parts of the codebase
-
β€’ File patterns like "*.py" or "*.js" help limit analysis scope
-
β€’ Large repositories ({state.maxFiles}+ files) will show a confirmation dialog
-
-
- )} -
- - {/* Settings Modal */} - {showSettings && ( -
setShowSettings(false)}> -
e.stopPropagation()}> -
-

Settings

+
-
- - {/* Performance Settings */} -
-

Performance Settings

-
- - updateState({ maxFiles: parseInt(e.target.value) || 500 })} - style={inputStyle} - /> -
- Large repositories will show a confirmation dialog if they exceed this limit -
-
-
- - {/* GitHub Settings */} -
-

GitHub Settings

-
- - updateState({ githubToken: e.target.value })} - placeholder="ghp_xxxxxxxxxxxx" - style={inputStyle} - /> -
- Increases rate limit from 60 to 5,000 requests per hour -
-
-
- - {/* LLM Settings */} -
-

AI Chat Settings

- -
-
- - -
- -
- - -
-
- -
- - updateState({ llmApiKey: e.target.value })} - placeholder={`Enter your ${services.llm.getProviderDisplayName(state.llmProvider)} API key`} - style={inputStyle} - /> -
- Your API key is stored locally and never sent to our servers -
-
-
- -
)} - - {/* Large Repository Confirmation Dialog */} -
);