Skip to content

Commit 4b353a9

Browse files
committed
v1 lats visualization done
1 parent 5810d20 commit 4b353a9

File tree

1 file changed

+232
-31
lines changed

1 file changed

+232
-31
lines changed

visual-tree-search-app/components/LATSVisual.tsx

Lines changed: 232 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1819
interface 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-zA-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

Comments
 (0)