@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
|
@@ -105,8 +105,11 @@
|
|
|
105
105
|
jobs: caps.durableQueryJobs === true || h && h.durableQueryJobs === true || !!(caps.durableQueryJobs && caps.durableQueryJobs.enabled) ||
|
|
106
106
|
!!(h && h.features && h.features.durableQueryJobs === true),
|
|
107
107
|
steer: !!(C.caps && C.caps.steer),
|
|
108
|
-
thread: !!(drop && drop.enabled === true && drop.thread && drop.thread.protocol === 1)
|
|
108
|
+
thread: !!(drop && drop.enabled === true && drop.thread && drop.thread.protocol === 1),
|
|
109
|
+
// 6.99.19: Prep (capabilities.prep, protocol 1). Absent on an older server: "Prep me" stays a plain Ask.
|
|
110
|
+
prep: prepCapsOf(caps.prep)
|
|
109
111
|
};
|
|
112
|
+
if (next.prep && next.prep.serverInstanceId) PREP_INST = next.prep.serverInstanceId;
|
|
110
113
|
var changed = !C.caps || C.caps.dropIt !== next.dropIt || C.caps.jobs !== next.jobs || C.caps.thread !== next.thread;
|
|
111
114
|
var filesChanged = !!C.caps && C.caps.dropFiles !== next.dropFiles;
|
|
112
115
|
C.caps = next;
|
|
@@ -115,6 +118,13 @@
|
|
|
115
118
|
else if (filesChanged) paintButtons(CHAT);
|
|
116
119
|
if (next.dropIt) refreshSummary();
|
|
117
120
|
}
|
|
121
|
+
function prepCapsOf(p) {
|
|
122
|
+
if (!p || typeof p !== 'object' || !(Number(p.protocol) >= 1)) return null;
|
|
123
|
+
var files = Number.isInteger(p.maxFiles) && p.maxFiles > 0 ? Math.min(p.maxFiles, 10) : 10;
|
|
124
|
+
var chars = Number.isInteger(p.steerChars) && p.steerChars > 0 ? Math.min(p.steerChars, 4000) : 4000;
|
|
125
|
+
return { protocol: Number(p.protocol), maxFiles: files, steerChars: chars, bind: p.bind === true, suggest: p.suggest === true,
|
|
126
|
+
serverInstanceId: typeof p.serverInstanceId === 'string' ? p.serverInstanceId : '' };
|
|
127
|
+
}
|
|
118
128
|
// The shell's status read (shell.js checkServer) hands its /api/health answer here.
|
|
119
129
|
function fromHealth(h) { setCaps(h); }
|
|
120
130
|
function ensureCaps() {
|
|
@@ -305,6 +315,7 @@
|
|
|
305
315
|
if (!el || el.getAttribute('data-mode') !== chatMode()) {
|
|
306
316
|
host.innerHTML = chatSkeleton();
|
|
307
317
|
hydrate(CHAT);
|
|
318
|
+
if (P) paintPrep(true);
|
|
308
319
|
}
|
|
309
320
|
if (!visible) return;
|
|
310
321
|
loadModels();
|
|
@@ -976,7 +987,8 @@
|
|
|
976
987
|
if (action === 'open_result') return W.go(linkOf('session') ? 'sessions' : linkOf('card') ? 'work' : 'messages');
|
|
977
988
|
if (action === 'open') { if (linkOf('thread')) return W.go('threads'); if (linkOf('meeting')) return W.go('meetings'); return; }
|
|
978
989
|
if (action === 'review') return W.go('work');
|
|
979
|
-
|
|
990
|
+
// 6.99.19: with Prep on the Mac, Prep me opens the Prep composer (notes and files first); without it, the plain Ask.
|
|
991
|
+
if (action === 'prep') { if (prepOn()) return openPrep(item); if (C.caps && C.caps.jobs) submitAsk('Prep me for ' + (item.title || 'my next meeting'), ''); return; }
|
|
980
992
|
if (action === 'reply') {
|
|
981
993
|
var box = document.getElementById('cos-input');
|
|
982
994
|
if (box) { box.value = 'remind me '; box.focus(); }
|
|
@@ -1019,9 +1031,594 @@
|
|
|
1019
1031
|
api('/api/query-jobs/' + a.jobId + '/cancel', { method: 'POST', body: { generation: 1 } }).then(function (d) { if (d && d.job) take(a, d.job); paintAsk(a); }, function (err) { W.toast((err && err.message) || 'Cancel did not go through.'); });
|
|
1020
1032
|
}
|
|
1021
1033
|
|
|
1034
|
+
// ── Prep (6.99.19, PREP_CONTRACT_v1) ──────────────────
|
|
1035
|
+
// With capabilities.prep on the Mac, "Prep me" on a prep card opens this composer above the chat's own: what the prep
|
|
1036
|
+
// should focus on, and files, the way the live meeting view takes them. It is a server draft (POST /api/prep/drafts,
|
|
1037
|
+
// the clientDraftId kept in this browser, so opening it again resumes it through GET). Every write sends the draft's
|
|
1038
|
+
// revision; a 409 revision_conflict hands back the newer draft, which is shown, with one line saying so. Prep me
|
|
1039
|
+
// posts .../run, and the job it returns is drawn and polled in the chat exactly like an Ask (take, poll, and the
|
|
1040
|
+
// by-client lookup when the answer to the run was lost). Files reach a meeting only from that meeting's live view.
|
|
1041
|
+
var PREP_STORE = 'cos.web.prepDrafts';
|
|
1042
|
+
var PREP_ACCEPT = 'image/*,.heic,.heif,application/pdf,.pdf,text/plain,.txt,text/markdown,.md,text/csv,.csv,application/json,.json,.xlsx';
|
|
1043
|
+
var PREP_SAVE_MS = 700;
|
|
1044
|
+
var PREP_FILE_STATE = { unsupported: 'No readable text', failed: 'Could not be read', 'secret-held': 'Held: it appears to contain credentials' };
|
|
1045
|
+
// Amendment 19: every Prep write names the server it expects (X-COS-Expected-Server-Instance), the id last read from
|
|
1046
|
+
// capabilities.prep or the most recent Prep answer (every one carries it). The id survives restarts; a different id
|
|
1047
|
+
// means a different COS server answered (409 server_instance_mismatch).
|
|
1048
|
+
var PREP_INST = '';
|
|
1049
|
+
var P = null; // the open composer: { key, title, startsAt, draft, steer, dirty, ups, note, warn, running, runId, chain, timer, conflicts }
|
|
1050
|
+
function prepCaps() { return C.caps && C.caps.prep ? C.caps.prep : null; }
|
|
1051
|
+
function prepOn() { return !!prepCaps(); }
|
|
1052
|
+
function prepMax() { var c = prepCaps(); return c ? c.maxFiles : 10; }
|
|
1053
|
+
function prepChars() { var c = prepCaps(); return c ? c.steerChars : 4000; }
|
|
1054
|
+
function prepRec(key) { var r = W.readStore(PREP_STORE)[key]; return r && typeof r === 'object' && typeof r.cd === 'string' ? r : null; }
|
|
1055
|
+
function savePrepRec(key, rec) {
|
|
1056
|
+
var all = W.readStore(PREP_STORE);
|
|
1057
|
+
all[key] = rec;
|
|
1058
|
+
// The newest twenty: a prep card a week old has no reason to be found again here.
|
|
1059
|
+
var keys = Object.keys(all).sort(function (a, b) { return (Number(all[a] && all[a].at) || 0) - (Number(all[b] && all[b].at) || 0); });
|
|
1060
|
+
while (keys.length > 20) delete all[keys.shift()];
|
|
1061
|
+
W.writeStore(PREP_STORE, all);
|
|
1062
|
+
}
|
|
1063
|
+
function forgetPrepRec(key) { var all = W.readStore(PREP_STORE); delete all[key]; W.writeStore(PREP_STORE, all); }
|
|
1064
|
+
// The run in flight, kept with the draft in this browser (closing the composer or reloading the page does not lose it):
|
|
1065
|
+
// its clientRequestId and its own model, effort and continue, so a retry is the same request and the server re-drives it.
|
|
1066
|
+
function setPendingRun(p, run) {
|
|
1067
|
+
p.pending = run;
|
|
1068
|
+
var rec = prepRec(p.key); if (!rec) return;
|
|
1069
|
+
if (run) rec.run = run; else delete rec.run;
|
|
1070
|
+
savePrepRec(p.key, rec);
|
|
1071
|
+
}
|
|
1072
|
+
/** A Prep route, answered as { status, body } (a 409 carries the newer draft). Never rejects: no answer is status 0. */
|
|
1073
|
+
function prepCall(path, opts) {
|
|
1074
|
+
var o = opts || {}, method = o.method || 'GET';
|
|
1075
|
+
var headers = { 'X-COS-Web': '1' };
|
|
1076
|
+
if (o.body !== undefined) headers['Content-Type'] = 'application/json';
|
|
1077
|
+
if (method !== 'GET' && PREP_INST) headers['X-COS-Expected-Server-Instance'] = PREP_INST;
|
|
1078
|
+
var ctrl = typeof AbortController === 'function' ? new AbortController() : null;
|
|
1079
|
+
var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, o.timeoutMs || (method === 'GET' ? 20000 : 30000)) : null;
|
|
1080
|
+
return fetch(path, { method: method, headers: headers, credentials: 'same-origin', body: o.body !== undefined ? JSON.stringify(o.body) : undefined, signal: ctrl ? ctrl.signal : undefined })
|
|
1081
|
+
.then(function (res) {
|
|
1082
|
+
return res.text().then(function (t) {
|
|
1083
|
+
clearTimeout(timer); var b = {}; try { b = t ? JSON.parse(t) : {}; } catch (_e) { b = {}; }
|
|
1084
|
+
b = b || {};
|
|
1085
|
+
var inst = typeof b.serverInstanceId === 'string' && b.serverInstanceId ? b.serverInstanceId : b.draft && typeof b.draft.serverInstanceId === 'string' ? b.draft.serverInstanceId : '';
|
|
1086
|
+
// The server's id as it answers now: from a read, a write it took, or the refusal that says it changed.
|
|
1087
|
+
if (inst && (res.status < 300 || (res.status === 409 && prepCode({ body: b }) === 'server_instance_mismatch'))) PREP_INST = inst;
|
|
1088
|
+
if (res.status === 428 && typeof console !== 'undefined') console.error('[prep] a write went without X-COS-Expected-Server-Instance', method, path);
|
|
1089
|
+
return { status: res.status, body: b };
|
|
1090
|
+
});
|
|
1091
|
+
})
|
|
1092
|
+
.catch(function () { clearTimeout(timer); return { status: 0, body: {} }; });
|
|
1093
|
+
}
|
|
1094
|
+
// sha256 of the bytes this browser uploaded (amendment 20). crypto.subtle needs a secure context, and a plain-http
|
|
1095
|
+
// tailnet page is not one, so a small FIPS 180-4 implementation stands in there.
|
|
1096
|
+
function sha256Hex(bytes) {
|
|
1097
|
+
var subtle = typeof crypto !== 'undefined' && crypto.subtle && (typeof window === 'undefined' || window.isSecureContext !== false) ? crypto.subtle : null;
|
|
1098
|
+
if (subtle) return subtle.digest('SHA-256', bytes).then(function (h) { return hex(new Uint8Array(h)); }, function () { return sha256Js(bytes); });
|
|
1099
|
+
return Promise.resolve(sha256Js(bytes));
|
|
1100
|
+
}
|
|
1101
|
+
function hex(a) { return Array.prototype.map.call(a, function (x) { return (x + 0x100).toString(16).slice(1); }).join(''); }
|
|
1102
|
+
var SHA_K = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
1103
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
1104
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
1105
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2];
|
|
1106
|
+
function sha256Js(bytes) {
|
|
1107
|
+
var len = bytes.length, total = ((len + 9 + 63) >> 6) << 6, m = new Uint8Array(total);
|
|
1108
|
+
m.set(bytes); m[len] = 0x80;
|
|
1109
|
+
var bits = len * 8, dv = new DataView(m.buffer);
|
|
1110
|
+
dv.setUint32(total - 8, Math.floor(bits / 0x100000000)); dv.setUint32(total - 4, bits >>> 0);
|
|
1111
|
+
var h = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19], w = new Array(64);
|
|
1112
|
+
var rotr = function (x, n) { return (x >>> n) | (x << (32 - n)); };
|
|
1113
|
+
for (var off = 0; off < total; off += 64) {
|
|
1114
|
+
for (var i = 0; i < 16; i++) w[i] = dv.getUint32(off + i * 4);
|
|
1115
|
+
for (i = 16; i < 64; i++) {
|
|
1116
|
+
var s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3), s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
1117
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
|
|
1118
|
+
}
|
|
1119
|
+
var a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], k = h[7];
|
|
1120
|
+
for (i = 0; i < 64; i++) {
|
|
1121
|
+
var t1 = (k + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + SHA_K[i] + w[i]) | 0;
|
|
1122
|
+
var t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) | 0;
|
|
1123
|
+
k = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0;
|
|
1124
|
+
}
|
|
1125
|
+
h[0] = (h[0] + a) | 0; h[1] = (h[1] + b) | 0; h[2] = (h[2] + c) | 0; h[3] = (h[3] + d) | 0; h[4] = (h[4] + e) | 0; h[5] = (h[5] + f) | 0; h[6] = (h[6] + g) | 0; h[7] = (h[7] + k) | 0;
|
|
1126
|
+
}
|
|
1127
|
+
return h.map(function (x) { return ('00000000' + (x >>> 0).toString(16)).slice(-8); }).join('');
|
|
1128
|
+
}
|
|
1129
|
+
/** The exact bytes the media upload sent: the resized JPEG for a photo (uploadMedia keeps it as t.thumb), else the file. */
|
|
1130
|
+
function uploadedBytes(t) {
|
|
1131
|
+
if (t.kind === 'photo' && /^data:/.test(t.thumb || '')) {
|
|
1132
|
+
var bin = atob(t.thumb.slice(t.thumb.indexOf(',') + 1)), out = new Uint8Array(bin.length);
|
|
1133
|
+
for (var i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
1134
|
+
return Promise.resolve(out);
|
|
1135
|
+
}
|
|
1136
|
+
if (t.file && typeof t.file.arrayBuffer === 'function') return t.file.arrayBuffer().then(function (b) { return new Uint8Array(b); });
|
|
1137
|
+
return Promise.reject(new Error('This browser could not read the file again to check it. Remove it and add it again.'));
|
|
1138
|
+
}
|
|
1139
|
+
function prepCode(r) { var b = (r && r.body) || {}; return typeof b.error === 'string' ? b.error : (b.error && b.error.code) || b.code || b.reason || ''; }
|
|
1140
|
+
var PREP_CONFLICT = 'This prep changed on another device. It now shows the newest notes and files. Check them, then try again.';
|
|
1141
|
+
function prepErrorText(r) {
|
|
1142
|
+
var code = prepCode(r);
|
|
1143
|
+
if (r.status === 0) return 'COS on your Mac did not answer. Check that it is awake, then try again.';
|
|
1144
|
+
if (r.status === 401) return 'This browser was signed out. Your words are kept here. Sign in, then try again.';
|
|
1145
|
+
if (code === 'server_instance_mismatch') return 'The COS server on your Mac changed. Close this prep and tap Prep me on the card again.';
|
|
1146
|
+
if (r.status === 428) return 'This page did not name the Mac\u2019s COS server, so nothing was changed. Reload the page, then try again.';
|
|
1147
|
+
if (code === 'media_hash_mismatch') return 'The file your Mac kept does not match what this browser sent. Retry to send it again.';
|
|
1148
|
+
if (code === 'maintenance' || r.status === 503) return 'COS is updating. Try again in a minute.';
|
|
1149
|
+
if (code === 'prep_file_limit' || r.status === 413) return 'This prep holds ' + prepMax() + ' files. Remove one, then retry.';
|
|
1150
|
+
if (code === 'prep_draft_limit') return 'COS already keeps as many preps as it can. Open an old prep card, tap Archive this prep, then try again.';
|
|
1151
|
+
if (r.status === 507) return 'COS is out of room for prep files. Remove some files from older preps, then retry.';
|
|
1152
|
+
if (code === 'durable_query_jobs_disabled') return 'Prep me needs the durable query jobs switch on your Mac. Your notes and files are saved.';
|
|
1153
|
+
if (/^media_/.test(code)) return 'The upload expired before COS kept it. Retry.';
|
|
1154
|
+
if (r.status === 404) return 'This prep is no longer on your Mac. Close it and tap Prep me on the card again.';
|
|
1155
|
+
var msg = r.body && typeof r.body.message === 'string' ? r.body.message : '';
|
|
1156
|
+
return msg ? msg : 'COS did not take that (HTTP ' + r.status + '). Try again.';
|
|
1157
|
+
}
|
|
1158
|
+
function prepQueue(p, fn) { var next = p.chain.then(fn); p.chain = next.catch(function () {}); return next; }
|
|
1159
|
+
function prepPath(p) { return '/api/prep/drafts/' + encodeURIComponent(p.draft.id); }
|
|
1160
|
+
function prepFiles(d) { return ((d && d.files) || []).filter(function (f) { return f && f.status !== 'removed'; }); }
|
|
1161
|
+
function prepUploading(p) { return p.ups.some(function (t) { return t.state !== 'failed'; }); }
|
|
1162
|
+
function prepWhen(iso) { return iso && isFinite(Date.parse(iso)) ? timeLabel(iso) : ''; }
|
|
1163
|
+
|
|
1164
|
+
function openPrep(item) {
|
|
1165
|
+
var act = ((item && item.actions) || []).filter(function (a) { return a && a.id === 'prep'; })[0] || {};
|
|
1166
|
+
var info = (act.prep && typeof act.prep === 'object' ? act.prep : item.prep && typeof item.prep === 'object' ? item.prep : {}) || {};
|
|
1167
|
+
var key = 'item:' + item.id;
|
|
1168
|
+
if (P && P.key === key) { paintPrep(); focusPrep(); return; }
|
|
1169
|
+
if (P) closePrep();
|
|
1170
|
+
P = { key: key, title: clip(info.title || item.title || 'my next meeting', 200), startsAt: typeof info.startsAt === 'string' ? info.startsAt : null,
|
|
1171
|
+
draft: null, steer: '', dirty: false, ups: [], note: '', warn: false, running: false, pending: null, chain: Promise.resolve(), timer: null, conflicts: 0, loading: true };
|
|
1172
|
+
paintPrep(true); focusPrep();
|
|
1173
|
+
// On a phone the page scrolls: bring the composer up to where the card was tapped.
|
|
1174
|
+
var slot = prepSlot(false); if (slot && slot.scrollIntoView && !wide()) slot.scrollIntoView({ block: 'start' });
|
|
1175
|
+
loadPrep(P);
|
|
1176
|
+
}
|
|
1177
|
+
function closePrep() {
|
|
1178
|
+
if (!P) return;
|
|
1179
|
+
var p = P;
|
|
1180
|
+
// Words typed and not yet saved go out once more on the way.
|
|
1181
|
+
if (p.dirty && p.draft) savePrepSteer(p);
|
|
1182
|
+
clearTimeout(p.timer);
|
|
1183
|
+
p.ups.forEach(function (t) { if (t.xhr) { try { t.xhr.abort(); } catch (_e) {} } });
|
|
1184
|
+
P = null;
|
|
1185
|
+
paintPrep();
|
|
1186
|
+
}
|
|
1187
|
+
// Resume when this browser has the draft; otherwise create it (the same clientDraftId answers the same draft).
|
|
1188
|
+
function loadPrep(p) {
|
|
1189
|
+
var rec = prepRec(p.key);
|
|
1190
|
+
var run = rec && rec.run;
|
|
1191
|
+
if (run && typeof run.rid === 'string') p.pending = { rid: run.rid, model: run.model || '', effort: run.effort || '', cont: !!run.cont };
|
|
1192
|
+
var create = function (cd) {
|
|
1193
|
+
savePrepRec(p.key, { cd: cd, id: '', at: Date.now() });
|
|
1194
|
+
var body = { clientDraftId: cd, title: p.title };
|
|
1195
|
+
if (p.startsAt) body.startsAt = p.startsAt;
|
|
1196
|
+
return prepCall('/api/prep/drafts', { method: 'POST', body: body });
|
|
1197
|
+
};
|
|
1198
|
+
var step = rec && rec.id ? prepCall('/api/prep/drafts/' + encodeURIComponent(rec.id)).then(function (r) {
|
|
1199
|
+
return r.status === 404 ? create(uuid()) : r;
|
|
1200
|
+
}) : create(rec ? rec.cd : uuid());
|
|
1201
|
+
return step.then(function (r) {
|
|
1202
|
+
p.loading = false;
|
|
1203
|
+
if (r.status >= 200 && r.status < 300 && r.body.draft) { adoptPrep(p, r.body.draft, false); if (p.dirty) schedulePrepSave(p); return; }
|
|
1204
|
+
setPrepNote(p, prepErrorText(r), true);
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
function adoptPrep(p, draft, conflict) {
|
|
1208
|
+
p.draft = draft;
|
|
1209
|
+
var rec = prepRec(p.key);
|
|
1210
|
+
var next = { cd: (draft && draft.clientDraftId) || (rec && rec.cd) || '', id: draft.id, at: Date.now() };
|
|
1211
|
+
if (rec && rec.run) next.run = rec.run;
|
|
1212
|
+
savePrepRec(p.key, next);
|
|
1213
|
+
if (conflict || !p.dirty) {
|
|
1214
|
+
p.steer = typeof draft.steer === 'string' ? draft.steer : '';
|
|
1215
|
+
if (conflict) { p.dirty = false; clearTimeout(p.timer); p.conflicts++; }
|
|
1216
|
+
var box = P === p && prepBox();
|
|
1217
|
+
if (box && box.value !== p.steer) { box.value = p.steer; W.autoGrow(box); }
|
|
1218
|
+
}
|
|
1219
|
+
if (conflict) setPrepNote(p, PREP_CONFLICT, true);
|
|
1220
|
+
paintPrep();
|
|
1221
|
+
}
|
|
1222
|
+
/** A write to the draft: the answer's draft is adopted (a 409 revision_conflict's too, with the line that says so). */
|
|
1223
|
+
function prepWrite(p, method, path, body) {
|
|
1224
|
+
return prepCall(path, { method: method, body: body }).then(function (r) {
|
|
1225
|
+
var conflict = r.status === 409 && prepCode(r) === 'revision_conflict';
|
|
1226
|
+
if (r.body && r.body.draft && (conflict || (r.status >= 200 && r.status < 300))) adoptPrep(p, r.body.draft, conflict);
|
|
1227
|
+
if (r.status === 409 && prepCode(r) === 'server_instance_mismatch') { r.mismatch = true; reloadAfterServerChange(p); }
|
|
1228
|
+
return r;
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
// A different COS server answered than the one this prep was read from: say so, read the draft again,
|
|
1232
|
+
// and keep words not yet saved, which go out once the draft is read.
|
|
1233
|
+
var PREP_SERVER_CHANGED = 'The COS server on your Mac changed since this prep opened. It was read again. Check it, then try again.';
|
|
1234
|
+
function reloadAfterServerChange(p) {
|
|
1235
|
+
setPrepNote(p, PREP_SERVER_CHANGED, true);
|
|
1236
|
+
if (p.reloading || !p.draft) return;
|
|
1237
|
+
p.reloading = true;
|
|
1238
|
+
prepCall(prepPath(p)).then(function (r) {
|
|
1239
|
+
p.reloading = false;
|
|
1240
|
+
if (r.status === 200 && r.body.draft) { adoptPrep(p, r.body.draft, false); if (p.dirty) schedulePrepSave(p); }
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
function schedulePrepSave(p) { clearTimeout(p.timer); p.timer = setTimeout(function () { savePrepSteer(p); }, PREP_SAVE_MS); }
|
|
1244
|
+
function savePrepSteer(p) {
|
|
1245
|
+
clearTimeout(p.timer); p.timer = null;
|
|
1246
|
+
return prepQueue(p, function () {
|
|
1247
|
+
if (!p.dirty || !p.draft) return null;
|
|
1248
|
+
var text = p.steer; p.dirty = false;
|
|
1249
|
+
return prepWrite(p, 'PATCH', prepPath(p), { revision: p.draft.revision, steer: text }).then(function (r) {
|
|
1250
|
+
if (r.mismatch) { if (p.steer === text) p.dirty = true; return r; }
|
|
1251
|
+
if ((r.status >= 200 && r.status < 300) || r.status === 409) return r;
|
|
1252
|
+
// Kept, and sent again with the next change or with Prep me.
|
|
1253
|
+
if (p.steer === text) p.dirty = true;
|
|
1254
|
+
setPrepNote(p, 'Your notes are not saved yet. ' + prepErrorText(r), true);
|
|
1255
|
+
return r;
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
function onPrepInput(box) {
|
|
1260
|
+
var p = P; if (!p) return;
|
|
1261
|
+
var max = prepChars();
|
|
1262
|
+
if (box.value.length > max) box.value = box.value.slice(0, max);
|
|
1263
|
+
p.steer = box.value; p.dirty = true;
|
|
1264
|
+
W.autoGrow(box);
|
|
1265
|
+
schedulePrepSave(p);
|
|
1266
|
+
paintPrepParts(p);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// Files: the composer's own upload (uploadMedia), then POST .../files with the draft's revision.
|
|
1270
|
+
function addPrepFiles(list) {
|
|
1271
|
+
var p = P; if (!p) return;
|
|
1272
|
+
var files = Array.prototype.slice.call(list || []), refusals = [];
|
|
1273
|
+
if (!p.draft) { setPrepNote(p, 'This prep is still opening. Add the files again in a moment.', true); return; }
|
|
1274
|
+
files.forEach(function (file) {
|
|
1275
|
+
if (prepFiles(p.draft).length + p.ups.length >= prepMax()) { refusals.push('A prep holds ' + prepMax() + ' files. ' + (file.name || 'A file') + ' was not added.'); return; }
|
|
1276
|
+
var c = classify(file);
|
|
1277
|
+
if (c.refuse) { refusals.push(c.refuse); return; }
|
|
1278
|
+
if (c.kind === 'video') { refusals.push((file.name || 'That video') + ' is a video. Prep reads documents and the text in images, so attach a PDF, a text file or a screenshot.'); return; }
|
|
1279
|
+
var t = { id: uuid(), file: file, name: file.name || (c.kind === 'photo' ? 'Screenshot' : 'File'), size: file.size, kind: c.kind, mime: c.mime || '', state: 'reading', progress: 0, mediaId: '', error: '' };
|
|
1280
|
+
p.ups.push(t);
|
|
1281
|
+
sendPrepFile(p, t);
|
|
1282
|
+
});
|
|
1283
|
+
setPrepNote(p, refusals.join(' '), !!refusals.length);
|
|
1284
|
+
paintPrep();
|
|
1285
|
+
}
|
|
1286
|
+
function sendPrepFile(p, t) {
|
|
1287
|
+
t.state = t.mediaId ? 'adding' : 'reading'; t.progress = 0; t.error = '';
|
|
1288
|
+
var step = t.mediaId ? Promise.resolve({ id: t.mediaId }) : uploadMedia(t, {
|
|
1289
|
+
uploading: function () { t.state = 'uploading'; paintPrep(); },
|
|
1290
|
+
progress: function (x) { t.progress = x; paintPrepProgress(t); }
|
|
1291
|
+
});
|
|
1292
|
+
paintPrep();
|
|
1293
|
+
step.then(function (ref) {
|
|
1294
|
+
if (p.ups.indexOf(t) < 0) return null;
|
|
1295
|
+
if (!ref || !ref.id) { var e = new Error('COS did not keep the file. Retry.'); e.code = 'kept'; throw e; }
|
|
1296
|
+
t.mediaId = ref.id; t.state = 'adding'; paintPrep();
|
|
1297
|
+
// Amendment 20: the sha256 of exactly the bytes uploaded, which the server checks its copy against.
|
|
1298
|
+
return t.sha256 ? null : uploadedBytes(t).then(sha256Hex).then(function (h) { t.sha256 = h; });
|
|
1299
|
+
}).then(function () {
|
|
1300
|
+
if (p.ups.indexOf(t) < 0 || !t.mediaId) return null;
|
|
1301
|
+
// v1.1: one operationId per add, the same on a retry after a lost answer (the server answers the first result).
|
|
1302
|
+
t.opId = t.opId || uuid();
|
|
1303
|
+
return prepQueue(p, function () {
|
|
1304
|
+
var path = prepPath(p) + '/files';
|
|
1305
|
+
var body = function () { return { operationId: t.opId, revision: p.draft.revision, media: [{ id: t.mediaId, name: t.name.slice(0, 240), sha256: t.sha256 }] }; };
|
|
1306
|
+
return prepWrite(p, 'POST', path, body()).then(function (r) { return r.status === 0 ? prepWrite(p, 'POST', path, body()) : r; });
|
|
1307
|
+
});
|
|
1308
|
+
}).then(function (r) {
|
|
1309
|
+
if (!r || p.ups.indexOf(t) < 0) return;
|
|
1310
|
+
if (r.status >= 200 && r.status < 300) { p.ups = p.ups.filter(function (x) { return x !== t; }); paintPrep(); return; }
|
|
1311
|
+
// Refused (a 4xx), so nothing was recorded: Retry is a new operation. No answer or a 5xx: Retry is the same one.
|
|
1312
|
+
if (r.status >= 400 && r.status < 500) t.opId = '';
|
|
1313
|
+
// The staged copy is gone or differs: Retry uploads the bytes again.
|
|
1314
|
+
var code = prepCode(r);
|
|
1315
|
+
if (code === 'media_hash_mismatch' || /^media_(not_found|expired)$/.test(code)) { t.mediaId = ''; t.sha256 = ''; }
|
|
1316
|
+
t.state = 'failed';
|
|
1317
|
+
t.error = r.mismatch ? 'Not added: the Mac\u2019s COS server changed. Retry.' : r.status === 409 && code === 'revision_conflict' ? 'Not added: the prep changed. Retry.' : prepErrorText(r);
|
|
1318
|
+
paintPrep();
|
|
1319
|
+
}).catch(function (err) {
|
|
1320
|
+
if (p.ups.indexOf(t) < 0 || (err && err.code === 'aborted')) return;
|
|
1321
|
+
t.state = 'failed';
|
|
1322
|
+
t.error = err && err.code === 'kept' ? err.message : err && err.status === 401 ? 'Signed out. Sign in, then retry.' : uploadErrorText(err);
|
|
1323
|
+
paintPrep();
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
function removePrepFile(p, fileId) {
|
|
1327
|
+
if (!p.draft) return;
|
|
1328
|
+
prepQueue(p, function () {
|
|
1329
|
+
return prepWrite(p, 'DELETE', prepPath(p) + '/files/' + encodeURIComponent(fileId) + '?revision=' + encodeURIComponent(p.draft.revision));
|
|
1330
|
+
}).then(function (r) { if (r && !(r.status >= 200 && r.status < 300) && r.status !== 409) setPrepNote(p, 'Not removed. ' + prepErrorText(r), true); });
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// Prep me: the notes saved first, then .../run. The run in flight (its clientRequestId and its own params) is kept with
|
|
1334
|
+
// the draft until its outcome is known, so the next tap, a close or a reload resends the same request.
|
|
1335
|
+
function runPrep(p) {
|
|
1336
|
+
if (!p || !p.draft || p.running || prepUploading(p)) return;
|
|
1337
|
+
p.running = true; setPrepNote(p, '', false); paintPrep();
|
|
1338
|
+
var seen = p.conflicts;
|
|
1339
|
+
if (p.dirty) savePrepSteer(p);
|
|
1340
|
+
prepQueue(p, function () {
|
|
1341
|
+
// The notes changed elsewhere, or did not save: nothing runs on words the person has not seen.
|
|
1342
|
+
if (p.conflicts !== seen) return { status: -1, body: {} };
|
|
1343
|
+
if (p.dirty) return { status: -2, body: {} };
|
|
1344
|
+
if (!p.pending) {
|
|
1345
|
+
var cont = !!((p.draft.runs || []).length && p.draft.conversation && p.draft.conversation.sessionId);
|
|
1346
|
+
setPendingRun(p, { rid: uuid(), model: C.model || '', effort: C.effort || '', cont: cont });
|
|
1347
|
+
}
|
|
1348
|
+
return postRun(p, p.pending, true);
|
|
1349
|
+
}).then(function (r) { return settleRun(p, r, true); });
|
|
1350
|
+
}
|
|
1351
|
+
function runBody(p, run) {
|
|
1352
|
+
var body = { clientRequestId: run.rid, revision: p.draft.revision };
|
|
1353
|
+
if (run.model) body.model = run.model;
|
|
1354
|
+
if (run.effort) body.effort = run.effort;
|
|
1355
|
+
if (run.cont) body.continue = true;
|
|
1356
|
+
return body;
|
|
1357
|
+
}
|
|
1358
|
+
function postRun(p, run, retryLost) {
|
|
1359
|
+
var path = prepPath(p) + '/run';
|
|
1360
|
+
return prepWrite(p, 'POST', path, runBody(p, run)).then(function (r) { return r.status === 0 && retryLost ? prepWrite(p, 'POST', path, runBody(p, run)) : r; });
|
|
1361
|
+
}
|
|
1362
|
+
function settleRun(p, r, mayRedrive) {
|
|
1363
|
+
p.running = false;
|
|
1364
|
+
var code = prepCode(r), run = r.body && r.body.run;
|
|
1365
|
+
if (r.status === 202 || r.status === 200) { setPendingRun(p, null); return ended(p, run, r.body.job) || startPrepJob(p, run || {}, r.body.job || null); }
|
|
1366
|
+
if (r.status === 0) return recoverPrepRun(p, p.pending ? p.pending.rid : '');
|
|
1367
|
+
// One run at a time per draft. Still being submitted: send THAT run again, with its own params (the server
|
|
1368
|
+
// re-drives it). Already taken: show it, as its own Ask, instead of a second one.
|
|
1369
|
+
if (r.status === 409 && code === 'run_in_progress' && run) {
|
|
1370
|
+
if (run.state === 'submitting' && typeof run.clientRequestId === 'string' && mayRedrive) {
|
|
1371
|
+
setPendingRun(p, { rid: run.clientRequestId, model: run.model || '', effort: run.effort || '', cont: !!run.continue });
|
|
1372
|
+
p.running = true; paintPrep();
|
|
1373
|
+
return prepQueue(p, function () { return postRun(p, p.pending, true); }).then(function (r2) { return settleRun(p, r2, false); });
|
|
1374
|
+
}
|
|
1375
|
+
if (run.clientJobId) { setPendingRun(p, null); return startPrepJob(p, run, null); }
|
|
1376
|
+
}
|
|
1377
|
+
// The run failed on the Mac: say so plainly; the next tap is a fresh run.
|
|
1378
|
+
if (r.status === 409 && code === 'run_failed') {
|
|
1379
|
+
setPendingRun(p, null);
|
|
1380
|
+
var why = run && typeof run.error === 'string' && run.error ? run.error.replace(/_/g, ' ') : '';
|
|
1381
|
+
setPrepNote(p, 'This prep did not start on your Mac' + (why ? ' (' + why + ')' : '') + '. Tap Prep me to try again.', true);
|
|
1382
|
+
return paintPrep();
|
|
1383
|
+
}
|
|
1384
|
+
// No answer the Mac could vouch for (503, run_status_unknown): keep the run; the next tap is the same request.
|
|
1385
|
+
if (r.status >= 500) { setPrepNote(p, prepErrorText(r) + ' Tap Prep me again; it will not run twice.', true); return paintPrep(); }
|
|
1386
|
+
// Refused, so no run exists: the next tap is a new request. (-1 and -2: stopped here; the note already says why.)
|
|
1387
|
+
if (r.status > 0) setPendingRun(p, null);
|
|
1388
|
+
if (r.status !== -1 && r.status !== -2 && r.status !== 409) setPrepNote(p, prepErrorText(r), true);
|
|
1389
|
+
paintPrep();
|
|
1390
|
+
}
|
|
1391
|
+
// The run's answer was lost twice. v1.1: GET .../runs/<clientRequestId> answers {run, job} when the run was taken and
|
|
1392
|
+
// 404 run_not_accepted when it was not. A server without that route: the draft lists its runs, and the run's
|
|
1393
|
+
// clientJobId finds the job (/api/query-jobs/by-client).
|
|
1394
|
+
// 202 {run}: the Mac is still admitting it, so ask again shortly (a few times). 503 run_status_unknown: the Mac could
|
|
1395
|
+
// not tell; the next tap is the same request, so it cannot run twice.
|
|
1396
|
+
var PREP_RUN_WAITS = [1500, 1500, 3000, 3000, 5000];
|
|
1397
|
+
function recoverPrepRun(p, rid, tries) {
|
|
1398
|
+
tries = tries || 0;
|
|
1399
|
+
return prepCall(prepPath(p) + '/runs/' + encodeURIComponent(rid)).then(function (r) {
|
|
1400
|
+
if (r.status === 200 && r.body.run && (r.body.run.clientJobId || r.body.run.outcome)) { setPendingRun(p, null); return ended(p, r.body.run, r.body.job) || startPrepJob(p, r.body.run, r.body.job || null); }
|
|
1401
|
+
if (r.status === 202 && tries < PREP_RUN_WAITS.length) {
|
|
1402
|
+
setPrepNote(p, 'Your Mac is starting this prep.', false); p.running = true; paintPrep();
|
|
1403
|
+
return new Promise(function (res) { setTimeout(res, PREP_RUN_WAITS[tries]); }).then(function () { p.running = false; return recoverPrepRun(p, rid, tries + 1); });
|
|
1404
|
+
}
|
|
1405
|
+
if (r.status === 202 || (r.status === 404 && prepCode(r) === 'run_not_accepted') || (r.status === 503 && prepCode(r) === 'run_status_unknown')) return notStarted(p);
|
|
1406
|
+
return recoverFromDraft(p, rid);
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
// A run that already ended and whose job the Mac no longer keeps (outcome 'unavailable', job null): there is nothing
|
|
1410
|
+
// to poll. Its copied answer is shown when the Mac kept one; otherwise one line says so. The stored run is released
|
|
1411
|
+
// by the caller, so the next tap is a fresh run.
|
|
1412
|
+
function ended(p, run, job) {
|
|
1413
|
+
if (!run || job || !(run.outcome === 'unavailable' || run.settledAt)) return false;
|
|
1414
|
+
if (typeof run.answer === 'string' && run.answer) {
|
|
1415
|
+
var a = { cid: run.clientJobId || uuid(), query: prepQuery(p), at: Date.now(), status: 'completed', partial: '', response: run.answer, dropId: '', replyKey: '', prepDraftId: p.draft.id, atts: [] };
|
|
1416
|
+
C.asks.push(a);
|
|
1417
|
+
if (P === p) closePrep();
|
|
1418
|
+
paintFeed(true);
|
|
1419
|
+
return true;
|
|
1420
|
+
}
|
|
1421
|
+
setPrepNote(p, 'This prep finished earlier, but its answer is no longer kept. Tap Prep me to run it again.', true);
|
|
1422
|
+
paintPrep();
|
|
1423
|
+
return true;
|
|
1424
|
+
}
|
|
1425
|
+
function notStarted(p) {
|
|
1426
|
+
setPrepNote(p, 'COS on your Mac did not answer, so this prep may not have started. Tap Prep me again; it will not run twice.', true);
|
|
1427
|
+
paintPrep();
|
|
1428
|
+
}
|
|
1429
|
+
function recoverFromDraft(p, rid) {
|
|
1430
|
+
return prepCall(prepPath(p)).then(function (r) {
|
|
1431
|
+
if (r.status === 200 && r.body.draft) adoptPrep(p, r.body.draft, false);
|
|
1432
|
+
var run = r.status === 200 ? (((r.body.draft || {}).runs) || []).filter(function (x) { return x && x.clientRequestId === rid; })[0] : null;
|
|
1433
|
+
if (run && run.clientJobId) { setPendingRun(p, null); return startPrepJob(p, run, null); }
|
|
1434
|
+
notStarted(p);
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
// v1.1: the job is an ordinary web Ask titled "Prep: <title>"; the chat shows the same words, and the focus under them.
|
|
1438
|
+
function prepQuery(p) {
|
|
1439
|
+
var steer = String((p.draft && p.draft.steer) || '').trim();
|
|
1440
|
+
return 'Prep: ' + p.title + (steer ? '\n' + clip(steer, 160) : '');
|
|
1441
|
+
}
|
|
1442
|
+
function startPrepJob(p, run, job) {
|
|
1443
|
+
var cid = (run && run.clientJobId) || (job && job.clientJobId) || '';
|
|
1444
|
+
var a = { cid: cid || uuid(), query: prepQuery(p), at: Date.now(), status: 'sending', partial: '', dropId: '', replyKey: '', prepDraftId: p.draft.id,
|
|
1445
|
+
atts: prepFiles(p.draft).filter(function (f) { return f.status === 'ready'; }).map(function (f) { return { name: f.name, kind: f.kind === 'image' ? 'photo' : 'doc' }; }) };
|
|
1446
|
+
C.asks.push(a);
|
|
1447
|
+
if (P === p) closePrep();
|
|
1448
|
+
paintFeed(true);
|
|
1449
|
+
if (job && job.jobId) { take(a, job); paintAsk(a); poll(a); return a; }
|
|
1450
|
+
if (!cid) { a.status = 'failed'; a.error = 'Not sent: the Mac did not say which answer is this prep. Tap Prep me on the card again.'; paintFeed(false); return a; }
|
|
1451
|
+
api('/api/query-jobs/by-client/' + encodeURIComponent(cid) + '?generation=1').then(function (d) {
|
|
1452
|
+
if (!d || !d.job) throw new Error('the Mac did not find the prep answer');
|
|
1453
|
+
take(a, d.job); paintAsk(a); poll(a);
|
|
1454
|
+
}).catch(function (err) {
|
|
1455
|
+
a.status = 'failed';
|
|
1456
|
+
a.error = 'Not sent: ' + ((err && err.message) || 'the Mac did not answer') + '. Tap Prep me on the card again; it will not run twice.';
|
|
1457
|
+
paintFeed(false);
|
|
1458
|
+
});
|
|
1459
|
+
return a;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Archive this prep (DELETE /api/prep/drafts/:id, the server keeps it as archived): it frees one of the drafts the Mac
|
|
1463
|
+
// keeps, and the card's Prep me then starts a new one. Nothing a meeting already took is touched.
|
|
1464
|
+
function archivePrep(p) {
|
|
1465
|
+
if (!p.draft || p.running || p.archiving) return;
|
|
1466
|
+
p.archiving = true; setPrepNote(p, 'Archiving this prep…', false);
|
|
1467
|
+
clearTimeout(p.timer);
|
|
1468
|
+
prepQueue(p, function () {
|
|
1469
|
+
return prepCall(prepPath(p) + '?revision=' + encodeURIComponent(p.draft.revision), { method: 'DELETE' });
|
|
1470
|
+
}).then(function (r) {
|
|
1471
|
+
p.archiving = false;
|
|
1472
|
+
if (r.status >= 200 && r.status < 300) {
|
|
1473
|
+
forgetPrepRec(p.key);
|
|
1474
|
+
p.dirty = false; p.pending = null;
|
|
1475
|
+
if (P === p) closePrep();
|
|
1476
|
+
W.toast('Prep archived. Prep me on the card starts a new one.');
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
var conflict = r.status === 409 && prepCode(r) === 'revision_conflict';
|
|
1480
|
+
if (conflict && r.body.draft) return adoptPrep(p, r.body.draft, true);
|
|
1481
|
+
setPrepNote(p, 'Not archived. ' + prepErrorText(r), true);
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// ── the Prep composer on the page ─────────────────────
|
|
1486
|
+
// It sits above the chat's composer, outside the feed, so a poll that redraws the feed never takes the words or the
|
|
1487
|
+
// caret. Built once per draft; after that only its parts are written.
|
|
1488
|
+
function prepSlot(create) {
|
|
1489
|
+
var chat = C.host && C.host.querySelector && C.host.querySelector('[data-cos-chat]');
|
|
1490
|
+
if (!chat) return null;
|
|
1491
|
+
var slot = chat.querySelector('[data-cos-prep]');
|
|
1492
|
+
if (!P) { if (slot) slot.remove(); return null; }
|
|
1493
|
+
if (!slot && create !== false) {
|
|
1494
|
+
slot = document.createElement('div');
|
|
1495
|
+
slot.className = 'cc-prep'; slot.setAttribute('data-cos-prep', '');
|
|
1496
|
+
chat.insertBefore(slot, chat.querySelector('[data-cos-form="chat"]'));
|
|
1497
|
+
}
|
|
1498
|
+
return slot;
|
|
1499
|
+
}
|
|
1500
|
+
function prepBox() { var slot = prepSlot(false); return slot && slot.querySelector('[data-cos-prep-steer]'); }
|
|
1501
|
+
function focusPrep() { var box = prepBox(); if (box && keyboardFirst()) box.focus(); }
|
|
1502
|
+
function prepHeadLine(p) {
|
|
1503
|
+
var when = prepWhen(p.startsAt);
|
|
1504
|
+
return (when ? 'Starts ' + when + '. ' : '') + 'Add what matters and any files, then Prep me. Add them to the meeting from its live view.';
|
|
1505
|
+
}
|
|
1506
|
+
function prepPanelHtml(p) {
|
|
1507
|
+
return '<section class="mt-panel cc-prep-panel" aria-label="' + esc('Prep for ' + p.title) + '">' +
|
|
1508
|
+
'<div class="mt-ask-head"><h3 class="mt-ask-title cc-prep-title">' + esc('Prep for ' + p.title) + '</h3>' +
|
|
1509
|
+
'<span class="cc-prep-head-acts"><button type="button" class="mt-link" data-cos="prep-archive">Archive this prep</button>' +
|
|
1510
|
+
'<button type="button" class="mt-link" data-cos="prep-close">Close</button></span></div>' +
|
|
1511
|
+
'<p class="mt-ask-line">' + esc(prepHeadLine(p)) + '</p>' +
|
|
1512
|
+
'<form class="mt-composer cc-prep-form" data-cos-prep-form autocomplete="off">' +
|
|
1513
|
+
'<textarea class="field" rows="3" data-cos-prep-steer maxlength="' + prepChars() + '" placeholder="What should this prep focus on?" aria-label="What should this prep focus on?">' + esc(p.steer) + '</textarea></form>' +
|
|
1514
|
+
'<div class="cc-prep-acts"><span class="mt-muted cc-prep-count" data-prep-count>' + esc(prepCountText(p)) + '</span>' +
|
|
1515
|
+
'<label class="cx-btn quiet mt-addfile cc-prep-add" title="Add a screenshot or files to this prep">' + '<svg class="icon" aria-hidden="true"><use href="#i-clip"/></svg><span>Add file</span>' +
|
|
1516
|
+
'<input type="file" class="cc-file" multiple accept="' + PREP_ACCEPT + '" data-cos-prep-file aria-label="Add files to this prep"></label>' +
|
|
1517
|
+
'<button type="button" class="cx-btn primary" data-cos="prep-run">' + esc(prepRunLabel(p)) + '</button></div>' +
|
|
1518
|
+
'<p class="' + (p.warn ? 'mt-amber-line' : 'mt-muted') + ' cc-prep-note" data-prep-note role="status"' + (p.note ? '' : ' hidden') + '>' + esc(p.note) +
|
|
1519
|
+
(p.signedOut ? ' <button type="button" class="link-btn cc-text-btn" data-cos="sign-in">Sign in again</button>' : '') + '</p>' +
|
|
1520
|
+
// The box and Prep me first, so both show as soon as it opens; the files list under them grows downward.
|
|
1521
|
+
'<div data-prep-files>' + prepFilesHtml(p) + '</div>' +
|
|
1522
|
+
'</section>';
|
|
1523
|
+
}
|
|
1524
|
+
function prepFileMeta(f) {
|
|
1525
|
+
if (f.status === 'ready') return [f.notice || '', Number(f.bytes) > 0 ? sizeLabel(Number(f.bytes)) : ''].filter(Boolean).join(' · ');
|
|
1526
|
+
return f.notice || PREP_FILE_STATE[f.status] || '';
|
|
1527
|
+
}
|
|
1528
|
+
function prepUpMeta(t) {
|
|
1529
|
+
if (t.state === 'reading') return t.kind === 'photo' ? 'Preparing the image' : 'Preparing';
|
|
1530
|
+
if (t.state === 'uploading') return 'Uploading ' + Math.round(t.progress * 100) + '%';
|
|
1531
|
+
if (t.state === 'adding') return t.kind === 'photo' ? 'Reading the text in the image' : 'Adding to the prep';
|
|
1532
|
+
return t.error || 'Did not upload';
|
|
1533
|
+
}
|
|
1534
|
+
function prepFilesHtml(p) {
|
|
1535
|
+
var files = prepFiles(p.draft), ups = p.ups;
|
|
1536
|
+
var title = p.loading ? 'Opening this prep…' : files.length ? 'Files · ' + files.length + (ups.length ? ' · adding ' + ups.filter(function (t) { return t.state !== 'failed'; }).length : '')
|
|
1537
|
+
: ups.length ? 'Adding files' : 'No files yet. Add file, paste or drop one here.';
|
|
1538
|
+
var rows = files.map(function (f) {
|
|
1539
|
+
var meta = prepFileMeta(f);
|
|
1540
|
+
return '<li class="mt-up ready"><span class="mt-files-name">' + esc(f.name) + '</span>' + (meta ? '<span class="mt-up-meta mt-muted">' + esc(meta) + '</span>' : '') +
|
|
1541
|
+
'<span class="mt-up-acts"><button type="button" class="mt-link" data-cos="prep-remove" data-file="' + esc(f.id) + '" aria-label="' + esc('Remove ' + f.name) + '">Remove</button></span></li>';
|
|
1542
|
+
}).concat(ups.map(function (t) {
|
|
1543
|
+
return '<li class="mt-up ' + t.state + '" data-prep-up="' + esc(t.id) + '"><span class="mt-files-name">' + esc(t.name) + '</span>' +
|
|
1544
|
+
'<span class="mt-up-meta ' + (t.state === 'failed' ? 'mt-amber' : 'mt-muted') + '" data-up-meta>' + esc(prepUpMeta(t)) + '</span>' +
|
|
1545
|
+
'<span class="mt-up-acts">' + (t.state === 'failed' ? '<button type="button" class="mt-link" data-cos="prep-retry" data-up="' + esc(t.id) + '">Retry</button>' : '') +
|
|
1546
|
+
'<button type="button" class="mt-link" data-cos="prep-cancel" data-up="' + esc(t.id) + '" aria-label="' + esc('Remove ' + t.name) + '">' + (t.state === 'failed' ? 'Remove' : 'Cancel') + '</button></span>' +
|
|
1547
|
+
(t.state === 'uploading' || t.state === 'reading' ? '<span class="mt-up-bar" data-up-bar style="width:' + Math.round(t.progress * 100) + '%"></span>' : '') + '</li>';
|
|
1548
|
+
}));
|
|
1549
|
+
return '<div class="mt-files live"><div class="mt-files-head quiet"><svg class="icon" aria-hidden="true"><use href="#i-clip"/></svg><span>' + esc(title) + '</span></div>' +
|
|
1550
|
+
(rows.length ? '<ul class="mt-files-list">' + rows.join('') + '</ul>' : '') + '</div>';
|
|
1551
|
+
}
|
|
1552
|
+
function prepCountText(p) {
|
|
1553
|
+
var n = p.steer.length, max = prepChars();
|
|
1554
|
+
return n >= max - 400 ? n.toLocaleString() + ' of ' + max.toLocaleString() + ' characters' : '';
|
|
1555
|
+
}
|
|
1556
|
+
function prepRunLabel(p) { return p.running ? 'Prepping…' : prepUploading(p) ? 'Uploading…' : 'Prep me'; }
|
|
1557
|
+
function setPrepNote(p, text, warn) {
|
|
1558
|
+
p.note = text || ''; p.warn = !!warn && !!text;
|
|
1559
|
+
p.signedOut = /signed out/i.test(p.note);
|
|
1560
|
+
if (P === p) paintPrepParts(p);
|
|
1561
|
+
}
|
|
1562
|
+
function paintPrep(rebuild) {
|
|
1563
|
+
var slot = prepSlot(!!P);
|
|
1564
|
+
if (!slot || !P) return;
|
|
1565
|
+
if (rebuild || slot.getAttribute('data-key') !== P.key) {
|
|
1566
|
+
slot.setAttribute('data-key', P.key);
|
|
1567
|
+
slot.innerHTML = prepPanelHtml(P);
|
|
1568
|
+
var box = slot.querySelector('[data-cos-prep-steer]'); if (box) W.autoGrow(box);
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
var files = slot.querySelector('[data-prep-files]'); if (files) files.innerHTML = prepFilesHtml(P);
|
|
1572
|
+
paintPrepParts(P);
|
|
1573
|
+
}
|
|
1574
|
+
// The button, the count and the note: never the box (the words and the caret stay where they are).
|
|
1575
|
+
function paintPrepParts(p) {
|
|
1576
|
+
var slot = P === p ? prepSlot(false) : null; if (!slot) return;
|
|
1577
|
+
var run = slot.querySelector('[data-cos="prep-run"]');
|
|
1578
|
+
if (run) { run.disabled = !p.draft || p.running || prepUploading(p); run.textContent = prepRunLabel(p); }
|
|
1579
|
+
var count = slot.querySelector('[data-prep-count]'); if (count) count.textContent = prepCountText(p);
|
|
1580
|
+
var pick = slot.querySelector('[data-cos-prep-file]'); if (pick) pick.disabled = !p.draft || prepFiles(p.draft).length + p.ups.length >= prepMax();
|
|
1581
|
+
var note = slot.querySelector('[data-prep-note]');
|
|
1582
|
+
if (note) {
|
|
1583
|
+
note.className = (p.warn ? 'mt-amber-line' : 'mt-muted') + ' cc-prep-note';
|
|
1584
|
+
note.innerHTML = esc(p.note) + (p.signedOut ? ' <button type="button" class="link-btn cc-text-btn" data-cos="sign-in">Sign in again</button>' : '');
|
|
1585
|
+
note.hidden = !p.note;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function paintPrepProgress(t) {
|
|
1589
|
+
var slot = prepSlot(false); if (!slot) return;
|
|
1590
|
+
var row = slot.querySelector('[data-prep-up="' + t.id + '"]'); if (!row) return;
|
|
1591
|
+
var meta = row.querySelector('[data-up-meta]'); if (meta) meta.textContent = prepUpMeta(t);
|
|
1592
|
+
var bar = row.querySelector('[data-up-bar]'); if (bar) bar.style.width = Math.round(t.progress * 100) + '%';
|
|
1593
|
+
}
|
|
1594
|
+
function onPrepClick(t, what) {
|
|
1595
|
+
var p = P; if (!p) return;
|
|
1596
|
+
if (what === 'prep-close') return closePrep();
|
|
1597
|
+
if (what === 'prep-run') return runPrep(p);
|
|
1598
|
+
if (what === 'prep-archive') return archivePrep(p);
|
|
1599
|
+
if (what === 'prep-remove') return removePrepFile(p, t.getAttribute('data-file'));
|
|
1600
|
+
var up = p.ups.filter(function (x) { return x.id === t.getAttribute('data-up'); })[0];
|
|
1601
|
+
if (!up) return;
|
|
1602
|
+
if (what === 'prep-retry' && up.state === 'failed') return sendPrepFile(p, up);
|
|
1603
|
+
if (what === 'prep-cancel') {
|
|
1604
|
+
if (up.xhr) { try { up.xhr.abort(); } catch (_e) {} }
|
|
1605
|
+
p.ups = p.ups.filter(function (x) { return x !== up; });
|
|
1606
|
+
paintPrep();
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
function onPrepDrag(e, slot) {
|
|
1610
|
+
var types = e.dataTransfer && e.dataTransfer.types;
|
|
1611
|
+
if (!(types && Array.prototype.indexOf.call(types, 'Files') >= 0)) return;
|
|
1612
|
+
e.preventDefault();
|
|
1613
|
+
if (e.type === 'dragover' || e.type === 'dragenter') { slot.classList.add('dragging'); return; }
|
|
1614
|
+
slot.classList.remove('dragging');
|
|
1615
|
+
if (e.type === 'drop') addPrepFiles(e.dataTransfer.files);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1022
1618
|
// ── events (delegated from the Messages pane) ─────────
|
|
1023
1619
|
function onClick(t) {
|
|
1024
1620
|
var what = t.getAttribute('data-cos');
|
|
1621
|
+
if (/^prep-/.test(what || '')) return onPrepClick(t, what);
|
|
1025
1622
|
if (what === 'ask') return doAsk(kOf(t));
|
|
1026
1623
|
if (what === 'drop') return doDrop();
|
|
1027
1624
|
if (what === 'all') return W.go('messages');
|
|
@@ -1060,6 +1657,7 @@
|
|
|
1060
1657
|
doAsk(k);
|
|
1061
1658
|
}
|
|
1062
1659
|
function onInput(e) {
|
|
1660
|
+
if (e.target && e.target.hasAttribute && e.target.hasAttribute('data-cos-prep-steer')) return onPrepInput(e.target);
|
|
1063
1661
|
if (isComposerInput(e.target)) { var k = kOf(e.target); k.draft = e.target.value; saveDraft(k); W.autoGrow(e.target); paintButtons(k); }
|
|
1064
1662
|
}
|
|
1065
1663
|
function onChangeEvent(e) {
|
|
@@ -1068,6 +1666,7 @@
|
|
|
1068
1666
|
if (t.hasAttribute('data-cos-model')) { C.model = t.value; save(); syncMenus(); }
|
|
1069
1667
|
else if (t.hasAttribute('data-cos-effort')) { C.effort = t.value; save(); syncMenus(); }
|
|
1070
1668
|
else if (t.hasAttribute('data-cos-file')) { addFiles(kOf(t), t.files); t.value = ''; }
|
|
1669
|
+
else if (t.hasAttribute('data-cos-prep-file')) { addPrepFiles(t.files); t.value = ''; }
|
|
1071
1670
|
}
|
|
1072
1671
|
// Two composers can be on screen (the chat beside a reader is not, but a reload can leave either): keep both menus true.
|
|
1073
1672
|
function syncMenus() {
|
|
@@ -1078,15 +1677,23 @@
|
|
|
1078
1677
|
var f = e.target.closest && e.target.closest('[data-cos-steer]');
|
|
1079
1678
|
if (f) { e.preventDefault(); steer(f.getAttribute('data-cos-steer'), (f.querySelector('input') || {}).value || ''); return true; }
|
|
1080
1679
|
if (e.target.matches && e.target.matches('[data-cos-form]')) { e.preventDefault(); return true; }
|
|
1680
|
+
if (e.target.matches && e.target.matches('[data-cos-prep-form]')) { e.preventDefault(); return true; }
|
|
1081
1681
|
return false;
|
|
1082
1682
|
}
|
|
1083
1683
|
// Paste a photo or a file into the box; drop files on the composer.
|
|
1084
1684
|
function onPaste(e) {
|
|
1685
|
+
if (e.target && e.target.hasAttribute && e.target.hasAttribute('data-cos-prep-steer')) {
|
|
1686
|
+
var pf = e.clipboardData && e.clipboardData.files;
|
|
1687
|
+
if (pf && pf.length) { e.preventDefault(); addPrepFiles(pf); }
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1085
1690
|
if (!isComposerInput(e.target)) return;
|
|
1086
1691
|
var files = e.clipboardData && e.clipboardData.files;
|
|
1087
1692
|
if (files && files.length) { e.preventDefault(); addFiles(kOf(e.target), files); }
|
|
1088
1693
|
}
|
|
1089
1694
|
function onDrag(e) {
|
|
1695
|
+
var prep = e.target && e.target.closest && e.target.closest('[data-cos-prep]');
|
|
1696
|
+
if (prep) return onPrepDrag(e, prep);
|
|
1090
1697
|
var form = e.target && e.target.closest && e.target.closest('[data-cos-form]');
|
|
1091
1698
|
if (!form) return;
|
|
1092
1699
|
var types = e.dataTransfer && e.dataTransfer.types;
|
|
@@ -1113,6 +1720,12 @@
|
|
|
1113
1720
|
setOnChange: function (fn) { C.onChange = fn; }, setOnAnswered: function (fn) { C.onAnswered = fn; },
|
|
1114
1721
|
// Test and render hooks: what the chat holds right now.
|
|
1115
1722
|
classify: classify, uploadMedia: uploadMedia, uploadErrorText: uploadErrorText, uuid: uuid,
|
|
1723
|
+
// 6.99.19: Prep. meetings.js reads the capability from here (one /api/health read for the page).
|
|
1724
|
+
prepCaps: prepCaps,
|
|
1725
|
+
// The Prep server id to name on a write: the newest from capabilities.prep or any Prep answer (meetings.js reports its own).
|
|
1726
|
+
prepInstance: function () { return PREP_INST; }, notePrepInstance: function (id) { if (typeof id === 'string' && id) PREP_INST = id; },
|
|
1727
|
+
_prep: function () { return P; }, _openPrep: openPrep, _prepHtml: function () { return P ? prepPanelHtml(P) : ''; }, _addPrepFiles: addPrepFiles,
|
|
1728
|
+
_runPrep: function () { runPrep(P); }, _sha256Js: sha256Js, _closePrep: closePrep, _onPrepInput: onPrepInput, _savePrepSteer: function () { return P ? savePrepSteer(P) : null; },
|
|
1116
1729
|
_state: C, _composer: composer, _classify: classify, _modelsFrom: modelsFrom, _addFiles: addFiles, _itemHtml: itemHtml, _setCaps: setCaps, _threadLine: threadLine, _postTurn: postTurn
|
|
1117
1730
|
};
|
|
1118
1731
|
})();
|