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/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
A tool for [Pi coding agent](https://github.com/badlogic/pi-mono/) that presents multi-slide visual decision decks in the browser. Each slide shows 2-4 high-fidelity previews — code diffs, architecture diagrams, UI mockups — and you pick one per slide. The agent gets back a clean selection map and moves on to implementation.
|
|
8
8
|
|
|
9
|
+
<img width="1340" alt="Design Deck screenshot" src="https://github.com/user-attachments/assets/20864ac6-9223-4e2e-ba3c-db3eaae0abd8" />
|
|
10
|
+
|
|
9
11
|
## Usage
|
|
10
12
|
|
|
11
13
|
Just ask. The agent reaches for the design deck when visual comparison makes sense.
|
|
@@ -40,6 +42,8 @@ Restart pi to load the extension and the bundled `design-deck` skill.
|
|
|
40
42
|
**Requirements:**
|
|
41
43
|
- pi-agent v0.35.0 or later (extensions API)
|
|
42
44
|
|
|
45
|
+
https://github.com/user-attachments/assets/aff1bac6-8bc2-461a-8828-f588ce655f7f
|
|
46
|
+
|
|
43
47
|
## Quick Start
|
|
44
48
|
|
|
45
49
|
The agent builds slides as JSON. Each slide is one decision, each option is one approach:
|
|
@@ -104,24 +108,12 @@ The browser opens, the user picks "JWT + Refresh Tokens", and the agent receives
|
|
|
104
108
|
|
|
105
109
|
## How It Works
|
|
106
110
|
|
|
107
|
-
```
|
|
108
|
-
┌─────────┐ ┌────────────────────────────────────────────┐ ┌─────────┐
|
|
109
|
-
│ Agent │ │ Browser Deck │ │ Agent │
|
|
110
|
-
│ invokes ├─────►│ ├─────►│ receives │
|
|
111
|
-
│design_ │ │ pick → pick → generate more → pick → submit │selections│
|
|
112
|
-
│ deck │ │ ↑ ↑ │ └─────────┘
|
|
113
|
-
└─────────┘ │ │ SSE push ─┘ heartbeat ────────────┤
|
|
114
|
-
└────────────────────────────────────────────┘
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**Lifecycle:**
|
|
118
111
|
1. Agent calls `design_deck()` with slides JSON — local HTTP server starts, browser opens
|
|
119
112
|
2. User navigates slides, picks one option per slide
|
|
120
113
|
3. Optionally clicks "Generate another option" — agent generates and pushes via `add-option`, deck stays open
|
|
121
114
|
4. User submits — selections returned to agent as `{ slideId: "selected label" }`
|
|
122
|
-
5. Tab auto-closes, thinking level restored to pre-deck value
|
|
123
115
|
|
|
124
|
-
The server persists across tool re-invocations. When generate-more fires, the tool resolves with instructions for the agent to create a new option
|
|
116
|
+
The server persists across tool re-invocations. When generate-more fires, the tool resolves with instructions for the agent to create a new option. The browser shows a skeleton placeholder with shimmer animation until the new option arrives via SSE.
|
|
125
117
|
|
|
126
118
|
## Slides
|
|
127
119
|
|
|
@@ -188,7 +180,7 @@ The browser shows the new option with an entry animation. The tool blocks again,
|
|
|
188
180
|
|
|
189
181
|
### Model Override
|
|
190
182
|
|
|
191
|
-
The deck shows a model dropdown when 2+ models are available. Users pick which model generates new options. When a model other than the current one is selected, the generate-more result instructs the agent to
|
|
183
|
+
The deck shows a model dropdown when 2+ models are available. Users pick which model generates new options. When a model other than the current one is selected, the generate-more result instructs the agent to use the built-in `deck_generate` tool, which spawns pi headlessly with that model.
|
|
192
184
|
|
|
193
185
|
The default model can be set in the UI (saved to settings) or in `settings.json`:
|
|
194
186
|
|
|
@@ -296,6 +288,7 @@ Three modes of invocation:
|
|
|
296
288
|
pi-design-deck/
|
|
297
289
|
├── index.ts # Tool registration, module-level state, lifecycle
|
|
298
290
|
├── generate-prompts.ts # Prompt builders for generate-more / regenerate
|
|
291
|
+
├── model-runner.ts # Headless pi spawner for deck_generate tool
|
|
299
292
|
├── deck-schema.ts # TypeScript types and validation (no dependencies)
|
|
300
293
|
├── deck-server.ts # HTTP server, SSE, asset serving, snapshots
|
|
301
294
|
├── server-utils.ts # Shared HTTP/session utilities
|
package/deck-server.ts
CHANGED
|
@@ -173,16 +173,16 @@ export interface DeckServerOptions {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
export interface DeckServerCallbacks {
|
|
176
|
-
onSubmit: (selections: Record<string, string
|
|
176
|
+
onSubmit: (selections: Record<string, string>, notes?: Record<string, string>, finalNotes?: string) => void;
|
|
177
177
|
onCancel: (reason?: "user" | "stale" | "aborted") => void;
|
|
178
|
-
onGenerateMore: (slideId: string, prompt?: string, model?: string, thinking?: string) => void;
|
|
178
|
+
onGenerateMore: (slideId: string, prompt?: string, model?: string, thinking?: string, count?: number) => void;
|
|
179
179
|
onRegenerateSlide: (slideId: string, prompt?: string, model?: string, thinking?: string) => void;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
export interface DeckServerHandle {
|
|
183
183
|
url: string;
|
|
184
184
|
port: number;
|
|
185
|
-
close: () => void;
|
|
185
|
+
close: (reason?: string) => void;
|
|
186
186
|
pushOption: (slideId: string, option: DeckOption) => void;
|
|
187
187
|
cancelGenerate: () => void;
|
|
188
188
|
replaceSlideOptions: (slideId: string, options: DeckOption[]) => void;
|
|
@@ -414,12 +414,14 @@ export async function startDeckServer(
|
|
|
414
414
|
return;
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
-
const payload = body as { selections?: unknown };
|
|
417
|
+
const payload = body as { selections?: unknown; notes?: unknown; finalNotes?: unknown };
|
|
418
418
|
const selections = toStringMap(payload.selections);
|
|
419
419
|
if (!selections) {
|
|
420
420
|
sendJson(res, 400, { ok: false, error: "Invalid selections payload" });
|
|
421
421
|
return;
|
|
422
422
|
}
|
|
423
|
+
const notes = toStringMap(payload.notes) ?? undefined;
|
|
424
|
+
const finalNotes = typeof payload.finalNotes === "string" ? payload.finalNotes.trim() : undefined;
|
|
423
425
|
|
|
424
426
|
touchHeartbeat();
|
|
425
427
|
if (autoSaveOnSubmit !== false) {
|
|
@@ -431,7 +433,7 @@ export async function startDeckServer(
|
|
|
431
433
|
unregisterSession(sessionId);
|
|
432
434
|
pushEvent("deck-close", { reason: "submitted" });
|
|
433
435
|
sendJson(res, 200, { ok: true });
|
|
434
|
-
setImmediate(() => callbacks.onSubmit(selections));
|
|
436
|
+
setImmediate(() => callbacks.onSubmit(selections, notes, finalNotes || undefined));
|
|
435
437
|
return;
|
|
436
438
|
}
|
|
437
439
|
|
|
@@ -492,7 +494,7 @@ export async function startDeckServer(
|
|
|
492
494
|
return;
|
|
493
495
|
}
|
|
494
496
|
|
|
495
|
-
const payload = body as { slideId?: string; prompt?: string; model?: string; thinking?: string };
|
|
497
|
+
const payload = body as { slideId?: string; prompt?: string; model?: string; thinking?: string; count?: number };
|
|
496
498
|
if (typeof payload.slideId !== "string" || payload.slideId.trim() === "") {
|
|
497
499
|
sendJson(res, 400, { ok: false, error: "slideId is required" });
|
|
498
500
|
return;
|
|
@@ -509,12 +511,13 @@ export async function startDeckServer(
|
|
|
509
511
|
const prompt = typeof payload.prompt === "string" ? payload.prompt.trim() || undefined : undefined;
|
|
510
512
|
const model = typeof payload.model === "string" ? (payload.model.trim() || "") : undefined;
|
|
511
513
|
const thinking = typeof payload.thinking === "string" ? payload.thinking.trim() || undefined : undefined;
|
|
514
|
+
const count = typeof payload.count === "number" && payload.count >= 1 && payload.count <= 5 ? payload.count : 1;
|
|
512
515
|
|
|
513
516
|
setPendingGenerate(payload.slideId as string, false);
|
|
514
517
|
touchHeartbeat();
|
|
515
518
|
sendJson(res, 200, { ok: true });
|
|
516
519
|
setImmediate(() => {
|
|
517
|
-
callbacks.onGenerateMore(payload.slideId as string, prompt, model, thinking);
|
|
520
|
+
callbacks.onGenerateMore(payload.slideId as string, prompt, model, thinking, count);
|
|
518
521
|
});
|
|
519
522
|
return;
|
|
520
523
|
}
|
|
@@ -603,11 +606,11 @@ export async function startDeckServer(
|
|
|
603
606
|
resolve({
|
|
604
607
|
url,
|
|
605
608
|
port: addr.port,
|
|
606
|
-
close: () => {
|
|
609
|
+
close: (reason?: string) => {
|
|
607
610
|
if (!completed) {
|
|
608
611
|
markCompleted();
|
|
609
612
|
unregisterSession(sessionId);
|
|
610
|
-
pushEvent("deck-close", { reason: "closed" });
|
|
613
|
+
pushEvent("deck-close", { reason: reason || "closed" });
|
|
611
614
|
}
|
|
612
615
|
try {
|
|
613
616
|
server.close();
|
package/form/css/controls.css
CHANGED
|
@@ -27,6 +27,30 @@
|
|
|
27
27
|
}
|
|
28
28
|
.btn-gen-more:disabled { opacity: 0.3; cursor: not-allowed; }
|
|
29
29
|
|
|
30
|
+
.gen-group { display: flex; align-items: center; gap: 6px; }
|
|
31
|
+
|
|
32
|
+
.gen-count {
|
|
33
|
+
appearance: none;
|
|
34
|
+
-webkit-appearance: none;
|
|
35
|
+
background: rgba(var(--dk-ink),0.12);
|
|
36
|
+
border: 1px solid rgba(var(--dk-ink),0.15);
|
|
37
|
+
border-radius: 4px;
|
|
38
|
+
padding: 4px 8px;
|
|
39
|
+
font-size: 12px;
|
|
40
|
+
font-weight: 600;
|
|
41
|
+
color: var(--dk-accent-text);
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
text-align: center;
|
|
44
|
+
min-width: 32px;
|
|
45
|
+
}
|
|
46
|
+
.gen-count:focus { outline: none; border-color: rgba(138,190,183,0.4); background: rgba(138,190,183,0.1); }
|
|
47
|
+
.gen-count:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
48
|
+
|
|
49
|
+
.gen-count-label {
|
|
50
|
+
font-size: 12px;
|
|
51
|
+
color: var(--dk-text-dim);
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
.btn-regen {
|
|
31
55
|
display: inline-flex; align-items: center; gap: 6px;
|
|
32
56
|
padding: 8px 14px; border-radius: 8px;
|
|
@@ -87,6 +111,7 @@
|
|
|
87
111
|
background: rgba(16,185,129,0.12); color: var(--dk-status-success);
|
|
88
112
|
margin-left: auto;
|
|
89
113
|
}
|
|
114
|
+
.badge-generated + .option-num { margin-left: 6px; }
|
|
90
115
|
|
|
91
116
|
/* Skeleton shimmer placeholder */
|
|
92
117
|
.option-skeleton {
|
|
@@ -119,6 +144,102 @@
|
|
|
119
144
|
}
|
|
120
145
|
@keyframes skel-shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
|
|
121
146
|
|
|
147
|
+
/* Regeneration overlay - covers options grid during regenerate-all */
|
|
148
|
+
.regen-overlay {
|
|
149
|
+
position: absolute;
|
|
150
|
+
inset: 0;
|
|
151
|
+
display: grid;
|
|
152
|
+
gap: 20px;
|
|
153
|
+
padding: 0;
|
|
154
|
+
z-index: 10;
|
|
155
|
+
animation: regen-fade-in 0.3s ease-out;
|
|
156
|
+
}
|
|
157
|
+
.regen-overlay.cols-3 { grid-template-columns: repeat(3, 1fr); }
|
|
158
|
+
.regen-overlay.cols-2 { grid-template-columns: repeat(2, 1fr); }
|
|
159
|
+
.regen-overlay.cols-1 { grid-template-columns: 1fr; }
|
|
160
|
+
|
|
161
|
+
@keyframes regen-fade-in {
|
|
162
|
+
from { opacity: 0; }
|
|
163
|
+
to { opacity: 1; }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.regen-skeleton {
|
|
167
|
+
border: 2px dashed rgba(138,190,183,0.15);
|
|
168
|
+
border-radius: 12px;
|
|
169
|
+
background: var(--dk-surface);
|
|
170
|
+
min-height: 280px;
|
|
171
|
+
position: relative;
|
|
172
|
+
overflow: hidden;
|
|
173
|
+
}
|
|
174
|
+
.regen-skeleton::before {
|
|
175
|
+
content: '';
|
|
176
|
+
position: absolute;
|
|
177
|
+
top: 0; left: 0; right: 0;
|
|
178
|
+
height: 44px;
|
|
179
|
+
background: linear-gradient(110deg, rgba(var(--dk-ink),0.03) 30%, rgba(var(--dk-ink),0.07) 50%, rgba(var(--dk-ink),0.03) 70%);
|
|
180
|
+
background-size: 200% 100%;
|
|
181
|
+
animation: skel-shimmer 1.5s ease-in-out infinite;
|
|
182
|
+
border-bottom: 1px solid rgba(var(--dk-ink),0.06);
|
|
183
|
+
}
|
|
184
|
+
.regen-skeleton::after {
|
|
185
|
+
content: '';
|
|
186
|
+
position: absolute;
|
|
187
|
+
top: 45px; left: 0; right: 0; bottom: 0;
|
|
188
|
+
background: linear-gradient(110deg, var(--dk-surface) 30%, var(--dk-surface-shimmer) 50%, var(--dk-surface) 70%);
|
|
189
|
+
background-size: 200% 100%;
|
|
190
|
+
animation: skel-shimmer 1.5s ease-in-out infinite;
|
|
191
|
+
animation-delay: 0.1s;
|
|
192
|
+
}
|
|
193
|
+
.regen-skeleton:nth-child(2)::before,
|
|
194
|
+
.regen-skeleton:nth-child(2)::after { animation-delay: 0.2s; }
|
|
195
|
+
.regen-skeleton:nth-child(3)::before,
|
|
196
|
+
.regen-skeleton:nth-child(3)::after { animation-delay: 0.3s; }
|
|
197
|
+
.regen-skeleton:nth-child(4)::before,
|
|
198
|
+
.regen-skeleton:nth-child(4)::after { animation-delay: 0.4s; }
|
|
199
|
+
|
|
200
|
+
/* Regeneration status text */
|
|
201
|
+
.regen-status {
|
|
202
|
+
position: absolute;
|
|
203
|
+
bottom: 16px; left: 50%;
|
|
204
|
+
transform: translateX(-50%);
|
|
205
|
+
font-family: var(--dk-font-mono);
|
|
206
|
+
font-size: 11px;
|
|
207
|
+
color: var(--dk-accent-text);
|
|
208
|
+
letter-spacing: 0.3px;
|
|
209
|
+
display: flex;
|
|
210
|
+
align-items: center;
|
|
211
|
+
gap: 8px;
|
|
212
|
+
}
|
|
213
|
+
.regen-status::before {
|
|
214
|
+
content: '';
|
|
215
|
+
width: 14px; height: 14px;
|
|
216
|
+
border: 2px solid rgba(138,190,183,0.2);
|
|
217
|
+
border-top-color: var(--dk-accent);
|
|
218
|
+
border-radius: 50%;
|
|
219
|
+
animation: spin 0.8s linear infinite;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* Options grid regenerating state */
|
|
223
|
+
.options.regenerating {
|
|
224
|
+
opacity: 0;
|
|
225
|
+
pointer-events: none;
|
|
226
|
+
transition: opacity 0.25s ease-out;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* Regenerated options entry animation */
|
|
230
|
+
.option-regenerated {
|
|
231
|
+
animation: opt-regen-appear 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
232
|
+
opacity: 0;
|
|
233
|
+
}
|
|
234
|
+
.option-regenerated:nth-child(1) { animation-delay: 0.05s; }
|
|
235
|
+
.option-regenerated:nth-child(2) { animation-delay: 0.1s; }
|
|
236
|
+
.option-regenerated:nth-child(3) { animation-delay: 0.15s; }
|
|
237
|
+
.option-regenerated:nth-child(4) { animation-delay: 0.2s; }
|
|
238
|
+
@keyframes opt-regen-appear {
|
|
239
|
+
0% { opacity: 0; transform: translateY(12px) scale(0.96); }
|
|
240
|
+
100% { opacity: 1; transform: translateY(0) scale(1); }
|
|
241
|
+
}
|
|
242
|
+
|
|
122
243
|
/* ─────────────────────────────────────────────────────────────
|
|
123
244
|
MODEL BAR
|
|
124
245
|
───────────────────────────────────────────────────────────── */
|
package/form/css/layout.css
CHANGED
|
@@ -177,12 +177,6 @@ body {
|
|
|
177
177
|
background: rgba(138,190,183,0.08);
|
|
178
178
|
border-bottom-color: rgba(138,190,183,0.12);
|
|
179
179
|
}
|
|
180
|
-
.option.dimmed { opacity: 0.55; }
|
|
181
|
-
.option.dimmed:hover {
|
|
182
|
-
opacity: 0.8;
|
|
183
|
-
border-color: rgba(var(--dk-ink),0.15);
|
|
184
|
-
transform: translateY(-1px);
|
|
185
|
-
}
|
|
186
180
|
|
|
187
181
|
.option-header {
|
|
188
182
|
display: flex; align-items: center; gap: 8px;
|
|
@@ -205,8 +199,6 @@ body {
|
|
|
205
199
|
.option:hover .option-radio { border-color: rgba(var(--dk-ink),0.35); }
|
|
206
200
|
.option.selected .option-radio { border-color: var(--dk-accent); background: var(--dk-accent); }
|
|
207
201
|
.option.selected .option-radio::after { background: #fff; }
|
|
208
|
-
.option.dimmed .option-radio { border-color: rgba(var(--dk-ink),0.14); }
|
|
209
|
-
.option.dimmed:hover .option-radio { border-color: rgba(var(--dk-ink),0.2); }
|
|
210
202
|
|
|
211
203
|
.option-label { font-size: 13px; font-weight: 600; color: var(--dk-text-label); }
|
|
212
204
|
|
|
@@ -276,6 +268,21 @@ body {
|
|
|
276
268
|
color: var(--dk-text-secondary); font-family: var(--dk-font-mono);
|
|
277
269
|
font-style: italic;
|
|
278
270
|
}
|
|
271
|
+
.summary-notes {
|
|
272
|
+
margin-top: 10px; padding: 8px 10px;
|
|
273
|
+
font-size: 11px; line-height: 1.5;
|
|
274
|
+
color: var(--dk-accent-text);
|
|
275
|
+
background: rgba(138,190,183,0.08);
|
|
276
|
+
border-radius: 4px;
|
|
277
|
+
border-left: 2px solid rgba(138,190,183,0.3);
|
|
278
|
+
}
|
|
279
|
+
.summary-notes-label {
|
|
280
|
+
font-weight: 600;
|
|
281
|
+
font-size: 9px;
|
|
282
|
+
text-transform: uppercase;
|
|
283
|
+
letter-spacing: 0.3px;
|
|
284
|
+
opacity: 0.8;
|
|
285
|
+
}
|
|
279
286
|
.summary-preview {
|
|
280
287
|
margin-top: 12px; border-radius: 6px; overflow: hidden;
|
|
281
288
|
border: 1px solid rgba(var(--dk-ink),0.12); max-height: 120px;
|
|
@@ -296,6 +303,47 @@ body {
|
|
|
296
303
|
}
|
|
297
304
|
.summary-mermaid svg { max-width: 100%; max-height: 100%; }
|
|
298
305
|
|
|
306
|
+
/* Final notes input on summary slide */
|
|
307
|
+
.final-notes {
|
|
308
|
+
width: 100%;
|
|
309
|
+
max-width: 560px;
|
|
310
|
+
margin-top: 24px;
|
|
311
|
+
display: flex;
|
|
312
|
+
flex-direction: column;
|
|
313
|
+
gap: 8px;
|
|
314
|
+
}
|
|
315
|
+
.final-notes-label {
|
|
316
|
+
font-size: 11px;
|
|
317
|
+
font-weight: 500;
|
|
318
|
+
color: var(--dk-text-dim);
|
|
319
|
+
text-transform: uppercase;
|
|
320
|
+
letter-spacing: 0.5px;
|
|
321
|
+
}
|
|
322
|
+
.final-notes-input {
|
|
323
|
+
width: 100%;
|
|
324
|
+
padding: 12px 14px;
|
|
325
|
+
border-radius: 8px;
|
|
326
|
+
border: 1px solid rgba(var(--dk-ink),0.12);
|
|
327
|
+
background: rgba(var(--dk-ink),0.04);
|
|
328
|
+
font-size: 13px;
|
|
329
|
+
font-family: inherit;
|
|
330
|
+
color: var(--dk-text);
|
|
331
|
+
resize: vertical;
|
|
332
|
+
min-height: 60px;
|
|
333
|
+
max-height: 120px;
|
|
334
|
+
transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
|
|
335
|
+
}
|
|
336
|
+
.final-notes-input::placeholder {
|
|
337
|
+
color: var(--dk-text-placeholder);
|
|
338
|
+
font-style: italic;
|
|
339
|
+
}
|
|
340
|
+
.final-notes-input:focus {
|
|
341
|
+
outline: none;
|
|
342
|
+
border-color: rgba(138,190,183,0.4);
|
|
343
|
+
background: rgba(var(--dk-ink),0.06);
|
|
344
|
+
box-shadow: 0 0 0 3px rgba(138,190,183,0.1);
|
|
345
|
+
}
|
|
346
|
+
|
|
299
347
|
.btn-generate {
|
|
300
348
|
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
|
301
349
|
padding: 14px 48px; border-radius: 10px; font-size: 14px; font-weight: 600;
|
package/form/css/preview.css
CHANGED
|
@@ -59,17 +59,71 @@ code[class*="language-"], pre[class*="language-"] {
|
|
|
59
59
|
OPTION ASIDE
|
|
60
60
|
───────────────────────────────────────────────────────────── */
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
/* Option footer - contains aside + notes */
|
|
63
|
+
.option-footer {
|
|
64
64
|
padding: 12px 14px;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
gap: 8px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.option-aside {
|
|
71
|
+
padding: 8px 10px;
|
|
72
|
+
border-left: 2px solid rgba(138,190,183,0.2);
|
|
73
|
+
background: rgba(var(--dk-ink),0.02);
|
|
74
|
+
font-size: 11px; line-height: 1.55;
|
|
69
75
|
color: var(--dk-text-aside);
|
|
70
76
|
font-family: var(--dk-font-mono);
|
|
71
77
|
font-style: italic;
|
|
72
78
|
letter-spacing: -0.01em;
|
|
79
|
+
border-radius: 0 4px 4px 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Option notes input */
|
|
83
|
+
.option-notes {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: 8px;
|
|
87
|
+
}
|
|
88
|
+
.option-notes-label {
|
|
89
|
+
font-size: 9px;
|
|
90
|
+
font-weight: 500;
|
|
91
|
+
color: var(--dk-text-hint);
|
|
92
|
+
text-transform: uppercase;
|
|
93
|
+
letter-spacing: 0.3px;
|
|
94
|
+
white-space: nowrap;
|
|
95
|
+
opacity: 0.6;
|
|
96
|
+
}
|
|
97
|
+
.option-notes-input {
|
|
98
|
+
flex: 1;
|
|
99
|
+
padding: 5px 8px;
|
|
100
|
+
border-radius: 4px;
|
|
101
|
+
border: 1px dashed rgba(var(--dk-ink),0.12);
|
|
102
|
+
background: transparent;
|
|
103
|
+
font-size: 11px;
|
|
104
|
+
font-family: var(--dk-font-mono);
|
|
105
|
+
color: var(--dk-text);
|
|
106
|
+
resize: none;
|
|
107
|
+
min-height: 24px;
|
|
108
|
+
transition: border-color 0.2s, background 0.2s;
|
|
109
|
+
}
|
|
110
|
+
.option-notes-input::placeholder {
|
|
111
|
+
color: var(--dk-text-placeholder);
|
|
112
|
+
opacity: 0.4;
|
|
113
|
+
font-size: 10px;
|
|
114
|
+
}
|
|
115
|
+
.option-notes-input:focus {
|
|
116
|
+
outline: none;
|
|
117
|
+
border-style: solid;
|
|
118
|
+
border-color: rgba(138,190,183,0.3);
|
|
119
|
+
background: rgba(var(--dk-ink),0.02);
|
|
120
|
+
}
|
|
121
|
+
.option.selected .option-notes-input {
|
|
122
|
+
border-color: rgba(138,190,183,0.2);
|
|
123
|
+
}
|
|
124
|
+
.option-notes-input:disabled {
|
|
125
|
+
opacity: 0.4;
|
|
126
|
+
cursor: not-allowed;
|
|
73
127
|
}
|
|
74
128
|
|
|
75
129
|
/* ─────────────────────────────────────────────────────────────
|
package/form/js/deck-core.js
CHANGED
|
@@ -23,6 +23,8 @@ let isClosed = false;
|
|
|
23
23
|
let isSubmitting = false;
|
|
24
24
|
|
|
25
25
|
const selections = {};
|
|
26
|
+
const optionNotes = {};
|
|
27
|
+
let finalNotes = "";
|
|
26
28
|
const pendingGenerate = new Map();
|
|
27
29
|
let selectedModel = "";
|
|
28
30
|
let selectedThinking = "off";
|
|
@@ -184,13 +186,23 @@ function initTheme() {
|
|
|
184
186
|
const SELECTIONS_KEY = `pi-deck-${typeof deckData.sessionId === "string" ? deckData.sessionId : "unknown"}`;
|
|
185
187
|
|
|
186
188
|
function saveSelectionsToStorage() {
|
|
187
|
-
try {
|
|
189
|
+
try {
|
|
190
|
+
const data = { selections, optionNotes };
|
|
191
|
+
if (finalNotes) data.finalNotes = finalNotes;
|
|
192
|
+
localStorage.setItem(SELECTIONS_KEY, JSON.stringify(data));
|
|
193
|
+
} catch {}
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
function loadSelectionsFromStorage() {
|
|
191
197
|
try {
|
|
192
198
|
const saved = localStorage.getItem(SELECTIONS_KEY);
|
|
193
|
-
|
|
199
|
+
if (!saved) return null;
|
|
200
|
+
const parsed = JSON.parse(saved);
|
|
201
|
+
// Handle both old format (just selections) and new format ({ selections, optionNotes })
|
|
202
|
+
if (parsed && typeof parsed === "object" && !parsed.selections) {
|
|
203
|
+
return { selections: parsed, optionNotes: {} };
|
|
204
|
+
}
|
|
205
|
+
return parsed;
|
|
194
206
|
} catch { return null; }
|
|
195
207
|
}
|
|
196
208
|
|
package/form/js/deck-interact.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// ─── SELECTION ───────────────────────────────────────────────
|
|
2
2
|
|
|
3
|
-
function applySavedSelections(
|
|
4
|
-
if (!
|
|
5
|
-
for (const [slideId, label] of Object.entries(
|
|
3
|
+
function applySavedSelections(savedSelections, savedNotes) {
|
|
4
|
+
if (!savedSelections || typeof savedSelections !== "object") return;
|
|
5
|
+
for (const [slideId, label] of Object.entries(savedSelections)) {
|
|
6
6
|
if (typeof label !== "string") continue;
|
|
7
7
|
const slideEl = document.querySelector(`.slide[data-id="${CSS.escape(slideId)}"]`);
|
|
8
8
|
if (!slideEl) continue;
|
|
@@ -13,30 +13,44 @@ function applySavedSelections(saved) {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
// Restore notes
|
|
17
|
+
if (savedNotes && typeof savedNotes === "object") {
|
|
18
|
+
for (const [slideId, noteData] of Object.entries(savedNotes)) {
|
|
19
|
+
if (noteData && typeof noteData === "object" && noteData.label && noteData.notes) {
|
|
20
|
+
optionNotes[slideId] = noteData;
|
|
21
|
+
// Find and populate the textarea
|
|
22
|
+
const input = document.querySelector(`.option-notes-input[data-slide-id="${CSS.escape(slideId)}"][data-option-label="${CSS.escape(noteData.label)}"]`);
|
|
23
|
+
if (input) input.value = noteData.notes;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
function restoreSelections() {
|
|
19
30
|
const serverSaved = deckData.savedSelections;
|
|
20
31
|
if (serverSaved && typeof serverSaved === "object" && Object.keys(serverSaved).length > 0) {
|
|
21
|
-
applySavedSelections(serverSaved);
|
|
32
|
+
applySavedSelections(serverSaved, null);
|
|
22
33
|
return;
|
|
23
34
|
}
|
|
24
35
|
const stored = loadSelectionsFromStorage();
|
|
25
|
-
if (stored)
|
|
36
|
+
if (stored) {
|
|
37
|
+
applySavedSelections(stored.selections || stored, stored.optionNotes);
|
|
38
|
+
// Restore final notes if present
|
|
39
|
+
if (stored.finalNotes) {
|
|
40
|
+
finalNotes = stored.finalNotes;
|
|
41
|
+
const input = document.getElementById("final-notes-input");
|
|
42
|
+
if (input) input.value = stored.finalNotes;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
26
45
|
}
|
|
27
46
|
|
|
28
47
|
function applySelectionClasses(slideElement, selectedElement) {
|
|
29
48
|
slideElement.querySelectorAll(".option").forEach((optionEl) => {
|
|
30
|
-
optionEl.classList.remove("selected"
|
|
49
|
+
optionEl.classList.remove("selected");
|
|
31
50
|
optionEl.setAttribute("aria-checked", "false");
|
|
32
51
|
});
|
|
33
52
|
selectedElement.classList.add("selected");
|
|
34
53
|
selectedElement.setAttribute("aria-checked", "true");
|
|
35
|
-
slideElement.querySelectorAll(".option").forEach((optionEl) => {
|
|
36
|
-
if (optionEl !== selectedElement) {
|
|
37
|
-
optionEl.classList.add("dimmed");
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
function selectOption(optionElement) {
|
|
@@ -342,7 +356,8 @@ function initModelBar(modelsData) {
|
|
|
342
356
|
}
|
|
343
357
|
|
|
344
358
|
function syncDefaultCheck() {
|
|
345
|
-
|
|
359
|
+
const effectiveModel = selectedModel || modelsData.current;
|
|
360
|
+
defaultCheck.checked = modelsData.defaultModel != null && effectiveModel === modelsData.defaultModel;
|
|
346
361
|
}
|
|
347
362
|
|
|
348
363
|
function activateProvider(provider) {
|
|
@@ -374,12 +389,18 @@ function initModelBar(modelsData) {
|
|
|
374
389
|
});
|
|
375
390
|
|
|
376
391
|
defaultCheck.addEventListener("change", async () => {
|
|
377
|
-
|
|
378
|
-
const
|
|
392
|
+
// Use selectedModel, or fall back to current model when on "Current" pill
|
|
393
|
+
const modelToSave = selectedModel || modelsData.current;
|
|
394
|
+
if (defaultCheck.checked && !modelToSave) { defaultCheck.checked = false; return; }
|
|
395
|
+
const model = defaultCheck.checked ? modelToSave : null;
|
|
379
396
|
try {
|
|
380
397
|
await postJson("/save-model-default", { token: sessionToken, model });
|
|
381
398
|
modelsData.defaultModel = model;
|
|
382
|
-
} catch {
|
|
399
|
+
} catch (err) {
|
|
400
|
+
console.error("Failed to save default model:", err);
|
|
401
|
+
// Revert checkbox to match actual state
|
|
402
|
+
syncDefaultCheck();
|
|
403
|
+
}
|
|
383
404
|
});
|
|
384
405
|
|
|
385
406
|
if (modelsData.defaultModel) {
|