11import { Component , memo , startTransition , useCallback , useEffect , useMemo , useRef , useState } from "react" ;
22import ForceGraph3D from "react-force-graph-3d" ;
3- import * as THREE from "three" ;
43import { getAgentColor , truncate } from "./constants.js" ;
54import { AppIcon } from "./ui-icons.jsx" ;
5+ import {
6+ CONSTELLATION_SHELL_NAME ,
7+ createConstellationShells ,
8+ disposeConstellationShells ,
9+ } from "./brain/ShellGeometry.js" ;
10+ import { applyShellLayout , createShellProjectionForce } from "./brain/ShellLayout.js" ;
611
712const BRAIN_NODE_COLORS = Object . freeze ( {
813 memory : "#22d3ee" ,
@@ -23,16 +28,6 @@ const BRAIN_LEGEND = Object.freeze([
2328const BRAIN_FOCUS_DISTANCE = 136 ;
2429const BRAIN_FOCUS_TRANSITION_MS = 1550 ;
2530const BRAIN_OVERVIEW_LINK_CAP = 96 ;
26- const BRAIN_SHAPE_FORCE = 0.42 ;
27- const BRAIN_JARVIS_SHELL_NAME = "cortex-jarvis-brain-shell" ;
28-
29- const BRAIN_REGIONS = Object . freeze ( [
30- { key : "frontal" , x : 0.88 , y : 0.34 , z : 0.12 } ,
31- { key : "parietal" , x : 0.55 , y : 0.74 , z : - 0.18 } ,
32- { key : "temporal" , x : 0.90 , y : - 0.30 , z : 0.22 } ,
33- { key : "occipital" , x : 0.36 , y : - 0.10 , z : - 0.34 } ,
34- { key : "limbic" , x : 0.30 , y : 0.06 , z : 0.48 } ,
35- ] ) ;
3631
3732// Error boundary to catch Three.js/WebGL crashes
3833class GraphErrorBoundary extends Component {
@@ -88,21 +83,6 @@ function graphNodePosition(node) {
8883 } ;
8984}
9085
91- function hashString ( value ) {
92- let hash = 2166136261 ;
93- const text = String ( value || "" ) ;
94- for ( let index = 0 ; index < text . length ; index += 1 ) {
95- hash ^= text . charCodeAt ( index ) ;
96- hash = Math . imul ( hash , 16777619 ) ;
97- }
98- return hash >>> 0 ;
99- }
100-
101- function seededUnit ( seed , salt = 0 ) {
102- const value = Math . sin ( ( seed + ( salt * 1013 ) ) * 12.9898 ) * 43758.5453 ;
103- return value - Math . floor ( value ) ;
104- }
105-
10686function brainOverviewThreshold ( nodeCount ) {
10787 return Math . max ( 480 , Math . min ( 1080 , Math . sqrt ( Math . max ( nodeCount , 1 ) ) * 72 ) ) ;
10888}
@@ -133,156 +113,6 @@ function isDecisionNode(node) {
133113 return node ?. group === "decision" || node ?. type === "decision" ;
134114}
135115
136- function brainRegionForNode ( node , seed ) {
137- if ( isDecisionNode ( node ) ) return seed % 2 === 0 ? BRAIN_REGIONS [ 0 ] : BRAIN_REGIONS [ 4 ] ;
138- const type = String ( node ?. type || "" ) . toLowerCase ( ) ;
139- if ( type . includes ( "episodic" ) || type . includes ( "conversation" ) ) return BRAIN_REGIONS [ 2 ] ;
140- if ( type . includes ( "summary" ) || type . includes ( "semantic" ) ) return BRAIN_REGIONS [ 1 ] ;
141- if ( type . includes ( "goal" ) || type . includes ( "plan" ) ) return BRAIN_REGIONS [ 0 ] ;
142- return BRAIN_REGIONS [ seed % BRAIN_REGIONS . length ] ;
143- }
144-
145- function brainLayoutPoint ( node , index , total ) {
146- const seed = hashString ( `${ node . id } :${ node . agent } :${ node . type } :${ index } ` ) ;
147- const region = brainRegionForNode ( node , seed ) ;
148- const hemisphere = seed % 2 === 0 ? - 1 : 1 ;
149- const angle = ( index * 2.399963229728653 ) + ( seededUnit ( seed , 2 ) * Math . PI * 2 ) ;
150- const density = 0.28 + ( seededUnit ( seed , 3 ) * 0.72 ) ;
151- const layer = total > 1 ? index / ( total - 1 ) : 0.5 ;
152- const outerX = 120 ;
153- const outerY = 92 ;
154- const outerZ = 74 ;
155- const centralGap = 22 ;
156- const x = hemisphere * ( centralGap + ( region . x * outerX * 0.66 ) )
157- + ( hemisphere * Math . cos ( angle ) * outerX * 0.20 * density ) ;
158- const y = ( ( region . y - 0.18 ) * outerY )
159- + ( Math . sin ( angle ) * outerY * 0.24 * density )
160- + ( Math . sin ( layer * Math . PI ) * 16 ) ;
161- const z = ( region . z * outerZ )
162- + ( Math . sin ( angle * 1.7 ) * outerZ * 0.32 * density ) ;
163-
164- return {
165- brainRegion : region . key ,
166- brainHemisphere : hemisphere < 0 ? "left" : "right" ,
167- brainX : x ,
168- brainY : y ,
169- brainZ : z ,
170- x,
171- y,
172- z,
173- } ;
174- }
175-
176- function applyBrainLayout ( nodes ) {
177- const total = Math . max ( nodes . length , 1 ) ;
178- return nodes . map ( ( node , index ) => ( {
179- ...node ,
180- ...brainLayoutPoint ( node , index , total ) ,
181- } ) ) ;
182- }
183-
184- function createBrainShapeForce ( strength = BRAIN_SHAPE_FORCE ) {
185- let nodes = [ ] ;
186- function force ( alpha ) {
187- const pull = Math . min ( 0.24 , strength * alpha ) ;
188- for ( const node of nodes ) {
189- if ( ! Number . isFinite ( node . brainX ) || ! Number . isFinite ( node . brainY ) || ! Number . isFinite ( node . brainZ ) ) continue ;
190- node . vx += ( node . brainX - ( node . x || 0 ) ) * pull ;
191- node . vy += ( node . brainY - ( node . y || 0 ) ) * pull ;
192- node . vz += ( node . brainZ - ( node . z || 0 ) ) * pull ;
193- }
194- }
195- force . initialize = assignedNodes => { nodes = assignedNodes || [ ] ; } ;
196- return force ;
197- }
198-
199- function createLine ( points , color = "#40e0ff" , opacity = 0.36 ) {
200- const material = new THREE . LineBasicMaterial ( {
201- color,
202- transparent : true ,
203- opacity,
204- depthWrite : false ,
205- blending : THREE . AdditiveBlending ,
206- } ) ;
207- const geometry = new THREE . BufferGeometry ( ) . setFromPoints ( points ) ;
208- return new THREE . Line ( geometry , material ) ;
209- }
210-
211- function hemisphereOutline ( centerX , radiusX , radiusY , zBias , side ) {
212- const points = [ ] ;
213- const segments = 112 ;
214- for ( let index = 0 ; index <= segments ; index += 1 ) {
215- const theta = ( index / segments ) * Math . PI * 2 ;
216- const lobe = 1 + ( Math . sin ( theta * 3 + side ) * 0.035 ) ;
217- const x = centerX + ( Math . cos ( theta ) * radiusX * lobe ) ;
218- const y = Math . sin ( theta ) * radiusY * ( 1 - ( Math . cos ( theta ) * 0.05 ) ) ;
219- const z = zBias + ( Math . sin ( theta * 2 ) * 12 ) + ( Math . cos ( theta ) * side * 8 ) ;
220- points . push ( new THREE . Vector3 ( x , y , z ) ) ;
221- }
222- return points ;
223- }
224-
225- function cortexPath ( centerX , y , width , z , side , phase ) {
226- const points = [ ] ;
227- const segments = 72 ;
228- for ( let index = 0 ; index <= segments ; index += 1 ) {
229- const t = index / segments ;
230- const x = centerX - ( width / 2 ) + ( width * t ) ;
231- const arch = Math . sin ( t * Math . PI ) * 18 ;
232- const wave = Math . sin ( ( t * Math . PI * 5 ) + phase ) * 5 ;
233- points . push ( new THREE . Vector3 ( x , y + arch + wave , z + ( Math . cos ( t * Math . PI ) * side * 7 ) ) ) ;
234- }
235- return points ;
236- }
237-
238- function ellipseRing ( radiusX , radiusY , z , rotationX = 0 , rotationY = 0 ) {
239- const points = [ ] ;
240- const segments = 128 ;
241- const euler = new THREE . Euler ( rotationX , rotationY , 0 ) ;
242- for ( let index = 0 ; index <= segments ; index += 1 ) {
243- const theta = ( index / segments ) * Math . PI * 2 ;
244- const point = new THREE . Vector3 ( Math . cos ( theta ) * radiusX , Math . sin ( theta ) * radiusY , z ) ;
245- point . applyEuler ( euler ) ;
246- points . push ( point ) ;
247- }
248- return points ;
249- }
250-
251- function createJarvisBrainShell ( ) {
252- const group = new THREE . Group ( ) ;
253- group . name = BRAIN_JARVIS_SHELL_NAME ;
254-
255- const lines = [
256- createLine ( hemisphereOutline ( - 76 , 72 , 82 , - 4 , - 1 ) , "#40e0ff" , 0.42 ) ,
257- createLine ( hemisphereOutline ( 76 , 72 , 82 , - 4 , 1 ) , "#40e0ff" , 0.42 ) ,
258- createLine ( hemisphereOutline ( - 76 , 54 , 62 , 18 , - 1 ) , "#40e0ff" , 0.18 ) ,
259- createLine ( hemisphereOutline ( 76 , 54 , 62 , 18 , 1 ) , "#40e0ff" , 0.18 ) ,
260- createLine ( cortexPath ( - 76 , 36 , 92 , 20 , - 1 , 0.2 ) , "#40e0ff" , 0.28 ) ,
261- createLine ( cortexPath ( - 76 , 4 , 106 , 10 , - 1 , 1.7 ) , "#40e0ff" , 0.24 ) ,
262- createLine ( cortexPath ( - 76 , - 32 , 82 , - 2 , - 1 , 2.9 ) , "#ffd166" , 0.16 ) ,
263- createLine ( cortexPath ( 76 , 36 , 92 , 20 , 1 , 1.1 ) , "#40e0ff" , 0.28 ) ,
264- createLine ( cortexPath ( 76 , 4 , 106 , 10 , 1 , 2.4 ) , "#40e0ff" , 0.24 ) ,
265- createLine ( cortexPath ( 76 , - 32 , 82 , - 2 , 1 , 3.2 ) , "#ffd166" , 0.16 ) ,
266- createLine ( [
267- new THREE . Vector3 ( 0 , - 72 , 12 ) ,
268- new THREE . Vector3 ( - 4 , - 40 , 20 ) ,
269- new THREE . Vector3 ( 5 , - 8 , 24 ) ,
270- new THREE . Vector3 ( - 3 , 30 , 18 ) ,
271- new THREE . Vector3 ( 0 , 74 , 10 ) ,
272- ] , "#f8fbff" , 0.22 ) ,
273- createLine ( ellipseRing ( 172 , 110 , - 10 , Math . PI * 0.20 , 0 ) , "#40e0ff" , 0.14 ) ,
274- createLine ( ellipseRing ( 138 , 84 , 24 , Math . PI * 0.34 , Math . PI * 0.12 ) , "#ffd166" , 0.10 ) ,
275- createLine ( ellipseRing ( 118 , 68 , - 32 , Math . PI * 0.48 , - Math . PI * 0.10 ) , "#40e0ff" , 0.11 ) ,
276- ] ;
277-
278- for ( const line of lines ) {
279- line . renderOrder = 1 ;
280- group . add ( line ) ;
281- }
282-
283- return group ;
284- }
285-
286116function brainNodeBaseColor ( node ) {
287117 return isDecisionNode ( node ) ? BRAIN_NODE_COLORS . decision : BRAIN_NODE_COLORS . memory ;
288118}
@@ -392,7 +222,9 @@ function BrainVisualizerComponent({ api = null, cortexBase = "http://127.0.0.1:7
392222 const selectionFrameRef = useRef ( null ) ;
393223 const zoomFrameRef = useRef ( null ) ;
394224 const viewDepthRef = useRef ( "detail" ) ;
225+ const useShellSplitRef = useRef ( true ) ;
395226 const [ graphData , setGraphData ] = useState ( { nodes : [ ] , links : [ ] } ) ;
227+ const [ useShellSplit , setUseShellSplit ] = useState ( true ) ;
396228 const [ loading , setLoading ] = useState ( true ) ;
397229 const [ error , setError ] = useState ( null ) ;
398230 const [ hoverNode , setHoverNode ] = useState ( null ) ;
@@ -409,6 +241,14 @@ function BrainVisualizerComponent({ api = null, cortexBase = "http://127.0.0.1:7
409241 selectedNodeRef . current = selectedNode ;
410242 } , [ selectedNode ] ) ;
411243
244+ useEffect ( ( ) => {
245+ useShellSplitRef . current = useShellSplit ;
246+ setGraphData ( prev => {
247+ if ( ! prev ?. nodes ?. length ) return prev ;
248+ return { ...prev , nodes : applyShellLayout ( prev . nodes , { useShellSplit } ) } ;
249+ } ) ;
250+ } , [ useShellSplit ] ) ;
251+
412252 useEffect ( ( ) => ( ) => {
413253 if ( selectionFrameRef . current ) cancelAnimationFrame ( selectionFrameRef . current ) ;
414254 if ( zoomFrameRef . current ) cancelAnimationFrame ( zoomFrameRef . current ) ;
@@ -445,11 +285,11 @@ function BrainVisualizerComponent({ api = null, cortexBase = "http://127.0.0.1:7
445285 if ( ! scene ) return undefined ;
446286
447287 if ( ! jarvisShellRef . current ) {
448- jarvisShellRef . current = createJarvisBrainShell ( ) ;
288+ jarvisShellRef . current = createConstellationShells ( ) ;
449289 }
450290
451291 const shell = jarvisShellRef . current ;
452- if ( ! scene . getObjectByName ( BRAIN_JARVIS_SHELL_NAME ) ) {
292+ if ( ! scene . getObjectByName ( CONSTELLATION_SHELL_NAME ) ) {
453293 scene . add ( shell ) ;
454294 }
455295
@@ -458,6 +298,13 @@ function BrainVisualizerComponent({ api = null, cortexBase = "http://127.0.0.1:7
458298 } ;
459299 } , [ active , graphData . nodes . length , webglAvailable ] ) ;
460300
301+ useEffect ( ( ) => ( ) => {
302+ if ( jarvisShellRef . current ) {
303+ disposeConstellationShells ( jarvisShellRef . current ) ;
304+ jarvisShellRef . current = null ;
305+ }
306+ } , [ ] ) ;
307+
461308 const syncViewDepth = useCallback ( ( ) => {
462309 const graph = graphRef . current ;
463310 const camera = graph && typeof graph . camera === "function" ? graph . camera ( ) : null ;
@@ -524,7 +371,7 @@ function BrainVisualizerComponent({ api = null, cortexBase = "http://127.0.0.1:7
524371 if ( linkForce && typeof linkForce . strength === "function" ) {
525372 linkForce . strength ( link => Math . min ( 0.32 , Math . max ( 0.04 , Number ( link . weight || 1 ) * 0.08 ) ) ) ;
526373 }
527- if ( typeof graph . d3Force === "function" ) graph . d3Force ( "brainShape " , createBrainShapeForce ( ) ) ;
374+ if ( typeof graph . d3Force === "function" ) graph . d3Force ( "shellProjection " , createShellProjectionForce ( ) ) ;
528375 if ( typeof graph . d3ReheatSimulation === "function" ) graph . d3ReheatSimulation ( ) ;
529376 } catch {
530377 // Force tuning is best-effort; the graph should still render with library defaults.
@@ -678,7 +525,7 @@ function BrainVisualizerComponent({ api = null, cortexBase = "http://127.0.0.1:7
678525 // Final validation — ensure all link targets exist
679526 const validLinks = links . filter ( l => nodeIds . has ( l . source ) && nodeIds . has ( l . target ) ) ;
680527
681- setGraphData ( { nodes : applyBrainLayout ( nodes ) , links : validLinks } ) ;
528+ setGraphData ( { nodes : applyShellLayout ( nodes , { useShellSplit : useShellSplitRef . current } ) , links : validLinks } ) ;
682529 setLoading ( false ) ;
683530 } catch ( err ) {
684531 setError ( err . message ) ;
0 commit comments