Spaces:
Sleeping
Sleeping
| """ | |
| 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> | |
| """) | |