Spaces:
Running on Zero
Running on Zero
CI deploy 13e1bd68
Browse files- app.py +7 -2
- src/ui/bridge.py +48 -2
app.py
CHANGED
|
@@ -28,7 +28,8 @@ from src.runtime import RunError, RunRecord, RunRequest
|
|
| 28 |
from src.ui import components as C
|
| 29 |
from src.ui import compare_tab as CT
|
| 30 |
from src.ui import shell, theme
|
| 31 |
-
from src.ui.bridge import ACTION_ELEMENT_ID, BRIDGE_JS,
|
|
|
|
| 32 |
from src.ui.format import EM, count, esc, money, num, pct, tone
|
| 33 |
|
| 34 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(message)s")
|
|
@@ -635,11 +636,15 @@ def build_app() -> gr.Blocks:
|
|
| 635 |
render_table(runtime.coverage_frame(), empty="no coverage"),
|
| 636 |
extension.status_html())
|
| 637 |
|
|
|
|
|
|
|
|
|
|
| 638 |
demo.load(on_load, [state],
|
| 639 |
[state, left_html, catalog_meta, *compare_out,
|
| 640 |
models_note, acc_plot, cal_plot, model_bars, score_table,
|
| 641 |
sig_panel, runs_table, coverage_kpis, coverage_html,
|
| 642 |
-
extend_panel]
|
|
|
|
| 643 |
|
| 644 |
return demo
|
| 645 |
|
|
|
|
| 28 |
from src.ui import components as C
|
| 29 |
from src.ui import compare_tab as CT
|
| 30 |
from src.ui import shell, theme
|
| 31 |
+
from src.ui.bridge import (ACTION_ELEMENT_ID, BRIDGE_JS, BRIDGE_LOAD_JS,
|
| 32 |
+
parse_action, parse_pair)
|
| 33 |
from src.ui.format import EM, count, esc, money, num, pct, tone
|
| 34 |
|
| 35 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(message)s")
|
|
|
|
| 636 |
render_table(runtime.coverage_frame(), empty="no coverage"),
|
| 637 |
extension.status_html())
|
| 638 |
|
| 639 |
+
# `js=` is what actually installs the click bridge on Spaces: the
|
| 640 |
+
# `head=` injection above works locally but HF serves the page from its
|
| 641 |
+
# own template and drops it. The installer is idempotent.
|
| 642 |
demo.load(on_load, [state],
|
| 643 |
[state, left_html, catalog_meta, *compare_out,
|
| 644 |
models_note, acc_plot, cal_plot, model_bars, score_table,
|
| 645 |
sig_panel, runs_table, coverage_kpis, coverage_html,
|
| 646 |
+
extend_panel],
|
| 647 |
+
js=BRIDGE_LOAD_JS)
|
| 648 |
|
| 649 |
return demo
|
| 650 |
|
src/ui/bridge.py
CHANGED
|
@@ -66,8 +66,11 @@ ACTION_ELEMENT_ID = "bit-action"
|
|
| 66 |
# (benchmark)" -- which legitimately contain spaces. No value contains a pipe.
|
| 67 |
NONCE_SEP = "|"
|
| 68 |
|
| 69 |
-
#
|
| 70 |
-
#
|
|
|
|
|
|
|
|
|
|
| 71 |
BRIDGE_JS = """
|
| 72 |
<script>
|
| 73 |
(function () {
|
|
@@ -157,3 +160,46 @@ def parse_pair(value: str):
|
|
| 157 |
"""Split a compound action value like `fast_ma=20` -> ("fast_ma", "20")."""
|
| 158 |
name, _, val = value.partition("=")
|
| 159 |
return name.strip(), val.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
# (benchmark)" -- which legitimately contain spaces. No value contains a pipe.
|
| 67 |
NONCE_SEP = "|"
|
| 68 |
|
| 69 |
+
# The listener body. Installed two ways, because neither alone is reliable:
|
| 70 |
+
# `gr.Blocks(head=...)` works locally but Hugging Face Spaces serves the page
|
| 71 |
+
# through its own template and drops it, while `demo.load(js=...)` runs
|
| 72 |
+
# client-side wherever the page came from. Both call the same idempotent
|
| 73 |
+
# installer, guarded by a window flag, so running twice is harmless.
|
| 74 |
BRIDGE_JS = """
|
| 75 |
<script>
|
| 76 |
(function () {
|
|
|
|
| 160 |
"""Split a compound action value like `fast_ma=20` -> ("fast_ma", "20")."""
|
| 161 |
name, _, val = value.partition("=")
|
| 162 |
return name.strip(), val.strip()
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
# `demo.load(js=...)` takes a JS *function*. This is the same installer as
|
| 166 |
+
# BRIDGE_JS, in the form Gradio's load hook expects, and is the path that
|
| 167 |
+
# actually works on Hugging Face Spaces.
|
| 168 |
+
BRIDGE_LOAD_JS = """
|
| 169 |
+
() => {
|
| 170 |
+
if (window.__bitBridgeInstalled) return;
|
| 171 |
+
window.__bitBridgeInstalled = true;
|
| 172 |
+
|
| 173 |
+
function holder() {
|
| 174 |
+
var root = document.getElementById('__ELEM_ID__');
|
| 175 |
+
return root ? root.querySelector('textarea, input') : null;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
function send(payload) {
|
| 179 |
+
var box = holder();
|
| 180 |
+
if (!box) return;
|
| 181 |
+
var nonce = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
| 182 |
+
box.value = payload + '|' + nonce;
|
| 183 |
+
box.dispatchEvent(new Event('input', { bubbles: true }));
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
document.addEventListener('click', function (e) {
|
| 187 |
+
var el = e.target && e.target.closest ? e.target.closest('[data-bit]') : null;
|
| 188 |
+
if (!el) return;
|
| 189 |
+
var action = el.getAttribute('data-bit');
|
| 190 |
+
if (!action || action.indexOf('noop:') === 0) return;
|
| 191 |
+
e.preventDefault();
|
| 192 |
+
e.stopPropagation();
|
| 193 |
+
send(action);
|
| 194 |
+
}, true);
|
| 195 |
+
|
| 196 |
+
function commit(el) {
|
| 197 |
+
if (!el || !el.hasAttribute || !el.hasAttribute('data-bit-input')) return;
|
| 198 |
+
send(el.getAttribute('data-bit-input') + '=' + el.value);
|
| 199 |
+
}
|
| 200 |
+
document.addEventListener('change', function (e) { commit(e.target); }, true);
|
| 201 |
+
document.addEventListener('keydown', function (e) {
|
| 202 |
+
if (e.key === 'Enter') { commit(e.target); }
|
| 203 |
+
}, true);
|
| 204 |
+
}
|
| 205 |
+
""".replace("__ELEM_ID__", ACTION_ELEMENT_ID)
|