@@ -13,6 +13,7 @@ interface TreeNode {
13
13
visits ?: number ;
14
14
feedback ?: string ;
15
15
reward ?: number ;
16
+ isSimulated ?: boolean ; // Flag to track newly simulated nodes
16
17
}
17
18
18
19
interface Message {
@@ -31,8 +32,10 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
31
32
const tooltipRef = useRef < HTMLDivElement | null > ( null ) ;
32
33
const { theme } = useTheme ( ) ;
33
34
const [ selectedNodeId , setSelectedNodeId ] = useState < number | null > ( null ) ;
35
+ const [ simulationStartNodeId , setSimulationStartNodeId ] = useState < number | null > ( null ) ; // Track simulation starting node (existing node)
34
36
const [ treeNodes , setTreeNodes ] = useState < TreeNode [ ] > ( [ ] ) ;
35
37
const [ containerWidth , setContainerWidth ] = useState < number > ( 0 ) ;
38
+ const [ simulatedNodes , setSimulatedNodes ] = useState < number [ ] > ( [ ] ) ; // Keep track of new simulated node IDs
36
39
37
40
// Set up resize observer to make the visualization responsive
38
41
useEffect ( ( ) => {
@@ -71,27 +74,91 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
71
74
72
75
let updatedTreeNodes : TreeNode [ ] = [ ...treeNodes ] ;
73
76
let newSelectedNodeId = selectedNodeId ;
77
+ let newSimulationStartNodeId = simulationStartNodeId ;
78
+ let newSimulatedNodes = [ ...simulatedNodes ] ;
74
79
let hasChanges = false ;
75
80
76
81
messages . forEach ( msg => {
77
82
try {
78
83
const data = JSON . parse ( msg . content ) ;
79
84
80
- // Handle node selection updates
85
+ // Handle regular node selection (during tree expansion/evaluation)
81
86
if ( data . type === 'node_selected' && data . node_id !== undefined ) {
82
87
newSelectedNodeId = data . node_id ;
83
88
hasChanges = true ;
84
89
}
85
90
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
+
86
97
// 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
+ }
89
118
hasChanges = true ;
90
119
}
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
+ }
91
151
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
95
162
hasChanges = true ;
96
163
}
97
164
} catch {
@@ -102,8 +169,10 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
102
169
if ( hasChanges ) {
103
170
setTreeNodes ( updatedTreeNodes ) ;
104
171
setSelectedNodeId ( newSelectedNodeId ) ;
172
+ setSimulationStartNodeId ( newSimulationStartNodeId ) ;
173
+ setSimulatedNodes ( newSimulatedNodes ) ;
105
174
}
106
- } , [ messages , treeNodes , selectedNodeId ] ) ;
175
+ } , [ messages ] ) ;
107
176
108
177
// Render the tree visualization
109
178
useEffect ( ( ) => {
@@ -136,6 +205,7 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
136
205
. style ( "z-index" , "1000" )
137
206
. style ( "max-width" , "400px" )
138
207
. 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" )
139
209
. node ( ) as HTMLDivElement ;
140
210
} ;
141
211
@@ -187,9 +257,41 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
187
257
return `M${ sourceY } ,${ sourceX } C${ ( sourceY + targetY ) / 2 } ,${ sourceX } ${ ( sourceY + targetY ) / 2 } ,${ targetX } ${ targetY } ,${ targetX } ` ;
188
258
} )
189
259
. 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
+ } ) ;
193
295
194
296
// Create node groups
195
297
const nodes = g . selectAll ( ".node" )
@@ -203,6 +305,16 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
203
305
nodes . append ( "circle" )
204
306
. attr ( "r" , 12 )
205
307
. 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
+
206
318
// Selected node (blue)
207
319
if ( d . data . id === selectedNodeId ) {
208
320
return theme === 'dark' ? "#3B82F6" : "#60A5FA" ;
@@ -216,12 +328,29 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
216
328
// Action node (default)
217
329
return theme === 'dark' ? "#4B5563" : "#E5E7EB" ;
218
330
} )
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
+ } ) ;
223
352
224
- // Add node labels directly on the node circles
353
+ // Add node labels with tooltips
225
354
nodes . append ( "text" )
226
355
. attr ( "dy" , ".35em" )
227
356
. attr ( "x" , d => d . children ? - 18 : 18 )
@@ -230,20 +359,28 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
230
359
// For root node
231
360
if ( d . data . parent_id === null ) return "ROOT" ;
232
361
233
- // Extract action name from action string
362
+ // Show full action string
234
363
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 ;
237
365
}
238
366
239
367
return d . data . id . toString ( ) . slice ( - 4 ) ;
240
368
} )
241
- . attr ( "font-size" , "14px " )
369
+ . attr ( "font-size" , "15px " )
242
370
. attr ( "font-weight" , "500" )
243
371
. 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
+
244
380
if ( d . data . id === selectedNodeId ) {
245
- return theme === 'dark' ? "#93C5FD" : "#2563EB" ;
381
+ return theme === 'dark' ? "#93C5FD" : "#1D4ED8" ; // Blue for selected node
246
382
}
383
+
247
384
return theme === 'dark' ? "#FFFFFF" : "#111827" ;
248
385
} ) ;
249
386
@@ -265,28 +402,66 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
265
402
nodes
266
403
. on ( "mouseover" , function ( event , d ) {
267
404
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
+ }
275
450
276
451
const tooltip = d3 . select ( tooltipRef . current ) ;
277
452
tooltip . transition ( )
278
453
. duration ( 200 )
279
454
. style ( "opacity" , .9 ) ;
280
- tooltip . html ( content )
455
+ tooltip . html ( tooltipContent )
281
456
. style ( "left" , ( event . pageX + 15 ) + "px" )
282
- . style ( "top" , ( event . pageY - 30 ) + "px" ) ;
457
+ . style ( "top" , ( event . pageY - 60 ) + "px" ) ;
283
458
}
284
459
} )
285
460
. on ( "mousemove" , function ( event ) {
286
461
if ( tooltipRef . current ) {
287
462
d3 . select ( tooltipRef . current )
288
463
. style ( "left" , ( event . pageX + 15 ) + "px" )
289
- . style ( "top" , ( event . pageY - 30 ) + "px" ) ;
464
+ . style ( "top" , ( event . pageY - 28 ) + "px" ) ;
290
465
}
291
466
} )
292
467
. on ( "mouseout" , function ( ) {
@@ -307,7 +482,7 @@ const LATSVisual: React.FC<SimpleSearchVisualProps> = ({ messages }) => {
307
482
308
483
svg . call ( zoom ) ;
309
484
310
- } , [ treeNodes , selectedNodeId , theme , containerWidth ] ) ;
485
+ } , [ treeNodes , selectedNodeId , simulationStartNodeId , simulatedNodes , theme , containerWidth ] ) ;
311
486
312
487
return (
313
488
< 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 }) => {
318
493
</ svg >
319
494
Tree Visualization
320
495
</ 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 >
321
522
</ div >
322
523
< div
323
524
ref = { containerRef }
0 commit comments