pi-design-deck 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -14
- package/deck-server.ts +12 -9
- package/form/css/controls.css +121 -0
- package/form/css/layout.css +56 -8
- package/form/css/preview.css +60 -6
- package/form/js/deck-core.js +14 -2
- package/form/js/deck-interact.js +36 -15
- package/form/js/deck-render.js +99 -10
- package/form/js/deck-session.js +128 -30
- package/generate-prompts.ts +17 -9
- package/index.ts +74 -32
- package/model-runner.ts +50 -0
- package/package.json +2 -1
- package/skills/design-deck/SKILL.md +3 -3
package/form/js/deck-render.js
CHANGED
|
@@ -114,6 +114,10 @@ function applyPreviewHtml(preview, previewHtml) {
|
|
|
114
114
|
if (dataset.fonts) {
|
|
115
115
|
preview.dataset.fonts = dataset.fonts;
|
|
116
116
|
}
|
|
117
|
+
// Copy inline styles from source preview div
|
|
118
|
+
if (first.style.cssText) {
|
|
119
|
+
preview.style.cssText = first.style.cssText;
|
|
120
|
+
}
|
|
117
121
|
preview.innerHTML = first.innerHTML;
|
|
118
122
|
return;
|
|
119
123
|
}
|
|
@@ -142,12 +146,12 @@ function optionHint(count) {
|
|
|
142
146
|
return `Choose one - press ${parts.join(" ")} or click`;
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
function createOptionCard(option, slideId,
|
|
149
|
+
function createOptionCard(option, slideId, generatedBy) {
|
|
146
150
|
const card = createElement("div", "option");
|
|
147
151
|
card.setAttribute("role", "radio");
|
|
148
152
|
card.setAttribute("aria-checked", "false");
|
|
149
153
|
card.tabIndex = 0;
|
|
150
|
-
if (
|
|
154
|
+
if (generatedBy !== false) {
|
|
151
155
|
card.classList.add("option-generated");
|
|
152
156
|
}
|
|
153
157
|
card.dataset.value = option.label;
|
|
@@ -164,8 +168,10 @@ function createOptionCard(option, slideId, generated) {
|
|
|
164
168
|
const label = createElement("span", "option-label", option.label);
|
|
165
169
|
header.appendChild(label);
|
|
166
170
|
|
|
167
|
-
if (
|
|
168
|
-
|
|
171
|
+
if (generatedBy !== false) {
|
|
172
|
+
const modelShort = generatedBy ? generatedBy.split("/").pop() : null;
|
|
173
|
+
const badgeText = modelShort ? `Generated by ${modelShort}` : "Generated";
|
|
174
|
+
header.appendChild(createElement("span", "badge-generated", badgeText));
|
|
169
175
|
} else if (option.recommended) {
|
|
170
176
|
header.appendChild(createElement("span", "rec-badge", "Recommended"));
|
|
171
177
|
}
|
|
@@ -184,11 +190,47 @@ function createOptionCard(option, slideId, generated) {
|
|
|
184
190
|
el.tabIndex = -1;
|
|
185
191
|
});
|
|
186
192
|
|
|
193
|
+
// Footer contains aside (optional) + notes input
|
|
194
|
+
const footer = createElement("div", "option-footer");
|
|
195
|
+
|
|
187
196
|
if (option.aside) {
|
|
188
197
|
const aside = createElement("div", "option-aside");
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
// Handle both actual newlines and literal \n sequences
|
|
199
|
+
aside.innerHTML = escapeHtml(option.aside).replace(/\\n/g, "<br>").replace(/\n/g, "<br>");
|
|
200
|
+
footer.appendChild(aside);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Notes input for user instructions
|
|
204
|
+
const notesContainer = createElement("div", "option-notes");
|
|
205
|
+
const notesLabel = createElement("label", "option-notes-label", "Your notes (optional)");
|
|
206
|
+
const notesInput = createElement("textarea", "option-notes-input");
|
|
207
|
+
notesInput.placeholder = "Add notes...";
|
|
208
|
+
notesInput.rows = 1;
|
|
209
|
+
notesInput.dataset.slideId = slideId;
|
|
210
|
+
notesInput.dataset.optionLabel = option.label;
|
|
211
|
+
|
|
212
|
+
// Restore saved notes if this option was previously selected with notes
|
|
213
|
+
if (optionNotes[slideId]?.label === option.label && optionNotes[slideId]?.notes) {
|
|
214
|
+
notesInput.value = optionNotes[slideId].notes;
|
|
191
215
|
}
|
|
216
|
+
|
|
217
|
+
notesInput.addEventListener("input", (e) => {
|
|
218
|
+
const value = e.target.value.trim();
|
|
219
|
+
if (value) {
|
|
220
|
+
optionNotes[slideId] = { label: option.label, notes: value };
|
|
221
|
+
} else if (optionNotes[slideId]?.label === option.label) {
|
|
222
|
+
delete optionNotes[slideId];
|
|
223
|
+
}
|
|
224
|
+
saveSelectionsToStorage();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Prevent click from bubbling to card (which would select it)
|
|
228
|
+
notesInput.addEventListener("click", (e) => e.stopPropagation());
|
|
229
|
+
|
|
230
|
+
notesContainer.appendChild(notesLabel);
|
|
231
|
+
notesContainer.appendChild(notesInput);
|
|
232
|
+
footer.appendChild(notesContainer);
|
|
233
|
+
card.appendChild(footer);
|
|
192
234
|
|
|
193
235
|
if (option.description) {
|
|
194
236
|
card.setAttribute("title", option.description);
|
|
@@ -205,10 +247,33 @@ function createGenerateBar(slideId) {
|
|
|
205
247
|
const bar = createElement("div", "gen-bar");
|
|
206
248
|
|
|
207
249
|
const row = createElement("div", "gen-row");
|
|
250
|
+
|
|
251
|
+
// Generate button with separate count selector
|
|
252
|
+
const genGroup = createElement("div", "gen-group");
|
|
208
253
|
const button = createElement("button", "btn-gen-more");
|
|
209
254
|
button.type = "button";
|
|
210
255
|
button.appendChild(createElement("span", "btn-gen-plus", "+"));
|
|
211
|
-
button.appendChild(document.createTextNode("
|
|
256
|
+
button.appendChild(document.createTextNode("Generate"));
|
|
257
|
+
|
|
258
|
+
const countSelect = document.createElement("select");
|
|
259
|
+
countSelect.className = "gen-count";
|
|
260
|
+
countSelect.setAttribute("aria-label", "Number of options to generate");
|
|
261
|
+
[1, 2, 3].forEach((n) => {
|
|
262
|
+
const opt = document.createElement("option");
|
|
263
|
+
opt.value = n;
|
|
264
|
+
opt.textContent = n;
|
|
265
|
+
countSelect.appendChild(opt);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const countLabel = createElement("span", "gen-count-label", "option");
|
|
269
|
+
countSelect.addEventListener("change", () => {
|
|
270
|
+
const count = parseInt(countSelect.value, 10);
|
|
271
|
+
countLabel.textContent = count === 1 ? "option" : "options";
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
genGroup.appendChild(button);
|
|
275
|
+
genGroup.appendChild(countSelect);
|
|
276
|
+
genGroup.appendChild(countLabel);
|
|
212
277
|
|
|
213
278
|
const regenButton = createElement("button", "btn-regen");
|
|
214
279
|
regenButton.type = "button";
|
|
@@ -229,8 +294,8 @@ function createGenerateBar(slideId) {
|
|
|
229
294
|
e.stopPropagation();
|
|
230
295
|
});
|
|
231
296
|
|
|
232
|
-
button.addEventListener("click", () => generateMore(button, slideId, input));
|
|
233
|
-
row.appendChild(
|
|
297
|
+
button.addEventListener("click", () => generateMore(button, slideId, input, countSelect));
|
|
298
|
+
row.appendChild(genGroup);
|
|
234
299
|
row.appendChild(regenButton);
|
|
235
300
|
row.appendChild(input);
|
|
236
301
|
bar.appendChild(row);
|
|
@@ -299,6 +364,22 @@ function createSummarySlide(index) {
|
|
|
299
364
|
summaryGrid.id = "summary-grid";
|
|
300
365
|
section.appendChild(summaryGrid);
|
|
301
366
|
|
|
367
|
+
// Final instructions textarea
|
|
368
|
+
const finalNotesContainer = createElement("div", "final-notes");
|
|
369
|
+
const finalNotesLabel = createElement("label", "final-notes-label", "Additional instructions (optional)");
|
|
370
|
+
finalNotesLabel.setAttribute("for", "final-notes-input");
|
|
371
|
+
const finalNotesInput = createElement("textarea", "final-notes-input");
|
|
372
|
+
finalNotesInput.id = "final-notes-input";
|
|
373
|
+
finalNotesInput.placeholder = "Add any final notes or instructions for implementation...";
|
|
374
|
+
finalNotesInput.rows = 2;
|
|
375
|
+
finalNotesInput.addEventListener("input", (e) => {
|
|
376
|
+
finalNotes = e.target.value.trim();
|
|
377
|
+
saveSelectionsToStorage();
|
|
378
|
+
});
|
|
379
|
+
finalNotesContainer.appendChild(finalNotesLabel);
|
|
380
|
+
finalNotesContainer.appendChild(finalNotesInput);
|
|
381
|
+
section.appendChild(finalNotesContainer);
|
|
382
|
+
|
|
302
383
|
const submitButton = createElement("button", "btn-generate", "Submit Selections");
|
|
303
384
|
submitButton.type = "button";
|
|
304
385
|
submitButton.id = "btn-generate";
|
|
@@ -380,10 +461,18 @@ function createSummaryCard(slide) {
|
|
|
380
461
|
const text = selectedOption.aside.length > 120
|
|
381
462
|
? selectedOption.aside.slice(0, 120).trimEnd() + "..."
|
|
382
463
|
: selectedOption.aside;
|
|
383
|
-
aside.
|
|
464
|
+
aside.innerHTML = escapeHtml(text).replace(/\\n/g, "<br>").replace(/\n/g, "<br>");
|
|
384
465
|
card.appendChild(aside);
|
|
385
466
|
}
|
|
386
467
|
}
|
|
468
|
+
|
|
469
|
+
// Show user notes if present
|
|
470
|
+
const noteData = optionNotes[slide.id];
|
|
471
|
+
if (noteData && noteData.label === selectedLabel && noteData.notes) {
|
|
472
|
+
const notesDisplay = createElement("div", "summary-notes");
|
|
473
|
+
notesDisplay.innerHTML = `<span class="summary-notes-label">Your notes:</span> ${escapeHtml(noteData.notes)}`;
|
|
474
|
+
card.appendChild(notesDisplay);
|
|
475
|
+
}
|
|
387
476
|
}
|
|
388
477
|
|
|
389
478
|
return card;
|
package/form/js/deck-session.js
CHANGED
|
@@ -74,7 +74,7 @@ function disableDeckInteractions() {
|
|
|
74
74
|
document.querySelectorAll(".btn-gen-more, .btn-regen").forEach((button) => {
|
|
75
75
|
button.disabled = true;
|
|
76
76
|
});
|
|
77
|
-
document.querySelectorAll(".gen-prompt").forEach((input) => {
|
|
77
|
+
document.querySelectorAll(".gen-prompt, .gen-count").forEach((input) => {
|
|
78
78
|
input.disabled = true;
|
|
79
79
|
});
|
|
80
80
|
document.querySelectorAll(".model-pill, .model-list-item, .model-default-check").forEach((el) => {
|
|
@@ -98,7 +98,16 @@ async function submitDeck() {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
try {
|
|
101
|
-
|
|
101
|
+
// Build notes object with only notes for selected options
|
|
102
|
+
const notes = {};
|
|
103
|
+
for (const [slideId, noteData] of Object.entries(optionNotes)) {
|
|
104
|
+
if (noteData && selections[slideId] === noteData.label && noteData.notes) {
|
|
105
|
+
notes[slideId] = noteData.notes;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const payload = { token: sessionToken, selections, notes };
|
|
109
|
+
if (finalNotes) payload.finalNotes = finalNotes;
|
|
110
|
+
await postJson("/submit", payload);
|
|
102
111
|
clearSelectionsStorage();
|
|
103
112
|
isClosed = true;
|
|
104
113
|
if (submitButton) {
|
|
@@ -242,33 +251,39 @@ function restoreGenerateButton(slideId) {
|
|
|
242
251
|
const pending = pendingGenerate.get(slideId);
|
|
243
252
|
if (!pending || pending.isRegen) return;
|
|
244
253
|
|
|
245
|
-
|
|
246
|
-
|
|
254
|
+
// Clear timeout if any
|
|
255
|
+
if (pending.timeoutId) clearTimeout(pending.timeoutId);
|
|
256
|
+
|
|
257
|
+
// Remove all skeletons from DOM (query directly, don't rely on array)
|
|
258
|
+
const slideElement = document.querySelector(`.slide[data-id="${CSS.escape(slideId)}"]`);
|
|
259
|
+
if (slideElement) {
|
|
260
|
+
slideElement.querySelectorAll(".option-skeleton").forEach((skel) => skel.remove());
|
|
247
261
|
}
|
|
262
|
+
|
|
248
263
|
pending.button.classList.remove("loading");
|
|
249
264
|
const plus = pending.button.querySelector(".btn-gen-plus");
|
|
250
265
|
if (plus) plus.textContent = "+";
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
266
|
+
pending.button.lastChild.textContent = "Generate";
|
|
267
|
+
|
|
254
268
|
if (pending.input && !isClosed) pending.input.disabled = false;
|
|
269
|
+
if (pending.countSelect && !isClosed) pending.countSelect.disabled = false;
|
|
255
270
|
|
|
256
271
|
pendingGenerate.delete(slideId);
|
|
257
272
|
}
|
|
258
273
|
|
|
259
|
-
function
|
|
274
|
+
function reapplySelectionAfterInsert(slideElement, slideId) {
|
|
260
275
|
const selected = selections[slideId];
|
|
261
276
|
if (!selected) return;
|
|
262
|
-
insertedOption.classList.add("dimmed");
|
|
263
277
|
const selectedEl = Array.from(slideElement.querySelectorAll(".option")).find(
|
|
264
278
|
(el) => el.dataset.value === selected
|
|
265
279
|
);
|
|
266
280
|
if (selectedEl) {
|
|
267
281
|
selectedEl.classList.add("selected");
|
|
282
|
+
selectedEl.setAttribute("aria-checked", "true");
|
|
268
283
|
}
|
|
269
284
|
}
|
|
270
285
|
|
|
271
|
-
function insertGeneratedOption(slideId, option) {
|
|
286
|
+
function insertGeneratedOption(slideId, option, model) {
|
|
272
287
|
const slide = slides.find((entry) => entry.id === slideId);
|
|
273
288
|
if (!slide) return;
|
|
274
289
|
slide.options.push(option);
|
|
@@ -281,9 +296,9 @@ function insertGeneratedOption(slideId, option) {
|
|
|
281
296
|
|
|
282
297
|
optionsGrid.className = `options ${optionCountClass(slide.options.length, slide.columns)}`;
|
|
283
298
|
|
|
284
|
-
const optionCard = createOptionCard(option, slideId,
|
|
299
|
+
const optionCard = createOptionCard(option, slideId, model);
|
|
285
300
|
optionsGrid.appendChild(optionCard);
|
|
286
|
-
|
|
301
|
+
reapplySelectionAfterInsert(slideElement, slideId);
|
|
287
302
|
equalizeBlockHeights(slideElement);
|
|
288
303
|
|
|
289
304
|
const pick = slideElement.querySelector(".slide-pick");
|
|
@@ -301,6 +316,7 @@ function replaceSlideOptions(slideId, newOptions) {
|
|
|
301
316
|
slide.options = newOptions;
|
|
302
317
|
|
|
303
318
|
delete selections[slideId];
|
|
319
|
+
delete optionNotes[slideId];
|
|
304
320
|
saveSelectionsToStorage();
|
|
305
321
|
|
|
306
322
|
const slideElement = document.querySelector(`.slide[data-id="${CSS.escape(slideId)}"]`);
|
|
@@ -309,16 +325,28 @@ function replaceSlideOptions(slideId, newOptions) {
|
|
|
309
325
|
const optionsGrid = slideElement.querySelector(".options");
|
|
310
326
|
if (!optionsGrid) return;
|
|
311
327
|
|
|
328
|
+
// Remove regeneration overlay and state
|
|
329
|
+
const overlay = optionsGrid.querySelector(".regen-overlay");
|
|
330
|
+
if (overlay) overlay.remove();
|
|
331
|
+
optionsGrid.classList.remove("regenerating");
|
|
332
|
+
optionsGrid.style.position = "";
|
|
333
|
+
|
|
312
334
|
optionsGrid.innerHTML = "";
|
|
313
|
-
optionsGrid.style.opacity = "";
|
|
314
|
-
optionsGrid.style.pointerEvents = "";
|
|
315
335
|
optionsGrid.className = `options ${optionCountClass(newOptions.length, slide.columns)}`;
|
|
316
336
|
|
|
317
337
|
newOptions.forEach((option) => {
|
|
318
338
|
const card = createOptionCard(option, slideId, false);
|
|
339
|
+
card.classList.add("option-regenerated");
|
|
319
340
|
optionsGrid.appendChild(card);
|
|
320
341
|
});
|
|
321
342
|
|
|
343
|
+
// Remove animation class after animation completes
|
|
344
|
+
setTimeout(() => {
|
|
345
|
+
optionsGrid.querySelectorAll(".option-regenerated").forEach((el) => {
|
|
346
|
+
el.classList.remove("option-regenerated");
|
|
347
|
+
});
|
|
348
|
+
}, 500);
|
|
349
|
+
|
|
322
350
|
equalizeBlockHeights(slideElement);
|
|
323
351
|
|
|
324
352
|
const pick = slideElement.querySelector(".slide-pick");
|
|
@@ -343,8 +371,37 @@ function connectEvents() {
|
|
|
343
371
|
if (!payload || typeof payload.slideId !== "string" || !payload.option) {
|
|
344
372
|
return;
|
|
345
373
|
}
|
|
346
|
-
|
|
347
|
-
|
|
374
|
+
const pending = pendingGenerate.get(payload.slideId);
|
|
375
|
+
const model = pending?.model || null;
|
|
376
|
+
|
|
377
|
+
// Remove one skeleton
|
|
378
|
+
if (pending?.skeletons?.length > 0) {
|
|
379
|
+
const skel = pending.skeletons.shift();
|
|
380
|
+
if (skel.parentElement) skel.remove();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
insertGeneratedOption(payload.slideId, payload.option, model);
|
|
384
|
+
|
|
385
|
+
// Track received count and restore button when all received
|
|
386
|
+
if (pending) {
|
|
387
|
+
pending.receivedCount = (pending.receivedCount || 0) + 1;
|
|
388
|
+
if (pending.receivedCount >= (pending.expectedCount || 1)) {
|
|
389
|
+
restoreGenerateButton(payload.slideId);
|
|
390
|
+
} else {
|
|
391
|
+
// Reset timeout for next option
|
|
392
|
+
if (pending.timeoutId) clearTimeout(pending.timeoutId);
|
|
393
|
+
pending.timeoutId = setTimeout(() => {
|
|
394
|
+
const current = pendingGenerate.get(payload.slideId);
|
|
395
|
+
if (!current || current.isRegen) return;
|
|
396
|
+
const received = current.receivedCount || 0;
|
|
397
|
+
const expected = current.expectedCount || 1;
|
|
398
|
+
restoreGenerateButton(payload.slideId);
|
|
399
|
+
if (received < expected) {
|
|
400
|
+
showSaveToast(`Generated ${received} of ${expected} options`, true);
|
|
401
|
+
}
|
|
402
|
+
}, 30000);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
348
405
|
});
|
|
349
406
|
|
|
350
407
|
events.addEventListener("generate-failed", (event) => {
|
|
@@ -358,6 +415,8 @@ function connectEvents() {
|
|
|
358
415
|
restoreGenerateButton(payload.slideId);
|
|
359
416
|
if (payload.reason === "timeout") {
|
|
360
417
|
showSaveToast("Generation timed out — try again", true);
|
|
418
|
+
} else {
|
|
419
|
+
showSaveToast("Generation failed", true);
|
|
361
420
|
}
|
|
362
421
|
}
|
|
363
422
|
});
|
|
@@ -387,6 +446,8 @@ function connectEvents() {
|
|
|
387
446
|
restoreRegenButton(payload.slideId);
|
|
388
447
|
if (payload.reason === "timeout") {
|
|
389
448
|
showSaveToast("Regeneration timed out — try again", true);
|
|
449
|
+
} else {
|
|
450
|
+
showSaveToast("Regeneration failed", true);
|
|
390
451
|
}
|
|
391
452
|
}
|
|
392
453
|
});
|
|
@@ -414,7 +475,7 @@ function connectEvents() {
|
|
|
414
475
|
});
|
|
415
476
|
}
|
|
416
477
|
|
|
417
|
-
async function generateMore(button, slideId, input) {
|
|
478
|
+
async function generateMore(button, slideId, input, countSelect) {
|
|
418
479
|
if (isClosed || pendingGenerate.size > 0) return;
|
|
419
480
|
|
|
420
481
|
const slideElement = document.querySelector(`.slide[data-id="${CSS.escape(slideId)}"]`);
|
|
@@ -427,21 +488,42 @@ async function generateMore(button, slideId, input) {
|
|
|
427
488
|
|
|
428
489
|
const prompt = input ? input.value.trim() : "";
|
|
429
490
|
if (input) input.value = "";
|
|
491
|
+
|
|
492
|
+
const count = countSelect ? parseInt(countSelect.value, 10) || 1 : 1;
|
|
493
|
+
|
|
494
|
+
// Create skeletons for each option being generated
|
|
495
|
+
const skeletons = [];
|
|
496
|
+
for (let i = 0; i < count; i++) {
|
|
497
|
+
const skeleton = createElement("div", "option-skeleton");
|
|
498
|
+
optionsGrid.appendChild(skeleton);
|
|
499
|
+
skeletons.push(skeleton);
|
|
500
|
+
}
|
|
430
501
|
|
|
431
|
-
const skeleton = createElement("div", "option-skeleton");
|
|
432
|
-
optionsGrid.appendChild(skeleton);
|
|
433
|
-
|
|
434
|
-
const originalText = button.childNodes[1] ? button.childNodes[1].textContent || "" : "";
|
|
435
502
|
button.classList.add("loading");
|
|
436
503
|
const plus = button.querySelector(".btn-gen-plus");
|
|
437
504
|
if (plus) plus.textContent = "";
|
|
438
|
-
|
|
505
|
+
button.lastChild.textContent = "Generating...";
|
|
439
506
|
if (input) input.disabled = true;
|
|
507
|
+
if (countSelect) countSelect.disabled = true;
|
|
508
|
+
|
|
509
|
+
// Set timeout for generation (30s per option)
|
|
510
|
+
const timeoutId = setTimeout(() => {
|
|
511
|
+
const pending = pendingGenerate.get(slideId);
|
|
512
|
+
if (pending && !pending.isRegen) {
|
|
513
|
+
const received = pending.receivedCount || 0;
|
|
514
|
+
restoreGenerateButton(slideId);
|
|
515
|
+
if (received === 0) {
|
|
516
|
+
showSaveToast("Generation timed out — try again", true);
|
|
517
|
+
} else {
|
|
518
|
+
showSaveToast(`Generated ${received} of ${count} options`, true);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}, 30000 * count);
|
|
440
522
|
|
|
441
|
-
pendingGenerate.set(slideId, { button,
|
|
523
|
+
pendingGenerate.set(slideId, { button, skeletons, input, countSelect, model: selectedModel || null, expectedCount: count, receivedCount: 0, timeoutId });
|
|
442
524
|
|
|
443
525
|
try {
|
|
444
|
-
const body = { token: sessionToken, slideId };
|
|
526
|
+
const body = { token: sessionToken, slideId, count };
|
|
445
527
|
if (prompt) body.prompt = prompt;
|
|
446
528
|
if (hasModelBar) {
|
|
447
529
|
body.model = selectedModel;
|
|
@@ -469,8 +551,21 @@ async function regenerateSlide(button, slideId) {
|
|
|
469
551
|
const prompt = input ? input.value.trim() : "";
|
|
470
552
|
if (input) input.value = "";
|
|
471
553
|
|
|
472
|
-
|
|
473
|
-
|
|
554
|
+
// Create skeleton overlay
|
|
555
|
+
const optionCount = slide.options?.length || 2;
|
|
556
|
+
const colClass = optionsGrid.classList.contains("cols-3") ? "cols-3" :
|
|
557
|
+
optionsGrid.classList.contains("cols-1") ? "cols-1" : "cols-2";
|
|
558
|
+
const overlay = createElement("div", `regen-overlay ${colClass}`);
|
|
559
|
+
for (let i = 0; i < optionCount; i++) {
|
|
560
|
+
overlay.appendChild(createElement("div", "regen-skeleton"));
|
|
561
|
+
}
|
|
562
|
+
const status = createElement("div", "regen-status", "Regenerating options...");
|
|
563
|
+
overlay.appendChild(status);
|
|
564
|
+
|
|
565
|
+
// Position overlay relative to options grid
|
|
566
|
+
optionsGrid.style.position = "relative";
|
|
567
|
+
optionsGrid.classList.add("regenerating");
|
|
568
|
+
optionsGrid.appendChild(overlay);
|
|
474
569
|
|
|
475
570
|
const originalText = button.textContent || "";
|
|
476
571
|
button.classList.add("loading");
|
|
@@ -481,7 +576,7 @@ async function regenerateSlide(button, slideId) {
|
|
|
481
576
|
const genMoreBtn = slideElement.querySelector(".btn-gen-more");
|
|
482
577
|
if (genMoreBtn) genMoreBtn.disabled = true;
|
|
483
578
|
|
|
484
|
-
pendingGenerate.set(slideId, { button, originalText, input, isRegen: true, optionsGrid, genMoreBtn });
|
|
579
|
+
pendingGenerate.set(slideId, { button, originalText, input, isRegen: true, optionsGrid, genMoreBtn, overlay });
|
|
485
580
|
|
|
486
581
|
try {
|
|
487
582
|
const body = { token: sessionToken, slideId };
|
|
@@ -501,7 +596,7 @@ function restoreRegenButton(slideId) {
|
|
|
501
596
|
if (!pending || !pending.isRegen) return;
|
|
502
597
|
pendingGenerate.delete(slideId);
|
|
503
598
|
|
|
504
|
-
const { button, originalText, input, optionsGrid, genMoreBtn } = pending;
|
|
599
|
+
const { button, originalText, input, optionsGrid, genMoreBtn, overlay } = pending;
|
|
505
600
|
if (button) {
|
|
506
601
|
button.classList.remove("loading");
|
|
507
602
|
button.disabled = false;
|
|
@@ -509,8 +604,11 @@ function restoreRegenButton(slideId) {
|
|
|
509
604
|
}
|
|
510
605
|
if (input) input.disabled = false;
|
|
511
606
|
if (optionsGrid) {
|
|
512
|
-
optionsGrid.
|
|
513
|
-
optionsGrid.style.
|
|
607
|
+
optionsGrid.classList.remove("regenerating");
|
|
608
|
+
optionsGrid.style.position = "";
|
|
609
|
+
}
|
|
610
|
+
if (overlay && overlay.parentElement) {
|
|
611
|
+
overlay.remove();
|
|
514
612
|
}
|
|
515
613
|
if (genMoreBtn) genMoreBtn.disabled = false;
|
|
516
614
|
}
|
package/generate-prompts.ts
CHANGED
|
@@ -29,14 +29,14 @@ function optionTemplate(hasBlocks: boolean): string {
|
|
|
29
29
|
function modelHints(generateModel?: string, thinking?: string, action?: string): string {
|
|
30
30
|
if (!generateModel) return "";
|
|
31
31
|
const verb = action === "replace-options" ? "replace-options" : "add-option";
|
|
32
|
-
let hint = `\nGenerate ${verb === "replace-options" ? "options " : ""}using
|
|
32
|
+
let hint = `\nGenerate ${verb === "replace-options" ? "options " : ""}using deck_generate({ model: "${generateModel}", task: "..." }), then push with ${verb}.`;
|
|
33
33
|
if (thinking && thinking !== "off") {
|
|
34
34
|
hint += `\nUse thinking level: "${thinking}".`;
|
|
35
35
|
}
|
|
36
36
|
return hint;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export function buildGenerateMoreResult(slideId: string, slide: DeckSlide | undefined, prompt?: string, generateModel?: string, thinking?: string): string {
|
|
39
|
+
export function buildGenerateMoreResult(slideId: string, slide: DeckSlide | undefined, prompt?: string, generateModel?: string, thinking?: string, count: number = 1): string {
|
|
40
40
|
const title = slide?.title ?? slideId;
|
|
41
41
|
const context = slide?.context ? `\nContext: ${slide.context}` : "";
|
|
42
42
|
|
|
@@ -54,15 +54,22 @@ export function buildGenerateMoreResult(slideId: string, slide: DeckSlide | unde
|
|
|
54
54
|
const { hasBlocks, formatHint } = formatHintForSlide(slide);
|
|
55
55
|
const template = optionTemplate(hasBlocks);
|
|
56
56
|
const userInstructions = prompt ? `\nUser instructions: "${prompt}"` : "";
|
|
57
|
+
|
|
58
|
+
const optionWord = count === 1 ? "option" : "options";
|
|
59
|
+
const callInstructions = count === 1
|
|
60
|
+
? `YOU MUST generate one distinctive additional option and call design_deck with add-option.`
|
|
61
|
+
: `YOU MUST generate ${count} distinctive additional options. Call design_deck with add-option ${count} times (once per option).`;
|
|
57
62
|
|
|
58
63
|
return (
|
|
59
|
-
"The design deck is still open.\n\n" +
|
|
60
|
-
`User
|
|
64
|
+
"The design deck is still open and waiting for your response.\n\n" +
|
|
65
|
+
`User clicked "Generate ${count} ${optionWord}" for slide \"${title}\".${context}${userInstructions}\n\n` +
|
|
61
66
|
`Existing options:\n${existingText}\n\n` +
|
|
62
|
-
|
|
67
|
+
`${callInstructions} ` +
|
|
68
|
+
`Do not skip this step or decide the user has enough options — they explicitly requested ${count === 1 ? "another one" : `${count} more`}.${modelHints(generateModel, thinking, "add-option")}\n\n` +
|
|
63
69
|
`design_deck({\"action\":\"add-option\",\"slideId\":\"${slideId}\",\"option\":${JSON.stringify(template)}})\n\n` +
|
|
64
70
|
`The option field must be a JSON string with: label, optional description, optional aside (explanatory notes below preview), and optional recommended.\n` +
|
|
65
|
-
`${formatHint}`
|
|
71
|
+
`${formatHint}` +
|
|
72
|
+
(count > 1 ? `\n\nRemember: Call add-option ${count} times, each with a different option. Make each option distinctive.` : "")
|
|
66
73
|
);
|
|
67
74
|
}
|
|
68
75
|
|
|
@@ -75,9 +82,10 @@ export function buildRegenerateResult(slideId: string, slide: DeckSlide | undefi
|
|
|
75
82
|
const userInstructions = prompt ? `\nUser instructions: "${prompt}"` : "";
|
|
76
83
|
|
|
77
84
|
return (
|
|
78
|
-
"The design deck is still open.\n\n" +
|
|
79
|
-
`User
|
|
80
|
-
`
|
|
85
|
+
"The design deck is still open and waiting for your response.\n\n" +
|
|
86
|
+
`User clicked "Regenerate all" for slide \"${title}\".${context}${userInstructions}\n\n` +
|
|
87
|
+
`YOU MUST generate ${optionCount} fresh, distinctive options and call design_deck with replace-options. ` +
|
|
88
|
+
`Do not skip this step — the user explicitly requested regeneration.${modelHints(generateModel, thinking, "replace-options")}\n\n` +
|
|
81
89
|
`design_deck({\"action\":\"replace-options\",\"slideId\":\"${slideId}\",\"options\":\"[${template}, ...]\"})` +
|
|
82
90
|
`\n\nThe options field must be a JSON string containing an array of ${optionCount} option objects.\n` +
|
|
83
91
|
`Each option needs: label, optional description, optional aside, optional recommended, and either previewHtml or previewBlocks.\n` +
|