From 4541468320bf8bd31ba09c2250525325c4c9431f Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Wed, 28 Jan 2026 13:14:16 +0530 Subject: [PATCH 1/2] process maps and funding.yml --- .github/FUNDING.yml | 3 + gitnexus/src/components/MermaidDiagram.tsx | 120 +++--- gitnexus/src/components/ProcessFlowModal.tsx | 245 +++++++++++ gitnexus/src/components/ProcessesPanel.tsx | 416 +++++++++++++++++++ gitnexus/src/components/RightPanel.tsx | 27 +- gitnexus/src/lib/mermaid-generator.ts | 158 +++++++ 6 files changed, 909 insertions(+), 60 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 gitnexus/src/components/ProcessFlowModal.tsx create mode 100644 gitnexus/src/components/ProcessesPanel.tsx create mode 100644 gitnexus/src/lib/mermaid-generator.ts diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..1f4e1b2c07 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: abhigyanpatwari diff --git a/gitnexus/src/components/MermaidDiagram.tsx b/gitnexus/src/components/MermaidDiagram.tsx index 23d43ea1bb..ddc29aac9f 100644 --- a/gitnexus/src/components/MermaidDiagram.tsx +++ b/gitnexus/src/components/MermaidDiagram.tsx @@ -1,31 +1,34 @@ import { useEffect, useRef, useState } from 'react'; import mermaid from 'mermaid'; -import { AlertTriangle, Maximize2, Minimize2 } from 'lucide-react'; +import { AlertTriangle, Maximize2 } from 'lucide-react'; +import { ProcessFlowModal } from './ProcessFlowModal'; +import type { ProcessData } from '../lib/mermaid-generator'; -// Initialize mermaid with dark theme +// Initialize mermaid with cyan theme matching ProcessFlowModal mermaid.initialize({ startOnLoad: false, - theme: 'dark', + maxTextSize: 900000, + theme: 'base', themeVariables: { - primaryColor: '#06b6d4', - primaryTextColor: '#e4e4ed', - primaryBorderColor: '#1e1e2a', - lineColor: '#3b3b54', - secondaryColor: '#1e1e2a', - tertiaryColor: '#0a0a10', - background: '#0a0a10', - mainBkg: '#0f0f18', - nodeBorder: '#3b3b54', - clusterBkg: '#1e1e2a', - titleColor: '#e4e4ed', - edgeLabelBackground: '#0f0f18', - nodeTextColor: '#e4e4ed', + primaryColor: '#1e293b', // node bg - slate + primaryTextColor: '#f1f5f9', + primaryBorderColor: '#22d3ee', // cyan + lineColor: '#94a3b8', + secondaryColor: '#1e293b', + tertiaryColor: '#0f172a', + mainBkg: '#1e293b', + nodeBorder: '#22d3ee', // cyan + clusterBkg: '#1e293b', + clusterBorder: '#475569', + titleColor: '#f1f5f9', + edgeLabelBackground: '#0f172a', }, flowchart: { curve: 'basis', padding: 15, nodeSpacing: 50, rankSpacing: 50, + htmlLabels: true, }, sequence: { actorMargin: 50, @@ -36,7 +39,7 @@ mermaid.initialize({ }, fontFamily: '"JetBrains Mono", "Fira Code", monospace', fontSize: 13, - suppressErrorRendering: true, // Prevent default error div appending + suppressErrorRendering: true, }); // Override the default error handler to prevent it from logging to UI @@ -51,7 +54,7 @@ interface MermaidDiagramProps { export const MermaidDiagram = ({ code }: MermaidDiagramProps) => { const containerRef = useRef(null); const [error, setError] = useState(null); - const [isExpanded, setIsExpanded] = useState(false); + const [showModal, setShowModal] = useState(false); const [svg, setSvg] = useState(''); useEffect(() => { @@ -84,6 +87,17 @@ export const MermaidDiagram = ({ code }: MermaidDiagramProps) => { return () => clearTimeout(timeoutId); }, [code]); + // Create a pseudo ProcessData for the modal (with custom rawMermaid property) + const processData: any = showModal ? { + id: 'ai-generated', + label: 'AI Generated Diagram', + processType: 'intra_community', + steps: [], // Empty - we'll render raw mermaid + edges: [], + clusters: [], + rawMermaid: code, // Pass raw mermaid code + } : null; + if (error) { return (
@@ -105,49 +119,39 @@ export const MermaidDiagram = ({ code }: MermaidDiagramProps) => { } return ( -
- {/* Backdrop for expanded view */} - {isExpanded && ( -
setIsExpanded(false)} - /> - )} - -
- {/* Header */} -
- - Diagram - - + +
+ + {/* Diagram container */} +
+
- {/* Diagram container */} -
setShowModal(false)} /> -
-
+ )} + ); }; - diff --git a/gitnexus/src/components/ProcessFlowModal.tsx b/gitnexus/src/components/ProcessFlowModal.tsx new file mode 100644 index 0000000000..d7da4feff6 --- /dev/null +++ b/gitnexus/src/components/ProcessFlowModal.tsx @@ -0,0 +1,245 @@ +/** + * Process Flow Modal + * + * Displays a Mermaid flowchart for a process in a centered modal popup. + */ + +import { useEffect, useRef, useCallback, useState } from 'react'; +import { X, GitBranch, Copy, Focus, Layers } from 'lucide-react'; +import mermaid from 'mermaid'; +import { ProcessData, generateProcessMermaid } from '../lib/mermaid-generator'; + +interface ProcessFlowModalProps { + process: ProcessData | null; + onClose: () => void; + onFocusInGraph?: (nodeIds: string[]) => void; +} + +// Initialize mermaid with cyan/purple theme matching GitNexus +// Initialize mermaid with cyan/purple theme matching GitNexus +mermaid.initialize({ + startOnLoad: false, + suppressErrorRendering: true, // Try to suppress if supported + maxTextSize: 900000, // Increase from default 50000 to handle large combined diagrams + theme: 'base', + themeVariables: { + primaryColor: '#1e293b', // node bg + primaryTextColor: '#f1f5f9', + primaryBorderColor: '#22d3ee', + lineColor: '#94a3b8', + secondaryColor: '#1e293b', + tertiaryColor: '#0f172a', + mainBkg: '#1e293b', // background + nodeBorder: '#22d3ee', + clusterBkg: '#1e293b', + clusterBorder: '#475569', + titleColor: '#f1f5f9', + edgeLabelBackground: '#0f172a', + }, + flowchart: { + curve: 'basis', + padding: 20, + nodeSpacing: 50, + rankSpacing: 60, + htmlLabels: true, + }, +}); + +// Suppress distinct syntax error overlay +mermaid.parseError = (err) => { + // Suppress visual error - we handle errors in the render try/catch + console.debug('Mermaid parse error (suppressed):', err); +}; + +export const ProcessFlowModal = ({ process, onClose, onFocusInGraph }: ProcessFlowModalProps) => { + const containerRef = useRef(null); + const diagramRef = useRef(null); + const scrollContainerRef = useRef(null); + const [zoom, setZoom] = useState(1); + const [pan, setPan] = useState({ x: 0, y: 0 }); + const [isPanning, setIsPanning] = useState(false); + const [panStart, setPanStart] = useState({ x: 0, y: 0 }); + + // Handle zoom with scroll wheel + useEffect(() => { + const handleWheel = (e: WheelEvent) => { + e.preventDefault(); + const delta = e.deltaY * -0.001; + setZoom(prev => Math.min(Math.max(0.1, prev + delta), 5)); + }; + + const container = scrollContainerRef.current; + if (container) { + container.addEventListener('wheel', handleWheel, { passive: false }); + return () => container.removeEventListener('wheel', handleWheel); + } + }, []); + + // Handle pan with mouse drag + const handleMouseDown = useCallback((e: React.MouseEvent) => { + setIsPanning(true); + setPanStart({ x: e.clientX - pan.x, y: e.clientY - pan.y }); + }, [pan]); + + const handleMouseMove = useCallback((e: React.MouseEvent) => { + if (!isPanning) return; + setPan({ x: e.clientX - panStart.x, y: e.clientY - panStart.y }); + }, [isPanning, panStart]); + + const handleMouseUp = useCallback(() => { + setIsPanning(false); + }, []); + + const resetView = useCallback(() => { + setZoom(1); + setPan({ x: 0, y: 0 }); + }, []); + + // Render mermaid diagram + useEffect(() => { + if (!process || !diagramRef.current) return; + + const renderDiagram = async () => { + try { + // Check if we have raw mermaid code (from AI chat) or need to generate it + const mermaidCode = (process as any).rawMermaid + ? (process as any).rawMermaid + : generateProcessMermaid(process); + const id = `mermaid-${Date.now()}`; + + // Clear previous content + diagramRef.current!.innerHTML = ''; + + const { svg } = await mermaid.render(id, mermaidCode); + diagramRef.current!.innerHTML = svg; + } catch (error) { + console.error('Mermaid render error:', error); + const errorMessage = error instanceof Error ? error.message : String(error); + const isSizeError = errorMessage.includes('Maximum') || errorMessage.includes('exceeded'); + + diagramRef.current!.innerHTML = ` +
+
+ ${isSizeError ? '📊 Diagram Too Large' : '⚠️ Render Error'} +
+
+ ${isSizeError + ? `This diagram has ${process.steps?.length || 0} steps and is too complex to render. Try viewing individual processes instead of "All Processes".` + : `Unable to render diagram. Steps: ${process.steps?.length || 0}` + } +
+
+ `; + } + }; + + renderDiagram(); + }, [process]); + + // Close on escape + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', handleEscape); + return () => window.removeEventListener('keydown', handleEscape); + }, [onClose]); + + // Close on backdrop click + const handleBackdropClick = useCallback((e: React.MouseEvent) => { + if (e.target === containerRef.current) { + onClose(); + } + }, [onClose]); + + // Copy mermaid code to clipboard + const handleCopyMermaid = useCallback(async () => { + if (!process) return; + const mermaidCode = generateProcessMermaid(process); + await navigator.clipboard.writeText(mermaidCode); + }, [process]); + + // Focus in graph + const handleFocusInGraph = useCallback(() => { + if (!process || !onFocusInGraph) return; + const nodeIds = process.steps.map(s => s.id); + onFocusInGraph(nodeIds); + onClose(); + }, [process, onFocusInGraph, onClose]); + + if (!process) return null; + + return ( +
+ {/* Glassmorphism Modal */} +
+ {/* Subtle gradient overlay for extra glass feel */} +
+ + {/* Header */} +
+

+ Process: {process.label} +

+
+ + {/* Diagram */} +
+
+
+ + {/* Footer Actions */} +
+ + {onFocusInGraph && ( + + )} + + +
+
+
+ ); +}; diff --git a/gitnexus/src/components/ProcessesPanel.tsx b/gitnexus/src/components/ProcessesPanel.tsx new file mode 100644 index 0000000000..7a05c1f393 --- /dev/null +++ b/gitnexus/src/components/ProcessesPanel.tsx @@ -0,0 +1,416 @@ +/** + * Processes Panel + * + * Lists all detected processes grouped by type (cross-community / intra-community). + * Clicking a process opens the ProcessFlowModal with a flowchart. + */ + +import { useState, useMemo, useCallback, useEffect } from 'react'; +import { GitBranch, Search, Eye, Zap, Home, ChevronDown, ChevronRight, Sparkles } from 'lucide-react'; +import { useAppState } from '../hooks/useAppState'; +import { ProcessFlowModal } from './ProcessFlowModal'; +import type { ProcessData, ProcessStep } from '../lib/mermaid-generator'; + +export const ProcessesPanel = () => { + const { graph, runQuery, setHighlightedNodeIds, highlightedNodeIds } = useAppState(); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedProcess, setSelectedProcess] = useState(null); + const [expandedSections, setExpandedSections] = useState>(new Set(['cross', 'intra'])); + const [loadingProcess, setLoadingProcess] = useState(null); + + // Extract processes from graph + const processes = useMemo(() => { + if (!graph) return { cross: [], intra: [] }; + + const processNodes = graph.nodes.filter(n => n.label === 'Process'); + + const cross: Array<{ id: string; label: string; stepCount: number; clusters: string[] }> = []; + const intra: Array<{ id: string; label: string; stepCount: number; clusters: string[] }> = []; + + for (const node of processNodes) { + const item = { + id: node.id, + label: node.properties.heuristicLabel || node.properties.name || node.id, + stepCount: node.properties.stepCount || 0, + clusters: node.properties.communities || [], + }; + + if (node.properties.processType === 'cross_community') { + cross.push(item); + } else { + intra.push(item); + } + } + + // Sort by step count (most complex first) + cross.sort((a, b) => b.stepCount - a.stepCount); + intra.sort((a, b) => b.stepCount - a.stepCount); + + return { cross, intra }; + }, [graph]); + + // Filter by search + const filteredProcesses = useMemo(() => { + if (!searchQuery.trim()) return processes; + + const query = searchQuery.toLowerCase(); + return { + cross: processes.cross.filter(p => p.label.toLowerCase().includes(query)), + intra: processes.intra.filter(p => p.label.toLowerCase().includes(query)), + }; + }, [processes, searchQuery]); + + // Toggle section expansion + const toggleSection = useCallback((section: string) => { + setExpandedSections(prev => { + const next = new Set(prev); + if (next.has(section)) { + next.delete(section); + } else { + next.add(section); + } + return next; + }); + }, []); + + // Load process steps and open modal + const handleViewProcess = useCallback(async (processId: string, label: string, processType: string) => { + setLoadingProcess(processId); + + try { + // Query for process steps + const stepsQuery = ` + MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process {id: '${processId.replace(/'/g, "''")}'}) + RETURN s.id AS id, s.name AS name, s.filePath AS filePath, r.step AS stepNumber + ORDER BY r.step + `; + + const stepsResult = await runQuery(stepsQuery); + + const steps: ProcessStep[] = stepsResult.map((row: any) => ({ + id: row.id || row[0], + name: row.name || row[1] || 'Unknown', + filePath: row.filePath || row[2], + stepNumber: row.stepNumber || row.step || row[3] || 0, + })); + + // Get step IDs for edge query + const stepIds = steps.map(s => s.id); + + // Query for CALLS edges between the steps in this process + let edges: Array<{ from: string; to: string; type: string }> = []; + if (stepIds.length > 0) { + const edgesQuery = ` + MATCH (from)-[r:CodeRelation {type: 'CALLS'}]->(to) + WHERE from.id IN [${stepIds.map(id => `'${id.replace(/'/g, "''")}'`).join(',')}] + AND to.id IN [${stepIds.map(id => `'${id.replace(/'/g, "''")}'`).join(',')}] + RETURN from.id AS fromId, to.id AS toId, r.type AS type + `; + + try { + const edgesResult = await runQuery(edgesQuery); + edges = edgesResult + .map((row: any) => ({ + from: row.fromId || row[0], + to: row.toId || row[1], + type: row.type || row[2] || 'CALLS', + })) + .filter(edge => edge.from !== edge.to); // Remove self-loops + } catch (err) { + console.warn('Could not fetch edges:', err); + // Continue with empty edges - will fallback to linear + } + } + + // Get clusters for this process + const processNode = graph?.nodes.find(n => n.id === processId); + const clusters = processNode?.properties.communities || []; + + const processData: ProcessData = { + id: processId, + label, + processType: processType as 'cross_community' | 'intra_community', + steps, + edges, + clusters, + }; + + setSelectedProcess(processData); + } catch (error) { + console.error('Failed to load process steps:', error); + } finally { + setLoadingProcess(null); + } + }, [runQuery, graph]); + + // Load ALL processes and combine into one mega-diagram + const handleViewAllProcesses = useCallback(async () => { + setLoadingProcess('all'); + + try { + const allProcessIds = [...processes.cross, ...processes.intra].map(p => p.id); + + if (allProcessIds.length === 0) return; + + // Collect all steps from all processes + const allStepsMap = new Map(); + const allEdges: Array<{ from: string; to: string; type: string }> = []; + const processColors: Map = new Map(); + + for (let i = 0; i < allProcessIds.length; i++) { + const processId = allProcessIds[i]; + processColors.set(processId, i); + + // Query steps for this process + const stepsQuery = ` + MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process {id: '${processId.replace(/'/g, "''")}'}) + RETURN s.id AS id, s.name AS name, s.filePath AS filePath, r.step AS stepNumber + `; + + const stepsResult = await runQuery(stepsQuery); + + for (const row of stepsResult) { + const stepId = row.id || row[0]; + if (!allStepsMap.has(stepId)) { + allStepsMap.set(stepId, { + id: stepId, + name: row.name || row[1] || 'Unknown', + filePath: row.filePath || row[2], + stepNumber: row.stepNumber || row.step || row[3] || 0, + }); + } + } + } + + const allSteps = Array.from(allStepsMap.values()); + const stepIds = allSteps.map(s => s.id); + + // Query for all CALLS edges between the combined steps + if (stepIds.length > 0) { + const edgesQuery = ` + MATCH (from)-[r:CodeRelation {type: 'CALLS'}]->(to) + WHERE from.id IN [${stepIds.map(id => `'${id.replace(/'/g, "''")}'`).join(',')}] + AND to.id IN [${stepIds.map(id => `'${id.replace(/'/g, "''")}'`).join(',')}] + RETURN from.id AS fromId, to.id AS toId, r.type AS type + `; + + try { + const edgesResult = await runQuery(edgesQuery); + allEdges.push(...edgesResult + .map((row: any) => ({ + from: row.fromId || row[0], + to: row.toId || row[1], + type: row.type || row[2] || 'CALLS', + })) + .filter(edge => edge.from !== edge.to)); + } catch (err) { + console.warn('Could not fetch combined edges:', err); + } + } + + const combinedProcessData: ProcessData = { + id: 'combined-all', + label: `All Processes (${allProcessIds.length} combined)`, + processType: 'cross_community', + steps: allSteps, + edges: allEdges, + clusters: [], + }; + + setSelectedProcess(combinedProcessData); + } catch (error) { + console.error('Failed to load combined processes:', error); + } finally { + setLoadingProcess(null); + } + }, [processes, runQuery]); + + // Focus in graph callback - toggles highlight + const handleFocusInGraph = useCallback((nodeIds: string[]) => { + // Check if all these nodes are already highlighted + const allAlreadyHighlighted = nodeIds.every(id => highlightedNodeIds.has(id)) + && highlightedNodeIds.size === nodeIds.length; + + if (allAlreadyHighlighted) { + // Clear if already highlighted + setHighlightedNodeIds(new Set()); + } else { + // Highlight if not + setHighlightedNodeIds(new Set(nodeIds)); + } + }, [highlightedNodeIds, setHighlightedNodeIds]); + + const totalCount = processes.cross.length + processes.intra.length; + + // Auto-show combined diagram when panel first loads + useEffect(() => { + if (totalCount > 0 && !selectedProcess && loadingProcess === null) { + // Auto-trigger view all on first load + handleViewAllProcesses(); + } + }, [totalCount]); // Only run when totalCount changes from 0 + + if (totalCount === 0) { + return ( +
+
+ +
+

No Processes Detected

+

+ Processes are execution flows traced from entry points. Load a codebase to see detected processes. +

+
+ ); + } + + return ( +
+ {/* Header with search */} +
+
+
+ + setSearchQuery(e.target.value)} + placeholder="Filter processes..." + className="flex-1 bg-transparent border-none outline-none text-sm text-text-primary placeholder:text-text-muted" + /> +
+ +
+
+ {totalCount} processes detected +
+
+ + {/* Process list */} +
+ {/* Cross-Community Section */} + {filteredProcesses.cross.length > 0 && ( +
+ + + {expandedSections.has('cross') && ( +
+ {filteredProcesses.cross.map((process) => ( + handleViewProcess(process.id, process.label, 'cross_community')} + /> + ))} +
+ )} +
+ )} + + {/* Intra-Community Section */} + {filteredProcesses.intra.length > 0 && ( +
+ + + {expandedSections.has('intra') && ( +
+ {filteredProcesses.intra.map((process) => ( + handleViewProcess(process.id, process.label, 'intra_community')} + /> + ))} +
+ )} +
+ )} +
+ + {/* Modal */} + setSelectedProcess(null)} + onFocusInGraph={handleFocusInGraph} + /> +
+ ); +}; + +// Individual process item +interface ProcessItemProps { + process: { id: string; label: string; stepCount: number; clusters: string[] }; + isLoading: boolean; + onView: () => void; +} + +const ProcessItem = ({ process, isLoading, onView }: ProcessItemProps) => { + return ( +
+ +
+
{process.label}
+
+ {process.stepCount} steps + {process.clusters.length > 0 && ( + <> + + {process.clusters.length} clusters + + )} +
+
+ +
+ ); +}; diff --git a/gitnexus/src/components/RightPanel.tsx b/gitnexus/src/components/RightPanel.tsx index 73022cd74d..f9b405d797 100644 --- a/gitnexus/src/components/RightPanel.tsx +++ b/gitnexus/src/components/RightPanel.tsx @@ -1,13 +1,14 @@ import { useState, useRef, useEffect, useCallback } from 'react'; import { Send, Sparkles, User, - PanelRightClose, Loader2, AlertTriangle, Activity + PanelRightClose, Loader2, AlertTriangle, Activity, GitBranch } from 'lucide-react'; import { useAppState } from '../hooks/useAppState'; import { ToolCallCard } from './ToolCallCard'; import { isProviderConfigured } from '../core/llm/settings-service'; import { ActivityFeed } from './ActivityFeed'; import { MarkdownRenderer } from './MarkdownRenderer'; +import { ProcessesPanel } from './ProcessesPanel'; export const RightPanel = () => { const { isRightPanelOpen, @@ -27,7 +28,7 @@ export const RightPanel = () => { } = useAppState(); const [chatInput, setChatInput] = useState(''); - const [activeTab, setActiveTab] = useState<'chat' | 'activity'>('chat'); + const [activeTab, setActiveTab] = useState<'chat' | 'activity' | 'processes'>('chat'); const textareaRef = useRef(null); const messagesEndRef = useRef(null); @@ -234,6 +235,21 @@ export const RightPanel = () => { Activity + + {/* Processes Tab */} +
{/* Close button */} @@ -253,6 +269,13 @@ export const RightPanel = () => {
)} + {/* Processes Tab */} + {activeTab === 'processes' && ( +
+ +
+ )} + {/* Chat Content - only show when chat tab is active */} {activeTab === 'chat' && (
diff --git a/gitnexus/src/lib/mermaid-generator.ts b/gitnexus/src/lib/mermaid-generator.ts new file mode 100644 index 0000000000..f5a56e865a --- /dev/null +++ b/gitnexus/src/lib/mermaid-generator.ts @@ -0,0 +1,158 @@ +/** + * Mermaid Diagram Generator for Processes + * + * Generates Mermaid flowchart syntax from Process step data. + * Designed to show branching/merging when CALLS edges exist between steps. + */ + +export interface ProcessStep { + id: string; + name: string; + filePath?: string; + stepNumber: number; + cluster?: string; +} + +export interface ProcessEdge { + from: string; + to: string; + type: string; +} + +export interface ProcessData { + id: string; + label: string; + processType: 'intra_community' | 'cross_community'; + steps: ProcessStep[]; + edges?: ProcessEdge[]; // CALLS edges between steps for branching + clusters?: string[]; +} + +/** + * Generate Mermaid flowchart from process data + */ +export function generateProcessMermaid(process: ProcessData): string { + const { steps, edges, clusters } = process; + + if (!steps || steps.length === 0) { + return 'graph TD\n A[No steps found]'; + } + + const lines: string[] = ['graph TD']; + + // Add class definitions for styling (rounded corners + colors) + lines.push(' %% Styles'); + lines.push(' classDef default fill:#1e293b,stroke:#94a3b8,stroke-width:1px,color:#f8fafc,rx:10,ry:10;'); + lines.push(' classDef entry fill:#1e293b,stroke:#34d399,stroke-width:3px,color:#f8fafc,rx:10,ry:10;'); + lines.push(' classDef step fill:#1e293b,stroke:#22d3ee,stroke-width:2px,color:#f8fafc,rx:10,ry:10;'); + lines.push(' classDef terminal fill:#1e293b,stroke:#f472b6,stroke-width:3px,color:#f8fafc,rx:10,ry:10;'); + lines.push(' classDef cluster fill:#0f172a,stroke:#334155,stroke-width:1px,color:#94a3b8,rx:4,ry:4;'); + + // Track clusters for subgraph grouping + const clusterGroups = new Map(); + const noCluster: ProcessStep[] = []; + + for (const step of steps) { + if (step.cluster) { + const group = clusterGroups.get(step.cluster) || []; + group.push(step); + clusterGroups.set(step.cluster, group); + } else { + noCluster.push(step); + } + } + + // Generate node IDs (sanitized) - use actual ID to avoid collisions when combining processes + const nodeId = (step: ProcessStep) => { + // Sanitize the actual ID to be Mermaid-safe + return step.id.replace(/[^a-zA-Z0-9_]/g, '_'); + }; + const sanitizeLabel = (text: string) => text.replace(/["\[\]<>{}()]/g, '').substring(0, 30); + const getFileName = (path?: string) => path?.split('/').pop() || ''; + + // Determine node class (entry, terminal, or normal step) + const sortedStepsRef = [...steps].sort((a, b) => a.stepNumber - b.stepNumber); + const entryId = sortedStepsRef[0]?.id; + const terminalId = sortedStepsRef[sortedStepsRef.length - 1]?.id; + + const getNodeClass = (step: ProcessStep) => { + if (step.id === entryId) return 'entry'; + if (step.id === terminalId) return 'terminal'; + return 'step'; + }; + + // If we have cluster groupings and cross-community, use subgraphs + const useClusters = process.processType === 'cross_community' && clusterGroups.size > 1; + + if (useClusters) { + // Generate subgraphs for each cluster + let clusterIndex = 0; + + for (const [clusterName, clusterSteps] of clusterGroups) { + lines.push(` subgraph ${sanitizeLabel(clusterName)}["${sanitizeLabel(clusterName)}"]:::cluster`); + + for (const step of clusterSteps) { + const id = nodeId(step); + const label = `${step.stepNumber}. ${sanitizeLabel(step.name)}`; + const file = getFileName(step.filePath); + const className = getNodeClass(step); + lines.push(` ${id}["${label}
${file}"]:::${className}`); + } + lines.push(' end'); + clusterIndex++; + } + + // Add unclustered steps + for (const step of noCluster) { + const id = nodeId(step); + const label = `${step.stepNumber}. ${sanitizeLabel(step.name)}`; + const file = getFileName(step.filePath); + const className = getNodeClass(step); + lines.push(` ${id}["${label}
${file}"]:::${className}`); + } + } else { + // Simple flat layout + for (const step of steps) { + const id = nodeId(step); + const label = `${step.stepNumber}. ${sanitizeLabel(step.name)}`; + const file = getFileName(step.filePath); + const className = getNodeClass(step); + lines.push(` ${id}["${label}
${file}"]:::${className}`); + } + } + + // Generate edges + if (edges && edges.length > 0) { + // Use actual CALLS edges for branching + const stepById = new Map(steps.map(s => [s.id, s])); + for (const edge of edges) { + const fromStep = stepById.get(edge.from); + const toStep = stepById.get(edge.to); + if (fromStep && toStep) { + lines.push(` ${nodeId(fromStep)} --> ${nodeId(toStep)}`); + } + } + // Ensure all nodes are connected (fallback for disconnected components) + // For now assume graph is connected enough or user accepts fragments. + } else { + // Fallback: linear chain based on step order + const sortedSteps = [...steps].sort((a, b) => a.stepNumber - b.stepNumber); + for (let i = 0; i < sortedSteps.length - 1; i++) { + lines.push(` ${nodeId(sortedSteps[i])} --> ${nodeId(sortedSteps[i + 1])}`); + } + } + + return lines.join('\n'); +} + +/** + * Simple linear mermaid for quick preview + */ +export function generateSimpleMermaid(processLabel: string, stepCount: number): string { + const [entry, terminal] = processLabel.split(' → ').map(s => s.trim()); + + return `graph LR + classDef entry fill:#059669,stroke:#34d399,stroke-width:2px,color:#ffffff,rx:10,ry:10; + classDef terminal fill:#be185d,stroke:#f472b6,stroke-width:2px,color:#ffffff,rx:10,ry:10; + A["🟢 ${entry || 'Start'}"]:::entry --> B["... ${stepCount - 2} steps ..."] --> C["🔴 ${terminal || 'End'}"]:::terminal`; +} From f8b4a5c31fbffe7c9d8bb6a1d77c7f7181f03dc7 Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Thu, 29 Jan 2026 00:03:15 +0530 Subject: [PATCH 2/2] Process Map mermaid and other UI tweaks --- ARCHITECTURE.md | 584 +++++++++++++++++++ ARCHITECTURE_QUICK_REF.md | 375 ++++++++++++ GITNEXUS_ANALYSIS.md | 374 ++++++++++++ gitnexus/package-lock.json | 15 + gitnexus/package.json | 1 + gitnexus/src/components/GraphCanvas.tsx | 11 +- gitnexus/src/components/MCPToggle.tsx | 2 +- gitnexus/src/components/ProcessFlowModal.tsx | 88 ++- gitnexus/src/components/ProcessesPanel.tsx | 295 ++++++---- gitnexus/src/components/SettingsPanel.tsx | 102 +++- gitnexus/src/core/llm/agent.ts | 32 + gitnexus/src/core/llm/settings-service.ts | 49 ++ gitnexus/src/core/llm/types.ts | 21 +- gitnexus/src/hooks/useAppState.tsx | 8 +- gitnexus/src/lib/mermaid-generator.ts | 10 +- 15 files changed, 1834 insertions(+), 133 deletions(-) create mode 100644 ARCHITECTURE.md create mode 100644 ARCHITECTURE_QUICK_REF.md create mode 100644 GITNEXUS_ANALYSIS.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000000..94c4f72a9c --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,584 @@ +# PyBaMM Architecture - End-to-End Analysis + +## Executive Summary + +**PyBaMM** (Python Battery Mathematical Modelling) is a comprehensive, open-source framework for modeling and simulating battery behavior. The project contains **973 files**, **4,342 functions**, and **735 classes** organized in a layered architecture optimized for modularity, extensibility, and scientific computation. + +--- + +## 🏗️ High-Level Architecture Layers + +``` +┌─────────────────────────────────────────────────────────────┐ +│ USER INTERFACE & EXAMPLES │ +│ (Jupyter Notebooks, Scripts, Experiments) │ +└──────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────────────────┐ +│ SIMULATION & EXPERIMENT ORCHESTRATION LAYER │ +│ • Simulation - High-level simulation runner │ +│ • Experiment - Define charging/discharging cycles │ +│ • BatchStudy - Multi-parameter studies │ +│ • Callbacks - Monitor simulation progress │ +└──────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────────────────┐ +│ MODEL LAYER (Hierarchical) │ +├─ BaseBatteryModel (Physical domain constraints) │ +├─ Full Models: │ +│ ├─ Lithium-Ion (DFN, SPM, SPMe, MPM, MSMR, etc.) │ +│ ├─ Lead-Acid (Full, LOQS models) │ +│ ├─ Sodium-Ion (emerging battery chemistry) │ +│ └─ Equivalent Circuit Models (ECM) │ +├─ Submodels (Pluggable domain-specific components): │ +│ ├─ Particle Diffusion (kinetics in electrodes) │ +│ ├─ Electrode Kinetics (Butler-Volmer, Marcus, etc.) │ +│ ├─ Interface Chemistry (SEI growth, Li-plating, OCP) │ +│ ├─ Thermal Management (lumped, distributed 1D-3D) │ +│ ├─ Current Collector Physics │ +│ ├─ Electrolyte Transport (conductivity, diffusion) │ +│ ├─ Convection (internal circulation) │ +│ ├─ Porosity & Tortuosity (pore network) │ +│ └─ Active Material Loss (cycling degradation) │ +└──────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────────────────┐ +│ EXPRESSION TREE (Symbolic Computation Layer) │ +│ Directed Acyclic Graph (DAG) of mathematical expressions │ +├─ Symbol - Base class for all nodes │ +│ ├─ Variable - State vector entries │ +│ ├─ Parameter - Model parameters │ +│ ├─ Scalar/Array - Constants │ +│ ├─ StateVector - Discretized spatial domain │ +│ └─ InputParameter - Time-varying inputs │ +├─ Operators │ +│ ├─ BinaryOperators - +, -, *, /, power, etc. │ +│ ├─ UnaryOperators - exp, log, sin, cos, etc. │ +│ ├─ Concatenations - Stack vectors │ +│ └─ Broadcasts - Repeat/tile operations │ +└──────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────────────────┐ +│ DISCRETISATION LAYER (PDE → ODE/DAE Conversion) │ +│ Transforms continuous PDEs into discrete systems │ +├─ Discretisation - Master converter class │ +├─ Spatial Methods: │ +│ ├─ FiniteVolume - 1D/2D finite volume schemes │ +│ ├─ SpectralVolume - Spectral approach │ +│ ├─ ScikitFiniteElement - 1D unstructured meshes │ +│ ├─ ScikitFiniteElement3D- 3D tetrahedral meshes │ +│ └─ ZeroDimensionalMethod- Lumped (0D) approximations │ +├─ Meshes: │ +│ ├─ 1D Submeshes - Line domains │ +│ ├─ 2D Submeshes - Sheet domains │ +│ ├─ 3D Submeshes - Volume domains (via scikit-fem)│ +│ └─ Composite Meshes - Combined domains │ +└──────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────────────────┐ +│ SOLVER LAYER (DAE System Integration) │ +│ Converts discrete system → numerical solution │ +├─ Solver Interfaces: │ +│ ├─ BaseSolver - Abstract interface │ +│ ├─ ODE Solvers: │ +│ │ ├─ ScipySolver - scipy.integrate.ode │ +│ │ ├─ JAXSolver - JAX backend (jit-compiled) │ +│ │ ├─ JAXBDFSolver - JAX BDF method │ +│ │ └─ IDAKLUSolver - SUNDIALS IDA (C++ wrapper) │ +│ ├─ DAE Solvers: │ +│ │ ├─ CasadiSolver - CasADi symbolic optimization │ +│ │ ├─ IDakluJax - IDA + JAX hybrid │ +│ │ └─ AlgebraicSolver - Solve algebraic eqns only │ +│ └─ Special: │ +│ ├─ DummySolver - Testing/debugging │ +│ └─ Solution - Stores results + post-process │ +├─ Features: │ +│ ├─ Jacobian Computation - Auto diff or symbolic │ +│ ├─ Event Detection - Trigger on state changes │ +│ ├─ Callbacks - Hooks during integration │ +│ └─ Processed Variables - Post-compute derived quantities│ +└──────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────────────────┐ +│ PARAMETER & DATA LAYER │ +│ Manages model coefficients and experimental data │ +├─ ParameterValues - Substitutes symbols → numbers │ +├─ Parameter Sets: │ +│ ├─ Lithium-Ion Parameter Sets (Chen2020, OKane2022, etc)│ +│ ├─ Lead-Acid Parameter Sets (Sulzer2019) │ +│ ├─ Sodium-Ion Parameter Sets (Chayambuka2022) │ +│ └─ ECM Parameter Sets (voltage model coefficients) │ +├─ Special Parameters: │ +│ ├─ ElectricalParameters - Conductivity, diffusivity │ +│ ├─ ThermalParameters - Heat capacity, conductivity │ +│ ├─ GeometricParameters - Dimensions, areas, volumes │ +│ └─ ProcessParameterData - Fit to experimental results │ +└──────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────────────────┐ +│ VISUALIZATION & POST-PROCESSING │ +│ Analysis and interpretation of results │ +├─ Plotting Modules: │ +│ ├─ quick_plot() - 1-line quick visualization │ +│ ├─ plot() - Customizable plotting │ +│ ├─ plot_voltage_components() - Decompose voltage │ +│ ├─ plot_summary_variables() - Key metrics │ +│ ├─ plot_3d_heatmap() - 3D temperature fields │ +│ └─ plot_3d_cross_section() - 2D slices of 3D │ +├─ Dynamic Plotting: │ +│ └─ DynamicPlot - Live update during solving │ +└──────────────────────────────────────────────────────────────┘ +``` + +--- + +## 📊 Core Components Deep Dive + +### 1. **Expression Tree (Symbolic Computation)** + +**Purpose:** Represents mathematical expressions as a directed acyclic graph (DAG). + +**Key Classes:** + +``` +Symbol (Base Class) +├── Variable - Represents y(t), y_dot(t) +├── Parameter - Fixed model coefficients +├── Scalar/Array - Numerical constants +├── StateVector - Discretized spatial variables +├── InputParameter - Time-varying inputs (current, temperature) +│ +BinaryOperator +├── Addition/Subtraction +├── Multiplication/Division +├── Power +├── MatrixMultiplication +└── Equality (for algebraic equations) + +UnaryOperator +├── Exponential, Logarithm +├── Trigonometric (sin, cos, tan) +├── Sign, Absolute Value +└── Specialized (exp, log, cosh, etc.) +``` + +**Why This Matters:** +- Enables **symbolic differentiation** (Jacobian computation) +- **Backend-agnostic**: Same expression can be evaluated as Python, CasADi, or JAX code +- Supports **automatic code generation** for performance + +--- + +### 2. **Model Hierarchy** + +**Top Level: `BaseModel`** +- Holds empty RHS and algebraic equation dictionaries +- Manages variables, parameters, boundary conditions +- Coordinates discretisation and conversion + +**Next Level: `BaseBatteryModel`** +- Enforces battery-specific physics constraints +- Implements standard lifecycle: `build_model()` → `discretise()` → `solve()` + +**Bottom Level: Concrete Models (Plug-and-Play Architecture)** + +| Model | Type | Complexity | Use Case | +|-------|------|-----------|----------| +| **SPM** | Lithium-Ion | Simplest | Quick simulations, education | +| **SPMe** | Lithium-Ion | Medium | Semi-empirical electrolyte | +| **DFN** | Lithium-Ion | Complex | High accuracy, research | +| **MSMR** | Lithium-Ion | Very Complex | Multi-scale particle size dist. | +| **MPM** | Lithium-Ion | Complex | Mesoscale particle modeling | +| **Half-Cell** | Lithium-Ion | Custom | Single electrode testing | +| **Thermal Models** | Any | Adds complexity | Temperature effects | +| **ECM (Thevenin)** | Equivalent Circuit | Simple | Real-time estimation | + +**Submodel Pattern:** +``` +Full Models = Combination of pluggable submodels + +Example: DFN Model +├── Active Material (constant or loss) +├── Particle Diffusion (negative & positive electrodes) +├── Electrode Kinetics (interface reactions) +├── Open Circuit Potential (voltage lookup) +├── SEI Growth (lithium loss) +├── Current Collector (ohmic drop) +├── Convection (internal flow) +├── Thermal (heat generation & transfer) +└── External Circuit (boundary conditions) +``` + +--- + +### 3. **Discretisation Pipeline** + +**Convert PDEs → Finite-Dimensional ODEs/DAEs** + +``` +Physics-Based PDE + ↓ +[Spatial Method Selected: Finite Volume / Spectral / FEM] + ↓ +Mesh Generation (1D/2D/3D depending on model) + ↓ +Gradient/Divergence Operators Discretized + ↓ +Boundary Conditions Applied + ↓ +Expression Tree Converted (y → discretized vector) + ↓ +Final System: M*dy/dt = f(t,y) + g(t,y) = 0 [DAE form] +``` + +**Mesh Strategy:** +- **1D**: Uniform or non-uniform grids (electrodes, separator) +- **2D**: Cartesian or polar (pouch cell cross-sections) +- **3D**: Tetrahedral (scikit-fem), complex geometries + +--- + +### 4. **Solver Pipeline** + +**Goal:** Integrate DAE system over time + +**Solver Family:** +- **ScipySolver**: Reliable, well-tested, pure Python +- **CasadiSolver**: Symbolic optimization, slow but accurate +- **IDAKLUSolver**: C++ SUNDIALS, fastest +- **JAXSolver**: JIT-compiled, GPU-capable +- **IDakluJax**: Hybrid IDA + JAX + +**Key Features:** +- **Event Detection**: Stop when voltage hits limit +- **Jacobian**: Computed symbolically or via auto-diff +- **Callbacks**: Monitor state during integration +- **Mass Matrix**: Handle DAE systems with singular mass matrices + +--- + +### 5. **Parameter System** + +**Strategy:** Keep symbolic model separate from numerical values + +``` +Model Construction: + pybamm.Parameter("Conductivity") → generic symbol + ↓ + [Stored in expression tree] + ↓ +Before Solving: + parameter_values = pybamm.ParameterValues({ + "Conductivity": 1.23 # Numerical value + }) + parameter_values.process_model(model) + ↓ + All symbols substituted with values + ↓ + Ready to solve! +``` + +**Pre-built Parameter Sets:** +- **Lithium-Ion**: Chen2020, OKane2022, Ai2020, Ecker2015, ORegan2022 +- **Lead-Acid**: Sulzer2019 +- **Sodium-Ion**: Chayambuka2022 +- **ECM**: Thevenin model coefficients + +--- + +## 🔄 Execution Flow: From Model to Solution + +### Example: Simple SPM Simulation + +```python +import pybamm + +# Step 1: Create model +model = pybamm.lithium_ion.SPM() + +# Step 2: Define simulation +sim = pybamm.Simulation( + model, + parameter_values=pybamm.ParameterValues("Chen2020"), + solver=pybamm.IDAKLUSolver() +) + +# Step 3: Run +sim.solve([0, 3600]) # Solve 1 hour + +# Step 4: Plot +sim.plot() +``` + +**Behind the Scenes:** + +1. **Model Initialization** → Submodels concatenated +2. **Build Phase** → RHS, algebraic equations assembled +3. **Parameter Substitution** → Symbols replaced with values +4. **Discretisation** → Spatial PDE → ODE/DAE +5. **Jacobian Computation** → Auto-differentiation +6. **Solver Setup** → Initial conditions, events configured +7. **Integration Loop** → Time-stepping with callbacks +8. **Post-Processing** → Compute derived variables (impedance, etc.) +9. **Visualization** → Plot results + +--- + +## 🔗 Key Dependencies & Data Flow + +### Upstream (Inputs) +``` +Experiment (current profile) + ↓ +ParameterValues (physical constants) + ↓ +Geometry (cell dimensions) + ↓ +ModelOptions (choose submodels) + ↓ +BaseModel +``` + +### Downstream (Outputs) +``` +Discretisation + ↓ +DAE System (M*dy/dt = f(t,y)) + ↓ +Solver + ↓ +Solution object (t, y, processed_variables) + ↓ +Plotting/Analysis + ↓ +Results (voltage, capacity, temperature, etc.) +``` + +--- + +## 🌳 Hotspot Nodes (Most Connected Components) + +These are the "hubs" that everything depends on: + +| Node | Type | Connections | Role | +|------|------|-----------|------| +| `src/pybamm/__init__.py` | File | **500** | Central export hub | +| `Variable` | Class | **474** | Core state representation | +| `Scalar` | Class | **397** | Constant handling | +| `evaluate()` | Function | **344** | Expression evaluation | +| `solve()` | Function | **311** | Solver invocation | +| `BaseModel` | Class | **305** | Model parent | +| `Discretisation` | Class | **289** | Discretisation orchestration | +| `linspace()` | Function | **267** | Mesh generation | + +--- + +## 📁 Directory Structure + +``` +src/pybamm/ +├── models/ # Model hierarchy +│ ├── base_model.py # Abstract base +│ ├── full_battery_models/ # Concrete implementations +│ │ ├── lithium_ion/ +│ │ ├── lead_acid/ +│ │ ├── sodium_ion/ +│ │ └── equivalent_circuit/ +│ └── submodels/ # Pluggable physics components +│ ├── interface/ # Electrode kinetics, SEI, OCP +│ ├── particle/ # Particle diffusion +│ ├── thermal/ # Heat transfer +│ ├── electrode/ # Ohmic drop +│ ├── convection/ # Internal flow +│ └── [more...] +│ +├── expression_tree/ # Symbolic DAG +│ ├── symbol.py # Base class +│ ├── binary_operators.py # +, -, *, / +│ ├── unary_operators.py # sin, exp, log +│ ├── operations/ # Evaluation, Jacobian, serialization +│ └── [more...] +│ +├── discretisations/ # PDE → ODE conversion +│ └── discretisation.py +│ +├── spatial_methods/ # Finite volume, spectral, FEM +│ ├── finite_volume.py +│ ├── spectral_volume.py +│ └── [more...] +│ +├── meshes/ # Grid generation +│ ├── meshes.py +│ └── [submesh types...] +│ +├── solvers/ # DAE integration +│ ├── base_solver.py +│ ├── scipy_solver.py +│ ├── casadi_solver.py +│ ├── idaklu_solver.py +│ └── [more...] +│ +├── parameters/ # Physical coefficients +│ ├── base_parameters.py +│ ├── parameter_values.py +│ ├── lithium_ion_parameters.py +│ └── input/ +│ └── parameters/ # Pre-built parameter sets +│ +├── plotting/ # Visualization +│ ├── plot.py +│ ├── quick_plot.py +│ ├── plot_voltage_components.py +│ └── [more...] +│ +├── batch_study.py # Multi-parameter studies +├── simulation.py # High-level runner +├── experiment/ # Charge/discharge cycles +└── [more...] + +tests/ +├── unit/ # Isolated component tests +└── integration/ # End-to-end tests +``` + +--- + +## 🎯 Design Patterns + +### 1. **Plugin Architecture (Submodels)** +- Models are built by combining plug-and-play submodels +- Easy to swap implementations (e.g., different kinetics models) +- **Example**: Switch from Butler-Volmer to Marcus kinetics + +### 2. **Expression Tree Pattern** +- Decouple symbolic math from backend +- Same expression → Python, CasADi, or JAX code +- Enables automatic differentiation + +### 3. **Factory Pattern (Solvers)** +- `solve()` returns appropriate solver based on model type +- User doesn't need to know solver implementation details + +### 4. **Strategy Pattern (Spatial Methods)** +- Choose discretization strategy (FV, Spectral, FEM) at runtime +- Swap without changing model code + +### 5. **Template Method (Model Lifecycle)** +1. `model.build_model()` +2. `disc.discretise(model)` +3. `solver.solve(t_eval, y0)` + +--- + +## 🚀 Performance Considerations + +### Bottlenecks +1. **Discretisation**: Large spatial grids → huge state vectors +2. **Jacobian Computation**: Dense matrices for implicit solvers +3. **Parameter Substitution**: Re-expression tree traversal + +### Optimizations +1. **CasADi Backend**: Symbolic optimization + JIT +2. **JAX Solver**: GPU acceleration, batched derivatives +3. **IDA Solver**: C++ wrapper, sparse Jacobian support +4. **LRU Caching**: Avoid recomputation + +--- + +## 🔐 Testing Strategy + +### Unit Tests (973 files) +- Component-level validation +- Expression tree operations +- Spatial method correctness + +### Integration Tests +- Full model runs +- Solver convergence +- Different parameter sets + +### Benchmark Tests +- Performance tracking +- Memory profiling +- Scaling analysis + +--- + +## 📚 Key Math Concepts + +### Governing Equations +**DAE System:** +``` +M(t,y) * dy/dt = f(t, y, u(t)) [Differential equations] +0 = g(t, y, u(t)) [Algebraic equations] +``` + +where: +- `y` = state vector (concentrations, potentials, temperature) +- `u(t)` = inputs (applied current, ambient temperature) +- `M` = mass matrix (handles singular systems) + +### Typical Physics + +**Particle Diffusion (Fick's Law):** +``` +∂c/∂t = ∇·(D∇c) +``` + +**Charge Conservation (Poisson):** +``` +∇·(σ∇φ) = i +``` + +**Energy Balance (Heat Equation):** +``` +ρCp ∂T/∂t = ∇·(k∇T) + Q_gen +``` + +--- + +## 🎓 Learning Path + +1. **Start**: Run SPM model (`pybamm.lithium_ion.SPM()`) +2. **Progress**: Modify parameter set, change solver +3. **Intermediate**: Swap submodels (DFN, thermal) +4. **Advanced**: Create custom submodel +5. **Expert**: Implement new spatial method + +--- + +## 🔮 Architecture Strengths + +✅ **Modularity**: Plug-and-play submodels +✅ **Extensibility**: Easy to add new models/solvers +✅ **Physics-First**: Expression tree mirrors actual equations +✅ **Backend-Agnostic**: Switch solvers without changing model +✅ **Scientific Quality**: Validated against experiments +✅ **Performance**: Multiple backends (Python, C++, JAX) + +--- + +## ⚠️ Architecture Tradeoffs + +⚖️ **Complexity**: Large learning curve +⚖️ **Symbolic Overhead**: DAG construction has memory cost +⚖️ **Debug Difficulty**: Multiple abstraction layers +⚖️ **Startup Time**: Model compilation + discretisation + +--- + +## 🎯 Conclusion + +PyBaMM's architecture is a **layered, modular system** optimized for: +- **Scientific fidelity** (physics-based discretisation) +- **Extensibility** (plug-and-play submodels) +- **Performance** (multiple backends) +- **Usability** (high-level simulation API) + +The design cleanly separates concerns across 7 layers, from symbolic math to numerical solvers, making it suitable for both research and production use. + +--- + +*Analysis powered by GitNexus MCP - Code Intelligence Engine* + + diff --git a/ARCHITECTURE_QUICK_REF.md b/ARCHITECTURE_QUICK_REF.md new file mode 100644 index 0000000000..51f476ff56 --- /dev/null +++ b/ARCHITECTURE_QUICK_REF.md @@ -0,0 +1,375 @@ +# PyBaMM Architecture - Quick Reference + +## System Overview Diagram + +``` +USER CODE + │ + └─→ pybamm.Simulation(model, experiment, solver) + │ + ├─ model.build_model() [Assemble physics] + ├─ discretisation.discretise() [Convert PDE→ODE] + ├─ solver.solve(t_eval, y0) [Integrate] + └─ solution.plot() [Visualize] +``` + +## Dependency Hierarchy + +``` +HIGH-LEVEL (User-Facing) + ↓ +[Experiment] [ParameterValues] [Geometry] + ↓ ↓ ↓ +Simulation + ↓ +BaseBatteryModel (DFN, SPM, etc.) + ├─ Submodels (pluggable physics) + └─ Expression Tree (symbolic math) + ↓ +Discretisation (spatial methods) + ↓ +Solver (ODE/DAE integrator) + ↓ +LOW-LEVEL (Numerical computation) +``` + +## Model Hierarchy + +``` +BaseModel (Abstract) + ↓ +BaseBatteryModel (Battery-specific) + ├─ Lithium-Ion Models + │ ├─ SPM (Simplest) + │ ├─ SPMe (w/ electrolyte) + │ ├─ DFN (Most common) + │ ├─ MSMR (Multi-scale particle) + │ ├─ MPM (Mesoscale) + │ └─ Half-Cell (Single electrode) + │ + ├─ Lead-Acid Models + │ ├─ Full (Detailed) + │ └─ LOQS (Simplified) + │ + ├─ Sodium-Ion Models + │ └─ BasicDFN (DFN for Na-ion) + │ + └─ ECM (Equivalent Circuit) + └─ Thevenin (RC ladder) +``` + +## Submodel Categories + +``` +Full Models combine these pluggable components: + +├─ Particle Diffusion +│ ├─ Fickian Diffusion +│ ├─ MSMR (Multi-scale multi-reaction) +│ └─ Polynomial Profile +│ +├─ Interface Kinetics +│ ├─ Butler-Volmer +│ ├─ Marcus Theory +│ ├─ Linear Kinetics +│ └─ Diffusion-Limited +│ +├─ Open Circuit Potential +│ ├─ Single OCP +│ ├─ MSMR OCP +│ └─ Hysteresis Models +│ +├─ Solid-Electrolyte Interface +│ ├─ Constant SEI +│ ├─ SEI Growth +│ └─ No SEI +│ +├─ Electrode Physics +│ ├─ Ohmic Drop (various complexity levels) +│ ├─ Current Collector +│ └─ Active Material Loss +│ +├─ Electrolyte Transport +│ ├─ Conductivity (full, leading-order) +│ ├─ Diffusion +│ └─ Convection (internal flow) +│ +├─ Thermal Effects +│ ├─ Isothermal +│ ├─ Lumped (single temperature) +│ ├─ Distributed 1D/2D/3D +│ └─ Pouch Cell Specific +│ +└─ Other Physics + ├─ Porosity + ├─ Transport Efficiency + ├─ Particle Mechanics + └─ Lithium Plating +``` + +## Expression Tree Structure + +``` +Symbols (Leaf Nodes): +├─ Variable(y) → State vector entry +├─ Parameter(σ) → Model coefficient +├─ Scalar(3.14) → Constant +├─ StateVector → Discretized spatial grid +└─ InputParameter(I) → Time-varying current + +Operations (Internal Nodes): +├─ Binary: +, -, *, /, power +├─ Unary: exp, log, sin, cos +├─ Broadcast: repeat, reshape +└─ Concatenate: stack vectors + +Root: dy/dt = RHS_expression +``` + +## Discretisation Flow + +``` +Continuous Domains + ↓ [Choose spatial method] + ├─ Finite Volume (FV) + ├─ Spectral Volume (SV) + ├─ Finite Element (FEM) + └─ Zero-Dimensional (lumped) + ↓ [Generate mesh] + ├─ 1D: Uniform/non-uniform line + ├─ 2D: Cartesian/polar grid + └─ 3D: Tetrahedral (scikit-fem) + ↓ [Apply operators] + ├─ Gradient (∇) + ├─ Divergence (∇·) + └─ Laplacian (∇²) + ↓ [Apply boundary conditions] + ├─ Dirichlet (fixed value) + ├─ Neumann (fixed flux) + └─ Robin (mixed) + ↓ +Discrete DAE System: M*dy/dt = f(t,y) + g_alg(t,y) = 0 +``` + +## Solver Selection Strategy + +``` +Model Type → Solver Choice + +Algebraic only → AlgebraicSolver + +ODE only +├─ Speed priority → IDAKLUSolver (C++) +├─ Accuracy priority → CasadiSolver (symbolic) +├─ GPU available → JAXSolver (JIT) +└─ Portability → ScipySolver (pure Python) + +DAE (ODE + algebraic) +├─ Default → IDAKLUSolver +├─ Complex Jacobian → CasadiSolver +├─ Large-scale → IDAKLUSolver + JAX +└─ Testing → DummySolver +``` + +## Data Flow: Solve Pipeline + +``` +┌────────────────────────────────────────────────────┐ +│ 1. Model Specification │ +│ model = pybamm.lithium_ion.DFN() │ +└────────────┬───────────────────────────────────────┘ + │ +┌────────────▼───────────────────────────────────────┐ +│ 2. Parameter Assignment │ +│ param_vals.process_model(model) │ +│ [Symbols → Numbers] │ +└────────────┬───────────────────────────────────────┘ + │ +┌────────────▼───────────────────────────────────────┐ +│ 3. Build Model │ +│ model.build_model() │ +│ [Assemble RHS: m*dy/dt = f(t,y)] │ +└────────────┬───────────────────────────────────────┘ + │ +┌────────────▼───────────────────────────────────────┐ +│ 4. Discretisation │ +│ disc.discretise(model) │ +│ [Convert PDE → ODE using spatial method] │ +└────────────┬───────────────────────────────────────┘ + │ +┌────────────▼───────────────────────────────────────┐ +│ 5. Prepare for Solving │ +│ ├─ Compute Jacobian (symbolic or auto-diff) │ +│ ├─ Setup events (voltage threshold, etc.) │ +│ └─ Extract initial conditions (y0) │ +└────────────┬───────────────────────────────────────┘ + │ +┌────────────▼───────────────────────────────────────┐ +│ 6. Solve │ +│ solver.solve(t_eval=[0,3600]) │ +│ [Time-stepping: y(t) ← ODE integrator] │ +└────────────┬───────────────────────────────────────┘ + │ +┌────────────▼───────────────────────────────────────┐ +│ 7. Post-Process │ +│ solution.compute_variable("Current") │ +│ [Evaluate derived quantities from y(t)] │ +└────────────┬───────────────────────────────────────┘ + │ +┌────────────▼───────────────────────────────────────┐ +│ 8. Visualize │ +│ solution.plot() │ +│ [Render voltage, temperature, etc. vs. time] │ +└────────────────────────────────────────────────────┘ +``` + +## Key Classes & Interfaces + +``` +BaseModel (Abstract) +├─ submodels: Dict[str, BaseSubmodel] +├─ _rhs: Dict[str, Symbol] → RHS expressions +├─ _algebraic: Dict[str, Symbol] → Algebraic constraints +├─ _variables: FuzzyDict[str, Symbol] +├─ build_model() → Assemble equations +├─ set_rhs() → Add RHS equation +├─ set_algebraic() → Add algebraic constraint +└─ set_boundary_conditions() → Apply BCs + +Discretisation +├─ mesh: Dict[str, Mesh] +├─ spatial_methods: Dict[str, SpatialMethod] +├─ discretise(model) → Convert PDE→ODE +└─ process_boundary_conditions() + +BaseSolver (Abstract) +├─ _integrate(t_eval, y0, model) +├─ solve(t_eval, y0) → Solve + return Solution +├─ handle_events() → Trigger on conditions +└── compute_jacobian() → ∂f/∂y matrix + +Solution +├─ t: ndarray → Time points +├─ y: ndarray → State vectors +├─ compute_variable(name) → Evaluate derived quantity +├─ plot() → Quick visualization +└─ save/load → Persistence +``` + +## Parameter System + +``` +Parameter (Symbol) + ↓ (in model building) +Expression Tree + ↓ (before solving) +ParameterValues.process_model() + ↓ +Parameter → Literal Value (float, array) + ↓ (substituted into expression tree) +Expression Tree (numerical, ready to solve) +``` + +## File Statistics + +``` +Total Files: 973 +├─ Python files: ≈ 800 +├─ Jupyter notebooks: ≈ 50 +├─ Documentation: ≈ 100 +└─ Other: ≈ 23 + +Code Statistics: +├─ Functions: 4,342 +├─ Classes: 735 +├─ Interfaces: 0 +└─ Methods: ~2,000 + +Hottest Files (by connections): +├─ src/pybamm/__init__.py (500 connections) +├─ src/pybamm/expression_tree/variable.py (474 connections) +├─ src/pybamm/expression_tree/scalar.py (397 connections) +├─ src/pybamm/models/base_model.py (305 connections) +└─ src/pybamm/discretisations/discretisation.py (289 connections) +``` + +## Performance Tiers + +``` +Fastest (< 1 sec) Slowest (> 10 sec) + ↓ ↓ +IDAKLUSolver (C++) CasadiSolver (sym opt) +JAXSolver (GPU) Complex 3D models +ScipySolver Large parameter sweeps + ↓ +DummySolver (debug) +``` + +## Extension Points + +Want to add something? Extend these: + +``` +Custom Model + └─ Inherit from BaseBatteryModel + └─ Override build_model(), set_rhs(), etc. + +Custom Submodel + └─ Inherit from BaseSubModel + └─ Define physics equations + +Custom Spatial Method + └─ Inherit from SpatialMethod + └─ Implement discretise_operator() + +Custom Solver + └─ Inherit from BaseSolver + └─ Implement _integrate() + +Custom Parameter Set + └─ Dict of {"symbol_name": numerical_value} +``` + +## Common Workflows + +### Quick Discharge Curve +```python +model = pybamm.lithium_ion.SPM() +sim = pybamm.Simulation(model) +sim.solve([0, 3600]) +sim.plot() +``` + +### Multi-Model Comparison +```python +models = [ + pybamm.lithium_ion.SPM(), + pybamm.lithium_ion.DFN(), + pybamm.lithium_ion.MSMR() +] +for model in models: + sim = pybamm.Simulation(model) + sim.solve([0, 3600]) + sim.plot() +``` + +### Parameter Sensitivity +```python +batch = pybamm.BatchStudy(...) +batch.solve(...) +# Multi-parameter sweep +``` + +### Thermal Model +```python +model = pybamm.lithium_ion.DFN( + options={"thermal": "lumped"} +) +# Add temperature physics +``` + +--- + +*Quick reference for PyBaMM v1.x* + + diff --git a/GITNEXUS_ANALYSIS.md b/GITNEXUS_ANALYSIS.md new file mode 100644 index 0000000000..38bb2ce761 --- /dev/null +++ b/GITNEXUS_ANALYSIS.md @@ -0,0 +1,374 @@ +# PyBaMM Analysis via GitNexus MCP + +## What is GitNexus MCP? + +GitNexus is a **Model Context Protocol (MCP) server** that exposes a codebase's knowledge graph as queryable tools. It provides: + +- **Semantic Search**: Find code by meaning, not just text +- **Graph Queries**: Traverse code dependencies via Cypher +- **Hybrid Analysis**: Combine keyword + semantic search with 1-hop graph expansion +- **Impact Analysis**: See what breaks when you change something +- **Code Reading**: Smart path resolution and fuzzy matching + +--- + +## Key Findings from GitNexus Analysis + +### 1. Project Metadata +``` +Project: PyBaMM-develop +Total Files: 973 +Functions: 4,342 +Classes: 735 +Interfaces: 0 +Languages: Python (primary) +``` + +### 2. Most Critical Components (by Connection Count) + +| Rank | Component | Type | File | Connections | +|------|-----------|------|------|-------------| +| 1 | `__init__.py` | File | src/pybamm/__init__.py | 500 | +| 2 | `Variable` | Class | expression_tree/variable.py | 474 | +| 3 | `Scalar` | Class | expression_tree/scalar.py | 397 | +| 4 | `evaluate()` | Function | expression_tree/binary_operators.py | 344 | +| 5 | `solve()` | Function | batch_study.py | 311 | +| 6 | `BaseModel` | Class | models/base_model.py | 305 | +| 7 | `Discretisation` | Class | discretisations/discretisation.py | 289 | +| 8 | `linspace()` | Function | expression_tree/array.py | 267 | + +**Insight**: Expression tree classes are the backbone; every model uses Variable and Scalar for mathematical operations. + +### 3. Model Inheritance Chain (via GitNexus Cypher Query) + +``` +BaseModel (Abstract) + ├─ BasicFull (Lead-Acid) + ├─ Full (Lead-Acid) + ├─ LOQS (Lead-Acid) + ├─ BasicDFN (Li-Ion) + ├─ BasicDFN2D (Li-Ion 2D) + ├─ BasicDFNComposite (Li-Ion) + ├─ BasicDFNHalfCell (Li-Ion) + ├─ BasicSPM (Li-Ion) + ├─ Basic3DThermalSPM (Li-Ion + Thermal) + ├─ DFN (Li-Ion) + ├─ SPM (Li-Ion) + └─ [30+ more models...] + +Submodels that extend BaseModel: + ├─ Constant (active_material) + ├─ LossActiveMaterial + ├─ BaseThroughCellModel + ├─ BaseTransverseModel + ├─ Uniform (current_collector) + └─ [100+ more submodels...] +``` + +**Insight**: Battery models use inheritance hierarchy for code reuse; submodels can be mixed and matched. + +### 4. Solver Architecture (Top-Down Dependency) + +``` +Function: solve(model, t_eval, y0) + ├─ IDAKLUSolver._integrate() + │ ├─ idaklu C++ bindings + │ └─ Jacobian computation + ├─ CasadiSolver._integrate() + │ ├─ CasADi symbolic engine + │ └─ Auto-differentiation + ├─ ScipySolver._integrate() + │ ├─ scipy.integrate + │ └─ Event detection + ├─ JAXSolver._integrate() + │ ├─ JAX jit compilation + │ └─ GPU acceleration + └─ IDakluJax._integrate() + └─ Hybrid IDA + JAX +``` + +### 5. Spatial Method Stack + +``` +Discretisation + ├─ FiniteVolume (1D/2D) + ├─ FiniteVolume2D + ├─ SpectralVolume + ├─ ScikitFiniteElement (1D unstructured) + ├─ ScikitFiniteElement3D (3D tetrahedral) + └─ ZeroDimensionalMethod (lumped) + +Each method implements: + ├─ discretise_operator() → convert ∇, ∇·, ∇² + ├─ boundary_conditions() → apply BC + └─ mesh_generation() → create grid +``` + +### 6. Expression Tree Topology + +The symbolic computation layer forms a DAG (Directed Acyclic Graph): + +``` +Symbol (abstract base - 191 properties/methods) + ├─ Leaf Nodes: + │ ├─ Variable (state vector components) + │ ├─ Parameter (model coefficients) + │ ├─ Scalar (constants) + │ ├─ Array (numeric arrays) + │ └─ StateVector (spatial discretization) + │ + └─ Internal Nodes: + ├─ BinaryOperator (54 methods, 16 types) + │ ├─ Addition + │ ├─ Subtraction + │ ├─ Multiplication + │ ├─ Division + │ └─ [12 more...] + ├─ UnaryOperator + │ ├─ Exponential + │ ├─ Logarithm + │ ├─ Trigonometric + │ └─ [10+ more...] + └─ SpecialOperators + ├─ Concatenation + ├─ Broadcast + └─ Interpolant +``` + +### 7. Parameter System Integration + +``` +Parameter (Symbol) + ↓ +ParameterValues (collection) + ├─ lithium_ion_parameters.py (100+ predefined sets) + ├─ lead_acid_parameters.py + ├─ electrical_parameters.py + ├─ geometric_parameters.py + ├─ thermal_parameters.py + └─ bpx.py (Battery Parameter eXchange format) + ↓ +process_model(model) + ├─ Traverse expression tree + ├─ Replace Symbol nodes with values + └─ Return numerical model +``` + +--- + +## GitNexus Cypher Query Examples + +### Query 1: Find All Solvers + +```cypher +MATCH (c:Class)<-[r:CodeRelation {type: "EXTENDS"}]-(solver:Class) +WHERE c.name = "BaseSolver" +RETURN solver.name, solver.filePath +``` + +**Result**: IDAKLUSolver, CasadiSolver, ScipySolver, JAXSolver, IDakluJax, etc. + +### Query 2: Trace Model Dependencies + +```cypher +MATCH (m:Class {name: "DFN"})-[r:CodeRelation]->(dep) +WHERE r.type IN ["IMPORTS", "CALLS"] +RETURN r.type, dep.name +``` + +**Result**: DFN depends on Discretisation, ParameterValues, solver classes, submodels, etc. + +### Query 3: Find All Spatial Methods + +```cypher +MATCH (base:Class {name: "SpatialMethod"})<-[r:CodeRelation {type: "EXTENDS"}]-(impl:Class) +RETURN impl.name, impl.filePath +``` + +**Result**: FiniteVolume, SpectralVolume, ScikitFiniteElement, etc. + +### Query 4: Impact Analysis - What calls `solve()`? + +```cypher +MATCH (f:Function {name: "solve"})<-[r:CodeRelation {type: "CALLS"}]-(caller:Function) +RETURN caller.name, caller.filePath +LIMIT 20 +``` + +**Result**: Simulation, Experiment, BatchStudy, all depend on solve() + +--- + +## GitNexus Hybrid Search Examples + +### Search 1: "How does the model build process work?" + +**Result**: Found `build_model()` in BaseModel with context: +- Incoming: Model initialization calls +- Outgoing: RHS assembly, algebraic constraint setup +- 1-hop neighbors: submodels, expression tree, parameter processing + +### Search 2: "Where is thermal physics integrated?" + +**Result**: +- thermal/ submodule (8 implementations) +- Thermal submodels extend BaseModel +- Integrated into full models via options +- Parameters in thermal_parameters.py + +### Search 3: "Which solvers support GPU acceleration?" + +**Result**: +- JAXSolver (uses JAX JIT + GPU) +- IDakluJax (hybrid IDA + JAX) +- Both connect to JAX backend + +--- + +## Blast Radius Analysis + +### Example: What happens if we change `Variable.evaluate()`? + +**Direction**: Upstream (what depends on this) + +**Depth**: 3 levels + +**Results**: +- **Depth 1 (direct callers)**: + - BinaryOperator.evaluate() + - UnaryOperator.evaluate() + - Symbol.evaluate() + - ≈50 subclasses affected + +- **Depth 2 (indirect)**: + - Solver._integrate() + - expression tree evaluators + - ≈150 functions + +- **Depth 3 (transitive)**: + - All model execution paths + - Simulation.solve() + - Plotting, visualization + +**Conclusion**: Changing Variable.evaluate() breaks nearly the entire codebase - it's a critical node. + +--- + +## Code Statistics by Component + +### Expression Tree Module +``` +Files: 45 +Functions: 1,200+ +Classes: 120+ +Lines: ≈80,000 +Critical because: Everything mathematical flows through here +``` + +### Solvers Module +``` +Files: 12 +Functions: 500+ +Classes: 10+ (base + implementations) +Lines: ≈40,000 +Critical because: Integration logic; bridges symbolic→numerical +``` + +### Models Module +``` +Files: 150+ +Functions: 2,000+ +Classes: 400+ +Lines: ≈100,000 +Critical because: Domain-specific physics models +``` + +### Spatial Methods Module +``` +Files: 8 +Functions: 300+ +Classes: 8+ +Lines: ≈30,000 +Critical because: Discretisation strategy; PDE→ODE conversion +``` + +--- + +## Architecture Patterns Identified by GitNexus + +### 1. **Template Method Pattern** +``` +BaseModel.build_model() + ├─ set_rhs() + ├─ set_algebraic() + ├─ set_boundary_conditions() + └─ [subclasses override] +``` + +### 2. **Strategy Pattern** +``` +Solver (interface) + ├─ IDAKLUSolver (strategy A: C++ backend) + ├─ CasadiSolver (strategy B: symbolic) + └─ JAXSolver (strategy C: GPU) +``` + +### 3. **Composite Pattern** +``` +BaseModel contains: + ├─ submodels[] (nested BaseModel instances) + ├─ expression_tree (nested Symbol nodes) + └─ geometry (nested Geometry instances) +``` + +### 4. **Factory Pattern** +``` +model_options → choose submodels → factory creates model + E.g., {"thermal": "lumped"} → adds lumped thermal +``` + +### 5. **Visitor Pattern** +``` +Expression tree traversal: + ├─ Jacobian visitor + ├─ Serialization visitor + ├─ Code generation visitor + └─ Evaluation visitor +``` + +--- + +## Recommended Learning Path (from GitNexus) + +1. **Start with**: `src/pybamm/__init__.py` - Central hub (500 connections) +2. **Then learn**: Expression tree (`symbol.py` - 191 methods) +3. **Next**: BaseModel (`base_model.py` - 305 connections) +4. **Then**: Discretisation (280+ connections) +5. **Finally**: Solvers (specialized backends) + +--- + +## Key Insights + +✅ **Well-structured**: Clear layering with minimal coupling between layers +✅ **Extensible**: Easy to add new models, solvers, spatial methods +✅ **Physics-first**: Expression tree mirrors actual mathematical structure +✅ **Production-ready**: Multiple backends, comprehensive testing + +⚠️ **High complexity**: Many abstraction layers to learn +⚠️ **Slow startup**: Model building + discretisation takes time +⚠️ **Memory-heavy**: Symbolic expressions consume RAM + +--- + +## Files Created + +1. **ARCHITECTURE.md** - Comprehensive 400+ line architecture guide +2. **ARCHITECTURE_QUICK_REF.md** - Visual diagrams and quick lookups +3. **GITNEXUS_ANALYSIS.md** - This file, MCP-powered insights + +--- + +*Analysis conducted using GitNexus MCP v1.0 - Code Intelligence for AI Agents* + + diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index 0bbe7d72a8..cf591cb309 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -39,6 +39,7 @@ "react-dom": "^18.3.1", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.0", + "react-zoom-pan-pinch": "^3.7.0", "remark-gfm": "^4.0.1", "sigma": "^3.0.2", "tailwindcss": "^4.1.18", @@ -8440,6 +8441,20 @@ "react": ">= 0.14.0" } }, + "node_modules/react-zoom-pan-pinch": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz", + "integrity": "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA==", + "license": "MIT", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", diff --git a/gitnexus/package.json b/gitnexus/package.json index 76571053b1..20ee8b00ba 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -40,6 +40,7 @@ "react-dom": "^18.3.1", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.0", + "react-zoom-pan-pinch": "^3.7.0", "remark-gfm": "^4.0.1", "sigma": "^3.0.2", "tailwindcss": "^4.1.18", diff --git a/gitnexus/src/components/GraphCanvas.tsx b/gitnexus/src/components/GraphCanvas.tsx index 118d386f93..cfc5566dce 100644 --- a/gitnexus/src/components/GraphCanvas.tsx +++ b/gitnexus/src/components/GraphCanvas.tsx @@ -20,6 +20,7 @@ export const GraphCanvas = forwardRef((_, ref) => { openCodePanel, depthFilter, highlightedNodeIds, + setHighlightedNodeIds, aiCitationHighlightedNodeIds, aiToolHighlightedNodeIds, blastRadiusNodeIds, @@ -303,13 +304,19 @@ export const GraphCanvas = forwardRef((_, ref) => { {/* AI Highlights toggle - Top Right */}
diff --git a/gitnexus/src/components/MCPToggle.tsx b/gitnexus/src/components/MCPToggle.tsx index 87c2c3e94b..aa05cd04df 100644 --- a/gitnexus/src/components/MCPToggle.tsx +++ b/gitnexus/src/components/MCPToggle.tsx @@ -30,7 +30,7 @@ const MCP_CONFIG = `{ "mcpServers": { "gitnexus": { "command": "npx", - "args": ["-y", "gitnexus-mcp"] + "args": ["--prefer-online", "-y", "gitnexus-mcp@latest"] } } }`; diff --git a/gitnexus/src/components/ProcessFlowModal.tsx b/gitnexus/src/components/ProcessFlowModal.tsx index d7da4feff6..45124bfd81 100644 --- a/gitnexus/src/components/ProcessFlowModal.tsx +++ b/gitnexus/src/components/ProcessFlowModal.tsx @@ -5,14 +5,15 @@ */ import { useEffect, useRef, useCallback, useState } from 'react'; -import { X, GitBranch, Copy, Focus, Layers } from 'lucide-react'; +import { X, GitBranch, Copy, Focus, Layers, ZoomIn, ZoomOut } from 'lucide-react'; import mermaid from 'mermaid'; import { ProcessData, generateProcessMermaid } from '../lib/mermaid-generator'; interface ProcessFlowModalProps { process: ProcessData | null; onClose: () => void; - onFocusInGraph?: (nodeIds: string[]) => void; + onFocusInGraph?: (nodeIds: string[], processId: string) => void; + isFullScreen?: boolean; } // Initialize mermaid with cyan/purple theme matching GitNexus @@ -38,9 +39,9 @@ mermaid.initialize({ }, flowchart: { curve: 'basis', - padding: 20, - nodeSpacing: 50, - rankSpacing: 60, + padding: 50, + nodeSpacing: 120, + rankSpacing: 140, htmlLabels: true, }, }); @@ -51,21 +52,32 @@ mermaid.parseError = (err) => { console.debug('Mermaid parse error (suppressed):', err); }; -export const ProcessFlowModal = ({ process, onClose, onFocusInGraph }: ProcessFlowModalProps) => { +export const ProcessFlowModal = ({ process, onClose, onFocusInGraph, isFullScreen = false }: ProcessFlowModalProps) => { const containerRef = useRef(null); const diagramRef = useRef(null); const scrollContainerRef = useRef(null); - const [zoom, setZoom] = useState(1); + + // Full process map gets higher default zoom (667%) and max zoom (3000%) + const defaultZoom = isFullScreen ? 6.67 : 1; + const maxZoom = isFullScreen ? 30 : 10; + + const [zoom, setZoom] = useState(defaultZoom); const [pan, setPan] = useState({ x: 0, y: 0 }); const [isPanning, setIsPanning] = useState(false); const [panStart, setPanStart] = useState({ x: 0, y: 0 }); + + // Reset zoom when switching between full screen and regular mode + useEffect(() => { + setZoom(defaultZoom); + setPan({ x: 0, y: 0 }); + }, [isFullScreen, defaultZoom]); // Handle zoom with scroll wheel useEffect(() => { const handleWheel = (e: WheelEvent) => { e.preventDefault(); const delta = e.deltaY * -0.001; - setZoom(prev => Math.min(Math.max(0.1, prev + delta), 5)); + setZoom(prev => Math.min(Math.max(0.1, prev + delta), maxZoom)); }; const container = scrollContainerRef.current; @@ -73,6 +85,28 @@ export const ProcessFlowModal = ({ process, onClose, onFocusInGraph }: ProcessFl container.addEventListener('wheel', handleWheel, { passive: false }); return () => container.removeEventListener('wheel', handleWheel); } + }, [process, maxZoom]); // Re-attach when process or maxZoom changes + + // Handle keyboard zoom + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === '+' || e.key === '=') { + setZoom(prev => Math.min(prev + 0.2, maxZoom)); + } else if (e.key === '-' || e.key === '_') { + setZoom(prev => Math.max(prev - 0.2, 0.1)); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [maxZoom]); + + // Zoom in/out handlers + const handleZoomIn = useCallback(() => { + setZoom(prev => Math.min(prev + 0.25, maxZoom)); + }, [maxZoom]); + + const handleZoomOut = useCallback(() => { + setZoom(prev => Math.max(prev - 0.25, 0.1)); }, []); // Handle pan with mouse drag @@ -91,9 +125,9 @@ export const ProcessFlowModal = ({ process, onClose, onFocusInGraph }: ProcessFl }, []); const resetView = useCallback(() => { - setZoom(1); + setZoom(defaultZoom); setPan({ x: 0, y: 0 }); - }, []); + }, [defaultZoom]); // Render mermaid diagram useEffect(() => { @@ -163,7 +197,7 @@ export const ProcessFlowModal = ({ process, onClose, onFocusInGraph }: ProcessFl const handleFocusInGraph = useCallback(() => { if (!process || !onFocusInGraph) return; const nodeIds = process.steps.map(s => s.id); - onFocusInGraph(nodeIds); + onFocusInGraph(nodeIds, process.id); onClose(); }, [process, onFocusInGraph, onClose]); @@ -172,11 +206,14 @@ export const ProcessFlowModal = ({ process, onClose, onFocusInGraph }: ProcessFl return (
{/* Glassmorphism Modal */} -
+
{/* Subtle gradient overlay for extra glass feel */}
@@ -190,7 +227,7 @@ export const ProcessFlowModal = ({ process, onClose, onFocusInGraph }: ProcessFl {/* Diagram */}
{/* Footer Actions */}
+ {/* Zoom controls */} +
+ + + {Math.round(zoom * 100)}% + + +
-
{totalCount} processes detected @@ -295,6 +328,30 @@ export const ProcessesPanel = () => { {/* Process list */}
+ {/* View All Processes Card */} +
+ +
+ {/* Cross-Community Section */} {filteredProcesses.cross.length > 0 && (
@@ -321,7 +378,10 @@ export const ProcessesPanel = () => { key={process.id} process={process} isLoading={loadingProcess === process.id} + isSelected={selectedProcess?.id === process.id} + isFocused={focusedProcessId === process.id} onView={() => handleViewProcess(process.id, process.label, 'cross_community')} + onToggleFocus={() => handleToggleFocusForProcess(process.id)} /> ))}
@@ -355,7 +415,10 @@ export const ProcessesPanel = () => { key={process.id} process={process} isLoading={loadingProcess === process.id} + isSelected={selectedProcess?.id === process.id} + isFocused={focusedProcessId === process.id} onView={() => handleViewProcess(process.id, process.label, 'intra_community')} + onToggleFocus={() => handleToggleFocusForProcess(process.id)} /> ))}
@@ -369,6 +432,7 @@ export const ProcessesPanel = () => { process={selectedProcess} onClose={() => setSelectedProcess(null)} onFocusInGraph={handleFocusInGraph} + isFullScreen={selectedProcess?.id === 'combined-all'} />
); @@ -378,12 +442,22 @@ export const ProcessesPanel = () => { interface ProcessItemProps { process: { id: string; label: string; stepCount: number; clusters: string[] }; isLoading: boolean; + isSelected: boolean; + isFocused: boolean; onView: () => void; + onToggleFocus: () => void; } -const ProcessItem = ({ process, isLoading, onView }: ProcessItemProps) => { +const ProcessItem = ({ process, isLoading, isSelected, isFocused, onView, onToggleFocus }: ProcessItemProps) => { + // Determine row styling - focused gets special highlight + const rowClass = isFocused + ? 'bg-amber-950/40 border border-amber-500/50 ring-1 ring-amber-400/30' + : isSelected + ? 'bg-cyan-950/40 border border-cyan-500/50 ring-1 ring-cyan-400/30' + : ''; + return ( -
+
{process.label}
@@ -397,13 +471,32 @@ const ProcessItem = ({ process, isLoading, onView }: ProcessItemProps) => { )}
+ {/* Lightbulb icon - appears on hover, always visible when focused */} +
{getProviderDisplayName(provider)} @@ -534,6 +546,92 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
)} + {/* OpenRouter Settings */} + {settings.activeProvider === 'openrouter' && ( +
+
+ +
+ setSettings(prev => ({ + ...prev, + openrouter: { ...prev.openrouter!, apiKey: e.target.value } + }))} + placeholder="Enter your OpenRouter API key" + className="w-full px-4 py-3 pr-12 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all" + /> + +
+

+ Get your API key from{' '} + + OpenRouter Keys + +

+
+ +
+ + +

+ Browse all models at{' '} + + OpenRouter Models + +

+
+
+ )} + {/* Intelligent Clustering Settings */}
diff --git a/gitnexus/src/core/llm/agent.ts b/gitnexus/src/core/llm/agent.ts index 8ec2cc9d03..cc0c752db3 100644 --- a/gitnexus/src/core/llm/agent.ts +++ b/gitnexus/src/core/llm/agent.ts @@ -20,6 +20,7 @@ import type { GeminiConfig, AnthropicConfig, OllamaConfig, + OpenRouterConfig, AgentStreamChunk, } from './types'; import { @@ -188,6 +189,37 @@ export const createChatModel = (config: ProviderConfig): BaseChatModel => { }); } + case 'openrouter': { + const openRouterConfig = config as OpenRouterConfig; + + // Debug logging + if (import.meta.env.DEV) { + console.log('🌐 OpenRouter config:', { + hasApiKey: !!openRouterConfig.apiKey, + apiKeyLength: openRouterConfig.apiKey?.length || 0, + model: openRouterConfig.model, + baseUrl: openRouterConfig.baseUrl, + }); + } + + if (!openRouterConfig.apiKey || openRouterConfig.apiKey.trim() === '') { + throw new Error('OpenRouter API key is required but was not provided'); + } + + return new ChatOpenAI({ + openAIApiKey: openRouterConfig.apiKey, + apiKey: openRouterConfig.apiKey, // Fallback for some versions + modelName: openRouterConfig.model, + temperature: openRouterConfig.temperature ?? 0.1, + maxTokens: openRouterConfig.maxTokens, + configuration: { + apiKey: openRouterConfig.apiKey, // Ensure client receives it + baseURL: openRouterConfig.baseUrl ?? 'https://openrouter.ai/api/v1', + }, + streaming: true, + }); + } + default: throw new Error(`Unsupported provider: ${(config as any).provider}`); } diff --git a/gitnexus/src/core/llm/settings-service.ts b/gitnexus/src/core/llm/settings-service.ts index 333463b957..0c35b9623a 100644 --- a/gitnexus/src/core/llm/settings-service.ts +++ b/gitnexus/src/core/llm/settings-service.ts @@ -14,6 +14,7 @@ import { GeminiConfig, AnthropicConfig, OllamaConfig, + OpenRouterConfig, ProviderConfig, } from './types'; @@ -55,6 +56,10 @@ export const loadSettings = (): LLMSettings => { ...DEFAULT_LLM_SETTINGS.ollama, ...parsed.ollama, }, + openrouter: { + ...DEFAULT_LLM_SETTINGS.openrouter, + ...parsed.openrouter, + }, }; } catch (error) { console.warn('Failed to load LLM settings:', error); @@ -146,6 +151,17 @@ export const updateProviderSettings = ( saveSettings(updated); return updated; } + case 'openrouter': { + const updated: LLMSettings = { + ...current, + openrouter: { + ...(current.openrouter ?? {}), + ...(updates as Partial>), + }, + }; + saveSettings(updated); + return updated; + } default: { // Should be unreachable due to T extends LLMProvider, but keep a safe fallback const updated: LLMSettings = { ...current }; @@ -217,6 +233,19 @@ export const getActiveProviderConfig = (): ProviderConfig | null => { ...settings.ollama, } as OllamaConfig; + case 'openrouter': + if (!settings.openrouter?.apiKey || settings.openrouter.apiKey.trim() === '') { + return null; + } + return { + provider: 'openrouter', + apiKey: settings.openrouter.apiKey, + model: settings.openrouter.model || '', + baseUrl: settings.openrouter.baseUrl || 'https://openrouter.ai/api/v1', + temperature: settings.openrouter.temperature, + maxTokens: settings.openrouter.maxTokens, + } as OpenRouterConfig; + default: return null; } @@ -251,6 +280,8 @@ export const getProviderDisplayName = (provider: LLMProvider): string => { return 'Anthropic'; case 'ollama': return 'Ollama (Local)'; + case 'openrouter': + return 'OpenRouter'; default: return provider; } @@ -277,3 +308,21 @@ export const getAvailableModels = (provider: LLMProvider): string[] => { } }; +/** + * Fetch available models from OpenRouter API + */ +export const fetchOpenRouterModels = async (): Promise> => { + try { + const response = await fetch('https://openrouter.ai/api/v1/models'); + if (!response.ok) throw new Error('Failed to fetch models'); + const data = await response.json(); + return data.data.map((model: any) => ({ + id: model.id, + name: model.name || model.id, + })); + } catch (error) { + console.error('Error fetching OpenRouter models:', error); + return []; + } +}; + diff --git a/gitnexus/src/core/llm/types.ts b/gitnexus/src/core/llm/types.ts index d9ca8b30b7..21de11b13c 100644 --- a/gitnexus/src/core/llm/types.ts +++ b/gitnexus/src/core/llm/types.ts @@ -8,7 +8,7 @@ /** * Supported LLM providers */ -export type LLMProvider = 'openai' | 'azure-openai' | 'gemini' | 'anthropic' | 'ollama'; +export type LLMProvider = 'openai' | 'azure-openai' | 'gemini' | 'anthropic' | 'ollama' | 'openrouter'; /** * Base configuration shared by all providers @@ -68,10 +68,20 @@ export interface OllamaConfig extends BaseProviderConfig { model: string; } +/** + * OpenRouter configuration + */ +export interface OpenRouterConfig extends BaseProviderConfig { + provider: 'openrouter'; + apiKey: string; + model: string; // e.g., 'anthropic/claude-3.5-sonnet', 'openai/gpt-4-turbo' + baseUrl?: string; // defaults to https://openrouter.ai/api/v1 +} + /** * Union type for all provider configurations */ -export type ProviderConfig = OpenAIConfig | AzureOpenAIConfig | GeminiConfig | AnthropicConfig | OllamaConfig; +export type ProviderConfig = OpenAIConfig | AzureOpenAIConfig | GeminiConfig | AnthropicConfig | OllamaConfig | OpenRouterConfig; /** * Stored settings (what goes to localStorage) @@ -87,6 +97,7 @@ export interface LLMSettings { gemini?: Partial>; anthropic?: Partial>; ollama?: Partial>; + openrouter?: Partial>; // Intelligent Clustering Settings intelligentClustering: boolean; @@ -131,6 +142,12 @@ export const DEFAULT_LLM_SETTINGS: LLMSettings = { model: 'llama3.2', temperature: 0.1, }, + openrouter: { + apiKey: '', + model: '', + baseUrl: 'https://openrouter.ai/api/v1', + temperature: 0.1, + }, }; /** diff --git a/gitnexus/src/hooks/useAppState.tsx b/gitnexus/src/hooks/useAppState.tsx index 21d6ef32b3..720b92314e 100644 --- a/gitnexus/src/hooks/useAppState.tsx +++ b/gitnexus/src/hooks/useAppState.tsx @@ -1042,10 +1042,10 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { } } - // Parse impact marker from tool results - const impactMatch = tc.result.match(/\[IMPACT:([^\]]+)\]/); - if (impactMatch) { - const rawIds = impactMatch[1].split(',').map((id: string) => id.trim()).filter(Boolean); + // Parse impact marker from tool results + const impactMatch = tc.result.match(/\[IMPACT:([^\]]+)\]/); + if (impactMatch) { + const rawIds = impactMatch[1].split(',').map((id: string) => id.trim()).filter(Boolean); if (rawIds.length > 0 && graph) { const matchedIds = new Set(); const graphNodeIds = graph.nodes.map(n => n.id); diff --git a/gitnexus/src/lib/mermaid-generator.ts b/gitnexus/src/lib/mermaid-generator.ts index f5a56e865a..4b34a718d5 100644 --- a/gitnexus/src/lib/mermaid-generator.ts +++ b/gitnexus/src/lib/mermaid-generator.ts @@ -42,11 +42,11 @@ export function generateProcessMermaid(process: ProcessData): string { // Add class definitions for styling (rounded corners + colors) lines.push(' %% Styles'); - lines.push(' classDef default fill:#1e293b,stroke:#94a3b8,stroke-width:1px,color:#f8fafc,rx:10,ry:10;'); - lines.push(' classDef entry fill:#1e293b,stroke:#34d399,stroke-width:3px,color:#f8fafc,rx:10,ry:10;'); - lines.push(' classDef step fill:#1e293b,stroke:#22d3ee,stroke-width:2px,color:#f8fafc,rx:10,ry:10;'); - lines.push(' classDef terminal fill:#1e293b,stroke:#f472b6,stroke-width:3px,color:#f8fafc,rx:10,ry:10;'); - lines.push(' classDef cluster fill:#0f172a,stroke:#334155,stroke-width:1px,color:#94a3b8,rx:4,ry:4;'); + lines.push(' classDef default fill:#1e293b,stroke:#94a3b8,stroke-width:3px,color:#f8fafc,rx:10,ry:10,font-size:24px;'); + lines.push(' classDef entry fill:#1e293b,stroke:#34d399,stroke-width:5px,color:#f8fafc,rx:10,ry:10,font-size:24px;'); + lines.push(' classDef step fill:#1e293b,stroke:#22d3ee,stroke-width:3px,color:#f8fafc,rx:10,ry:10,font-size:24px;'); + lines.push(' classDef terminal fill:#1e293b,stroke:#f472b6,stroke-width:5px,color:#f8fafc,rx:10,ry:10,font-size:24px;'); + lines.push(' classDef cluster fill:#0f172a,stroke:#334155,stroke-width:3px,color:#94a3b8,rx:4,ry:4,font-size:20px;'); // Track clusters for subgraph grouping const clusterGroups = new Map();