@gotcos/glasses-server 6.99.16 → 6.99.20
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/CHANGELOG.md +46 -0
- package/package.json +1 -1
- package/server/index.ts +21 -0
- package/server/lib/ask-work-plan.ts +116 -0
- package/server/lib/batch-transcript-quality.ts +2 -0
- package/server/lib/drop-store.ts +16 -2
- package/server/lib/meeting-ask-runtime.ts +1 -1
- package/server/lib/meeting-batch-transcribe.ts +40 -1
- package/server/lib/ping-sources.ts +6 -2
- package/server/lib/prep-runtime.ts +915 -0
- package/server/lib/prep-store.ts +226 -0
- package/server/lib/stt-resolver.ts +26 -2
- package/server/lib/transcript-cleanup.ts +11 -2
- package/server/lib/work-orchestrator/runtime.ts +8 -2
- package/server/routes/ask-work-plan.ts +114 -0
- package/server/routes/health.ts +8 -0
- package/server/routes/prep.ts +81 -0
- package/server/routes/welcome-context.ts +29 -1
- package/server/routes/work-orchestrator.ts +57 -0
- package/server/web-control/panes/cos-chat.css +22 -0
- package/server/web-control/panes/cos-chat.js +615 -2
- package/server/web-control/panes/meetings.css +30 -0
- package/server/web-control/panes/meetings.js +230 -7
- package/shared/meeting-ask.ts +6 -2
- package/shared/prep.ts +317 -0
|
@@ -306,6 +306,36 @@
|
|
|
306
306
|
.mt-up-bar { position: absolute; left: 0; bottom: 0; height: 2px; background: var(--accent); transition: width 0.2s ease; }
|
|
307
307
|
@media (prefers-reduced-motion: reduce) { .mt-up-bar { transition: none; } }
|
|
308
308
|
#mt-lfiles .mt-files { margin-top: 0; }
|
|
309
|
+
/* 6.99.19: the area offering a prep made before the call. Its height is fixed and reserved from the first paint
|
|
310
|
+
whenever a suggestion is possible, and every state (looking, none, one prep, several, adding, added, ended) fills the
|
|
311
|
+
same box, so the answer arriving, Adding… and Added never move the footer or the Ask panel under it. */
|
|
312
|
+
#mt-lprep:empty { display: none; }
|
|
313
|
+
#mt-lprep { margin-top: 8px; }
|
|
314
|
+
.mt-prepslot { height: 96px; container-type: inline-size; }
|
|
315
|
+
.mt-prepline { display: flex; align-items: center; gap: 10px; height: 100%; box-sizing: border-box; padding: 0 6px 0 12px; border: 1px solid var(--hairline); border-radius: 9px; min-width: 0; }
|
|
316
|
+
.mt-prepline-text { flex: 1; min-width: 0; font-size: 13px; line-height: 1.35; color: var(--sub); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
|
317
|
+
.mt-prepline .cx-btn { flex: none; white-space: nowrap; height: 36px; }
|
|
318
|
+
/* Narrow (a phone): one prep reads as its whole sentence over the button. */
|
|
319
|
+
@container (max-width: 559px) {
|
|
320
|
+
.mt-prepline:not(.list):has(.cx-btn) { display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; gap: 6px; padding: 8px 8px 8px 12px; }
|
|
321
|
+
.mt-prepline:not(.list) .cx-btn { justify-self: end; }
|
|
322
|
+
}
|
|
323
|
+
/* Several preps: a short list in the same box, each row with its own Add to this meeting; it scrolls inside. */
|
|
324
|
+
.mt-prepline.list { padding: 0; align-items: stretch; }
|
|
325
|
+
.mt-preplist { list-style: none; margin: 0; padding: 2px 6px 2px 12px; width: 100%; overflow-y: auto; overscroll-behavior: contain; }
|
|
326
|
+
.mt-preplist li { display: flex; align-items: center; gap: 10px; min-height: 44px; border-bottom: 1px solid var(--hairline); }
|
|
327
|
+
.mt-preplist li:last-child { border-bottom: 0; }
|
|
328
|
+
.mt-preplist .mt-prepline-text { -webkit-line-clamp: 2; color: var(--ink); line-height: 1.3; }
|
|
329
|
+
.mt-preplist .cx-btn { flex: none; white-space: nowrap; height: 34px; }
|
|
330
|
+
/* The saved meeting's Prep: what it focused on and its files. */
|
|
331
|
+
#mt-dprep:empty { display: none; }
|
|
332
|
+
.mt-prep { margin: 12px 0; display: grid; gap: 10px; }
|
|
333
|
+
.mt-prep-item { display: grid; gap: 6px; }
|
|
334
|
+
.mt-prep-item .mt-q { margin: 0; }
|
|
335
|
+
.mt-prep-steer p { margin: 0 0 0.5em; font-size: 14px; line-height: 1.5; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
336
|
+
.mt-prep-steer p:last-child { margin-bottom: 0; }
|
|
337
|
+
.mt-prep-files { border: 1px solid var(--hairline); border-radius: 9px; }
|
|
338
|
+
.mt-prep-files li:last-child { border-bottom: 0; }
|
|
309
339
|
#mt-lfoot .mt-bar { border-top: 0; }
|
|
310
340
|
@media (min-width: 860px) {
|
|
311
341
|
.mt-turns { height: clamp(320px, calc(100dvh - 400px), 1100px); }
|
|
@@ -722,6 +722,7 @@
|
|
|
722
722
|
var o = opts || {};
|
|
723
723
|
var headers = { 'X-COS-Web': '1' };
|
|
724
724
|
if (o.body !== undefined) headers['Content-Type'] = 'application/json';
|
|
725
|
+
if (o.headers) Object.keys(o.headers).forEach(function (k) { headers[k] = o.headers[k]; });
|
|
725
726
|
// A read gives up at 20 s, as W.api's do, so a stalled Mac never freezes a poll that waits on it.
|
|
726
727
|
var method = o.method || 'GET', ctrl = method === 'GET' && typeof AbortController === 'function' ? new AbortController() : null;
|
|
727
728
|
var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, 20000) : null;
|
|
@@ -1170,7 +1171,7 @@
|
|
|
1170
1171
|
if (S.detailRow) W.setCrumb(S.detailRow.title || 'Meeting');
|
|
1171
1172
|
paintDetail();
|
|
1172
1173
|
if (!S.inbox) loadInbox();
|
|
1173
|
-
if (S.detail) { W.setCrumb(detailRow().title || 'Meeting'); detailSessions().forEach(function (sid) { panelOpen(sid); }); return; }
|
|
1174
|
+
if (S.detail) { W.setCrumb(detailRow().title || 'Meeting'); detailSessions().forEach(function (sid) { panelOpen(sid); }); loadDetailPrep(); return; }
|
|
1174
1175
|
var id = ++S.detailSeq;
|
|
1175
1176
|
S.detailLoading = true;
|
|
1176
1177
|
api('/api/meetings/detail?domain=' + encodeURIComponent(domain) + '&month=' + encodeURIComponent(month) + '&filename=' + encodeURIComponent(filename)).then(function (d) {
|
|
@@ -1180,6 +1181,7 @@
|
|
|
1180
1181
|
W.setCrumb(detailRow().title || 'Meeting');
|
|
1181
1182
|
paintDetail();
|
|
1182
1183
|
detailSessions().forEach(function (sid) { panelOpen(sid); });
|
|
1184
|
+
loadDetailPrep();
|
|
1183
1185
|
// A deep link has no list row; Control always opens one (its session, voices and captures). Read that day's list.
|
|
1184
1186
|
if (!S.detailRow && /^\d{4}-\d{2}-\d{2}$/.test(S.detail.date || '')) {
|
|
1185
1187
|
api('/api/meetings?limit=200&domain=all&day=' + S.detail.date).then(function (l) {
|
|
@@ -1189,6 +1191,7 @@
|
|
|
1189
1191
|
S.detailRow = hit;
|
|
1190
1192
|
paintDetail();
|
|
1191
1193
|
detailSessions().forEach(function (sid) { if (before.indexOf(sid) < 0) panelOpen(sid); });
|
|
1194
|
+
loadDetailPrep();
|
|
1192
1195
|
}, function () {});
|
|
1193
1196
|
}
|
|
1194
1197
|
}, function (err) {
|
|
@@ -1248,10 +1251,79 @@
|
|
|
1248
1251
|
A(sid).ctx = { saved: true, meeting: row, captureLabel: 'Original recording ' + (i + 1) + '. Questions use its saved cutoff; each recording keeps its own history.' };
|
|
1249
1252
|
return '<div class="mt-side-panel" id="mt-ask-' + safeId(sid) + '" data-sid="' + esc(sid) + '">' + sidePanelHtml(sid) + '</div>';
|
|
1250
1253
|
}).join('') +
|
|
1254
|
+
'<div id="mt-dprep">' + detailPrepHtml() + '</div>' +
|
|
1251
1255
|
'<article class="mt-doc">' + documentHtml(d) + '</article></div>';
|
|
1252
1256
|
}
|
|
1253
1257
|
pane.innerHTML = '<div class="cx mt mt-detail" data-tint="meetings">' + head + body + '</div>';
|
|
1254
1258
|
}
|
|
1259
|
+
// ── Prep in a saved meeting (6.99.19) ─────────────────
|
|
1260
|
+
// GET /api/prep/by-session/:sessionId for each of the meeting's recordings: what the prep focused on and its files,
|
|
1261
|
+
// each opening from the Prep store. Nothing shows when there is no prep, or on a server without Prep.
|
|
1262
|
+
function detailPrepKey() { return S.detailKey + '|' + detailSessions().join(','); }
|
|
1263
|
+
function loadDetailPrep() {
|
|
1264
|
+
var key = detailPrepKey(), sessions = detailSessions(), cc = W.cosChat;
|
|
1265
|
+
if (!sessions.length || !cc || typeof cc.ensureCaps !== 'function') return;
|
|
1266
|
+
if (S.dprep && S.dprep.key === key) { paintDetailPrep(); return; }
|
|
1267
|
+
S.dprep = { key: key, drafts: [] };
|
|
1268
|
+
cc.ensureCaps().then(function () {
|
|
1269
|
+
if (!prepCapsNow()) return;
|
|
1270
|
+
return Promise.all(sessions.map(function (sid) { return answer('/api/prep/by-session/' + encodeURIComponent(sid)); })).then(function (list) {
|
|
1271
|
+
list.forEach(function (r) { if (r.status === 200) notePrepInstance(r.body); });
|
|
1272
|
+
if (!S.dprep || S.dprep.key !== key) return;
|
|
1273
|
+
var seen = {}, out = [];
|
|
1274
|
+
list.forEach(function (r) {
|
|
1275
|
+
// Amendment 21: {serverInstanceId, snapshots}, one per binding to this meeting, newest first, each frozen at bind
|
|
1276
|
+
// time (title, startsAt, steer, files), never the draft as it is now. A binding taken back off (removedAt) is
|
|
1277
|
+
// not shown. Open links name the draft and the file.
|
|
1278
|
+
if (r.status !== 200 || !Array.isArray(r.body.snapshots)) return;
|
|
1279
|
+
// Several binds of one draft are kept, newest first; `current` marks the one the meeting's copies hold now.
|
|
1280
|
+
// Only that one is shown when the server marks it. A 'removed' bind (or removedAt) is never shown.
|
|
1281
|
+
var rows = r.body.snapshots.filter(function (sn) { return sn && typeof sn.draftId === 'string' && sn.state !== 'removed' && !sn.removedAt; });
|
|
1282
|
+
var hasCurrent = {};
|
|
1283
|
+
r.body.snapshots.forEach(function (sn) { if (sn && sn.current === true) hasCurrent[sn.draftId] = 1; });
|
|
1284
|
+
rows.forEach(function (sn) {
|
|
1285
|
+
if (hasCurrent[sn.draftId] && sn.current !== true) return;
|
|
1286
|
+
var k = sn.draftId + '@' + sn.draftRevision;
|
|
1287
|
+
if (seen[k]) return;
|
|
1288
|
+
seen[k] = 1;
|
|
1289
|
+
out.push({ id: sn.draftId, title: sn.title, startsAt: sn.startsAt, steer: typeof sn.steer === 'string' ? sn.steer : '', files: Array.isArray(sn.files) ? sn.files : [],
|
|
1290
|
+
applying: sn.state === 'applying', held: sn.held === true });
|
|
1291
|
+
});
|
|
1292
|
+
});
|
|
1293
|
+
S.dprep.drafts = out;
|
|
1294
|
+
paintDetailPrep();
|
|
1295
|
+
});
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
function prepItemHtml(d) {
|
|
1299
|
+
var files = (d.files || []).filter(function (x) { return x && x.status !== 'removed' && String(x.name || '').trim(); });
|
|
1300
|
+
var steer = String(d.steer || '').trim();
|
|
1301
|
+
if (!steer && !files.length && !d.held && !d.applying) return '';
|
|
1302
|
+
var at = d.startsAt ? Date.parse(d.startsAt) : NaN;
|
|
1303
|
+
var when = isFinite(at) ? new Date(at).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ', ' + clockOf(new Date(at)) : '';
|
|
1304
|
+
return '<div class="mt-prep-item"><p class="mt-q">' + esc((cleanTitle(d.title) || 'Prep') + (when ? ' · ' + when : '')) + '</p>' +
|
|
1305
|
+
// Amendment 11: a bind is written 'applying' before the meeting's files change. Said as it is, never as adopted.
|
|
1306
|
+
(d.applying ? '<p class="mt-amber-line mt-prep-state">Still being added to this meeting. These notes and files are not in its files yet.</p>' : '') +
|
|
1307
|
+
// A held note (it looked like a password or key) was not adopted; its words are not shown.
|
|
1308
|
+
(d.held ? '<p class="mt-muted mt-prep-held">Notes held: they look like a password or key.</p>' : '') +
|
|
1309
|
+
(steer ? '<div class="mt-prep-steer">' + steer.split(/\n{2,}/).map(function (para) { return '<p>' + esc(para) + '</p>'; }).join('') + '</div>' : '') +
|
|
1310
|
+
(files.length ? '<ul class="mt-files-list mt-prep-files">' + files.map(function (x) {
|
|
1311
|
+
var meta = x.status === 'ready' ? (x.notice || '') : (x.notice || FILE_STATE[x.status] || '');
|
|
1312
|
+
return '<li class="mt-up"><span class="mt-files-name">' + esc(x.name) + '</span>' + (meta ? '<span class="mt-up-meta mt-muted">' + esc(meta) + '</span>' : '') +
|
|
1313
|
+
'<span class="mt-up-acts"><a class="mt-link" href="/api/prep/drafts/' + encodeURIComponent(d.id) + '/files/' + encodeURIComponent(x.id) + '" target="_blank" rel="noopener" aria-label="' + esc('Open ' + x.name) + '">Open</a></span></li>';
|
|
1314
|
+
}).join('') + '</ul>' : '') + '</div>';
|
|
1315
|
+
}
|
|
1316
|
+
function detailPrepHtml() {
|
|
1317
|
+
var dp = S.dprep;
|
|
1318
|
+
if (!dp || !S.detail || dp.key !== detailPrepKey()) return '';
|
|
1319
|
+
var items = dp.drafts.map(prepItemHtml).filter(Boolean);
|
|
1320
|
+
return items.length ? '<section class="mt-panel mt-prep" aria-label="Prep"><h3 class="mt-ask-title">Prep</h3>' + items.join('') + '</section>' : '';
|
|
1321
|
+
}
|
|
1322
|
+
function paintDetailPrep() {
|
|
1323
|
+
var el = $('mt-dprep'); if (!el || S.view !== 'detail') return;
|
|
1324
|
+
var html = detailPrepHtml();
|
|
1325
|
+
if (el.innerHTML !== html) el.innerHTML = html;
|
|
1326
|
+
}
|
|
1255
1327
|
function paintBar() {
|
|
1256
1328
|
if (S.view !== 'detail' || !S.detail) return;
|
|
1257
1329
|
var bar = pane.querySelector('.mt-bar'); if (!bar) return;
|
|
@@ -1336,8 +1408,8 @@
|
|
|
1336
1408
|
exchanges.reduce(function (all, e) { return all.concat(e.files || []); }, []).concat(current).forEach(function (f) { var k = f.id + '@' + f.revision; if (!seen[k]) { seen[k] = 1; out.push(f); } });
|
|
1337
1409
|
return out;
|
|
1338
1410
|
}
|
|
1339
|
-
function fullContext(title, transcript, exchanges, files, cues, cueNote) {
|
|
1340
|
-
var sections = ['# ' + title, '## Transcript\n' + transcript];
|
|
1411
|
+
function fullContext(title, transcript, exchanges, files, cues, cueNote, plainTitle) {
|
|
1412
|
+
var sections = [(plainTitle ? '' : '# ') + title, '## Transcript\n' + transcript];
|
|
1341
1413
|
exchanges.forEach(function (e, i) {
|
|
1342
1414
|
var ns = 'Q' + (i + 1), ans = e.answer != null ? e.answer : 'No answer (' + e.status + ').';
|
|
1343
1415
|
(e.sources || []).forEach(function (s) { ans = ans.split('[' + s.id + ']').join('[' + ns + ':' + s.id + ']'); });
|
|
@@ -1814,7 +1886,7 @@
|
|
|
1814
1886
|
icon('clip') + '<span>' + esc(title + extra) + (bad ? '<span class="mt-amber"> \u00b7 ' + esc(bad) + '</span>' : '') + '</span>' + (openable ? icon('chev') + '</button>' : '</div>');
|
|
1815
1887
|
if (a.filesOpen && openable) {
|
|
1816
1888
|
html += '<ul class="mt-files-list">' + shown.map(function (x) {
|
|
1817
|
-
var line = [x.status !== 'ready' ? (x.notice || FILE_STATE[x.status] || '') : (x.notice || ''), x.origin === 'phone' ? 'Added from a phone or browser' : ''].filter(Boolean).join(' \u00b7 ');
|
|
1889
|
+
var line = [x.status !== 'ready' ? (x.notice || FILE_STATE[x.status] || '') : (x.notice || ''), x.origin === 'phone' ? 'Added from a phone or browser' : x.origin === 'prep' ? 'From prep' : ''].filter(Boolean).join(' \u00b7 ');
|
|
1818
1890
|
return '<li><span class="mt-files-name">' + esc(x.name) + '</span>' + (line ? '<span class="mt-muted">' + esc(line) + '</span>' : '') + '</li>';
|
|
1819
1891
|
}).join('') + ups.map(uploadRowHtml).join('') + '</ul>' +
|
|
1820
1892
|
(note ? '<p class="mt-amber-line mt-files-note" role="status">' + esc(note) + '</p>' : '') +
|
|
@@ -1916,6 +1988,8 @@
|
|
|
1916
1988
|
var hours = f.turns.some(function (t) { return t.elapsedMs >= 3600000; });
|
|
1917
1989
|
return [header].concat(f.turns.map(function (t) { return '[' + stampMs(t.elapsedMs, hours) + '] ' + t.speaker + ': ' + t.text; })).join('\n');
|
|
1918
1990
|
}
|
|
1991
|
+
// The full context's first line for a live meeting: "Live meeting: <its name>", never a bare "# Live meeting".
|
|
1992
|
+
function liveContextTitle(f) { return 'Live meeting: ' + liveTitle(f); }
|
|
1919
1993
|
function openLive(sid) {
|
|
1920
1994
|
S.view = 'live';
|
|
1921
1995
|
if (S.liveSid !== sid) { S.liveSid = sid; S.confirm = ''; S.liveCopy = ''; S.liveReason = ''; }
|
|
@@ -1930,10 +2004,10 @@
|
|
|
1930
2004
|
pane.innerHTML = '<div class="cx mt mt-livev" data-tint="meetings"><div id="mt-lhead"></div><div class="mt-lgrid"><div class="mt-lmain" id="mt-lmain">' +
|
|
1931
2005
|
'<div class="mt-tbox"><div id="mt-turns" class="mt-turns" tabindex="0" role="log" aria-live="polite" aria-label="Live transcript"></div>' +
|
|
1932
2006
|
'<button type="button" class="cx-btn quiet mt-jump" id="mt-jump" data-mt="jump" hidden>Jump to live</button></div>' +
|
|
1933
|
-
'<div id="mt-lbar" class="mt-lbarslot"></div><div id="mt-lfiles"></div><div id="mt-lfoot"></div>' +
|
|
2007
|
+
'<div id="mt-lbar" class="mt-lbarslot"></div><div id="mt-lfiles"></div><div id="mt-lprep"></div><div id="mt-lfoot"></div>' +
|
|
1934
2008
|
'<input type="file" id="mt-lpick" class="mt-pick" multiple accept="' + esc(LIVE_ACCEPT) + '" tabindex="-1" aria-hidden="true"></div>' +
|
|
1935
2009
|
'<aside class="mt-side-panel" id="mt-ask-' + safeId(sid) + '" data-sid="' + esc(sid) + '">' + sidePanelHtml(sid) + '</aside></div></div>';
|
|
1936
|
-
S.headHtml = S.footHtml = S.barHtml = S.filesHtml = null;
|
|
2010
|
+
S.headHtml = S.footHtml = S.barHtml = S.filesHtml = S.prepHtml = null;
|
|
1937
2011
|
paintLiveHead(); paintTurns(true); paintLiveFoot();
|
|
1938
2012
|
clearInterval(S.liveTimer); clearInterval(S.clockTimer);
|
|
1939
2013
|
livePoll();
|
|
@@ -2031,6 +2105,7 @@
|
|
|
2031
2105
|
if (html !== S.headHtml) { S.headHtml = html; el.innerHTML = html; }
|
|
2032
2106
|
if (W.crumb() !== liveTitle(f)) W.setCrumb(liveTitle(f));
|
|
2033
2107
|
paintLiveBar(); paintLiveFiles();
|
|
2108
|
+
suggestPrep(f); paintLivePrep();
|
|
2034
2109
|
}
|
|
2035
2110
|
|
|
2036
2111
|
// ── the recording bar (LiveRecordingBar, 0.5.339) ──────
|
|
@@ -2266,6 +2341,151 @@
|
|
|
2266
2341
|
if (html !== S.filesHtml) { S.filesHtml = html; el.innerHTML = html; }
|
|
2267
2342
|
}
|
|
2268
2343
|
function liveNote(text) { LU().note = text; if (S.liveSid) A(S.liveSid).filesOpen = true; paintLiveFiles(); }
|
|
2344
|
+
// ── Prep in the live view (6.99.19, PREP_CONTRACT_v1) ──
|
|
2345
|
+
// A prep made before the call (the chat's Prep me) can join this meeting. GET /api/prep/suggest lists up to five
|
|
2346
|
+
// drafts, closest to this recording's start first. One area under the files offers them: one prep as a line, several
|
|
2347
|
+
// as a short list, each with its own Add to this meeting. Only that tap binds (POST .../bind); its notes and files
|
|
2348
|
+
// then join the meeting's files, each marked From prep.
|
|
2349
|
+
// The area's height is reserved from the first paint whenever a suggestion is possible (Prep on, or not known yet,
|
|
2350
|
+
// and a recording that takes files), and every state (looking, none, one, several, adding, added, ended) fills the
|
|
2351
|
+
// same fixed box, so nothing under it moves when the answer arrives. The transcript box sits above it.
|
|
2352
|
+
function prepCapsNow() { var cc = W.cosChat; return cc && typeof cc.prepCaps === 'function' ? cc.prepCaps() : null; }
|
|
2353
|
+
function livePrep(sid) { if (!S.lprep || S.lprep.sid !== sid) S.lprep = { sid: sid, asked: false, state: 'idle', drafts: [], busy: '', done: null, note: '', changed: '' }; return S.lprep; }
|
|
2354
|
+
function notePrepInstance(b) { var cc = W.cosChat; if (b && cc && typeof cc.notePrepInstance === 'function') cc.notePrepInstance(b.serverInstanceId); }
|
|
2355
|
+
function prepBoundTo(d, sid) { return ((d && d.bindings) || []).some(function (b) { return b && b.sessionId === sid && !b.removedAt; }); }
|
|
2356
|
+
function prepReadyFiles(d) { return ((d && d.files) || []).filter(function (x) { return x && x.status === 'ready'; }); }
|
|
2357
|
+
function prepHasNotes(d) { return !!String((d && d.steer) || '').trim(); }
|
|
2358
|
+
// Whether this live view may offer a prep at all: the area is reserved exactly when this is true.
|
|
2359
|
+
function prepPossible(f) {
|
|
2360
|
+
var cc = W.cosChat;
|
|
2361
|
+
if (!f || !canAddFiles(f) && !(S.lprep && S.lprep.sid === f.sid && (S.lprep.done || S.lprep.state !== 'idle'))) return false;
|
|
2362
|
+
if (!cc || typeof cc.known !== 'function') return false;
|
|
2363
|
+
if (!cc.known()) return true;
|
|
2364
|
+
var caps = prepCapsNow();
|
|
2365
|
+
return !!(caps && caps.suggest && caps.bind);
|
|
2366
|
+
}
|
|
2367
|
+
function suggestPrep(f) {
|
|
2368
|
+
if (!f || f.endedAt || !f.start || !canAddFiles(f)) return;
|
|
2369
|
+
var p = livePrep(f.sid), cc = W.cosChat;
|
|
2370
|
+
if (p.asked || !cc || typeof cc.ensureCaps !== 'function') return;
|
|
2371
|
+
p.asked = true; p.state = 'checking';
|
|
2372
|
+
cc.ensureCaps().then(function () {
|
|
2373
|
+
var caps = prepCapsNow();
|
|
2374
|
+
if (!caps || !caps.suggest || !caps.bind) { p.state = 'off'; paintLivePrep(); return; }
|
|
2375
|
+
return answer('/api/prep/suggest?sessionId=' + encodeURIComponent(f.sid) + '&startedAt=' + Math.round(f.start)).then(function (r) {
|
|
2376
|
+
if (S.lprep !== p) return;
|
|
2377
|
+
notePrepInstance(r.body);
|
|
2378
|
+
p.drafts = r.status === 200 && Array.isArray(r.body.drafts) ? r.body.drafts.filter(function (d) {
|
|
2379
|
+
return d && typeof d.id === 'string' && !prepBoundTo(d, f.sid) && (prepHasNotes(d) || prepReadyFiles(d).length > 0);
|
|
2380
|
+
}).slice(0, 5) : [];
|
|
2381
|
+
p.state = r.status === 200 ? 'ready' : 'failed';
|
|
2382
|
+
paintLivePrep();
|
|
2383
|
+
});
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
function prepWhat(d) {
|
|
2387
|
+
var n = prepReadyFiles(d).length, files = n + (n === 1 ? ' file' : ' files');
|
|
2388
|
+
return prepHasNotes(d) ? (n ? 'its notes and ' + files : 'its notes') : 'its ' + files;
|
|
2389
|
+
}
|
|
2390
|
+
function prepAddBtn(p, d) {
|
|
2391
|
+
return '<button type="button" class="cx-btn quiet" data-mt="prep-bind" data-id="' + esc(d.id) + '"' + (p.busy ? ' disabled' : '') + '>' + (p.busy === d.id ? 'Adding…' : 'Add to this meeting') + '</button>';
|
|
2392
|
+
}
|
|
2393
|
+
function prepQuiet(text, warn) { return '<div class="mt-prepline" role="status"><span class="mt-prepline-text' + (warn ? ' mt-amber' : '') + '">' + esc(text) + '</span></div>'; }
|
|
2394
|
+
function livePrepHtml() {
|
|
2395
|
+
var f = S.feeds[S.liveSid];
|
|
2396
|
+
if (!prepPossible(f)) return '';
|
|
2397
|
+
var p = livePrep(f.sid), html;
|
|
2398
|
+
if (p.done) html = prepQuiet('Added ' + (cleanTitle(p.done.title) ? 'the prep for ' + cleanTitle(p.done.title) : 'the prep') + ' to this meeting. Its files say From prep.');
|
|
2399
|
+
else if (f.endedAt) html = prepQuiet(p.drafts.length ? 'The recording ended, so prep can no longer be added here.' : 'No prep was added to this meeting.');
|
|
2400
|
+
else if (p.state === 'failed') html = prepQuiet('Prep for this meeting could not be checked. It is checked again when you reopen this view.', true);
|
|
2401
|
+
else if (p.state !== 'ready') html = prepQuiet('Looking for a prep for this meeting…');
|
|
2402
|
+
else if (!p.drafts.length) html = prepQuiet('No prep for this meeting. Make one from a Prep me card in COS chat.');
|
|
2403
|
+
else if (p.drafts.length === 1) {
|
|
2404
|
+
var d = p.drafts[0], title = cleanTitle(d.title) || 'this meeting';
|
|
2405
|
+
var text = p.note || 'Prep for ' + title + (p.changed === d.id ? ' changed since it was offered: add ' : ' is ready: add ') + prepWhat(d) + ' to this meeting';
|
|
2406
|
+
html = '<div class="mt-prepline"><span class="mt-prepline-text' + (p.note ? ' mt-amber' : '') + '" title="' + esc(text) + '">' + esc(text) + '</span>' + prepAddBtn(p, d) + '</div>';
|
|
2407
|
+
} else {
|
|
2408
|
+
// Several: the person picks. Each row says what it adds; the list scrolls inside the same box.
|
|
2409
|
+
html = '<div class="mt-prepline list"><ul class="mt-preplist" aria-label="' + esc(p.drafts.length + ' preps are ready for this meeting') + '">' +
|
|
2410
|
+
(p.note ? '<li class="mt-prepnote"><span class="mt-prepline-text mt-amber">' + esc(p.note) + '</span></li>' : '') +
|
|
2411
|
+
p.drafts.map(function (d) {
|
|
2412
|
+
var label = (cleanTitle(d.title) || 'Prep') + (p.changed === d.id ? ' (changed)' : '') + ': ' + prepWhat(d).replace(/^its /, '');
|
|
2413
|
+
return '<li><span class="mt-prepline-text" title="' + esc(label) + '">' + esc(label) + '</span>' + prepAddBtn(p, d) + '</li>';
|
|
2414
|
+
}).join('') + '</ul></div>';
|
|
2415
|
+
}
|
|
2416
|
+
return '<div class="mt-prepslot">' + html + '</div>';
|
|
2417
|
+
}
|
|
2418
|
+
function paintLivePrep() {
|
|
2419
|
+
var el = $('mt-lprep'); if (!el || S.view !== 'live') return;
|
|
2420
|
+
var html = livePrepHtml();
|
|
2421
|
+
if (html !== S.prepHtml) { S.prepHtml = html; el.innerHTML = html; }
|
|
2422
|
+
}
|
|
2423
|
+
var PREP_SERVER_CHANGED = 'A different COS server answered than the one that offered this prep. It was read again; tap Add to this meeting again.';
|
|
2424
|
+
var PREP_BIND_COPY = {
|
|
2425
|
+
maintenance: 'COS is updating. Try again in a minute.', invalid_session_id: 'This recording does not take prep files.',
|
|
2426
|
+
file_context_limit: 'This meeting holds as many files as Meeting Ask reads. Remove one in COS Control on your Mac, then try again.',
|
|
2427
|
+
file_context_storage_limit: 'COS is out of room for meeting files. Remove some in COS Control on your Mac, then try again.',
|
|
2428
|
+
not_found: 'That prep is no longer on your Mac.', unknown_session: 'COS does not know this recording yet. Try again in a moment.',
|
|
2429
|
+
request_reused: 'COS saw a different request with the same id. Reload the page, then try again.'
|
|
2430
|
+
};
|
|
2431
|
+
function kb(n) { return Math.max(1, Math.round(num(n) / 1024)).toLocaleString() + ' KB'; }
|
|
2432
|
+
// v1.1: the bind names the revision the offer described ({revision, sessionId, serverInstanceId, surface}), so a newer
|
|
2433
|
+
// version nobody has seen is never attached. The server id is the newest one read: capabilities.prep or the most
|
|
2434
|
+
// recent Prep answer (W.cosChat.prepInstance), else Meeting Ask's, which is the same id.
|
|
2435
|
+
function bindOnce(sid, d) {
|
|
2436
|
+
var cc = W.cosChat, known = cc && typeof cc.prepInstance === 'function' ? cc.prepInstance() : '';
|
|
2437
|
+
return (known ? Promise.resolve(known) : loadConfig().then(function (conf) { return conf && conf.serverInstanceId; })).then(function (inst) {
|
|
2438
|
+
if (!inst) throw new Error(S.conf.notice || 'Meeting assistance is not available on this server.');
|
|
2439
|
+
// Amendment 19: the header is what authorizes the write; the body carries the same id.
|
|
2440
|
+
return answer('/api/prep/drafts/' + encodeURIComponent(d.id) + '/bind', { method: 'POST', headers: { 'X-COS-Expected-Server-Instance': inst }, body: { revision: d.revision, sessionId: sid, serverInstanceId: inst, surface: 'web' } });
|
|
2441
|
+
}).then(function (a) {
|
|
2442
|
+
var b = a.body || {}, code = typeof b.error === 'string' ? b.error : (b.error && b.error.code) || b.reason || '';
|
|
2443
|
+
if (a.status >= 200 && a.status < 300 || a.status === 409) notePrepInstance(b);
|
|
2444
|
+
if (a.status >= 200 && a.status < 300) return b;
|
|
2445
|
+
// A different server than the one that offered it: the prep is read again (amendment 19) and the line says so.
|
|
2446
|
+
// Adding it takes another tap, against the server that answers now.
|
|
2447
|
+
if (a.status === 409 && code === 'server_instance_mismatch') {
|
|
2448
|
+
return answer('/api/prep/drafts/' + encodeURIComponent(d.id)).then(function (g) {
|
|
2449
|
+
if (g.status === 200) notePrepInstance(g.body);
|
|
2450
|
+
var e = new Error(PREP_SERVER_CHANGED);
|
|
2451
|
+
e.draft = g.status === 200 && g.body.draft ? g.body.draft : d;
|
|
2452
|
+
e.changedServer = true;
|
|
2453
|
+
throw e;
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
if (a.status === 428 && typeof console !== 'undefined') console.error('[prep] bind went without X-COS-Expected-Server-Instance');
|
|
2457
|
+
var err = new Error(a.status === 0 ? 'COS on your Mac did not answer. Try again when it is awake.' : a.status === 401 ? SIGNED_OUT
|
|
2458
|
+
: a.status === 428 ? 'This page did not name the Mac’s COS server, so nothing was added. Reload the page, then try again.'
|
|
2459
|
+
: code === 'file_context_limit' && b.needBytes != null ? 'This meeting has too much file text for this prep: it needs ' + kb(b.needBytes) + ' and the meeting has room for ' + kb(b.haveBytes) + '. Remove a large file from the meeting in COS Control on your Mac, then try again.'
|
|
2460
|
+
: PREP_BIND_COPY[code] || (a.status === 404 ? PREP_BIND_COPY.not_found : 'COS could not add the prep (HTTP ' + a.status + '). Try again.'));
|
|
2461
|
+
// Changed since it was offered: the area now describes the newer version, and adding it takes another tap.
|
|
2462
|
+
if (a.status === 409 && code === 'revision_conflict' && b.draft) err.draft = b.draft;
|
|
2463
|
+
throw err;
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
// Only from a tap on Add to this meeting: nothing here binds on its own.
|
|
2467
|
+
function bindPrep(id) {
|
|
2468
|
+
var f = S.feeds[S.liveSid], p = S.lprep;
|
|
2469
|
+
if (!f || !p || p.sid !== f.sid || p.busy || p.done || f.endedAt) return;
|
|
2470
|
+
var d = p.drafts.filter(function (x) { return x.id === id; })[0]; if (!d) return;
|
|
2471
|
+
p.busy = id; p.note = ''; paintLivePrep();
|
|
2472
|
+
bindOnce(f.sid, d).then(function (b) {
|
|
2473
|
+
p.busy = ''; p.done = (b && b.draft) || d;
|
|
2474
|
+
var a = A(f.sid);
|
|
2475
|
+
// The meeting's files as the bind answered them, when it answered Meeting Ask files; and read again either way.
|
|
2476
|
+
if (Array.isArray(b && b.files) && b.files.every(function (x) { return x && typeof x.id === 'string' && x.revision != null; })) a.files = b.files;
|
|
2477
|
+
a.filesOpen = true;
|
|
2478
|
+
paintLivePrep(); paintLiveFiles();
|
|
2479
|
+
(a.openP || Promise.resolve()).then(function () { return askOpen(f.sid, 'poll'); });
|
|
2480
|
+
}, function (err) {
|
|
2481
|
+
p.busy = '';
|
|
2482
|
+
if (err && err.draft && typeof err.draft.id === 'string') {
|
|
2483
|
+
p.drafts = p.drafts.map(function (x) { return x.id === err.draft.id ? err.draft : x; });
|
|
2484
|
+
if (err.changedServer) p.note = err.message; else { p.changed = err.draft.id; p.note = ''; }
|
|
2485
|
+
} else p.note = 'Not added. ' + ((err && err.message) || 'Try again.');
|
|
2486
|
+
paintLivePrep();
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2269
2489
|
function findUpload(id) { var u = LU(); for (var i = 0; i < u.items.length; i++) if (u.items[i].id === id) return u.items[i]; return null; }
|
|
2270
2490
|
function onLivePaste(e) {
|
|
2271
2491
|
if (S.view !== 'live') return;
|
|
@@ -2378,7 +2598,9 @@
|
|
|
2378
2598
|
if (contextReady([f.sid])) {
|
|
2379
2599
|
var a = A(f.sid);
|
|
2380
2600
|
var cueNote = a.cues ? null : (a.cuesState && a.cuesState !== 'older' ? 'Context coverage: cues could not be read from the server.' : null);
|
|
2381
|
-
|
|
2601
|
+
// The first line names the meeting in plain words ("Live meeting: <title>"), so a session started from this paste
|
|
2602
|
+
// is titled by it; the sections under it are what they were.
|
|
2603
|
+
var text = fullContext(liveContextTitle(f), f.turns.length ? liveCopyText(f) : '', a.history, evidenceFiles(a.history, a.files || []), a.cues || [], cueNote, true) + (a.error ? '\n\nContext coverage: ' + a.error : '');
|
|
2382
2604
|
W.copyText(text).then(function () { done('Copied full meeting context'); }, function (err) { done(err.message); });
|
|
2383
2605
|
return;
|
|
2384
2606
|
}
|
|
@@ -2476,6 +2698,7 @@
|
|
|
2476
2698
|
case 'live-copy': liveCopy('transcript'); return;
|
|
2477
2699
|
case 'live-context': liveCopy('context'); return;
|
|
2478
2700
|
case 'jump': jumpToLive(); return;
|
|
2701
|
+
case 'prep-bind': bindPrep(t.getAttribute('data-id')); return;
|
|
2479
2702
|
}
|
|
2480
2703
|
});
|
|
2481
2704
|
pane.addEventListener('input', function (e) {
|
package/shared/meeting-ask.ts
CHANGED
|
@@ -33,8 +33,12 @@ export interface MeetingAskFile {
|
|
|
33
33
|
status: 'ready' | 'pending' | 'unsupported' | 'failed' | 'removed' | 'secret-held';
|
|
34
34
|
text: string;
|
|
35
35
|
notice?: string;
|
|
36
|
-
/**
|
|
37
|
-
|
|
36
|
+
/**
|
|
37
|
+
* "phone" for a file the phone attached through POST files/media (6.71.1). "prep" (6.99.19) for a copy a Prep bind
|
|
38
|
+
* adopted (id prep_<draftId>_<fileId>, or prep_<draftId>_steer for "Prep notes"). Control's own rows omit it, and a
|
|
39
|
+
* client that does not know a value shows the row as a plain file.
|
|
40
|
+
*/
|
|
41
|
+
origin?: 'phone' | 'prep';
|
|
38
42
|
}
|
|
39
43
|
export interface MeetingAskSource {
|
|
40
44
|
/** Server-owned accepted post-stop quotation; no live turn target or precise word clock. */
|