slidemind 0.3.4 → 0.3.12
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/.agents/plugins/marketplace.json +2 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +5 -5
- package/bin/slidemind.mjs +535 -134
- package/package.json +1 -1
- package/skills/create-slides/SKILL.md +718 -127
- package/skills/search/SKILL.md +217 -57
- package/src/bridge/build-and-push.mjs +3 -3
- package/src/bridge/bundle.mjs +91 -36
- package/src/bridge/capture.mjs +34 -0
- package/src/bridge/client.mjs +81 -23
- package/src/bridge/config.mjs +21 -2
- package/src/bridge/engine-download.mjs +26 -7
- package/src/bridge/recolor.mjs +46 -0
- package/src/bridge/setup-claim.mjs +7 -1
- package/src/builders/build.mjs +179 -5
- package/src/builders/gate-server.mjs +25 -36
- package/src/builders/gate-session.js +28 -48
- package/src/builders/lint-fix.mjs +88 -0
- package/src/builders/plan-overlay.js +507 -136
- package/src/builders/plan.mjs +337 -60
- package/src/builders/review-overlay.js +126 -109
- package/src/builders/review.mjs +118 -64
- package/src/builders/storyboard-markdown.mjs +15 -0
- package/src/builders/storyboard-schema.mjs +23 -6
- package/src/builders/visual-families.mjs +20 -4
- package/src/lib/adopt.mjs +10 -6
- package/src/lib/conversions.mjs +26 -6
- package/src/lib/overwrite-guard.mjs +14 -1
- package/src/lib/reconcile.mjs +15 -4
- package/src/pptx/tags.mjs +49 -0
|
@@ -18,25 +18,19 @@
|
|
|
18
18
|
// round send button (a "story iteration" pill while storyline edits are staged)
|
|
19
19
|
// and — fused to its right — the Approve button, whose sub-label says the agent
|
|
20
20
|
// builds the slides next. Above it, the LEDGER rail lists every comment of this
|
|
21
|
-
// session with its anchor and status (
|
|
21
|
+
// session with its anchor and status (sent → addressed).
|
|
22
22
|
//
|
|
23
23
|
// TWO MODES, not "reviewing vs. locked out":
|
|
24
24
|
// - PARKED (the command is blocking on this tab): everything is live. Send
|
|
25
|
-
// resolves the gate at once (decision "comments"
|
|
26
|
-
//
|
|
27
|
-
// direct edits and story iteration are allowed.
|
|
25
|
+
// resolves the gate at once (decision "comments"); Approve resolves it as
|
|
26
|
+
// "approve"; direct edits and story iteration are allowed.
|
|
28
27
|
// - WORKING (the agent is revising): the plan stays readable and the strip
|
|
29
|
-
// stays navigable
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// agent's next `slidemind plan` run claims it the reload's boot POSTs the whole
|
|
36
|
-
// backlog to /plan-review/backlog — which resolves that run immediately as a
|
|
37
|
-
// drain round, so the tab simply stays in working mode with the ledger flipped
|
|
38
|
-
// to "sent". A tab that is superseded by a fresh window flushes its backlog
|
|
39
|
-
// before retiring, for the same reason.
|
|
28
|
+
// stays navigable; the composer, direct edits, story iteration and Approve
|
|
29
|
+
// are off (the displayed round is already superseded). A status strip says
|
|
30
|
+
// so — the add-in's "Revising the plan / this window refreshes when it's
|
|
31
|
+
// ready". Nothing is queued behind the agent's back: the tab polls the
|
|
32
|
+
// heartbeat and reloads itself when the next `slidemind plan` run presents
|
|
33
|
+
// the revised round.
|
|
40
34
|
//
|
|
41
35
|
// A round transition is detected from the SERVER's per-run nonce (CFG.round):
|
|
42
36
|
// a different nonce means a newer `slidemind plan` run served this page. It
|
|
@@ -44,7 +38,8 @@
|
|
|
44
38
|
// unsent draft, shows a transient banner, and diffs the cached previous plan
|
|
45
39
|
// body per slide to badge the changed tabs "updated" — the body diff is
|
|
46
40
|
// COSMETIC only; it never decides whether a round arrived. The web notification
|
|
47
|
-
// fires
|
|
41
|
+
// fires when the arriving round parks (a plain refresh of a sent round stays
|
|
42
|
+
// silent).
|
|
48
43
|
//
|
|
49
44
|
// A fresh window (no session of its own) seeds its ledger from CFG.ledgerSeed,
|
|
50
45
|
// the deck's persisted plan-review.json round, so the reviewer still sees the
|
|
@@ -79,8 +74,8 @@
|
|
|
79
74
|
|
|
80
75
|
// The shared gate session (src/builders/gate-session.js, inlined into the page
|
|
81
76
|
// as window.__SLIDEMIND_GATE__): the deck-keyed sessionStorage ledger, the
|
|
82
|
-
//
|
|
83
|
-
//
|
|
77
|
+
// server-nonce round transition, the round-ready announce and the heartbeat
|
|
78
|
+
// poll — one implementation, shared with the preview gate.
|
|
84
79
|
var SES = window.__SLIDEMIND_GATE__.createGateSession({
|
|
85
80
|
name: "plan",
|
|
86
81
|
deck: CFG.deck || "",
|
|
@@ -91,7 +86,6 @@
|
|
|
91
86
|
round: CFG.round || "",
|
|
92
87
|
service: "slidemind-plan",
|
|
93
88
|
heartbeat: CFG.heartbeat || "/__plan/heartbeat",
|
|
94
|
-
backlogEndpoint: CFG.backlogEndpoint || "/plan-review/backlog",
|
|
95
89
|
pollMs: CFG.pollMs,
|
|
96
90
|
pollTtlMs: CFG.pollTtlMs,
|
|
97
91
|
attentionTitle: "● Plan ready — slidemind",
|
|
@@ -105,9 +99,9 @@
|
|
|
105
99
|
// ---------- state ----------
|
|
106
100
|
var submitted = false;
|
|
107
101
|
var approveArmed = false; // inline "discard unsent feedback?" confirm state
|
|
108
|
-
var working = false; // the agent is revising → edits
|
|
102
|
+
var working = false; // the agent is revising → edits, composer and approve off
|
|
109
103
|
var baseline = CFG.planBody || ""; // the served plan body; edits diff against it
|
|
110
|
-
var ledger = []; // every comment of this session: {…, status:
|
|
104
|
+
var ledger = []; // every comment of this session: {…, status:sent|addressed}
|
|
111
105
|
var roundNo = 1; // this tab's round counter (sessionStorage, not server state)
|
|
112
106
|
var changedSlides = {}; // 1-based tab position → true ("updated" badges this round)
|
|
113
107
|
var bannerText = null; // transient round-arrival banner, null once dismissed
|
|
@@ -297,24 +291,37 @@
|
|
|
297
291
|
".plan-storyboard-draft-note p{margin:0;}.plan-storyboard-draft-note strong{color:var(--sm-fg);font-weight:600;}" +
|
|
298
292
|
/* storyline tab strip */
|
|
299
293
|
".plan-storyline{margin:0;}" +
|
|
300
|
-
".plan-storyline-head{display:flex;flex-
|
|
294
|
+
".plan-storyline-head{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:10px 20px;padding:16px 20px 12px;}" +
|
|
295
|
+
".plan-storyline-head-text{display:flex;flex:1 1 240px;flex-direction:column;gap:3px;min-width:0;}" +
|
|
296
|
+
/* deck-level layout knob: one pick binds every slide's layout */
|
|
297
|
+
".plan-storyline-deck-layout{display:flex;flex:0 1 280px;flex-direction:column;gap:4px;min-width:200px;}" +
|
|
298
|
+
".plan-storyline-deck-layout-label{color:var(--sm-dim);font-size:9px;font-weight:700;letter-spacing:.18em;line-height:1.2;text-transform:uppercase;}" +
|
|
299
|
+
".plan-storyline-deck-layout .plan-storyboard-layout-picker{margin:0;}" +
|
|
300
|
+
".plan-storyline-deck-layout .plan-storyboard-layout-menu{left:auto;right:0;min-width:340px;}" +
|
|
301
301
|
".plan-storyline-label{color:var(--plan-story);font-size:9px;font-weight:700;letter-spacing:.18em;line-height:1.2;text-transform:uppercase;}" +
|
|
302
302
|
".plan-storyline-track{display:flex;flex-wrap:wrap;align-items:stretch;margin:0;padding:4px 14px 10px;list-style:none;" +
|
|
303
303
|
"border-bottom:1px solid var(--sm-border);row-gap:2px;}" +
|
|
304
|
-
|
|
305
|
-
|
|
304
|
+
/* grid: numeral + move text share a baseline row; the layout chip fills the
|
|
305
|
+
second row and pins to the bottom, so cards in a row stay one height */
|
|
306
|
+
".plan-storyline-beat{position:relative;display:grid;flex:1 1 150px;min-width:132px;max-width:260px;grid-template-columns:auto minmax(0,1fr);grid-template-rows:auto 1fr;" +
|
|
307
|
+
"align-items:baseline;column-gap:9px;padding:8px 8px 9px;border:0;border-radius:8px;background:transparent;cursor:pointer;transition:background-color 130ms ease;}" +
|
|
306
308
|
".plan-storyline-beat:hover{background:var(--sm-muted);}" +
|
|
307
309
|
".plan-storyline-beat--active{background:color-mix(in srgb,var(--sm-fg) 8%,transparent);}" +
|
|
308
310
|
".plan-storyline-beat--draggable{touch-action:none;}" +
|
|
309
311
|
".plan-storyline-beat--dragging{z-index:20;cursor:grabbing;background:var(--sm-surface);box-shadow:0 16px 36px rgba(0,0,0,.5);transition:none;}" +
|
|
310
312
|
".plan-storyline-beat--editing{cursor:text;}" +
|
|
311
|
-
".plan-storyline-beat-slide{
|
|
313
|
+
".plan-storyline-beat-slide{grid-row:1;grid-column:1;color:color-mix(in srgb,var(--sm-dim) 55%,transparent);" +
|
|
312
314
|
"font-family:Georgia,'Times New Roman',serif;font-size:21px;font-weight:400;line-height:1;transition:color 130ms ease;}" +
|
|
313
315
|
".plan-storyline-beat--active .plan-storyline-beat-slide{color:var(--plan-story);}" +
|
|
314
|
-
".plan-storyline-beat-text{display:-webkit-box;min-width:0;overflow:hidden;color:var(--sm-dim);font-size:12px;" +
|
|
316
|
+
".plan-storyline-beat-text{grid-row:1;grid-column:2;display:-webkit-box;min-width:0;overflow:hidden;color:var(--sm-dim);font-size:12px;" +
|
|
315
317
|
"font-weight:500;line-height:1.35;-webkit-box-orient:vertical;-webkit-line-clamp:2;}" +
|
|
316
318
|
".plan-storyline-beat--active .plan-storyline-beat-text{color:var(--sm-fg);font-weight:600;}" +
|
|
317
319
|
".plan-storyline-beat-text[contenteditable=\"true\"]{display:block;border-radius:4px;cursor:text;outline:none;-webkit-line-clamp:unset;}" +
|
|
320
|
+
".plan-storyboard-layout-picker--beat{grid-row:2;grid-column:2;align-self:end;min-width:0;margin:5px 0 0;max-width:100%;}" +
|
|
321
|
+
".plan-storyboard-layout-picker--beat .plan-storyboard-layout-current{width:auto;max-width:100%;gap:6px;border-color:transparent;border-radius:5px;padding:2px 4px;margin-left:-4px;color:var(--sm-dim);font-size:10.5px;font-weight:500;}" +
|
|
322
|
+
".plan-storyboard-layout-picker--beat .plan-storyboard-layout-current:not(:disabled):hover{border-color:var(--sm-line);}" +
|
|
323
|
+
".plan-storyboard-layout-picker--beat .plan-storyboard-layout-current>.plan-storyboard-layout-thumb{width:36px;height:20px;}" +
|
|
324
|
+
".plan-storyboard-layout-picker--beat .plan-storyboard-layout-menu{right:auto;min-width:320px;}" +
|
|
318
325
|
/* "updated" badge — this slide changed in the round that just arrived */
|
|
319
326
|
".plan-storyline-beat-updated{position:absolute;top:-6px;left:6px;border-radius:999px;padding:0 6px;" +
|
|
320
327
|
"background:color-mix(in srgb,var(--sm-accent) 22%,var(--sm-bg));color:var(--sm-accent);" +
|
|
@@ -357,8 +364,8 @@
|
|
|
357
364
|
"linear-gradient(90deg,color-mix(in srgb,var(--plan-annotation) 13%,transparent) 1px,transparent 1px);" +
|
|
358
365
|
"background-size:calc(100%/12) calc(100%/6);background-color:color-mix(in srgb,#3565d4 3%,transparent);}" +
|
|
359
366
|
".plan-storyboard-block{position:absolute;display:flex;min-width:0;min-height:0;flex-direction:column;gap:clamp(4px,.72vw,7px);" +
|
|
360
|
-
"overflow:hidden;border:1.5px dashed color-mix(in srgb,var(--plan-annotation)
|
|
361
|
-
"background:color-mix(in srgb,var(--sm-surface)
|
|
367
|
+
"overflow:hidden;border:1.5px dashed color-mix(in srgb,var(--plan-annotation) 55%,transparent);border-radius:6px;" +
|
|
368
|
+
"background:color-mix(in srgb,var(--sm-surface) 94%,transparent);box-shadow:0 1px 4px rgba(15,23,42,.12);padding:clamp(6px,.95vw,10px);}" +
|
|
362
369
|
".plan-storyboard-block[data-emphasis=\"primary\"]{border-width:2px;border-color:color-mix(in srgb,var(--plan-annotation) 70%,transparent);}" +
|
|
363
370
|
".plan-storyboard-block[data-emphasis=\"tertiary\"]{opacity:.78;}" +
|
|
364
371
|
".plan-storyboard-block--movable{cursor:grab;touch-action:none;}" +
|
|
@@ -392,6 +399,40 @@
|
|
|
392
399
|
".plan-storyboard-field[data-field=\"content\"] .plan-storyboard-field-control{color:var(--sm-fg);font-weight:500;line-height:1.18;}" +
|
|
393
400
|
".plan-storyboard-detail{position:sticky;top:0;max-height:min(62vh,620px);overflow:auto;padding:30px 0 0;}" +
|
|
394
401
|
".plan-storyboard-detail-label{margin:0 0 5px;color:var(--sm-dim);font-size:10px;font-weight:700;letter-spacing:.04em;line-height:1.2;text-transform:uppercase;}" +
|
|
402
|
+
".plan-storyboard-frame--chrome{background:#fff;}" +
|
|
403
|
+
".plan-storyboard-chrome{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;pointer-events:none;opacity:.95;}" +
|
|
404
|
+
".plan-storyboard-zones{position:absolute;inset:0;pointer-events:none;}" +
|
|
405
|
+
".plan-storyboard-zone-wash{position:absolute;background:rgba(15,23,42,.16);}" +
|
|
406
|
+
".plan-storyboard-zone-notice-layer{position:absolute;inset:0;z-index:30;pointer-events:none;}" +
|
|
407
|
+
".plan-storyboard-zone-canvas{position:absolute;border:2px solid rgba(53,101,212,.8);border-radius:4px;box-shadow:0 0 0 1px rgba(255,255,255,.55);}" +
|
|
408
|
+
".plan-storyboard-zone-label{position:absolute;top:-1px;left:-1px;border-radius:3px 0 4px 0;background:rgba(53,101,212,.85);padding:1px 7px;color:#fff;font-size:9px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;}" +
|
|
409
|
+
".plan-storyboard-zone-notice{position:absolute;top:10px;left:50%;transform:translateX(-50%);max-width:78%;border-radius:999px;background:rgba(15,23,42,.82);padding:5px 14px;color:#fff;font-size:11px;font-weight:600;line-height:1.35;text-align:center;}" +
|
|
410
|
+
".plan-storyboard-ph{position:absolute;border:1px solid rgba(15,23,42,.5);border-radius:3px;background:rgba(255,255,255,.12);box-shadow:0 0 0 1px rgba(255,255,255,.55);}" +
|
|
411
|
+
".plan-storyboard-ph-label{position:absolute;top:-1px;left:-1px;border-radius:2px 0 3px 0;background:rgba(15,23,42,.78);padding:0 5px;color:#fff;font-size:8px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;box-shadow:0 0 0 1px rgba(255,255,255,.5);}" +
|
|
412
|
+
".plan-storyboard-image-layer{position:absolute;inset:0;pointer-events:none;}" +
|
|
413
|
+
".plan-storyboard-image-slot{position:absolute;display:flex;flex-direction:column;pointer-events:auto;border:1px dashed rgba(53,101,212,.75);border-radius:4px;background:rgba(255,255,255,.55);overflow:hidden;}" +
|
|
414
|
+
".plan-storyboard-image-slot--filled{border-style:solid;background:rgba(255,255,255,.86);}" +
|
|
415
|
+
".plan-storyboard-image-caption{flex:none;padding:1px 5px;background:rgba(53,101,212,.85);color:#fff;font-size:8px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;}" +
|
|
416
|
+
".plan-storyboard-image-input{flex:1;width:100%;min-height:0;resize:none;border:0;background:transparent;padding:4px 5px;font:inherit;font-size:10px;line-height:1.3;color:#0f172a;}" +
|
|
417
|
+
".plan-storyboard-image-input:focus{outline:none;}" +
|
|
418
|
+
".plan-storyboard-image-input::placeholder{color:rgba(15,23,42,.45);font-style:italic;}" +
|
|
419
|
+
".plan-storyboard-layout-picker{position:relative;margin:0 0 18px;}" +
|
|
420
|
+
".plan-storyboard-layout-current{display:flex;align-items:center;gap:8px;width:100%;border:1px solid var(--sm-line);border-radius:8px;background:transparent;padding:6px 8px;color:var(--sm-fg);font-size:12.5px;font-weight:550;text-align:left;cursor:pointer;font-family:inherit;}" +
|
|
421
|
+
".plan-storyboard-layout-current:disabled{cursor:default;opacity:.85;}" +
|
|
422
|
+
".plan-storyboard-layout-current:not(:disabled):hover{background:rgba(255,255,255,.05);}" +
|
|
423
|
+
".plan-storyboard-layout-thumb{width:52px;height:30px;flex:none;border:1px solid var(--sm-line);border-radius:4px;object-fit:cover;background:#fff;}" +
|
|
424
|
+
".plan-storyboard-layout-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}" +
|
|
425
|
+
".plan-storyboard-layout-chevron{flex:none;margin-left:auto;color:var(--sm-dim);font-size:11px;line-height:1;}" +
|
|
426
|
+
".plan-storyboard-layout-unknown{flex:none;border-radius:999px;background:rgba(217,75,75,.14);padding:1px 7px;color:#e07272;font-size:10px;font-weight:650;}" +
|
|
427
|
+
".plan-storyboard-layout-menu{position:absolute;z-index:30;top:calc(100% + 4px);left:0;right:0;max-height:300px;overflow:auto;border:1px solid var(--sm-line);border-radius:10px;background:var(--sm-bg);box-shadow:0 10px 32px rgba(0,0,0,.35);padding:4px;}" +
|
|
428
|
+
".plan-storyboard-layout-option{display:flex;align-items:center;gap:8px;width:100%;border:0;border-radius:7px;background:transparent;padding:6px;color:var(--sm-fg);text-align:left;cursor:pointer;font-family:inherit;}" +
|
|
429
|
+
".plan-storyboard-layout-option:hover{background:rgba(255,255,255,.06);}" +
|
|
430
|
+
".plan-storyboard-layout-option--selected{background:rgba(255,255,255,.09);}" +
|
|
431
|
+
".plan-storyboard-layout-thumb--option{width:72px;height:41px;}" +
|
|
432
|
+
".plan-storyboard-layout-option-text{display:flex;min-width:0;flex-direction:column;gap:1px;}" +
|
|
433
|
+
".plan-storyboard-layout-option-text .plan-storyboard-layout-name{font-size:12.5px;font-weight:600;}" +
|
|
434
|
+
".plan-storyboard-layout-purpose{overflow:hidden;color:var(--sm-dim);font-size:11px;line-height:1.3;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;}" +
|
|
435
|
+
".plan-storyboard-layout-canvas{color:var(--sm-dim);font-size:10px;font-variant-numeric:tabular-nums;}" +
|
|
395
436
|
".plan-storyboard-key-message{margin:0 0 18px;color:var(--sm-fg);font-size:15px;font-weight:650;line-height:1.34;white-space:pre-wrap;word-break:break-word;outline:none;}" +
|
|
396
437
|
".plan-storyboard-detail-text{min-height:9em;border:0;background:transparent;color:var(--sm-fg);font-size:13.5px;font-weight:400;line-height:1.42;outline:none;white-space:pre-wrap;word-break:break-word;}" +
|
|
397
438
|
".plan-storyboard-warnings{margin:.75em 0 0;padding-left:1.2em;font-size:.86em;color:var(--sm-dim);}" +
|
|
@@ -419,7 +460,6 @@
|
|
|
419
460
|
".sm-ledger-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}" +
|
|
420
461
|
".sm-ledger-status{flex:none;border-radius:999px;padding:1px 8px;font-size:10px;font-weight:600;" +
|
|
421
462
|
"letter-spacing:.04em;text-transform:uppercase;border:1px solid var(--sm-border);color:var(--sm-dim);}" +
|
|
422
|
-
".sm-ledger-status[data-state=\"queued\"]{color:var(--sm-warn);border-color:rgba(246,196,84,.4);}" +
|
|
423
463
|
".sm-ledger-status[data-state=\"sent\"]{color:var(--sm-accent);border-color:rgba(169,194,255,.4);}" +
|
|
424
464
|
".sm-ledger-status[data-state=\"addressed\"]{color:var(--sm-success);border-color:rgba(46,204,113,.4);}" +
|
|
425
465
|
/* scope control */
|
|
@@ -439,6 +479,7 @@
|
|
|
439
479
|
"background:var(--sm-accent);color:#0f1220;transition:transform .14s ease,box-shadow .14s ease,opacity .14s ease;}" +
|
|
440
480
|
".sm-send svg{width:16px;height:16px;margin:1px 1px 0 0;}" +
|
|
441
481
|
".sm-send:hover{transform:translateY(-1px) scale(1.05);box-shadow:0 8px 22px rgba(169,194,255,.35);}" +
|
|
482
|
+
".sm-send:disabled,.sm-chat-ta:disabled{opacity:.45;cursor:not-allowed;transform:none;box-shadow:none;}" +
|
|
442
483
|
".sm-send.is-off{opacity:.35;cursor:not-allowed;pointer-events:none;}" +
|
|
443
484
|
/* story-iteration pill (send while a storyline draft is staged) */
|
|
444
485
|
".sm-send-pill{display:inline-flex;align-items:center;gap:7px;height:38px;flex-shrink:0;border:0;border-radius:999px;" +
|
|
@@ -532,7 +573,7 @@
|
|
|
532
573
|
// the unsent draft (the server nonce is added by the shared session). The
|
|
533
574
|
// plan body is NOT in here — see BODY_KEY.
|
|
534
575
|
function ctxFields() {
|
|
535
|
-
return { round: roundNo, selectedIndex: S.selectedIndex, draft: feedbackText };
|
|
576
|
+
return { round: roundNo, selectedIndex: S.selectedIndex, draft: feedbackText, working: working };
|
|
536
577
|
}
|
|
537
578
|
function saveCtx() {
|
|
538
579
|
SES.saveCtx(ctxFields());
|
|
@@ -541,12 +582,6 @@
|
|
|
541
582
|
function saveDraftSoon() {
|
|
542
583
|
SES.saveCtxSoon(ctxFields());
|
|
543
584
|
}
|
|
544
|
-
function queuedComments() {
|
|
545
|
-
return SES.queued(ledger);
|
|
546
|
-
}
|
|
547
|
-
function flushBacklog() {
|
|
548
|
-
return SES.flushBacklog(ledger);
|
|
549
|
-
}
|
|
550
585
|
|
|
551
586
|
// Per-slide segment diff between the previous round's plan body and this one,
|
|
552
587
|
// keyed by the slide NUMBER the `### Slide N` heading declares — never by the
|
|
@@ -633,7 +668,7 @@
|
|
|
633
668
|
if (editedBadge) editedBadge.style.display = planWasEdited() ? "" : "none";
|
|
634
669
|
if (hintEl) {
|
|
635
670
|
hintEl.textContent = working
|
|
636
|
-
? "The agent is revising · read on
|
|
671
|
+
? "The agent is revising · read on and browse the strip — commenting and editing resume with the next round"
|
|
637
672
|
: "Shape the red thread and storyboards below · drag to reorder, rename moves, edit blocks · decide when it reads right";
|
|
638
673
|
}
|
|
639
674
|
|
|
@@ -655,16 +690,6 @@
|
|
|
655
690
|
sub.style.display = "";
|
|
656
691
|
sub.textContent = "The agent is revising — approve the next round";
|
|
657
692
|
}
|
|
658
|
-
} else if (queuedComments().length) {
|
|
659
|
-
// Only reachable when a backlog flush failed: approving would drop them.
|
|
660
|
-
approveArmed = false;
|
|
661
|
-
approveBtn.classList.remove("is-armed");
|
|
662
|
-
approveBtn.classList.add("is-off");
|
|
663
|
-
if (main) main.textContent = "Approve plan";
|
|
664
|
-
if (sub) {
|
|
665
|
-
sub.style.display = "";
|
|
666
|
-
sub.textContent = "Send your queued comments first";
|
|
667
|
-
}
|
|
668
693
|
} else if (S.storyDraft) {
|
|
669
694
|
// Approve is blocked until the staged storyline is sent or discarded.
|
|
670
695
|
approveArmed = false;
|
|
@@ -872,7 +897,7 @@
|
|
|
872
897
|
dossier.appendChild(root);
|
|
873
898
|
|
|
874
899
|
root.appendChild(
|
|
875
|
-
renderStrip(slides, committed, beatMessages, storyboardData, canEdit, activeIndex),
|
|
900
|
+
renderStrip(slides, committed, beatMessages, storyboardData, storyboardSegments, canEdit, activeIndex),
|
|
876
901
|
);
|
|
877
902
|
|
|
878
903
|
var body = el("div", "plan-dossier-body");
|
|
@@ -910,10 +935,13 @@
|
|
|
910
935
|
}
|
|
911
936
|
|
|
912
937
|
// ---------- storyline tab strip ----------
|
|
913
|
-
function renderStrip(slides, committed, committedMessages, storyboardData, canEdit, activeIndex) {
|
|
938
|
+
function renderStrip(slides, committed, committedMessages, storyboardData, storyboardSegments, canEdit, activeIndex) {
|
|
914
939
|
var section = el("section", "plan-storyline");
|
|
915
940
|
var head = el("div", "plan-storyline-head");
|
|
916
|
-
|
|
941
|
+
var headText = el("div", "plan-storyline-head-text");
|
|
942
|
+
headText.appendChild(el("span", "plan-storyline-label", "Red thread"));
|
|
943
|
+
head.appendChild(headText);
|
|
944
|
+
head.appendChild(renderDeckLayoutKnob(storyboardData));
|
|
917
945
|
section.appendChild(head);
|
|
918
946
|
|
|
919
947
|
var track = el("ol", "plan-storyline-track");
|
|
@@ -1094,6 +1122,24 @@
|
|
|
1094
1122
|
});
|
|
1095
1123
|
}
|
|
1096
1124
|
li.appendChild(textEl);
|
|
1125
|
+
// Layout chip: the board's layout, picked in place. A tab resolves to the
|
|
1126
|
+
// board of the slide it originally was; staged new slides have no board.
|
|
1127
|
+
var chipIndex = S.storyDraft ? item.originalIndex : index;
|
|
1128
|
+
var chipSegment = chipIndex === null || chipIndex === undefined ? undefined : storyboardSegments[chipIndex];
|
|
1129
|
+
if (chipSegment && chipSegment.data) {
|
|
1130
|
+
var chip = renderLayoutPicker(chipSegment.data.layout, {
|
|
1131
|
+
className: "plan-storyboard-layout-picker--beat",
|
|
1132
|
+
ariaLabel: "Choose the layout of slide " + (index + 1),
|
|
1133
|
+
onPick: function (componentName) {
|
|
1134
|
+
var d = {};
|
|
1135
|
+
for (var k in chipSegment.data) d[k] = chipSegment.data[k];
|
|
1136
|
+
d.layout = componentName;
|
|
1137
|
+
commitStoryboard(chipSegment, d);
|
|
1138
|
+
},
|
|
1139
|
+
});
|
|
1140
|
+
chip.setAttribute("data-no-drag", "");
|
|
1141
|
+
li.appendChild(chip);
|
|
1142
|
+
}
|
|
1097
1143
|
|
|
1098
1144
|
if (canEdit) {
|
|
1099
1145
|
var rm = el("button", "plan-storyline-beat-remove", ICONS.x);
|
|
@@ -1237,13 +1283,16 @@
|
|
|
1237
1283
|
// Commit a storyboard mutation into S.value and re-render the surface. Safe to
|
|
1238
1284
|
// full-re-render because every canvas edit commits on blur / pointerup, so no
|
|
1239
1285
|
// field is focused at commit time.
|
|
1240
|
-
function
|
|
1286
|
+
function commitPlan(nextValue) {
|
|
1241
1287
|
if (locked()) return;
|
|
1242
|
-
S.value =
|
|
1288
|
+
S.value = nextValue;
|
|
1243
1289
|
approveArmed = false; // a storyboard edit disarms a pending approve-confirm
|
|
1244
1290
|
refreshChrome();
|
|
1245
1291
|
renderSurface();
|
|
1246
1292
|
}
|
|
1293
|
+
function commitStoryboard(segment, nextData) {
|
|
1294
|
+
commitPlan(PLAN.replaceStoryboardSegment(S.value, segment, nextData));
|
|
1295
|
+
}
|
|
1247
1296
|
|
|
1248
1297
|
function renderField(variant, value, onCommit) {
|
|
1249
1298
|
var wrap = el("div", "plan-storyboard-field");
|
|
@@ -1286,13 +1335,43 @@
|
|
|
1286
1335
|
return wrap;
|
|
1287
1336
|
}
|
|
1288
1337
|
|
|
1338
|
+
// Where the generic body-safe area lands on the frame. Blocks are planned in
|
|
1339
|
+
// the 1600×900 stage inside BODY_SAFE_AREA; a bound layout's content zone
|
|
1340
|
+
// (normalized 0..1) IS the real body, so the safe area is stretched onto it —
|
|
1341
|
+
// every block reads as a fraction of the body, never as a bare stage position
|
|
1342
|
+
// that may fall outside the template's content area. Without a zone the
|
|
1343
|
+
// stage is the frame. {x,y} = normalized frame offset, {sx,sy} = normalized
|
|
1344
|
+
// frame units per stage unit.
|
|
1345
|
+
function frameMapFor(data) {
|
|
1346
|
+
var selected = selectedPlanLayout(data);
|
|
1347
|
+
var zone = selected && selected.canvasBounds;
|
|
1348
|
+
// A zero-size zone would make the per-pixel scale infinite and a drag
|
|
1349
|
+
// write NaN coordinates — treat it as no zone.
|
|
1350
|
+
if (!zone || !(zone.w > 0) || !(zone.h > 0)) {
|
|
1351
|
+
return { x: 0, y: 0, sx: 1 / PLAN.STORYBOARD_WIDTH, sy: 1 / PLAN.STORYBOARD_HEIGHT };
|
|
1352
|
+
}
|
|
1353
|
+
var A = PLAN.BODY_SAFE_AREA;
|
|
1354
|
+
var sx = zone.w / A.w;
|
|
1355
|
+
var sy = zone.h / A.h;
|
|
1356
|
+
return { x: zone.x - A.x * sx, y: zone.y - A.y * sy, sx: sx, sy: sy };
|
|
1357
|
+
}
|
|
1358
|
+
function placeBlock(node, map, x, y, w, h) {
|
|
1359
|
+
node.style.left = (map.x + x * map.sx) * 100 + "%";
|
|
1360
|
+
node.style.top = (map.y + y * map.sy) * 100 + "%";
|
|
1361
|
+
node.style.width = w * map.sx * 100 + "%";
|
|
1362
|
+
node.style.height = h * map.sy * 100 + "%";
|
|
1363
|
+
}
|
|
1364
|
+
// Stage units per client pixel, per axis, for a 16:9 frame `width` px wide.
|
|
1365
|
+
function stagePerPixel(map, width) {
|
|
1366
|
+
var height = (width * PLAN.STORYBOARD_HEIGHT) / PLAN.STORYBOARD_WIDTH;
|
|
1367
|
+
return { x: 1 / (width * map.sx), y: 1 / (height * map.sy) };
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1289
1370
|
function renderBlock(frameEl, block, data, segment, canRemove) {
|
|
1290
1371
|
var node = el("div", "plan-storyboard-block" + (locked() ? "" : " plan-storyboard-block--movable"));
|
|
1291
1372
|
node.setAttribute("data-emphasis", block.emphasis || "secondary");
|
|
1292
|
-
|
|
1293
|
-
node
|
|
1294
|
-
node.style.width = (block.w / PLAN.STORYBOARD_WIDTH) * 100 + "%";
|
|
1295
|
-
node.style.height = (block.h / PLAN.STORYBOARD_HEIGHT) * 100 + "%";
|
|
1373
|
+
var map = frameMapFor(data);
|
|
1374
|
+
placeBlock(node, map, block.x, block.y, block.w, block.h);
|
|
1296
1375
|
|
|
1297
1376
|
function change(patch) {
|
|
1298
1377
|
commitStoryboard(segment, updateBlock(data, block.id, patch));
|
|
@@ -1335,13 +1414,13 @@
|
|
|
1335
1414
|
|
|
1336
1415
|
if (!locked()) {
|
|
1337
1416
|
node.title = "Drag to reposition";
|
|
1338
|
-
wireBlockDrag(node, frameEl, block, change);
|
|
1417
|
+
wireBlockDrag(node, frameEl, block, map, change);
|
|
1339
1418
|
var rz = el("button", "plan-storyboard-block-resize");
|
|
1340
1419
|
rz.type = "button";
|
|
1341
1420
|
rz.setAttribute("data-no-drag", "");
|
|
1342
1421
|
rz.title = "Drag to resize";
|
|
1343
1422
|
rz.setAttribute("aria-label", "Drag to resize");
|
|
1344
|
-
wireBlockResize(rz, frameEl, block, change);
|
|
1423
|
+
wireBlockResize(rz, frameEl, block, map, change);
|
|
1345
1424
|
node.appendChild(rz);
|
|
1346
1425
|
}
|
|
1347
1426
|
return node;
|
|
@@ -1361,7 +1440,7 @@
|
|
|
1361
1440
|
h: clamp(h, PLAN.MIN_BLOCK_HEIGHT, A.y + A.h - block.y),
|
|
1362
1441
|
};
|
|
1363
1442
|
}
|
|
1364
|
-
function wireBlockDrag(node, frameEl, block, change) {
|
|
1443
|
+
function wireBlockDrag(node, frameEl, block, map, change) {
|
|
1365
1444
|
var st = null;
|
|
1366
1445
|
node.addEventListener("pointerdown", function (e) {
|
|
1367
1446
|
if (locked() || e.button !== 0) return;
|
|
@@ -1374,7 +1453,7 @@
|
|
|
1374
1453
|
oy: e.clientY,
|
|
1375
1454
|
sx: block.x,
|
|
1376
1455
|
sy: block.y,
|
|
1377
|
-
scale:
|
|
1456
|
+
scale: stagePerPixel(map, rect.width),
|
|
1378
1457
|
moved: false,
|
|
1379
1458
|
};
|
|
1380
1459
|
try {
|
|
@@ -1390,9 +1469,8 @@
|
|
|
1390
1469
|
if (!st.moved && Math.hypot(dx, dy) < BLOCK_DRAG_THRESHOLD) return;
|
|
1391
1470
|
st.moved = true;
|
|
1392
1471
|
node.classList.add("plan-storyboard-block--dragging");
|
|
1393
|
-
var p = clampPos(block, st.sx + dx * st.scale, st.sy + dy * st.scale);
|
|
1394
|
-
node
|
|
1395
|
-
node.style.top = (p.y / PLAN.STORYBOARD_HEIGHT) * 100 + "%";
|
|
1472
|
+
var p = clampPos(block, st.sx + dx * st.scale.x, st.sy + dy * st.scale.y);
|
|
1473
|
+
placeBlock(node, map, p.x, p.y, block.w, block.h);
|
|
1396
1474
|
});
|
|
1397
1475
|
function end(e) {
|
|
1398
1476
|
if (!st || st.pointerId !== e.pointerId) return;
|
|
@@ -1407,7 +1485,7 @@
|
|
|
1407
1485
|
node.classList.remove("plan-storyboard-block--dragging");
|
|
1408
1486
|
return;
|
|
1409
1487
|
}
|
|
1410
|
-
var p = clampPos(block, s.sx + (e.clientX - s.ox) * s.scale, s.sy + (e.clientY - s.oy) * s.scale);
|
|
1488
|
+
var p = clampPos(block, s.sx + (e.clientX - s.ox) * s.scale.x, s.sy + (e.clientY - s.oy) * s.scale.y);
|
|
1411
1489
|
var x = Math.round(p.x);
|
|
1412
1490
|
var y = Math.round(p.y);
|
|
1413
1491
|
if (x !== block.x || y !== block.y) change({ x: x, y: y });
|
|
@@ -1420,7 +1498,7 @@
|
|
|
1420
1498
|
node.classList.remove("plan-storyboard-block--dragging");
|
|
1421
1499
|
});
|
|
1422
1500
|
}
|
|
1423
|
-
function wireBlockResize(handle, frameEl, block, change) {
|
|
1501
|
+
function wireBlockResize(handle, frameEl, block, map, change) {
|
|
1424
1502
|
var st = null;
|
|
1425
1503
|
handle.addEventListener("pointerdown", function (e) {
|
|
1426
1504
|
if (locked() || e.button !== 0) return;
|
|
@@ -1433,7 +1511,7 @@
|
|
|
1433
1511
|
oy: e.clientY,
|
|
1434
1512
|
sw: block.w,
|
|
1435
1513
|
sh: block.h,
|
|
1436
|
-
scale:
|
|
1514
|
+
scale: stagePerPixel(map, rect.width),
|
|
1437
1515
|
moved: false,
|
|
1438
1516
|
};
|
|
1439
1517
|
try {
|
|
@@ -1449,10 +1527,9 @@
|
|
|
1449
1527
|
if (!st.moved && Math.hypot(dx, dy) < BLOCK_DRAG_THRESHOLD) return;
|
|
1450
1528
|
st.moved = true;
|
|
1451
1529
|
var node = handle.parentNode;
|
|
1452
|
-
var p = clampSize(block, st.sw + dx * st.scale, st.sh + dy * st.scale);
|
|
1530
|
+
var p = clampSize(block, st.sw + dx * st.scale.x, st.sh + dy * st.scale.y);
|
|
1453
1531
|
node.classList.add("plan-storyboard-block--dragging");
|
|
1454
|
-
node.
|
|
1455
|
-
node.style.height = (p.h / PLAN.STORYBOARD_HEIGHT) * 100 + "%";
|
|
1532
|
+
placeBlock(node, map, block.x, block.y, p.w, p.h);
|
|
1456
1533
|
});
|
|
1457
1534
|
function end(e) {
|
|
1458
1535
|
if (!st || st.pointerId !== e.pointerId) return;
|
|
@@ -1464,7 +1541,7 @@
|
|
|
1464
1541
|
var s = st;
|
|
1465
1542
|
st = null;
|
|
1466
1543
|
if (!s.moved) return;
|
|
1467
|
-
var p = clampSize(block, s.sw + (e.clientX - s.ox) * s.scale, s.sh + (e.clientY - s.oy) * s.scale);
|
|
1544
|
+
var p = clampSize(block, s.sw + (e.clientX - s.ox) * s.scale.x, s.sh + (e.clientY - s.oy) * s.scale.y);
|
|
1468
1545
|
var w = Math.round(p.w);
|
|
1469
1546
|
var h = Math.round(p.h);
|
|
1470
1547
|
if (w !== block.w || h !== block.h) change({ w: w, h: h });
|
|
@@ -1508,8 +1585,161 @@
|
|
|
1508
1585
|
return field;
|
|
1509
1586
|
}
|
|
1510
1587
|
|
|
1588
|
+
// Layout picker: current choice (chrome thumbnail + name) plus a dropdown of
|
|
1589
|
+
// the bundle's layouts (CFG.layouts; empty → read-only). Used per slide in
|
|
1590
|
+
// the details aside and once for the deck-level knob; `opts.onPick` owns the
|
|
1591
|
+
// commit. All labels/names go through textContent — bundle strings never
|
|
1592
|
+
// reach innerHTML.
|
|
1593
|
+
// One short line per option instead of the codegen's geometry dump: the
|
|
1594
|
+
// count is what a human picks on; the exact rectangles are drawn on the
|
|
1595
|
+
// storyboard frame once the layout is chosen.
|
|
1596
|
+
function contentAreaSummary(option) {
|
|
1597
|
+
var n = option.contentAreas ? option.contentAreas.length : 0;
|
|
1598
|
+
if (!n && option.canvas) n = (option.canvas.match(/(?:^|;)\s*\d+\./g) || []).length || 1;
|
|
1599
|
+
if (!n) return "Chrome only";
|
|
1600
|
+
return n === 1 ? "1 content area" : n + " content areas";
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function renderLayoutPicker(value, opts) {
|
|
1604
|
+
var options = Array.isArray(CFG.layouts) ? CFG.layouts : [];
|
|
1605
|
+
var root = el("div", "plan-storyboard-layout-picker" + (opts.className ? " " + opts.className : ""));
|
|
1606
|
+
var selected = null;
|
|
1607
|
+
for (var i = 0; i < options.length; i++) {
|
|
1608
|
+
if (options[i].componentName === value) selected = options[i];
|
|
1609
|
+
}
|
|
1610
|
+
var canPick = !locked() && options.length > 0;
|
|
1611
|
+
|
|
1612
|
+
var current = document.createElement("button");
|
|
1613
|
+
current.type = "button";
|
|
1614
|
+
current.className = "plan-storyboard-layout-current";
|
|
1615
|
+
current.setAttribute("aria-label", opts.ariaLabel || "Choose the slide's layout");
|
|
1616
|
+
current.disabled = !canPick;
|
|
1617
|
+
if (selected && selected.previewUrl) {
|
|
1618
|
+
var thumb = document.createElement("img");
|
|
1619
|
+
thumb.className = "plan-storyboard-layout-thumb";
|
|
1620
|
+
thumb.src = selected.previewUrl;
|
|
1621
|
+
thumb.alt = "";
|
|
1622
|
+
thumb.setAttribute("aria-hidden", "true");
|
|
1623
|
+
current.appendChild(thumb);
|
|
1624
|
+
}
|
|
1625
|
+
var nameSpan = el("span", "plan-storyboard-layout-name");
|
|
1626
|
+
nameSpan.textContent = value || opts.placeholder || "No layout chosen";
|
|
1627
|
+
current.appendChild(nameSpan);
|
|
1628
|
+
if (value && options.length > 0 && !selected) {
|
|
1629
|
+
var badge = el("span", "plan-storyboard-layout-unknown");
|
|
1630
|
+
badge.textContent = "Not in this brand";
|
|
1631
|
+
current.appendChild(badge);
|
|
1632
|
+
}
|
|
1633
|
+
if (canPick) {
|
|
1634
|
+
var chevron = el("span", "plan-storyboard-layout-chevron", "\u25BE");
|
|
1635
|
+
chevron.setAttribute("aria-hidden", "true");
|
|
1636
|
+
current.appendChild(chevron);
|
|
1637
|
+
}
|
|
1638
|
+
root.appendChild(current);
|
|
1639
|
+
|
|
1640
|
+
if (!canPick) return root;
|
|
1641
|
+
|
|
1642
|
+
var menu = null;
|
|
1643
|
+
function closeMenu() {
|
|
1644
|
+
if (menu) {
|
|
1645
|
+
menu.remove();
|
|
1646
|
+
menu = null;
|
|
1647
|
+
document.removeEventListener("pointerdown", onOutside);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
function onOutside(e) {
|
|
1651
|
+
if (!root.contains(e.target)) closeMenu();
|
|
1652
|
+
}
|
|
1653
|
+
current.addEventListener("click", function () {
|
|
1654
|
+
if (menu) {
|
|
1655
|
+
closeMenu();
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
menu = el("div", "plan-storyboard-layout-menu");
|
|
1659
|
+
menu.setAttribute("role", "listbox");
|
|
1660
|
+
options.forEach(function (option) {
|
|
1661
|
+
var btn = document.createElement("button");
|
|
1662
|
+
btn.type = "button";
|
|
1663
|
+
btn.className =
|
|
1664
|
+
option.componentName === value
|
|
1665
|
+
? "plan-storyboard-layout-option plan-storyboard-layout-option--selected"
|
|
1666
|
+
: "plan-storyboard-layout-option";
|
|
1667
|
+
btn.setAttribute("role", "option");
|
|
1668
|
+
btn.setAttribute("aria-selected", option.componentName === value ? "true" : "false");
|
|
1669
|
+
if (option.previewUrl) {
|
|
1670
|
+
var t = document.createElement("img");
|
|
1671
|
+
t.className = "plan-storyboard-layout-thumb plan-storyboard-layout-thumb--option";
|
|
1672
|
+
t.src = option.previewUrl;
|
|
1673
|
+
t.alt = "";
|
|
1674
|
+
t.setAttribute("aria-hidden", "true");
|
|
1675
|
+
btn.appendChild(t);
|
|
1676
|
+
}
|
|
1677
|
+
var text = el("span", "plan-storyboard-layout-option-text");
|
|
1678
|
+
var n = el("span", "plan-storyboard-layout-name");
|
|
1679
|
+
n.textContent = option.componentName;
|
|
1680
|
+
text.appendChild(n);
|
|
1681
|
+
if (option.purpose) {
|
|
1682
|
+
var p = el("span", "plan-storyboard-layout-purpose");
|
|
1683
|
+
p.textContent = option.purpose;
|
|
1684
|
+
text.appendChild(p);
|
|
1685
|
+
}
|
|
1686
|
+
var c = el("span", "plan-storyboard-layout-canvas");
|
|
1687
|
+
c.textContent = contentAreaSummary(option);
|
|
1688
|
+
text.appendChild(c);
|
|
1689
|
+
btn.appendChild(text);
|
|
1690
|
+
btn.addEventListener("click", function () {
|
|
1691
|
+
closeMenu();
|
|
1692
|
+
if (option.componentName === value) return;
|
|
1693
|
+
opts.onPick(option.componentName);
|
|
1694
|
+
});
|
|
1695
|
+
menu.appendChild(btn);
|
|
1696
|
+
});
|
|
1697
|
+
root.appendChild(menu);
|
|
1698
|
+
document.addEventListener("pointerdown", onOutside);
|
|
1699
|
+
});
|
|
1700
|
+
return root;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// Deck-level layout knob: the one layout every board shares (or "Mixed"); a
|
|
1704
|
+
// pick binds EVERY storyboard's `layout` in one commit.
|
|
1705
|
+
function renderDeckLayoutKnob(storyboardData) {
|
|
1706
|
+
var wrap = el("div", "plan-storyline-deck-layout");
|
|
1707
|
+
wrap.appendChild(el("span", "plan-storyline-deck-layout-label", "Deck layout"));
|
|
1708
|
+
var seen = {};
|
|
1709
|
+
var distinct = 0;
|
|
1710
|
+
for (var i = 0; i < storyboardData.length; i++) {
|
|
1711
|
+
var name = storyboardData[i].layout || "";
|
|
1712
|
+
if (!seen[name]) {
|
|
1713
|
+
seen[name] = true;
|
|
1714
|
+
distinct++;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
var common = distinct === 1 && storyboardData[0] ? storyboardData[0].layout : undefined;
|
|
1718
|
+
wrap.appendChild(
|
|
1719
|
+
renderLayoutPicker(common || undefined, {
|
|
1720
|
+
placeholder: distinct > 1 ? "Mixed" : undefined,
|
|
1721
|
+
ariaLabel: "Set one layout for every slide",
|
|
1722
|
+
onPick: function (componentName) {
|
|
1723
|
+
commitPlan(PLAN.setStoryboardLayoutForAll(S.value, componentName));
|
|
1724
|
+
},
|
|
1725
|
+
}),
|
|
1726
|
+
);
|
|
1727
|
+
return wrap;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1511
1730
|
function renderDetails(data, segment) {
|
|
1512
1731
|
var aside = el("aside", "plan-storyboard-detail");
|
|
1732
|
+
aside.appendChild(el("div", "plan-storyboard-detail-label", "Layout"));
|
|
1733
|
+
aside.appendChild(
|
|
1734
|
+
renderLayoutPicker(data.layout, {
|
|
1735
|
+
onPick: function (componentName) {
|
|
1736
|
+
var d = {};
|
|
1737
|
+
for (var k in data) d[k] = data[k];
|
|
1738
|
+
d.layout = componentName;
|
|
1739
|
+
commitStoryboard(segment, d);
|
|
1740
|
+
},
|
|
1741
|
+
}),
|
|
1742
|
+
);
|
|
1513
1743
|
aside.appendChild(el("div", "plan-storyboard-detail-label", "Story beat"));
|
|
1514
1744
|
aside.appendChild(
|
|
1515
1745
|
renderDetailField(
|
|
@@ -1552,29 +1782,174 @@
|
|
|
1552
1782
|
return aside;
|
|
1553
1783
|
}
|
|
1554
1784
|
|
|
1785
|
+
// The planned layout, when the storyboard names one the bundle has: its
|
|
1786
|
+
// chrome render grounds the frame and its canvas rect draws the content/
|
|
1787
|
+
// no-go zones. Absent both, the generic hatched bands render as before.
|
|
1788
|
+
function selectedPlanLayout(data) {
|
|
1789
|
+
var options = Array.isArray(CFG.layouts) ? CFG.layouts : [];
|
|
1790
|
+
if (!data.layout) return null;
|
|
1791
|
+
for (var i = 0; i < options.length; i++) {
|
|
1792
|
+
if (options[i].componentName === data.layout) return options[i];
|
|
1793
|
+
}
|
|
1794
|
+
return null;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
function pctOf(v) {
|
|
1798
|
+
return (v * 100).toFixed(2) + "%";
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function renderZoneOverlay(zone, areas) {
|
|
1802
|
+
var zones = el("div", "plan-storyboard-zones");
|
|
1803
|
+
zones.setAttribute("aria-hidden", "true");
|
|
1804
|
+
function wash(style) {
|
|
1805
|
+
var d = el("div", "plan-storyboard-zone-wash");
|
|
1806
|
+
for (var k in style) d.style[k] = style[k];
|
|
1807
|
+
zones.appendChild(d);
|
|
1808
|
+
}
|
|
1809
|
+
wash({ left: "0", top: "0", right: "0", height: pctOf(zone.y) });
|
|
1810
|
+
wash({ left: "0", top: pctOf(zone.y + zone.h), right: "0", bottom: "0" });
|
|
1811
|
+
wash({ left: "0", top: pctOf(zone.y), width: pctOf(zone.x), height: pctOf(zone.h) });
|
|
1812
|
+
wash({
|
|
1813
|
+
left: pctOf(zone.x + zone.w),
|
|
1814
|
+
top: pctOf(zone.y),
|
|
1815
|
+
right: "0",
|
|
1816
|
+
height: pctOf(zone.h),
|
|
1817
|
+
});
|
|
1818
|
+
// One rectangle per writable area; the washes above cover the union bbox
|
|
1819
|
+
// and the gutters between areas stay unmarked on purpose.
|
|
1820
|
+
for (var i = 0; i < areas.length; i++) {
|
|
1821
|
+
var area = areas[i];
|
|
1822
|
+
var rect = el("div", "plan-storyboard-zone-canvas");
|
|
1823
|
+
rect.style.left = pctOf(area.x);
|
|
1824
|
+
rect.style.top = pctOf(area.y);
|
|
1825
|
+
rect.style.width = pctOf(area.w);
|
|
1826
|
+
rect.style.height = pctOf(area.h);
|
|
1827
|
+
var chip = el("span", "plan-storyboard-zone-label");
|
|
1828
|
+
chip.textContent = areas.length > 1 ? "Content area " + (i + 1) : "Content area";
|
|
1829
|
+
rect.appendChild(chip);
|
|
1830
|
+
zones.appendChild(rect);
|
|
1831
|
+
}
|
|
1832
|
+
return zones;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// Per-image-slot prompt fields drawn INSIDE the bound layout's image
|
|
1836
|
+
// placeholders. The value lives structurally in `imagePrompts` (keyed by the
|
|
1837
|
+
// slot's codegen prop name); a populated prompt is binding guidance for the
|
|
1838
|
+
// downstream image search/generation, empty = the AI decides. Own layer
|
|
1839
|
+
// after the blocks (paint order = DOM order), click-through except the fields.
|
|
1840
|
+
function renderImagePrompts(segment, data, slots) {
|
|
1841
|
+
var layer = el("div", "plan-storyboard-image-layer");
|
|
1842
|
+
slots.forEach(function (slot) {
|
|
1843
|
+
var value = (data.imagePrompts && data.imagePrompts[slot.key]) || "";
|
|
1844
|
+
var box = el("div", "plan-storyboard-image-slot" + (value.trim() ? " plan-storyboard-image-slot--filled" : ""));
|
|
1845
|
+
box.style.left = pctOf(slot.x);
|
|
1846
|
+
box.style.top = pctOf(slot.y);
|
|
1847
|
+
box.style.width = pctOf(slot.w);
|
|
1848
|
+
box.style.height = pctOf(slot.h);
|
|
1849
|
+
box.appendChild(el("span", "plan-storyboard-image-caption", "Image idea"));
|
|
1850
|
+
var input = document.createElement("textarea");
|
|
1851
|
+
input.className = "plan-storyboard-image-input";
|
|
1852
|
+
input.value = value;
|
|
1853
|
+
input.maxLength = PLAN.MAX_IMAGE_PROMPT_LENGTH;
|
|
1854
|
+
input.placeholder = "Describe the image for this slot — leave empty to let the AI decide";
|
|
1855
|
+
input.disabled = locked();
|
|
1856
|
+
input.setAttribute("aria-label", "Image idea " + slot.key);
|
|
1857
|
+
input.addEventListener("blur", function () {
|
|
1858
|
+
var trimmed = input.value.trim().slice(0, PLAN.MAX_IMAGE_PROMPT_LENGTH);
|
|
1859
|
+
if (trimmed === value.trim()) return;
|
|
1860
|
+
var next = {};
|
|
1861
|
+
for (var k in data.imagePrompts || {}) next[k] = data.imagePrompts[k];
|
|
1862
|
+
if (trimmed) next[slot.key] = trimmed;
|
|
1863
|
+
else delete next[slot.key];
|
|
1864
|
+
var rest = {};
|
|
1865
|
+
for (var k2 in data) if (k2 !== "imagePrompts") rest[k2] = data[k2];
|
|
1866
|
+
if (Object.keys(next).length) rest.imagePrompts = next;
|
|
1867
|
+
commitStoryboard(segment, rest);
|
|
1868
|
+
});
|
|
1869
|
+
box.appendChild(input);
|
|
1870
|
+
layer.appendChild(box);
|
|
1871
|
+
});
|
|
1872
|
+
return layer;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1555
1875
|
function renderCanvas(segment) {
|
|
1556
1876
|
var data = segment.data;
|
|
1557
1877
|
var figure = el("figure", "plan-storyboard");
|
|
1878
|
+
var selected = selectedPlanLayout(data);
|
|
1879
|
+
var chromeUrl = selected && selected.previewUrl ? selected.previewUrl : null;
|
|
1880
|
+
var zone = selected && selected.canvasBounds ? selected.canvasBounds : null;
|
|
1881
|
+
var areas = selected && selected.contentAreas && selected.contentAreas.length ? selected.contentAreas : zone ? [zone] : [];
|
|
1882
|
+
// A known layout WITHOUT a canvas has no plannable region — wash the whole
|
|
1883
|
+
// frame + notice instead of presenting the generic guides as its zone.
|
|
1884
|
+
var noCanvas = Boolean(selected) && !zone;
|
|
1885
|
+
var showBands = !selected && !chromeUrl;
|
|
1886
|
+
|
|
1887
|
+
var addBtn = null;
|
|
1888
|
+
if (!locked()) {
|
|
1889
|
+
addBtn = el("button", "plan-storyboard-add", ICONS.plus + "<span>Add container</span>");
|
|
1890
|
+
addBtn.type = "button";
|
|
1891
|
+
addBtn.title = "Add container";
|
|
1892
|
+
addBtn.setAttribute("aria-label", "Add container");
|
|
1893
|
+
addBtn.disabled = data.blocks.length >= PLAN.MAX_BLOCKS;
|
|
1894
|
+
}
|
|
1558
1895
|
|
|
1559
1896
|
var header = el("div", "plan-storyboard-header");
|
|
1560
1897
|
header.appendChild(el("span", null, "Slide " + data.slide + " body storyboard"));
|
|
1561
1898
|
var legend = el("span", "plan-storyboard-legend");
|
|
1562
1899
|
legend.appendChild(el("span", "plan-storyboard-legend-item plan-storyboard-legend-item--element", "Design element"));
|
|
1563
1900
|
legend.appendChild(el("span", "plan-storyboard-legend-item plan-storyboard-legend-item--content", "Content"));
|
|
1901
|
+
if (!showBands && addBtn) legend.appendChild(addBtn);
|
|
1564
1902
|
header.appendChild(legend);
|
|
1565
1903
|
figure.appendChild(header);
|
|
1566
1904
|
|
|
1567
1905
|
var layout = el("div", "plan-storyboard-layout");
|
|
1568
|
-
var frame = el(
|
|
1906
|
+
var frame = el(
|
|
1907
|
+
"div",
|
|
1908
|
+
chromeUrl
|
|
1909
|
+
? "plan-storyboard-frame plan-storyboard-frame--chrome slide-emboss"
|
|
1910
|
+
: "plan-storyboard-frame slide-emboss",
|
|
1911
|
+
);
|
|
1912
|
+
if (chromeUrl) {
|
|
1913
|
+
var chromeImg = document.createElement("img");
|
|
1914
|
+
chromeImg.className = "plan-storyboard-chrome";
|
|
1915
|
+
chromeImg.src = chromeUrl;
|
|
1916
|
+
chromeImg.alt = "";
|
|
1917
|
+
chromeImg.setAttribute("aria-hidden", "true");
|
|
1918
|
+
frame.appendChild(chromeImg);
|
|
1919
|
+
}
|
|
1920
|
+
// Abstract guides grid ONLY without a template: once the real layout
|
|
1921
|
+
// render is the ground, its own artwork is the guide.
|
|
1922
|
+
if (showBands) {
|
|
1923
|
+
var grid = el("div", "plan-storyboard-body");
|
|
1924
|
+
grid.setAttribute("aria-hidden", "true");
|
|
1925
|
+
frame.appendChild(grid);
|
|
1926
|
+
}
|
|
1927
|
+
// Placeholder outlines go UNDER the washes: template ground, muted by the
|
|
1928
|
+
// no-go wash instead of competing with the sketch as another line system.
|
|
1929
|
+
if (selected && Array.isArray(selected.placeholders) && selected.placeholders.length) {
|
|
1930
|
+
var phLayer = el("div", "plan-storyboard-zones");
|
|
1931
|
+
phLayer.setAttribute("aria-hidden", "true");
|
|
1932
|
+
selected.placeholders.forEach(function (ph) {
|
|
1933
|
+
var box = el("div", "plan-storyboard-ph");
|
|
1934
|
+
box.style.left = pctOf(ph.x);
|
|
1935
|
+
box.style.top = pctOf(ph.y);
|
|
1936
|
+
box.style.width = pctOf(ph.w);
|
|
1937
|
+
box.style.height = pctOf(ph.h);
|
|
1938
|
+
if (ph.label) {
|
|
1939
|
+
var chip = el("span", "plan-storyboard-ph-label");
|
|
1940
|
+
chip.textContent = ph.label;
|
|
1941
|
+
box.appendChild(chip);
|
|
1942
|
+
}
|
|
1943
|
+
phLayer.appendChild(box);
|
|
1944
|
+
});
|
|
1945
|
+
frame.appendChild(phLayer);
|
|
1946
|
+
}
|
|
1947
|
+
if (zone) frame.appendChild(renderZoneOverlay(zone, areas));
|
|
1569
1948
|
|
|
1570
1949
|
var top = el("div", "plan-storyboard-reserved plan-storyboard-reserved-top");
|
|
1571
1950
|
top.appendChild(document.createTextNode("Template header/title area not planned here"));
|
|
1572
|
-
if (
|
|
1573
|
-
var add =
|
|
1574
|
-
add.type = "button";
|
|
1575
|
-
add.title = "Add container";
|
|
1576
|
-
add.setAttribute("aria-label", "Add container");
|
|
1577
|
-
add.disabled = data.blocks.length >= PLAN.MAX_BLOCKS;
|
|
1951
|
+
if (addBtn) {
|
|
1952
|
+
var add = addBtn;
|
|
1578
1953
|
add.addEventListener("click", function () {
|
|
1579
1954
|
if (data.blocks.length >= PLAN.MAX_BLOCKS) return;
|
|
1580
1955
|
var serial = data.blocks.length + 1;
|
|
@@ -1601,21 +1976,34 @@
|
|
|
1601
1976
|
]);
|
|
1602
1977
|
commitStoryboard(segment, withBlocks(data, nextBlocks));
|
|
1603
1978
|
});
|
|
1604
|
-
top.appendChild(add);
|
|
1979
|
+
if (showBands) top.appendChild(add);
|
|
1605
1980
|
}
|
|
1606
|
-
frame.appendChild(top);
|
|
1981
|
+
if (showBands) frame.appendChild(top);
|
|
1607
1982
|
|
|
1608
|
-
var grid = el("div", "plan-storyboard-body");
|
|
1609
|
-
grid.setAttribute("aria-hidden", "true");
|
|
1610
|
-
frame.appendChild(grid);
|
|
1611
1983
|
|
|
1612
1984
|
var canRemove = !locked() && data.blocks.length > 1;
|
|
1613
1985
|
data.blocks.forEach(function (block) {
|
|
1614
1986
|
frame.appendChild(renderBlock(frame, block, data, segment, canRemove));
|
|
1615
1987
|
});
|
|
1616
1988
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1989
|
+
if (showBands) {
|
|
1990
|
+
var bottom = el("div", "plan-storyboard-reserved plan-storyboard-reserved-bottom", "Footer/template area not planned here");
|
|
1991
|
+
frame.appendChild(bottom);
|
|
1992
|
+
}
|
|
1993
|
+
var imageSlots = selected && Array.isArray(selected.placeholders)
|
|
1994
|
+
? selected.placeholders.filter(function (ph) { return typeof ph.key === "string"; })
|
|
1995
|
+
: [];
|
|
1996
|
+
if (imageSlots.length) frame.appendChild(renderImagePrompts(segment, data, imageSlots));
|
|
1997
|
+
if (noCanvas) {
|
|
1998
|
+
// Own top layer, click-through: the warning must never hide behind blocks.
|
|
1999
|
+
var noticeLayer = el("div", "plan-storyboard-zone-notice-layer");
|
|
2000
|
+
noticeLayer.setAttribute("aria-hidden", "true");
|
|
2001
|
+
var notice = el("span", "plan-storyboard-zone-notice");
|
|
2002
|
+
notice.textContent =
|
|
2003
|
+
"No content area on this layout — pick a layout with a content body";
|
|
2004
|
+
noticeLayer.appendChild(notice);
|
|
2005
|
+
frame.appendChild(noticeLayer);
|
|
2006
|
+
}
|
|
1619
2007
|
|
|
1620
2008
|
layout.appendChild(frame);
|
|
1621
2009
|
layout.appendChild(renderDetails(data, segment));
|
|
@@ -1662,7 +2050,7 @@
|
|
|
1662
2050
|
slideHeading: a.slideHeading,
|
|
1663
2051
|
text: text,
|
|
1664
2052
|
at: new Date().toISOString(),
|
|
1665
|
-
status: "
|
|
2053
|
+
status: "sent",
|
|
1666
2054
|
};
|
|
1667
2055
|
}
|
|
1668
2056
|
function anchorLabel(c) {
|
|
@@ -1674,7 +2062,7 @@
|
|
|
1674
2062
|
}
|
|
1675
2063
|
// The rail: every comment of this session, newest last, with its anchor and
|
|
1676
2064
|
// where it stands. This is what replaced the single "previous round" note —
|
|
1677
|
-
// it survives rounds and shows
|
|
2065
|
+
// it survives rounds and shows every comment the reviewer sent.
|
|
1678
2066
|
function ledgerRail() {
|
|
1679
2067
|
var rail = el("div", "sm-ledger");
|
|
1680
2068
|
ledger.forEach(function (c) {
|
|
@@ -1707,9 +2095,8 @@
|
|
|
1707
2095
|
note.innerHTML =
|
|
1708
2096
|
"<b>" +
|
|
1709
2097
|
(workingNote || "Comments sent.") +
|
|
1710
|
-
"</b> The agent is revising the plan — this tab
|
|
1711
|
-
"
|
|
1712
|
-
"queued and delivered with that round.";
|
|
2098
|
+
"</b> The agent is revising the plan — this tab refreshes when the next " +
|
|
2099
|
+
"round is ready. Keep reading and navigating; comment again on that round.";
|
|
1713
2100
|
status.appendChild(note);
|
|
1714
2101
|
composerEl.appendChild(status);
|
|
1715
2102
|
// The permission prompt was raised at the send click (a user gesture, the
|
|
@@ -1771,8 +2158,9 @@
|
|
|
1771
2158
|
commentEl = el("textarea", "sm-chat-ta");
|
|
1772
2159
|
commentEl.rows = 1;
|
|
1773
2160
|
commentEl.placeholder = working
|
|
1774
|
-
? "
|
|
2161
|
+
? "The agent is revising — comment on the next round."
|
|
1775
2162
|
: "What should change? Leave a note, or edit the plan directly.";
|
|
2163
|
+
commentEl.disabled = working;
|
|
1776
2164
|
commentEl.value = feedbackText;
|
|
1777
2165
|
commentEl.addEventListener("input", function () {
|
|
1778
2166
|
feedbackText = commentEl.value;
|
|
@@ -1795,8 +2183,9 @@
|
|
|
1795
2183
|
} else {
|
|
1796
2184
|
sendBtn = el("button", "sm-send", ICONS.send);
|
|
1797
2185
|
sendBtn.type = "button";
|
|
1798
|
-
sendBtn.title = working ? "
|
|
2186
|
+
sendBtn.title = working ? "The agent is revising — send on the next round" : "Send feedback";
|
|
1799
2187
|
sendBtn.setAttribute("aria-label", sendBtn.title);
|
|
2188
|
+
sendBtn.disabled = working;
|
|
1800
2189
|
}
|
|
1801
2190
|
sendBtn.onclick = requestChanges;
|
|
1802
2191
|
row.appendChild(sendBtn);
|
|
@@ -1811,7 +2200,7 @@
|
|
|
1811
2200
|
"<span>Approve plan</span></span>" +
|
|
1812
2201
|
'<span class="sub">the agent builds the slides next</span>';
|
|
1813
2202
|
approveBtn.onclick = function () {
|
|
1814
|
-
if (working
|
|
2203
|
+
if (working) return; // superseded round
|
|
1815
2204
|
if (S.storyDraft) return; // blocked while a storyline draft is staged
|
|
1816
2205
|
// Approving with unsent feedback in the input discards it — confirm inline.
|
|
1817
2206
|
if (commentText() && !approveArmed) {
|
|
@@ -1828,35 +2217,28 @@
|
|
|
1828
2217
|
}
|
|
1829
2218
|
|
|
1830
2219
|
// ---------- working mode (the agent is revising) ----------
|
|
1831
|
-
// The tab neither veils over nor freezes: the plan stays readable
|
|
1832
|
-
// stays navigable
|
|
1833
|
-
//
|
|
1834
|
-
//
|
|
2220
|
+
// The tab neither veils over nor freezes: the plan stays readable and the
|
|
2221
|
+
// strip stays navigable; the composer, the editing affordances and Approve go
|
|
2222
|
+
// away until the next round. Heartbeat polling waits for the agent's next
|
|
2223
|
+
// `slidemind plan` round, which reloads this same tab.
|
|
1835
2224
|
function enterWorking(note) {
|
|
1836
2225
|
working = true;
|
|
1837
2226
|
workingNote = note || null;
|
|
1838
2227
|
if (wsEl) wsEl.classList.add("is-waiting");
|
|
1839
2228
|
document.title = WAITING_TITLE; // the tab label signals the state from the tab bar
|
|
2229
|
+
saveCtx(); // a plain refresh of this round stays in working mode
|
|
1840
2230
|
renderSurface(); // rebuild the surface read-only (no contenteditable attrs)
|
|
1841
2231
|
renderComposer();
|
|
1842
2232
|
startNextRoundPolling();
|
|
1843
2233
|
}
|
|
1844
2234
|
|
|
1845
2235
|
// ---------- submit ----------
|
|
1846
|
-
// Send
|
|
1847
|
-
//
|
|
1848
|
-
// carrying any queued stragglers from a flush that couldn't reach the server.
|
|
2236
|
+
// Send: resolves the gate right now with the typed comment (plus a staged
|
|
2237
|
+
// storyline restructure as a whole-story instruction). Off while WORKING.
|
|
1849
2238
|
function requestChanges() {
|
|
2239
|
+
if (working) return;
|
|
1850
2240
|
var typed = commentText();
|
|
1851
|
-
|
|
1852
|
-
if (!typed) return;
|
|
1853
|
-
ledger.push(newComment(typed));
|
|
1854
|
-
saveLedger();
|
|
1855
|
-
clearDraft();
|
|
1856
|
-
renderComposer();
|
|
1857
|
-
return;
|
|
1858
|
-
}
|
|
1859
|
-
var batch = queuedComments();
|
|
2241
|
+
var batch = [];
|
|
1860
2242
|
if (S.storyDraft) {
|
|
1861
2243
|
var staged = PLAN.stageStoryIteration(S.value, S.storyDraft);
|
|
1862
2244
|
if (!staged) return;
|
|
@@ -1896,23 +2278,14 @@
|
|
|
1896
2278
|
})
|
|
1897
2279
|
.then(function (res) {
|
|
1898
2280
|
if (res.status === 409) {
|
|
1899
|
-
//
|
|
1900
|
-
//
|
|
1901
|
-
//
|
|
1902
|
-
//
|
|
1903
|
-
//
|
|
1904
|
-
//
|
|
1905
|
-
//
|
|
1906
|
-
|
|
1907
|
-
if (decision === "comments") {
|
|
1908
|
-
pendingComments.forEach(function (c) {
|
|
1909
|
-
c.status = "queued";
|
|
1910
|
-
if (ledger.indexOf(c) < 0) ledger.push(c);
|
|
1911
|
-
});
|
|
1912
|
-
saveLedger();
|
|
1913
|
-
clearDraft();
|
|
1914
|
-
S.storyDraft = null;
|
|
1915
|
-
}
|
|
2281
|
+
// Another tab already decided this run — this round is stale. The
|
|
2282
|
+
// typed text stays in the input (restored on the next round's reload)
|
|
2283
|
+
// so it isn't lost; disarm the submit latch — leaving it armed made
|
|
2284
|
+
// every later send a silent no-op. A staged story restructure is
|
|
2285
|
+
// dropped deliberately: its editedPlanMarkdown was computed against
|
|
2286
|
+
// the superseded plan and applying it to the next round would corrupt
|
|
2287
|
+
// it (same call the add-in dialog makes on round arrival).
|
|
2288
|
+
S.storyDraft = null;
|
|
1916
2289
|
submitted = false;
|
|
1917
2290
|
enterWorking("Round superseded.");
|
|
1918
2291
|
return;
|
|
@@ -1949,14 +2322,16 @@
|
|
|
1949
2322
|
}, 120);
|
|
1950
2323
|
}
|
|
1951
2324
|
// The shared heartbeat poll (gate-session.js) with this gate's two endings:
|
|
1952
|
-
// a retired tab
|
|
1953
|
-
// it starts with an empty sessionStorage, so anything still queued here would
|
|
1954
|
-
// otherwise be lost (the handover also drains the new run immediately).
|
|
2325
|
+
// a retired tab simply says where the review went.
|
|
1955
2326
|
function startNextRoundPolling() {
|
|
1956
2327
|
SES.startNextRoundPolling({
|
|
1957
2328
|
onEnded: showSessionEnded,
|
|
2329
|
+
// A retired tab still holding an unsent draft must not discard it: the
|
|
2330
|
+
// fresh window that replaced it has its own (empty) sessionStorage, so
|
|
2331
|
+
// this tab reloads into the new round instead and flags itself.
|
|
1958
2332
|
onSuperseded: function () {
|
|
1959
|
-
|
|
2333
|
+
if (commentText()) SES.markRoundReadyAndReload();
|
|
2334
|
+
else showSuperseded();
|
|
1960
2335
|
},
|
|
1961
2336
|
});
|
|
1962
2337
|
}
|
|
@@ -2028,8 +2403,8 @@
|
|
|
2028
2403
|
var stale = document.getElementById("plan-root");
|
|
2029
2404
|
if (stale && stale.parentNode) stale.parentNode.removeChild(stale);
|
|
2030
2405
|
|
|
2031
|
-
// Session first: it decides the MODE this page renders in (a
|
|
2032
|
-
//
|
|
2406
|
+
// Session first: it decides the MODE this page renders in (a refresh of an
|
|
2407
|
+
// already-sent round stays in working mode) and the round-transition state.
|
|
2033
2408
|
var flagged = SES.consumeRoundFlag();
|
|
2034
2409
|
restoreSession();
|
|
2035
2410
|
|
|
@@ -2038,14 +2413,10 @@
|
|
|
2038
2413
|
buildWorkspace();
|
|
2039
2414
|
|
|
2040
2415
|
if (working) {
|
|
2041
|
-
//
|
|
2042
|
-
// the reviewer already knows they're mid-round.
|
|
2416
|
+
// A refresh of a round the reviewer already sent: keep waiting quietly.
|
|
2043
2417
|
if (wsEl) wsEl.classList.add("is-waiting");
|
|
2044
2418
|
document.title = WAITING_TITLE;
|
|
2045
2419
|
startNextRoundPolling();
|
|
2046
|
-
flushBacklog().then(function () {
|
|
2047
|
-
renderComposer(); // rows flip queued → sent
|
|
2048
|
-
});
|
|
2049
2420
|
} else {
|
|
2050
2421
|
// Parked round: the agent is waiting on the reviewer — announce it.
|
|
2051
2422
|
SES.announceNewRound(flagged);
|