create-pmos 0.527.0 → 0.529.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 +57 -29
- 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/adler-moments-test.js +4 -3
- package/workspace/briefing-premium-calendar-3-test.js +11 -9
- package/workspace/briefing-premium-calendar-test.js +1 -1
- package/workspace/changelog.json +56 -0
- package/workspace/ev-everywhere-test.js +2 -6
- package/workspace/fixtures/premium-viewer-fixtures.js +4 -2
- package/workspace/guide/home.md +7 -6
- package/workspace/guide/idle-lab.md +1 -1
- package/workspace/guide/notes.md +19 -7
- package/workspace/guide/reports-set-up.md +4 -1
- package/workspace/lib/bg-queue.js +10 -3
- package/workspace/lib/meeting-replay.js +11 -2
- package/workspace/lib/meeting-suggest.js +105 -0
- package/workspace/lib/meeting-terms.js +57 -17
- package/workspace/lib/okr-goals.js +23 -3
- package/workspace/lib/report-chat.js +3 -3
- package/workspace/lib/routes-lab.js +44 -2
- package/workspace/lib/routes-meeting-points.js +27 -3
- package/workspace/lib/routes-morning-goals.js +5 -1
- package/workspace/lib/routes-report-flow.js +35 -5
- package/workspace/news-viewers-test.js +2 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-vw-meet-test.js +17 -11
- package/workspace/product-lane-test.js +1 -1
- package/workspace/program-charter-test.js +2 -2
- package/workspace/public/pm/bd-ini-panel.js +1 -1
- package/workspace/public/pm/bd-span.js +10 -7
- package/workspace/public/pm/cozy-board.css +1 -0
- package/workspace/public/pm/cozy-lab-ins.js +13 -4
- package/workspace/public/pm/cozy-search.js +10 -17
- package/workspace/public/pm/cozy-settings.js +8 -2
- package/workspace/public/pm/cozy-today.css +30 -14
- package/workspace/public/pm/cozy-vw-mtg.css +122 -24
- package/workspace/public/pm/good-morning.js +30 -17
- package/workspace/public/pm/home-app.js +11 -1
- package/workspace/public/pm/home-data.js +4 -1
- package/workspace/public/pm/home-day.js +12 -5
- package/workspace/public/pm/premium-board.js +7 -5
- package/workspace/public/pm/premium-initiative-head.js +9 -1
- package/workspace/public/pm/premium-insights.js +5 -3
- package/workspace/public/pm/premium-meeting.js +2 -1
- package/workspace/public/pm/premium-mtg-replay.js +149 -73
- package/workspace/public/pm/premium-report-read.js +11 -4
- package/workspace/public/pm/premium-report-tab.js +33 -4
- package/workspace/public/pm/premium-vw-mtg.js +176 -53
- package/workspace/public/pm/program-model.js +2 -1
- package/workspace/public/pm/report-chat.js +1 -0
- package/workspace/public/pm/report-flow.js +7 -3
- package/workspace/public/premium-board.html +4 -4
- package/workspace/server.js +1 -1
- package/workspace/task-milestones-test.js +2 -2
- package/workspace/timeline-holds-test.js +3 -3
|
@@ -317,7 +317,8 @@ export function replayModel(rep) {
|
|
|
317
317
|
const stretches = (Array.isArray(rep.runs) ? rep.runs : []).filter((x) => Number.isFinite(x.from_ms) && Number.isFinite(x.to_ms) && x.to_ms > x.from_ms).map((x) => { const t = topics.find((y) => y.id === x.topic); return { n: x.n || (t && t.n) || 0, text: t ? t.text : '', from: Math.max(0, x.from_ms), to: Math.min(len, x.to_ms) }; }).filter((x) => x.to > x.from);
|
|
318
318
|
const keys = (Array.isArray(rep.keys) ? rep.keys : []).filter((k) => k && ['decision', 'action', 'question'].includes(k.k)).map((k, i) => ({ id: k.id || 'k' + i, k: k.k, text: clip(k.text, 240), t: Number.isFinite(k.t) ? k.t : null, owner: clip(k.owner, 60), due: k.due || '', by: clip(k.by, 60), state: k.state || '',
|
|
319
319
|
need: !!k.warn || k.state === 'owner' || k.state === 'open' }));
|
|
320
|
-
const shots = (Array.isArray(rep.shots) ? rep.shots : []).filter((x) => Number.isFinite(x.t)).map((x) => ({ t: x.t, why: clip(x.why, 120)
|
|
320
|
+
const shots = (Array.isArray(rep.shots) ? rep.shots : []).filter((x) => Number.isFinite(x.t)).map((x) => ({ t: x.t, why: clip(x.why, 120), w: Number(x.w) || 0, h: Number(x.h) || 0,
|
|
321
|
+
...(x.jev && (x.jev.kind === 'content' || x.jev.kind === 'faces') ? { jev: { kind: x.jev.kind, ...(Array.isArray(x.jev.box) && x.jev.box.length === 4 ? { box: x.jev.box.map(Number) } : {}) } } : {}) })).sort((a, b) => a.t - b.t);
|
|
321
322
|
/* PMOS's notes (the meeting companion's, from the Focus file), the gaps where the recording was off, and the line each key point was caught from */
|
|
322
323
|
const pmos = (Array.isArray(rep.notes) ? rep.notes : []).filter((x) => x && Number.isFinite(x.t) && String(x.body || '').trim()).map((x) => ({ t: Math.max(0, Math.min(len, x.t)), cat: clip(x.cat, 30), body: clip(x.body, 4000) })).sort((a, b) => a.t - b.t);
|
|
323
324
|
const gaps = (Array.isArray(rep.gaps) ? rep.gaps : []).filter((g) => Number.isFinite(g.from_ms) && Number.isFinite(g.to_ms) && g.to_ms > g.from_ms).map((g) => ({ from: g.from_ms, to: Math.min(len, g.to_ms) }));
|
|
@@ -6,19 +6,27 @@
|
|
|
6
6
|
* are one row, the To do under it with Make task. One ✓ per row confirms it. No checkbox, no filter, no bulk bar.
|
|
7
7
|
* Under the points, ONE strip: Left out, Next steps, Since the last one, Facts, Names and terms, Prep and material,
|
|
8
8
|
* Transcript (premium-vw-mtg.js fills the ones it owns). One opens at a time.
|
|
9
|
-
* RIGHT the
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
9
|
+
* RIGHT the player (batch 27 item 12, "Meeting player": MpD1, MpD2, MpD4; adopted from music and podcast players). The meeting is
|
|
10
|
+
* audio plus screen captures, so the capture at the playhead is the artwork, with whose screen and when, and an icon that
|
|
11
|
+
* shows the whole frame. Under it the chapter's name and "chapter N of M"; ONE thin bar where the chapters are gaps (no text
|
|
12
|
+
* on the bar) and each kept point is its kind's icon inside its chapter; hovering the bar shows the capture there, the
|
|
13
|
+
* chapter and the time. Elapsed left, total right. One centred row: ‹ back 15 s, play, forward 15 s ›; the speed chip
|
|
14
|
+
* (1×, 1.5×, 2×) left; a captures icon (the row of captures, hidden by default) and a chapters icon (the list: name, its
|
|
15
|
+
* kept points, its start; press one to play from there) right. Under it the caption: the line now, larger, with its
|
|
16
|
+
* speaker, the lines before and after faded, as a music player shows lyrics. The old bottom dock is gone.
|
|
17
|
+
* JEV CROP (MpJ) when Jev for images read a capture, the artwork and the captures row show only the box around the slide or
|
|
18
|
+
* document, and a capture of faces only is left out (the card keeps the last slide). Every crop keeps the whole-frame icon:
|
|
19
|
+
* Jev's own confidence cannot flag a bad box. A capture Jev did not read shows whole, as before.
|
|
20
|
+
* WORD FIX (MvF, batch 27 word fix A: MpW1, MpW2) a word heard wrong is fixed in place: the PM selects it or double-clicks it in any
|
|
21
|
+
* text of the notes; a popup offers the ✦ spelling when one is found, the places it changes, ✓. It shows fixed in every
|
|
22
|
+
* place this note shows it. Remembering it for later notes waits for the ✦ ask in Names and terms. Undo forgets it.
|
|
23
|
+
* lib/meeting-terms.js keeps it; the note's own files are not rewritten.
|
|
24
|
+
* Phone (MpD3): the player without the captures icon and the chapter steps, then the kept points.
|
|
17
25
|
*
|
|
18
26
|
* DATA (nothing is invented): rep = premium-meeting.js replayModel(GET /api/meeting-replay) with rep.raw = the answer's keys (for the owner,
|
|
19
27
|
* due and state); n = the note (GET /api/notes/one) for the remarks of the live transcript; points, edits, lessons, terms = GET /api/meeting-points.
|
|
20
28
|
* replayView(rep, { n, points, edits, lessons, terms, now }) → the view model
|
|
21
|
-
* replayHtml(V, ui) → the desk and the phone. ui = { pos, playing, speed, pt (the selected point), strip,
|
|
29
|
+
* replayHtml(V, ui) → the desk and the phone. ui = { pos, playing, speed, pt (the selected point), strip, caps, chaps, allShots, edit, draft } */
|
|
22
30
|
|
|
23
31
|
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
24
32
|
const clip = (s, n) => { const t = String(s == null ? '' : s).replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
|
|
@@ -101,7 +109,10 @@ export function replayView(rep, { n = null, points = {}, edits = {}, lessons = [
|
|
|
101
109
|
const tot = rep.stretches.reduce((s, x) => s + (x.to - x.from), 0) || 1;
|
|
102
110
|
const strip = rep.stretches.map((x, i) => ({ from: x.from, to: x.to, w: Math.max(1, x.to - x.from) / tot, c: i % 2, text: clip(x.text || 'Topic ' + (i + 1), 40), min: Math.max(1, Math.round((x.to - x.from) / 60000)) }));
|
|
103
111
|
const whoAt = (t) => { let w = ''; for (const l of rep.lines) { if (l.t <= t + NEAR) w = l.who; else break; } return w; };
|
|
104
|
-
|
|
112
|
+
/* a capture Jev called faces only is left out; one it boxed shows only the box (MpJ). Jev not asked: the capture shows whole, as before */
|
|
113
|
+
const shots = rep.shots.map((s, i) => ({ i, t: s.t, time: mmss(s.t), why: clip(s.why, 80), who: whoAt(s.t), w: s.w || 0, h: s.h || 0,
|
|
114
|
+
faces: !!(s.jev && s.jev.kind === 'faces'), box: s.jev && Array.isArray(s.jev.box) && s.jev.box.length === 4 ? s.jev.box : null }));
|
|
115
|
+
const caps = shots.filter((x) => !x.faces);
|
|
105
116
|
/* a decision and the To do that follows it are one row: the To do said within FOLLOW_MS after it, in the same topic */
|
|
106
117
|
const topOf = (t) => strip.find((x) => t >= x.from && t < x.to) || null;
|
|
107
118
|
const rows = [], under = new Map();
|
|
@@ -115,14 +126,16 @@ export function replayView(rep, { n = null, points = {}, edits = {}, lessons = [
|
|
|
115
126
|
const noteId = rep.noteId || (n && n.id) || '';
|
|
116
127
|
return {
|
|
117
128
|
rows: rows.map((r) => ({ ...r, follow: under.get(r.id) || null })), W: wordsOf(terms, noteId),
|
|
118
|
-
len: rep.len, lenText: mmss(rep.len), noteId, audio: rep.audio, strip, shots,
|
|
129
|
+
len: rep.len, lenText: mmss(rep.len), noteId, audio: rep.audio, strip, shots, caps,
|
|
130
|
+
lines: (rep.lines || []).filter((l) => l && Number.isFinite(l.t) && l.text).map((l) => ({ t: l.t, who: clip(l.who, 30), text: clip(l.text, 400) })),
|
|
119
131
|
points: kept, left, raised, toConfirm: kept.filter((x) => !x.verdict).length,
|
|
120
132
|
seg: [['all', 'All', kept.length], ['decision', 'Decided', count('decision')], ['action', 'To do', count('action')], ['question', 'Open', count('question')]],
|
|
121
133
|
keptLine: raised > kept.length ? 'Kept ' + kept.length + ' of ' + raised + ' raised: ' + KEPT_WHY : kept.length ? 'Kept ' + kept.length + ': ' + KEPT_WHY : 'Nothing was agreed, owned or left open',
|
|
122
134
|
leftLine: left.length ? 'Left out · ' + left.length + (left.length === 1 ? ' remark' : ' remarks') : '',
|
|
123
135
|
leftEg: left.length ? 'e.g. “' + clip(left[0].text, 50) + '”, ' + left[0].why : '',
|
|
124
136
|
stretchAt: (pos) => strip.find((x) => pos >= x.from && pos < x.to) || strip[strip.length - 1] || null,
|
|
125
|
-
shotAt: (pos) => { let k = -1; shots.forEach((s, i) => { if (s.t <= pos) k = i; }); return k < 0 &&
|
|
137
|
+
shotAt: (pos) => { let k = -1; shots.forEach((s, i) => { if (s.t <= pos && !s.faces) k = i; }); return k < 0 && caps.length ? caps[0].i : k; },
|
|
138
|
+
chapterAt: (pos) => { const i = strip.findIndex((x) => pos >= x.from && pos < x.to); return i >= 0 ? i : strip.length ? (pos >= strip[strip.length - 1].from ? strip.length - 1 : 0) : -1; },
|
|
126
139
|
};
|
|
127
140
|
}
|
|
128
141
|
|
|
@@ -132,7 +145,7 @@ export function wordsOf(terms, noteId) {
|
|
|
132
145
|
const all = (Array.isArray(terms) ? terms : []).filter((x) => x && x.heard && x.means);
|
|
133
146
|
const fix = all.filter((x) => x.note === noteId), seen = new Set(fix.map((x) => x.heard.toLowerCase()));
|
|
134
147
|
const guess = [];
|
|
135
|
-
for (const x of [...all].reverse()) { const k = x.heard.toLowerCase(); if (x.note === noteId || seen.has(k)) continue; seen.add(k); guess.push(x); }
|
|
148
|
+
for (const x of [...all].reverse()) { const k = x.heard.toLowerCase(); if (x.note === noteId || seen.has(k) || (x.keep && x.keep !== 'yes')) continue; seen.add(k); guess.push(x); } // only a fix the PM said to remember reaches other notes
|
|
136
149
|
return { fix, guess };
|
|
137
150
|
}
|
|
138
151
|
const reEsc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -147,7 +160,7 @@ export function wx(W, text) {
|
|
|
147
160
|
t.replace(wordRe(all.map((x) => x.heard)), (m, pre, word, off) => {
|
|
148
161
|
const at = off + pre.length, x = all.find((y) => y.heard.toLowerCase() === word.toLowerCase());
|
|
149
162
|
out += esc(t.slice(last, at));
|
|
150
|
-
out += x.k === 'fix' ? `<span class="vzr-fx" title="${esc('Heard as “' + word + '”')}">${esc(x.means)}</span>`
|
|
163
|
+
out += x.k === 'fix' ? `<span class="vzr-fx" data-w="${esc(word)}" title="${esc('Heard as “' + word + '”')}">${esc(x.means)}</span>`
|
|
151
164
|
: `<button type="button" class="vzr-fl" data-a="wfix" data-w="${esc(word)}" data-g="${esc(x.means)}" title="${esc('Heard as “' + word + '”. Likely ' + x.means + ', a word you fixed before.')}">${esc(word)}</button>`;
|
|
152
165
|
last = at + word.length;
|
|
153
166
|
return m;
|
|
@@ -159,23 +172,54 @@ export function placesOf(texts, heard) {
|
|
|
159
172
|
const h = String(heard || '').trim(); if (!h) return 0;
|
|
160
173
|
return (texts || []).reduce((n, t) => n + (String(t == null ? '' : t).match(wordRe([h])) || []).length, 0);
|
|
161
174
|
}
|
|
162
|
-
/** The texts of the view that a word fix reaches: the points, their follow-ups
|
|
163
|
-
export const viewTexts = (V) => V ? [].concat(V.points.map((p) => p.text), V.
|
|
164
|
-
|
|
165
|
-
|
|
175
|
+
/** The texts of the view that a word fix reaches: the points, their follow-ups, the chapter names, Left out. */
|
|
176
|
+
export const viewTexts = (V) => V ? [].concat(V.points.map((p) => p.text), V.strip.map((s) => s.text), V.left.map((x) => x.text)) : [];
|
|
177
|
+
const KIND_PLACE = [['points', 'kept point', 'kept points'], ['steps', 'next step', 'next steps'], ['facts', 'fact', 'facts'], ['left', 'Left out', 'Left out'],
|
|
178
|
+
['lines', 'transcript line', 'transcript lines'], ['summary', 'in the summary', 'in the summary'], ['chapters', 'chapter name', 'chapter names']];
|
|
179
|
+
/** Where a word fix changes the words (MpW1 "12 places in this meeting" and its breakdown): the count per kind of text. o = the page's own
|
|
180
|
+
* texts { summary, steps, facts, lines }. → { n, by: [{ k, n, text }] } with kinds that hold none left out (zero is silence). Pure. */
|
|
181
|
+
export function placesBy(V, o, heard) {
|
|
182
|
+
const T = { points: V ? V.points.map((p) => p.text) : [], chapters: V ? V.strip.map((x) => x.text) : [], left: V ? V.left.map((x) => x.text) : [],
|
|
183
|
+
summary: [o && o.summary || ''], steps: (o && o.steps) || [], facts: (o && o.facts) || [], lines: (o && o.lines) || [] };
|
|
184
|
+
const by = KIND_PLACE.map(([k, one, many]) => { const n = placesOf(T[k], heard); return { k, n, text: k === 'summary' ? many : n + ' ' + (n === 1 ? one : many) }; }).filter((x) => x.n);
|
|
185
|
+
return { n: by.reduce((a, x) => a + x.n, 0), by };
|
|
186
|
+
}
|
|
187
|
+
/** The word fix's popup (MpW1; a bottom sheet on a phone, MpWPhone): "Heard as …", the field with the ✦ spelling when one was found, ✓; the
|
|
188
|
+
* ✦ line with where the spelling comes from and two other spellings to press; how many places in this meeting, by kind. Escape leaves it.
|
|
189
|
+
* sug = lib/meeting-suggest.js suggest() (null: no ✦ line, the field holds the heard words), P = placesBy(). */
|
|
190
|
+
export function wordPopHtml(heard, sug, P) {
|
|
191
|
+
const h = String(heard || ''), best = sug && sug.means ? sug.means : '', n = P ? P.n : 0;
|
|
192
|
+
const from = sug ? [sug.from.includes('fix') ? 'you fixed it that way before' : '', sug.from.includes('invite') ? 'the invite' : '', sug.from.includes('repo') ? (sug.file ? sug.file : 'your repository') : ''].filter(Boolean) : [];
|
|
193
|
+
const why = !best ? '' : sug.from.includes('fix') && from.length === 1 ? 'you fixed it that way before.' : from.filter((x) => !/^you fixed/.test(x)).join(' and ') + (from.filter((x) => !/^you fixed/.test(x)).length > 1 ? ' spell' : ' spells') + ' it that way' + (sug.from.includes('fix') ? ', and you fixed it that way before.' : '.');
|
|
194
|
+
const alts = best ? [best].concat(sug.alts || [], [h]).filter((x, i, a) => x && a.indexOf(x) === i).slice(0, 3) : [];
|
|
195
|
+
const label = n > 1 ? 'Fix it in ' + n + ' places' : 'Fix it';
|
|
196
|
+
return `<div class="vzr-wpop" role="dialog" aria-label="${esc('Fix “' + h + '”')}" data-w="${esc(h)}"><span class="vzr-grab" aria-hidden="true"></span><p class="vzr-wl">Heard as “${esc(h)}”</p>`
|
|
197
|
+
+ `<div class="vzr-wf"><input data-a="wfin" aria-label="${esc('Fix “' + h + '”')}" value="${esc(best || h)}" maxlength="60" spellcheck="false"><button type="button" class="vzr-go" data-a="wfok" aria-label="${esc(label)}" title="${esc(label)}">${ic('chk', 16)}</button></div>`
|
|
198
|
+
+ (best ? `<p class="vzr-sug"><span class="vzr-star" aria-hidden="true">✦</span><span>Claude suggests <b>${esc(best)}</b>: ${esc(why)}</span></p><div class="vzr-alt" role="group" aria-label="Other spellings">${alts.map((x, i) => `<button type="button" data-a="walt" data-v="${esc(x)}" aria-pressed="${i === 0}">${esc(x)}</button>`).join('')}</div>` : '')
|
|
199
|
+
+ (n ? `<p class="vzr-scope"><b>${n === 1 ? '1 place' : n + ' places'}</b> in this meeting</p>${P.by.length > 1 || (P.by[0] && P.by[0].k !== 'summary') ? `<div class="vzr-brk">${P.by.map((x) => `<span>${esc(x.text)}</span>`).join('')}</div>` : ''}` : '')
|
|
200
|
+
+ '<i class="vzr-arr" aria-hidden="true"></i></div>';
|
|
201
|
+
}
|
|
166
202
|
|
|
167
203
|
/* ── html (pure) ── */
|
|
168
|
-
const P = { play: 'M8 5v14l11-7z', pause: 'M8 5h3v14H8zM13 5h3v14h-3z', big: 'M14 4h6v6M10 20H4v-6M20 4l-7 7M4 20l7-7', chev: 'M10 7l5 5-5 5', chk: 'M5 12l5 5 9-10', task: 'M5 12l4 4L19 6', x: 'M6 6l12 12M18 6L6 18', bin: 'M5 7h14M10 7V4h4v3M7 7l1 13h8l1-13',
|
|
169
|
-
box: 'M5 5h14v14H5z', ask: 'M9.2 9.2a2.8 2.8 0 1 1 3.9 2.6c-.7.3-1.1 1-1.1 1.7v.5M12 17.2v.01', pen: 'M4 20h4L19 9l-4-4L4 16zM13.5 6.5l4 4', no: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM5.6 5.6l12.8 12.8', more: 'M5 12h.01M12 12h.01M19 12h.01'
|
|
204
|
+
const P = { play: 'M8 5v14l11-7z', pause: 'M8 5h3v14H8zM13 5h3v14h-3z', big: 'M14 4h6v6M10 20H4v-6M20 4l-7 7M4 20l7-7', chev: 'M10 7l5 5-5 5', prev: 'M14 7l-5 5 5 5', chk: 'M5 12l5 5 9-10', task: 'M5 12l4 4L19 6', x: 'M6 6l12 12M18 6L6 18', bin: 'M5 7h14M10 7V4h4v3M7 7l1 13h8l1-13',
|
|
205
|
+
box: 'M5 5h14v14H5z', ask: 'M9.2 9.2a2.8 2.8 0 1 1 3.9 2.6c-.7.3-1.1 1-1.1 1.7v.5M12 17.2v.01', pen: 'M4 20h4L19 9l-4-4L4 16zM13.5 6.5l4 4', no: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM5.6 5.6l12.8 12.8', more: 'M5 12h.01M12 12h.01M19 12h.01',
|
|
206
|
+
caps: 'M4 6h16v12H4zM4 16l4.5-4.5 4 4 3-3L20 17M9 9.5h.01', chaps: 'M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01', undo: 'M9 14L4 9l5-5M4 9h9a6 6 0 0 1 0 12h-3' };
|
|
170
207
|
const ic = (k, w = 16, cls = '') => `<svg aria-hidden="true" width="${w}" height="${w}" viewBox="0 0 24 24"${cls ? ` class="${cls}"` : ''}><path d="${P[k]}"/></svg>`;
|
|
208
|
+
/** Back or forward 15 seconds (MpD1): the round arrow with 15 in it. */
|
|
209
|
+
const skipIc = (fwd) => `<svg aria-hidden="true" width="28" height="28" viewBox="0 0 24 24"><g${fwd ? ' transform="translate(24 0) scale(-1 1)"' : ''}><path d="M4.5 12a7.5 7.5 0 1 0 2.4-5.5"/><path d="M4 3.5v4h4"/></g><g transform="scale(0.577)"><text x="20.8" y="26.3" text-anchor="middle" fill="currentColor" stroke="none">15</text></g></svg>`;
|
|
171
210
|
const pct = (x) => Math.min(100, Math.max(0, x)).toFixed(2);
|
|
172
|
-
|
|
211
|
+
/** A capture as an image. A capture Jev boxed carries its box; the page crops it to the box (premium-vw-mtg.js crop). */
|
|
212
|
+
const img = (V, t, sh) => (V.noteId ? `<img src="/api/notes/screen?id=${encodeURIComponent(V.noteId)}&t=${t}" alt="" loading="lazy"${sh && sh.box ? ` data-box="${sh.box.join(',')}" data-wh="${sh.w || 16},${sh.h || 9}"` : ''}>` : '');
|
|
173
213
|
const KIND_IC = { decision: '', action: 'box', question: 'ask' };
|
|
174
214
|
const kindIcon = (k) => `<span class="vzr-k" data-k="${k}" role="img" aria-label="${KIND[k].label}">${KIND_IC[k] ? ic(KIND_IC[k], 16) : ''}</span>`;
|
|
175
215
|
/** The selected point: the one the PM pressed, else the first row. */
|
|
176
216
|
export const selOf = (V, ui = {}) => (V && (V.points.find((p) => p.id === ui.pt) || V.rows[0])) || null;
|
|
217
|
+
/** Where the replay is: the playhead, else the selected point's time. */
|
|
218
|
+
export const posOf = (V, ui = {}) => { const p = selOf(V, ui); return ui.pos != null ? ui.pos : p && p.t != null ? p.t : 0; };
|
|
219
|
+
export const WORD_HINT = 'Select a word or a few words anywhere in the notes, or double-click one.';
|
|
177
220
|
|
|
178
|
-
/** One kept point as a lean row (MvC): its kind as an icon, its words, its time, ✓; a decision's
|
|
221
|
+
/** One kept point as a lean row (MvC): its kind as an icon, its words (selectable: a word in them can be fixed), its time, ✓; a decision's
|
|
222
|
+
* follow-up To do under it. The selected row also holds Fix (the edit card opens under it) and Not right. */
|
|
179
223
|
function rowHtml(V, p, ui, sel) {
|
|
180
224
|
const W = V.W, f = p.follow, on = p.verdict === 'confirmed' && (!f || f.verdict === 'confirmed');
|
|
181
225
|
const sub = p.kind === 'question' ? [p.by, p.open ? 'no answer at the end' : ''].filter(Boolean).join(' · ') : p.kind === 'action' ? [p.owner, p.due ? dueText(p.due) : ''].filter(Boolean).join(' · ') : '';
|
|
@@ -184,11 +228,14 @@ function rowHtml(V, p, ui, sel) {
|
|
|
184
228
|
: sub ? `<div class="vzr-fu vzr-sub"><span class="vzr-who">${esc(sub)}</span></div>` : '';
|
|
185
229
|
const chk = p.check ? `<p class="vzr-chk">Check: you said “${esc(p.check.phrase)}” means ${esc(p.check.meaning)}</p>` : '';
|
|
186
230
|
const ids = [p.id].concat(f ? [f.id] : []).join(',');
|
|
231
|
+
const acts = sel ? `<div class="vzr-ra"><button type="button" class="vzp-ib vzr-ghost" data-a="pedit" data-id="${esc(p.id)}" aria-label="Fix its kind or its words" title="Fix its kind or its words">${ic('pen', 15)}</button>`
|
|
232
|
+
+ `<button type="button" class="vzp-ib vzr-ghost" data-a="pwrong" data-id="${esc(p.id)}" aria-label="Not right: move it to Left out" title="Not right">${ic('no', 15)}</button></div>` : '';
|
|
233
|
+
const edit = sel && ui.edit === p.id ? `<div class="vzr-edit">${editHtml(p, ui.draft || {}, ui.also || 0)}</div>` : '';
|
|
187
234
|
return `<div class="vzp-kr vzr-row${sel ? ' sel' : ''}" data-id="${esc(p.id)}">${kindIcon(p.kind)}`
|
|
188
|
-
+ `<
|
|
235
|
+
+ `<div class="vzp-kt" data-a="psel" data-id="${esc(p.id)}" tabindex="0" role="button" aria-pressed="${sel}"><b>${wx(W, p.text)}</b></div>`
|
|
189
236
|
+ (p.time ? `<button type="button" class="vzp-tm${p.open ? ' risk' : ''}" data-a="seekto" data-t="${p.t}" aria-label="Play from ${p.time}">${p.time}</button>` : '<span></span>')
|
|
190
237
|
+ `<button type="button" class="vzr-cf${on ? ' on' : ''}" data-a="pconf" data-ids="${esc(ids)}" aria-pressed="${on}" aria-label="${on ? (p.edited ? 'Fixed and confirmed. Press to undo' : 'Confirmed. Press to undo') : 'Confirm this point'}" title="${on ? 'Confirmed' : 'Confirm'}">${ic('chk', 15)}</button>`
|
|
191
|
-
+ fu + chk + '</div>';
|
|
238
|
+
+ fu + chk + acts + edit + '</div>';
|
|
192
239
|
}
|
|
193
240
|
/** The kept points (MvC): "N kept", a count per kind, Confirm N in the header; the rows. */
|
|
194
241
|
export function pointsCardHtml(V, ui = {}) {
|
|
@@ -210,46 +257,83 @@ export function stripHtml(items, ui = {}) {
|
|
|
210
257
|
return `<div class="vzr-fold" role="group" aria-label="More from this meeting">${list.map((x) => `<button type="button" class="vzr-ch" data-a="strip" data-k="${esc(x.k)}" aria-expanded="${!!open && open.k === x.k}"><b>${esc(x.title)}</b>${x.n ? `<span>${esc(x.n)}</span>` : ''}</button>`).join('')}</div>`
|
|
211
258
|
+ (open ? `<div class="vzr-panel" data-k="${esc(open.k)}">${open.html}</div>` : '');
|
|
212
259
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
260
|
+
|
|
261
|
+
/* ── the player (MpD1, MpD2, MpD4, MpD3) ── */
|
|
262
|
+
/** The kept points' icons inside the bar: a decision's dot, a To do's box, a question's ?. An SVG in the bar's SVG, at the point's place. */
|
|
263
|
+
function markHtml(p, on) {
|
|
264
|
+
const g = p.kind === 'decision' ? '<circle cx="7" cy="7" r="4.5" class="vzr-md"/>' : `<g transform="scale(0.5833)"><path d="${P[KIND_IC[p.kind]]}"/></g>`;
|
|
265
|
+
return `<svg x="${pct(p.at)}%" y="0" width="1" height="1" overflow="visible" class="vzr-mk${on ? ' on' : ''}" data-k="${p.kind}"><title>${esc(p.time + ' · ' + p.text)}</title><g transform="translate(-7 0)">${g}</g></svg>`;
|
|
266
|
+
}
|
|
267
|
+
/** ONE thin bar: the chapters are gaps (no text on it), the part played darker, the kept points as icons in their chapter, the playhead. */
|
|
268
|
+
export function barHtml(V, ui = {}) {
|
|
269
|
+
const pos = posOf(V, ui), ci = V.chapterAt(pos), sel = selOf(V, ui), len = V.len || 1;
|
|
270
|
+
const bars = V.strip.length ? V.strip : [{ from: 0, to: V.len }];
|
|
271
|
+
const gap = bars.length > 1 ? 0.45 : 0;
|
|
272
|
+
const rects = bars.map((b, i) => {
|
|
273
|
+
const x = b.from / len * 100, w = Math.max(0.3, (b.to - b.from) / len * 100 - gap), k = i < ci ? 'past' : i === ci ? 'cur' : 'next';
|
|
274
|
+
const prog = i === ci ? `<rect x="${x.toFixed(2)}%" y="13" width="${Math.max(0, Math.min(w, (pos - b.from) / len * 100)).toFixed(2)}%" height="10" rx="5" class="vzr-prog" data-clk="prog" data-x="${x.toFixed(2)}" data-w="${w.toFixed(2)}"/>` : '';
|
|
275
|
+
return `<rect x="${x.toFixed(2)}%" y="${k === 'cur' ? 13 : 14}" width="${w.toFixed(2)}%" height="${k === 'cur' ? 10 : 8}" rx="${k === 'cur' ? 5 : 4}" class="vzr-sg" data-k="${k}"/>` + prog;
|
|
276
|
+
}).join('');
|
|
277
|
+
const marks = V.points.filter((p) => p.at != null).map((p) => markHtml(p, !!sel && sel.id === p.id)).join('');
|
|
278
|
+
const head = `<svg x="${pct(pos / len * 100)}%" y="0" width="1" height="1" overflow="visible" data-clk="head"><rect x="-2" y="11" width="4" height="14" rx="2" class="vzr-head"/></svg>`;
|
|
279
|
+
const st = ci >= 0 ? V.strip[ci] : null;
|
|
280
|
+
return `<div class="vzr-barw"><div class="vzp-strip vzr-bar" data-a="seek" role="slider" tabindex="0" aria-label="Where the replay is" aria-valuemin="0" aria-valuemax="${Math.round(V.len / 1000)}" aria-valuenow="${Math.round(pos / 1000)}" aria-valuetext="${esc(mmss(pos) + (st && st.text ? ', ' + st.text : ''))}"><svg width="100%" height="26" aria-hidden="true">${rects}${marks}${head}</svg></div>`
|
|
281
|
+
+ `<div class="vzr-hov" hidden aria-hidden="true"><span class="vzr-hth"></span><b></b><small></small></div></div>`;
|
|
233
282
|
}
|
|
234
|
-
/** The
|
|
235
|
-
export function
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return `<div class="vzp-pl vzr-dock" role="group" aria-label="Replay"><button type="button" class="vzp-ib vzp-solid" data-a="play" aria-label="${ui.playing ? 'Pause the recording' : 'Play from ' + mmss(pos)}">${ic(ui.playing ? 'pause' : 'play', 16, 'vzp-fill')}</button>`
|
|
244
|
-
+ `<div class="vzp-ctl"><b data-clk="pos">${mmss(pos)}</b><span class="vzp-mut">of ${V.lenText}</span></div>`
|
|
245
|
-
+ `<div class="vzr-tl">${names}<div class="vzp-strip" data-a="seek" role="slider" tabindex="0" aria-label="Where the replay is" aria-valuemin="0" aria-valuemax="${Math.round(V.len / 1000)}" aria-valuenow="${Math.round(pos / 1000)}" aria-valuetext="${esc(mmss(pos) + (st && st.text ? ', ' + st.text : ''))}"><svg width="100%" height="26" aria-hidden="true">${rects}${dots}${head}</svg></div></div>`
|
|
246
|
-
+ `<button type="button" class="vzp-speed" data-a="speed" aria-label="Playback speed ${speed} times. Change">${speed}×</button></div>`;
|
|
283
|
+
/** The caption under the player, as a music player shows lyrics: the line now, larger, with its speaker; the lines before and after faded. */
|
|
284
|
+
export function captionHtml(V, pos) {
|
|
285
|
+
const L = V.lines || [];
|
|
286
|
+
let i = -1; for (let k = 0; k < L.length; k++) { if (L[k].t <= pos + 500) i = k; else break; }
|
|
287
|
+
if (i < 0) i = L.length ? 0 : -1;
|
|
288
|
+
if (i < 0) return '';
|
|
289
|
+
const pv = L[i - 1], cur = L[i], nx = L[i + 1];
|
|
290
|
+
return (pv ? `<span class="vzr-cp">${wx(V.W, pv.text)}</span>` : '') + `<span class="vzr-cn">${cur.who ? `<span class="vzr-cw">${esc(cur.who)}</span>` : ''}<span class="vzr-said">${wx(V.W, cur.text)}</span></span>`
|
|
291
|
+
+ (nx ? `<span class="vzr-cx">${wx(V.W, nx.text)}</span>` : '');
|
|
247
292
|
}
|
|
248
|
-
/** The
|
|
249
|
-
|
|
293
|
+
/** The line of the caption now: the page draws the caption again only when it changes. */
|
|
294
|
+
export const lineAtPos = (V, pos) => { const L = V.lines || []; let i = -1; for (let k = 0; k < L.length; k++) { if (L[k].t <= pos + 500) i = k; else break; } return i; };
|
|
295
|
+
/** The captures row (MpD4): hidden by default, the captures icon shows it. Five near the playhead and "+N more"; all of them when pressed. */
|
|
296
|
+
function capsHtml(V, ui, si) {
|
|
297
|
+
if (!V.caps.length) return '';
|
|
298
|
+
const N = 5, all = !!ui.allShots || V.caps.length <= N + 1, ci = Math.max(0, V.caps.findIndex((x) => x.i === si));
|
|
299
|
+
const from = all ? 0 : Math.max(0, Math.min(ci - 2, V.caps.length - N)), seen = all ? V.caps : V.caps.slice(from, from + N);
|
|
300
|
+
return `<div class="vzr-film${all ? ' all' : ''}" role="group" aria-label="${esc('Screen captures · ' + V.caps.length)}">${seen.map((s) => `<button type="button" class="vzp-th vzr-th${s.i === si ? ' on' : ''}" data-a="seekto" data-t="${s.t}" aria-label="${esc('Screen at ' + s.time + (s.why ? ': ' + s.why : ''))}" aria-pressed="${s.i === si}"><span class="vzr-crop">${img(V, s.t, s)}</span></button>`).join('')}`
|
|
301
|
+
+ (V.caps.length > N + 1 ? `<button type="button" class="vzr-more" data-a="allshots" aria-expanded="${all}" aria-label="${all ? 'Show only the captures near the playhead' : 'All ' + V.caps.length + ' screen captures'}">${all ? ic('x', 14) : '+' + (V.caps.length - seen.length) + ' more'}</button>` : '') + '</div>';
|
|
302
|
+
}
|
|
303
|
+
/** The chapters list (MpD2): its name, the kept points in it, its start; the chapter now highlighted; press one to play from there. */
|
|
304
|
+
function chaptersHtml(V, ui, ci) {
|
|
305
|
+
return `<div class="vzr-chs" role="dialog" aria-label="Chapters"><div class="vzr-chh"><span class="vzr-lbl">Chapters · ${V.strip.length}</span><span class="vzp-sp"></span><span class="vzr-hint0">Press one to play from there</span></div>`
|
|
306
|
+
+ V.strip.map((b, i) => { const ks = V.points.filter((p) => p.t != null && p.t >= b.from && p.t < b.to).slice(0, 3);
|
|
307
|
+
return `<button type="button" class="vzr-chr${i === ci ? ' cur' : ''}" data-a="chgo" data-t="${b.from}"${i === ci ? ' aria-current="true"' : ''}><b>${wx(V.W, b.text)}</b><span class="vzr-chm">${ks.map((p) => kindIcon(p.kind)).join('')}</span><span class="vzr-cht">${mmss(b.from)}</span></button>`; }).join('') + '</div>';
|
|
308
|
+
}
|
|
309
|
+
/** The player (MpD1): the capture as the artwork, the chapter, the bar, the times, the controls, the caption. phone: MpD3 (no chapter steps,
|
|
310
|
+
* no captures icon, no caption). */
|
|
311
|
+
export function playerHtml(V, ui = {}, phone = false) {
|
|
312
|
+
const pos = posOf(V, ui), si = V.shotAt(pos), shot = si >= 0 ? V.shots[si] : null, ci = V.chapterAt(pos), st = ci >= 0 ? V.strip[ci] : null, speed = ui.speed || 1;
|
|
313
|
+
const art = shot ? `<div class="vzr-art"><span class="vzr-crop">${img(V, shot.t, shot)}</span><span class="vzr-tag">${esc((shot.who ? shot.who + '’s screen' : 'Shared screen') + ' · ' + shot.time)}</span><button type="button" class="vzr-full" data-a="shotbig" data-t="${shot.t}" aria-label="Show the whole frame" title="Show the whole frame">${ic('big', 16)}</button></div>`
|
|
314
|
+
: '<div class="vzr-noshot"><p class="vzp-mut vzp-sm">No screen was shared, so there are no captures.</p></div>';
|
|
315
|
+
const title = st ? `<div class="vzr-nt"><b data-clk="chap">${wx(V.W, st.text)}</b><span>chapter ${ci + 1} of ${V.strip.length}</span></div>` : '';
|
|
316
|
+
const nextSp = SPEEDS[(SPEEDS.indexOf(speed) + 1) % SPEEDS.length];
|
|
317
|
+
const ctl = `<div class="vzr-tr"><span class="vzr-L"><button type="button" class="vzr-speed" data-a="speed" aria-label="${esc('Speed ' + speed + ' times, press for ' + nextSp)}">${speed}×</button></span>`
|
|
318
|
+
+ (!phone && V.strip.length > 1 ? `<button type="button" class="vzr-ib" data-a="chprev" aria-label="Previous chapter" title="Previous chapter">${ic('prev', 20)}</button>` : '')
|
|
319
|
+
+ `<button type="button" class="vzr-skip" data-a="skip" data-d="-15000" aria-label="Back 15 seconds" title="Back 15 seconds">${skipIc(false)}</button>`
|
|
320
|
+
+ `<button type="button" class="vzr-big" data-a="play" aria-label="${ui.playing ? 'Pause' : 'Play from ' + mmss(pos)}">${ic(ui.playing ? 'pause' : 'play', 24, 'vzr-fill')}</button>`
|
|
321
|
+
+ `<button type="button" class="vzr-skip" data-a="skip" data-d="15000" aria-label="Forward 15 seconds" title="Forward 15 seconds">${skipIc(true)}</button>`
|
|
322
|
+
+ (!phone && V.strip.length > 1 ? `<button type="button" class="vzr-ib" data-a="chnext" aria-label="Next chapter" title="Next chapter">${ic('chev', 20)}</button>` : '')
|
|
323
|
+
+ `<span class="vzr-R">${!phone && V.caps.length ? `<button type="button" class="vzr-ib vzr-o${ui.caps ? ' on' : ''}" data-a="caps" aria-pressed="${!!ui.caps}" aria-label="${esc('Screen captures, ' + V.caps.length)}" title="Screen captures">${ic('caps', 18)}</button>` : ''}`
|
|
324
|
+
+ (V.strip.length ? `<button type="button" class="vzr-ib vzr-o${ui.chaps ? ' on' : ''}" data-a="chaps" aria-expanded="${!!ui.chaps}" aria-label="${esc('Chapters, ' + V.strip.length)}" title="Chapters">${ic('chaps', 18)}</button>` : '') + '</span></div>';
|
|
325
|
+
const cap = phone ? '' : captionHtml(V, pos);
|
|
326
|
+
return `<section class="vzr-np${phone ? ' vzr-ph1' : ''}" aria-label="Replay">${art}`
|
|
327
|
+
+ (ui.chaps && V.strip.length ? `<div class="vzr-chw">${chaptersHtml(V, ui, ci)}</div>` : '')
|
|
328
|
+
+ (!phone && ui.caps ? capsHtml(V, ui, si) : '') + title + barHtml(V, ui)
|
|
329
|
+
+ `<div class="vzr-times"><span data-clk="pos">${mmss(pos)}</span><span>${V.lenText}</span></div>` + ctl
|
|
330
|
+
+ (cap ? `<div class="vzr-cap" data-pl="cap" aria-live="off">${cap}</div>` : '') + '</section>';
|
|
331
|
+
}
|
|
332
|
+
/** The desk (MvC + MpD1): the left column (the summary the page gives, the kept points, the strip, the one-time word hint) and the player.
|
|
333
|
+
* o = { summary: html, strip: [{ k, title, n, html }], hint: bool } from the page; Left out is this module's own part of the strip. */
|
|
250
334
|
export function deskHtml(V, ui = {}, o = {}) {
|
|
251
335
|
const items = [V.left.length ? { k: 'left', title: 'Left out', n: String(V.left.length), html: leftHtml(V) } : null].concat(o.strip || []);
|
|
252
|
-
return `<div class="vzp vzp-card vzp-dt vzr"><div class="vzr-cols"><div class="vzr-l">${o.summary || ''}${pointsCardHtml(V, ui)}${stripHtml(items, ui)}
|
|
336
|
+
return `<div class="vzp vzp-card vzp-dt vzr"><div class="vzr-cols"><div class="vzr-l">${o.summary || ''}${pointsCardHtml(V, ui)}${stripHtml(items, ui)}${o.hint ? `<p class="vzr-hint">${esc(WORD_HINT)}</p>` : ''}</div>${playerHtml(V, ui)}</div></div>`;
|
|
253
337
|
}
|
|
254
338
|
const stop = (t) => (/[.!?…”]$/.test(t) ? t : t + '.');
|
|
255
339
|
/** The correction (XvAReplayEdit): the kind, the old words struck, the new words, what PMOS will remember, the trash (Delete: no confirm,
|
|
@@ -269,20 +353,12 @@ export function editHtml(p, d = {}, also = 0) {
|
|
|
269
353
|
+ `<div class="vzp-ef"><span class="vzp-mut vzp-sm">${esc([p.time, p.by].filter(Boolean).join(' · '))}</span><span class="vzp-sp"></span><button type="button" class="vzp-ib vzp-del" data-a="edel" data-id="${esc(p.id)}" aria-label="Delete" title="Delete">${ic('bin', 16)}</button><button type="button" class="vzp-ib vzp-del" data-a="pwrong" data-id="${esc(p.id)}" aria-label="Not right: move it to Left out" title="Not right">${ic('no', 16)}</button><button type="button" class="vzp-gbtn" data-a="ecancel">Cancel</button><button type="button" class="vzp-dark" data-a="esave"${changed && text.trim() ? '' : ' disabled'}>Save</button></div></div>`;
|
|
270
354
|
}
|
|
271
355
|
|
|
272
|
-
/** The phone's Replay (
|
|
356
|
+
/** The phone's Replay (MpD3, MpWPhone): the player, then "N kept" with Confirm N and the kept points (their ✓ is on the desk). */
|
|
273
357
|
export function phoneHtml(V, ui = {}) {
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
+ (
|
|
278
|
-
const toConfirm = V.points.filter((p) => !p.verdict);
|
|
279
|
-
h.push(`<div class="vzp-phh"><p class="vzp-lbl">Key points · ${V.points.length}</p>${toConfirm.length ? `<button type="button" class="vzp-dark" data-a="confirmall">Confirm all</button>` : ''}</div>`);
|
|
280
|
-
for (const k of ['decision', 'action', 'question']) {
|
|
281
|
-
const rows = V.points.filter((p) => p.kind === k); if (!rows.length) continue;
|
|
282
|
-
h.push(`<p class="vzp-h3">${KIND[k].many}</p>` + rows.map((p) => `<div class="vzp-prow"><button type="button" class="vzp-tm${p.open ? ' risk' : ''}" data-a="seekto" data-t="${p.t == null ? '' : p.t}"${p.time ? '' : ' disabled'}>${p.time || '–'}</button><span>${esc(p.text)}</span>${k === 'action' ? (ui.pmade && ui.pmade[p.id] ? '<span class="vzp-pill" data-k="you">Task</span>' : `<button type="button" class="vzp-soft" data-a="ptask" data-id="${esc(p.id)}">Make task</button>`) : ''}</div>`).join(''));
|
|
283
|
-
}
|
|
284
|
-
if (!V.points.length) h.push('<p class="vzp-empty">Nothing was agreed, owned or left open in this meeting.</p>');
|
|
285
|
-
return h.join('');
|
|
358
|
+
const sel = selOf(V, ui);
|
|
359
|
+
const rows = V.rows.map((p) => rowHtml(V, p, ui, !!sel && sel.id === p.id)).join('');
|
|
360
|
+
return playerHtml(V, ui, true) + `<div class="vzr-phk"><div class="vzr-ph"><h3>${V.points.length} kept</h3><span class="vzp-sp"></span>${V.toConfirm ? `<button type="button" class="vzp-dark vzr-cfall" data-a="confirmall">${ic('chk', 14)}Confirm ${V.toConfirm}</button>` : ''}</div>`
|
|
361
|
+
+ (rows ? `<div class="vzp-kl">${rows}</div>` : '<p class="vzp-empty">Nothing was agreed, owned or left open in this meeting.</p>') + '</div>';
|
|
286
362
|
}
|
|
287
363
|
|
|
288
364
|
/** The whole of it: the desk and the phone; the stylesheet shows one (cozy-vw-mtg.css). o: deskHtml's (the summary and the strip). */
|
|
@@ -560,7 +560,12 @@ function draw() {
|
|
|
560
560
|
}
|
|
561
561
|
const ae = document.activeElement, keep = ae && b.contains(ae) ? ae.getAttribute('data-r') : null;
|
|
562
562
|
syncMore();
|
|
563
|
-
|
|
563
|
+
const html = panelHtml();
|
|
564
|
+
// batch 27 item 13 (flicker while a draft is written): the same panel is not built again, and a new one keeps the scroll
|
|
565
|
+
if (R.fromFile && html === b.__rpHtml) return;
|
|
566
|
+
const top = b.scrollTop;
|
|
567
|
+
b.innerHTML = html; b.__rpHtml = html; R.draws = (R.draws || 0) + 1;
|
|
568
|
+
b.scrollTop = top;
|
|
564
569
|
wire(b);
|
|
565
570
|
fit(b);
|
|
566
571
|
const f = R.pop && R.pop !== R.popWas ? b.querySelector('.rp-pops button:not([disabled]), .rp-pops a, .rp-pops input') : null; // a popover that just opened takes the focus
|
|
@@ -1009,7 +1014,7 @@ export async function openReport(row, hand = {}) {
|
|
|
1009
1014
|
// the template behind one icon (batch 22 item 14, canvas RfH1): format, readers, when it goes out, and its history
|
|
1010
1015
|
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
1016
|
onPress: (a) => { if (R !== me) return; if (a === 'template') openTemplate(); else if (a === 'chat') openChat(); },
|
|
1012
|
-
render: (b) => { syncMore(); b.innerHTML = panelHtml(); wire(b); }, onClose: () => { if (R === me && !me.reopen) { present(false); R = null; } } });
|
|
1017
|
+
render: (b) => { syncMore(); b.__rpHtml = b.innerHTML = panelHtml(); wire(b); }, onClose: () => { if (R === me && !me.reopen) { present(false); R = null; } } });
|
|
1013
1018
|
vOpen(head());
|
|
1014
1019
|
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: [] }; }
|
|
1015
1020
|
if (R !== me) return me;
|
|
@@ -1032,14 +1037,16 @@ const CHECK_MS = 2500;
|
|
|
1032
1037
|
/** Read the report again when its file changed on disk (a chat or a draft run wrote it). Stops when another report opens or the panel closes. */
|
|
1033
1038
|
async function watchFile(me) {
|
|
1034
1039
|
const stamp = async () => { const j = await Promise.resolve(pmGet('/api/doc-sheet/stamp?' + q({ ws: me.ws, path: me.row.path }))).catch(() => null); return j && j.ok ? j.hash : null; };
|
|
1035
|
-
let seen = await stamp();
|
|
1040
|
+
let seen = await stamp(), pending = false;
|
|
1036
1041
|
while (R === me) {
|
|
1037
1042
|
await new Promise((ok) => setTimeout(ok, CHECK_MS));
|
|
1038
1043
|
if (R !== me) return;
|
|
1039
1044
|
if (document.hidden || me.edit || me.busy || me.pop || (me.cuts && me.cuts.on === 'sponsors')) continue; // the sponsors cut is read from the file, not the file
|
|
1040
1045
|
const h = await stamp();
|
|
1041
1046
|
if (R !== me || !h) continue;
|
|
1042
|
-
|
|
1047
|
+
// a session writes the draft in many steps: read it when the file has been still for one more check, so a burst of writes is one redraw
|
|
1048
|
+
if (seen && h !== seen) { seen = h; pending = true; continue; }
|
|
1049
|
+
if (pending) { pending = false; await reread(); if (R === me && !me.edit && !me.pop) { me.fromFile = true; draw(); me.fromFile = false; readReview(); } }
|
|
1043
1050
|
seen = h;
|
|
1044
1051
|
}
|
|
1045
1052
|
}
|
|
@@ -93,7 +93,7 @@ export function openReportTab(block, o = {}) {
|
|
|
93
93
|
// itself shows the report as it is
|
|
94
94
|
if (block.id && block.change && !t.passed && (!o.auto || linked) && !PASSED.has(JSON.stringify(block))) { PASSED.add(JSON.stringify(block)); t.passed = true; passChange(t, !!o.auto); return show(t); }
|
|
95
95
|
if (block.id && t.panel === 'current') return current(t);
|
|
96
|
-
if (block.id && t.panel === 'template') { const v = show(t); loadFlow(t).then(() => { redraw(t); if (!o.auto) makeLook(t); }); return v; }
|
|
96
|
+
if (block.id && t.panel === 'template') { const v = show(t); loadFlow(t).then(() => { redraw(t); if (!o.auto || linked) makeLook(t); }); return v; }
|
|
97
97
|
return show(t);
|
|
98
98
|
}
|
|
99
99
|
/** The set-up report shown beside the chat `chatKey`, by id, or null (Wawan, 2026-11-04). The chat sends it with each message, so feedback in any
|
|
@@ -112,6 +112,13 @@ async function makeLook(t) {
|
|
|
112
112
|
if (!words || PASSED.has(key) || !t.F || !t.F.ok) return;
|
|
113
113
|
PASSED.add(key);
|
|
114
114
|
const had = t.F.look && t.F.look.newer ? t.F.look.newer.n : null;
|
|
115
|
+
// the file the PM attached to follow (a deck, a screenshot) is added to the look's samples first (Wawan, 2026-11-04)
|
|
116
|
+
if (t.block.sample) {
|
|
117
|
+
const sm = t.block.sample, ext = (sm.split('.').pop() || '').toLowerCase(), name = sm.split('/').pop();
|
|
118
|
+
const data = KIND_OF[ext] ? await fileB64(sm) : null;
|
|
119
|
+
const a = data ? await post('/api/report-design/sample', { id: t.block.id, name, kind: KIND_OF[ext], data }) : null;
|
|
120
|
+
if (!a || !a.ok) { t.msg = 'The look could not read ' + name + (a ? ': ' + why(a, 'no answer') : '') + '. It follows your words only.'; redraw(t); }
|
|
121
|
+
}
|
|
115
122
|
const r = await post('/api/report-design/turn', { id: t.block.id, text: words, points: [] });
|
|
116
123
|
if (!r || !r.ok) { t.msg = 'The look was not started: ' + why(r, 'the page could not reach PMOS') + '.'; redraw(t); return; }
|
|
117
124
|
t.making = words; redraw(t);
|
|
@@ -165,13 +172,32 @@ async function passChange(t, auto) {
|
|
|
165
172
|
let said;
|
|
166
173
|
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.`;
|
|
167
174
|
else {
|
|
175
|
+
const since = Date.now() - 5000;
|
|
168
176
|
const r = await post('/api/report-flow/ask', { id: t.block.id, text: t.block.change, ...(to.path ? { path: to.path } : {}) });
|
|
177
|
+
if (r && r.ok) setTimeout(() => answerOf(t, since), 0);
|
|
169
178
|
const which = to.slot ? `the ${to.slot} draft` : 'the draft';
|
|
170
179
|
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.`)
|
|
171
180
|
: 'The change was not passed on: ' + why(r, 'the page could not reach PMOS') + '.';
|
|
172
181
|
}
|
|
182
|
+
const look = t.block.design ? { kind: 'report', id: t.block.id, design: t.block.design, ...(t.block.sample ? { sample: t.block.sample } : {}) } : null;
|
|
173
183
|
t.block = { kind: 'report', id: t.block.id }; t.passed = false;
|
|
174
|
-
|
|
184
|
+
const v = await current(t, said);
|
|
185
|
+
// one answer that asked for the look as well: the look starts its own version, kept with ✓ (the text change went to the session above)
|
|
186
|
+
if (look) openReportTab(look, { chat: t.chat, auto: true });
|
|
187
|
+
return v;
|
|
188
|
+
}
|
|
189
|
+
/** The report's session's answer to a change, once it writes one (answer.json, read by GET /api/report-flow): it shows beside the chat, so a
|
|
190
|
+
* change the session did not make never looks like it is still drafting (Wawan, 2026-11-04). Reads every few seconds, 15 minutes at most. */
|
|
191
|
+
async function answerOf(t, since) {
|
|
192
|
+
const end = Date.now() + 15 * 60000;
|
|
193
|
+
while (Date.now() < end && T.get(t.id) === t) {
|
|
194
|
+
await new Promise((ok) => setTimeout(ok, POLL_MS * 2));
|
|
195
|
+
const F = await get('/api/report-flow', { id: t.block.id });
|
|
196
|
+
if (F && F.ok && F.answer && Date.parse(F.answer.at) >= since) {
|
|
197
|
+
if (t.panel === 'current') return current(t, 'The report\'s session: ' + F.answer.text);
|
|
198
|
+
t.msg = 'The report\'s session: ' + F.answer.text; redraw(t); return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
175
201
|
}
|
|
176
202
|
/** The chat's proposal names a report that is set up already (an earlier page, or a reload): open that report instead of the proposal. */
|
|
177
203
|
async function adoptSetUp(id, name) {
|
|
@@ -416,7 +442,10 @@ async function press(t, e) {
|
|
|
416
442
|
if (!r || !r.ok) return 'The template did not change: ' + why(r, 'the page could not reach PMOS') + '.';
|
|
417
443
|
if (body.sections) t.block = Object.assign({}, t.block, { sections: undefined, structure: undefined });
|
|
418
444
|
if (r.state) t.F = r.state; await loadFlow(t);
|
|
419
|
-
|
|
445
|
+
const rd = r.redraw;
|
|
446
|
+
if (rd && /report engine/.test(rd.reason || '')) return 'T' + r.n + ' is the current template. The open ' + (rd.slot ? rd.slot + ' ' : '') + 'draft draws in the new look now; its numbers stay the same. Nothing is issued.';
|
|
447
|
+
return 'T' + r.n + ' is the current template. ' + (rd && rd.started ? 'The open ' + (rd.slot ? rd.slot + ' ' : '') + 'draft is being redrawn in the new look; its numbers stay the same. Nothing is issued.'
|
|
448
|
+
: 'The next cycle follows it' + (rd && rd.reason === 'sent' ? '; the open cycle is issued, so it keeps its look.' : '; the open draft stays as it is.') + ' The earlier cycles keep their look.');
|
|
420
449
|
});
|
|
421
450
|
return;
|
|
422
451
|
}
|
|
@@ -485,7 +514,7 @@ const KIND_OF = { pdf: 'pdf', pptx: 'pptx', docx: 'docx', png: 'image', jpg: 'im
|
|
|
485
514
|
/** A workspace file as base64, read through /api/asset. null when it cannot be read. */
|
|
486
515
|
async function fileB64(path) {
|
|
487
516
|
try {
|
|
488
|
-
const res = await fetch('/api/asset?' + qs({ ws: ws(), path }));
|
|
517
|
+
const res = await fetch(String(path).startsWith('/') ? '/api/chat-file?' + qs({ path }) : '/api/asset?' + qs({ ws: ws(), path }));
|
|
489
518
|
if (!res.ok) return null;
|
|
490
519
|
const u8 = new Uint8Array(await res.arrayBuffer());
|
|
491
520
|
let bin = ''; for (let i = 0; i < u8.length; i += 0x8000) bin += String.fromCharCode.apply(null, u8.subarray(i, i + 0x8000));
|