Spaces:
Running
Running
| """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 `<button>` elements with exact inline styles. | |
| Gradio renders a structurally different DOM for the same concepts -- a | |
| `gr.Radio` is a `<fieldset>` of `<label>`s wrapping hidden inputs -- so | |
| restyling Gradio's controls only ever approximates the design. Instead the | |
| visible layer is the design's own markup, and this module is how a real | |
| `<button>` reaches Python: | |
| <button data-bit="sort:Likes">Likes</button> | |
| | | |
| | 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=<text>) | |
| "suggestbox", # the suggestion input (arrives as suggestbox=<text>) | |
| "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 = """ | |
| <script> | |
| (function () { | |
| if (window.__bitBridgeInstalled) return; | |
| window.__bitBridgeInstalled = true; | |
| function holder() { | |
| var root = document.getElementById('__ELEM_ID__'); | |
| return root ? root.querySelector('textarea, input') : null; | |
| } | |
| 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(); | |
| var box = holder(); | |
| if (!box) return; | |
| // The nonce makes every click a distinct value, so clicking the same | |
| // button twice still fires Gradio's change event. | |
| var nonce = Date.now().toString(36) + Math.random().toString(36).slice(2, 8); | |
| box.value = action + '|' + nonce; | |
| box.dispatchEvent(new Event('input', { bubbles: true })); | |
| }, true); | |
| // Numeric and text fields emit on commit (blur / Enter), not per keystroke, | |
| // so a backtest is not re-run on every digit typed. | |
| function commit(el) { | |
| if (!el || !el.hasAttribute('data-bit-input')) return; | |
| var box = holder(); | |
| if (!box) return; | |
| var nonce = Date.now().toString(36) + Math.random().toString(36).slice(2, 8); | |
| box.value = el.getAttribute('data-bit-input') + '=' + el.value + '|' + nonce; | |
| box.dispatchEvent(new Event('input', { bubbles: true })); | |
| } | |
| document.addEventListener('change', function (e) { commit(e.target); }, true); | |
| document.addEventListener('keydown', function (e) { | |
| if (e.key === 'Enter') { commit(e.target); } | |
| }, true); | |
| })(); | |
| </script> | |
| """.replace("__ELEM_ID__", ACTION_ELEMENT_ID).replace( | |
| "__TRIGGER_ID__", TRIGGER_ELEMENT_ID) | |
| class Action: | |
| key: str | |
| value: str | |
| 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) | |