fix: the last run's illustration no longer sits under an empty form
Some checks failed
Forgejo Docker Build / Build Docker image (push) Blocked by required conditions
Forgejo Docker Build / Deploy to the host (push) Blocked by required conditions
Forgejo Android APK / Root app tests (push) Successful in 56s
Forgejo Docker Build / Root app tests (push) Successful in 49s
Forgejo Android APK / Build signed APK (push) Has been cancelled

app.js loads a tab's component once and marks it data-loaded, so the DOM
survives leaving and returning. Nothing cleared the result area, and an
illustration from a previous generation stayed on screen under a blank form as
though it were output for a topic nobody had typed. A full page refresh rebuilt
the component and cleared it, which is why it looked like a leak that fixed
itself.

Cleared at the start of a generation, at the start of a modification, and on
re-entering the tab — not on the first visit, where there is nothing to clear.
Covers the illustration area, the searches line, the image-failure line and the
status text.

Also documented what a modification can actually change. The deck vocabulary is
structural — bullets, compare, table, callout, figure, image, section, title —
and none of those carries a colour; the palette is fixed in render_pptx.py and
the model never sees it. So "make it yellow" lands on the only field that takes
a colour, image_prompt, and yellow figures appear on an otherwise blue deck.
That is not modify reaching only the images; it is the model using the one lever
the schema gives it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
Daniel 2026-09-12 17:43:19 +02:00
parent 31e634e0ce
commit 7305443243
3 changed files with 79 additions and 0 deletions

View file

@ -253,6 +253,26 @@ download that works without it.
of Python. Both pip packages are pinned: unpinned, a rebuild from the same commit
could produce different documents.
## What a modification can and cannot change
The deck vocabulary describes *structure*, not *style*: `bullets`, `compare`,
`table`, `callout`, `figure`, `image`, `section`, `title`. None of them carries a
colour. The palette lives in `scripts/render_pptx.py` as fixed constants —
`ACCENT` (#2563EB), `INK`, `MUTED`, `RULE`, `PAPER` — and the model never sees
them.
So an instruction like "make it yellow" has nowhere to land. The model applies it
to the only colour lever it has: the `image_prompt` text, which produces yellow
*figures* on an otherwise blue deck. That is not the modification going only to
the images — it is the model using the one field that accepts a colour at all.
The exception is a `custom` slide, whose shapes take `fill` and `color` (see
`slideShapes.js`). A model can restyle those, but it will rarely rebuild an
ordinary slide as a custom one just to change a colour.
If deck styling should be changeable, the honest fix is a theme — an accent
colour on the resource, passed to the renderer — not a wider slide vocabulary.
## The image library
Library → **Images** is every picture the account has generated, across all

View file

@ -14,6 +14,10 @@
document.addEventListener('tabChanged', function (e) {
if (!e.detail || e.detail.tab !== 'myresources') return;
if (!inited) { init(); inited = true; }
// Reopening the tab is a fresh start, not a resumed one. The DOM survives
// the visit, so without this the previous run's illustration and status sit
// under an empty form as though they belonged to it.
else clearResults();
loadLibrary();
});
@ -162,10 +166,25 @@
el.style.color = tone === 'bad' ? 'var(--red)' : tone === 'good' ? 'var(--green)' : 'var(--g600)';
}
// Everything the last run left on screen. The tab keeps its DOM between
// visits — app.js loads a component once and marks it data-loaded — so an
// illustration from a previous generation stayed visible under an empty form
// when the tab was reopened, looking like output for a topic nobody had
// typed. It cleared on a full page refresh and only then, which is why it
// read as a leak.
function clearResults() {
['mr-images', 'mr-searches', 'mr-image-failures'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.textContent = '';
});
status('');
}
function runGenerate() {
var topic = (document.getElementById('mr-topic') || {}).value || '';
if (!topic.trim()) { status('Enter a topic first.', 'bad'); return; }
clearResults();
var btn = document.getElementById('btn-mr-generate');
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating...'; }
status('Searching the library and writing. This takes a moment.');
@ -605,6 +624,7 @@
if (!instructions) return say('Say what to change.', 'bad');
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Applying'; }
clearResults();
say('Rewriting…');
fetch('/api/my-resources/' + encodeURIComponent(id) + '/refine', {
method: 'POST',

View file

@ -0,0 +1,39 @@
// The tab keeps its DOM between visits (app.js loads a component once and marks
// it data-loaded), so whatever the last run rendered stays on screen. An
// illustration from a previous generation sat under an empty form as though it
// were output for a topic nobody had typed, and only a full page refresh
// cleared it.
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ui = fs.readFileSync(path.join(__dirname, '..', 'public/js/myResources.js'), 'utf8');
test('a new generation starts from a clean result area', () => {
assert.match(ui, /function clearResults\(\)/);
const gen = ui.slice(ui.indexOf('function runGenerate'), ui.indexOf('function reportSearches'));
assert.match(gen, /clearResults\(\);/, 'generate clears before it runs');
});
test('a modification clears it too', () => {
const start = ui.indexOf('function runModify');
const mod = ui.slice(start, ui.indexOf('function renderRow', start));
assert.match(mod, /clearResults\(\);/);
});
test('reopening the tab clears it, but the first visit does not need to', () => {
const tab = ui.slice(ui.indexOf("e.detail.tab !== 'myresources'"), ui.indexOf('var available'));
assert.match(tab, /else clearResults\(\);/,
'only on a revisit — on the first visit there is nothing to clear and init() has not run');
});
test('clearing covers every container a run writes into', () => {
const fn = ui.slice(ui.indexOf('function clearResults'), ui.indexOf('function runGenerate'));
for (const id of ['mr-images', 'mr-searches', 'mr-image-failures']) {
assert.ok(fn.includes(id), id + ' is not cleared');
}
assert.match(fn, /status\(''\)/, 'the status line is part of the debris');
// textContent, not innerHTML: these hold model-derived text and image nodes.
assert.match(fn, /el\.textContent = ''/);
});