mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-30 06:42:43 +00:00
Merge pull request #34 from abhigyanpatwari/leidens_algorithm_and_processmap
Leidens algorithm and processmap
This commit is contained in:
commit
baa300bea3
18 changed files with 2623 additions and 73 deletions
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: abhigyanpatwari
|
||||
584
ARCHITECTURE.md
Normal file
584
ARCHITECTURE.md
Normal file
|
|
@ -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*
|
||||
|
||||
|
||||
375
ARCHITECTURE_QUICK_REF.md
Normal file
375
ARCHITECTURE_QUICK_REF.md
Normal file
|
|
@ -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*
|
||||
|
||||
|
||||
374
GITNEXUS_ANALYSIS.md
Normal file
374
GITNEXUS_ANALYSIS.md
Normal file
|
|
@ -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*
|
||||
|
||||
|
||||
15
gitnexus/package-lock.json
generated
15
gitnexus/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
|
|||
openCodePanel,
|
||||
depthFilter,
|
||||
highlightedNodeIds,
|
||||
setHighlightedNodeIds,
|
||||
aiCitationHighlightedNodeIds,
|
||||
aiToolHighlightedNodeIds,
|
||||
blastRadiusNodeIds,
|
||||
|
|
@ -303,13 +304,19 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
|
|||
{/* AI Highlights toggle - Top Right */}
|
||||
<div className="absolute top-4 right-4 z-20">
|
||||
<button
|
||||
onClick={toggleAIHighlights}
|
||||
onClick={() => {
|
||||
// If turning off, also clear process highlights
|
||||
if (isAIHighlightsEnabled) {
|
||||
setHighlightedNodeIds(new Set());
|
||||
}
|
||||
toggleAIHighlights();
|
||||
}}
|
||||
className={
|
||||
isAIHighlightsEnabled
|
||||
? 'w-10 h-10 flex items-center justify-center bg-cyan-500/15 border border-cyan-400/40 rounded-lg text-cyan-200 hover:bg-cyan-500/20 hover:border-cyan-300/60 transition-colors'
|
||||
: 'w-10 h-10 flex items-center justify-center bg-elevated border border-border-subtle rounded-lg text-text-muted hover:bg-hover hover:text-text-primary transition-colors'
|
||||
}
|
||||
title={isAIHighlightsEnabled ? 'Turn off AI highlights' : 'Turn on AI highlights'}
|
||||
title={isAIHighlightsEnabled ? 'Turn off all highlights' : 'Turn on AI highlights'}
|
||||
>
|
||||
{isAIHighlightsEnabled ? <Lightbulb className="w-4 h-4" /> : <LightbulbOff className="w-4 h-4" />}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const MCP_CONFIG = `{
|
|||
"mcpServers": {
|
||||
"gitnexus": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "gitnexus-mcp"]
|
||||
"args": ["--prefer-online", "-y", "gitnexus-mcp@latest"]
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [svg, setSvg] = useState<string>('');
|
||||
|
||||
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 (
|
||||
<div className="my-3 p-4 bg-rose-500/10 border border-rose-500/30 rounded-lg">
|
||||
|
|
@ -105,49 +119,39 @@ export const MermaidDiagram = ({ code }: MermaidDiagramProps) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={`my-3 relative group ${isExpanded ? 'fixed inset-4 z-50' : ''}`}>
|
||||
{/* Backdrop for expanded view */}
|
||||
{isExpanded && (
|
||||
<div
|
||||
className="absolute inset-0 -m-4 bg-deep/95 backdrop-blur-sm"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
<>
|
||||
<div className="my-3 relative group">
|
||||
<div className="relative bg-gradient-to-b from-surface to-elevated border border-border-subtle rounded-xl overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-surface/60 border-b border-border-subtle">
|
||||
<span className="text-[10px] text-text-muted uppercase tracking-wider font-medium">
|
||||
Diagram
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setShowModal(true)}
|
||||
className="p-1 text-text-muted hover:text-text-primary hover:bg-hover rounded transition-colors"
|
||||
title="Expand"
|
||||
>
|
||||
<Maximize2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Diagram container */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex items-center justify-center p-4 overflow-auto max-h-[400px]"
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Use ProcessFlowModal for expansion */}
|
||||
{showModal && processData && (
|
||||
<ProcessFlowModal
|
||||
process={processData}
|
||||
onClose={() => setShowModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={`
|
||||
relative bg-gradient-to-b from-surface to-elevated
|
||||
border border-border-subtle rounded-xl overflow-hidden
|
||||
${isExpanded ? 'h-full' : ''}
|
||||
`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-surface/60 border-b border-border-subtle">
|
||||
<span className="text-[10px] text-text-muted uppercase tracking-wider font-medium">
|
||||
Diagram
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="p-1 text-text-muted hover:text-text-primary hover:bg-hover rounded transition-colors"
|
||||
title={isExpanded ? 'Minimize' : 'Expand'}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<Minimize2 className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<Maximize2 className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Diagram container */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`
|
||||
flex items-center justify-center p-4 overflow-auto
|
||||
${isExpanded ? 'h-[calc(100%-40px)]' : 'max-h-[400px]'}
|
||||
`}
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
301
gitnexus/src/components/ProcessFlowModal.tsx
Normal file
301
gitnexus/src/components/ProcessFlowModal.tsx
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
/**
|
||||
* 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, 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[], processId: string) => void;
|
||||
isFullScreen?: boolean;
|
||||
}
|
||||
|
||||
// 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: 50,
|
||||
nodeSpacing: 120,
|
||||
rankSpacing: 140,
|
||||
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, isFullScreen = false }: ProcessFlowModalProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const diagramRef = useRef<HTMLDivElement>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 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), maxZoom));
|
||||
};
|
||||
|
||||
const container = scrollContainerRef.current;
|
||||
if (container) {
|
||||
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
|
||||
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(defaultZoom);
|
||||
setPan({ x: 0, y: 0 });
|
||||
}, [defaultZoom]);
|
||||
|
||||
// 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 = `
|
||||
<div class="text-center p-8">
|
||||
<div class="text-red-400 text-sm font-medium mb-2">
|
||||
${isSizeError ? '📊 Diagram Too Large' : '⚠️ Render Error'}
|
||||
</div>
|
||||
<div class="text-slate-400 text-xs max-w-md">
|
||||
${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}`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
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, process.id);
|
||||
onClose();
|
||||
}, [process, onFocusInGraph, onClose]);
|
||||
|
||||
if (!process) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/20 animate-fade-in"
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
{/* Glassmorphism Modal */}
|
||||
<div className={`bg-slate-900/60 backdrop-blur-2xl border border-white/10 rounded-3xl shadow-2xl shadow-cyan-500/10 flex flex-col animate-scale-in overflow-hidden relative ${isFullScreen
|
||||
? 'w-[98%] h-[95vh] max-w-none'
|
||||
: 'w-[95%] max-w-5xl max-h-[90vh]'
|
||||
}`}>
|
||||
{/* Subtle gradient overlay for extra glass feel */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-white/5 to-transparent pointer-events-none" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-6 py-5 border-b border-white/10 relative z-10">
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
Process: {process.label}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Diagram */}
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className={`flex-1 p-8 flex items-center justify-center relative z-10 overflow-hidden ${isFullScreen ? 'min-h-[70vh]' : 'min-h-[400px]'}`}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
style={{ cursor: isPanning ? 'grabbing' : 'grab' }}
|
||||
>
|
||||
<div
|
||||
ref={diagramRef}
|
||||
className="[&_.edgePath_.path]:stroke-slate-400 [&_.edgePath_.path]:stroke-2 [&_.marker]:fill-slate-400 transition-transform origin-center w-fit h-fit"
|
||||
style={{
|
||||
transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="flex items-center justify-center gap-3 px-6 py-4 border-t border-white/10 bg-slate-900/50 relative z-10">
|
||||
{/* Zoom controls */}
|
||||
<div className="flex items-center gap-1 bg-white/5 border border-white/10 rounded-lg p-1">
|
||||
<button
|
||||
onClick={handleZoomOut}
|
||||
className="p-2 text-slate-300 hover:text-white hover:bg-white/10 rounded-md transition-all"
|
||||
title="Zoom out (-)"
|
||||
>
|
||||
<ZoomOut className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="px-2 text-xs text-slate-400 font-mono min-w-[3rem] text-center">
|
||||
{Math.round(zoom * 100)}%
|
||||
</span>
|
||||
<button
|
||||
onClick={handleZoomIn}
|
||||
className="p-2 text-slate-300 hover:text-white hover:bg-white/10 rounded-md transition-all"
|
||||
title="Zoom in (+)"
|
||||
>
|
||||
<ZoomIn className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={resetView}
|
||||
className="flex items-center gap-2 px-4 py-2.5 text-sm font-medium text-slate-300 hover:text-white bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg transition-all"
|
||||
title="Reset zoom and pan"
|
||||
>
|
||||
Reset View
|
||||
</button>
|
||||
{onFocusInGraph && (
|
||||
<button
|
||||
onClick={handleFocusInGraph}
|
||||
className="flex items-center gap-2 px-5 py-2.5 text-sm font-medium text-slate-900 bg-cyan-400 hover:bg-cyan-300 rounded-lg transition-all shadow-lg shadow-cyan-500/20"
|
||||
>
|
||||
<Focus className="w-4 h-4" />
|
||||
Toggle Focus
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleCopyMermaid}
|
||||
className="flex items-center gap-2 px-5 py-2.5 text-sm font-medium text-white bg-purple-600 hover:bg-purple-500 rounded-lg transition-all shadow-lg shadow-purple-500/20"
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
Copy Mermaid
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-5 py-2.5 text-sm font-medium text-slate-300 hover:text-white bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg transition-all"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
509
gitnexus/src/components/ProcessesPanel.tsx
Normal file
509
gitnexus/src/components/ProcessesPanel.tsx
Normal file
|
|
@ -0,0 +1,509 @@
|
|||
/**
|
||||
* 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, Lightbulb, Layers } 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<ProcessData | null>(null);
|
||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(['cross', 'intra']));
|
||||
const [loadingProcess, setLoadingProcess] = useState<string | null>(null);
|
||||
const [focusedProcessId, setFocusedProcessId] = useState<string | null>(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 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<string, ProcessStep>();
|
||||
const allEdges: Array<{ from: string; to: string; type: string }> = [];
|
||||
|
||||
// Fetch steps for all processes concurrently in batches if needed, but for now sequentially to be safe
|
||||
// Optimization: Fetch all steps in one query if possible
|
||||
const allStepsQuery = `
|
||||
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
||||
WHERE p.id IN [${allProcessIds.map(id => `'${id.replace(/'/g, "''")}'`).join(',')}]
|
||||
RETURN s.id AS id, s.name AS name, s.filePath AS filePath, r.step AS stepNumber
|
||||
`;
|
||||
|
||||
const stepsResult = await runQuery(allStepsQuery);
|
||||
|
||||
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) {
|
||||
// Batch query if too many steps
|
||||
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', // Treat as cross-community for styling
|
||||
steps: allSteps,
|
||||
edges: allEdges,
|
||||
clusters: [],
|
||||
};
|
||||
|
||||
setSelectedProcess(combinedProcessData);
|
||||
} catch (error) {
|
||||
console.error('Failed to load combined processes:', error);
|
||||
} finally {
|
||||
setLoadingProcess(null);
|
||||
}
|
||||
}, [processes, runQuery]);
|
||||
|
||||
// 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]);
|
||||
|
||||
// Cache for process steps (so we don't re-query when toggling focus)
|
||||
const [processStepsCache, setProcessStepsCache] = useState<Map<string, string[]>>(new Map());
|
||||
|
||||
// Toggle focus for any process - loads steps on demand
|
||||
const handleToggleFocusForProcess = useCallback(async (processId: string) => {
|
||||
// If already focused on this process, turn off
|
||||
if (focusedProcessId === processId) {
|
||||
setHighlightedNodeIds(new Set());
|
||||
setFocusedProcessId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have cached steps
|
||||
if (processStepsCache.has(processId)) {
|
||||
const stepIds = processStepsCache.get(processId)!;
|
||||
setHighlightedNodeIds(new Set(stepIds));
|
||||
setFocusedProcessId(processId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load steps for this process
|
||||
setLoadingProcess(processId);
|
||||
try {
|
||||
const stepsQuery = `
|
||||
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process {id: '${processId.replace(/'/g, "''")}'})
|
||||
RETURN s.id AS id
|
||||
`;
|
||||
const stepsResult = await runQuery(stepsQuery);
|
||||
const stepIds = stepsResult.map((row: any) => row.id || row[0]);
|
||||
|
||||
// Cache the result
|
||||
setProcessStepsCache(prev => new Map(prev).set(processId, stepIds));
|
||||
|
||||
// Set focus
|
||||
setHighlightedNodeIds(new Set(stepIds));
|
||||
setFocusedProcessId(processId);
|
||||
} catch (error) {
|
||||
console.error('Failed to load process steps for focus:', error);
|
||||
} finally {
|
||||
setLoadingProcess(null);
|
||||
}
|
||||
}, [focusedProcessId, processStepsCache, runQuery, setHighlightedNodeIds]);
|
||||
|
||||
// Focus in graph callback - toggles highlight (used by modal)
|
||||
const handleFocusInGraph = useCallback((nodeIds: string[], processId: string) => {
|
||||
// Check if this process is already focused
|
||||
if (focusedProcessId === processId) {
|
||||
// Clear focus
|
||||
setHighlightedNodeIds(new Set());
|
||||
setFocusedProcessId(null);
|
||||
} else {
|
||||
// Set focus and cache
|
||||
setHighlightedNodeIds(new Set(nodeIds));
|
||||
setFocusedProcessId(processId);
|
||||
setProcessStepsCache(prev => new Map(prev).set(processId, nodeIds));
|
||||
}
|
||||
}, [focusedProcessId, setHighlightedNodeIds]);
|
||||
|
||||
// Clear focused process when highlights are cleared externally
|
||||
useEffect(() => {
|
||||
if (highlightedNodeIds.size === 0 && focusedProcessId !== null) {
|
||||
setFocusedProcessId(null);
|
||||
}
|
||||
}, [highlightedNodeIds, focusedProcessId]);
|
||||
|
||||
const totalCount = processes.cross.length + processes.intra.length;
|
||||
|
||||
|
||||
if (totalCount === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full p-6 text-center">
|
||||
<div className="w-14 h-14 mb-4 flex items-center justify-center bg-surface rounded-xl">
|
||||
<GitBranch className="w-7 h-7 text-text-muted" />
|
||||
</div>
|
||||
<h3 className="text-base font-medium text-text-primary mb-2">No Processes Detected</h3>
|
||||
<p className="text-sm text-text-secondary max-w-xs">
|
||||
Processes are execution flows traced from entry points. Load a codebase to see detected processes.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header with search */}
|
||||
<div className="p-3 border-b border-border-subtle">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="flex-1 flex items-center gap-2 px-3 py-2 bg-elevated border border-border-subtle rounded-lg focus-within:border-accent focus-within:ring-2 focus-within:ring-accent/20">
|
||||
<Search className="w-4 h-4 text-text-muted" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-text-muted">
|
||||
<span>{totalCount} processes detected</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Process list */}
|
||||
<div className="flex-1 overflow-y-auto scrollbar-thin">
|
||||
{/* View All Processes Card */}
|
||||
<div className="px-4 py-3">
|
||||
<button
|
||||
onClick={handleViewAllProcesses}
|
||||
disabled={loadingProcess !== null}
|
||||
className="w-full flex items-center gap-3 p-3 bg-elevated/40 hover:bg-elevated/80 border border-border-subtle hover:border-cyan-500/30 rounded-xl transition-all group shadow-sm hover:shadow-cyan-900/10 text-left"
|
||||
>
|
||||
<div className="p-2 bg-cyan-500/10 rounded-lg group-hover:bg-cyan-500/20 transition-colors">
|
||||
<Layers className="w-5 h-5 text-cyan-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium text-text-primary group-hover:text-cyan-200">Full Process Map</h4>
|
||||
<p className="text-xs text-text-muted">View combined map of {totalCount} processes</p>
|
||||
</div>
|
||||
{loadingProcess === 'all' ? (
|
||||
<span className="animate-spin mr-1">
|
||||
<Sparkles className="w-4 h-4 text-cyan-400" />
|
||||
</span>
|
||||
) : (
|
||||
<Eye className="w-4 h-4 text-text-muted group-hover:text-cyan-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Cross-Community Section */}
|
||||
{filteredProcesses.cross.length > 0 && (
|
||||
<div className="border-b border-border-subtle">
|
||||
<button
|
||||
onClick={() => toggleSection('cross')}
|
||||
className="w-full flex items-center gap-2 px-4 py-2.5 text-left hover:bg-hover transition-colors"
|
||||
>
|
||||
{expandedSections.has('cross') ? (
|
||||
<ChevronDown className="w-4 h-4 text-text-muted" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-text-muted" />
|
||||
)}
|
||||
<Zap className="w-4 h-4 text-amber-400" />
|
||||
<span className="text-sm font-medium text-text-primary">Cross-Community</span>
|
||||
<span className="ml-auto text-xs text-text-muted bg-surface px-2 py-0.5 rounded-full">
|
||||
{filteredProcesses.cross.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{expandedSections.has('cross') && (
|
||||
<div className="pb-2">
|
||||
{filteredProcesses.cross.map((process) => (
|
||||
<ProcessItem
|
||||
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)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Intra-Community Section */}
|
||||
{filteredProcesses.intra.length > 0 && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => toggleSection('intra')}
|
||||
className="w-full flex items-center gap-2 px-4 py-2.5 text-left hover:bg-hover transition-colors"
|
||||
>
|
||||
{expandedSections.has('intra') ? (
|
||||
<ChevronDown className="w-4 h-4 text-text-muted" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-text-muted" />
|
||||
)}
|
||||
<Home className="w-4 h-4 text-emerald-400" />
|
||||
<span className="text-sm font-medium text-text-primary">Intra-Community</span>
|
||||
<span className="ml-auto text-xs text-text-muted bg-surface px-2 py-0.5 rounded-full">
|
||||
{filteredProcesses.intra.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{expandedSections.has('intra') && (
|
||||
<div className="pb-2">
|
||||
{filteredProcesses.intra.map((process) => (
|
||||
<ProcessItem
|
||||
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)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal */}
|
||||
<ProcessFlowModal
|
||||
process={selectedProcess}
|
||||
onClose={() => setSelectedProcess(null)}
|
||||
onFocusInGraph={handleFocusInGraph}
|
||||
isFullScreen={selectedProcess?.id === 'combined-all'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Individual process item
|
||||
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, 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 (
|
||||
<div className={`flex items-center gap-2 px-4 py-2 mx-2 rounded-lg hover:bg-hover group transition-all ${rowClass}`}>
|
||||
<GitBranch className="w-4 h-4 text-text-muted flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-text-primary truncate">{process.label}</div>
|
||||
<div className="flex items-center gap-2 text-xs text-text-muted">
|
||||
<span>{process.stepCount} steps</span>
|
||||
{process.clusters.length > 0 && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>{process.clusters.length} clusters</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Lightbulb icon - appears on hover, always visible when focused */}
|
||||
<button
|
||||
onClick={onToggleFocus}
|
||||
className={`p-1.5 rounded-md transition-all ${isFocused
|
||||
? 'text-amber-400 hover:text-amber-300 bg-amber-500/20 hover:bg-amber-500/30 border border-amber-400/40 animate-pulse opacity-100'
|
||||
: 'text-text-muted hover:text-cyan-400 bg-white/5 hover:bg-cyan-500/20 border border-white/10 hover:border-cyan-400/40 opacity-0 group-hover:opacity-100'
|
||||
}`}
|
||||
title={isFocused ? 'Click to remove highlight from graph' : 'Click to highlight in graph'}
|
||||
>
|
||||
<Lightbulb className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onView}
|
||||
disabled={isLoading}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-md transition-all disabled:opacity-50 shadow-sm ${isSelected
|
||||
? 'text-cyan-300 bg-cyan-900/60 border border-cyan-400/60 opacity-100'
|
||||
: 'text-cyan-400 hover:text-cyan-300 bg-cyan-950/30 hover:bg-cyan-900/50 border border-cyan-500/30 hover:border-cyan-400/50 opacity-0 group-hover:opacity-100 shadow-cyan-900/20'
|
||||
}`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="animate-pulse">Loading...</span>
|
||||
) : isSelected ? (
|
||||
<>
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
Viewing
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
View
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<HTMLTextAreaElement>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
@ -234,6 +235,21 @@ export const RightPanel = () => {
|
|||
<Activity className="w-3.5 h-3.5" />
|
||||
<span>Activity</span>
|
||||
</button>
|
||||
|
||||
{/* Processes Tab */}
|
||||
<button
|
||||
onClick={() => setActiveTab('processes')}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${activeTab === 'processes'
|
||||
? 'bg-accent/15 text-accent'
|
||||
: 'text-text-muted hover:text-text-primary hover:bg-hover'
|
||||
}`}
|
||||
>
|
||||
<GitBranch className="w-3.5 h-3.5" />
|
||||
<span>Processes</span>
|
||||
<span className="text-[10px] px-1.5 py-0.5 bg-gradient-to-r from-violet-500 to-fuchsia-500 text-white rounded-full font-semibold">
|
||||
NEW
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Close button */}
|
||||
|
|
@ -253,6 +269,13 @@ export const RightPanel = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Processes Tab */}
|
||||
{activeTab === 'processes' && (
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<ProcessesPanel />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Chat Content - only show when chat tab is active */}
|
||||
{activeTab === 'chat' && (
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
loadSettings,
|
||||
saveSettings,
|
||||
getProviderDisplayName,
|
||||
fetchOpenRouterModels,
|
||||
} from '../core/llm/settings-service';
|
||||
import type { LLMSettings, LLMProvider } from '../core/llm/types';
|
||||
|
||||
|
|
@ -46,6 +47,9 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
// Ollama connection state
|
||||
const [ollamaError, setOllamaError] = useState<string | null>(null);
|
||||
const [isCheckingOllama, setIsCheckingOllama] = useState(false);
|
||||
// OpenRouter models state
|
||||
const [openRouterModels, setOpenRouterModels] = useState<Array<{ id: string; name: string }>>([]);
|
||||
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
||||
|
||||
// Load settings when panel opens
|
||||
useEffect(() => {
|
||||
|
|
@ -66,6 +70,14 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
setOllamaError(error);
|
||||
}, []);
|
||||
|
||||
// Load OpenRouter models
|
||||
const loadOpenRouterModels = useCallback(async () => {
|
||||
setIsLoadingModels(true);
|
||||
const models = await fetchOpenRouterModels();
|
||||
setOpenRouterModels(models);
|
||||
setIsLoadingModels(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings.activeProvider === 'ollama') {
|
||||
const baseUrl = settings.ollama?.baseUrl ?? 'http://localhost:11434';
|
||||
|
|
@ -97,7 +109,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const providers: LLMProvider[] = ['openai', 'gemini', 'anthropic', 'azure-openai', 'ollama'];
|
||||
const providers: LLMProvider[] = ['openai', 'gemini', 'anthropic', 'azure-openai', 'ollama', 'openrouter'];
|
||||
|
||||
|
||||
return (
|
||||
|
|
@ -153,7 +165,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
w-8 h-8 rounded-lg flex items-center justify-center text-lg
|
||||
${settings.activeProvider === provider ? 'bg-accent/20' : 'bg-surface'}
|
||||
`}>
|
||||
{provider === 'openai' ? '🤖' : provider === 'gemini' ? '💎' : provider === 'anthropic' ? '🧠' : provider === 'ollama' ? '🦙' : '☁️'}
|
||||
{provider === 'openai' ? '🤖' : provider === 'gemini' ? '💎' : provider === 'anthropic' ? '🧠' : provider === 'ollama' ? '🦙' : provider === 'openrouter' ? '🌐' : '☁️'}
|
||||
</div>
|
||||
<span className="font-medium">{getProviderDisplayName(provider)}</span>
|
||||
</button>
|
||||
|
|
@ -534,6 +546,92 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* OpenRouter Settings */}
|
||||
{settings.activeProvider === 'openrouter' && (
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-text-secondary">
|
||||
<Key className="w-4 h-4" />
|
||||
API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showApiKey['openrouter'] ? 'text' : 'password'}
|
||||
value={settings.openrouter?.apiKey ?? ''}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleApiKeyVisibility('openrouter')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-text-muted hover:text-text-primary transition-colors"
|
||||
>
|
||||
{showApiKey['openrouter'] ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted">
|
||||
Get your API key from{' '}
|
||||
<a
|
||||
href="https://openrouter.ai/keys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent hover:underline"
|
||||
>
|
||||
OpenRouter Keys
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-text-secondary">Model</label>
|
||||
<select
|
||||
value={settings.openrouter?.model ?? ''}
|
||||
onChange={e => setSettings(prev => ({
|
||||
...prev,
|
||||
openrouter: { ...prev.openrouter!, model: e.target.value }
|
||||
}))}
|
||||
onFocus={() => {
|
||||
if (openRouterModels.length === 0 && !isLoadingModels) {
|
||||
loadOpenRouterModels();
|
||||
}
|
||||
}}
|
||||
disabled={isLoadingModels}
|
||||
className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm disabled:opacity-50"
|
||||
>
|
||||
{isLoadingModels ? (
|
||||
<option>Loading models...</option>
|
||||
) : openRouterModels.length === 0 ? (
|
||||
<option value="">Click to load models</option>
|
||||
) : (
|
||||
<>
|
||||
<option value="">Select a model</option>
|
||||
{openRouterModels.map(model => (
|
||||
<option key={model.id} value={model.id}>
|
||||
{model.name}
|
||||
</option>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
<p className="text-xs text-text-muted">
|
||||
Browse all models at{' '}
|
||||
<a
|
||||
href="https://openrouter.ai/models"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent hover:underline"
|
||||
>
|
||||
OpenRouter Models
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Intelligent Clustering Settings */}
|
||||
<div className="space-y-4 animate-fade-in pt-2 border-t border-border-subtle">
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = <T extends LLMProvider>(
|
|||
saveSettings(updated);
|
||||
return updated;
|
||||
}
|
||||
case 'openrouter': {
|
||||
const updated: LLMSettings = {
|
||||
...current,
|
||||
openrouter: {
|
||||
...(current.openrouter ?? {}),
|
||||
...(updates as Partial<Omit<OpenRouterConfig, 'provider'>>),
|
||||
},
|
||||
};
|
||||
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<Array<{ id: string; name: string }>> => {
|
||||
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 [];
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Omit<GeminiConfig, 'provider'>>;
|
||||
anthropic?: Partial<Omit<AnthropicConfig, 'provider'>>;
|
||||
ollama?: Partial<Omit<OllamaConfig, 'provider'>>;
|
||||
openrouter?: Partial<Omit<OpenRouterConfig, 'provider'>>;
|
||||
|
||||
// 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,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<string>();
|
||||
const graphNodeIds = graph.nodes.map(n => n.id);
|
||||
|
|
|
|||
158
gitnexus/src/lib/mermaid-generator.ts
Normal file
158
gitnexus/src/lib/mermaid-generator.ts
Normal file
|
|
@ -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: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<string, ProcessStep[]>();
|
||||
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}<br/><small>${file}</small>"]:::${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}<br/><small>${file}</small>"]:::${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}<br/><small>${file}</small>"]:::${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`;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue