MonteWalk / ui /tools.py
Obotu's picture
final touches
dc45523
"""
Tools UI Components
Generates enhanced Gradio interfaces for trading tools.
"""
import gradio as gr
import inspect
from typing import Callable, Dict, List
def render_tools_tab(tools_map: Dict[str, List[Callable]]):
gr.Markdown("## 🛠️ Toolbox")
gr.Markdown("Select a tool to get started. Each tool opens in a separate modal for a focused experience.")
# State to track the currently selected tool
current_tool = gr.State(None)
# Modal with simpler structure - no nested groups
with gr.Column(visible=True, elem_id="modal_wrapper", elem_classes="modal-wrapper") as modal:
with gr.Column(elem_classes="modal-content"):
# Header: Title + Close Button
with gr.Row(elem_classes="modal-header"):
tool_title_md = gr.Markdown("## Tool Name", elem_classes="modal-title")
close_button = gr.Button("✕", size="sm", elem_classes="modal-close-btn")
# Body: Description + Inputs + Run + Output
with gr.Column(elem_classes="modal-body"):
tool_desc_md = gr.Markdown("_Description_")
# Inputs
input_components = []
for i in range(5):
input_components.append(
gr.Textbox(
label=f"Parameter {i+1}",
visible=True,
interactive=True,
elem_classes="modal-input"
)
)
# Run button and output
run_button = gr.Button("▶ Run Tool", variant="primary", size="lg")
output = gr.Textbox(
label="Output",
lines=8,
interactive=False,
max_lines=15
)
# Function to show modal with tool info (Content Only)
def update_modal_content(tool: Callable):
tool_name = tool.__name__.replace('_', ' ').title()
tool_doc = ""
if tool.__doc__:
tool_doc = inspect.getdoc(tool)
else:
tool_doc = "No description available."
# Get parameters
sig = inspect.signature(tool)
params = list(sig.parameters.items())
print(f"\n{'='*60}")
print(f"OPENING TOOL (Content Update): {tool_name}")
# Build updates list
updates = []
# 1. Update title
updates.append(f"## {tool_name}")
# 2. Update description
updates.append(f"_{tool_doc}_")
# 3-7. Update 5 input fields
for i in range(5):
if i < len(params):
param_name, param_obj = params[i]
label = param_name.replace('_', ' ').title()
# Infer placeholder
placeholder = ""
pn_lower = param_name.lower()
if "symbol" in pn_lower or "ticker" in pn_lower:
placeholder = "e.g., AAPL"
elif "interval" in pn_lower:
placeholder = "e.g., 1d, 1h, 5m"
elif "period" in pn_lower:
placeholder = "e.g., 1mo, 1y, 5y"
elif "limit" in pn_lower:
placeholder = "e.g., 10"
elif param_obj.annotation is bool:
placeholder = "true/false"
elif param_obj.annotation is int:
placeholder = "integer"
elif param_obj.annotation is float:
placeholder = "number"
# Add type to label
if param_obj.annotation not in (str, inspect._empty):
ann_name = getattr(param_obj.annotation, '__name__', str(param_obj.annotation))
if ann_name != 'str':
label += f" [{ann_name}]"
updates.append(gr.update(
visible=True,
label=label,
placeholder=placeholder,
value=""
))
else:
updates.append(gr.update(visible=False, value=""))
# 8. Clear output
updates.append("")
# 9. Store tool in state
updates.append(tool)
return updates
# JS for opening/closing modal
js_open_modal = """
(args) => {
console.log("Attempting to open modal...");
const modal = document.querySelector('#modal_wrapper');
if (modal) {
console.log("Modal found, adding 'open' class");
modal.classList.add('open');
} else {
console.error("CRITICAL: Modal wrapper #modal_wrapper not found in DOM!");
}
return args;
}
"""
js_close_modal = """
() => {
console.log("Attempting to close modal...");
const modal = document.querySelector('#modal_wrapper');
if (modal) {
console.log("Modal found, removing 'open' class");
modal.classList.remove('open');
}
}
"""
# Function to execute tool
def execute_tool(tool, *input_values):
if tool is None:
return "❌ Error: No tool selected"
try:
sig = inspect.signature(tool)
params = list(sig.parameters.items())
param_count = len(params)
print(f"\n{'='*60}")
print(f"EXECUTING: {tool.__name__}")
print(f"Raw inputs: {input_values[:param_count]}")
# Convert arguments
args = []
for i in range(param_count):
raw_val = input_values[i] if i < len(input_values) else ""
param_name, param_obj = params[i]
param_type = param_obj.annotation
# Skip empty values
if not raw_val or raw_val == "":
args.append(None)
continue
# Type conversion
try:
if param_type is int:
args.append(int(raw_val))
elif param_type is float:
args.append(float(raw_val))
elif param_type is bool:
args.append(raw_val.lower() in ('true', 't', '1', 'yes'))
else:
args.append(raw_val)
except ValueError as e:
return f"❌ Error: Parameter '{param_name}' expects {param_type.__name__}, got '{raw_val}'"
print(f"Converted args: {args}")
result = tool(*args)
print(f"Result: {str(result)[:200]}...")
print(f"{'='*60}\n")
return str(result)
except Exception as e:
error_msg = f"❌ Error: {str(e)}"
print(f"ERROR: {error_msg}\n")
return error_msg
# Generate the tool grid
with gr.Tabs() as tabs:
for category, tools in tools_map.items():
with gr.Tab(category):
with gr.Row(elem_classes="tool-grid"):
for tool in tools:
tool_name = tool.__name__
tool_title = tool_name.replace('_', ' ').title()
description = tool.__doc__.strip().splitlines()[0] if (tool.__doc__ and tool.__doc__.strip()) else "No description available."
with gr.Column(elem_classes="tool-card"):
gr.Markdown(f"### {tool_title}")
gr.Markdown(f"<p class='tool-desc'>{description}</p>")
with gr.Row(elem_classes="tool-tags"):
gr.HTML(f"<span class='tool-tag'>{category}</span>")
open_button = gr.Button("", elem_classes="card-overlay-btn")
def make_click_handler(t):
return lambda: update_modal_content(t)
open_button.click(
fn=make_click_handler(tool),
inputs=[],
outputs=[tool_title_md, tool_desc_md] + input_components + [output, current_tool],
js=js_open_modal
)
# Wire up close button - Pure JS
close_button.click(
fn=None,
inputs=[],
outputs=[],
js=js_close_modal
)
# Wire up run button
run_button.click(
fn=execute_tool,
inputs=[current_tool] + input_components,
outputs=output
)
# Add some CSS for the modal and clickable cards
gr.HTML("""
<style>
/* Make tool-card relative so overlay button positions correctly */
.tool-card {
position: relative !important;
cursor: pointer;
}
/* Overlay Button - covers entire card */
.card-overlay-btn {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
opacity: 0 !important;
z-index: 5 !important;
cursor: pointer !important;
border: none !important;
background: transparent !important;
margin: 0 !important;
padding: 0 !important;
}
/* Modal Styles */
.modal-wrapper {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
background-color: rgba(0, 0, 0, 0.7) !important;
backdrop-filter: blur(8px) !important;
-webkit-backdrop-filter: blur(8px) !important;
display: none !important; /* Hidden by default */
justify-content: center !important;
align-items: center !important;
z-index: 9999 !important;
transition: opacity 0.2s ease;
opacity: 0;
}
/* Class to show the modal */
.modal-wrapper.open {
display: flex !important;
opacity: 1;
}
.modal-content {
background: var(--bg-secondary) !important;
border: 1px solid var(--fill-primary) !important;
border-radius: var(--radius-lg) !important;
padding: 0 !important;
width: 100% !important;
max-width: 500px !important; /* Compact width */
max-height: 85vh !important;
overflow-y: auto !important;
box-shadow: var(--shadow-lg) !important;
position: relative !important;
display: flex !important;
flex-direction: column !important;
animation: modalSlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes modalSlideIn {
from { opacity: 0; transform: translateY(20px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.modal-header {
padding: var(--space-lg) !important;
border-bottom: 1px solid var(--fill-primary) !important;
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
background: transparent !important;
}
.modal-title p {
font-size: 1.1rem !important;
font-weight: 600 !important;
color: var(--text-primary) !important;
margin: 0 !important;
}
.modal-body {
padding: var(--space-lg) !important;
display: flex !important;
flex-direction: column !important;
gap: var(--space-md) !important;
}
.modal-close-btn {
background: transparent !important;
border: 1px solid var(--fill-secondary) !important;
color: var(--text-muted) !important;
font-size: 0.9rem !important;
cursor: pointer !important;
padding: 0.25rem 0.75rem !important;
border-radius: var(--radius-md) !important;
transition: all 0.2s ease !important;
min-width: auto !important;
}
.modal-close-btn:hover {
background: var(--fill-secondary) !important;
color: var(--text-primary) !important;
border-color: var(--text-secondary) !important;
}
/* Hide default Gradio close button */
.modal-wrapper > .close {
display: none !important;
}
/* Custom Scrollbar for modal */
.modal-content::-webkit-scrollbar {
width: 6px;
}
.modal-content::-webkit-scrollbar-track {
background: transparent;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: var(--fill-secondary);
border-radius: 20px;
}
</style>
""")