"""Click bridge: design markup in, Python state out. Ported from the Backtest Lab, where this pattern was worked out; the mechanism is identical and only the action vocabulary differs. The design is built from real ` | | delegated listener (installed via demo.load(js=...)) v hidden Textbox #bit-action <- value set via the native setter | v hidden Button #bit-trigger clicked -> Python handler -> re-render A nonce is appended to every action because Gradio's `.change()` only fires when the value actually differs; clicking the same button twice must still register. Nothing here evaluates anything. Actions are `key:value` pairs, the key must be on `ALLOWED_KEYS`, and the value is returned as an opaque string for the caller to validate against its own domain. """ from __future__ import annotations from dataclasses import dataclass # Every action the UI is allowed to emit. An unknown key is dropped, so a # hand-crafted click cannot reach code paths the UI does not offer. ALLOWED_KEYS = frozenset({ "task", # toggle a task filter chip "asset", # toggle an asset-class filter chip "lic", # toggle a license-bucket filter row "verified", # toggle "verified by Bit Trading" "maintained", # toggle "maintained only" -- the graveyard switch "haseval", # toggle "has evaluation" "hasdata", # toggle "has documented training data" "sort", # choose a sort order "clear", # clear every filter "graveyard", # show unmaintained models "open", # open a model's detail drawer "close", # close the drawer "suggest", # submit a model suggestion "q", # the search box (arrives as q=) "suggestbox", # the suggestion input (arrives as suggestbox=) "noop", # explicit no-op, used by non-interactive elements }) ACTION_ELEMENT_ID = "bit-action" TRIGGER_ELEMENT_ID = "bit-trigger" # Separates the action from its nonce. A pipe is used rather than a space # because action values are human-readable names -- "SMA Crossover", "Buy & Hold # (benchmark)" -- which legitimately contain spaces. No value contains a pipe. NONCE_SEP = "|" # The listener body. Installed two ways, because neither alone is reliable: # `gr.Blocks(head=...)` works locally but Hugging Face Spaces serves the page # through its own template and drops it, while `demo.load(js=...)` runs # client-side wherever the page came from. Both call the same idempotent # installer, guarded by a window flag, so running twice is harmless. BRIDGE_JS = """ """.replace("__ELEM_ID__", ACTION_ELEMENT_ID).replace( "__TRIGGER_ID__", TRIGGER_ELEMENT_ID) @dataclass(frozen=True) class Action: key: str value: str @property def is_noop(self) -> bool: return self.key == "noop" def emit(key: str, value: str = "") -> str: """Build the `data-bit` attribute value for a clickable element.""" if key not in ALLOWED_KEYS: raise ValueError(f"{key!r} is not an allowed UI action") if NONCE_SEP in str(value): raise ValueError(f"action values may not contain {NONCE_SEP!r}") return f"{key}:{value}" def parse_action(raw): """Parse what the bridge wrote into the hidden textbox. Returns None for anything unparseable or not on the allow-list, so a malformed or hand-crafted payload is ignored rather than raising. """ if not raw or not isinstance(raw, str): return None payload = raw.split(NONCE_SEP, 1)[0] if ":" not in payload: return None key, _, value = payload.partition(":") key = key.strip() if key not in ALLOWED_KEYS: return None return Action(key=key, value=value.strip()) def parse_pair(value: str): """Split a compound action value like `fast_ma=20` -> ("fast_ma", "20").""" name, _, val = value.partition("=") return name.strip(), val.strip() # `demo.load(js=...)` takes a JS *function*. This is the same installer as # BRIDGE_JS, in the form Gradio's load hook expects, and is the path that # actually works on Hugging Face Spaces. BRIDGE_LOAD_JS = """ () => { if (window.__bitBridgeInstalled) return; window.__bitBridgeInstalled = true; function holder() { var root = document.getElementById('__ELEM_ID__'); return root ? root.querySelector('textarea, input') : null; } function send(payload) { var box = holder(); if (!box) return; var nonce = Date.now().toString(36) + Math.random().toString(36).slice(2, 8); // Setting .value directly does not reliably wake Svelte's binding, so the // native setter is used and both events are dispatched... var proto = box.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype; var setter = Object.getOwnPropertyDescriptor(proto, 'value').set; setter.call(box, payload + '|' + nonce); box.dispatchEvent(new Event('input', { bubbles: true })); box.dispatchEvent(new Event('change', { bubbles: true })); // ...and a hidden Gradio Button is then clicked. A real click on a real // Button is the one event path Gradio always honours, so the handler fires // even if the textbox binding did not register. var trigger = document.getElementById('__TRIGGER_ID__'); var btn = trigger ? (trigger.tagName === 'BUTTON' ? trigger : trigger.querySelector('button')) : null; if (btn) { setTimeout(function () { btn.click(); }, 0); } } document.addEventListener('click', function (e) { var el = e.target && e.target.closest ? e.target.closest('[data-bit]') : null; if (!el) return; var action = el.getAttribute('data-bit'); if (!action || action.indexOf('noop:') === 0) return; e.preventDefault(); e.stopPropagation(); send(action); }, true); function commit(el) { if (!el || !el.hasAttribute || !el.hasAttribute('data-bit-input')) return; send(el.getAttribute('data-bit-input') + '=' + el.value); } document.addEventListener('change', function (e) { commit(e.target); }, true); document.addEventListener('keydown', function (e) { if (e.key === 'Enter') { commit(e.target); } }, true); } """.replace("__ELEM_ID__", ACTION_ELEMENT_ID).replace( "__TRIGGER_ID__", TRIGGER_ELEMENT_ID)