create-pmos 0.502.0 → 0.504.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 +39 -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-calendar-2-test.js +4 -3
- package/workspace/briefing-premium-calendar-3-test.js +2 -3
- package/workspace/changelog.json +38 -0
- package/workspace/fixtures/premium-viewer-fixtures.js +3 -2
- package/workspace/guide/briefing-premium.md +1 -1
- package/workspace/guide/home.md +1 -0
- package/workspace/guide/notes.md +21 -1
- package/workspace/lib/meeting-terms.js +53 -0
- package/workspace/lib/routes-meeting-points.js +7 -4
- package/workspace/lib/transcript-read.js +38 -3
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-chat-line-test.js +3 -3
- package/workspace/premium-vw-meet-test.js +28 -17
- package/workspace/public/pm/bd-cols.js +5 -2
- package/workspace/public/pm/bd-deliveries.js +3 -3
- package/workspace/public/pm/bd-pin-card.js +3 -3
- package/workspace/public/pm/bd-pins.js +10 -0
- package/workspace/public/pm/bd-span.js +11 -9
- package/workspace/public/pm/cozy-vw-mtg.css +81 -0
- package/workspace/public/pm/home-line.css +2 -2
- package/workspace/public/pm/home-line.js +42 -13
- package/workspace/public/pm/premium-mtg-replay.js +155 -67
- package/workspace/public/pm/premium-vw-mtg.js +119 -25
|
@@ -1,20 +1,24 @@
|
|
|
1
|
-
/* pm/premium-mtg-replay.js —
|
|
2
|
-
*
|
|
1
|
+
/* pm/premium-mtg-replay.js — a recorded meeting in the Viewer (batch 25 item 11, "Meeting recording view C": MvC, MvF, MvN on the main
|
|
2
|
+
* canvas; was MtReplay and XvAReplay). Pure: the page half is in premium-vw-mtg.js.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
4
|
+
* LEFT the summary first (premium-vw-mtg.js draws it), then the kept points: one lean row each. The kind is an icon; the header
|
|
5
|
+
* counts each kind and holds Confirm N. A decision and the To do that follows it (said within 2 min, in the same topic)
|
|
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
|
+
* Under the points, ONE strip: Left out, Next steps, Since the last one, Facts, Names and terms, Prep and material,
|
|
8
|
+
* Transcript (premium-vw-mtg.js fills the ones it owns). One opens at a time.
|
|
9
|
+
* RIGHT the evidence of the selected point: the screen at that moment, Play from its time, Fix (the edit card), Not right,
|
|
10
|
+
* the quote, and a filmstrip of the screen captures near it (+N shows all). Jev for Images is not in PMOS yet, so the
|
|
11
|
+
* filmstrip shows every kept capture, not only the shared-screen frames.
|
|
12
|
+
* DOCK the timeline at the bottom: Play, the time of the length, the full chapter names over the strip, a dot per kept point.
|
|
13
|
+
* WORD FIX (MvF) a word heard wrong is fixed in place: the PM selects it (or presses an underlined one), types the fix, ✓ or Enter.
|
|
14
|
+
* It shows fixed in every place this note shows it, and other notes underline the heard words with the fix as the guess.
|
|
15
|
+
* Undo forgets it. lib/meeting-learn.js term keeps it; the note's own files are not rewritten.
|
|
16
|
+
* Phone: the phone's Replay (phoneHtml), unchanged.
|
|
13
17
|
*
|
|
14
18
|
* DATA (nothing is invented): rep = premium-meeting.js replayModel(GET /api/meeting-replay) with rep.raw = the answer's keys (for the owner,
|
|
15
|
-
* due and state); n = the note (GET /api/notes/one) for the remarks of the live transcript; points = GET /api/meeting-points.
|
|
16
|
-
* replayView(rep, { n, points, now }) → the view model
|
|
17
|
-
* replayHtml(V, ui) → the
|
|
19
|
+
* 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
|
+
* 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, allShots, leftOpen, edit, draft } */
|
|
18
22
|
|
|
19
23
|
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
20
24
|
const clip = (s, n) => { const t = String(s == null ? '' : s).replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
|
|
@@ -25,6 +29,7 @@ const NEAR = 3000; // a key point belongs to the line this close to its time (
|
|
|
25
29
|
export const KIND = { decision: { label: 'Decided', pill: 'ok', many: 'Decided' }, action: { label: 'To do', pill: 'y', many: 'To do' }, question: { label: 'Open', pill: 'w', many: 'Open questions' } };
|
|
26
30
|
export const KEPT_WHY = 'agreed, owned, or still open';
|
|
27
31
|
export const SHOTS_HINT = 'A capture is taken each time the shared screen changes. Press one to jump there.';
|
|
32
|
+
export const FOLLOW_MS = 120000; // a To do said this soon after a decision, in the same topic, is its follow-up (one row)
|
|
28
33
|
export const SPEEDS = [1, 1.5, 2];
|
|
29
34
|
/** "Fri" for a date this week or next, else "24 Oct". */
|
|
30
35
|
export function dueText(due, now = Date.now()) {
|
|
@@ -58,7 +63,7 @@ export const mentions = (text, phrase) => !!phrase && (' ' + words(text).join('
|
|
|
58
63
|
export const KIND_OF = { decision: 'Decision', question: 'Question', action: 'Action' };
|
|
59
64
|
|
|
60
65
|
/** The view model of the Replay tab. edits: the PM's corrections ({ [pointId]: { kind, text, orig } }); lessons: what PMOS learned. */
|
|
61
|
-
export function replayView(rep, { n = null, points = {}, edits = {}, lessons = [], now = Date.now() } = {}) {
|
|
66
|
+
export function replayView(rep, { n = null, points = {}, edits = {}, lessons = [], terms = [], now = Date.now() } = {}) {
|
|
62
67
|
if (!rep) return null;
|
|
63
68
|
const raw = Array.isArray(rep.raw) ? rep.raw : [];
|
|
64
69
|
const rawOf = (k) => raw.find((x) => x && (x.id === k.id || (x.k === k.k && x.text === k.text))) || k;
|
|
@@ -97,8 +102,20 @@ export function replayView(rep, { n = null, points = {}, edits = {}, lessons = [
|
|
|
97
102
|
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)) }));
|
|
98
103
|
const whoAt = (t) => { let w = ''; for (const l of rep.lines) { if (l.t <= t + NEAR) w = l.who; else break; } return w; };
|
|
99
104
|
const shots = rep.shots.map((s, i) => ({ i, t: s.t, time: mmss(s.t), why: clip(s.why, 80), who: whoAt(s.t) }));
|
|
105
|
+
/* 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
|
+
const topOf = (t) => strip.find((x) => t >= x.from && t < x.to) || null;
|
|
107
|
+
const rows = [], under = new Map();
|
|
108
|
+
for (const p of kept) {
|
|
109
|
+
if (p.kind === 'action' && p.t != null) {
|
|
110
|
+
const d = [...rows].reverse().find((r) => r.kind === 'decision' && r.t != null && !under.has(r.id) && p.t >= r.t && p.t - r.t <= FOLLOW_MS && topOf(p.t) === topOf(r.t));
|
|
111
|
+
if (d) { under.set(d.id, p); continue; }
|
|
112
|
+
}
|
|
113
|
+
rows.push(p);
|
|
114
|
+
}
|
|
115
|
+
const noteId = rep.noteId || (n && n.id) || '';
|
|
100
116
|
return {
|
|
101
|
-
|
|
117
|
+
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,
|
|
102
119
|
points: kept, left, raised, toConfirm: kept.filter((x) => !x.verdict).length,
|
|
103
120
|
seg: [['all', 'All', kept.length], ['decision', 'Decided', count('decision')], ['action', 'To do', count('action')], ['question', 'Open', count('question')]],
|
|
104
121
|
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',
|
|
@@ -109,39 +126,134 @@ export function replayView(rep, { n = null, points = {}, edits = {}, lessons = [
|
|
|
109
126
|
};
|
|
110
127
|
}
|
|
111
128
|
|
|
129
|
+
/* ── words fixed in place (MvF) ── */
|
|
130
|
+
/** The word fixes for this note (shown fixed) and from other notes (heard words underlined, the fix as the guess). Pure. */
|
|
131
|
+
export function wordsOf(terms, noteId) {
|
|
132
|
+
const all = (Array.isArray(terms) ? terms : []).filter((x) => x && x.heard && x.means);
|
|
133
|
+
const fix = all.filter((x) => x.note === noteId), seen = new Set(fix.map((x) => x.heard.toLowerCase()));
|
|
134
|
+
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); }
|
|
136
|
+
return { fix, guess };
|
|
137
|
+
}
|
|
138
|
+
const reEsc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
139
|
+
const wordRe = (heards) => new RegExp('(^|[^\\p{L}\\p{N}])(' + heards.map(reEsc).join('|') + ')(?=$|[^\\p{L}\\p{N}])', 'giu');
|
|
140
|
+
/** The text as HTML with the word fixes in it: a fixed word shows its fix (green), a word fixed in another note is underlined and is a
|
|
141
|
+
* press that offers its fix (data-a="wfix"). Text with no fix is only escaped. */
|
|
142
|
+
export function wx(W, text) {
|
|
143
|
+
const t = String(text == null ? '' : text);
|
|
144
|
+
const all = W ? [...W.fix.map((x) => ({ ...x, k: 'fix' })), ...W.guess.map((x) => ({ ...x, k: 'guess' }))].sort((a, b) => b.heard.length - a.heard.length) : [];
|
|
145
|
+
if (!all.length || !t) return esc(t);
|
|
146
|
+
let out = '', last = 0;
|
|
147
|
+
t.replace(wordRe(all.map((x) => x.heard)), (m, pre, word, off) => {
|
|
148
|
+
const at = off + pre.length, x = all.find((y) => y.heard.toLowerCase() === word.toLowerCase());
|
|
149
|
+
out += esc(t.slice(last, at));
|
|
150
|
+
out += x.k === 'fix' ? `<span class="vzr-fx" title="${esc('Heard as “' + word + '”')}">${esc(x.means)}</span>`
|
|
151
|
+
: `<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
|
+
last = at + word.length;
|
|
153
|
+
return m;
|
|
154
|
+
});
|
|
155
|
+
return out + esc(t.slice(last));
|
|
156
|
+
}
|
|
157
|
+
/** How many times the words occur in these texts (the places a fix changes). Pure. */
|
|
158
|
+
export function placesOf(texts, heard) {
|
|
159
|
+
const h = String(heard || '').trim(); if (!h) return 0;
|
|
160
|
+
return (texts || []).reduce((n, t) => n + (String(t == null ? '' : t).match(wordRe([h])) || []).length, 0);
|
|
161
|
+
}
|
|
162
|
+
/** The texts of the view that a word fix reaches: the points, their follow-ups and quotes, the chapter names, Left out. */
|
|
163
|
+
export const viewTexts = (V) => V ? [].concat(V.points.map((p) => p.text), V.points.map((p) => p.quote), V.strip.map((s) => s.text), V.left.map((x) => x.text)) : [];
|
|
164
|
+
/** The inline field of a word fix: the best guess, ✓ (Enter) fixes it in every place, Escape leaves it. */
|
|
165
|
+
export const wordFieldHtml = (heard, guess, places) => `<span class="vzr-fe" data-w="${esc(heard)}"><input data-a="wfin" aria-label="${esc('Fix “' + heard + '”')}" value="${esc(guess || heard)}" maxlength="60" spellcheck="false"><button type="button" class="vzr-ok" data-a="wfok" aria-label="${esc(places > 1 ? 'Fix it in ' + places + ' places' : 'Fix it')}" title="${esc(places > 1 ? 'Fix it in ' + places + ' places' : 'Fix it')}">${ic('chk', 14)}</button></span>`;
|
|
166
|
+
|
|
112
167
|
/* ── html (pure) ── */
|
|
113
|
-
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'
|
|
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' };
|
|
114
170
|
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>`;
|
|
115
171
|
const pct = (x) => Math.min(100, Math.max(0, x)).toFixed(2);
|
|
172
|
+
const img = (V, t) => (V.noteId ? `<img src="/api/notes/screen?id=${encodeURIComponent(V.noteId)}&t=${t}" alt="" loading="lazy">` : '');
|
|
173
|
+
const KIND_IC = { decision: '', action: 'box', question: 'ask' };
|
|
174
|
+
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
|
+
/** The selected point: the one the PM pressed, else the first row. */
|
|
176
|
+
export const selOf = (V, ui = {}) => (V && (V.points.find((p) => p.id === ui.pt) || V.rows[0])) || null;
|
|
116
177
|
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
178
|
+
/** One kept point as a lean row (MvC): its kind as an icon, its words, its time, ✓; a decision's follow-up To do under it. */
|
|
179
|
+
function rowHtml(V, p, ui, sel) {
|
|
180
|
+
const W = V.W, f = p.follow, on = p.verdict === 'confirmed' && (!f || f.verdict === 'confirmed');
|
|
181
|
+
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(' · ') : '';
|
|
182
|
+
const fu = f ? `<div class="vzr-fu">${ic('box', 15, 'vzr-bx')}<span class="vzr-grow">${wx(W, f.text)}${f.owner ? ` <span class="vzr-who">· ${esc(f.owner)}${f.due ? ', ' + esc(dueText(f.due)) : ''}</span>` : ''}</span>`
|
|
183
|
+
+ (ui.pmade && ui.pmade[f.id] ? '<span class="vzr-tk">Task</span>' : `<button type="button" class="vzp-ib vzr-ghost" data-a="ptask" data-id="${esc(f.id)}" aria-label="${esc('Make a task: ' + f.text)}" title="Make a task">${ic('task', 15)}</button>`) + '</div>'
|
|
184
|
+
: sub ? `<div class="vzr-fu vzr-sub"><span class="vzr-who">${esc(sub)}</span></div>` : '';
|
|
185
|
+
const chk = p.check ? `<p class="vzr-chk">Check: you said “${esc(p.check.phrase)}” means ${esc(p.check.meaning)}</p>` : '';
|
|
186
|
+
const ids = [p.id].concat(f ? [f.id] : []).join(',');
|
|
187
|
+
return `<div class="vzp-kr vzr-row${sel ? ' sel' : ''}" data-id="${esc(p.id)}">${kindIcon(p.kind)}`
|
|
188
|
+
+ `<button type="button" class="vzp-kt" data-a="psel" data-id="${esc(p.id)}" aria-pressed="${sel}"><b>${wx(W, p.text)}</b></button>`
|
|
189
|
+
+ (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
|
+
+ `<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>';
|
|
192
|
+
}
|
|
193
|
+
/** The kept points (MvC): "N kept", a count per kind, Confirm N in the header; the rows. */
|
|
194
|
+
export function pointsCardHtml(V, ui = {}) {
|
|
195
|
+
const sel = selOf(V, ui), cnt = ['decision', 'action', 'question'].map((k) => { const c = V.points.filter((p) => p.kind === k).length; return c ? `<span class="vzr-cnt">${kindIcon(k)}${c}</span>` : ''; }).join('');
|
|
196
|
+
const head = `<div class="vzr-ph"><h3>${V.points.length} kept</h3>${cnt}<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>`;
|
|
197
|
+
const rows = V.rows.length ? V.rows.map((p) => rowHtml(V, p, ui, !!sel && sel.id === p.id)).join('') : `<p class="vzp-empty vzr-pad">Nothing was agreed, owned or left open in this meeting.</p>`;
|
|
198
|
+
return `<section class="vzr-pts" aria-label="Kept points">${head}<div class="vzp-kl">${rows}</div></section>`;
|
|
199
|
+
}
|
|
200
|
+
/** Left out (a panel of the strip): each remark and why it was left out; a point marked not right can be put back. */
|
|
201
|
+
export function leftHtml(V) {
|
|
202
|
+
if (!V.left.length) return '';
|
|
203
|
+
return `<p class="vzp-mut vzp-sm">${esc(V.keptLine)}</p><div class="vzp-ll">${V.left.map((x) => `<div class="vzp-lr"><span>${wx(V.W, x.text)}</span><small>${esc(x.why)}</small>${x.verdict === 'wrong' ? `<button type="button" class="vzp-link" data-a="unwrong" data-id="${esc(x.id)}">Put it back</button>` : x.time ? `<button type="button" class="vzp-tm" data-a="seekto" data-t="${x.t}" aria-label="Play from ${x.time}">${x.time}</button>` : ''}</div>`).join('')}</div>`;
|
|
204
|
+
}
|
|
205
|
+
/** The one strip under the points: a chip per part (zero is silence), the open one's body below it. items = [{ k, title, n, html }] */
|
|
206
|
+
export function stripHtml(items, ui = {}) {
|
|
207
|
+
const list = (items || []).filter((x) => x && x.html);
|
|
208
|
+
if (!list.length) return '';
|
|
209
|
+
const open = list.find((x) => x.k === ui.strip);
|
|
210
|
+
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
|
+
+ (open ? `<div class="vzr-panel" data-k="${esc(open.k)}">${open.html}</div>` : '');
|
|
212
|
+
}
|
|
213
|
+
/** The evidence of the selected point (MvC right): the screen at the replay's place, Play from the point, Fix, Not right, the quote
|
|
214
|
+
* (or the edit card), and the filmstrip of the captures near it. */
|
|
215
|
+
export function evidenceHtml(V, ui = {}) {
|
|
216
|
+
const p = selOf(V, ui);
|
|
217
|
+
const pos = ui.pos != null ? ui.pos : p && p.t != null ? p.t : 0, si = V.shotAt(pos), shot = si >= 0 ? V.shots[si] : null;
|
|
218
|
+
const big = shot ? `<div class="vzp-shot vzr-shot"><span class="vzp-img">${img(V, shot.t)}</span><span class="vzp-who">${esc((shot.who ? shot.who + '’s screen' : 'Shared screen') + ' · ' + shot.time)}</span><button type="button" class="vzp-ib vzp-big" data-a="shotbig" data-t="${shot.t}" aria-label="Open the screen capture large">${ic('big', 14)}</button></div>`
|
|
219
|
+
: '<div class="vzr-noshot"><p class="vzp-mut vzp-sm">No screen was shared, so there are no captures.</p></div>';
|
|
220
|
+
const acts = p ? `<div class="vzr-acts">${p.time ? `<button type="button" class="vzp-ib vzr-pri" data-a="pplay" data-t="${p.t}" aria-label="Play from ${p.time}">${ic('play', 16, 'vzp-fill')}</button><b class="vzr-at">${p.time}</b>` : ''}<span class="vzp-sp"></span>`
|
|
221
|
+
+ `<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', 16)}</button>`
|
|
222
|
+
+ `<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', 16)}</button></div>` : '';
|
|
223
|
+
const body = p && ui.edit === p.id ? editHtml(p, ui.draft || {}, ui.also || 0)
|
|
224
|
+
: p ? (p.quote ? `<p class="vzr-q">${wx(V.W, p.quote)}</p>` : '') + (p.check ? `<p class="vzr-chk">Check: you said “${esc(p.check.phrase)}” means ${esc(p.check.meaning)}</p>` : '') : '';
|
|
225
|
+
let film = '';
|
|
226
|
+
if (V.shots.length) {
|
|
227
|
+
const N = 7, all = !!ui.allShots || V.shots.length <= N + 1, from = all ? 0 : Math.max(0, Math.min(Math.max(si, 0) - 3, V.shots.length - N));
|
|
228
|
+
const seen = all ? V.shots : V.shots.slice(from, from + N);
|
|
229
|
+
film = `<div class="vzr-film${all ? ' all' : ''}" role="group" aria-label="${esc('Screen captures · ' + V.shots.length)}">${seen.map((s) => `<button type="button" class="vzp-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}">${img(V, s.t)}</button>`).join('')}`
|
|
230
|
+
+ (V.shots.length > N + 1 ? `<button type="button" class="vzr-more" data-a="allshots" aria-expanded="${all}" aria-label="${all ? 'Show only the captures near this point' : 'All ' + V.shots.length + ' screen captures'}">${all ? ic('x', 14) : '+' + (V.shots.length - seen.length)}</button>` : '') + '</div>';
|
|
231
|
+
}
|
|
232
|
+
return `<section class="vzr-ev" aria-label="Evidence for the selected point">${big}${acts}${body}${film}</section>`;
|
|
233
|
+
}
|
|
234
|
+
/** The timeline docked at the bottom (MvC): Play, the time of the length, the full chapter names over the strip, a dot per kept point. */
|
|
235
|
+
export function dockHtml(V, ui = {}) {
|
|
236
|
+
const pos = ui.pos || 0, st = V.stretchAt(pos), speed = ui.speed || 1;
|
|
125
237
|
const bars = V.strip.length ? V.strip : [{ w: 1, c: 0, text: '', min: 0, from: 0, to: V.len }];
|
|
126
238
|
let x = 0;
|
|
127
|
-
const rects = bars.map((b) => { const w = b.w * 100 - (bars.length > 1 ? 0.4 : 0), r = `<rect x="${x.toFixed(2)}%" y="
|
|
128
|
-
const dots = V.points.filter((p) => p.at != null).map((p) => `<circle cx="${pct(p.at)}%" cy="
|
|
129
|
-
const head = `<rect class="vzp-head" data-clk="head" x="${pct(pos / V.len * 100)}%" y="0" width="2" height="
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
239
|
+
const rects = bars.map((b) => { const w = b.w * 100 - (bars.length > 1 ? 0.4 : 0), r = `<rect x="${x.toFixed(2)}%" y="9" width="${Math.max(0.5, w).toFixed(2)}%" height="8" rx="4" class="vzp-st" data-c="${b.c}"/>`; x += b.w * 100; return r; }).join('');
|
|
240
|
+
const dots = V.points.filter((p) => p.at != null).map((p) => `<circle cx="${pct(p.at)}%" cy="5" r="4.5" class="vzp-kd" data-k="${p.kind}"/>`).join('');
|
|
241
|
+
const head = `<rect class="vzp-head" data-clk="head" x="${pct(pos / V.len * 100)}%" y="0" width="2" height="26" rx="1"/>`;
|
|
242
|
+
const names = V.strip.length ? `<div class="vzp-tops vzr-tops" data-n="${V.strip.length}">${V.strip.map((b) => `<button type="button" class="vzp-top" data-a="seekto" data-t="${b.from}" data-w="${(b.w * 1000).toFixed(0)}" title="${esc(b.text + ' · ' + b.min + ' min')}"${st === b ? ' aria-current="true"' : ''}>${wx(V.W, b.text)}</button>`).join('')}</div>` : '';
|
|
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>`;
|
|
133
247
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
function
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return `<button type="button" class="vzp-pt" data-a="pedit" data-id="${esc(p.id)}" title="Fix its kind or its words"><span class="vzp-pill" data-k="${p.pill}">${KIND_OF[p.kind]}</span><span class="vzp-kt"><b>${esc(p.text)}</b><small>${esc(meta)}</small>`
|
|
140
|
-
+ (p.check ? `<small class="vzp-chk2">Check: you said “${esc(p.check.phrase)}” means ${esc(p.check.meaning)}</small>` : '') + '</span></button>';
|
|
248
|
+
/** The desk (MvC): the left column (the summary the page gives, the kept points, the strip), the evidence, and the docked timeline.
|
|
249
|
+
* o = { summary: html, strip: [{ k, title, n, html }] } from the page; Left out is this module's own part of the strip. */
|
|
250
|
+
export function deskHtml(V, ui = {}, o = {}) {
|
|
251
|
+
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)}</div>${evidenceHtml(V, ui)}</div>${dockHtml(V, ui)}</div>`;
|
|
141
253
|
}
|
|
142
254
|
const stop = (t) => (/[.!?…”]$/.test(t) ? t : t + '.');
|
|
143
255
|
/** The correction (XvAReplayEdit): the kind, the old words struck, the new words, what PMOS will remember, the trash (Delete: no confirm,
|
|
144
|
-
* Undo in the message, like a task's Delete), Cancel and Save.
|
|
256
|
+
* Undo in the message, like a task's Delete), Not right (it moves to Left out; batch 25 item 11 moved it here from the bulk bar), Cancel and Save.
|
|
145
257
|
* d = { kind, text, learn }; also = how many other points of this note use the learned words. */
|
|
146
258
|
export function editHtml(p, d = {}, also = 0) {
|
|
147
259
|
const kind = d.kind || p.kind, text = d.text == null ? p.text : d.text, learn = d.learn !== false;
|
|
@@ -154,31 +266,7 @@ export function editHtml(p, d = {}, also = 0) {
|
|
|
154
266
|
+ (changed ? `<p class="vzp-was"><s>${esc(KIND_OF[p.orig.kind] + ' · ' + p.orig.text)}</s></p>` : '')
|
|
155
267
|
+ `<textarea class="vzp-eta" data-a="etext" rows="3" aria-label="What was really said">${esc(text)}</textarea>`
|
|
156
268
|
+ `<div class="vzp-learn"><b>${learn && changed ? 'I will remember this.' : ''}</b> <span data-pl="lesson">${esc(lessonText)}</span>${changed ? ` <button type="button" class="vzp-link" data-a="elearn" aria-pressed="${!learn}">${learn ? 'Only fix this one' : 'Remember it'}</button>` : ''}</div>`
|
|
157
|
-
+ `<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-gbtn" data-a="ecancel">Cancel</button><button type="button" class="vzp-dark" data-a="esave"${changed && text.trim() ? '' : ' disabled'}>Save</button></div></div>`;
|
|
158
|
-
}
|
|
159
|
-
/** "In this part · <topic>": the kept points said in the stretch the replay is in. */
|
|
160
|
-
export function partHtml(V, ui = {}) {
|
|
161
|
-
const pos = ui.pos || 0, st = V.stretchAt(pos);
|
|
162
|
-
const rows = V.points.filter((p) => p.t != null && st && p.t >= st.from && p.t < st.to);
|
|
163
|
-
if (!rows.length) return ''; // zero is silence: a part with nothing kept says nothing
|
|
164
|
-
return `<p class="vzp-lbl">In this part${st && st.text ? ' · ' + esc(st.text) : ''}</p><div class="vzp-pts">${rows.map((p) => pointRow(p, ui)).join('')}</div>`;
|
|
165
|
-
}
|
|
166
|
-
/** Every kept point as a row to press (the "All N kept" fold): grouped as the filter says, the bulk bar for the ticked, Left out. */
|
|
167
|
-
export function keptHtml(V, ui = {}) { return pointsHtml(V, { ...ui, rows: true }); }
|
|
168
|
-
|
|
169
|
-
/** The kept points, desktop: filter, the kept line, the bulk bar, the rows, the Left out fold. */
|
|
170
|
-
export function pointsHtml(V, ui = {}) {
|
|
171
|
-
const f = ui.filter || 'all', sel = ui.sel || new Set(), rows = V.points.filter((p) => f === 'all' || p.kind === f);
|
|
172
|
-
const h = [];
|
|
173
|
-
h.push(`<div class="vzp-fl"><div class="vzp-seg" role="group" aria-label="Show">${V.seg.map(([k, l, c]) => `<button type="button" data-a="pf" data-k="${k}" aria-pressed="${f === k}">${esc(l + ' ' + c)}</button>`).join('')}</div><span class="vzp-mut vzp-sm">${esc(V.keptLine)}</span></div>`);
|
|
174
|
-
if (sel.size) h.push(`<div class="vzp-bulk" role="toolbar" aria-label="The ticked points"><b>${sel.size} selected</b><button type="button" data-a="bulk" data-k="task">Make tasks</button><button type="button" data-a="bulk" data-k="confirmed">Confirm</button><button type="button" data-a="bulk" data-k="wrong" class="vzp-gh">Not right</button></div>`);
|
|
175
|
-
h.push(`<div class="vzp-kl">${rows.length ? rows.map((p) => ui.edit === p.id ? editHtml(p, ui.draft || {}, ui.also || 0) : `<div class="vzp-kr${sel.has(p.id) ? ' sel' : ''}"><button type="button" class="vzp-chk" role="checkbox" aria-checked="${sel.has(p.id)}" data-a="tick" data-id="${esc(p.id)}" aria-label="${esc('Tick: ' + p.text)}">${sel.has(p.id) ? ic('chk', 12) : ''}</button><span class="vzp-pill" data-k="${p.pill}">${p.label}</span><button type="button" class="vzp-kt" data-a="pedit" data-id="${esc(p.id)}" title="Fix its kind or its words"><p><b>${esc(p.text)}</b>${p.verdict === 'confirmed' ? ' <span class="vzp-conf">' + ic('chk', 12) + (p.edited ? 'Fixed' : 'Confirmed') + '</span>' : ''}</p>${p.quote ? `<p class="vzp-q">${esc(p.quote)}</p>` : ''}${p.check ? `<p class="vzp-q vzp-chk2">Check: you said “${esc(p.check.phrase)}” means ${esc(p.check.meaning)}</p>` : ''}</button>${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>'}</div>`).join('')
|
|
176
|
-
: `<p class="vzp-empty">${V.points.length ? 'No point of this kind was kept.' : 'Nothing was agreed, owned or left open in this meeting.'}</p>`}</div>`);
|
|
177
|
-
if (V.left.length) {
|
|
178
|
-
h.push(`<button type="button" class="vzp-left" data-a="leftout" aria-expanded="${!!ui.leftOpen}">${ic('chev', 14)}<b>${esc(V.leftLine)}</b><span>${esc(V.leftEg)}</span></button>`);
|
|
179
|
-
if (ui.leftOpen) h.push(`<div class="vzp-ll">${V.left.map((x) => `<div class="vzp-lr"><span>${esc(x.text)}</span><small>${esc(x.why)}</small>${x.verdict === 'wrong' ? `<button type="button" class="vzp-link" data-a="unwrong" data-id="${esc(x.id)}">Put it back</button>` : x.time ? `<button type="button" class="vzp-tm" data-a="seekto" data-t="${x.t}" aria-label="Play from ${x.time}">${x.time}</button>` : ''}</div>`).join('')}</div>`);
|
|
180
|
-
}
|
|
181
|
-
return h.join('');
|
|
269
|
+
+ `<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>`;
|
|
182
270
|
}
|
|
183
271
|
|
|
184
272
|
/** The phone's Replay (MtPhone.dc.html): the player without the capture, "Now: <topic>", and the points grouped by kind. made = { [pointId]: true } */
|
|
@@ -197,8 +285,8 @@ export function phoneHtml(V, ui = {}) {
|
|
|
197
285
|
return h.join('');
|
|
198
286
|
}
|
|
199
287
|
|
|
200
|
-
/** The whole
|
|
201
|
-
export function replayHtml(V, ui = {}) {
|
|
288
|
+
/** 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). */
|
|
289
|
+
export function replayHtml(V, ui = {}, o = {}) {
|
|
202
290
|
if (!V) return '<p class="vzp-empty">This meeting has no recording to replay.</p>';
|
|
203
|
-
return
|
|
291
|
+
return `${deskHtml(V, ui, o)}<div class="vzp vzp-mb">${phoneHtml(V, ui)}</div>`;
|
|
204
292
|
}
|