create-pmos 0.132.0 → 0.134.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/pmos.js +18 -2
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/notes.md +19 -1
- package/workspace/guide/welcome.md +38 -0
- package/workspace/lib/import-overlay.js +32 -0
- package/workspace/lib/notes-audio.js +28 -9
- package/workspace/lib/notes-capture.js +128 -0
- package/workspace/lib/notes-live-guard.js +117 -0
- package/workspace/lib/notes-whisper.js +132 -16
- package/workspace/lib/notes.js +4 -2
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-notes.js +60 -8
- package/workspace/lib/routes-onboarding.js +215 -0
- package/workspace/lib/vendor/scripts/anchor-resolve.js +351 -0
- package/workspace/lib/vendor/scripts/delivery-index.js +356 -0
- package/workspace/lib/vendor/scripts/intake.js +78 -0
- package/workspace/lib/vendor/scripts/lane-defs.js +226 -0
- package/workspace/lib/vendor/scripts/migrate-layout.js +775 -0
- package/workspace/lib/vendor/scripts/nine-stages.js +81 -0
- package/workspace/lib/vendor/scripts/onboarding/apply.js +110 -0
- package/workspace/lib/vendor/scripts/onboarding/brief.js +154 -0
- package/workspace/lib/vendor/scripts/onboarding/first-briefing.js +41 -0
- package/workspace/lib/vendor/scripts/onboarding/first-look.js +84 -0
- package/workspace/lib/vendor/scripts/onboarding/infer-brief.js +70 -0
- package/workspace/lib/vendor/scripts/onboarding/proposals.js +98 -0
- package/workspace/lib/vendor/scripts/onboarding/scan-worker.js +27 -0
- package/workspace/lib/vendor/scripts/onboarding/scan.js +859 -0
- package/workspace/lib/vendor/scripts/onboarding/structure.js +217 -0
- package/workspace/lib/vendor/scripts/plan-items-stage.js +271 -0
- package/workspace/lib/vendor/scripts/read-jsonl.js +53 -0
- package/workspace/lib/vendor/scripts/signals/layout-paths.js +43 -0
- package/workspace/lib/vendor/scripts/status-facts.js +214 -0
- package/workspace/lib/vendor/scripts/trust/import-guard.js +36 -0
- package/workspace/lib/vendor/scripts/trust/ledger.js +53 -0
- package/workspace/lib/vendor/scripts/workflow-state.js +1888 -0
- package/workspace/lib/vendor/workspace/lib/actor.js +91 -0
- package/workspace/lib/vendor/workspace/lib/board-cache.js +156 -0
- package/workspace/lib/vendor/workspace/lib/console-state.js +295 -0
- package/workspace/lib/vendor/workspace/lib/doc-category.js +227 -0
- package/workspace/lib/vendor/workspace/lib/factory-join.js +158 -0
- package/workspace/lib/vendor/workspace/lib/git-info.js +306 -0
- package/workspace/lib/vendor/workspace/lib/plan-items.js +922 -0
- package/workspace/lib/vendor/workspace/lib/plan-tree.js +419 -0
- package/workspace/lib/vendor/workspace/lib/review-gate.js +39 -0
- package/workspace/lib/vendor/workspace/lib/sink.js +321 -0
- package/workspace/lib/vendor/workspace/lib/state-root.js +587 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/calendar-views.js +25 -12
- package/workspace/public/pm/components.css +280 -0
- package/workspace/public/pm/console3.css +19 -0
- package/workspace/public/pm/shell-recording.js +122 -12
- package/workspace/public/pm/view-notes.js +7 -2
- package/workspace/public/pm/welcome.js +283 -0
- package/workspace/public/welcome.html +20 -0
- package/workspace/server.js +2 -2
- package/workspace/test-files.json +2 -0
- package/workspace/test-lists/meeting-notes.json +5 -3
- package/workspace/test-lists/onboarding-firstrun.json +15 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (.calbar, .lens, .seg, .ib), the Today chip (.tdy), the lens header (.lh), the day timeline (.vday, .vb), the month grid
|
|
4
4
|
* (.cal, .cd), the progress chart (svg.pg: progSVG, line 2712), the direction card (.fc.dir with .dial and .sevs), the key
|
|
5
5
|
* result rows (.krg, .krt) and the swipe and key gestures. The styles are pm/console3.css (see pm/console3-lift.md).
|
|
6
|
-
* This file keeps the data call and draws what the server sent. It reads GET /api/calendar-views only
|
|
6
|
+
* This file keeps the data call and draws what the server sent. It reads GET /api/calendar-views only; its Record button starts, resumes or stops a recording through pm/note-dock.js.
|
|
7
7
|
* A KR without a metric reads "no metric" and is not scored; an empty lens says why; nothing is invented.
|
|
8
8
|
*
|
|
9
9
|
* mountCalendarViews(host, ctx) is safe to call on every render. disposeCalendarViews(host) drops the host's state. */
|
|
@@ -18,6 +18,7 @@ import { renderCase } from './c3-case.js';
|
|
|
18
18
|
import { makeXp, attachExpand } from './c3-expand.js';
|
|
19
19
|
import { mountQuarter, mountYear, dispose as disposeStrategy } from './c3-cal-strategy.js'; // the Quarter and Year panes
|
|
20
20
|
import { simDock, simStart, simState, isDemo } from './shell-recording.js';
|
|
21
|
+
import * as dock from './note-dock.js';
|
|
21
22
|
import { drawCalendarPage } from './c3-calendar.js'; // the Calendar tab as a page about time (ctx.page)
|
|
22
23
|
|
|
23
24
|
/* ── shared state of the /c3.html page (console3 wave C3) ──────────────────────────────────────────────────────────
|
|
@@ -597,18 +598,30 @@ function scrubWire(v, timed, end, t0, pph) {
|
|
|
597
598
|
/* ── the stage under a day's cards on the Calendar tab: the mockup's Before, During and After (stageHtml line 2598) ── */
|
|
598
599
|
|
|
599
600
|
const autoPhase = (e) => (endH(e) <= nowH() ? 'after' : timeOf(e.at) <= nowH() ? 'during' : 'before');
|
|
600
|
-
const NOREC = 'Nothing on a page can record. Recording happens in the main app\'s note dock.';
|
|
601
601
|
const IX2 = () => (typeof window !== 'undefined' && window.__c3cal2) || null; // c3-demo-cal2.js (demo only): the After notes, the Week grid, the progress rows
|
|
602
602
|
const simOn = () => simState().mode !== 'idle';
|
|
603
|
-
const
|
|
603
|
+
const realMode = () => { try { return dock.recordState().mode; } catch { return 'idle'; } };
|
|
604
604
|
|
|
605
605
|
/* the Record button of the During and After stages (the mockup's data-a="nlive"). In demo it drives the simulated recorder, which the top bar's
|
|
606
|
-
* recording group shows (shell-recording.js). In real mode it
|
|
607
|
-
|
|
606
|
+
* recording group shows (shell-recording.js). In real mode it records through the note dock, the same recorder as the top bar's Record control:
|
|
607
|
+
* Record starts a note linked to this calendar event, Resume continues a paused meeting, Stop asks first. `ev` is the calendar cell's event. */
|
|
608
|
+
const asEvent = (ev) => ({ ...ev, id: baseId(ev), start: ev.start || ev.at, end: ev.end || null });
|
|
609
|
+
function recordBtn(m, ev, redraw) {
|
|
608
610
|
const demo = isDemo(); const on = demo && simOn();
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
611
|
+
const mode = demo ? 'idle' : realMode();
|
|
612
|
+
const label = demo ? (on ? 'Stop' : 'Record') : mode === 'recording' ? 'Stop' : mode === 'paused' ? 'Resume' : 'Record';
|
|
613
|
+
const done = () => { if (redraw) setTimeout(redraw, 300); };
|
|
614
|
+
const real = async () => {
|
|
615
|
+
if (mode === 'recording') await dock.stopNow();
|
|
616
|
+
else if (mode === 'paused') await dock.resumeNow();
|
|
617
|
+
else if (ev) await dock.recordEvent(asEvent(ev));
|
|
618
|
+
done();
|
|
619
|
+
};
|
|
620
|
+
const title = demo ? (on ? 'Stop and write final notes' : 'Record (a simulation: the demo records nothing)') : mode === 'recording' ? 'Stop and write final notes' : mode === 'paused' ? 'Resume the paused meeting' : 'Record this meeting. Notes stay on this machine.';
|
|
621
|
+
const b = c3ib(title, label === 'Stop' ? 'stop' : 'mic', { 'data-cv': 'record', 'aria-label': label,
|
|
622
|
+
onClick: () => { if (!demo) { real(); return; } if (simOn()) simDock.stopNow(); else simStart(m.title); } }, 'pri c3cv-rec');
|
|
623
|
+
b.append(label);
|
|
624
|
+
return b;
|
|
612
625
|
}
|
|
613
626
|
const liveBody = () => [el('div', { class: 'wave', 'aria-hidden': 'true' }, ...new Array(28).fill(0).map(() => el('i'))),
|
|
614
627
|
el('div', { class: 'term', 'data-cv': 'live-text' }, (IX2() && IX2().NOTES.live) || 'Dara: the keys are the real blocker, not the scope.\nYou: then we cut to three flows and keep 24 Oct.\n[highlight] decision: cut to three flows\n[caught] action: Dara confirms sandbox keys'),
|
|
@@ -618,9 +631,9 @@ const avN = (n) => el('span', { class: 'av', title: n }, String(n)[0]);
|
|
|
618
631
|
/* the After stage's notes card: the mockup's notesView (line 1494). Decisions, actions with owner and due, routing, and who was told what. */
|
|
619
632
|
function notesCard(host, r, m) {
|
|
620
633
|
const N = isDemo() && IX2() && IX2().NOTES; const redraw = () => refreshSel(host, r);
|
|
621
|
-
const head = el('div', {
|
|
634
|
+
const head = el('div', { class: 'c3cv-head' }, el('h3', null, c3ic('notes'), 'Notes'), recordBtn(m, selEvent(r), redraw));
|
|
622
635
|
if (isDemo() && simOn()) return el('section', { class: 'card', 'data-cv': 'notes' }, head, ...liveBody());
|
|
623
|
-
if (!N) return el('section', { class: 'card', 'data-cv': 'notes' }, head, el('div', { class: 'fold', 'data-cv': 'no-notes' }, c3ic('moon', 'sm'), 'No notes, decisions or actions are held for this meeting.
|
|
636
|
+
if (!N) return el('section', { class: 'card', 'data-cv': 'notes' }, head, el('div', { class: 'fold', 'data-cv': 'no-notes' }, c3ic('moon', 'sm'), 'No notes, decisions or actions are held for this meeting. Press Record to take them. Notes stay on this machine.'));
|
|
624
637
|
const nt = r.nt || (r.nt = { owners: {}, due: {}, logged: {}, diff: false, sent: false });
|
|
625
638
|
const sel = (label, opts, val, on) => el('select', { 'aria-label': label, onChange: (e) => on(e.target.value) }, ...opts.map((x) => el('option', { value: x, selected: x === val }, x || 'No owner')));
|
|
626
639
|
const small2 = 'font-size:var(--fs-2)';
|
|
@@ -681,8 +694,8 @@ function stageNode(host, r) {
|
|
|
681
694
|
} else if (ph === 'during') {
|
|
682
695
|
const sim = isDemo() && simOn();
|
|
683
696
|
body = el('section', { class: 'card', 'data-cv': 'stage-during' },
|
|
684
|
-
el('div', {
|
|
685
|
-
sim ? liveBody() :
|
|
697
|
+
el('div', { class: 'c3cv-head' }, el('h3', null, c3ic('mic'), 'Live notes'), recordBtn(m, e, () => refreshSel(host, r))),
|
|
698
|
+
sim ? liveBody() : el('div', { class: 'fold', 'data-cv': 'live-idle' }, c3ic('mic', 'sm'), realMode() === 'idle' ? 'Press Record. Notes stay on this machine.' : 'A meeting is ' + (realMode() === 'recording' ? 'recording' : 'paused') + '. Use the Record control in the top bar to mark a moment, pause or stop.'));
|
|
686
699
|
} else {
|
|
687
700
|
const D = m.demo ? [['Note to Priya: 24 Oct is conditional', 'out'], ['Note to design: scope decision', 'out']] : [];
|
|
688
701
|
const drafts = el('section', { class: 'card', 'data-cv': 'stage-after' }, el('h3', null, c3ic('spark'), 'Drafted after the meeting'),
|
|
@@ -7965,5 +7965,285 @@
|
|
|
7965
7965
|
containers). The tab strip and the scratchpad are drawn with inline radii (workspace-cockpit-nav); the inline radii
|
|
7966
7966
|
(8px tab strip, 6px controls) stay. A tab's colour dot is a dot and keeps its circle. */
|
|
7967
7967
|
|
|
7968
|
+
/* -- layouts · .wl- Welcome: the first-run page (onboarding-scan-and-map slice 2) -- */
|
|
7969
|
+
.wl-page {
|
|
7970
|
+
background: var(--background);
|
|
7971
|
+
}
|
|
7972
|
+
.wl-body {
|
|
7973
|
+
margin: 0;
|
|
7974
|
+
background: var(--background);
|
|
7975
|
+
color: var(--foreground);
|
|
7976
|
+
font-family: var(--sans);
|
|
7977
|
+
font-size: 16px;
|
|
7978
|
+
line-height: 1.5;
|
|
7979
|
+
}
|
|
7980
|
+
.wl-wrap {
|
|
7981
|
+
max-width: 720px;
|
|
7982
|
+
margin: 0 auto;
|
|
7983
|
+
padding: 24px 16px 64px;
|
|
7984
|
+
box-sizing: border-box;
|
|
7985
|
+
}
|
|
7986
|
+
.wl-wrap * {
|
|
7987
|
+
box-sizing: border-box;
|
|
7988
|
+
}
|
|
7989
|
+
.wl-steps {
|
|
7990
|
+
display: flex;
|
|
7991
|
+
flex-wrap: wrap;
|
|
7992
|
+
gap: 6px;
|
|
7993
|
+
margin: 0 0 20px;
|
|
7994
|
+
padding: 0;
|
|
7995
|
+
list-style: none;
|
|
7996
|
+
font-family: var(--mono);
|
|
7997
|
+
font-size: 12px;
|
|
7998
|
+
color: var(--muted-foreground);
|
|
7999
|
+
}
|
|
8000
|
+
.wl-steps li {
|
|
8001
|
+
padding: 2px 8px;
|
|
8002
|
+
border: 1px solid var(--border);
|
|
8003
|
+
border-radius: 6px;
|
|
8004
|
+
}
|
|
8005
|
+
.wl-steps li[aria-current="step"] {
|
|
8006
|
+
color: var(--foreground);
|
|
8007
|
+
border-color: var(--foreground);
|
|
8008
|
+
}
|
|
8009
|
+
.wl-h1 {
|
|
8010
|
+
font-size: 20px;
|
|
8011
|
+
line-height: 1.2;
|
|
8012
|
+
margin: 0 0 8px;
|
|
8013
|
+
font-weight: 600;
|
|
8014
|
+
}
|
|
8015
|
+
.wl-h2 {
|
|
8016
|
+
font-size: 16px;
|
|
8017
|
+
margin: 0;
|
|
8018
|
+
font-weight: 600;
|
|
8019
|
+
}
|
|
8020
|
+
.wl-p {
|
|
8021
|
+
margin: 0 0 12px;
|
|
8022
|
+
}
|
|
8023
|
+
.wl-muted {
|
|
8024
|
+
color: var(--muted-foreground);
|
|
8025
|
+
}
|
|
8026
|
+
.wl-card {
|
|
8027
|
+
background: var(--card);
|
|
8028
|
+
border: 1px solid var(--border);
|
|
8029
|
+
border-radius: 8px;
|
|
8030
|
+
padding: 14px 16px;
|
|
8031
|
+
margin: 0 0 14px;
|
|
8032
|
+
}
|
|
8033
|
+
.wl-path {
|
|
8034
|
+
font-family: var(--mono);
|
|
8035
|
+
font-size: 12px;
|
|
8036
|
+
color: var(--muted-foreground);
|
|
8037
|
+
word-break: break-all;
|
|
8038
|
+
}
|
|
8039
|
+
.wl-promise {
|
|
8040
|
+
font-family: var(--mono);
|
|
8041
|
+
font-size: 12px;
|
|
8042
|
+
margin: 16px 0;
|
|
8043
|
+
padding: 10px 12px;
|
|
8044
|
+
border-left: 3px solid var(--foreground);
|
|
8045
|
+
background: var(--muted);
|
|
8046
|
+
}
|
|
8047
|
+
.wl-actions {
|
|
8048
|
+
display: flex;
|
|
8049
|
+
flex-wrap: wrap;
|
|
8050
|
+
align-items: center;
|
|
8051
|
+
gap: 12px;
|
|
8052
|
+
margin-top: 18px;
|
|
8053
|
+
}
|
|
8054
|
+
.wl-btn {
|
|
8055
|
+
font: inherit;
|
|
8056
|
+
cursor: pointer;
|
|
8057
|
+
border-radius: 8px;
|
|
8058
|
+
padding: 10px 16px;
|
|
8059
|
+
min-height: 44px;
|
|
8060
|
+
border: 1px solid var(--border);
|
|
8061
|
+
background: var(--secondary);
|
|
8062
|
+
color: var(--foreground);
|
|
8063
|
+
text-decoration: none;
|
|
8064
|
+
display: inline-flex;
|
|
8065
|
+
align-items: center;
|
|
8066
|
+
justify-content: center;
|
|
8067
|
+
}
|
|
8068
|
+
.wl-btn.wl-primary {
|
|
8069
|
+
background: var(--primary);
|
|
8070
|
+
color: var(--primary-foreground);
|
|
8071
|
+
border-color: var(--primary);
|
|
8072
|
+
font-weight: 600;
|
|
8073
|
+
}
|
|
8074
|
+
.wl-btn:disabled {
|
|
8075
|
+
opacity: 0.55;
|
|
8076
|
+
cursor: default;
|
|
8077
|
+
}
|
|
8078
|
+
.wl-btn.wl-quiet {
|
|
8079
|
+
background: none;
|
|
8080
|
+
border: none;
|
|
8081
|
+
color: var(--muted-foreground);
|
|
8082
|
+
text-decoration: underline;
|
|
8083
|
+
padding: 8px 4px;
|
|
8084
|
+
min-height: 36px;
|
|
8085
|
+
}
|
|
8086
|
+
.wl-btn.wl-small {
|
|
8087
|
+
min-height: 32px;
|
|
8088
|
+
padding: 4px 10px;
|
|
8089
|
+
font-size: 14px;
|
|
8090
|
+
}
|
|
8091
|
+
.wl-btn:focus-visible,
|
|
8092
|
+
.wl-input:focus-visible,
|
|
8093
|
+
.wl-check:focus-visible {
|
|
8094
|
+
outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground));
|
|
8095
|
+
outline-offset: 2px;
|
|
8096
|
+
}
|
|
8097
|
+
.wl-spin {
|
|
8098
|
+
display: inline-block;
|
|
8099
|
+
width: 14px;
|
|
8100
|
+
height: 14px;
|
|
8101
|
+
border: 2px solid var(--border);
|
|
8102
|
+
border-top-color: var(--foreground);
|
|
8103
|
+
border-radius: 6px;
|
|
8104
|
+
animation: wlspin 0.8s linear infinite;
|
|
8105
|
+
margin-right: 8px;
|
|
8106
|
+
}
|
|
8107
|
+
@keyframes wlspin {
|
|
8108
|
+
to {
|
|
8109
|
+
transform: rotate(360deg);
|
|
8110
|
+
}
|
|
8111
|
+
}
|
|
8112
|
+
.wl-error {
|
|
8113
|
+
border: 1px solid var(--destructive);
|
|
8114
|
+
color: var(--destructive);
|
|
8115
|
+
border-radius: 8px;
|
|
8116
|
+
padding: 10px 12px;
|
|
8117
|
+
margin: 0 0 14px;
|
|
8118
|
+
}
|
|
8119
|
+
.wl-findings {
|
|
8120
|
+
margin: 0;
|
|
8121
|
+
padding-left: 22px;
|
|
8122
|
+
}
|
|
8123
|
+
.wl-findings li {
|
|
8124
|
+
margin: 0 0 12px;
|
|
8125
|
+
}
|
|
8126
|
+
.wl-ref {
|
|
8127
|
+
font-family: var(--mono);
|
|
8128
|
+
font-size: 12px;
|
|
8129
|
+
color: var(--muted-foreground);
|
|
8130
|
+
word-break: break-all;
|
|
8131
|
+
}
|
|
8132
|
+
.wl-board > header {
|
|
8133
|
+
display: flex;
|
|
8134
|
+
flex-wrap: wrap;
|
|
8135
|
+
justify-content: space-between;
|
|
8136
|
+
gap: 8px;
|
|
8137
|
+
align-items: baseline;
|
|
8138
|
+
margin-bottom: 8px;
|
|
8139
|
+
}
|
|
8140
|
+
.wl-row {
|
|
8141
|
+
display: grid;
|
|
8142
|
+
grid-template-columns: auto 1fr;
|
|
8143
|
+
gap: 6px 10px;
|
|
8144
|
+
padding: 10px 0;
|
|
8145
|
+
border-top: 1px solid var(--border);
|
|
8146
|
+
align-items: start;
|
|
8147
|
+
}
|
|
8148
|
+
.wl-check {
|
|
8149
|
+
width: 20px;
|
|
8150
|
+
height: 20px;
|
|
8151
|
+
margin: 10px 0 0;
|
|
8152
|
+
}
|
|
8153
|
+
.wl-input {
|
|
8154
|
+
font: inherit;
|
|
8155
|
+
color: var(--foreground);
|
|
8156
|
+
background: var(--background);
|
|
8157
|
+
border: 1px solid var(--input);
|
|
8158
|
+
border-radius: 6px;
|
|
8159
|
+
padding: 6px 8px;
|
|
8160
|
+
min-height: 36px;
|
|
8161
|
+
max-width: 100%;
|
|
8162
|
+
}
|
|
8163
|
+
.wl-row .wl-input[type="text"] {
|
|
8164
|
+
width: 100%;
|
|
8165
|
+
}
|
|
8166
|
+
.wl-meta {
|
|
8167
|
+
display: flex;
|
|
8168
|
+
flex-wrap: wrap;
|
|
8169
|
+
gap: 6px 10px;
|
|
8170
|
+
align-items: center;
|
|
8171
|
+
font-family: var(--mono);
|
|
8172
|
+
font-size: 12px;
|
|
8173
|
+
color: var(--muted-foreground);
|
|
8174
|
+
}
|
|
8175
|
+
.wl-chip {
|
|
8176
|
+
border: 1px solid var(--border);
|
|
8177
|
+
border-radius: 6px;
|
|
8178
|
+
padding: 1px 8px;
|
|
8179
|
+
}
|
|
8180
|
+
.wl-docs {
|
|
8181
|
+
margin: 4px 0 0;
|
|
8182
|
+
padding-left: 18px;
|
|
8183
|
+
font-family: var(--mono);
|
|
8184
|
+
font-size: 12px;
|
|
8185
|
+
color: var(--muted-foreground);
|
|
8186
|
+
word-break: break-all;
|
|
8187
|
+
}
|
|
8188
|
+
.wl-q {
|
|
8189
|
+
margin: 0 0 14px;
|
|
8190
|
+
}
|
|
8191
|
+
.wl-label {
|
|
8192
|
+
display: block;
|
|
8193
|
+
margin: 4px 0;
|
|
8194
|
+
}
|
|
8195
|
+
.wl-q .wl-free {
|
|
8196
|
+
margin-top: 10px;
|
|
8197
|
+
color: var(--muted-foreground);
|
|
8198
|
+
}
|
|
8199
|
+
.wl-q .wl-free .wl-input {
|
|
8200
|
+
display: block;
|
|
8201
|
+
margin-top: 4px;
|
|
8202
|
+
}
|
|
8203
|
+
.wl-added {
|
|
8204
|
+
margin: 8px 0 0;
|
|
8205
|
+
padding-left: 18px;
|
|
8206
|
+
font-size: 14px;
|
|
8207
|
+
}
|
|
8208
|
+
.wl-sources {
|
|
8209
|
+
list-style: none;
|
|
8210
|
+
margin: 0 0 12px;
|
|
8211
|
+
padding: 0;
|
|
8212
|
+
display: grid;
|
|
8213
|
+
gap: 12px;
|
|
8214
|
+
}
|
|
8215
|
+
.wl-source {
|
|
8216
|
+
display: flex;
|
|
8217
|
+
flex-wrap: wrap;
|
|
8218
|
+
justify-content: space-between;
|
|
8219
|
+
align-items: center;
|
|
8220
|
+
gap: 12px;
|
|
8221
|
+
padding-bottom: 12px;
|
|
8222
|
+
border-bottom: 1px solid var(--border);
|
|
8223
|
+
}
|
|
8224
|
+
.wl-source-text {
|
|
8225
|
+
flex: 1 1 260px;
|
|
8226
|
+
min-width: 0;
|
|
8227
|
+
}
|
|
8228
|
+
.wl-source-actions {
|
|
8229
|
+
display: flex;
|
|
8230
|
+
flex-wrap: wrap;
|
|
8231
|
+
align-items: center;
|
|
8232
|
+
gap: 8px;
|
|
8233
|
+
}
|
|
8234
|
+
@media (max-width: 520px) {
|
|
8235
|
+
.wl-actions {
|
|
8236
|
+
flex-direction: column;
|
|
8237
|
+
align-items: stretch;
|
|
8238
|
+
}
|
|
8239
|
+
.wl-actions .wl-btn {
|
|
8240
|
+
width: 100%;
|
|
8241
|
+
}
|
|
8242
|
+
.wl-actions .wl-btn.wl-quiet {
|
|
8243
|
+
width: auto;
|
|
8244
|
+
align-self: center;
|
|
8245
|
+
}
|
|
8246
|
+
}
|
|
8247
|
+
|
|
7968
8248
|
/* -- layouts · .cal3- the console3 Calendar page (calendar-time-page): its rules sit in console3.css beside the .vday rules they extend -- */
|
|
7969
8249
|
/* -- end of the layout sections: add nothing below this line -- */
|
|
@@ -1699,6 +1699,25 @@
|
|
|
1699
1699
|
.c3 .c3hold .hb.off{opacity:.55;cursor:not-allowed}
|
|
1700
1700
|
.c3 .c3hold .hint{font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
|
|
1701
1701
|
.c3 .c3hold .cq{display:flex;gap:var(--sp-2);align-items:center;flex-wrap:wrap;font:var(--fs-2) var(--mono)}
|
|
1702
|
+
/* The Record control, always in the top bar (shell-recording.js, real mode). Left, while a meeting records or is paused: the red .recg group
|
|
1703
|
+
* with the chip, Highlight, Pause or Resume, and Stop. Right, always: the neutral .recg.idle group with New and the arrow that lists today's meetings. */
|
|
1704
|
+
.c3 .c3rec{position:relative;display:inline-flex;align-items:stretch;gap:6px;z-index:31}
|
|
1705
|
+
.c3 .c3rec .recg.idle{color:var(--foreground);border-color:var(--border);background:var(--card)}
|
|
1706
|
+
.c3 .c3rec .recg.idle>*+*{border-left-color:var(--border)}
|
|
1707
|
+
.c3 .c3rec .recg.idle .ic{color:var(--destructive)}
|
|
1708
|
+
.c3 .c3rec .recg .ic{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
1709
|
+
.c3 .c3rec .recg .c3rec__chip{max-width:44ch}
|
|
1710
|
+
.c3 .c3rec .c3rec__eng{margin-left:6px;font-size:11px;color:var(--muted-foreground);white-space:nowrap}
|
|
1711
|
+
.c3.ph .c3rec .c3rec__eng{display:none}
|
|
1712
|
+
.c3 .c3rec .c3rec__off{opacity:.45;cursor:not-allowed}
|
|
1713
|
+
@media (hover:hover){.c3 .c3rec .recg.idle button:hover{background:var(--muted)}.c3 .c3rec .c3rec__off:hover{background:none}}
|
|
1714
|
+
.c3 .c3rec .recg button>span{display:inline-flex;align-items:center;gap:6px;min-width:0}
|
|
1715
|
+
/* the calendar's Record button (calendar-views.js recordBtn) and the card heads it sits in */
|
|
1716
|
+
.c3 .ib.c3cv-rec{width:auto;padding:0 14px;gap:6px;display:inline-flex;align-items:center}
|
|
1717
|
+
.c3 .c3cv-head{display:flex;justify-content:space-between;align-items:center;gap:10px}
|
|
1718
|
+
.c3 .c3rec .c3rec__pop{position:absolute;top:calc(100% + 6px);right:0;z-index:40;min-width:280px}
|
|
1719
|
+
.c3.ph .c3rec .c3rec__lbl{display:none}
|
|
1720
|
+
.c3.ph .c3rec .c3rec__live{order:-1}
|
|
1702
1721
|
/* ===== the Work page (pm/pm-work-view.js, initiative console3-work-page): Focus, Upcoming by day, the weekly Review, offline work and the
|
|
1703
1722
|
* shared actions. Tokens only; corners 6px on a control and 8px on a container. */
|
|
1704
1723
|
.c3 .wk-min{min-width:0}
|
|
@@ -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, and it shows only while
|
|
12
|
+
* 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();
|
|
@@ -50,7 +54,8 @@ export function mountRecording(host, opts) {
|
|
|
50
54
|
if (!host) return;
|
|
51
55
|
disposeRecording(host);
|
|
52
56
|
ensureC3Css();
|
|
53
|
-
|
|
57
|
+
if (!(opts && opts.dock) && !isDemo()) return mountReal(host);
|
|
58
|
+
const d = (opts && opts.dock) || simDock;
|
|
54
59
|
const nowMs = () => (d.now ? d.now() : Date.now());
|
|
55
60
|
const grp = el('span', { class: 'recg', role: 'group', 'aria-label': 'Recording', hidden: true, 'data-shell': 'recording' });
|
|
56
61
|
host.insertBefore(grp, host.firstChild);
|
|
@@ -80,7 +85,8 @@ export function mountRecording(host, opts) {
|
|
|
80
85
|
else {
|
|
81
86
|
rec.clock = el('span', { class: 'recclk', 'data-shell-rec-clock': '' });
|
|
82
87
|
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)
|
|
88
|
+
el('span', { class: 'recdot' + (v.dot === 'static' ? ' static' : ''), 'data-dot': v.dot }), rec.clock, el('span', { class: 'rt' }, v.title),
|
|
89
|
+
...(eng.text ? [el('span', { class: 'c3rec__eng', 'data-fr-rec-engine': '', title: 'The live transcript: engine and language (Notes settings)' }, eng.text)] : []));
|
|
84
90
|
const kids = [open];
|
|
85
91
|
if (v.canMark) kids.push(btn('Mark this moment', 'highlighter', () => d.highlightNow(), 'data-shell-rec-mark'));
|
|
86
92
|
if (v.canPause) kids.push(btn('Pause', 'pause', () => d.pauseNow(), 'data-shell-rec-pause'));
|
|
@@ -103,8 +109,112 @@ export function disposeRecording(host) {
|
|
|
103
109
|
const rec = host && live.get(host);
|
|
104
110
|
if (!rec) return;
|
|
105
111
|
clearInterval(rec.timer);
|
|
112
|
+
if (rec.off) rec.off();
|
|
106
113
|
if (rec.onSim) window.removeEventListener('c3-sim-rec', rec.onSim);
|
|
107
114
|
rec.grp.remove();
|
|
108
115
|
for (const t of document.querySelectorAll(TAB)) { const dot = t.querySelector('.shell-recdot'); if (dot) dot.remove(); }
|
|
109
116
|
live.delete(host);
|
|
110
117
|
}
|
|
118
|
+
|
|
119
|
+
/* ── real mode: the always-visible Record control ─────────────────────────────────────────────────────────────────────────────────── */
|
|
120
|
+
|
|
121
|
+
const NS_SVG = 'http://www.w3.org/2000/svg';
|
|
122
|
+
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; };
|
|
123
|
+
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') };
|
|
124
|
+
|
|
125
|
+
/** The chip opens the note. console3 has no Notes place, so it opens the main app, where the full note is. */
|
|
126
|
+
const openNote = () => { try { window.open('/', '_blank', 'noopener'); } catch { /* a blocked pop-up */ } };
|
|
127
|
+
|
|
128
|
+
/* The engine and language the live transcript uses, on the chip (notes-recording-reliability): a meeting transcribed in the wrong
|
|
129
|
+
* language because of a setting is visible while it records. Read once per recording from GET /api/notes/engine. */
|
|
130
|
+
const ENGINE = { Whisper: 'whisper', Qwen: 'qwen' };
|
|
131
|
+
async function engineLabel() {
|
|
132
|
+
try {
|
|
133
|
+
const { pmGet } = await import('./http.js');
|
|
134
|
+
const r = await pmGet('/api/notes/engine');
|
|
135
|
+
const w = r && r.ok ? r.whisper : null;
|
|
136
|
+
if (!w || !w.live) return '';
|
|
137
|
+
const name = Object.keys(ENGINE).find((k) => ENGINE[k] === w.live) || w.live;
|
|
138
|
+
const lang = ((w.languages || []).find((x) => x.value === w.language) || {}).label || w.language || '';
|
|
139
|
+
return name + (lang ? ' · ' + lang : '');
|
|
140
|
+
} catch { return ''; }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function mountReal(host) {
|
|
144
|
+
ensureC3Css();
|
|
145
|
+
let ready = false;
|
|
146
|
+
const wrap = el('span', { class: 'c3rec', 'data-fr': 'record', 'data-shell': 'recording' });
|
|
147
|
+
host.insertBefore(wrap, host.firstChild);
|
|
148
|
+
const rec = { grp: wrap, key: '', clock: null };
|
|
149
|
+
const tabDot = (on) => {
|
|
150
|
+
for (const t of document.querySelectorAll(TAB)) {
|
|
151
|
+
let dot = t.querySelector('.shell-recdot');
|
|
152
|
+
if (!dot) { dot = el('span', { class: 'shell-recdot', 'aria-hidden': 'true' }); t.appendChild(dot); }
|
|
153
|
+
dot.hidden = !on;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const btn = (label, name, fn, attrs) => button({ label: RGL[name](), variant: 'ghost', onClick: fn, attrs: { title: label, 'aria-label': label, ...attrs } });
|
|
157
|
+
const off = (b, why) => { b.setAttribute('aria-disabled', 'true'); b.classList.add('c3rec__off'); b.title = why; b.setAttribute('aria-label', why); return b; };
|
|
158
|
+
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');
|
|
159
|
+
|
|
160
|
+
/** The arrow beside New: Untitled note, then today's meetings that have not started and have no note, the next one marked. */
|
|
161
|
+
async function openUpcoming(anchor) {
|
|
162
|
+
const { menu } = await import('./primitives.js');
|
|
163
|
+
const links = await import('./notes-links.js');
|
|
164
|
+
const evs = links.upcomingWithoutNote(await links.upcomingToday());
|
|
165
|
+
const fmt = (e, i) => {
|
|
166
|
+
const mins = Math.max(1, Math.round((Date.parse(e.start) - Date.now()) / 60000));
|
|
167
|
+
const inText = mins < 60 ? 'in ' + mins + ' min' : 'in ' + Math.floor(mins / 60) + ' h' + (mins % 60 ? ' ' + (mins % 60) + ' min' : '');
|
|
168
|
+
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) };
|
|
169
|
+
};
|
|
170
|
+
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 },
|
|
171
|
+
...(evs.length ? evs.map(fmt) : [{ label: 'No more meetings today', attrs: { 'data-fr-rec-none': '' }, onSelect: () => {} }])] });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let eng = { id: null, text: '' };
|
|
175
|
+
const sync = () => {
|
|
176
|
+
let st; try { st = dock.recordState(); } catch { st = { mode: 'idle' }; }
|
|
177
|
+
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(); } }); }
|
|
178
|
+
const hl = st.mode === 'recording' && dock.highlightBusy ? dock.highlightBusy() : null;
|
|
179
|
+
const key = [st.mode, st.id, st.title, hl ? hl.state + hl.open : '', eng.text].join(':');
|
|
180
|
+
if (key !== rec.key) {
|
|
181
|
+
rec.key = key; rec.clock = null;
|
|
182
|
+
const recording = st.mode === 'recording';
|
|
183
|
+
const kids = [];
|
|
184
|
+
if (st.mode !== 'idle') {
|
|
185
|
+
const v = recView(st, reducedMotion());
|
|
186
|
+
rec.clock = el('span', { class: 'recclk', 'data-fr-rec-clock': '' }, chipText(st));
|
|
187
|
+
const chip = button({ variant: 'ghost', onClick: () => openNote(st.id),
|
|
188
|
+
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': '' },
|
|
189
|
+
label: [el('span', { class: 'recdot' + (v.dot === 'static' ? ' static' : ''), 'data-dot': v.dot }), rec.clock, el('span', { class: 'rt' }, v.title),
|
|
190
|
+
...(eng.text ? [el('span', { class: 'c3rec__eng', 'data-fr-rec-engine': '', title: 'The live transcript: engine and language (Notes settings)' }, eng.text)] : [])] });
|
|
191
|
+
const group = [chip];
|
|
192
|
+
if (recording) {
|
|
193
|
+
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': '' }),
|
|
194
|
+
btn('Pause (you can resume within 1 hour)', 'pause', () => dock.pauseNow(), { 'data-fr-rec-pause': '', 'data-shell-rec-pause': '' }));
|
|
195
|
+
} else group.push(btn('Resume ' + st.title, 'play', () => dock.resumeNow(), { 'data-fr-rec-resume': '', 'data-shell-rec-resume': '' }));
|
|
196
|
+
group.push(btn('Stop for good (cannot be resumed)', 'square', () => dock.stopNow(), { 'data-fr-rec-stop': '', 'data-shell-rec-stop': '' }));
|
|
197
|
+
kids.push(el('span', { class: 'recg c3rec__live' + (recording ? '' : ' paused'), role: 'group', 'aria-label': (recording ? 'Recording ' : 'Paused: ') + st.title }, group));
|
|
198
|
+
}
|
|
199
|
+
const why = 'Pause or stop the meeting being recorded to start a new one';
|
|
200
|
+
const mic = button({ variant: 'ghost', label: [RGL.mic(), el('span', { class: 'c3rec__lbl' }, 'Record')], onClick: (e) => { if (!recording) dock.recordNew(e.currentTarget); },
|
|
201
|
+
attrs: { title: 'Record an untitled note', 'aria-label': 'Record an untitled note', 'data-fr-rec-new': '' } });
|
|
202
|
+
const arrow = btn('Record for an upcoming meeting', 'down', (e) => { if (!recording) openUpcoming(e.currentTarget); }, { 'data-fr-rec-upcoming': '', 'aria-haspopup': 'menu' });
|
|
203
|
+
if (recording) { off(mic, why); off(arrow, why); }
|
|
204
|
+
kids.push(el('span', { class: 'recg idle', role: 'group', 'aria-label': 'New recording' }, mic, arrow));
|
|
205
|
+
const panel = recording && dock.highlightPanel ? dock.highlightPanel() : null;
|
|
206
|
+
if (panel) kids.push(el('div', { class: 'c3rec__pop' }, panel));
|
|
207
|
+
wrap.replaceChildren(...kids);
|
|
208
|
+
tabDot(st.mode !== 'idle');
|
|
209
|
+
}
|
|
210
|
+
if (rec.clock) rec.clock.textContent = chipText(st);
|
|
211
|
+
};
|
|
212
|
+
/* note-dock reads the notes (a meeting the server still holds open is found again) and re-draws on every state change */
|
|
213
|
+
try { dock.mountDock(); } catch { /* the dock is not available on this page */ }
|
|
214
|
+
let unsub = null;
|
|
215
|
+
import('./state.js').then((m) => { unsub = m.subscribe(sync); }).catch(() => {});
|
|
216
|
+
rec.timer = setInterval(sync, 1000);
|
|
217
|
+
rec.off = () => { if (unsub) unsub(); };
|
|
218
|
+
sync();
|
|
219
|
+
live.set(host, rec);
|
|
220
|
+
}
|
|
@@ -647,7 +647,7 @@ function checkSilence(ctx, a) {
|
|
|
647
647
|
ctx.setState({});
|
|
648
648
|
}
|
|
649
649
|
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 };
|
|
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, cap: null, seq: {}, off: {} };
|
|
651
651
|
const opened = [];
|
|
652
652
|
// the meeting first, straight from the press: the browser lets a page share a screen only right after a click
|
|
653
653
|
for (const kind of want.slice().sort((x, y) => (x === 'meeting' ? -1 : y === 'meeting' ? 1 : 0))) {
|
|
@@ -662,6 +662,8 @@ async function startAudio(ctx, n, want) {
|
|
|
662
662
|
n.recorded_from = r.recorded_from;
|
|
663
663
|
// the clock counts the meeting time, as the line times do: Listen again carries on from where it stopped
|
|
664
664
|
a.started = Date.parse(r.clock_from || r.recorded_from) || a.started;
|
|
665
|
+
// each chunk names its capture, its sequence number and its sample offset, so the server writes it once and in its place
|
|
666
|
+
a.cap = r.capture && r.capture.token ? r.capture.token : null;
|
|
665
667
|
let ac;
|
|
666
668
|
// 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
669
|
try { ac = new AudioContext(); } catch { ac = new AudioContext({ sampleRate: RATE }); }
|
|
@@ -694,13 +696,16 @@ async function startAudio(ctx, n, want) {
|
|
|
694
696
|
function sendChunk(ctx, a, kind, final) {
|
|
695
697
|
const b = a.buf[kind] || []; a.buf[kind] = [];
|
|
696
698
|
const pcm = b64(Int16Array.from(b));
|
|
699
|
+
const seq = a.seq[kind] = a.seq[kind] === undefined ? 0 : a.seq[kind] + 1;
|
|
700
|
+
const off = a.off[kind] || 0; a.off[kind] = off + b.length;
|
|
701
|
+
const where = a.cap ? { cap: a.cap, seq, off } : {};
|
|
697
702
|
const b24 = a.buf24[kind] || []; a.buf24[kind] = [];
|
|
698
703
|
const pcm24 = b24.length ? b64(Int16Array.from(b24)) : undefined;
|
|
699
704
|
const seconds = b.length / RATE;
|
|
700
705
|
a.queue[kind] = a.queue[kind].then(async () => {
|
|
701
706
|
let r = null;
|
|
702
707
|
for (let tries = 0; tries < SEND_TRIES; tries++) {
|
|
703
|
-
r = await pmPost('/api/notes/audio', { id: a.id, source: kind, pcm, pcm24, final });
|
|
708
|
+
r = await pmPost('/api/notes/audio', { id: a.id, source: kind, pcm, pcm24, final, ...where });
|
|
704
709
|
if (r && r.ok) return;
|
|
705
710
|
if (r && (r.status === 409 || r.status === 404)) break; // the server refused this note's audio: sending again changes nothing
|
|
706
711
|
if (tries + 1 < SEND_TRIES) await new Promise((res) => setTimeout(res, SEND_BACKOFF_MS * 3 ** tries));
|