fix: modifying a presentation changes the presentation, not just its markdown
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 49s
Forgejo Docker Build / Root app tests (push) Successful in 52s
Forgejo Android APK / Build signed APK (push) Successful in 2m0s
Forgejo Docker Build / Build Docker image (push) Successful in 11s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 49s
Forgejo Docker Build / Root app tests (push) Successful in 52s
Forgejo Android APK / Build signed APK (push) Successful in 2m0s
Forgejo Docker Build / Build Docker image (push) Successful in 11s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
Export renders a presentation from its stored deck. Refine edited the markdown beside it and never touched the deck — so a modification reported success, updated the title and the library row, and produced a byte-identical download. Nothing said otherwise. It looked like the model had ignored the instruction. Measured before the fix, with a marker that was definitely not in the deck: refine succeeded, the stored markdown gained the new slide, the stored deck did not, and the exported pptx did not. After: the export gains the slide and the marker, twelve slides where there were eleven. A presentation with a stored deck is now edited as a deck — the deck goes to the model, a revised deck comes back, and the markdown is serialised from it, which is the same direction generation runs in. Layouts, custom slides and image_job values survive a modification instead of being flattened away. A reply that is not a usable deck is refused rather than saved as markdown: saving it would drop every layout the deck held while looking like it worked, which is the failure this commit exists to remove. Articles have no deck and keep the markdown path unchanged. The reply restates the whole resource, so the token budget is raised to match — the old default was already close to truncating a long deck's markdown. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
parent
4f8e686907
commit
f66daf0c02
2 changed files with 64 additions and 13 deletions
|
|
@ -456,7 +456,7 @@ router.post('/my-resources/:id/refine', async function (req, res) {
|
|||
if (!instructions) return res.status(400).json({ error: 'Say what to change' });
|
||||
|
||||
var existing = await db.get(
|
||||
'SELECT id, kind, topic, markdown FROM user_resources WHERE id = ? AND user_id = ?',
|
||||
'SELECT id, kind, topic, markdown, deck FROM user_resources WHERE id = ? AND user_id = ?',
|
||||
[parseInt(req.params.id, 10), req.user.id]
|
||||
);
|
||||
if (!existing) return res.status(404).json({ error: 'Not found' });
|
||||
|
|
@ -494,15 +494,29 @@ router.post('/my-resources/:id/refine', async function (req, res) {
|
|||
'substitute for it, and no image tag or URL goes into the markdown.'
|
||||
: '';
|
||||
|
||||
// The markdown is the thing being edited, which is the whole reason it is
|
||||
// what gets stored: "change slide 4" is a text edit, not a binary patch.
|
||||
var messages = [{ role: 'user', content:
|
||||
'Revise the following Pandoc markdown according to the instruction. ' +
|
||||
'Return ONLY the complete revised markdown, no commentary, no code fences. ' +
|
||||
'Keep the same overall structure unless the instruction asks otherwise, and keep any ' +
|
||||
'References section at the end.\n\nINSTRUCTION: ' + instructions + illustration + material +
|
||||
'\n\nMARKDOWN:\n"""\n' + existing.markdown + '\n"""' }];
|
||||
var options = { model: await resolveModel(req.body.model), temperature: 0.2 };
|
||||
// A presentation with a stored deck is edited as a deck. Editing its
|
||||
// markdown instead changed only the markdown: export renders from the deck,
|
||||
// so a modification succeeded, said so, and produced an identical download.
|
||||
var existingDeck = existing.deck
|
||||
? (typeof existing.deck === 'string' ? JSON.parse(existing.deck) : existing.deck) : null;
|
||||
if (existingDeck && !(existingDeck.slides || []).length) existingDeck = null;
|
||||
|
||||
var messages = [{ role: 'user', content: existingDeck
|
||||
? 'Revise the following slide deck according to the instruction. Return ONLY the ' +
|
||||
'complete revised JSON object, in exactly the same shape, no commentary and no code ' +
|
||||
'fences. Keep every slide that the instruction does not ask you to change, including ' +
|
||||
'its layout and its "image_job" values, and keep any References slide last.\n\n' +
|
||||
'INSTRUCTION: ' + instructions + illustration + material +
|
||||
'\n\nThe layouts available are:\n' + deckSchema.instructions(
|
||||
(existingDeck.slides || []).length, 0) +
|
||||
'\n\nDECK JSON:\n' + JSON.stringify({ slides: existingDeck.slides })
|
||||
: 'Revise the following Pandoc markdown according to the instruction. ' +
|
||||
'Return ONLY the complete revised markdown, no commentary, no code fences. ' +
|
||||
'Keep the same overall structure unless the instruction asks otherwise, and keep any ' +
|
||||
'References section at the end.\n\nINSTRUCTION: ' + instructions + illustration + material +
|
||||
'\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 : [];
|
||||
var callOptions = tools.length ? Object.assign({}, options, { tools: tools }) : options;
|
||||
if (tools.length && resourceImages.requestedCount(instructions)) callOptions.toolChoice = 'required';
|
||||
|
|
@ -515,7 +529,22 @@ router.post('/my-resources/:id/refine', async function (req, res) {
|
|||
});
|
||||
}
|
||||
|
||||
var revised = String((ai && ai.content) || '').trim();
|
||||
var revisedDeck = existingDeck ? deckBuild.parse(ai && ai.content) : null;
|
||||
if (existingDeck && !revisedDeck) {
|
||||
// A reply that is not a deck would otherwise be saved as the markdown and
|
||||
// silently drop every layout the deck held.
|
||||
console.warn('[my-resources] refine did not return a usable deck; nothing changed');
|
||||
return res.status(502).json({ error: 'That change could not be applied. Try wording it differently.' });
|
||||
}
|
||||
if (revisedDeck) {
|
||||
// Carried through rather than trusted from the reply.
|
||||
revisedDeck.title = existingDeck.title;
|
||||
revisedDeck.subtitle = existingDeck.subtitle;
|
||||
revisedDeck.date = existingDeck.date;
|
||||
}
|
||||
|
||||
var revised = revisedDeck ? deckSchema.toMarkdown(revisedDeck)
|
||||
: String((ai && ai.content) || '').trim();
|
||||
if (!revised) return res.status(502).json({ error: 'The model returned nothing. Try again.' });
|
||||
|
||||
// Figures from a modification are added to the ones already there, not
|
||||
|
|
@ -523,9 +552,10 @@ router.post('/my-resources/:id/refine', async function (req, res) {
|
|||
var added = (ai.imageJobs || []).map(function (job) { return job.jobId; }).filter(Boolean);
|
||||
var row = await db.get(
|
||||
'UPDATE user_resources SET markdown = ?, title = ?, updated_at = NOW(), ' +
|
||||
'image_ids = image_ids || ?::jsonb ' +
|
||||
'image_ids = image_ids || ?::jsonb, deck = COALESCE(?::jsonb, deck) ' +
|
||||
'WHERE id = ? AND user_id = ? RETURNING id, title, updated_at',
|
||||
[revised, firstHeading(revised), JSON.stringify(added), existing.id, req.user.id]
|
||||
[revised, firstHeading(revised), JSON.stringify(added),
|
||||
revisedDeck ? JSON.stringify(revisedDeck) : null, existing.id, req.user.id]
|
||||
);
|
||||
res.json({
|
||||
success: true, resource: row, markdown: revised,
|
||||
|
|
|
|||
|
|
@ -279,6 +279,27 @@ test('the library is bounded, searchable, and drives the modify picker', () => {
|
|||
assert.match(js, /library\.length\s*\n?\s*\? 'Nothing matches/);
|
||||
});
|
||||
|
||||
test('modifying a presentation edits the deck, not only its markdown', () => {
|
||||
const route = read('src/routes/myResources.js');
|
||||
// Export renders from the stored deck. Refine edited the markdown beside it
|
||||
// and left the deck alone, so a modification reported success, updated the
|
||||
// library row, and produced a byte-identical download. Measured: markdown
|
||||
// gained the new slide, the deck did not, and the exported pptx did not.
|
||||
assert.match(route, /SELECT id, kind, topic, markdown, deck FROM user_resources/);
|
||||
assert.match(route, /var revisedDeck = existingDeck \? deckBuild\.parse\(ai && ai\.content\) : null;/);
|
||||
assert.match(route, /deck = COALESCE\(\?::jsonb, deck\)/, 'and the deck is written back');
|
||||
|
||||
// A reply that is not a deck must not be saved as markdown: that would drop
|
||||
// every layout the deck held while looking like it worked.
|
||||
assert.match(route, /refine did not return a usable deck; nothing changed/);
|
||||
assert.match(route, /That change could not be applied\. Try wording it differently\./);
|
||||
|
||||
// An article has no deck and keeps the markdown path.
|
||||
assert.match(route, /: 'Revise the following Pandoc markdown according to the instruction\. '/);
|
||||
// The reply restates the whole resource, so it needs room for one.
|
||||
assert.match(route, /temperature: 0\.2, maxTokens: 16000/);
|
||||
});
|
||||
|
||||
test('modify revises something already generated, in place', () => {
|
||||
const js = read('public/js/myResources.js');
|
||||
// The endpoint existed with no way to reach it: the markdown is what is
|
||||
|
|
|
|||
Loading…
Reference in a new issue