create-pmos 0.76.0 → 0.78.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/guide/layout.md +10 -11
- package/workspace/guide/notes.md +32 -3
- package/workspace/lib/mobile-nav.js +9 -2
- package/workspace/lib/notes-audio.js +41 -11
- package/workspace/lib/notes-diarize.py +61 -0
- package/workspace/lib/notes-final.js +293 -0
- package/workspace/lib/notes.js +268 -29
- package/workspace/lib/routes-notes.js +175 -8
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/index.html +14 -20
- package/workspace/public/pm/components.css +27 -8
- package/workspace/public/pm/mobile.css +77 -93
- package/workspace/public/pm/mobile.js +6 -2
- package/workspace/public/pm/phone-deck.js +67 -143
- package/workspace/public/pm/view-notes.js +159 -7
- package/workspace/public/vendor/lucide-sprite.js +9 -2
- package/workspace/test-lists/meeting-notes.json +5 -3
- package/workspace/test-timings.json +1 -0
- package/workspace/third-party/README.md +1 -1
|
@@ -72,6 +72,8 @@ let pendingOpen = null; // a note another page asked to open (openNote), s
|
|
|
72
72
|
let transcriptOpen = { inline: false, full: true }; // the transcript is folded in the pop-up and open on the full screen
|
|
73
73
|
let inlineSwitch = null; // the note the pop-up is switching to (renderInline), so a redraw does not start it twice
|
|
74
74
|
let popped = null; // the note the pop-up shows (pm/note-dock.js), which the Notes tab then does not draw
|
|
75
|
+
let liveTranscript = false; // after the final pass, the PM can look at the live transcript instead (Run 9)
|
|
76
|
+
let wisprTimer = null;
|
|
75
77
|
const LABELS = [['decision', 'Decision'], ['action', 'Action'], ['question', 'Question'], ['fact', 'Fact'], ['topic', 'Topic'], ['statement', 'Statement'], ['small talk', 'Small talk']];
|
|
76
78
|
|
|
77
79
|
const nowClock = (iso) => { try { return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } catch { return ''; } };
|
|
@@ -213,7 +215,7 @@ async function loadList(ctx) {
|
|
|
213
215
|
if (!currentId && list.notes.length) currentId = list.notes[0].id;
|
|
214
216
|
ctx.setState({});
|
|
215
217
|
}
|
|
216
|
-
const sig = (n) => JSON.stringify([n.transcript, n.bullets, n.bullets_state && n.bullets_state.agent, n.summary && n.summary.at, n.agent && n.agent.markdown, n.private, n.speakers, n.title, (n.versions || []).length, n.__recording && n.__recording.bytes, n.meeting, n.initiative]);
|
|
218
|
+
const sig = (n) => JSON.stringify([n.final_state, n.final && n.final.at, n.final && n.final.names, n.final_notes && n.final_notes.at, n.wispr && [n.wispr.state, n.wispr.tries, n.wispr.reason], n.transcript, n.bullets, n.bullets_state && n.bullets_state.agent, n.summary && n.summary.at, n.agent && n.agent.markdown, n.private, n.speakers, n.title, (n.versions || []).length, n.__recording && n.__recording.bytes, n.meeting, n.initiative]);
|
|
217
219
|
async function loadNote(ctx, id) {
|
|
218
220
|
const r = await pmGet('/api/notes/one?id=' + encodeURIComponent(id));
|
|
219
221
|
if (r && r.ok) {
|
|
@@ -343,6 +345,7 @@ async function summarize(ctx, n, quiet = false) {
|
|
|
343
345
|
busy = '';
|
|
344
346
|
if (r && r.ok) {
|
|
345
347
|
n.summary = r.summary; if (r.initiative !== undefined) { n.initiative = r.initiative; const c = cache.get(n.id); if (c) c.initiative = r.initiative; } banner = null;
|
|
348
|
+
if (r.final) { n.final_state = r.final; const c = cache.get(n.id); if (c) c.final_state = r.final; pollAgent(ctx, n.id); }
|
|
346
349
|
if (r.agent && r.agent.started) { n.agent = { requested_at: new Date().toISOString(), started: true, reason: '', session: r.agent.session, withheld: r.agent.withheld, markdown: null, out: r.agent.out }; pollAgent(ctx, n.id); }
|
|
347
350
|
} else if (!quiet) banner = { tone: 'bad', text: (r && r.error) || 'no summary was made' };
|
|
348
351
|
ctx.setState({});
|
|
@@ -367,11 +370,45 @@ function pollAgent(ctx, id) {
|
|
|
367
370
|
agentTimer = setTimeout(async () => {
|
|
368
371
|
agentTimer = null;
|
|
369
372
|
const n = cache.get(id);
|
|
370
|
-
if (!n || !n.agent
|
|
373
|
+
if (!n || currentId !== id || !(finalPending(n) || (n.agent && n.agent.started && !n.agent.markdown))) return;
|
|
371
374
|
const r = await loadNote(ctx, id);
|
|
372
|
-
if (r && r.ok &&
|
|
375
|
+
if (r && r.ok && (finalPending(r.note) || (r.note.agent && r.note.agent.started && !r.note.agent.markdown))) pollAgent(ctx, id);
|
|
373
376
|
}, AGENT_POLL_MS);
|
|
374
377
|
}
|
|
378
|
+
const finalPending = (n) => !!(n && n.final_state && (n.final_state.state === 'queued' || n.final_state.state === 'running'));
|
|
379
|
+
const wisprPending = (n) => !!(n && n.wispr && (n.wispr.state === 'waiting' || n.wispr.state === 'asking'));
|
|
380
|
+
/** Wispr Flow's notes come minutes after Stop: the note is read again every 20 seconds while they are awaited. */
|
|
381
|
+
function pollWispr(ctx, id) {
|
|
382
|
+
if (wisprTimer) return;
|
|
383
|
+
wisprTimer = setTimeout(async () => {
|
|
384
|
+
wisprTimer = null;
|
|
385
|
+
const n = cache.get(id);
|
|
386
|
+
if (!n || currentId !== id || !wisprPending(n)) return;
|
|
387
|
+
const r = await loadNote(ctx, id);
|
|
388
|
+
if (r && r.ok && wisprPending(r.note)) pollWispr(ctx, id);
|
|
389
|
+
}, 20000);
|
|
390
|
+
}
|
|
391
|
+
async function runFinal(ctx, n) {
|
|
392
|
+
const r = await pmPost('/api/notes/final', { id: n.id });
|
|
393
|
+
if (r && r.ok) { n.final_state = { state: 'queued' }; pollAgent(ctx, n.id); } else banner = { tone: 'bad', text: (r && r.error) || 'the final pass did not start' };
|
|
394
|
+
ctx.setState({});
|
|
395
|
+
}
|
|
396
|
+
async function finalSetup(ctx) {
|
|
397
|
+
const ws = wsOf(ctx);
|
|
398
|
+
const r = await pmPost('/api/notes/final-setup', ws ? { ws } : {});
|
|
399
|
+
banner = r && r.ok && r.started
|
|
400
|
+
? { tone: 'ok', text: 'A terminal opened with the setup commands typed in. Read them and press Enter there. The downloads are about 3 GB. Then open Settings again.' }
|
|
401
|
+
: { tone: 'bad', text: 'No terminal opened' + (r && r.reason ? ': ' + r.reason : '') + '. Run this in a terminal: ' + ((r && r.command) || '') };
|
|
402
|
+
ctx.setState({});
|
|
403
|
+
}
|
|
404
|
+
async function renameFinalSpeaker(ctx, n, key, now) {
|
|
405
|
+
const name = await askText({ title: 'Name this speaker', label: 'Name', value: now, placeholder: now, confirmLabel: 'Save' });
|
|
406
|
+
if (name == null) return;
|
|
407
|
+
const r = await pmPost('/api/notes/speaker', { id: n.id, key, name });
|
|
408
|
+
// the note may have been read again since this button was drawn, so read it once more rather than change a stale copy
|
|
409
|
+
if (r && r.ok) { await loadNote(ctx, n.id); } else banner = { tone: 'bad', text: (r && r.error) || 'not renamed' };
|
|
410
|
+
ctx.setState({});
|
|
411
|
+
}
|
|
375
412
|
async function copyMarkdown(ctx, n) {
|
|
376
413
|
const text = noteMarkdown({ ...n, title: editor ? editor.title.value : n.title, body: editor ? editor.body.value : n.body });
|
|
377
414
|
try { await navigator.clipboard.writeText(text); banner = { tone: 'ok', text: 'The note is on the clipboard as Markdown.' }; }
|
|
@@ -758,6 +795,22 @@ function openSettings(ctx, n) {
|
|
|
758
795
|
w ? el('p', { class: 'nt-hint', 'data-nt-keep-hint': '' }, w.keep_audio !== false
|
|
759
796
|
? 'The meeting audio is kept on this machine, outside every repository, until you delete it. It is never sent anywhere. Browser recognition keeps no audio.'
|
|
760
797
|
: 'No audio is kept: each clip is deleted once it is transcribed, and a meeting cannot be replayed.') : null));
|
|
798
|
+
const fp = w && w.final;
|
|
799
|
+
if (fp) {
|
|
800
|
+
const have = [fp.transcriber === 'parakeet' ? 'Parakeet' : fp.transcriber === 'whisper' ? 'Whisper with beam search' : null,
|
|
801
|
+
fp.vad ? 'speech detection' : null, fp.speakers ? 'speaker separation' : null].filter(Boolean);
|
|
802
|
+
parts.push(el('div', { class: 'nt-set', 'data-nt-final-set': fp.ready ? 'ready' : 'missing' },
|
|
803
|
+
el('div', { class: 'nt-set__head' }, 'Final notes after Stop'),
|
|
804
|
+
el('p', { class: 'nt-hint' }, fp.ready
|
|
805
|
+
? 'After Stop, the whole meeting is transcribed again on this machine (' + have.join(', ') + '), and your agent session writes the notes from it.'
|
|
806
|
+
: 'Set up local transcription first. After Stop, the whole meeting is then transcribed again on this machine, and your agent session writes the notes from it.'),
|
|
807
|
+
fp.missing.length ? el('p', { class: 'nt-hint', 'data-nt-final-missing': '' }, 'For better notes, set up ' + fp.missing.map((m) => (m === 'parakeet' ? 'Parakeet (more accurate in English)' : m === 'vad' ? 'speech detection' : 'speaker separation')).join(', ')
|
|
808
|
+
+ '. The setup installs them into the workspace\'s own folder; the downloads are about 3 GB.') : null,
|
|
809
|
+
fp.missing.length ? button({ label: 'Set up in a terminal', onClick: () => { h.close(); finalSetup(ctx); }, attrs: { 'data-nt-final-setup': '' } }) : null,
|
|
810
|
+
field({ label: 'Fetch Wispr Flow notes after the meeting', kind: 'check', value: w.wispr !== false,
|
|
811
|
+
onCommit: (v) => setSwitch(ctx, { wispr: v }).then(() => { h.close(); openSettings(ctx, n); }), attrs: { 'data-nt-wispr-switch': '' } }),
|
|
812
|
+
el('p', { class: 'nt-hint' }, 'Five minutes after Stop, your agent session looks for the same meeting in Wispr Flow and copies its notes into this note. It needs the Wispr Flow connector in Claude Code.')));
|
|
813
|
+
}
|
|
761
814
|
if (!wsOf(ctx)) parts.push(el('p', { class: 'nt-hint' }, 'Choose a repository to check notes against it.'));
|
|
762
815
|
else {
|
|
763
816
|
const jsw = field({ label: 'Label lines with Jev', kind: 'check', value: !!(j && j.jev), onCommit: (v) => setSwitch(ctx, { jev: v }).then(() => { h.close(); openSettings(ctx, n); }), attrs: { 'data-nt-sw-jev': '' } });
|
|
@@ -772,6 +825,7 @@ function openSettings(ctx, n) {
|
|
|
772
825
|
el('div', { class: 'nt-row' },
|
|
773
826
|
button({ label: n.private ? 'Mark not private' : 'Mark private', onClick: () => { h.close(); setPrivate(ctx, n, !n.private); }, attrs: { [n.private ? 'data-nt-unprivate' : 'data-nt-mark-private']: '' } }),
|
|
774
827
|
button({ label: busy === 'agent' ? 'Starting …' : 'Summary by agent', disabled: n.private || busy === 'agent', onClick: () => { h.close(); summarizeAgent(ctx, n); }, attrs: { 'data-nt-summarize-agent': '' } }),
|
|
828
|
+
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,
|
|
775
829
|
n.__recording ? button({ label: 'Delete this note\'s audio', variant: 'ghost', onClick: () => { h.close(); deleteAudio(ctx, n); }, attrs: { 'data-nt-delete-audio': '' } }) : null,
|
|
776
830
|
button({ label: 'Delete', variant: 'ghost', onClick: () => { h.close(); deleteNote(ctx, n); }, attrs: { 'data-nt-delete': '' } }))));
|
|
777
831
|
return parts;
|
|
@@ -1035,6 +1089,75 @@ function itemRow(ctx, n, key, title, x) {
|
|
|
1035
1089
|
: key === 'fact' || key === 'tagged' || key === 'point' ? null
|
|
1036
1090
|
: iconButton({ icon: 'x', ariaLabel: 'Remove from ' + title, onClick: () => unpick(ctx, n, x), size: 'sm', attrs: { 'data-nt-unpick': String(x.index) } }));
|
|
1037
1091
|
}
|
|
1092
|
+
/* ── the final notes, the final transcript and Wispr Flow's notes (Run 9) ────────────────────────────── */
|
|
1093
|
+
function finalSpeakerOf(n, key) {
|
|
1094
|
+
const nm = n.final && n.final.names && n.final.names[key];
|
|
1095
|
+
if (nm) return nm;
|
|
1096
|
+
if (key === 'mic' || key === 'meeting') return speakerOf(n, key);
|
|
1097
|
+
const m = /^(?:mic|meeting)-(\d+)$/.exec(String(key || ''));
|
|
1098
|
+
return m ? 'Speaker ' + m[1] : 'Speaker';
|
|
1099
|
+
}
|
|
1100
|
+
function timeOf(ctx, n, t, k) {
|
|
1101
|
+
if (typeof t !== 'number') return el('span', { class: 'nt-at' }, '');
|
|
1102
|
+
return n.__recording && !listening(n)
|
|
1103
|
+
? button({ label: fmtTime(t), variant: 'ghost', size: 'sm', onClick: () => seek(ctx, n, t), attrs: { 'data-nt-seek-item': k, title: 'Play from here' } })
|
|
1104
|
+
: el('span', { class: 'nt-at' }, fmtTime(t));
|
|
1105
|
+
}
|
|
1106
|
+
function finalItem(ctx, n, sec, x, i) {
|
|
1107
|
+
const extra = [x.owner ? ' · ' + x.owner : '', x.due ? ' · ' + x.due : ''].join('');
|
|
1108
|
+
return el('li', { class: 'nt-item', 'data-nt-final-item': sec + '-' + i }, timeOf(ctx, n, x.t, sec + '-' + i),
|
|
1109
|
+
el('span', { class: 'nt-item__text' }, x.text, extra ? el('span', { class: 'nt-who' }, extra) : null, el('span', { class: 'nt-quote', 'data-nt-quote': '' }, 'Said: ' + x.quote)));
|
|
1110
|
+
}
|
|
1111
|
+
function finalNotesSections(ctx, n) {
|
|
1112
|
+
const f = n.final_notes;
|
|
1113
|
+
const from = f.from === 'final' ? ' · by your agent session, from the whole recording' : ' · by your agent session';
|
|
1114
|
+
const out = [el('section', { class: 'nt-sec', 'data-nt-summary': 'final' }, el('h3', { class: 'nt-sec__h' }, 'Overview', el('span', { class: 'nt-src' }, from)),
|
|
1115
|
+
f.purpose ? el('p', { class: 'nt-para', 'data-nt-purpose': '' }, f.purpose) : null, f.overview ? el('p', { class: 'nt-para' }, f.overview) : null)];
|
|
1116
|
+
const list = (key, title, items) => el('section', { class: 'nt-sec', 'data-nt-sec': key }, el('h3', { class: 'nt-sec__h' }, title),
|
|
1117
|
+
items.length ? el('ul', { class: 'nt-items' }, items.map((x, i) => finalItem(ctx, n, key, x, i))) : el('p', { class: 'nt-hint' }, 'None.'));
|
|
1118
|
+
out.push(list('decision', 'Decisions', f.decisions || []), list('action', 'Next steps', f.actions || []), list('question', 'Open questions', f.questions || []));
|
|
1119
|
+
if ((f.topics || []).length) {
|
|
1120
|
+
out.push(el('details', { class: 'nt-sec', 'data-nt-topics': '' }, el('summary', { class: 'nt-sec__h nt-fold' }, 'Topics', el('span', { class: 'nt-src' }, ' · ' + f.topics.length)),
|
|
1121
|
+
f.topics.map((tp) => el('div', { class: 'nt-topic' }, el('b', null, tp.title), el('ul', { class: 'nt-items' }, tp.points.map((p) => el('li', { class: 'nt-item nt-item--plain' }, el('span', { class: 'nt-item__text' }, p))))))));
|
|
1122
|
+
}
|
|
1123
|
+
if (f.dropped) out.push(el('p', { class: 'nt-hint', 'data-nt-dropped': '' }, f.dropped + (f.dropped === 1 ? ' item was' : ' items were') + ' left out: no quote from the transcript supports ' + (f.dropped === 1 ? 'it.' : 'them.')));
|
|
1124
|
+
return out;
|
|
1125
|
+
}
|
|
1126
|
+
function finalStateHint(ctx, n) {
|
|
1127
|
+
const st = n.final_state;
|
|
1128
|
+
if (!st || listening(n)) return null;
|
|
1129
|
+
if (st.state === 'queued' || st.state === 'running') {
|
|
1130
|
+
return el('p', { class: 'nt-hint', 'data-nt-final-wait': st.state, role: 'status' }, 'Writing the final notes: ' + (st.step || 'waiting to transcribe the meeting again') + '. Until then, these are the notes taken live.');
|
|
1131
|
+
}
|
|
1132
|
+
if (st.state === 'failed') return el('p', { class: 'nt-hint nt-hint--bad', 'data-nt-final-failed': '' }, 'The final notes were not written: ' + (st.reason || 'no reason given') + '. These are the notes taken live.');
|
|
1133
|
+
if (st.state === 'done' && n.agent && n.agent.started && !n.agent.markdown) return el('p', { class: 'nt-hint', 'data-nt-agent-wait': '' }, 'The meeting is transcribed again. Your agent session is writing the notes.');
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
1136
|
+
const WISPR_WORDS = {
|
|
1137
|
+
waiting: 'PMOS asks Wispr Flow for its notes of this meeting five minutes after Stop.',
|
|
1138
|
+
asking: 'Your agent session is looking for this meeting in Wispr Flow.',
|
|
1139
|
+
not_ready: 'Wispr Flow has no notes for this meeting yet. PMOS asks again.',
|
|
1140
|
+
not_found: 'Wispr Flow has no meeting at this time.',
|
|
1141
|
+
no_connector: 'Your agent session has no Wispr Flow connector. Add it in Claude Code to fetch Wispr Flow notes.',
|
|
1142
|
+
};
|
|
1143
|
+
function wisprSection(ctx, n) {
|
|
1144
|
+
const w = n.wispr;
|
|
1145
|
+
if (!w || w.state === 'off' || n.private) return null;
|
|
1146
|
+
if (wisprPending(n)) pollWispr(ctx, n.id);
|
|
1147
|
+
const body = [];
|
|
1148
|
+
if (w.state === 'found') {
|
|
1149
|
+
if (w.meeting && w.meeting.title) body.push(el('p', { class: 'nt-hint' }, w.meeting.title + (w.meeting.start ? ' · ' + dayOf(w.meeting.start) + ' ' + nowClock(w.meeting.start) : '')));
|
|
1150
|
+
let blocks = [];
|
|
1151
|
+
try { blocks = mdRender(w.summary || '').blocks || []; } catch { blocks = [el('div', { class: 'nt-pre' }, w.summary || '')]; }
|
|
1152
|
+
body.push(el('div', { class: 'nt-md', 'data-nt-wispr-summary': '' }, blocks));
|
|
1153
|
+
if (w.meeting && w.meeting.share_link) {
|
|
1154
|
+
const link = w.meeting.share_link;
|
|
1155
|
+
body.push(button({ label: 'Open in Wispr Flow', variant: 'ghost', size: 'sm', onClick: () => { try { window.open(link, '_blank', 'noopener'); } catch { /* blocked */ } }, attrs: { 'data-nt-wispr-open': '' } }));
|
|
1156
|
+
}
|
|
1157
|
+
} else body.push(el('p', { class: 'nt-hint', 'data-nt-wispr-state': w.state }, WISPR_WORDS[w.state] || ('Wispr Flow notes were not fetched: ' + (w.reason || 'no reason given') + '.')));
|
|
1158
|
+
return el('details', { class: 'nt-sec', 'data-nt-part': 'wispr', open: w.state === 'found' ? null : '' },
|
|
1159
|
+
el('summary', { class: 'nt-sec__h nt-fold' }, 'Wispr Flow notes', el('span', { class: 'nt-src' }, w.state === 'found' ? ' · fetched by your agent session' : '')), body);
|
|
1160
|
+
}
|
|
1038
1161
|
function sectionOf(ctx, n, key, title, items, extra) {
|
|
1039
1162
|
if (!items.length && !extra) return null;
|
|
1040
1163
|
return el('section', { class: 'nt-sec', 'data-nt-sec': key },
|
|
@@ -1106,7 +1229,12 @@ function noteColumn(ctx, n, ed) {
|
|
|
1106
1229
|
const bs = n.bullets_state;
|
|
1107
1230
|
const parts = [];
|
|
1108
1231
|
if (n.__audio && n.__audio.error && live) parts.push(el('p', { class: 'nt-hint nt-hint--bad', 'data-nt-audio-error': '' }, n.__audio.error));
|
|
1109
|
-
if (live
|
|
1232
|
+
if (!live && n.final_notes) {
|
|
1233
|
+
// the notes written after Stop replace the live draft (Run 9)
|
|
1234
|
+
parts.push(...finalNotesSections(ctx, n), myNotes(ctx, n, ed), wisprSection(ctx, n));
|
|
1235
|
+
return el('section', { class: 'nt-notecol', 'data-nt-part': 'notes' }, parts.filter(Boolean));
|
|
1236
|
+
}
|
|
1237
|
+
if (live) parts.push(myNotes(ctx, n, ed)); else parts.push(finalStateHint(ctx, n), summarySection(ctx, n));
|
|
1110
1238
|
const each = bs && bs.agent === 'on' ? el('span', { class: 'nt-src', 'data-nt-each-minute': '' }, ' · each minute') : null;
|
|
1111
1239
|
const secs = [sectionOf(ctx, n, 'point', 'Key points', c.point, c.point.length || each ? each : null)];
|
|
1112
1240
|
for (const [k, t] of SECTIONS) secs.push(sectionOf(ctx, n, k, t, c[k]));
|
|
@@ -1117,12 +1245,35 @@ function noteColumn(ctx, n, ed) {
|
|
|
1117
1245
|
else parts.push(el('p', { class: 'nt-hint', 'data-nt-caught-hint': '' }, live ? 'Listening … Key points, decisions, next steps and questions appear here as they are said.' : (n.transcript || []).length ? 'Nothing was caught as a decision, next step or question.'
|
|
1118
1246
|
: 'Decisions, actions and questions appear here as they are said, each checked against this repository.'));
|
|
1119
1247
|
if (bs && bs.agent === 'failed' && bs.reason) parts.push(el('p', { class: 'nt-hint', 'data-nt-bullets-failed': '' }, 'Key points are not written each minute: ' + bs.reason + '. Lines are labelled on this machine.'));
|
|
1120
|
-
if (!live) parts.push(myNotes(ctx, n, ed));
|
|
1248
|
+
if (!live) parts.push(myNotes(ctx, n, ed), wisprSection(ctx, n));
|
|
1121
1249
|
return el('section', { class: 'nt-notecol', 'data-nt-part': 'notes' }, parts.filter(Boolean));
|
|
1122
1250
|
}
|
|
1251
|
+
function finalTranscript(ctx, n, opts) {
|
|
1252
|
+
const segs = n.final.segments;
|
|
1253
|
+
const typed = (n.transcript || []).filter((x) => x.source === 'typed' && !x.dropped).map((x) => ({ t: x.t, text: x.text, typed: true }));
|
|
1254
|
+
const rows = [...segs.map((g, i) => ({ ...g, i })), ...typed].sort((a, b) => (a.t || 0) - (b.t || 0));
|
|
1255
|
+
const lines = rows.map((x) => {
|
|
1256
|
+
const who = x.typed ? 'You, typed' : finalSpeakerOf(n, x.spk);
|
|
1257
|
+
const spk = x.typed ? el('span', { class: 'nt-spk nt-spk--typed', 'data-nt-spk': 'typed' }, who)
|
|
1258
|
+
: button({ label: who, variant: 'ghost', size: 'sm', onClick: () => renameFinalSpeaker(ctx, n, x.spk, who), attrs: { 'data-nt-final-spk': x.spk, title: 'Name this speaker' } });
|
|
1259
|
+
const at = n.__recording && typeof x.t === 'number'
|
|
1260
|
+
? button({ label: fmtTime(x.t), variant: 'ghost', size: 'sm', onClick: () => seek(ctx, n, x.t), attrs: { 'data-nt-seek-final': String(x.i), title: 'Play from here' } })
|
|
1261
|
+
: el('span', { class: 'nt-at' }, fmtTime(x.t));
|
|
1262
|
+
return el('div', { class: 'nt-ln', 'data-nt-final-seg': x.typed ? null : String(x.i) }, el('div', { class: 'nt-ln__meta' }, at, spk), el('div', null, x.text));
|
|
1263
|
+
});
|
|
1264
|
+
const box = el('div', { class: 'nt-lines', 'data-nt-transcript': 'final' }, lines);
|
|
1265
|
+
const where = opts.inline ? 'inline' : 'full';
|
|
1266
|
+
const how = [n.final.engine === 'parakeet' ? 'Parakeet' : 'Whisper', n.final.diarized ? 'speakers told apart' : null].filter(Boolean).join(', ');
|
|
1267
|
+
const d = el('details', { class: 'nt-trans', 'data-nt-part': 'transcript', open: transcriptOpen[where] },
|
|
1268
|
+
el('summary', { class: 'nt-sec__h nt-fold' }, 'Transcript', el('span', { class: 'nt-src' }, ' · from the whole recording (' + how + '), ' + rows.length + (rows.length === 1 ? ' line' : ' lines'))),
|
|
1269
|
+
button({ label: 'Show the live transcript', variant: 'ghost', size: 'sm', onClick: () => { liveTranscript = true; ctx.setState({}); }, attrs: { 'data-nt-live-transcript': '' } }), box);
|
|
1270
|
+
d.addEventListener('toggle', () => { transcriptOpen[where] = d.open; });
|
|
1271
|
+
return d;
|
|
1272
|
+
}
|
|
1123
1273
|
function transcriptColumn(ctx, n, opts = {}) {
|
|
1124
1274
|
const tr = n.transcript || [];
|
|
1125
1275
|
const live = listening(n);
|
|
1276
|
+
if (!live && !liveTranscript && n.final && (n.final.segments || []).length) return finalTranscript(ctx, n, opts);
|
|
1126
1277
|
const lines = tr.map((x, i) => {
|
|
1127
1278
|
if (x.source === 'typed' && x.dropped) return null; // a note line the PM removed
|
|
1128
1279
|
const lab = x.cat && x.cat !== 'small talk' ? el('span', { class: 'nt-lab', 'data-nt-cat': x.cat }, x.cat) : null;
|
|
@@ -1148,7 +1299,8 @@ function transcriptColumn(ctx, n, opts = {}) {
|
|
|
1148
1299
|
const shownN = tr.filter((x) => !(x.source === 'typed' && x.dropped)).length;
|
|
1149
1300
|
const where = opts.inline ? 'inline' : 'full';
|
|
1150
1301
|
const d = el('details', { class: 'nt-trans', 'data-nt-part': 'transcript', open: transcriptOpen[where] },
|
|
1151
|
-
el('summary', { class: 'nt-sec__h nt-fold' }, 'Transcript', el('span', { class: 'nt-src' }, ' · ' + shownN + (shownN === 1 ? ' line' : ' lines') + (live ? ' so far' : ''))),
|
|
1302
|
+
el('summary', { class: 'nt-sec__h nt-fold' }, 'Transcript', el('span', { class: 'nt-src' }, ' · ' + shownN + (shownN === 1 ? ' line' : ' lines') + (live ? ' so far' : ''))),
|
|
1303
|
+
!live && n.final && (n.final.segments || []).length ? button({ label: 'Show the final transcript', variant: 'ghost', size: 'sm', onClick: () => { liveTranscript = false; ctx.setState({}); }, attrs: { 'data-nt-final-transcript': '' } }) : null, box);
|
|
1152
1304
|
d.addEventListener('toggle', () => { transcriptOpen[where] = d.open; if (d.open) box.scrollTop = box.scrollHeight; });
|
|
1153
1305
|
if (live && transcriptOpen[where]) requestAnimationFrame(() => { box.scrollTop = box.scrollHeight; });
|
|
1154
1306
|
return d;
|
|
@@ -1190,7 +1342,7 @@ export function renderBoard(inner, ctx) {
|
|
|
1190
1342
|
el('span', { class: 'nt-hint' }, 'Kept on this machine. Press a time in the transcript to play from there.')));
|
|
1191
1343
|
wrap.appendChild(el('div', { class: 'nt-body2' }, noteColumn(ctx, n, ed), transcriptColumn(ctx, n)));
|
|
1192
1344
|
if (pl) queueMicrotask(paintNow);
|
|
1193
|
-
if (n.agent && n.agent.started && !n.agent.markdown && !agentTimer) pollAgent(ctx, n.id);
|
|
1345
|
+
if (((n.agent && n.agent.started && !n.agent.markdown) || finalPending(n)) && !agentTimer) pollAgent(ctx, n.id);
|
|
1194
1346
|
}
|
|
1195
1347
|
inner.appendChild(wrap);
|
|
1196
1348
|
keep();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* lucide-sprite.js — GENERATED by workspace/third-party/build-lucide-sprite.js from lucide-static 1.52.0.
|
|
2
|
-
* Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right, panel-top, panel-left, maximize-2, minimize-2, git-branch, clock, bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud, share-2.
|
|
2
|
+
* Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right, panel-top, panel-left, maximize-2, minimize-2, git-branch, clock, bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud, share-2, house, target, square-kanban, file-text, square-terminal, calendar, chart-column.
|
|
3
3
|
*
|
|
4
4
|
* ISC License
|
|
5
5
|
*
|
|
@@ -94,5 +94,12 @@ export const SPRITE = '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true
|
|
|
94
94
|
+ '<symbol id="lucide-git-compare" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="18" r="3" /><circle cx="6" cy="6" r="3" /><path d="M13 6h3a2 2 0 0 1 2 2v7" /><path d="M11 18H8a2 2 0 0 1-2-2V9" /></symbol>'
|
|
95
95
|
+ '<symbol id="lucide-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>'
|
|
96
96
|
+ '<symbol id="lucide-share-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /><line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>'
|
|
97
|
+
+ '<symbol id="lucide-house" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" /><path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /></symbol>'
|
|
98
|
+
+ '<symbol id="lucide-target" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" /></symbol>'
|
|
99
|
+
+ '<symbol id="lucide-square-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M8 7v7" /><path d="M12 7v4" /><path d="M16 7v9" /></symbol>'
|
|
100
|
+
+ '<symbol id="lucide-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /><path d="M14 2v5a1 1 0 0 0 1 1h5" /><path d="M10 9H8" /><path d="M16 13H8" /><path d="M16 17H8" /></symbol>'
|
|
101
|
+
+ '<symbol id="lucide-square-terminal" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 11 2-2-2-2" /><path d="M11 13h4" /><rect width="18" height="18" x="3" y="3" rx="2" ry="2" /></symbol>'
|
|
102
|
+
+ '<symbol id="lucide-calendar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v3" /><path d="M16 2v3" /><rect x="3" y="3" width="18" height="18" rx="2" /><path d="M3 9h18" /></symbol>'
|
|
103
|
+
+ '<symbol id="lucide-chart-column" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v16a2 2 0 0 0 2 2h16" /><path d="M18 17V9" /><path d="M13 17V5" /><path d="M8 17v-3" /></symbol>'
|
|
97
104
|
+ '</svg>';
|
|
98
|
-
export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right","panel-top","panel-left","maximize-2","minimize-2","git-branch","clock","bold","italic","code","link","type","heading","list","list-checks","text-quote","file-code","git-compare","cloud","share-2"];
|
|
105
|
+
export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right","panel-top","panel-left","maximize-2","minimize-2","git-branch","clock","bold","italic","code","link","type","heading","list","list-checks","text-quote","file-code","git-compare","cloud","share-2","house","target","square-kanban","file-text","square-terminal","calendar","chart-column"];
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "meeting-notes Runs 1
|
|
2
|
+
"_comment": "meeting-notes Runs 1 to 9: the Notes engine and routes (notes.test.js), local Whisper, line labels and repository checks (notes-listen.test.js), the final pass after Stop and the Wispr Flow fetch (notes-final.test.js), and the Notes view in a browser (pm-notes-test.js).",
|
|
3
3
|
"modules": [
|
|
4
4
|
"notes.test.js",
|
|
5
|
-
"notes-listen.test.js"
|
|
5
|
+
"notes-listen.test.js",
|
|
6
|
+
"notes-final.test.js"
|
|
6
7
|
],
|
|
7
8
|
"panels": [
|
|
8
9
|
"pm-notes-test.js"
|
|
@@ -10,6 +11,7 @@
|
|
|
10
11
|
"seconds": {
|
|
11
12
|
"notes.test.js": 12,
|
|
12
13
|
"pm-notes-test.js": 40,
|
|
13
|
-
"notes-listen.test.js": 10
|
|
14
|
+
"notes-listen.test.js": 10,
|
|
15
|
+
"notes-final.test.js": 6
|
|
14
16
|
}
|
|
15
17
|
}
|
|
@@ -12,7 +12,7 @@ The workspace runs with no network (D62). Two third-party browser files are comm
|
|
|
12
12
|
| Package | Version | Licence | Tarball | Tarball sha256 | Shipped file |
|
|
13
13
|
|---|---|---|---|---|---|
|
|
14
14
|
| sortablejs | 1.15.7 | MIT | https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz | `66cbfc8fa18860783584fe0c346baf6cbf3399da72d23d21053425ba232abed4` | `public/vendor/Sortable.min.js`: an unmodified copy of `package/Sortable.min.js` (45 478 bytes, sha256 `bf4241bc73fef7f11c59a283a69fe8051cdd31c6d8ff5a2b9ba219e7831fcf76`; npm integrity `sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==`) |
|
|
15
|
-
| lucide-static | 1.52.0 | ISC, plus the Feather MIT notice | https://registry.npmjs.org/lucide-static/-/lucide-static-1.52.0.tgz | `4d2aa079171e4eba4c704844b8b0c9edb6e4cfb2fb8bd86f7b14353994f04a87` | `public/vendor/lucide-sprite.js`: generated by `build-lucide-sprite.js` (
|
|
15
|
+
| lucide-static | 1.52.0 | ISC, plus the Feather MIT notice | https://registry.npmjs.org/lucide-static/-/lucide-static-1.52.0.tgz | `4d2aa079171e4eba4c704844b8b0c9edb6e4cfb2fb8bd86f7b14353994f04a87` | `public/vendor/lucide-sprite.js`: generated by `build-lucide-sprite.js` (19 585 bytes, sha256 `405c8046d0954e3c942a4897eab7dcaca4cd3e2632b40788e4dbd4d9e5475461`). It holds 55 icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, then the eight the frame's chrome draws (Run 3): settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, then globe, columns-2, arrow-up-down and arrow-left-right for the dock strip's web, split and orientation chips (Run 3, C6), then panel-top, panel-left, maximize-2, minimize-2, git-branch and clock for the dock's icon controls and session rows (0.54.2), then bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud and share-2 for the document tab's in-place editor, its Confluence copy and its connections, then house, target, square-kanban, file-text, square-terminal, calendar and chart-column for the phone's tab bar and view switches |
|
|
16
16
|
|
|
17
17
|
The licences are in this folder: [sortablejs-1.15.7.LICENSE](sortablejs-1.15.7.LICENSE) and [lucide-static-1.52.0.LICENSE](lucide-static-1.52.0.LICENSE). The sprite also carries the lucide licence text as its first comment.
|
|
18
18
|
|