Download app.js from NagaYu/sludge: direct link, hf CLI and curl.
- Browser
- Download file 10.6 kB
-
https://huggingface.co/spaces/NagaYu/sludge/resolve/main/app.js
- Command line
-
hf download hf://spaces/NagaYu/sludge/app.js
-
curl -L -o app.js https://huggingface.co/spaces/NagaYu/sludge/resolve/main/app.js
10.6 kB
| /** | |
| * Static Space front-end. Wires the browser Sludge to the page. | |
| * | |
| * Everything runs client-side: the pasted interface never leaves the machine. That is a | |
| * deliberate property for this tool — people paste screens from products they are reviewing. | |
| */ | |
| import { SludgeWeb, treeFromHtml } from "./sludge.js"; | |
| import { contrastRatio } from "./featurizer.js"; | |
| const $ = (id) => document.getElementById(id); | |
| const PROFILE_MIN = { triage: 0.25, review: 0.5, audit: 0.75 }; | |
| const esc = (s) => String(s ?? "").replace(/[&<>"]/g, | |
| (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); | |
| const S = new SludgeWeb(); | |
| let lastReport = null; | |
| function setStatus(msg, pct = null) { | |
| $("status").textContent = msg; | |
| if (pct !== null) $("bar").style.width = `${Math.round(pct * 100)}%`; | |
| } | |
| // ---- tabs ------------------------------------------------------------------------------------ | |
| document.querySelectorAll('[role="tab"]').forEach((b) => { | |
| b.addEventListener("click", () => { | |
| document.querySelectorAll('[role="tab"]').forEach((x) => { | |
| x.setAttribute("aria-selected", String(x === b)); | |
| }); | |
| document.querySelectorAll('[role="tabpanel"]').forEach((p) => { | |
| p.classList.toggle("hidden", p.id !== b.dataset.panel); | |
| }); | |
| }); | |
| }); | |
| // ---- boot ------------------------------------------------------------------------------------ | |
| (async () => { | |
| try { | |
| let step = 0; | |
| await S.load("assets", (m, frac) => | |
| setStatus(m, frac === null || frac === undefined ? Math.min(0.9, ++step / 6) : frac)); | |
| setStatus("ready — the model is loaded and runs locally, cached for next time", 1); | |
| const juris = $("juris"); | |
| juris.innerHTML = Object.entries(S.provisions) | |
| .map(([k, v]) => `<option value="${esc(k)}"${k === "eu" ? " selected" : ""}>${esc(v.name)}</option>`) | |
| .join(""); | |
| const templates = [...new Set(S.examples.map((e) => e.template))].sort(); | |
| $("tpl").innerHTML = templates.map((t) => `<option>${esc(t)}</option>`).join(""); | |
| const syncPatterns = () => { | |
| const t = $("tpl").value; | |
| const pats = S.examples.filter((e) => e.template === t).map((e) => e.pattern); | |
| $("pat").innerHTML = pats.map((p) => `<option>${esc(p)}</option>`).join(""); | |
| }; | |
| $("tpl").addEventListener("change", syncPatterns); | |
| syncPatterns(); | |
| $("about").innerHTML = | |
| `Model: <code>${esc(S.meta.model.preset)}</code>, ` + | |
| `${(S.meta.model.parameters / 1e6).toFixed(0)}M parameters, ` + | |
| `${S.meta.pattern_ids.length} classes from published taxonomies · ` + | |
| `<a href="https://huggingface.co/NagaYu/sludge-small">model</a> · ` + | |
| `<a href="https://huggingface.co/datasets/NagaYu/sludge-ui-counterfactuals">dataset</a>`; | |
| renderTaxonomy(); | |
| $("run").disabled = false; | |
| } catch (e) { | |
| setStatus(`could not start: ${e.message}`); | |
| console.error(e); | |
| } | |
| })(); | |
| // ---- example loading --------------------------------------------------------------------------- | |
| $("load").addEventListener("click", () => { | |
| const ex = S.examples.find( | |
| (e) => e.template === $("tpl").value && e.pattern === $("pat").value); | |
| if (!ex) return; | |
| $("input").value = JSON.stringify(ex.unfair, null, 2); | |
| const diffs = ex.diffs.map( | |
| (d) => `<code>${esc(d.node_id)}</code> · <strong>${esc(d.field)}</strong>: ` | |
| + `${esc(String(d.before).slice(0, 34))} → ${esc(String(d.after).slice(0, 34))}`); | |
| $("exnote").innerHTML = | |
| `The tree above is a <strong>fair</strong> screen with exactly one change ` | |
| + `(<code>${esc(ex.edit_scope)}</code>):<br>${diffs.join("<br>")}<br>` | |
| + `Ground-truth evidence: <code>${esc(ex.evidence.join(", "))}</code>`; | |
| }); | |
| // ---- analysis ------------------------------------------------------------------------------------ | |
| $("run").addEventListener("click", async () => { | |
| const raw = $("input").value.trim(); | |
| if (!raw) { setStatus("paste a UI tree or an HTML fragment first"); return; } | |
| let tree; | |
| try { | |
| tree = raw.startsWith("{") ? JSON.parse(raw) : treeFromHtml(raw); | |
| } catch (e) { setStatus(`could not parse that input: ${e.message}`); return; } | |
| if (!tree.nodes || !tree.nodes.length) { setStatus("no elements found in that input"); return; } | |
| $("run").disabled = true; | |
| setStatus("analysing…"); | |
| try { | |
| const pred = await S.predict(tree); | |
| const minScore = PROFILE_MIN[$("profile").value]; | |
| const dets = S.decode(pred, minScore); | |
| const norm = S.fz.normalise(tree); | |
| const hits = S.mapProvisions(dets, $("juris").value); | |
| const fixes = S.remediate(norm, dets); | |
| lastReport = { | |
| determination: "none - Sludge does not determine legality", | |
| disclaimer: S.meta.disclaimer, | |
| profile: $("profile").value, jurisdiction: $("juris").value, | |
| ms: Math.round(pred.ms * 100) / 100, | |
| nodes: norm.nodes.length, screens: norm.screens.length, | |
| detections: dets, findings: hits, remediation: fixes, | |
| }; | |
| renderFindings(norm, dets, fixes); | |
| renderProvisions(hits); | |
| renderFixes(fixes, norm); | |
| $("p-json").innerHTML = | |
| `<pre class="mono">${esc(JSON.stringify(lastReport, null, 2))}</pre>`; | |
| setStatus(`${dets.length} pattern(s) across ${norm.nodes.length} elements on ` | |
| + `${norm.screens.length} screen(s) — model forward ${pred.ms.toFixed(1)} ms`); | |
| } catch (e) { | |
| setStatus(`analysis failed: ${e.message}`); | |
| console.error(e); | |
| } finally { $("run").disabled = false; } | |
| }); | |
| // ---- rendering ------------------------------------------------------------------------------- | |
| function renderFindings(tree, dets, fixes) { | |
| const el = $("p-find"); | |
| if (!dets.length) { | |
| el.innerHTML = `<div class="empty"><strong>No patterns crossed this profile's threshold.</strong> | |
| <p>That is not a clean bill of health: it means nothing crossed the threshold this profile | |
| fixes. A stricter profile, a different jurisdiction table, or a human may reach a different | |
| view.</p></div>`; | |
| return; | |
| } | |
| const byId = new Map(tree.nodes.map((n) => [n.node_id, n])); | |
| const fixOf = new Map(fixes.map((f) => [f.pattern_id, f])); | |
| el.innerHTML = dets.map((d) => { | |
| const p = S.patterns.get(d.pattern_id) || {}; | |
| const ev = d.evidence.map((nid) => { | |
| const n = byId.get(nid); | |
| if (!n) return ""; | |
| const c = contrastRatio(n.color, n.background); | |
| const lbl = (n.aria_label || n.text || "").slice(0, 70); | |
| return `<div class="ev"><span class="id">${esc(nid)}</span> | |
| <${esc(n.role)}> ${lbl ? `“${esc(lbl)}”` : ""} | |
| <div class="props">screen ${n.screen} · ${n.bbox[2].toFixed(0)}×${n.bbox[3].toFixed(0)} px | |
| · ${n.font_size.toFixed(0)} px type · contrast ${c.toFixed(2)}:1${ | |
| n.default_selected ? " · selected by default" : ""}${ | |
| n.disabled ? " · disabled" : ""}</div></div>`; | |
| }).join(""); | |
| const fx = fixOf.get(d.pattern_id); | |
| return `<div class="finding"> | |
| <span class="score">${d.score.toFixed(2)}</span> | |
| <h3>${esc(p.display || d.pattern_id)}</h3> | |
| <div class="cat">${esc(p.category || "")} · <code>${esc(d.pattern_id)}</code> | |
| · evidence is ${esc(p.scope || "element")}-scope | |
| · sources: ${esc((p.sources || []).join(", "))}</div> | |
| <div class="def">${esc(p.definition || "")}</div> | |
| ${ev} | |
| ${fx ? `<div class="fix"><span class="kind">${ | |
| fx.mechanical ? "measured fix" : "advisory — needs new copy"}</span><br>${esc(fx.text)}</div>` : ""} | |
| </div>`; | |
| }).join(""); | |
| } | |
| function renderProvisions(hits) { | |
| const el = $("p-prov"); | |
| if (!hits.length) { | |
| el.innerHTML = `<div class="empty">No provisions in this table are reached by the detected | |
| classes.</div>`; | |
| return; | |
| } | |
| el.innerHTML = `<p style="color:var(--ink-2);font-size:13.5px">Each entry below is a | |
| <strong>pointer for a human to assess</strong>, not a finding of breach.</p>` | |
| + hits.map((h) => `<div class="prov"> | |
| <div class="cite">${esc(h.citation)} — ${esc(h.title)}${ | |
| h.status !== "in_force" ? `<span class="pill">${esc(h.status)}</span>` : ""}</div> | |
| <div class="inst">${esc(h.instrument)}</div> | |
| ${h.scope_note ? `<div class="note">Scope: ${esc(h.scope_note)}</div>` : ""} | |
| ${h.status_note ? `<div class="note">${esc(h.status_note)}</div>` : ""} | |
| <ul>${h.elements.map((e) => | |
| `<li><code>${esc(e.id)}</code> — ${esc(e.requirement)}</li>`).join("")}</ul> | |
| <div class="note">Evidence: <code>${esc(h.evidence.join(", "))}</code> · | |
| via ${esc(h.patterns.join(", "))}</div> | |
| </div>`).join(""); | |
| } | |
| function renderFixes(fixes, tree) { | |
| const el = $("p-fix"); | |
| if (!fixes.length) { el.innerHTML = `<div class="empty">Nothing to change.</div>`; return; } | |
| const mech = fixes.filter((f) => f.mechanical); | |
| const adv = fixes.filter((f) => !f.mechanical); | |
| const rows = (list) => `<table><thead><tr><th>class</th><th>element</th><th>change</th> | |
| </tr></thead><tbody>${list.map((f) => `<tr> | |
| <td><code>${esc(f.pattern_id)}</code></td> | |
| <td><code>${esc(f.node_id)}</code></td> | |
| <td>${esc(f.text)}</td></tr>`).join("")}</tbody></table>`; | |
| el.innerHTML = | |
| (mech.length ? `<h3>Measured changes</h3><p style="color:var(--ink-2);font-size:13.5px"> | |
| Target values are read off this screen — the sibling control, the headline price — not | |
| guessed.</p>${rows(mech)}` : "") | |
| + (adv.length ? `<h3 style="margin-top:22px">Advisory</h3> | |
| <p style="color:var(--ink-2);font-size:13.5px">These need new copy or a new element, so no | |
| target value can be measured from the current screen.</p>${rows(adv)}` : ""); | |
| } | |
| function renderTaxonomy() { | |
| const byCat = new Map(); | |
| for (const p of S.meta.patterns) { | |
| if (!byCat.has(p.category)) byCat.set(p.category, []); | |
| byCat.get(p.category).push(p); | |
| } | |
| $("p-tax").innerHTML = `<p style="color:var(--ink-2);font-size:13.5px"> | |
| ${byCat.size} categories / ${S.meta.patterns.length} classes, all from published work. | |
| Entries marked <em>aggregation</em> are this project's own placement decision, not a claim | |
| from the cited work.</p>` | |
| + [...byCat].map(([cat, ps]) => `<h3>${esc(cat.replace(/_/g, " "))}</h3> | |
| <table><thead><tr><th>class</th><th>definition</th><th>sources</th></tr></thead> | |
| <tbody>${ps.map((p) => `<tr> | |
| <td><code>${esc(p.id)}</code>${ | |
| p.category_assignment !== "source" ? `<span class="pill">aggregation</span>` : ""}</td> | |
| <td>${esc(p.definition)}</td> | |
| <td style="font-size:12px;color:var(--ink-2)">${esc(p.sources.join(", "))}</td> | |
| </tr>`).join("")}</tbody></table>`).join(""); | |
| } | |