diff --git a/app/ui/features/generate/warn.js b/app/ui/features/generate/warn.js index 1cdbbe1b..155bbbb0 100644 --- a/app/ui/features/generate/warn.js +++ b/app/ui/features/generate/warn.js @@ -25,7 +25,11 @@ export default { const slow = c.button.sm({ label: "⚠ Slow torch for int8", tone: "ghost", title: "", onClick: () => app.maintenance?.cuda13(slow.title) }).node; slow.hidden = true; - host.append(chip, plain, keyless, twice, slow); + // Shot camera runs without spaCy, but aims its moves with plain word rules. + const rough = c.button.sm({ label: "Camera targets rougher", tone: "ghost", title: "Shot camera is on, but spaCy is not: camera moves aim with simple word rules and pick worse targets. Switch on Automatic camera and focus in Settings ▸ Modules.", + onClick: () => app.openSettings?.("modules") }).node; + rough.hidden = true; + host.append(chip, plain, keyless, twice, slow, rough); Promise.resolve().then(() => app.api?.torchBuild?.()).then((r) => { if (r && r.slow_int8) { slow.title = r.slow_int8; slow.hidden = false; } }).catch(() => {}); // unknown build: the loader still says it in the log when an int8 model loads @@ -40,9 +44,18 @@ export default { plain.hidden = !ps.allOff?.(); keyless.hidden = !(taste && ps.activeModules().includes(taste) && ps.useful(taste) && !String((ps.currentValues().taste || {}).key || "").trim()); }; + let asking = 0; + const drawRough = () => { + const ps = app.pipeline, cam = ps.modulesById?.().conditioning_shot_camera, ask = ++asking; + if (!cam || !ps.activeModules().includes(cam) || ps.allOff?.()) { rough.hidden = true; return; } + Promise.resolve().then(() => app.api?.addons?.()).then((all) => { + if (ask === asking) rough.hidden = !all || all.some((a) => a.id === "camera" && a.state === "installed"); + }).catch(() => {}); // no add-on list: say nothing + }; drawPlain(); + drawRough(); draw(app.pipeline.slots()); drawTwice(app.pipeline.slots()); - return app.pipeline.subscribe ? app.pipeline.subscribe(() => { draw(app.pipeline.slots()); drawTwice(app.pipeline.slots()); drawPlain(); }) : undefined; + return app.pipeline.subscribe ? app.pipeline.subscribe(() => { draw(app.pipeline.slots()); drawTwice(app.pipeline.slots()); drawPlain(); drawRough(); }) : undefined; }, }; diff --git a/app/ui/features/generate/warn.test.js b/app/ui/features/generate/warn.test.js index 8de5a2d4..12594aaf 100644 --- a/app/ui/features/generate/warn.test.js +++ b/app/ui/features/generate/warn.test.js @@ -61,3 +61,26 @@ test("the slow-torch chip shows the server's sentence when this build runs int8 await tick(); assert.equal(slowChip(host).hidden, true, "an unreadable build is not a claim"); }); + +test("the rougher-camera chip shows only while shot camera is active and the camera add-on is not installed", async () => { + const cam = { id: "conditioning_shot_camera" }; + let active = [cam], listener; + const pipeline = { slots: () => [], allOff: () => false, modulesById: () => ({ conditioning_shot_camera: cam }), activeModules: () => active, + useful: () => false, currentValues: () => ({}), subscribe: (fn) => { listener = fn; return () => {}; } }; + const rough = (host) => [...host.querySelectorAll("button")].find((b) => /Camera targets rougher/.test(b.textContent)); + const tick = () => new Promise((r) => setTimeout(r, 0)); + let state = "available"; + const host = document.createElement("div"); + let opened = null; + warn.setup({ host, app: { pipeline, openSettings: (id) => { opened = id; }, api: { addons: async () => [{ id: "camera", state }] } } }); + await tick(); + assert.equal(rough(host).hidden, false, "camera on, spaCy absent: said"); + rough(host).click(); + assert.equal(opened, "modules"); + state = "installed"; listener(); await tick(); + assert.equal(rough(host).hidden, true, "spaCy installed and on"); + state = "disabled"; listener(); await tick(); + assert.equal(rough(host).hidden, false, "spaCy installed but switched off"); + active = []; listener(); await tick(); + assert.equal(rough(host).hidden, true, "shot camera not in use: nothing to say"); +});