@@ -13,6 +13,7 @@ interface TreeNode {
1313 visits ?: number ;
1414 feedback ?: string ;
1515 reward ?: number ;
16+ isSimulated ?: boolean ; // Flag to track newly simulated nodes
1617}
1718
1819interface Message {
@@ -31,8 +32,10 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
3132 const tooltipRef = useRef < HTMLDivElement | null > ( null ) ;
3233 const { theme } = useTheme ( ) ;
3334 const [ selectedNodeId , setSelectedNodeId ] = useState < number | null > ( null ) ;
35+ const [ simulationStartNodeId , setSimulationStartNodeId ] = useState < number | null > ( null ) ; // Track simulation starting node (existing node)
3436 const [ treeNodes , setTreeNodes ] = useState < TreeNode [ ] > ( [ ] ) ;
3537 const [ containerWidth , setContainerWidth ] = useState < number > ( 0 ) ;
38+ const [ simulatedNodes , setSimulatedNodes ] = useState < number [ ] > ( [ ] ) ; // Keep track of new simulated node IDs
3639
3740 // Set up resize observer to make the visualization responsive
3841 useEffect ( ( ) => {
@@ -71,27 +74,91 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
7174
7275 let updatedTreeNodes : TreeNode [ ] = [ ...treeNodes ] ;
7376 let newSelectedNodeId = selectedNodeId ;
77+ let newSimulationStartNodeId = simulationStartNodeId ;
78+ let newSimulatedNodes = [ ...simulatedNodes ] ;
7479 let hasChanges = false ;
7580
7681 messages . forEach ( msg => {
7782 try {
7883 const data = JSON . parse ( msg . content ) ;
7984
80- // Handle node selection updates
85+ // Handle regular node selection (during tree expansion/evaluation)
8186 if ( data . type === 'node_selected' && data . node_id !== undefined ) {
8287 newSelectedNodeId = data . node_id ;
8388 hasChanges = true ;
8489 }
8590
91+ // Handle simulation start node selection (existing node highlighted as simulation start)
92+ if ( data . type === 'node_selected_for_simulation' && data . node_id !== undefined ) {
93+ newSimulationStartNodeId = data . node_id ;
94+ hasChanges = true ;
95+ }
96+
8697 // Handle tree structure updates
87- if ( data . type === 'tree_update_node_expansion' && Array . isArray ( data . tree ) ) {
88- updatedTreeNodes = data . tree ;
98+ if ( ( data . type === 'tree_update_node_expansion' || data . type === 'tree_update_node_children_evaluation' )
99+ && Array . isArray ( data . tree ) ) {
100+ // Preserve simulation flags when updating from tree
101+ if ( updatedTreeNodes . some ( node => node . isSimulated ) ) {
102+ // Find all nodes with isSimulated flag
103+ const simulatedNodeMap = new Map ( ) ;
104+ updatedTreeNodes . forEach ( node => {
105+ if ( node . isSimulated ) {
106+ simulatedNodeMap . set ( node . id , true ) ;
107+ }
108+ } ) ;
109+
110+ // Apply the flag to the updated tree
111+ updatedTreeNodes = data . tree . map ( ( node : TreeNode ) => ( {
112+ ...node ,
113+ isSimulated : simulatedNodeMap . has ( node . id ) ? true : false
114+ } ) ) ;
115+ } else {
116+ updatedTreeNodes = data . tree ;
117+ }
89118 hasChanges = true ;
90119 }
120+
121+ // Handle simulated node creation
122+ if ( data . type === 'node_simulated' && data . node_id !== undefined && data . parent_id !== undefined ) {
123+ // Check if the node already exists in the tree
124+ const nodeExists = updatedTreeNodes . some ( node => node . id === data . node_id ) ;
125+
126+ if ( ! nodeExists ) {
127+ // Add the new simulated node to the tree
128+ updatedTreeNodes . push ( {
129+ id : data . node_id ,
130+ parent_id : data . parent_id ,
131+ action : data . action ,
132+ description : data . description ,
133+ isSimulated : true , // Mark as simulated
134+ } ) ;
135+
136+ // Add to our list of simulated nodes
137+ newSimulatedNodes . push ( data . node_id ) ;
138+ hasChanges = true ;
139+ } else {
140+ // If node already exists, update it to mark as simulated
141+ updatedTreeNodes = updatedTreeNodes . map ( node =>
142+ node . id === data . node_id ? { ...node , isSimulated : true } : node
143+ ) ;
144+
145+ if ( ! newSimulatedNodes . includes ( data . node_id ) ) {
146+ newSimulatedNodes . push ( data . node_id ) ;
147+ hasChanges = true ;
148+ }
149+ }
150+ }
91151
92- // Handle node evaluation updates
93- if ( data . type === 'tree_update_node_evaluation' && Array . isArray ( data . tree ) ) {
94- updatedTreeNodes = data . tree ;
152+ // Handle simulation removal
153+ if ( data . type === 'removed_simulation' ) {
154+ // Remove simulation flags instead of removing nodes
155+ updatedTreeNodes = updatedTreeNodes . map ( node => ( {
156+ ...node ,
157+ isSimulated : false // Remove simulation flag
158+ } ) ) ;
159+
160+ newSimulatedNodes = [ ] ; // Clear simulated nodes list
161+ newSimulationStartNodeId = null ; // Clear simulation start node
95162 hasChanges = true ;
96163 }
97164 } catch {
@@ -102,8 +169,10 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
102169 if ( hasChanges ) {
103170 setTreeNodes ( updatedTreeNodes ) ;
104171 setSelectedNodeId ( newSelectedNodeId ) ;
172+ setSimulationStartNodeId ( newSimulationStartNodeId ) ;
173+ setSimulatedNodes ( newSimulatedNodes ) ;
105174 }
106- } , [ messages , treeNodes , selectedNodeId ] ) ;
175+ } , [ messages ] ) ;
107176
108177 // Render the tree visualization
109178 useEffect ( ( ) => {
@@ -136,6 +205,7 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
136205 . style ( "z-index" , "1000" )
137206 . style ( "max-width" , "400px" )
138207 . style ( "box-shadow" , "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)" )
208+ . style ( "line-height" , "1.5" )
139209 . node ( ) as HTMLDivElement ;
140210 } ;
141211
@@ -187,9 +257,41 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
187257 return `M${ sourceY } ,${ sourceX } C${ ( sourceY + targetY ) / 2 } ,${ sourceX } ${ ( sourceY + targetY ) / 2 } ,${ targetX } ${ targetY } ,${ targetX } ` ;
188258 } )
189259 . attr ( "fill" , "none" )
190- . attr ( "stroke" , theme === 'dark' ? "#9CA3AF" : "#6B7280" )
191- . attr ( "stroke-width" , 1.5 )
192- . attr ( "stroke-opacity" , 0.7 ) ;
260+ . attr ( "stroke" , d => {
261+ // Link to a simulated node gets an orange color
262+ if ( d . target . data . isSimulated ) {
263+ return theme === 'dark' ? "#F97316" : "#FB923C" ; // Orange for simulated paths
264+ }
265+
266+ // Link from simulation start node
267+ if ( d . source . data . id === simulationStartNodeId ) {
268+ return theme === 'dark' ? "#10B981" : "#34D399" ; // Green for simulation start path
269+ }
270+
271+ // Default link color
272+ return theme === 'dark' ? "#9CA3AF" : "#6B7280" ;
273+ } )
274+ . attr ( "stroke-width" , d => {
275+ // Thicker link for simulation paths
276+ if ( d . target . data . isSimulated || d . source . data . id === simulationStartNodeId ) {
277+ return 2.5 ;
278+ }
279+ return 1.5 ;
280+ } )
281+ . attr ( "stroke-opacity" , d => {
282+ // More visible for simulation paths
283+ if ( d . target . data . isSimulated || d . source . data . id === simulationStartNodeId ) {
284+ return 0.9 ;
285+ }
286+ return 0.7 ;
287+ } )
288+ . attr ( "stroke-dasharray" , d => {
289+ // Dashed line for simulation paths
290+ if ( d . target . data . isSimulated ) {
291+ return "5,3" ;
292+ }
293+ return null ;
294+ } ) ;
193295
194296 // Create node groups
195297 const nodes = g . selectAll ( ".node" )
@@ -203,6 +305,16 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
203305 nodes . append ( "circle" )
204306 . attr ( "r" , 12 )
205307 . attr ( "fill" , d => {
308+ // Simulated node (orange)
309+ if ( d . data . isSimulated ) {
310+ return theme === 'dark' ? "#F97316" : "#FDBA74" ; // Orange for simulated nodes
311+ }
312+
313+ // Simulation start node (green)
314+ if ( d . data . id === simulationStartNodeId ) {
315+ return theme === 'dark' ? "#10B981" : "#34D399" ; // Green for simulation start node
316+ }
317+
206318 // Selected node (blue)
207319 if ( d . data . id === selectedNodeId ) {
208320 return theme === 'dark' ? "#3B82F6" : "#60A5FA" ;
@@ -216,12 +328,29 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
216328 // Action node (default)
217329 return theme === 'dark' ? "#4B5563" : "#E5E7EB" ;
218330 } )
219- . attr ( "stroke" , d => d . data . id === selectedNodeId
220- ? theme === 'dark' ? "#93C5FD" : "#2563EB"
221- : theme === 'dark' ? "#374151" : "#D1D5DB" )
222- . attr ( "stroke-width" , d => d . data . id === selectedNodeId ? 3 : 2 ) ;
331+ . attr ( "stroke" , d => {
332+ if ( d . data . isSimulated ) {
333+ return theme === 'dark' ? "#EA580C" : "#F97316" ; // Darker orange stroke for simulated nodes
334+ }
335+
336+ if ( d . data . id === simulationStartNodeId ) {
337+ return theme === 'dark' ? "#059669" : "#10B981" ; // Darker green stroke for simulation start
338+ }
339+
340+ if ( d . data . id === selectedNodeId ) {
341+ return theme === 'dark' ? "#93C5FD" : "#2563EB" ;
342+ }
343+
344+ return theme === 'dark' ? "#374151" : "#D1D5DB" ;
345+ } )
346+ . attr ( "stroke-width" , d => {
347+ if ( d . data . isSimulated || d . data . id === simulationStartNodeId || d . data . id === selectedNodeId ) {
348+ return 3 ;
349+ }
350+ return 2 ;
351+ } ) ;
223352
224- // Add node labels directly on the node circles
353+ // Add node labels with tooltips
225354 nodes . append ( "text" )
226355 . attr ( "dy" , ".35em" )
227356 . attr ( "x" , d => d . children ? - 18 : 18 )
@@ -230,20 +359,28 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
230359 // For root node
231360 if ( d . data . parent_id === null ) return "ROOT" ;
232361
233- // Extract action name from action string
362+ // Show full action string
234363 if ( d . data . action ) {
235- const actionMatch = d . data . action . match ( / ^ ( [ a - z A - Z _ ] + ) \( / ) ;
236- return actionMatch ? actionMatch [ 1 ] : "action" ;
364+ return d . data . action ;
237365 }
238366
239367 return d . data . id . toString ( ) . slice ( - 4 ) ;
240368 } )
241- . attr ( "font-size" , "14px " )
369+ . attr ( "font-size" , "15px " )
242370 . attr ( "font-weight" , "500" )
243371 . attr ( "fill" , d => {
372+ if ( d . data . isSimulated ) {
373+ return theme === 'dark' ? "#FDBA74" : "#C2410C" ; // Orange for simulated node
374+ }
375+
376+ if ( d . data . id === simulationStartNodeId ) {
377+ return theme === 'dark' ? "#A7F3D0" : "#047857" ; // Green for simulation start
378+ }
379+
244380 if ( d . data . id === selectedNodeId ) {
245- return theme === 'dark' ? "#93C5FD" : "#2563EB" ;
381+ return theme === 'dark' ? "#93C5FD" : "#1D4ED8" ; // Blue for selected node
246382 }
383+
247384 return theme === 'dark' ? "#FFFFFF" : "#111827" ;
248385 } ) ;
249386
@@ -265,28 +402,66 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
265402 nodes
266403 . on ( "mouseover" , function ( event , d ) {
267404 if ( tooltipRef . current ) {
268- let content = `<div><strong>Node ID:</strong> ${ d . data . id } </div>` ;
269- if ( d . data . action ) content += `<div><strong>Action:</strong> ${ d . data . action } </div>` ;
270- if ( d . data . description ) content += `<div><strong>Description:</strong> ${ d . data . description } </div>` ;
271- if ( typeof d . data . value === 'number' ) content += `<div><strong>Value:</strong> ${ d . data . value . toFixed ( 2 ) } </div>` ;
272- if ( typeof d . data . reward === 'number' ) content += `<div><strong>Reward:</strong> ${ d . data . reward . toFixed ( 2 ) } </div>` ;
273- if ( typeof d . data . visits === 'number' ) content += `<div><strong>Visits:</strong> ${ d . data . visits } </div>` ;
274- if ( d . data . feedback ) content += `<div><strong>Feedback:</strong> ${ d . data . feedback } </div>` ;
405+ let tooltipContent = '' ;
406+
407+ // Add description if available
408+ if ( d . data . description ) {
409+ tooltipContent += `<p>${ d . data . description } </p>` ;
410+ }
411+
412+ // Add node status information
413+ const nodeInfo = [ ] ;
414+
415+ if ( d . data . id === simulationStartNodeId ) {
416+ nodeInfo . push ( `<span class="font-semibold text-green-600 dark:text-green-400">Simulation Starting Node</span>` ) ;
417+ }
418+
419+ if ( d . data . isSimulated ) {
420+ nodeInfo . push ( `<span class="font-semibold text-orange-600 dark:text-orange-400">Simulated Node</span>` ) ;
421+ }
422+
423+ if ( d . data . id === selectedNodeId ) {
424+ nodeInfo . push ( `<span class="font-semibold text-blue-600 dark:text-blue-400">Selected Node</span>` ) ;
425+ }
426+
427+ if ( nodeInfo . length > 0 ) {
428+ tooltipContent += `<div class="mt-2">${ nodeInfo . join ( ' | ' ) } </div>` ;
429+ }
430+
431+ // Add reward info if available
432+ if ( typeof d . data . reward === 'number' ) {
433+ tooltipContent += `<div class="mt-1">Reward: <span class="font-bold">${ d . data . reward . toFixed ( 2 ) } </span></div>` ;
434+ }
435+
436+ // Add value info if available
437+ if ( typeof d . data . value === 'number' ) {
438+ tooltipContent += `<div>Value: <span class="font-bold">${ d . data . value . toFixed ( 2 ) } </span></div>` ;
439+ }
440+
441+ // Add visits info if available
442+ if ( typeof d . data . visits === 'number' ) {
443+ tooltipContent += `<div>Visits: <span class="font-bold">${ d . data . visits } </span></div>` ;
444+ }
445+
446+ // Add depth info if available
447+ if ( typeof d . data . depth === 'number' ) {
448+ tooltipContent += `<div>Depth: <span class="font-bold">${ d . data . depth } </span></div>` ;
449+ }
275450
276451 const tooltip = d3 . select ( tooltipRef . current ) ;
277452 tooltip . transition ( )
278453 . duration ( 200 )
279454 . style ( "opacity" , .9 ) ;
280- tooltip . html ( content )
455+ tooltip . html ( tooltipContent )
281456 . style ( "left" , ( event . pageX + 15 ) + "px" )
282- . style ( "top" , ( event . pageY - 30 ) + "px" ) ;
457+ . style ( "top" , ( event . pageY - 60 ) + "px" ) ;
283458 }
284459 } )
285460 . on ( "mousemove" , function ( event ) {
286461 if ( tooltipRef . current ) {
287462 d3 . select ( tooltipRef . current )
288463 . style ( "left" , ( event . pageX + 15 ) + "px" )
289- . style ( "top" , ( event . pageY - 30 ) + "px" ) ;
464+ . style ( "top" , ( event . pageY - 28 ) + "px" ) ;
290465 }
291466 } )
292467 . on ( "mouseout" , function ( ) {
@@ -307,7 +482,7 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
307482
308483 svg . call ( zoom ) ;
309484
310- } , [ treeNodes , selectedNodeId , theme , containerWidth ] ) ;
485+ } , [ treeNodes , selectedNodeId , simulationStartNodeId , simulatedNodes , theme , containerWidth ] ) ;
311486
312487 return (
313488 < div className = "w-[30%] bg-white dark:bg-slate-800 rounded-r-lg overflow-hidden" >
@@ -318,6 +493,32 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
318493 </ svg >
319494 Tree Visualization
320495 </ h2 >
496+
497+ { /* Simulation indicator */ }
498+ { simulationStartNodeId && (
499+ < div className = "mt-2 flex items-center" >
500+ < span className = "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200" >
501+ < span className = "w-2 h-2 mr-1 rounded-full bg-green-500" > </ span >
502+ Simulation Mode
503+ </ span >
504+ </ div >
505+ ) }
506+
507+ { /* Legend */ }
508+ < div className = "mt-2 flex flex-wrap gap-2 text-xs" >
509+ < div className = "flex items-center" >
510+ < span className = "w-3 h-3 rounded-full inline-block mr-1 bg-blue-500 dark:bg-blue-600" > </ span >
511+ < span className = "text-gray-700 dark:text-gray-300" > Selected</ span >
512+ </ div >
513+ < div className = "flex items-center" >
514+ < span className = "w-3 h-3 rounded-full inline-block mr-1 bg-green-500 dark:bg-green-600" > </ span >
515+ < span className = "text-gray-700 dark:text-gray-300" > Sim Start</ span >
516+ </ div >
517+ < div className = "flex items-center" >
518+ < span className = "w-3 h-3 rounded-full inline-block mr-1 bg-orange-500 dark:bg-orange-600" > </ span >
519+ < span className = "text-gray-700 dark:text-gray-300" > Simulated</ span >
520+ </ div >
521+ </ div >
321522 </ div >
322523 < div
323524 ref = { containerRef }
0 commit comments