sludge / app.js
NagaYu's picture
Add app.js
7275d68 verified
Raw History Blame Contribute Delete
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[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>
&lt;${esc(n.role)}&gt; ${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("");
}