create-pmos 0.493.0 → 0.495.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/package.json +99 -27
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/briefing-premium-adddoc-test.js +2 -1
- package/workspace/briefing-premium-board-test.js +5 -4
- package/workspace/briefing-premium-initiative-test.js +4 -2
- package/workspace/changelog.json +98 -0
- package/workspace/guide/briefing-premium.md +4 -1
- package/workspace/guide/claude-proposes.md +2 -2
- package/workspace/guide/data-freshness.md +3 -1
- package/workspace/guide/initiatives-screen.md +3 -1
- package/workspace/guide/report-slides.md +14 -1
- package/workspace/guide/reports.md +2 -0
- package/workspace/guide/workflows.md +15 -3
- package/workspace/guide/your-impact.md +6 -7
- package/workspace/ini-workflow-test.js +2 -1
- package/workspace/lib/board-trail.js +30 -3
- package/workspace/lib/engine/workflows.js +19 -6
- package/workspace/lib/git-shape.js +21 -0
- package/workspace/lib/grill-runs.js +21 -6
- package/workspace/lib/grill.js +1 -1
- package/workspace/lib/impact.js +29 -16
- package/workspace/lib/initiative-cli.js +15 -1
- package/workspace/lib/open-mission.js +14 -0
- package/workspace/lib/page-warm.js +68 -0
- package/workspace/lib/report-design.js +6 -1
- package/workspace/lib/report-export.js +150 -9
- package/workspace/lib/report-pyramid.js +535 -0
- package/workspace/lib/report-structure.js +5 -1
- package/workspace/lib/report-templates.js +111 -20
- package/workspace/lib/routes-boards.js +36 -3
- package/workspace/lib/routes-brief-page.js +21 -3
- package/workspace/lib/routes-grill.js +10 -4
- package/workspace/lib/routes-impact.js +12 -4
- package/workspace/lib/routes-initiatives.js +1 -0
- package/workspace/lib/routes-programs.js +6 -4
- package/workspace/lib/routes-report-design.js +15 -0
- package/workspace/lib/routes-report-flow.js +115 -19
- package/workspace/lib/routes-report-setups.js +4 -2
- package/workspace/lib/routes-report-structure.js +3 -0
- package/workspace/lib/routes-workflows.js +6 -1
- package/workspace/lib/wf-bridge.js +5 -3
- package/workspace/lib/workflow-cli.js +2 -2
- package/workspace/lib/workflows-store.js +15 -1
- package/workspace/metric-drafts-ui-test.js +2 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-board-actions-test.js +2 -1
- package/workspace/premium-chat-report-tab-test.js +5 -1
- package/workspace/premium-vw-backlog-test.js +3 -2
- package/workspace/premium-vw-item-test.js +3 -2
- package/workspace/premium-vw-jira-test.js +3 -2
- package/workspace/program-panel-test.js +2 -1
- package/workspace/public/pm/cozy-home.css +1 -0
- package/workspace/public/pm/cozy-vw-rp.css +12 -0
- package/workspace/public/pm/fetch-share.js +38 -0
- package/workspace/public/pm/home-app.js +24 -4
- package/workspace/public/pm/home-chat.js +53 -11
- package/workspace/public/pm/home-impact.css +23 -13
- package/workspace/public/pm/home-impact.js +49 -32
- package/workspace/public/pm/home-ini.css +2 -0
- package/workspace/public/pm/ini-view.css +55 -0
- package/workspace/public/pm/ini-view.js +7 -4
- package/workspace/public/pm/premium-board.js +1 -0
- package/workspace/public/pm/premium-insights.js +1 -0
- package/workspace/public/pm/premium-report-read.js +25 -6
- package/workspace/public/pm/premium-report-tab.js +69 -14
- package/workspace/public/pm/premium-shell.js +57 -6
- package/workspace/public/pm/premium.css +1 -0
- package/workspace/public/pm/report-flow.js +93 -10
- package/workspace/public/pm/wf-page.css +8 -0
- package/workspace/public/pm/wf-page.js +33 -5
- package/workspace/server.js +24 -3
- package/workspace/skills/report-template.skill +10 -0
- package/workspace/split-merge-ui-test.js +3 -2
- package/workspace/stage-card-test.js +2 -1
- package/workspace/task-press-test.js +2 -1
- package/workspace/test-lists/page-open-speed.json +5 -0
- package/workspace/test-lists/report-pyramid.json +9 -0
- package/workspace/tk-tasks-test.js +2 -1
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
* Issue a confirm with the three checks of GET /api/viewer-report/checks, Issue it and Not yet. POST /api/report/issue freezes the cycle.
|
|
19
19
|
* Issued the lock line "Issued, so it stays as sent. Changes go into the next cycle." with Start the next cycle (RpStates).
|
|
20
20
|
* Discard a card at the foot of the panel: "Discard the Oct 12 draft?", the cycle that stays, "This cannot be undone." (RpStates).
|
|
21
|
-
* Slides the current slide, ‹ "Slide 1 of 5" ›, and a thumbnail of each slide.
|
|
21
|
+
* Slides the current slide, ‹ "Slide 1 of 5" ›, and a thumbnail of each slide. A cadence draft with no designed render shows the
|
|
22
|
+
* Pyramid deck (canvas RsA; GET /api/report-design/pyramid, lib/report-pyramid.js): answer first, the scorecard, the timeline,
|
|
23
|
+
* Needs your call and the sources. PDF prints the same slides, one to a page; the PowerPoint file draws them too.
|
|
22
24
|
* Document the words as paper pages: the eyebrow, the title, "In one minute", numbered sections, tables, the sources as footnotes,
|
|
23
25
|
* "Page n of N" and Download as PDF or Word (RpDoc; rp-parts.js paperModel). A designed cycle shows its designed page.
|
|
24
26
|
* Live page the numbers Insights reads (GET /api/insights-premium): the goal tiles, Goals on track, Shipped and At risk, the picked goal's
|
|
@@ -54,7 +56,7 @@ import * as MP from './premium-mtg-present.js'; // the one presenter window fo
|
|
|
54
56
|
import { presentInRecording } from './cozy-rec-bridge.js'; // while a meeting records, the recording tab presents it (rule 10)
|
|
55
57
|
import { placeholderHtml } from './cozy-placeholder.js';
|
|
56
58
|
import { vzStyle } from './premium-vw-open.js';
|
|
57
|
-
import { reviewHtml, ICON as RF_ICON } from './report-flow.js'; // Claude's review and the template icon (batch 22 item 14, canvas RfH2, RfH6)
|
|
59
|
+
import { reviewHtml, chatOf as rfChatOf, ICON as RF_ICON } from './report-flow.js'; // Claude's review and the template icon (batch 22 item 14, canvas RfH2, RfH6)
|
|
58
60
|
import { adler } from './cozy-adler.js'; // registers window.czViews; vzStyle adds the Viewer's sheet (cozy-vw.css)
|
|
59
61
|
import { cycleGroups, cycleName, briefModel, briefGlanceHtml, briefConfirmHtml, briefWhyHtml, briefLooksHtml, paperModel, paperHtml, liveModel, liveHtml, presentHtml, dayMon, reportSummary,
|
|
60
62
|
rpSumHtml, rpBarHtml, cadenceWords, goesOutWords, FORMATS, RP_ICONS } from './rp-parts.js';
|
|
@@ -170,6 +172,8 @@ const frame = (style, inner, cls, title) => `<iframe sandbox="" tabindex="-1" ti
|
|
|
170
172
|
/* ── what is shown ── */
|
|
171
173
|
function slides() {
|
|
172
174
|
if (R.cyc && R.cyc.slides.length) return R.cyc.slides.map((x) => ({ k: 'frame', h: x.h, html: x.html, css: R.cyc.style }));
|
|
175
|
+
// a cadence draft with no designed render: the finished Pyramid deck (canvas RsA), in the report's saved look or the standard PMOS look
|
|
176
|
+
if (R.pyr && R.pyr.slides.length) return R.pyr.slides.map((x) => ({ k: 'frame', h: x.h, html: x.html, css: R.pyr.style }));
|
|
173
177
|
const w = R.W || { title: R.name, sections: [] };
|
|
174
178
|
return [titleSlide(w)]
|
|
175
179
|
.concat(w.sections.map((x) => ({ k: 'text', h: x.h, html: slideHtml(x.lines) })));
|
|
@@ -493,6 +497,16 @@ async function rvAct(k, v) {
|
|
|
493
497
|
say((r.started ? 'The report\'s session started with ' : 'Passed to the report\'s session: ') + (todo.length === 1 ? 'one change' : todo.length + ' changes') + '. They show here as it writes them. Nothing is issued.');
|
|
494
498
|
}
|
|
495
499
|
function openTemplate() { import('./premium-report-tab.js').then((M) => M.openReportTab({ kind: 'report', id: R.id }, { panel: 'template' })); }
|
|
500
|
+
/** The chat icon (Wawan, 2026-11-02): the report's own conversation beside it. Home opens it in its chat (pmos-chat-about); another page goes
|
|
501
|
+
* to Home with the report open, and the conversation's context waits in sessionStorage for Home to read. */
|
|
502
|
+
async function openChat() {
|
|
503
|
+
const me = R; if (!me || !me.id) return;
|
|
504
|
+
const st = await pmGet('/api/report-flow?' + q({ ws: me.ws, id: me.id })).catch(() => null);
|
|
505
|
+
const t = rfChatOf({ name: me.name, ...(st && st.ok !== false ? st : {}), latest: (st && st.latest) || { path: me.row.path } }, me.id);
|
|
506
|
+
if (!dispatchEvent(new CustomEvent('pmos-chat-about', { detail: t, cancelable: true }))) return; // Home took it
|
|
507
|
+
try { sessionStorage.setItem('pmos-chat-about', JSON.stringify(t)); } catch { /* Home opens the chat with no context */ }
|
|
508
|
+
location.href = '/premium.html?' + q({ about: t.key, label: t.title, open: innerWidth > 760 ? 'report:' + me.id : '' }); // a phone: the chat takes the page
|
|
509
|
+
}
|
|
496
510
|
/** What Claude is still doing on this report, said where the PM reads it (Wawan, 2026-11-01, batch 22 item 13: "I have to wait, but there's
|
|
497
511
|
* no indication of me having to wait"). Nothing running → ''. */
|
|
498
512
|
export function waitWords(x, draft) {
|
|
@@ -910,6 +924,8 @@ function saveMd() {
|
|
|
910
924
|
function printHtml() {
|
|
911
925
|
const t = esc(R.name + ' ' + (R.row.slot || ''));
|
|
912
926
|
if (R.cyc) return frameDoc(R.cyc.style, R.view === 'deck' && R.cyc.slides.length ? R.cyc.slides.map((x) => x.html).join('') : R.cyc.body, R.view === 'deck' && R.cyc.slides.length ? 'pmos-print pmos-print-deck' : 'pmos-print pmos-print-doc').replace('<head>', '<head><title>' + t + '</title>');
|
|
927
|
+
// PDF = the same Pyramid slides, one to a 1280 x 720 page (canvas RsA, RsD)
|
|
928
|
+
if (R.pyr && R.pyr.slides.length && R.view === 'deck') return frameDoc(R.pyr.style, R.pyr.slides.map((x) => x.html).join(''), 'pmos-print pmos-print-deck').replace('<head>', '<head><title>' + t + '</title>');
|
|
913
929
|
const css = '<link rel="stylesheet" href="' + esc(href('premium-report-print.css')) + '">';
|
|
914
930
|
const w = R.W || { title: R.name, sections: [] };
|
|
915
931
|
const inner = R.view === 'deck'
|
|
@@ -930,7 +946,7 @@ function printIt() {
|
|
|
930
946
|
* its saved design is HTML the presenter cannot read yet, so it, and a presenter that does not start, use the full-screen view below. */
|
|
931
947
|
function presentIt() {
|
|
932
948
|
const me = R;
|
|
933
|
-
if (R.cyc || !R.W || R.err) return present(true);
|
|
949
|
+
if (R.cyc || (R.pyr && R.view === 'deck') || !R.W || R.err) return present(true);
|
|
934
950
|
if (presentInRecording({ kind: R.view === 'deck' ? 'pmos-deck' : 'report', ref: R.row.path, title: R.name, ws: R.ws })) return;
|
|
935
951
|
if (typeof MP.presentMaterial === 'function') {
|
|
936
952
|
const win = typeof MP.openAudience === 'function' ? MP.openAudience() : null;
|
|
@@ -971,7 +987,9 @@ async function readCycle() {
|
|
|
971
987
|
const c = await Promise.resolve(pmGet('/api/report-design/cycle?' + q({ ws: R.ws, path: R.row.path }))).catch(() => null);
|
|
972
988
|
if (R !== me) return;
|
|
973
989
|
R.cyc = null;
|
|
974
|
-
if (c && c.ok && c.designed) { try { const x = splitRender(await textOf('/api/report-design/cycle-html?' + q({ ws: R.ws, path: R.row.path }))); if (R === me) R.cyc = x; } catch { /* the words are shown */ } }
|
|
990
|
+
if (c && c.ok && c.designed) { try { const x = splitRender(await textOf('/api/report-design/cycle-html?' + q({ ws: R.ws, path: R.row.path }))); if (R === me) { R.cyc = x; R.pyr = null; } } catch { /* the words are shown */ } }
|
|
991
|
+
// not designed: the Pyramid deck of a cadence draft (404 for any other report, which keeps the title slide and a slide per section)
|
|
992
|
+
else { let x = null; try { x = splitRender(await textOf('/api/report-design/pyramid?' + q({ ws: R.ws, path: R.row.path }))); } catch { x = null; } if (R === me) R.pyr = x && x.slides.length ? x : null; }
|
|
975
993
|
}
|
|
976
994
|
const viewUrl = (row, ws) => '/premium-viewer.html?' + q({ open: 'report', path: row.path, ws });
|
|
977
995
|
|
|
@@ -985,11 +1003,12 @@ export async function openReport(row, hand = {}) {
|
|
|
985
1003
|
vzStyle(); rpStyle();
|
|
986
1004
|
if (R) present(false);
|
|
987
1005
|
const me = R = { row, ws: hand.ws || '', id: c.cadence, name: hand.name || row.name || c.cadence || 'Report', audience: hand.audience || '', issued: row.state === 'issued', archived: !!row.archived,
|
|
988
|
-
text: '', err: '', W: null, view: 'deck', BF: null, bfRaw: null, bfMsg: '', LV: null, blank: false, i: 0, cyc: null, setup: undefined, pop: null, conf: false, checks: null, issueErr: '', busy: false, ask: '', askErr: '', edit: false, msg: null,
|
|
1006
|
+
text: '', err: '', W: null, view: 'deck', BF: null, bfRaw: null, bfMsg: '', LV: null, blank: false, i: 0, cyc: null, pyr: null, setup: undefined, pop: null, conf: false, checks: null, issueErr: '', busy: false, ask: '', askErr: '', edit: false, msg: null,
|
|
989
1007
|
sch: schedPick(null), schErr: '', shr: null, rv: null, rvHide: new Set(), rvSent: new Set(), shares: null, more: [], hand, cuts: hand.cuts ? { on: 'team', team: null, sponsors: null, err: '' } : null, cycles: (hand.cycles || []).slice(), onIssue: hand.onIssue || null, onChange: hand.onChange || null, vid: 'report:' + row.path };
|
|
990
1008
|
const head = me.head = () => ({ id: me.vid, kind: 'Report' + (me.audience ? ' · ' + me.audience : ''), title: me.name, icon: me.view === 'doc' ? 'doc' : 'report', full: true, url: viewUrl(row, me.ws), more: me.more,
|
|
991
1009
|
// the template behind one icon (batch 22 item 14, canvas RfH1): format, readers, when it goes out, and its history
|
|
992
|
-
presses: me.setup ? [{ act: 'template', label: 'Template: format, readers, look, sources', icon: RF_ICON.tune }] : [],
|
|
1010
|
+
presses: me.setup ? [{ act: 'chat', label: 'Talk to Claude about this report', icon: RF_ICON.chat }, { act: 'template', label: 'Template: format, readers, look, sources', icon: RF_ICON.tune }] : [],
|
|
1011
|
+
onPress: (a) => { if (R !== me) return; if (a === 'template') openTemplate(); else if (a === 'chat') openChat(); },
|
|
993
1012
|
render: (b) => { syncMore(); b.innerHTML = panelHtml(); wire(b); }, onClose: () => { if (R === me && !me.reopen) { present(false); R = null; } } });
|
|
994
1013
|
vOpen(head());
|
|
995
1014
|
try { R.text = await textOf('/api/doc?' + q({ ws: R.ws, path: row.path })); R.W = wordsOf(R.text, R.name); if (!hand.name && R.W.title) R.name = R.W.title; } catch (x) { R.err = String((x && x.message) || x); R.W = { title: R.name, sections: [] }; }
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
* A CHANGE Claude passes from the chat (block.change) goes to the report's own session (POST /api/report-flow/ask), then the report opens.
|
|
24
24
|
* THE TEMPLATE (panel 'template', the header icon): the same questions on the current version, "changed" marks, Template history with Go back
|
|
25
25
|
* (POST /api/report-flow/restore), Keep the current template and Make a different template (POST /api/report-flow/template). Its sections and
|
|
26
|
-
* look
|
|
26
|
+
* look show as two read-only rows (canvas TplA): a sections change the PM named in the chat (block.sections) or a newer look made in Design
|
|
27
|
+
* is ONE ✦ "Make T<n+1>" (✕ drops it, ✓ makes it, with the settings, in one press). Editing the sections and look, the cycles and Delete
|
|
28
|
+
* template stay one press further (panel 'manage', the view described above).
|
|
27
29
|
*
|
|
28
30
|
* Every write is a press here, behind the launch nonce (http.js pmPost). Claude writes nothing: the routes refuse an agent session. A render is
|
|
29
31
|
* HTML the server sanitized, shown in a frame with an empty sandbox, so nothing in it runs. Nothing is sent outward. */
|
|
@@ -79,16 +81,37 @@ export function openReportTab(block, o = {}) {
|
|
|
79
81
|
if (!t && block.proposal) { setTimeout(() => adoptSetUp(id, block.proposal.name), 0); }
|
|
80
82
|
if (!t || JSON.stringify(t.block) !== JSON.stringify(block) || (o.panel && o.panel !== t.panel)) {
|
|
81
83
|
t = { id, block, nv: 'deck', sched: block.proposal ? (block.proposal.schedule || 'off') : null, open: false, view: 'structure', busy: false, msg: '', setup: null, docs: [], S: null, Z: null, sv: null, dv: null, render: new Map(), gone: false, loaded: false, i: 0, ask: null,
|
|
82
|
-
q: block.proposal ? RF.picksOf(block.proposal) : null, panel: block.proposal ? 'new' : o.panel || 'current', chat: o.chat || (t && t.chat) || null, F: null, tq: null, passed: t ? t.passed : false };
|
|
84
|
+
q: block.proposal ? RF.picksOf(block.proposal) : null, panel: block.proposal ? 'new' : o.panel || (tplAsk(block) ? 'template' : 'current'), chat: o.chat || (t && t.chat) || null, F: null, tq: null, passed: t ? t.passed : false };
|
|
83
85
|
T.set(id, t);
|
|
84
86
|
if (block.id && t.panel === 'manage') load(t);
|
|
85
87
|
} else if (o.chat) t.chat = o.chat;
|
|
86
88
|
// a change passes on the PM's press of the chip only; an answer that opens the view by itself shows the report as it is
|
|
87
89
|
if (block.id && block.change && !t.passed && !o.auto && !PASSED.has(JSON.stringify(block))) { PASSED.add(JSON.stringify(block)); t.passed = true; passChange(t); return show(t); }
|
|
88
90
|
if (block.id && t.panel === 'current') return current(t);
|
|
89
|
-
if (block.id && t.panel === 'template') { const v = show(t); loadFlow(t).then(() => redraw(t)); return v; }
|
|
91
|
+
if (block.id && t.panel === 'template') { const v = show(t); loadFlow(t).then(() => { redraw(t); if (!o.auto) makeLook(t); }); return v; }
|
|
90
92
|
return show(t);
|
|
91
93
|
}
|
|
94
|
+
/** A chat change to a set-up report's sections or look opens its template (canvas TplA), where it waits as one ✦. */
|
|
95
|
+
const tplAsk = (b) => !!(b && b.id && !b.change && ((Array.isArray(b.sections) && b.sections.length) || (typeof b.design === 'string' && b.design.trim())));
|
|
96
|
+
/** The look the PM asked for in words is made first (a design job, as Design's turn), once per chat change, on the PM's press of the chip; when
|
|
97
|
+
* it lands it is the newer look, and the ✦ offers it with the sections. Nothing is saved before ✓. */
|
|
98
|
+
async function makeLook(t) {
|
|
99
|
+
const words = typeof t.block.design === 'string' ? t.block.design.trim() : '';
|
|
100
|
+
const key = 'look:' + JSON.stringify(t.block);
|
|
101
|
+
if (!words || PASSED.has(key) || !t.F || !t.F.ok) return;
|
|
102
|
+
PASSED.add(key);
|
|
103
|
+
const had = t.F.look && t.F.look.newer ? t.F.look.newer.n : null;
|
|
104
|
+
const r = await post('/api/report-design/turn', { id: t.block.id, text: words, points: [] });
|
|
105
|
+
if (!r || !r.ok) { t.msg = 'The look was not started: ' + why(r, 'the page could not reach PMOS') + '.'; redraw(t); return; }
|
|
106
|
+
t.making = words; redraw(t);
|
|
107
|
+
const end = Date.now() + POLL_MAX;
|
|
108
|
+
while (Date.now() < end && T.get(t.id) === t) {
|
|
109
|
+
await new Promise((ok) => setTimeout(ok, POLL_MS * 2));
|
|
110
|
+
const F = await get('/api/report-flow', { id: t.block.id });
|
|
111
|
+
if (F && F.ok && F.look && F.look.newer && F.look.newer.n !== had) { t.F = Object.assign({}, t.F, { look: F.look }); t.making = null; t.tplDrop = false; redraw(t); return; }
|
|
112
|
+
}
|
|
113
|
+
t.making = null; t.msg = 'The look is not ready yet. It shows here when you open the template again.'; redraw(t);
|
|
114
|
+
}
|
|
92
115
|
/** The report's state from GET /api/report-flow; for the template, the answers start from its current version. */
|
|
93
116
|
async function loadFlow(t) {
|
|
94
117
|
const F = await get('/api/report-flow', { id: t.block.id });
|
|
@@ -122,11 +145,19 @@ async function waitFor(t) {
|
|
|
122
145
|
}
|
|
123
146
|
t.waiting = false;
|
|
124
147
|
}
|
|
125
|
-
/** A change Claude passed from the chat: to the report's own session, then the report opens (RfH2).
|
|
148
|
+
/** A change Claude passed from the chat: to the report's own session, on the latest draft by its file, then the report opens (RfH2).
|
|
149
|
+
* An issued latest cycle is frozen: the change is not passed, and the PM is told to change the template instead. */
|
|
126
150
|
async function passChange(t) {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
151
|
+
const F = await loadFlow(t);
|
|
152
|
+
const to = F && F.ok ? RF.changeTargetOf(F) : {};
|
|
153
|
+
let said;
|
|
154
|
+
if (to.frozen) said = `The ${to.frozen} report is issued, so it is frozen. The change was not passed on: change the template for the next cycle.`;
|
|
155
|
+
else {
|
|
156
|
+
const r = await post('/api/report-flow/ask', { id: t.block.id, text: t.block.change, ...(to.path ? { path: to.path } : {}) });
|
|
157
|
+
const which = to.slot ? `the ${to.slot} draft` : 'the draft';
|
|
158
|
+
said = r && r.ok ? (r.started ? `The report's session started with your change to ${which}. It shows in the report as it writes it. Nothing is issued.` : `Passed to the report's session for ${which}. It shows in the report as it writes it. Nothing is issued.`)
|
|
159
|
+
: 'The change was not passed on: ' + why(r, 'the page could not reach PMOS') + '.';
|
|
160
|
+
}
|
|
130
161
|
t.block = { kind: 'report', id: t.block.id }; t.passed = false;
|
|
131
162
|
return current(t, said);
|
|
132
163
|
}
|
|
@@ -239,19 +270,38 @@ function currentHtml(t) {
|
|
|
239
270
|
function templateHtml(t) {
|
|
240
271
|
if (!t.F) return '<p class="vz-empty" role="status">Reading the template.</p>';
|
|
241
272
|
if (t.F.err) return `<p class="vz-msg" data-tone="bad">${esc(t.F.err)}</p>`;
|
|
242
|
-
|
|
243
|
-
const tpl = { base: t.base, current: T0.current, versions: T0.versions || [], diff: RF.diffOf(t.tq, t.base) };
|
|
244
|
-
return RF.questionsHtml(t.tq, { template: tpl, busy: t.busy, msg: t.msg })
|
|
273
|
+
return RF.questionsHtml(t.tq, { template: tplOf(t), name: t.F.name, making: t.making, busy: t.busy, msg: t.msg })
|
|
245
274
|
+ `<p class="vz-note">Its sections and look, and its cycles: <button type="button" class="rp-linkbtn vz-hv" data-rt="manage">Sections, look and cycles</button></p>`;
|
|
246
275
|
}
|
|
247
276
|
const qOf = (t) => (t.block.proposal ? t.q : t.tq);
|
|
277
|
+
/** The sections a list of titles becomes when saved (lib/report-structure.js planFromSections): Sources last, and one named section alone gets
|
|
278
|
+
* "Needs your call" after it. */
|
|
279
|
+
export function savedTitles(L) {
|
|
280
|
+
const seen = new Set(), list = (L || []).map((x) => String(x || '').replace(/\s+/g, ' ').trim()).filter((x) => x && !seen.has(x.toLowerCase()) && seen.add(x.toLowerCase()));
|
|
281
|
+
if (!list.length) return [];
|
|
282
|
+
const si = list.findIndex((x) => /^sources?\b/i.test(x) || /\bsources?$/i.test(x));
|
|
283
|
+
const src = si >= 0 ? list.splice(si, 1)[0] : 'Sources';
|
|
284
|
+
if (list.length === 1) list.push('Needs your call');
|
|
285
|
+
return list.concat([src]);
|
|
286
|
+
}
|
|
287
|
+
/** The template as the panel draws it (canvas TplA): its versions, its sections and look, and the change waiting for ✓: the sections the PM
|
|
288
|
+
* named in the chat (block.sections) when they differ from the saved ones, and a newer look made in Design and not saved yet. */
|
|
289
|
+
function tplOf(t) {
|
|
290
|
+
const F = t.F || {}, T0 = F.template || {};
|
|
291
|
+
const S = F.sections || { saved: null, titles: [] }, L = F.look || null;
|
|
292
|
+
const named = Array.isArray(t.block.sections) && t.block.sections.length ? savedTitles(t.block.sections) : null;
|
|
293
|
+
const sNext = named && named.map((x) => x.toLowerCase()).join('\n') !== (S.titles || []).map((x) => String(x).toLowerCase()).join('\n') ? named : null;
|
|
294
|
+
return { base: t.base, current: T0.current, versions: T0.versions || [], diff: RF.diffOf(t.tq, t.base), dropped: !!t.tplDrop,
|
|
295
|
+
sections: { saved: S.saved, titles: S.titles || [], next: sNext },
|
|
296
|
+
look: L ? { saved: L.saved, words: L.words, follows: L.follows || [], next: L.newer || null } : null };
|
|
297
|
+
}
|
|
248
298
|
/** Readers, typed: kept as typed; Draft it and Make a different template wait for a name. */
|
|
249
299
|
function typed(t, e) {
|
|
250
300
|
const k = e.target && e.target.dataset ? e.target.dataset.rt : '';
|
|
251
301
|
const q = qOf(t); if (k !== 'aud' || !q) return;
|
|
252
302
|
q.audience = e.target.value;
|
|
253
303
|
const b = paneOf(t) && paneOf(t).querySelector(t.block.proposal ? '[data-rt="draftit"]' : '[data-rt="maketpl"]');
|
|
254
|
-
if (b) b.disabled = t.busy || !q.audience.trim() || (!t.block.proposal && !RF.
|
|
304
|
+
if (b) b.disabled = t.busy || !q.audience.trim() || (!t.block.proposal && !RF.templateChange(tplOf(t), q).length);
|
|
255
305
|
}
|
|
256
306
|
/** A pick in Goes out (how often, the day, the time). The template redraws, so its "changed" marks follow. */
|
|
257
307
|
function picked(t, e) {
|
|
@@ -344,12 +394,17 @@ async function press(t, e) {
|
|
|
344
394
|
if (a === 'draftit') { await setUp(t); return; }
|
|
345
395
|
if (a === 'keep') { await current(t); return; }
|
|
346
396
|
if (a === 'manage') { t.panel = 'manage'; t.loaded = false; show(t); await load(t); return; }
|
|
397
|
+
if (a === 'tpl-no') { t.tplDrop = true; t.msg = 'Dropped. Nothing is saved; the template stays T' + ((t.F && t.F.template && t.F.template.current) || 1) + '.'; redraw(t); return; }
|
|
347
398
|
if (a === 'maketpl') {
|
|
399
|
+
const tp = tplOf(t), body = { id: t.block.id, settings: RF.settingsOfPicks(t.tq) };
|
|
400
|
+
if (!tp.dropped && tp.sections.next) body.sections = tp.sections.next;
|
|
401
|
+
if (!tp.dropped && tp.look && tp.look.next) body.design = tp.look.next.n;
|
|
348
402
|
await act(t, async () => {
|
|
349
|
-
const r = await post('/api/report-flow/template',
|
|
403
|
+
const r = await post('/api/report-flow/template', body);
|
|
350
404
|
if (!r || !r.ok) return 'The template did not change: ' + why(r, 'the page could not reach PMOS') + '.';
|
|
405
|
+
if (body.sections) t.block = Object.assign({}, t.block, { sections: undefined, structure: undefined });
|
|
351
406
|
if (r.state) t.F = r.state; await loadFlow(t);
|
|
352
|
-
return 'T' + r.n + ' is the current template. The next
|
|
407
|
+
return 'T' + r.n + ' is the current template. The next cycle follows it; the open draft stays as it is, and the earlier cycles keep their look.';
|
|
353
408
|
});
|
|
354
409
|
return;
|
|
355
410
|
}
|
|
@@ -358,7 +413,7 @@ async function press(t, e) {
|
|
|
358
413
|
const r = await post('/api/report-flow/restore', { id: t.block.id, n: +v });
|
|
359
414
|
if (!r || !r.ok) return 'It did not go back: ' + why(r, 'the page could not reach PMOS') + '.';
|
|
360
415
|
await loadFlow(t);
|
|
361
|
-
return 'Back to T' + r.n + '. It is the current template again. Nothing is deleted.';
|
|
416
|
+
return 'Back to T' + r.n + (r.restored ? ': ' + r.restored : '') + '. It is the current template again. Nothing is deleted.';
|
|
362
417
|
});
|
|
363
418
|
return;
|
|
364
419
|
}
|
|
@@ -538,14 +538,50 @@ $('#dBody').addEventListener('click', async (e) => {
|
|
|
538
538
|
const b = e.target.closest('[data-lead="refresh"]'); if (!b || !S.key || !S.key.startsWith('pop:')) return;
|
|
539
539
|
b.disabled = true; await sendText(S.key, leadAsk(S.key));
|
|
540
540
|
});
|
|
541
|
+
/* A sent message shows at once (batch 24 item 2: "enter the prompt is so long to appear on the chat"): its turn draws the moment the PM presses
|
|
542
|
+
* Enter, lighter until PMOS takes it. It leaves when the transcript holds the PM's turn (the same words, or any PM turn from that moment on),
|
|
543
|
+
* and after 3 minutes at most. A message that was not sent leaves at once and its words go back in the box. Keyed by row key, or 'sid:' + id. */
|
|
544
|
+
const PEND = { key: null, list: [], th: null }, PEND_MAX = 180000;
|
|
545
|
+
const pendOf = (key) => { if (PEND.key !== key) { PEND.key = key; PEND.list = []; PEND.th = null; } return PEND.list; };
|
|
546
|
+
function pendAdd(key, text, files) {
|
|
547
|
+
const conv = key.startsWith('sid:') ? PEND.sidConv : S.conv;
|
|
548
|
+
const p = { text, files: files || [], at: new Date().toISOString(), sent: false, n: ((conv && conv.messages) || []).filter((m) => m.role === 'user').length };
|
|
549
|
+
pendOf(key).push(p); return p;
|
|
550
|
+
}
|
|
551
|
+
function pendHtml(key, conv) {
|
|
552
|
+
const list = pendOf(key); if (!list.length) return '';
|
|
553
|
+
const k = (t) => splitFiles(shownUser(t)).text.replace(/\s+/g, ' ').trim();
|
|
554
|
+
const users = ((conv && conv.messages) || []).filter((m) => m.role === 'user');
|
|
555
|
+
const lastUser = users.reduce((n, m) => Math.max(n, Date.parse(m.at) || 0), 0), now = Date.now();
|
|
556
|
+
PEND.list = list.filter((p) => {
|
|
557
|
+
if (!p.sent) return true;
|
|
558
|
+
const t0 = Date.parse(p.at);
|
|
559
|
+
return now - t0 <= PEND_MAX && !(lastUser && lastUser >= t0 - 2000) && !users.slice(p.n).some((m) => k(m.text) === k(p.text));
|
|
560
|
+
});
|
|
561
|
+
return PEND.list.map((p) => `<div class="turn u pend${p.sent ? '' : ' sending'}"><div class="ub">${filesHtml(p.files)}${p.text ? userHtml(p.text) : ''}</div></div>`).join('');
|
|
562
|
+
}
|
|
563
|
+
/** Draws the waiting turns now, without a read: the thread as last read plus the new turn, and the stream scrolled to its end. */
|
|
564
|
+
function pendDraw(key) {
|
|
565
|
+
if (key.startsWith('sid:')) { if (S.sid !== key.slice(4)) return; }
|
|
566
|
+
else if (S.key !== key) return;
|
|
567
|
+
const box = $('#dBody .stream'); if (!box) return;
|
|
568
|
+
if (!key.startsWith('sid:') && PEND.th) drawThread(PEND.th, key, S.conv);
|
|
569
|
+
else {
|
|
570
|
+
let w = $('#pendW', box); if (!w) { box.insertAdjacentHTML('beforeend', '<div id="pendW"></div>'); w = $('#pendW', box); }
|
|
571
|
+
putHtml(w, pendHtml(key, key.startsWith('sid:') ? PEND.sidConv : S.conv));
|
|
572
|
+
}
|
|
573
|
+
const b = $('#dBody'); b.scrollTop = b.scrollHeight;
|
|
574
|
+
}
|
|
541
575
|
export function drawThread(th, key, conv) {
|
|
542
576
|
if (S.key !== key) return;
|
|
577
|
+
pendOf(key); PEND.th = th;
|
|
543
578
|
const box = $('#dBody .stream'); if (!box) return;
|
|
544
579
|
drawLead(box, th);
|
|
545
580
|
let t = $('#thr', box); if (!t) { box.insertAdjacentHTML('beforeend', '<div id="thr"></div>'); t = $('#thr', box); }
|
|
546
581
|
const b = $('#dBody'), atEnd = b.scrollHeight - b.scrollTop - b.clientHeight < 80;
|
|
547
582
|
S.conv = conv;
|
|
548
|
-
|
|
583
|
+
const pw = $('#pendW', box); if (pw) pw.remove(); // a first message drew its turn before the thread was read
|
|
584
|
+
putHtml(t, threadHtml(th) + sessionHtml(conv, th.goal, { answer: true }) + pendHtml(key, conv));
|
|
549
585
|
ctxAuto(key, conv);
|
|
550
586
|
S.goal = th.goal || null; S.draft = readyDraft(conv); S.session = th.session || null;
|
|
551
587
|
syncTerm(termTarget({ live: th.session, command: th.resume, ws: new URLSearchParams(location.search).get('ws') }));
|
|
@@ -773,15 +809,29 @@ $('#dBody').addEventListener('submit', async (e) => {
|
|
|
773
809
|
const inp = $('#askIn'), text = inp.value.trim(), key = S.key;
|
|
774
810
|
if (!key && S.sid) { // premium-session-list: a session the list opened that has no row of its own
|
|
775
811
|
if (!text) { $('#askNote').textContent = 'Write a message for Claude.'; return; }
|
|
776
|
-
const id = S.sid,
|
|
777
|
-
|
|
778
|
-
|
|
812
|
+
const id = S.sid, pk = 'sid:' + id, p = pendAdd(pk, text);
|
|
813
|
+
inp.value = ''; drawSlash(); growAsk(); pendDraw(pk); // the turn shows at once (batch 24 item 2)
|
|
814
|
+
const r = await pmPost('/api/session/say', { id, text }).catch(() => null);
|
|
815
|
+
if (!r || !r.ok) {
|
|
816
|
+
PEND.list = pendOf(pk).filter((x) => x !== p); pendDraw(pk);
|
|
817
|
+
if (S.sid === id && !inp.value) { inp.value = text; growAsk(); }
|
|
818
|
+
$('#askNote').textContent = (r && r.error) || 'The message was not sent.'; return;
|
|
819
|
+
}
|
|
820
|
+
p.sent = true; pendDraw(pk);
|
|
821
|
+
$('#askNote').textContent = 'Sent.'; setTimeout(() => loadSid(id), 400); return;
|
|
779
822
|
}
|
|
780
823
|
if (!key) { $('#askNote').textContent = 'This row has no conversation, so the message was not sent.'; return; }
|
|
781
824
|
const files = attachNames();
|
|
782
825
|
if (files === null) { $('#askNote').textContent = 'A file is still saving. Send when its chip shows its size.'; return; }
|
|
783
826
|
if (!text && !files.length) { $('#askNote').textContent = 'Write a message for Claude.'; return; }
|
|
784
|
-
|
|
827
|
+
// the turn shows and the box empties at once (batch 24 item 2); a send that fails takes the turn away and puts the words back
|
|
828
|
+
const p = pendAdd(key, text, files);
|
|
829
|
+
inp.value = ''; drawSlash(); growAsk(); pendDraw(key);
|
|
830
|
+
if (await sendText(key, text, files)) { p.sent = true; inp.placeholder = 'Message Claude, or type /'; clearAttach(); pendDraw(key); }
|
|
831
|
+
else {
|
|
832
|
+
PEND.list = pendOf(key).filter((x) => x !== p); pendDraw(key);
|
|
833
|
+
if (S.key === key && !inp.value) { inp.value = text; growAsk(); }
|
|
834
|
+
}
|
|
785
835
|
});
|
|
786
836
|
|
|
787
837
|
/* ── on scroll: a card's head sticks while its card is on screen. The old hero no longer folds into a dock (premium-agreed-design). ── */
|
|
@@ -1424,7 +1474,8 @@ async function loadSid(id) {
|
|
|
1424
1474
|
syncTerm(termTarget({ live: c && c.ok && c.state !== 'ended' ? id : null }));
|
|
1425
1475
|
const box = $('#dBody .stream'); if (!box) return;
|
|
1426
1476
|
const b = $('#dBody'), atEnd = b.scrollHeight - b.scrollTop - b.clientHeight < 80;
|
|
1427
|
-
|
|
1477
|
+
if (c && c.ok) PEND.sidConv = c;
|
|
1478
|
+
putHtml(box, c && c.ok ? sessionHtml(c) + pendHtml('sid:' + id, c) : turnA(ptx('The conversation of this session could not be read.')));
|
|
1428
1479
|
$('#askNote').textContent = c && c.ask ? 'Claude is waiting for your answer above.' : 'Your message goes to this session.';
|
|
1429
1480
|
if (atEnd) b.scrollTop = b.scrollHeight;
|
|
1430
1481
|
if (c && c.ok && c.state !== 'ended' && $('#dlg').classList.contains('on')) R.sidTimer = setTimeout(() => loadSid(id), 2000);
|
|
@@ -285,6 +285,7 @@ a.round{color:inherit;text-decoration:none}
|
|
|
285
285
|
.tc{min-width:0}
|
|
286
286
|
.tt{margin:0;font-size:var(--fs-3);line-height:1.55;color:var(--ink);max-width:58ch}
|
|
287
287
|
.turn.u{justify-items:end}
|
|
288
|
+
.turn.u.pend.sending .ub{opacity:.72} /* batch 24 item 2: a sent turn shows at once, a touch lighter until PMOS takes it */
|
|
288
289
|
.ub{background:var(--solid);color:var(--on-solid);padding:11px 16px;border-radius:18px 18px 8px 18px;font-size:var(--fs-3);line-height:1.45;max-width:86%}
|
|
289
290
|
.acard{margin-left:40px;background:var(--well);border-radius:var(--r-in);padding:20px 22px;display:grid;gap:14px;animation:rise .6s var(--ease) both}
|
|
290
291
|
.still .turn,.still .acard{animation:none} /* a redraw of a live chat keeps its turns still (premium-shell.js putHtml) */
|
|
@@ -7,9 +7,14 @@
|
|
|
7
7
|
* changedOf(q, base) → ['output' | 'audience' | 'schedule'] what differs from the current template
|
|
8
8
|
* diffOf(q, base) → ['Format: Slides → Document', …] the same, in words
|
|
9
9
|
* questionsHtml(q, o) the questions (RfH4, RfH7): Format, Readers, Goes out, Look, Sources, then Draft it.
|
|
10
|
-
* o.template draws them as the template (RfH3):
|
|
10
|
+
* o.template draws them as the template (RfH3, TplA): Format, Readers, Goes out, Sections (chips, in
|
|
11
|
+
* order, read only) and Look (a small title slide and one line); "changed" marks; ONE ✦ "Make T<n+1>" when
|
|
12
|
+
* the sections or the look change (✕ drops it, ✓ makes it); the history; Keep the current template and
|
|
11
13
|
* Make a different template
|
|
12
|
-
*
|
|
14
|
+
* templateChange(T, q) → [words] everything a press of ✓ would change: the settings, the sections, the look
|
|
15
|
+
* changeTargetOf(F) → { path, slot } | { frozen: slot } | {} where a change from the chat goes: the latest draft; an issued
|
|
16
|
+
* latest cycle is frozen; no cycle yet → {} (the first draft takes it)
|
|
17
|
+
* historyHtml(versions) Template history: T<n>, what it is in words, when, the cycles issued with it, what Go back to T<n> restores
|
|
13
18
|
* waitHtml(F) the report's current state while no cycle is written yet: drafting, ended, or no draft (RfH1)
|
|
14
19
|
* reviewHtml(rv, o) Claude's review of a draft in the proposal pattern (RfH2): one line, a ✦ row per fix with Not now and Do it,
|
|
15
20
|
* Do all, and "N other checks found nothing"
|
|
@@ -19,7 +24,24 @@
|
|
|
19
24
|
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
20
25
|
const SV = (d, n = 16) => `<svg viewBox="0 0 24 24" width="${n}" height="${n}" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${d}"/></svg>`;
|
|
21
26
|
export const ICON = { go: 'M5 12h14M13 6l6 6-6 6', x: 'M6 6l12 12M18 6L6 18', check: 'M5 12.5l4.5 4.5L19 7.5', back: 'M9 14 4 9l5-5M4 9h11a5 5 0 0 1 0 10h-3',
|
|
22
|
-
tune: 'M4 7h10M18 7h2M4 17h2M10 17h10M14 5v4M8 15v4' };
|
|
27
|
+
tune: 'M4 7h10M18 7h2M4 17h2M10 17h10M14 5v4M8 15v4', chat: 'M5 6.5A2.5 2.5 0 0 1 7.5 4h9A2.5 2.5 0 0 1 19 6.5v7a2.5 2.5 0 0 1-2.5 2.5H11l-4 3.5V16h.5A2.5 2.5 0 0 1 5 13.5z' };
|
|
28
|
+
|
|
29
|
+
/** The chat a report's header icon opens beside it (Wawan, 2026-11-02: "How do I chat to continue adjusting the template?"): the report's own
|
|
30
|
+
* conversation, pop:report-<id>. A pop: key carries what it is about as its opening context (lib/briefing-premium-agent.js popRow), so Claude knows
|
|
31
|
+
* the report and its draft, and a change comes back as the change block the chip passes to the report's own session. */
|
|
32
|
+
export function chatOf(st = {}, id = '') {
|
|
33
|
+
const sid = String(id).replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[^A-Za-z0-9]+/, '').slice(0, 90) || 'report';
|
|
34
|
+
const name = String(st.name || 'this report'), draft = st.latest && st.latest.path ? String(st.latest.path) : '';
|
|
35
|
+
const settings = [st.output && (FORMAT_NAMES[st.output] || [st.output])[0], st.audience].filter(Boolean).join(' · ');
|
|
36
|
+
return { key: 'pop:report-' + sid, title: name,
|
|
37
|
+
intro: 'Tell me what to change in ' + name + '. I pass it to the report\'s own session, and the draft beside us changes. Nothing is issued.',
|
|
38
|
+
placeholder: 'Change the report',
|
|
39
|
+
context: { title: name, subject: 'A report set up in PMOS (report id ' + id + ')' + (settings ? ': ' + settings : ''),
|
|
40
|
+
say: 'When the PM asks to change the draft, answer in a few lines and end with ONE fenced code block with the language tag pmos-report holding {"kind":"report","id":"' + id
|
|
41
|
+
+ '","change":"the change, in the PM\'s words"}. The PM presses its chip to pass it to the report\'s own session. Do not change the draft yourself. The format, readers, schedule, look and sources change with the template icon in the report\'s header.',
|
|
42
|
+
facts: [['Report id', id]].concat(draft ? [['Latest draft', draft]] : []), files: draft ? [draft] : [] } };
|
|
43
|
+
}
|
|
44
|
+
|
|
23
45
|
export const FORMAT_NAMES = { 'google-slides': ['Slides', 'Read aloud or presented.'], 'google-doc': ['Document', 'Read alone, with tables and footnotes.'],
|
|
24
46
|
pdf: ['PDF', 'A fixed page to read or print.'], 'google-sheets': ['Sheets', 'The numbers as a table.'], confluence: ['Confluence page', 'A page in your wiki.'] };
|
|
25
47
|
export const CADENCES = [['weekly', 'Every week'], ['bi-weekly', 'Every two weeks'], ['monthly', 'Every month'], ['daily', 'Every day']];
|
|
@@ -27,6 +49,11 @@ const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
|
|
|
27
49
|
const TIMES = ['07:00', '08:00', '09:00', '10:00', '12:00', '14:00', '16:00', '17:00'];
|
|
28
50
|
|
|
29
51
|
/** The answers a new report starts with: Claude's picks. */
|
|
52
|
+
export function changeTargetOf(F) {
|
|
53
|
+
const L = F && F.latest;
|
|
54
|
+
if (!L || !L.path) return {};
|
|
55
|
+
return L.state === 'issued' ? { frozen: L.slot } : { path: L.path, slot: L.slot };
|
|
56
|
+
}
|
|
30
57
|
export function picksOf(p = {}) {
|
|
31
58
|
const s = p.schedule && typeof p.schedule === 'object' && p.schedule.cadence ? p.schedule : null;
|
|
32
59
|
return { output: p.output || 'google-slides', audience: p.audience || '', repeat: !!s, cadence: s ? s.cadence : 'weekly', day: (s && s.day) || 'Mon', dayOfMonth: (s && s.dayOfMonth) || 1, time: (s && s.time) || '09:00',
|
|
@@ -100,6 +127,55 @@ function lookHtml(o) {
|
|
|
100
127
|
const words = o.look ? `<p class="vz-note">Your words: “${esc(o.look)}”. Claude makes this look after the first draft; until then it uses the standard look.</p>` : '';
|
|
101
128
|
return `<fieldset class="rf-q"><legend>Look</legend><p class="rf-fix"><b>Standard PMOS look</b><span>Nunito, navy title slides, 16:9</span></p>${words}</fieldset>`;
|
|
102
129
|
}
|
|
130
|
+
/* ── the template's sections and look (canvas TplA) ── */
|
|
131
|
+
/** Section chips in order. was: the saved titles; a title new in `now` is marked new, a title gone is struck through. */
|
|
132
|
+
function chipsHtml(now, was) {
|
|
133
|
+
const low = (L) => (L || []).map((x) => String(x).toLowerCase());
|
|
134
|
+
const w = low(was), n = low(now);
|
|
135
|
+
const on = (now || []).map((t, i) => `<span class="rf-cp"${was && !w.includes(n[i]) ? ' data-k="new"' : ''}>${esc(t)}${was && !w.includes(n[i]) ? '<span class="rf-sr"> (new)</span>' : ''}</span>`);
|
|
136
|
+
const gone = was ? (was || []).filter((t, i) => !n.includes(w[i])).map((t) => `<span class="rf-cp" data-k="gone"><s>${esc(t)}</s><span class="rf-sr"> (taken out)</span></span>`) : [];
|
|
137
|
+
return `<div class="rf-cps">${on.concat(gone).join('')}</div>`;
|
|
138
|
+
}
|
|
139
|
+
function tplSectionsHtml(T) {
|
|
140
|
+
const S = T.sections; if (!S) return '';
|
|
141
|
+
const next = S.next && !T.dropped ? S.next : null;
|
|
142
|
+
return `<fieldset class="rf-q"><legend>Sections${next ? ' ' + tag('changed', 'ch') : ''}</legend>${chipsHtml(next || S.titles || [], next ? S.titles || [] : null)}</fieldset>`;
|
|
143
|
+
}
|
|
144
|
+
function tplLookHtml(T, name, o) {
|
|
145
|
+
const L = T.look; if (!L) return '';
|
|
146
|
+
const next = L.next && !T.dropped ? L.next : null;
|
|
147
|
+
const words = next ? next.words : L.words;
|
|
148
|
+
const std = next ? next.n == null : L.saved == null;
|
|
149
|
+
const fol = !std && (L.follows || []).length ? `<small>Follows ${esc(L.follows.join(', '))}</small>` : std ? '<small>Nunito, navy title slides, 16:9</small>' : '';
|
|
150
|
+
const making = o && o.making ? `<p class="vz-note" role="status">Claude is making the look you asked for: “${esc(o.making)}”. It shows here to approve when it is ready.</p>` : '';
|
|
151
|
+
return `<fieldset class="rf-q"><legend>Look${next ? ' ' + tag('changed', 'ch') : ''}</legend><div class="rf-lk"><span class="rf-mini" data-k="${std ? 'std' : 'own'}" aria-hidden="true"><b>${esc(name || '')}</b></span>`
|
|
152
|
+
+ `<p class="rf-fix"><b>${esc(words || 'Standard PMOS look')}</b>${fol}</p></div>${making}</fieldset>`;
|
|
153
|
+
}
|
|
154
|
+
/** Everything ✓ would change, in words: the settings (diff), then the sections and the look. */
|
|
155
|
+
export function templateChange(T, q) {
|
|
156
|
+
if (!T) return [];
|
|
157
|
+
const out = (q ? changedOf(q, T.base).length : 0) ? (T.diff || []).slice() : [];
|
|
158
|
+
if (T.dropped) return out;
|
|
159
|
+
if (T.sections && T.sections.next) out.push('Sections: ' + (sectionsDiff(T.sections.titles, T.sections.next) || 'the same'));
|
|
160
|
+
if (T.look && T.look.next) out.push('Look: ' + (T.look.words || 'Standard PMOS look') + ' → ' + (T.look.next.words || 'Standard PMOS look'));
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/** "+ Customers, − Risk watch", or "new order" (the same words lib/report-templates.js sectionsDiff says). */
|
|
164
|
+
export function sectionsDiff(was, now) {
|
|
165
|
+
const A = was || [], B = now || [], a = A.map((x) => String(x).toLowerCase()), b = B.map((x) => String(x).toLowerCase());
|
|
166
|
+
const out = B.filter((t, i) => !a.includes(b[i])).map((t) => '+ ' + t).concat(A.filter((t, i) => !b.includes(a[i])).map((t) => '− ' + t));
|
|
167
|
+
return out.length ? out.join(', ') : a.join('\n') !== b.join('\n') ? 'new order' : '';
|
|
168
|
+
}
|
|
169
|
+
/** The ✦ that makes T<n+1> (TplA 2, 3): the change in words, "T<n> keeps its draft and cycles", ✕ drops it, ✓ makes it. Only for a sections
|
|
170
|
+
* or look change: a format, readers or schedule change the PM made here is said in the note under the questions, as before. */
|
|
171
|
+
function proposalHtml(T, words, a, o) {
|
|
172
|
+
if (T.dropped || !((T.sections && T.sections.next) || (T.look && T.look.next))) return '';
|
|
173
|
+
const n = (T.versions || []).reduce((m, v) => Math.max(m, v.n), 0) + 1;
|
|
174
|
+
return `<div class="rf-fx rf-tplx"><span class="rf-star" aria-hidden="true">✦</span><span class="rf-fxt"><b>Make T${n}</b>${words.map((w) => `<span>${esc(w)}</span>`).join('')}`
|
|
175
|
+
+ `<span>T${esc(T.current || 1)} keeps its draft and cycles.</span></span>`
|
|
176
|
+
+ `<button type="button" class="rf-ib vz-hv" ${a}="tpl-no" aria-label="Not now" title="Not now">${SV(ICON.x)}</button>`
|
|
177
|
+
+ `<button type="button" class="rf-ib pri vz-hv" ${a}="maketpl" aria-label="Make T${n}" title="Make T${n}"${o.busy ? ' disabled' : ''}>${SV(ICON.check)}</button></div>`;
|
|
178
|
+
}
|
|
103
179
|
function sourcesHtml(o) {
|
|
104
180
|
const L = (o.sources || []).filter(Boolean);
|
|
105
181
|
if (!L.length) return '';
|
|
@@ -110,9 +186,11 @@ export function historyHtml(versions = [], a = 'data-rt', o = {}) {
|
|
|
110
186
|
if (!versions.length) return '';
|
|
111
187
|
const day = (iso) => { const d = new Date(iso || ''); return Number.isFinite(d.getTime()) ? d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) : ''; };
|
|
112
188
|
const pend = o.pending ? `<li class="rf-hv" data-k="new"><b>T${(versions[0] ? versions[0].n : 0) + 1}</b><span>${esc(o.pending)}<em>Unsaved · waits for Make a different template</em></span></li>` : '';
|
|
113
|
-
|
|
189
|
+
// each version says what it is; one that is not current also says what Go back restores (TplA 4): format, readers, schedule, sections, look
|
|
190
|
+
const rows = versions.map((v) => `<li class="rf-hv"${v.current ? ' aria-current="true"' : ''}><b>T${v.n}</b><span>${esc(v.why || v.words || '')}<em>${esc([day(v.at), v.current ? 'current' : '', v.cycles ? v.cycles + (v.cycles === 1 ? ' cycle issued' : ' cycles issued') : ''].filter(Boolean).join(' · '))}</em>`
|
|
191
|
+
+ (!v.current && v.words ? `<em class="rf-rest">Going back restores: ${esc(v.words)}</em>` : '') + '</span>'
|
|
114
192
|
+ (v.current ? '' : `<button type="button" class="rf-ib vz-hv" ${a}="restore|${v.n}" aria-label="Go back to T${v.n}" title="Go back to T${v.n}"${o.busy ? ' disabled' : ''}>${SV(ICON.back)}</button>`) + '</li>').join('');
|
|
115
|
-
return `<section class="rf-hist"><h3>Template history</h3><ol>${pend}${rows}</ol><p class="vz-note">Going back makes that version the current one. Nothing is deleted.</p></section>`;
|
|
193
|
+
return `<section class="rf-hist"><h3>Template history</h3><ol>${pend}${rows}</ol><p class="vz-note">Going back makes that version the current one. Nothing is deleted. Cycles already made keep the look they were made with; the open draft stays as it is.</p></section>`;
|
|
116
194
|
}
|
|
117
195
|
/**
|
|
118
196
|
* The questions. q = picksOf(…) as the PM changed it. o = { template?: { base, versions, diff }, look, sources, busy, msg }.
|
|
@@ -120,16 +198,21 @@ export function historyHtml(versions = [], a = 'data-rt', o = {}) {
|
|
|
120
198
|
*/
|
|
121
199
|
export function questionsHtml(q, o = {}, a = 'data-rt') {
|
|
122
200
|
const T = o.template, ch = T ? changedOf(q, T.base) : [];
|
|
123
|
-
const
|
|
201
|
+
const all = T ? templateChange(T, q) : [];
|
|
202
|
+
const nextN = T ? (T.versions || []).reduce((m, v) => Math.max(m, v.n), 0) + 1 : 0;
|
|
203
|
+
const st = T ? (all.length ? (ch.length ? 'Template · changed, not saved yet' : 'Template · T' + (T.current || 1) + ', T' + nextN + ' waiting') : 'Template · T' + (T.current || 1)) : 'Waits on your answers';
|
|
124
204
|
const head = `<p class="rf-st"><i class="rf-dot" data-k="you" aria-hidden="true"></i>${esc(st)}</p>`;
|
|
125
|
-
const
|
|
205
|
+
const tpl = T && (T.sections || T.look);
|
|
206
|
+
const body = formatHtml(q, ch, a) + readersHtml(q, ch, a, o) + goesHtml(q, ch, a) + (tpl ? tplSectionsHtml(T) + tplLookHtml(T, o.name, o) : lookHtml(o)) + (T ? '' : sourcesHtml(o));
|
|
126
207
|
const msg = o.msg ? `<p class="vz-msg" role="status">${esc(o.msg)}</p>` : '';
|
|
127
208
|
if (!T) {
|
|
128
209
|
return `<div class="rf-qs">${head}${body}${msg}<div class="rf-foot"><span class="vz-note">Nothing goes out</span><button type="button" class="rf-ib pri vz-hv" ${a}="draftit" aria-label="Draft it" title="Draft it"${o.busy || !q.audience.trim() ? ' disabled' : ''}>${SV(ICON.go, 18)}</button></div></div>`;
|
|
129
210
|
}
|
|
130
|
-
const words =
|
|
131
|
-
const
|
|
132
|
-
|
|
211
|
+
const words = all;
|
|
212
|
+
const fx = proposalHtml(T, words, a, o);
|
|
213
|
+
const note = words.length && !fx ? `<p class="rf-chg">${tag('changed', 'ch')} ${esc(words.join('; '))}. This makes a different template. The current one keeps its draft and its cycles.</p>` : '';
|
|
214
|
+
const ask = tpl && !fx ? `<p class="vz-note">To change the look or the sections, ask in the chat. Claude proposes; you approve.</p>` : '';
|
|
215
|
+
return `<div class="rf-qs">${head}${body}${fx}${note}${ask}${historyHtml(T.versions, a, { pending: words.length ? words.join('; ') : '', busy: o.busy })}${msg}`
|
|
133
216
|
+ `<div class="rf-foot"><button type="button" class="rf-ib vz-hv" ${a}="keep" aria-label="Keep the current template" title="Keep the current template">${SV(ICON.back)}</button>`
|
|
134
217
|
+ `<button type="button" class="rf-ib pri vz-hv" ${a}="maketpl" aria-label="Make a different template" title="Make a different template"${o.busy || !words.length || !q.audience.trim() ? ' disabled' : ''}>${SV(ICON.check, 18)}</button></div></div>`;
|
|
135
218
|
}
|
|
@@ -23,6 +23,14 @@ body.wf-page { min-height: 100vh; overflow: hidden; font-family: var(--cz-sans);
|
|
|
23
23
|
.wfl .t { font-size: var(--fs-3); font-weight: 700; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
24
24
|
.wfl .n { font-size: var(--fs-2); color: var(--cz-ink3); font-weight: 600; }
|
|
25
25
|
.wfl.arch .t { color: var(--cz-ink3); }
|
|
26
|
+
.wfl .wfb { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; padding: 0; border: 0; background: none; color: inherit; font: inherit; cursor: pointer; text-align: left; }
|
|
27
|
+
.wfl .wfb:focus-visible, .wfl .wfs:focus-visible { outline: 2px solid var(--cz-blue); outline-offset: 2px; border-radius: 8px; }
|
|
28
|
+
/* the star (batch 24 item 3, canvas DfA): filled on the default; an outline only on hover or focus elsewhere, and always on touch */
|
|
29
|
+
.wfl .wfs { width: 30px; height: 30px; margin: -4px 0; border-radius: 999px; display: inline-grid; place-items: center; border: 0; padding: 0; background: none; color: var(--cz-ink3); cursor: pointer; flex: none; opacity: 0; }
|
|
30
|
+
.wfl:hover .wfs, .wfl:focus-within .wfs, .wfl .wfs.on { opacity: 1; }
|
|
31
|
+
.wfl .wfs:hover { background: var(--cz-card); color: var(--cz-ink2); box-shadow: 0 0 0 1px var(--cz-hair); }
|
|
32
|
+
.wfl .wfs.on, .wfl .wfs.on:hover { color: var(--cz-risk); }
|
|
33
|
+
@media (hover: none) { .wfl .wfs { opacity: 1; } }
|
|
26
34
|
.wf-sub { font-size: var(--fs-1); font-weight: 800; color: var(--cz-ink3); padding: 14px 10px 4px; }
|
|
27
35
|
.wf-page .wfi { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 6px; background: var(--cz-well); color: var(--cz-ink2); flex: none; }
|
|
28
36
|
.wfl .wfi, .wf-head .wfi { background: var(--cz-well2); }
|