Spaces:
Running
Running
File size: 8,447 Bytes
895687d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | """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)
|