diff --git a/src/routes/myResources.js b/src/routes/myResources.js index 5579b2b3..fde2d229 100644 --- a/src/routes/myResources.js +++ b/src/routes/myResources.js @@ -490,12 +490,21 @@ router.post('/my-resources/:id/refine', async function (req, res) { 'a URL to fill the gap: leave the References section as it is.'; } - var illustration = sources.wantsImages - ? '\n\nAn illustration tool is available and the author has asked for illustration. ' + - resourceImages.guidance(instructions) + ' Schematic or anatomical teaching artwork only, ' + - 'never a real patient. Returning the markdown is still required; a tool call is not a ' + - 'substitute for it, and no image tag or URL goes into the markdown.' - : ''; + // How a figure is asked for depends on which thing is being edited. A deck + // places figures by declaring them on a slide; markdown has nowhere to put + // one, so it uses the tool. Offering the tool while editing a deck queued a + // figure that no slide referenced — it was generated, paid for, recorded + // against the resource, and never appeared in the export. + var illustration = !sources.wantsImages ? '' + : existingDeck + ? '\n\nThe author has asked for illustration. Add "image_prompt" to the slides that ' + + 'should carry a figure — schematic or anatomical teaching artwork only, never a real ' + + 'patient — and keep the "image_job" value of any slide that already has one. ' + + resourceImages.guidance(instructions) + : '\n\nAn illustration tool is available and the author has asked for illustration. ' + + resourceImages.guidance(instructions) + ' Schematic or anatomical teaching artwork only, ' + + 'never a real patient. Returning the markdown is still required; a tool call is not a ' + + 'substitute for it, and no image tag or URL goes into the markdown.'; // A presentation with a stored deck is edited as a deck. Editing its // markdown instead changed only the markdown: export renders from the deck, @@ -520,12 +529,13 @@ router.post('/my-resources/:id/refine', async function (req, res) { '\n\nMARKDOWN:\n"""\n' + existing.markdown + '\n"""' }]; // The reply restates the whole resource, so it needs room for one. var options = { model: await resolveModel(req.body.model), temperature: 0.2, maxTokens: 16000 }; - var tools = sources.wantsImages ? resourceImages.tools : []; + // Deck mode declares its figures; only the markdown path needs the tool. + var tools = sources.wantsImages && !existingDeck ? resourceImages.tools : []; var callOptions = tools.length ? Object.assign({}, options, { tools: tools }) : options; if (tools.length && resourceImages.requestedCount(instructions)) callOptions.toolChoice = 'required'; var ai = await callAI(messages, callOptions); - if (sources.wantsImages) { + if (sources.wantsImages && !existingDeck) { ai = await resourceImages.dispatch(ai, { owner: req.user.id, body: req.body, subject: subject, imageModel: sources.imageModel, messages: messages, options: options, callAI: callAI @@ -545,6 +555,14 @@ router.post('/my-resources/:id/refine', async function (req, res) { revisedDeck.title = existingDeck.title; revisedDeck.subtitle = existingDeck.subtitle; revisedDeck.date = existingDeck.date; + // Draw whatever the revised deck asked for, the same way generating does, + // so each new figure belongs to the slide that wanted it. + if (sources.wantsImages) { + var drawn = await deckBuild.drawFigures(revisedDeck, { + owner: req.user.id, body: req.body, subject: subject, imageModel: sources.imageModel + }); + ai = Object.assign({}, ai, { imageJobs: drawn.jobs, imageFailures: drawn.failures }); + } } var revised = revisedDeck ? deckSchema.toMarkdown(revisedDeck) diff --git a/test/my-resources.test.js b/test/my-resources.test.js index da58bac8..361b5ce0 100644 --- a/test/my-resources.test.js +++ b/test/my-resources.test.js @@ -279,6 +279,21 @@ test('the library is bounded, searchable, and drives the modify picker', () => { assert.match(js, /library\.length\s*\n?\s*\? 'Nothing matches/); }); +test('a figure asked for while modifying belongs to a slide', () => { + const route = read('src/routes/myResources.js'); + // How a figure is asked for depends on what is being edited. A deck places + // figures by declaring them on a slide; markdown has nowhere to put one, so + // it uses the tool. Offering the tool while editing a deck queued a figure + // that no slide referenced — generated, paid for, recorded against the + // resource, and absent from the export. Measured: 1 recorded, 0 on a slide. + assert.match(route, /var tools = sources\.wantsImages && !existingDeck \? resourceImages\.tools : \[\];/); + assert.match(route, /if \(sources\.wantsImages && !existingDeck\) \{\s*\n\s*ai = await resourceImages\.dispatch/); + // The deck path draws what the revised deck asked for, as generating does. + assert.match(route, /if \(sources\.wantsImages\) \{\s*\n\s*var drawn = await deckBuild\.drawFigures\(revisedDeck/); + // And a slide that already has a figure keeps it. + assert.match(route, /keep the "image_job" value of any slide that already has one/); +}); + test('the figure ids are computed before anything reads them', () => { const route = read('src/routes/myResources.js'); // They were declared inside the slide-review branch, so with no reviewer