diff --git a/docs/my-resources.md b/docs/my-resources.md index b42ab794..a9a31eb2 100644 --- a/docs/my-resources.md +++ b/docs/my-resources.md @@ -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 diff --git a/public/js/myResources.js b/public/js/myResources.js index 270b24d9..d63d5612 100644 --- a/public/js/myResources.js +++ b/public/js/myResources.js @@ -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 = ' 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 = ' Applying'; } + clearResults(); say('Rewriting…'); fetch('/api/my-resources/' + encodeURIComponent(id) + '/refine', { method: 'POST', diff --git a/test/my-resources-stale-results.test.js b/test/my-resources-stale-results.test.js new file mode 100644 index 00000000..54b98066 --- /dev/null +++ b/test/my-resources-stale-results.test.js @@ -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 = ''/); +});