create-pmos 0.133.0 → 0.135.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 +1 -1
- 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/bin/notes-replay.js +235 -0
- package/workspace/c3-briefing-test.js +130 -0
- package/workspace/c3-foundation-test.js +159 -0
- package/workspace/c3-ib-items-test.js +105 -0
- package/workspace/c3-ib-more-test.js +120 -0
- package/workspace/c3-ib-triage-backlog-test.js +119 -0
- package/workspace/c3-ini-harness.js +66 -0
- package/workspace/c3-initiatives-test.js +140 -0
- package/workspace/c3-one-home-test.js +14 -1
- package/workspace/c3-reports-test.js +57 -18
- package/workspace/c3-settings-test.js +139 -0
- package/workspace/c3-work-test.js +42 -10
- package/workspace/fixtures/one-home-baseline.json +1 -9
- package/workspace/guide/briefing-cards.md +15 -3
- package/workspace/guide/briefing.md +12 -8
- package/workspace/guide/calendar-popup.md +2 -2
- package/workspace/guide/console-calendar.md +39 -10
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-boards.md +65 -0
- package/workspace/guide/jira.md +3 -1
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/my-work.md +5 -3
- package/workspace/guide/notes.md +35 -2
- package/workspace/guide/reports-dashboards.md +29 -5
- package/workspace/guide/settings.md +68 -0
- package/workspace/guide/welcome.md +10 -10
- package/workspace/lib/calendar-cli.js +3 -2
- package/workspace/lib/calendar-layers.js +21 -10
- package/workspace/lib/calendar-tidy.js +230 -0
- package/workspace/lib/engine/boards.js +542 -0
- package/workspace/lib/engine/breakdown-ops.js +180 -0
- package/workspace/lib/engine/calendar-import.js +84 -0
- package/workspace/lib/engine/calendar-model.js +185 -0
- package/workspace/lib/engine/card-state.js +92 -0
- package/workspace/lib/engine/derived/derive.js +92 -0
- package/workspace/lib/engine/derived/plan.js +3 -0
- package/workspace/lib/engine/derived/trust.js +3 -0
- package/workspace/lib/engine/doc-health.js +142 -0
- package/workspace/lib/engine/doc-sheet.js +75 -0
- package/workspace/lib/engine/intake.js +78 -0
- package/workspace/lib/engine/outbox.js +86 -0
- package/workspace/lib/engine/plan-forecast.js +342 -0
- package/workspace/lib/engine/promises.js +29 -0
- package/workspace/lib/engine/records/index.js +114 -0
- package/workspace/lib/engine/releases.js +96 -0
- package/workspace/lib/engine/replies.js +99 -0
- package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
- package/workspace/lib/engine/report-families.js +175 -0
- package/workspace/lib/engine/roadmap-ops.js +150 -0
- package/workspace/lib/engine/sessions-list.js +27 -0
- package/workspace/lib/engine/signal-rules.js +95 -0
- package/workspace/lib/engine/signals/cards.js +57 -0
- package/workspace/lib/engine/signals/detectors.js +370 -0
- package/workspace/lib/engine/signals/facts-cli.js +8 -0
- package/workspace/lib/engine/signals/facts.js +197 -0
- package/workspace/lib/engine/signals/index.js +33 -0
- package/workspace/lib/engine/signals/kinds.js +110 -0
- package/workspace/lib/engine/signals/layout-paths.js +44 -0
- package/workspace/lib/engine/signals/options.js +24 -0
- package/workspace/lib/engine/signals/score.js +72 -0
- package/workspace/lib/engine/signals/select.js +47 -0
- package/workspace/lib/engine/step-planner.js +65 -0
- package/workspace/lib/engine/trust/claim-rules.json +93 -0
- package/workspace/lib/engine/trust/import-calendar.js +21 -0
- package/workspace/lib/engine/trust/import-guard.js +36 -0
- package/workspace/lib/engine/trust/ledger.js +53 -0
- package/workspace/lib/engine/trust/outbox.js +45 -0
- package/workspace/lib/engine/trust/plan.js +5 -0
- package/workspace/lib/engine/trust/trust.js +21 -0
- package/workspace/lib/engine/truth-table.js +57 -0
- package/workspace/lib/engine/workspace-lib.js +24 -0
- package/workspace/lib/factory-join.js +1 -1
- package/workspace/lib/facts-async.js +4 -4
- package/workspace/lib/headless.js +95 -0
- package/workspace/lib/import-overlay.js +2 -1
- package/workspace/lib/jira-status.js +30 -3
- package/workspace/lib/notes-audio.js +28 -9
- package/workspace/lib/notes-capture.js +136 -0
- package/workspace/lib/notes-final.js +38 -13
- package/workspace/lib/notes-live-guard.js +117 -0
- package/workspace/lib/notes-replay.js +125 -0
- package/workspace/lib/notes-screen.js +180 -0
- package/workspace/lib/notes-whisper.js +139 -16
- package/workspace/lib/notes.js +4 -2
- package/workspace/lib/outbox-store.js +10 -6
- package/workspace/lib/pmos-verbs.js +3 -1
- package/workspace/lib/proactive-cli.js +7 -0
- package/workspace/lib/proactive.js +15 -2
- package/workspace/lib/repo-script.js +36 -0
- package/workspace/lib/report-dashboards.js +5 -2
- package/workspace/lib/report-frame.js +122 -0
- package/workspace/lib/report-schedules.js +113 -0
- package/workspace/lib/route-modules.js +9 -2
- package/workspace/lib/routes-boards.js +734 -0
- package/workspace/lib/routes-brief-page.js +13 -4
- package/workspace/lib/routes-briefing.js +22 -11
- package/workspace/lib/routes-c3-init.js +4 -3
- package/workspace/lib/routes-c3-life.js +2 -1
- package/workspace/lib/routes-c3-reports.js +96 -13
- package/workspace/lib/routes-c3-strategy.js +4 -3
- package/workspace/lib/routes-c3-timeline.js +3 -2
- package/workspace/lib/routes-calendar-import.js +3 -2
- package/workspace/lib/routes-calendar-views.js +9 -4
- package/workspace/lib/routes-doc-health.js +2 -1
- package/workspace/lib/routes-doc-sheet.js +2 -1
- package/workspace/lib/routes-forecast.js +2 -1
- package/workspace/lib/routes-hooks.js +3 -2
- package/workspace/lib/routes-initiatives.js +2 -1
- package/workspace/lib/routes-intake.js +2 -1
- package/workspace/lib/routes-notes.js +113 -15
- package/workspace/lib/routes-outbox.js +23 -3
- package/workspace/lib/routes-pm-items.js +19 -6
- package/workspace/lib/routes-records.js +3 -2
- package/workspace/lib/routes-releases.js +2 -1
- package/workspace/lib/routes-report-facts.js +4 -3
- package/workspace/lib/routes-report-families.js +2 -1
- package/workspace/lib/routes-roadmap-ops.js +5 -4
- package/workspace/lib/routes-runs.js +6 -5
- package/workspace/lib/routes-settings.js +51 -0
- package/workspace/lib/routes-signals.js +3 -2
- package/workspace/lib/routes-stage-tools.js +220 -0
- package/workspace/lib/run-answers.js +2 -1
- package/workspace/lib/settings-store.js +226 -0
- package/workspace/lib/stage-tools.js +208 -0
- package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
- package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
- package/workspace/lib/work-evidence.js +73 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/briefing-deck.js +63 -17
- package/workspace/public/pm/briefing-model.js +36 -7
- package/workspace/public/pm/c3-ambient.js +100 -0
- package/workspace/public/pm/c3-brief-needs.js +26 -6
- package/workspace/public/pm/c3-brief-pulse.js +12 -4
- package/workspace/public/pm/c3-brief-today.js +56 -21
- package/workspace/public/pm/c3-brief.js +2 -1
- package/workspace/public/pm/c3-calendar.js +161 -42
- package/workspace/public/pm/c3-calpop.js +5 -5
- package/workspace/public/pm/c3-demo-boards.js +649 -0
- package/workspace/public/pm/c3-demo-brief-page.js +1 -1
- package/workspace/public/pm/c3-demo-cal4.js +3 -2
- package/workspace/public/pm/c3-demo-cal5.js +52 -0
- package/workspace/public/pm/c3-demo-data.js +17 -27
- package/workspace/public/pm/c3-demo-rep3.js +46 -5
- package/workspace/public/pm/c3-demo.js +2 -1
- package/workspace/public/pm/c3-dock.js +1 -1
- package/workspace/public/pm/c3-ib-docs.js +80 -0
- package/workspace/public/pm/c3-ib-initiatives.js +134 -0
- package/workspace/public/pm/c3-ib-items.js +201 -0
- package/workspace/public/pm/c3-ib-releases.js +51 -0
- package/workspace/public/pm/c3-ib-sprints.js +55 -0
- package/workspace/public/pm/c3-ib-triage.js +178 -0
- package/workspace/public/pm/c3-ii-tabs.js +240 -0
- package/workspace/public/pm/c3-ini-records.js +197 -0
- package/workspace/public/pm/c3-ini-timeline.js +278 -0
- package/workspace/public/pm/c3-ini-ui.js +229 -0
- package/workspace/public/pm/c3-ini-work.js +139 -0
- package/workspace/public/pm/c3-initiatives.css +218 -0
- package/workspace/public/pm/c3-initiatives.js +741 -0
- package/workspace/public/pm/c3-plan.js +2 -2
- package/workspace/public/pm/c3-reports.css +47 -47
- package/workspace/public/pm/c3-reports.js +223 -185
- package/workspace/public/pm/c3-settings.css +74 -0
- package/workspace/public/pm/c3-settings.js +703 -0
- package/workspace/public/pm/c3-shell.js +102 -36
- package/workspace/public/pm/c3-terminal-icons.js +2 -2
- package/workspace/public/pm/c3-terminal.js +2 -2
- package/workspace/public/pm/calendar-views.js +26 -13
- package/workspace/public/pm/components.css +319 -213
- package/workspace/public/pm/console3.css +118 -23
- package/workspace/public/pm/dest-notes.js +26 -4
- package/workspace/public/pm/notes-screen-capture.js +165 -0
- package/workspace/public/pm/notes-screen-view.js +98 -0
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/pm-work-view.js +233 -146
- package/workspace/public/pm/shell-bell.js +3 -2
- package/workspace/public/pm/shell-recording.js +148 -17
- package/workspace/public/pm/tokens.css +2 -2
- package/workspace/public/pm/view-notes.js +53 -11
- package/workspace/public/pm/welcome.js +197 -89
- package/workspace/public/welcome.html +6 -4
- package/workspace/server.js +105 -6
- package/workspace/skills/day-check.skill +1 -1
- package/workspace/skills/jira-sync.skill +6 -4
- package/workspace/skills/morning-brief.skill +1 -1
- package/workspace/test-files.json +14 -3
- package/workspace/test-lists/c3-ib-items.json +9 -0
- package/workspace/test-lists/c3-ib-more.json +9 -0
- package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
- package/workspace/test-lists/c3-initiative-boards.json +6 -0
- package/workspace/test-lists/calendar-tidy-up.json +6 -0
- package/workspace/test-lists/console3-briefing.json +9 -0
- package/workspace/test-lists/console3-foundation.json +9 -0
- package/workspace/test-lists/console3-reports.json +3 -3
- package/workspace/test-lists/meeting-notes.json +12 -4
- package/workspace/test-lists/workspace-headless-jobs.json +5 -0
- package/workspace/test-lists/workspace-package-script-closure.json +6 -0
- package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
- package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
- package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
- package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
- package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
- package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
- package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
- package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
- package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
- package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
|
@@ -66,7 +66,8 @@ let unreadSeen = 0; // how many the person has already looked
|
|
|
66
66
|
let lastCount = 0;
|
|
67
67
|
let bellSheet = null;
|
|
68
68
|
let liveTimer = null; let liveDone = false;
|
|
69
|
-
|
|
69
|
+
/* b.notices: what a dashboard schedule made (routes-briefing.js); each goes to Reports > Drafts. */
|
|
70
|
+
const notifList = (b) => [...arrived, ...notifsOf(b), ...(b && Array.isArray(b.notices) ? b.notices : [])];
|
|
70
71
|
|
|
71
72
|
export function openBellList() {
|
|
72
73
|
if (bellSheet && bellSheet.root.isConnected) { bellSheet.close(); return bellSheet; }
|
|
@@ -128,7 +129,7 @@ export function mountBell(host, opts) {
|
|
|
128
129
|
};
|
|
129
130
|
rec.off = onBriefing((b) => {
|
|
130
131
|
rec.b = b;
|
|
131
|
-
const n = bellCount(b) + arrived.length;
|
|
132
|
+
const n = bellCount(b) + arrived.length + (b && Array.isArray(b.notices) ? b.notices.length : 0);
|
|
132
133
|
const bump = n > rec.count; rec.count = n; lastCount = n;
|
|
133
134
|
rec.sync();
|
|
134
135
|
if (bump) { badge.classList.remove('bump'); void badge.offsetWidth; badge.classList.add('bump'); }
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
/* pm/shell-recording.js — the
|
|
1
|
+
/* pm/shell-recording.js — the Record control in console3's top bar (console3 wave E2b; the Record control port).
|
|
2
2
|
*
|
|
3
3
|
* mountRecording(host) / disposeRecording(host)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* The control is always there, idle included, with the same capabilities as the main app's Record control (pm/topbar.js, pm/note-dock.js):
|
|
6
|
+
* New (an untitled note that records at once), an arrow that lists today's meetings that have not started (a pick makes a note linked to that
|
|
7
|
+
* calendar event and records it), and, while a meeting records or is paused, a chip that names it with Highlight, Pause or Resume, and Stop
|
|
8
|
+
* (Stop asks first and offers "Pause instead"). All of it goes through pm/note-dock.js, which owns the real recorder, so the audio capture of
|
|
9
|
+
* pm/view-notes.js runs here without the main app's Notes place on screen. The chip opens the note in the main app, because console3 has no
|
|
10
|
+
* Notes place. While one meeting records, a red dot sits on the Calendar tab (`[data-c3-tab="cal"]`) and on a Notes tab if a frame draws one.
|
|
11
|
+
* On /c3.html?demo=1 the control drives a SIMULATED recorder (see below): it opens no microphone and saves nothing; it shows the idle Record button and
|
|
12
|
+
* arrow, and the live group while the simulation runs. Lifted: console3.html `.recg` and `recCtl` (lines 916 to 924, 4385 to 4400). The clock is the recorder's own start time.
|
|
10
13
|
*/
|
|
11
14
|
import { el } from './dom.js';
|
|
15
|
+
import { button } from './primitives.js';
|
|
12
16
|
import { ensureC3Css, c3ic } from './pm-work-view.js';
|
|
13
17
|
import * as dock from './note-dock.js';
|
|
14
18
|
import { recView, clockOf } from './shell-model.js';
|
|
@@ -20,10 +24,10 @@ import { toast } from './c3-toast.js';
|
|
|
20
24
|
const TAB = '[data-fr-dest="notes"], #mobNav [data-dest="notes"], [data-c3-tab="cal"]';
|
|
21
25
|
|
|
22
26
|
/* ── demo mode: a SIMULATED recorder ──────────────────────────────────────────────────────────────────────────────────────────────
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
+
* On /c3.html?demo=1 the Record button of the Calendar's During and After stages drives this stand-in, so the top bar shows the mockup's
|
|
28
|
+
* recording group: a pulsing dot, the clock, mark moment, pause or resume, and stop. It keeps a clock (performance.now: the demo freezes
|
|
29
|
+
* Date) and nothing else. No microphone is opened and nothing is saved. Real mode never reaches it: `isDemo()` reads the page's
|
|
30
|
+
* data-c3-demo mark, which only the demo sets. */
|
|
27
31
|
export const isDemo = () => typeof document !== 'undefined' && !!document.querySelector('[data-c3-demo]');
|
|
28
32
|
const SIM = { mode: 'idle', id: 'sim', title: '', started: 0, pausedAt: 0 };
|
|
29
33
|
const tick = () => performance.now();
|
|
@@ -46,15 +50,36 @@ export const simDock = {
|
|
|
46
50
|
};
|
|
47
51
|
const live = new WeakMap();
|
|
48
52
|
|
|
53
|
+
/* Demo mode, idle: the mockup's Record button and the arrow beside it (the same two buttons the real control draws). Record starts the simulated recorder with an
|
|
54
|
+
* untitled note; the arrow lists the same choice and today's meetings that have not started. Nothing is opened or saved. */
|
|
55
|
+
function simIdle(d) {
|
|
56
|
+
const glyph = (dd) => { const sv = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); sv.setAttribute('class', 'ic sm'); sv.setAttribute('viewBox', '0 0 24 24'); sv.setAttribute('aria-hidden', 'true'); const pth = document.createElementNS('http://www.w3.org/2000/svg', 'path'); pth.setAttribute('d', dd); sv.appendChild(pth); return sv; };
|
|
57
|
+
const start = (title) => { if (d === simDock) simStart(title); };
|
|
58
|
+
const mic = button({ variant: 'ghost', label: [c3ic('mic', 'sm'), el('span', { class: 'c3rec__lbl' }, 'Record')], onClick: () => start('Untitled note'),
|
|
59
|
+
attrs: { title: 'Record an untitled note', 'aria-label': 'Record an untitled note', 'data-fr-rec-new': '' } });
|
|
60
|
+
const arrow = button({ variant: 'ghost', label: glyph('M6 9l6 6 6-6'), attrs: { title: 'Record for an upcoming meeting', 'aria-label': 'Record for an upcoming meeting', 'aria-haspopup': 'menu', 'data-fr-rec-upcoming': '' },
|
|
61
|
+
onClick: async (e) => {
|
|
62
|
+
const anchor = e.currentTarget;
|
|
63
|
+
const { menu } = await import('./primitives.js');
|
|
64
|
+
let evs = [];
|
|
65
|
+
try { const links = await import('./notes-links.js'); evs = links.upcomingWithoutNote(await links.upcomingToday()); } catch { evs = []; }
|
|
66
|
+
menu({ anchor, ariaLabel: 'Record a new meeting', items: [{ label: 'Untitled note', icon: 'plus', detail: 'not linked to a meeting', attrs: { 'data-fr-rec-untitled': '' }, onSelect: () => start('Untitled note') }, { sep: true },
|
|
67
|
+
...(evs.length ? evs.slice(0, 4).map((x) => ({ label: x.title, hint: x.time || '', attrs: { 'data-fr-rec-event': x.id || '' }, onSelect: () => start(x.title) })) : [{ label: 'No more meetings today', attrs: { 'data-fr-rec-none': '' }, onSelect: () => {} }])] });
|
|
68
|
+
} });
|
|
69
|
+
return [mic, arrow];
|
|
70
|
+
}
|
|
71
|
+
|
|
49
72
|
export function mountRecording(host, opts) {
|
|
50
73
|
if (!host) return;
|
|
51
74
|
disposeRecording(host);
|
|
52
75
|
ensureC3Css();
|
|
53
|
-
|
|
76
|
+
if (!(opts && opts.dock) && !isDemo()) return mountReal(host);
|
|
77
|
+
const d = (opts && opts.dock) || simDock;
|
|
54
78
|
const nowMs = () => (d.now ? d.now() : Date.now());
|
|
55
79
|
const grp = el('span', { class: 'recg', role: 'group', 'aria-label': 'Recording', hidden: true, 'data-shell': 'recording' });
|
|
56
|
-
|
|
57
|
-
|
|
80
|
+
const wrap = el('span', { class: 'c3rec', 'data-fr': 'record' }, grp); // the same wrapper the real control has, so the idle group takes its look
|
|
81
|
+
host.insertBefore(wrap, host.firstChild);
|
|
82
|
+
const rec = { grp: wrap, key: '', clock: null, tabs: [] };
|
|
58
83
|
const tabDot = (on) => {
|
|
59
84
|
for (const t of document.querySelectorAll(TAB)) {
|
|
60
85
|
let dot = t.querySelector('.shell-recdot');
|
|
@@ -73,14 +98,16 @@ export function mountRecording(host, opts) {
|
|
|
73
98
|
const key = [v.mode, st.id, v.title, v.dot].join(':');
|
|
74
99
|
if (key !== rec.key) {
|
|
75
100
|
rec.key = key;
|
|
76
|
-
grp.hidden =
|
|
101
|
+
grp.hidden = false; // the mockup shows the idle Record control too
|
|
77
102
|
grp.classList.toggle('paused', v.mode === 'paused');
|
|
78
|
-
grp.
|
|
79
|
-
|
|
103
|
+
grp.classList.toggle('idle', !v.shown);
|
|
104
|
+
grp.setAttribute('aria-label', v.shown ? (v.mode === 'recording' ? 'Recording ' : 'Paused: ') + v.title : 'New recording');
|
|
105
|
+
if (!v.shown) { rec.clock = null; grp.replaceChildren(...simIdle(d)); }
|
|
80
106
|
else {
|
|
81
107
|
rec.clock = el('span', { class: 'recclk', 'data-shell-rec-clock': '' });
|
|
82
108
|
const open = el('button', { type: 'button', title: (v.mode === 'recording' ? 'Recording: ' : 'Paused: ') + v.title + '. Open the live note', 'aria-label': 'Open the note: ' + v.title, 'data-shell-rec-open': '', onclick: () => d.openDock(st.id) },
|
|
83
|
-
el('span', { class: 'recdot' + (v.dot === 'static' ? ' static' : ''), 'data-dot': v.dot }), rec.clock, el('span', { class: 'rt' }, v.title)
|
|
109
|
+
el('span', { class: 'recdot' + (v.dot === 'static' ? ' static' : ''), 'data-dot': v.dot }), rec.clock, el('span', { class: 'rt' }, v.title),
|
|
110
|
+
el('span', { class: 'c3rec__eng', 'data-fr-rec-engine': '', title: 'The live transcript: engine and language (Notes settings). Demo value.' }, 'EN forced \u00b7 whisper'));
|
|
84
111
|
const kids = [open];
|
|
85
112
|
if (v.canMark) kids.push(btn('Mark this moment', 'highlighter', () => d.highlightNow(), 'data-shell-rec-mark'));
|
|
86
113
|
if (v.canPause) kids.push(btn('Pause', 'pause', () => d.pauseNow(), 'data-shell-rec-pause'));
|
|
@@ -103,8 +130,112 @@ export function disposeRecording(host) {
|
|
|
103
130
|
const rec = host && live.get(host);
|
|
104
131
|
if (!rec) return;
|
|
105
132
|
clearInterval(rec.timer);
|
|
133
|
+
if (rec.off) rec.off();
|
|
106
134
|
if (rec.onSim) window.removeEventListener('c3-sim-rec', rec.onSim);
|
|
107
135
|
rec.grp.remove();
|
|
108
136
|
for (const t of document.querySelectorAll(TAB)) { const dot = t.querySelector('.shell-recdot'); if (dot) dot.remove(); }
|
|
109
137
|
live.delete(host);
|
|
110
138
|
}
|
|
139
|
+
|
|
140
|
+
/* ── real mode: the always-visible Record control ─────────────────────────────────────────────────────────────────────────────────── */
|
|
141
|
+
|
|
142
|
+
const NS_SVG = 'http://www.w3.org/2000/svg';
|
|
143
|
+
const glyph = (d) => { const s = document.createElementNS(NS_SVG, 'svg'); s.setAttribute('class', 'ic sm'); s.setAttribute('viewBox', '0 0 24 24'); s.setAttribute('aria-hidden', 'true'); const p = document.createElementNS(NS_SVG, 'path'); p.setAttribute('d', d); s.appendChild(p); return s; };
|
|
144
|
+
const RGL = { highlighter: () => c3ic('flag', 'sm'), pause: () => glyph('M8 5v14M16 5v14'), play: () => c3ic('play', 'sm'), square: () => c3ic('stop', 'sm'), mic: () => c3ic('mic', 'sm'), down: () => glyph('M6 9l6 6 6-6') };
|
|
145
|
+
|
|
146
|
+
/** The chip opens the note. console3 has no Notes place, so it opens the main app, where the full note is. */
|
|
147
|
+
const openNote = () => { try { window.open('/', '_blank', 'noopener'); } catch { /* a blocked pop-up */ } };
|
|
148
|
+
|
|
149
|
+
/* The engine and language the live transcript uses, on the chip (notes-recording-reliability): a meeting transcribed in the wrong
|
|
150
|
+
* language because of a setting is visible while it records. Read once per recording from GET /api/notes/engine. */
|
|
151
|
+
const ENGINE = { Whisper: 'whisper', Qwen: 'qwen' };
|
|
152
|
+
async function engineLabel() {
|
|
153
|
+
try {
|
|
154
|
+
const { pmGet } = await import('./http.js');
|
|
155
|
+
const r = await pmGet('/api/notes/engine');
|
|
156
|
+
const w = r && r.ok ? r.whisper : null;
|
|
157
|
+
if (!w || !w.live) return '';
|
|
158
|
+
const name = Object.keys(ENGINE).find((k) => ENGINE[k] === w.live) || w.live;
|
|
159
|
+
const lang = ((w.languages || []).find((x) => x.value === w.language) || {}).label || w.language || '';
|
|
160
|
+
return name + (lang ? ' · ' + lang : '');
|
|
161
|
+
} catch { return ''; }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function mountReal(host) {
|
|
165
|
+
ensureC3Css();
|
|
166
|
+
let ready = false;
|
|
167
|
+
const wrap = el('span', { class: 'c3rec', 'data-fr': 'record', 'data-shell': 'recording' });
|
|
168
|
+
host.insertBefore(wrap, host.firstChild);
|
|
169
|
+
const rec = { grp: wrap, key: '', clock: null };
|
|
170
|
+
const tabDot = (on) => {
|
|
171
|
+
for (const t of document.querySelectorAll(TAB)) {
|
|
172
|
+
let dot = t.querySelector('.shell-recdot');
|
|
173
|
+
if (!dot) { dot = el('span', { class: 'shell-recdot', 'aria-hidden': 'true' }); t.appendChild(dot); }
|
|
174
|
+
dot.hidden = !on;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const btn = (label, name, fn, attrs) => button({ label: RGL[name](), variant: 'ghost', onClick: fn, attrs: { title: label, 'aria-label': label, ...attrs } });
|
|
178
|
+
const off = (b, why) => { b.setAttribute('aria-disabled', 'true'); b.classList.add('c3rec__off'); b.title = why; b.setAttribute('aria-label', why); return b; };
|
|
179
|
+
const chipText = (st) => (st.mode === 'recording' ? (st.started ? clockOf(Date.now() - st.started) : '') : st.until ? 'paused · ' + Math.max(1, Math.ceil((st.until - Date.now()) / 60000)) + ' min left' : 'paused');
|
|
180
|
+
|
|
181
|
+
/** The arrow beside New: Untitled note, then today's meetings that have not started and have no note, the next one marked. */
|
|
182
|
+
async function openUpcoming(anchor) {
|
|
183
|
+
const { menu } = await import('./primitives.js');
|
|
184
|
+
const links = await import('./notes-links.js');
|
|
185
|
+
const evs = links.upcomingWithoutNote(await links.upcomingToday());
|
|
186
|
+
const fmt = (e, i) => {
|
|
187
|
+
const mins = Math.max(1, Math.round((Date.parse(e.start) - Date.now()) / 60000));
|
|
188
|
+
const inText = mins < 60 ? 'in ' + mins + ' min' : 'in ' + Math.floor(mins / 60) + ' h' + (mins % 60 ? ' ' + (mins % 60) + ' min' : '');
|
|
189
|
+
return { label: e.title, hint: e.time || '', detail: [i === 0 ? 'Next' : null, inText, e.with || null].filter(Boolean).join(' · '), attrs: { 'data-fr-rec-event': e.id || '' }, onSelect: () => dock.recordEvent(e) };
|
|
190
|
+
};
|
|
191
|
+
menu({ anchor, ariaLabel: 'Record a new meeting', items: [{ label: 'Untitled note', icon: 'plus', detail: 'not linked to a meeting', attrs: { 'data-fr-rec-untitled': '' }, onSelect: () => dock.recordNew() }, { sep: true },
|
|
192
|
+
...(evs.length ? evs.map(fmt) : [{ label: 'No more meetings today', attrs: { 'data-fr-rec-none': '' }, onSelect: () => {} }])] });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let eng = { id: null, text: '' };
|
|
196
|
+
const sync = () => {
|
|
197
|
+
let st; try { st = dock.recordState(); } catch { st = { mode: 'idle' }; }
|
|
198
|
+
if (st.mode !== 'idle' && eng.id !== st.id) { eng = { id: st.id, text: '' }; const want = st.id; engineLabel().then((t) => { if (eng.id === want && t) { eng.text = t; rec.key = ''; sync(); } }); }
|
|
199
|
+
const hl = st.mode === 'recording' && dock.highlightBusy ? dock.highlightBusy() : null;
|
|
200
|
+
const key = [st.mode, st.id, st.title, hl ? hl.state + hl.open : '', eng.text].join(':');
|
|
201
|
+
if (key !== rec.key) {
|
|
202
|
+
rec.key = key; rec.clock = null;
|
|
203
|
+
const recording = st.mode === 'recording';
|
|
204
|
+
const kids = [];
|
|
205
|
+
if (st.mode !== 'idle') {
|
|
206
|
+
const v = recView(st, reducedMotion());
|
|
207
|
+
rec.clock = el('span', { class: 'recclk', 'data-fr-rec-clock': '' }, chipText(st));
|
|
208
|
+
const chip = button({ variant: 'ghost', onClick: () => openNote(st.id),
|
|
209
|
+
attrs: { class: 'c3rec__chip', title: (recording ? 'Recording: ' : 'Paused: ') + v.title + '. Open the note in the main app', 'aria-label': (recording ? 'Recording ' : 'Paused: ') + v.title + '. Open the note.', 'data-fr-rec': st.mode, 'data-shell-rec-open': '' },
|
|
210
|
+
label: [el('span', { class: 'recdot' + (v.dot === 'static' ? ' static' : ''), 'data-dot': v.dot }), rec.clock, el('span', { class: 'rt' }, v.title),
|
|
211
|
+
...(eng.text ? [el('span', { class: 'c3rec__eng', 'data-fr-rec-engine': '', title: 'The live transcript: engine and language (Notes settings)' }, eng.text)] : [])] });
|
|
212
|
+
const group = [chip];
|
|
213
|
+
if (recording) {
|
|
214
|
+
group.push(btn(hl ? 'Highlight in progress: show it' : 'Highlight the last 30 seconds and what is said next', 'highlighter', () => dock.highlightNow(), { 'data-fr-rec-highlight': hl ? hl.state : '', 'data-shell-rec-mark': '' }),
|
|
215
|
+
btn('Pause (you can resume within 1 hour)', 'pause', () => dock.pauseNow(), { 'data-fr-rec-pause': '', 'data-shell-rec-pause': '' }));
|
|
216
|
+
} else group.push(btn('Resume ' + st.title, 'play', () => dock.resumeNow(), { 'data-fr-rec-resume': '', 'data-shell-rec-resume': '' }));
|
|
217
|
+
group.push(btn('Stop for good (cannot be resumed)', 'square', () => dock.stopNow(), { 'data-fr-rec-stop': '', 'data-shell-rec-stop': '' }));
|
|
218
|
+
kids.push(el('span', { class: 'recg c3rec__live' + (recording ? '' : ' paused'), role: 'group', 'aria-label': (recording ? 'Recording ' : 'Paused: ') + st.title }, group));
|
|
219
|
+
}
|
|
220
|
+
const why = 'Pause or stop the meeting being recorded to start a new one';
|
|
221
|
+
const mic = button({ variant: 'ghost', label: [RGL.mic(), el('span', { class: 'c3rec__lbl' }, 'Record')], onClick: (e) => { if (!recording) dock.recordNew(e.currentTarget); },
|
|
222
|
+
attrs: { title: 'Record an untitled note', 'aria-label': 'Record an untitled note', 'data-fr-rec-new': '' } });
|
|
223
|
+
const arrow = btn('Record for an upcoming meeting', 'down', (e) => { if (!recording) openUpcoming(e.currentTarget); }, { 'data-fr-rec-upcoming': '', 'aria-haspopup': 'menu' });
|
|
224
|
+
if (recording) { off(mic, why); off(arrow, why); }
|
|
225
|
+
kids.push(el('span', { class: 'recg idle', role: 'group', 'aria-label': 'New recording' }, mic, arrow));
|
|
226
|
+
const panel = recording && dock.highlightPanel ? dock.highlightPanel() : null;
|
|
227
|
+
if (panel) kids.push(el('div', { class: 'c3rec__pop' }, panel));
|
|
228
|
+
wrap.replaceChildren(...kids);
|
|
229
|
+
tabDot(st.mode !== 'idle');
|
|
230
|
+
}
|
|
231
|
+
if (rec.clock) rec.clock.textContent = chipText(st);
|
|
232
|
+
};
|
|
233
|
+
/* note-dock reads the notes (a meeting the server still holds open is found again) and re-draws on every state change */
|
|
234
|
+
try { dock.mountDock(); } catch { /* the dock is not available on this page */ }
|
|
235
|
+
let unsub = null;
|
|
236
|
+
import('./state.js').then((m) => { unsub = m.subscribe(sync); }).catch(() => {});
|
|
237
|
+
rec.timer = setInterval(sync, 1000);
|
|
238
|
+
rec.off = () => { if (unsub) unsub(); };
|
|
239
|
+
sync();
|
|
240
|
+
live.set(host, rec);
|
|
241
|
+
}
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
--brand-mark:oklch(0.623 0.214 259.815);
|
|
64
64
|
--brand:var(--foreground);--brand-wash:color-mix(in oklab, var(--foreground) 8%, transparent);
|
|
65
65
|
--success:oklch(0.76 0.17 152);--warning:oklch(0.82 0.15 82);--info:oklch(0.79 0.11 195);
|
|
66
|
-
--purple:oklch(0.72 0.15 300);--destructive:oklch(0.704 0.191 22.216);
|
|
66
|
+
--purple:oklch(0.72 0.15 300);--focus-window:oklch(0.72 0.15 300);--destructive:oklch(0.704 0.191 22.216);
|
|
67
67
|
--term-bg:oklch(0.148 0.004 228.8);
|
|
68
68
|
--mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
|
69
69
|
--sans:"Space Grotesk",ui-sans-serif,system-ui,sans-serif;
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
--brand-mark:oklch(0.488 0.243 264.376);
|
|
92
92
|
--brand:var(--foreground);--brand-wash:color-mix(in oklab, var(--foreground) 8%, transparent);
|
|
93
93
|
--success:oklch(0.46 0.13 152);--warning:oklch(0.48 0.11 72);--info:oklch(0.47 0.09 195);
|
|
94
|
-
--purple:oklch(0.5 0.2 300);--destructive:oklch(0.5 0.21 27.325);
|
|
94
|
+
--purple:oklch(0.5 0.2 300);--focus-window:oklch(0.5 0.2 300);--destructive:oklch(0.5 0.21 27.325);
|
|
95
95
|
--term-bg:oklch(0.148 0.004 228.8);
|
|
96
96
|
}
|
|
97
97
|
@keyframes pmblink{0%,49%{opacity:1}50%,100%{opacity:0}}
|
|
@@ -47,6 +47,8 @@ import { mdRender } from './markdown.js';
|
|
|
47
47
|
import { icon } from './icons.js';
|
|
48
48
|
import { button, iconButton, field, row, sheet, empty, menu, toast, ensureComponentsCss } from './primitives.js';
|
|
49
49
|
import { toInt16, makeResampler, openCapture } from './audio-capture.js';
|
|
50
|
+
import { startScreen } from './notes-screen-capture.js';
|
|
51
|
+
import { screenStrip } from './notes-screen-view.js';
|
|
50
52
|
import { agendaHeard, peopleOf as contextPeople } from './notes-context.js';
|
|
51
53
|
|
|
52
54
|
const SAVE_MS = 700; // a pause this long in typing saves the note
|
|
@@ -72,6 +74,8 @@ let banner = null; // { tone, text }
|
|
|
72
74
|
let speech = null; // the browser recogniser: { mode: 'dictate'|'record', id, state, rec, interim, lang, msg, last }
|
|
73
75
|
let audio = null; // local Whisper capture: { id, state, sources, ctx, streams, started, clock, msg }
|
|
74
76
|
let engine = null; // { ws, whisper, jev, at }
|
|
77
|
+
/** Capture the shared screen (notes-screen-capture): on unless the PM switched it off on this machine. */
|
|
78
|
+
const screenOn = () => !(engine && engine.whisper && engine.whisper.screen === false);
|
|
75
79
|
let showList = false;
|
|
76
80
|
let inlineDraw = false; // the live block is drawing: the fields it shows are left in place (renderLive)
|
|
77
81
|
/** A node this view keeps across redraws (an editor field, the player). The pop-up patches itself (dom.js patchChildren), so
|
|
@@ -257,6 +261,9 @@ async function loadNote(ctx, id) {
|
|
|
257
261
|
if (editor && editor.id === id && dirty) { r.note.title = editor.title.value; r.note.body = editor.body.value; }
|
|
258
262
|
r.note.__audio = r.audio || null;
|
|
259
263
|
r.note.__recording = r.recording || null;
|
|
264
|
+
r.note.__shots = r.screen ? r.screen.frames || [] : [];
|
|
265
|
+
// a new line that points at the screen, or a decision or next step, keeps the picture on screen when it was said
|
|
266
|
+
if (audio && audio.id === id && audio.screen) audio.screen.hear(r.note.transcript || []);
|
|
260
267
|
r.note.bullets_state = r.bullets_state || null;
|
|
261
268
|
cache.set(id, r.note);
|
|
262
269
|
if (!had || sig(had) !== sig(r.note)) ctx.setState({});
|
|
@@ -365,7 +372,7 @@ async function setPrivate(ctx, n, priv) {
|
|
|
365
372
|
// a private press drops what is still buffered: nothing more is sent, transcribed or summarized
|
|
366
373
|
if (priv) await stopAll(ctx, { discard: true });
|
|
367
374
|
const r = await pmPost('/api/notes/private', { id: n.id, private: priv });
|
|
368
|
-
if (r && r.ok) { n.private = r.private; if (priv) { n.__recording = null; dropPlayer(); } touchRow(n); banner = priv ? { tone: 'ok', text: 'Marked private. Listening stopped, its audio was deleted, and nothing from this note goes to Jev or to any agent.' } : null; }
|
|
375
|
+
if (r && r.ok) { n.private = r.private; if (priv) { n.__recording = null; n.__shots = []; dropPlayer(); } touchRow(n); banner = priv ? { tone: 'ok', text: 'Marked private. Listening stopped, its audio was deleted, and nothing from this note goes to Jev or to any agent.' } : null; }
|
|
369
376
|
else banner = { tone: 'bad', text: (r && r.error) || 'not changed' };
|
|
370
377
|
ctx.setState({});
|
|
371
378
|
}
|
|
@@ -595,6 +602,14 @@ function paintNow() {
|
|
|
595
602
|
const k = noteLineAt(editor && editor.id === n.id ? editor.body.value : n.body, n.line_times, ms);
|
|
596
603
|
if (k >= 0) { const nl = document.querySelector('[data-nt-noteline="' + k + '"]'); if (nl) nl.classList.add('nt-now'); }
|
|
597
604
|
}
|
|
605
|
+
async function deleteShots(ctx, n) {
|
|
606
|
+
const sure = await askConfirm({ title: 'Delete this note\'s screenshots?', body: 'The screenshots of "' + (n.title || 'Untitled note') + '" are deleted from this machine. The notes, the transcript and the audio stay. This cannot be undone.', confirmLabel: 'Delete screenshots', danger: true });
|
|
607
|
+
if (!sure) return;
|
|
608
|
+
const r = await pmPost('/api/notes/screen-delete', { id: n.id });
|
|
609
|
+
if (r && r.ok) { n.__shots = []; banner = { tone: 'ok', text: 'The screenshots were deleted. The notes, the transcript and the audio are kept.' }; }
|
|
610
|
+
else banner = { tone: 'bad', text: (r && r.error) || 'the screenshots were not deleted' };
|
|
611
|
+
ctx.setState({});
|
|
612
|
+
}
|
|
598
613
|
async function deleteAudio(ctx, n) {
|
|
599
614
|
const sure = await askConfirm({ title: 'Delete this note\'s audio?', body: 'The recording of "' + (n.title || 'Untitled note') + '" is deleted from this machine. The notes and the transcript stay. This cannot be undone.', confirmLabel: 'Delete audio', danger: true });
|
|
600
615
|
if (!sure) return;
|
|
@@ -621,16 +636,18 @@ async function cleanNow(ctx, n) {
|
|
|
621
636
|
}
|
|
622
637
|
|
|
623
638
|
/* ── listening with local Whisper ─────────────────────────────────────────────────────────────── */
|
|
624
|
-
async function openSource(kind) {
|
|
639
|
+
async function openSource(kind, { screen = false } = {}) {
|
|
625
640
|
const md = navigator.mediaDevices;
|
|
626
641
|
if (kind === 'mic') return md.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, channelCount: 1 } });
|
|
627
642
|
// The meeting is often in Zoom or another app, not in this tab: offer the whole screen first (its system audio carries every app),
|
|
628
643
|
// keep every window and tab selectable, and leave PMOS's own tab out, because it has no sound. Chrome ignores a hint it does not know.
|
|
629
644
|
const s = await md.getDisplayMedia({ video: { displaySurface: 'monitor' }, audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false }, systemAudio: 'include', monitorTypeSurfaces: 'include', surfaceSwitching: 'include', selfBrowserSurface: 'exclude' });
|
|
630
645
|
const tracks = s.getAudioTracks();
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
646
|
+
// the picture of the same share is kept for screenshots when Capture the shared screen is on (notes-screen-capture); else only the sound is used
|
|
647
|
+
const video = screen ? s.getVideoTracks()[0] || null : null;
|
|
648
|
+
for (const t of s.getVideoTracks()) if (t !== video) t.stop();
|
|
649
|
+
if (!tracks.length) { if (video) video.stop(); throw Object.assign(new Error('The shared screen, window or tab has no audio. Share it again, and tick "Also share system audio" (entire screen) or "Share tab audio" (a tab).'), { noAudio: true }); }
|
|
650
|
+
return Object.assign(new MediaStream(tracks), { screenTrack: video });
|
|
634
651
|
}
|
|
635
652
|
/** The meeting source reaches PMOS as exact silence while the microphone is heard: the other people are not being recorded. Most often the
|
|
636
653
|
* sharing window had the wrong tab (PMOS's own tab has no sound). Say so once, and take the message back if sound arrives. */
|
|
@@ -647,21 +664,23 @@ function checkSilence(ctx, a) {
|
|
|
647
664
|
ctx.setState({});
|
|
648
665
|
}
|
|
649
666
|
async function startAudio(ctx, n, want) {
|
|
650
|
-
const a = { id: n.id, state: 'starting', sources: [], streams: [], ctx: null, started: Date.now(), clock: null, msg: null, queue: {}, buf: {}, rs: {}, rs24: {}, buf24: {}, peak: {}, began: Date.now(), lost: 0, ended: 0 };
|
|
667
|
+
const a = { id: n.id, state: 'starting', sources: [], streams: [], ctx: null, started: Date.now(), clock: null, msg: null, queue: {}, buf: {}, rs: {}, rs24: {}, buf24: {}, peak: {}, began: Date.now(), lost: 0, ended: 0, cap: null, seq: {}, off: {} };
|
|
651
668
|
const opened = [];
|
|
652
669
|
// the meeting first, straight from the press: the browser lets a page share a screen only right after a click
|
|
653
670
|
for (const kind of want.slice().sort((x, y) => (x === 'meeting' ? -1 : y === 'meeting' ? 1 : 0))) {
|
|
654
|
-
try { opened.push([kind, await openSource(kind)]); }
|
|
671
|
+
try { opened.push([kind, await openSource(kind, { screen: kind === 'meeting' && screenOn() })]); }
|
|
655
672
|
catch (e) { a.msg = (a.msg ? a.msg + ' ' : '') + (kind === 'mic' ? 'The microphone was not opened' : 'The meeting audio was not opened') + ': ' + ((e && e.noAudio) ? e.message : ((e && e.message) || 'permission was refused') + '.'); }
|
|
656
673
|
}
|
|
657
674
|
await stopAll(ctx); await flushSave(ctx);
|
|
658
675
|
audio = a; if (banner && banner.audioMsg) banner = null; ctx.setState({});
|
|
659
676
|
if (!opened.length) { audio = null; banner = { tone: 'bad', text: a.msg || 'Nothing to listen to.' }; ctx.setState({}); return; }
|
|
660
677
|
const r = await pmPost('/api/notes/record', wsOf(ctx) ? { id: n.id, ws: wsOf(ctx) } : { id: n.id });
|
|
661
|
-
if (!r || !r.ok) { for (const [, s] of opened) for (const t of s.getTracks()) t.stop(); audio = null; banner = { tone: 'bad', text: (r && r.error) || 'listening did not start' }; ctx.setState({}); return; }
|
|
678
|
+
if (!r || !r.ok) { for (const [, s] of opened) { for (const t of s.getTracks()) t.stop(); if (s.screenTrack) s.screenTrack.stop(); } audio = null; banner = { tone: 'bad', text: (r && r.error) || 'listening did not start' }; ctx.setState({}); return; }
|
|
662
679
|
n.recorded_from = r.recorded_from;
|
|
663
680
|
// the clock counts the meeting time, as the line times do: Listen again carries on from where it stopped
|
|
664
681
|
a.started = Date.parse(r.clock_from || r.recorded_from) || a.started;
|
|
682
|
+
// each chunk names its capture, its sequence number and its sample offset, so the server writes it once and in its place
|
|
683
|
+
a.cap = r.capture && r.capture.token ? r.capture.token : null;
|
|
665
684
|
let ac;
|
|
666
685
|
// the device's own rate: the 16 kHz audio for transcription and the 24 kHz audio for the replay are both made from the same blocks (D92 as amended)
|
|
667
686
|
try { ac = new AudioContext(); } catch { ac = new AudioContext({ sampleRate: RATE }); }
|
|
@@ -685,6 +704,14 @@ async function startAudio(ctx, n, want) {
|
|
|
685
704
|
});
|
|
686
705
|
for (const t of stream.getTracks()) t.addEventListener('ended', () => { if (audio !== a) return; a.ended++; if (a.ended >= a.streams.length) stopAudio(ctx); });
|
|
687
706
|
}
|
|
707
|
+
// screenshots of the shared screen, on the meeting clock, while this capture lasts (notes-screen-capture)
|
|
708
|
+
const shared = opened.find(([kind, s]) => kind === 'meeting' && s.screenTrack);
|
|
709
|
+
if (shared && a.cap) {
|
|
710
|
+
const track = shared[1].screenTrack;
|
|
711
|
+
a.screen = startScreen({ track, id: a.id, cap: a.cap, clock: () => Date.now() - a.started, post: (b) => pmPost('/api/notes/screen', b) });
|
|
712
|
+
// the PM stops sharing the picture only: the sound goes on, and the shots stop
|
|
713
|
+
track.addEventListener('ended', () => { if (a.screen) a.screen.stop(); });
|
|
714
|
+
} else if (shared) shared[1].screenTrack.stop();
|
|
688
715
|
a.state = 'on';
|
|
689
716
|
a.clock = setInterval(() => { tickClocks(a.started); checkSilence(ctx, a); }, 1000);
|
|
690
717
|
ctx.setState({});
|
|
@@ -694,13 +721,16 @@ async function startAudio(ctx, n, want) {
|
|
|
694
721
|
function sendChunk(ctx, a, kind, final) {
|
|
695
722
|
const b = a.buf[kind] || []; a.buf[kind] = [];
|
|
696
723
|
const pcm = b64(Int16Array.from(b));
|
|
724
|
+
const seq = a.seq[kind] = a.seq[kind] === undefined ? 0 : a.seq[kind] + 1;
|
|
725
|
+
const off = a.off[kind] || 0; a.off[kind] = off + b.length;
|
|
726
|
+
const where = a.cap ? { cap: a.cap, seq, off } : {};
|
|
697
727
|
const b24 = a.buf24[kind] || []; a.buf24[kind] = [];
|
|
698
728
|
const pcm24 = b24.length ? b64(Int16Array.from(b24)) : undefined;
|
|
699
729
|
const seconds = b.length / RATE;
|
|
700
730
|
a.queue[kind] = a.queue[kind].then(async () => {
|
|
701
731
|
let r = null;
|
|
702
732
|
for (let tries = 0; tries < SEND_TRIES; tries++) {
|
|
703
|
-
r = await pmPost('/api/notes/audio', { id: a.id, source: kind, pcm, pcm24, final });
|
|
733
|
+
r = await pmPost('/api/notes/audio', { id: a.id, source: kind, pcm, pcm24, final, ...where });
|
|
704
734
|
if (r && r.ok) return;
|
|
705
735
|
if (r && (r.status === 409 || r.status === 404)) break; // the server refused this note's audio: sending again changes nothing
|
|
706
736
|
if (tries + 1 < SEND_TRIES) await new Promise((res) => setTimeout(res, SEND_BACKOFF_MS * 3 ** tries));
|
|
@@ -722,6 +752,7 @@ async function stopAudio(ctx, { discard = false, resumable = true } = {}) {
|
|
|
722
752
|
audio = null;
|
|
723
753
|
if (banner && banner.silent) banner = null;
|
|
724
754
|
clearInterval(a.clock);
|
|
755
|
+
if (a.screen) a.screen.stop();
|
|
725
756
|
for (const s of a.streams) for (const t of s.getTracks()) { try { t.stop(); } catch { /* stopped */ } }
|
|
726
757
|
try { if (a.ctx) await a.ctx.close(); } catch { /* closed */ }
|
|
727
758
|
if (discard) { ctx.setState({}); return; }
|
|
@@ -850,6 +881,7 @@ export function highlightNow(ctx, id) {
|
|
|
850
881
|
if (at === null) return false;
|
|
851
882
|
if (highlight && highlight.state === 'listening') { hlOpen = true; ctx.setState({}); return true; }
|
|
852
883
|
highlight = { id: n.id, title: n.title || '', at, from: Math.max(0, at - HL_BEFORE_MS), state: 'listening', pressed: Date.now(), result: null, error: null };
|
|
884
|
+
if (audio && audio.id === n.id && audio.screen) audio.screen.cue(at, 'highlight'); // the picture on screen when the PM pressed it
|
|
853
885
|
hlOpen = true;
|
|
854
886
|
clearInterval(highlightTimer);
|
|
855
887
|
highlightTimer = setInterval(() => hlTick(ctx), 500);
|
|
@@ -1003,7 +1035,7 @@ async function chooseSources(ctx, n) {
|
|
|
1003
1035
|
}
|
|
1004
1036
|
async function setSwitch(ctx, patch) {
|
|
1005
1037
|
const ws = wsOf(ctx);
|
|
1006
|
-
if (!ws && patch.language === undefined && patch.model === undefined && patch.keep_audio === undefined && patch.clean_audio === undefined) return;
|
|
1038
|
+
if (!ws && patch.language === undefined && patch.model === undefined && patch.keep_audio === undefined && patch.clean_audio === undefined && patch.screen === undefined) return;
|
|
1007
1039
|
const r = await pmPost('/api/notes/switch', ws ? { ws, ...patch } : patch);
|
|
1008
1040
|
if (r && r.ok) engine = { ws, whisper: r.whisper, jev: r.jev, at: Date.now() };
|
|
1009
1041
|
ctx.setState({});
|
|
@@ -1043,7 +1075,13 @@ function openSettings(ctx, n) {
|
|
|
1043
1075
|
w && w.keep_audio !== false ? el('p', { class: 'nt-hint', 'data-nt-clean-hint': w.ffmpeg ? 'ready' : 'missing' }, w.ffmpeg
|
|
1044
1076
|
? (w.clean_audio === true ? 'After Stop, the replay gets a cleaned copy: less rumble and hiss, and an even loudness. The transcript is not changed.'
|
|
1045
1077
|
: 'Off. When on, the replay gets a cleaned copy after Stop: less rumble and hiss, and an even loudness.')
|
|
1046
|
-
: 'This needs ffmpeg, which was not found on this machine. Install it with brew install ffmpeg, then open Settings again.') : null
|
|
1078
|
+
: 'This needs ffmpeg, which was not found on this machine. Install it with brew install ffmpeg, then open Settings again.') : null,
|
|
1079
|
+
w ? field({ label: 'Capture the shared screen', kind: 'check', value: w.screen !== false,
|
|
1080
|
+
onCommit: (v) => setSwitch(ctx, { screen: v }).then(() => { h.close(); openSettings(ctx, n); }), attrs: { 'data-nt-screen-switch': '' } }) : null,
|
|
1081
|
+
w ? el('p', { class: 'nt-hint', 'data-nt-screen-hint': w.ocr ? 'ocr' : 'no-ocr' }, w.screen !== false
|
|
1082
|
+
? 'When you share the meeting\'s screen, a screenshot is kept each time the picture changes, on this machine, beside the transcript. Nothing is sent anywhere. '
|
|
1083
|
+
+ (w.ocr ? 'The text on each screenshot is read on this machine with tesseract.' : 'To also read the text on each screenshot, install tesseract (brew install tesseract).')
|
|
1084
|
+
: 'Off. Only the sound of the shared screen is used.') : null));
|
|
1047
1085
|
const fp = w && w.final;
|
|
1048
1086
|
if (fp) {
|
|
1049
1087
|
const have = [fp.transcriber === 'qwen' ? 'Qwen3-ASR' : fp.transcriber === 'parakeet' ? 'Parakeet' : fp.transcriber === 'whisper' ? 'Whisper with beam search' : null,
|
|
@@ -1076,6 +1114,7 @@ function openSettings(ctx, n) {
|
|
|
1076
1114
|
button({ label: busy === 'agent' ? 'Starting …' : 'Summary by agent', disabled: n.private || busy === 'agent', onClick: () => { h.close(); summarizeAgent(ctx, n); }, attrs: { 'data-nt-summarize-agent': '' } }),
|
|
1077
1115
|
n.__recording || (n.final_state && n.final_state.state === 'failed') ? button({ label: 'Transcribe again', disabled: n.private || finalPending(n), onClick: () => { h.close(); runFinal(ctx, n); }, attrs: { 'data-nt-final-again': '' } }) : null,
|
|
1078
1116
|
n.__recording && w && w.clean_audio === true && w.ffmpeg && !n.__recording.cleaned ? button({ label: 'Clean up this note\'s sound', disabled: n.private || listening(n), onClick: () => { h.close(); cleanNow(ctx, n); }, attrs: { 'data-nt-clean-now': '' } }) : null,
|
|
1117
|
+
(n.__shots || []).length ? button({ label: 'Delete this note\'s screenshots', variant: 'ghost', onClick: () => { h.close(); deleteShots(ctx, n); }, attrs: { 'data-nt-delete-shots': '' } }) : null,
|
|
1079
1118
|
n.__recording ? button({ label: 'Delete this note\'s audio', variant: 'ghost', onClick: () => { h.close(); deleteAudio(ctx, n); }, attrs: { 'data-nt-delete-audio': '' } }) : null,
|
|
1080
1119
|
button({ label: 'Delete', variant: 'ghost', onClick: () => { h.close(); deleteNote(ctx, n); }, attrs: { 'data-nt-delete': '' } }))));
|
|
1081
1120
|
return parts;
|
|
@@ -1592,6 +1631,7 @@ function noteColumn(ctx, n, ed) {
|
|
|
1592
1631
|
const bs = n.bullets_state;
|
|
1593
1632
|
const parts = [];
|
|
1594
1633
|
if (n.__audio && n.__audio.error && live) parts.push(el('p', { class: 'nt-hint nt-hint--bad', 'data-nt-audio-error': '' }, n.__audio.error));
|
|
1634
|
+
if (live && audio && audio.id === n.id && audio.screen) parts.push(el('p', { class: 'nt-hint', 'data-nt-screen-live': '' }, 'The shared screen is captured each time it changes, on this machine.'));
|
|
1595
1635
|
if (!live && n.final_notes) {
|
|
1596
1636
|
// the notes written after Stop replace the live draft (Run 9)
|
|
1597
1637
|
parts.push(...finalNotesSections(ctx, n), myNotes(ctx, n, ed), wisprSection(ctx, n));
|
|
@@ -1698,6 +1738,8 @@ export function renderBoard(inner, ctx) {
|
|
|
1698
1738
|
const pl = !listening(n) ? playerFor(ctx, n) : null;
|
|
1699
1739
|
if (pl) wrap.appendChild(el('div', { class: 'nt-replay', 'data-nt-part': 'replay' }, el('span', { class: 'nt-sec__h' }, 'Replay'), pl.el,
|
|
1700
1740
|
el('span', { class: 'nt-hint' }, 'Kept on this machine. Press a time in the transcript to play from there.')));
|
|
1741
|
+
// the screenshots of the shared screen, on the replay's clock (notes-screen-capture)
|
|
1742
|
+
if (!listening(n) && !n.private && (n.__shots || []).length) wrap.appendChild(screenStrip({ id: n.id, frames: n.__shots, seek: pl ? (ms) => seek(ctx, n, ms) : null, replay: pl ? pl.el : null }));
|
|
1701
1743
|
wrap.appendChild(el('div', { class: 'nt-body2' }, noteColumn(ctx, n, ed), transcriptColumn(ctx, n)));
|
|
1702
1744
|
if (pl) queueMicrotask(paintNow);
|
|
1703
1745
|
if (((n.agent && n.agent.started && !n.agent.markdown) || finalPending(n)) && !agentTimer) pollAgent(ctx, n.id);
|