bit-atlas / src /ui /bridge.py
Bit-Trading-Company's picture
CI deploy local
895687d verified
Raw
History Blame Contribute Delete
8.45 kB
"""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)
@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)