@gotcos/glasses-server 6.99.12 → 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 +124 -0
- package/package.json +1 -1
- package/server/index.ts +44 -2
- package/server/lib/ask-work-plan.ts +116 -0
- package/server/lib/batch-transcript-quality.ts +2 -0
- package/server/lib/cos-thread.ts +582 -0
- package/server/lib/drop-attachments.ts +55 -0
- package/server/lib/drop-service.ts +104 -13
- package/server/lib/drop-settings.ts +13 -2
- package/server/lib/drop-store.ts +38 -2
- package/server/lib/media-store.ts +80 -0
- package/server/lib/meeting-ask-runtime.ts +1 -1
- package/server/lib/meeting-batch-transcribe.ts +54 -2
- package/server/lib/permission-broker.ts +6 -4
- package/server/lib/phone-recording.ts +1 -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/cos-thread.ts +79 -0
- package/server/routes/drops.ts +4 -2
- package/server/routes/health.ts +8 -0
- package/server/routes/permission-broker.ts +2 -2
- package/server/routes/prep.ts +81 -0
- package/server/routes/transcribe-stream.ts +9 -1
- package/server/routes/welcome-context.ts +29 -1
- package/server/routes/work-orchestrator.ts +57 -0
- package/server/scripts/web-live-cls/cdp.mjs +57 -0
- package/server/scripts/web-live-cls/measure.mjs +80 -0
- package/server/scripts/web-live-cls/mock.mjs +97 -0
- package/server/scripts/web-live-cls/render.mjs +26 -0
- package/server/web-control/control.css +13 -0
- package/server/web-control/panes/cos-chat.css +39 -0
- package/server/web-control/panes/cos-chat.js +799 -41
- package/server/web-control/panes/meetings.css +91 -4
- package/server/web-control/panes/meetings.js +585 -58
- package/server/web-control/panes/messages.js +25 -18
- package/shared/meeting-ask.ts +6 -2
- package/shared/prep.ts +317 -0
|
@@ -11,14 +11,24 @@
|
|
|
11
11
|
Steer and Cancel are the existing query-job routes. The Model and Effort menus come from /api/models (6.99.12).
|
|
12
12
|
- Drop (outlined, Drop it on only): POST /api/drops, then the receipt with Undo and Move. A drop the server reads as a
|
|
13
13
|
question runs the Ask the same way and is linked back (POST /api/drops/:id/ask), as COS Control does.
|
|
14
|
+
6.99.13: Drop takes the composer's files too (up to capabilities.dropIt.attachments, 5), as attachmentIds. The
|
|
15
|
+
server keeps them with the drop; the receipt lists each one with Open, and Undo removes them. A drop read as a
|
|
16
|
+
question sends its files with the Ask. An older server without that field keeps the words-only note.
|
|
14
17
|
- The clip (6.99.12) attaches photos, PDF, TXT, MD, CSV, JSON, XLSX, MP4 and MOV, as the Even app does: photos are
|
|
15
18
|
resized to a 1024 px JPEG and posted to /api/media, other files go raw to /api/media/file, and the Ask carries up to
|
|
16
|
-
five attachmentIds. Paste and drag-drop add files too.
|
|
19
|
+
five attachmentIds. Paste and drag-drop add files too. Drop sends them too where the server says it takes them.
|
|
17
20
|
With a keyboard, Return asks and Option-Return drops; Shift-Return is a new line.
|
|
18
21
|
|
|
19
22
|
The same composer answers in the message reader (6.99.12): Reply sends that turn's sessionId, so the answer continues
|
|
20
23
|
that conversation when it is still live (lib/web-ask-admission.ts).
|
|
21
24
|
|
|
25
|
+
6.99.16, the COS thread (capabilities.dropIt.thread, routes/cos-thread.ts): each Ask from the chat's own composer
|
|
26
|
+
reads GET /api/cos-thread first. `continue` sends that sessionId; `fresh` sends none and carries the server's digest
|
|
27
|
+
as the Ask's `reference` (the query-jobs field for quoted context, so the Messages row shows only the question).
|
|
28
|
+
While an Ask in the thread is still answering, the next one continues its conversation without asking. When an Ask
|
|
29
|
+
finishes, POST /api/cos-thread/turn once with its answer. "New thread" in the header posts /api/cos-thread/new.
|
|
30
|
+
Without the capability, the old rule stands: one conversation per day, kept in this browser.
|
|
31
|
+
|
|
22
32
|
The rail badge on Messages is a number for `waiting` (GET /api/inbox/summary) and a dot for unread only. It is read
|
|
23
33
|
with the shell's existing status read (shell.js checkServer), never on a timer of its own, and painting it touches
|
|
24
34
|
only the badge, never the pane. */
|
|
@@ -41,7 +51,10 @@
|
|
|
41
51
|
models: null, // [{ value, label, available }]
|
|
42
52
|
host: null,
|
|
43
53
|
onChange: null, // messages.js: re-render when Drop it or Ask turns on or off
|
|
44
|
-
onAnswered: null
|
|
54
|
+
onAnswered: null, // messages.js: re-read Recent when a web Ask lands
|
|
55
|
+
thread: null, // 6.99.16: the last GET /api/cos-thread answer
|
|
56
|
+
threadCarried: '', // the thread's newest session started fresh: 'summary' (it carried a digest) or 'plain'
|
|
57
|
+
threadBusy: false
|
|
45
58
|
};
|
|
46
59
|
var saved = W.readStore(STORE);
|
|
47
60
|
C.model = typeof saved.model === 'string' ? saved.model : '';
|
|
@@ -76,22 +89,42 @@
|
|
|
76
89
|
// ── capabilities and the rail badge ───────────────────
|
|
77
90
|
// Drop it on: the pinned COS chat. Ask on (durable query jobs): the composer, with or without Drop it.
|
|
78
91
|
function dropOn() { return !!(C.caps && C.caps.dropIt); }
|
|
92
|
+
function dropFiles() { return C.caps && C.caps.dropIt && C.caps.dropFiles > 0 ? C.caps.dropFiles : 0; }
|
|
79
93
|
function askOn() { return !!(C.caps && C.caps.jobs); }
|
|
80
94
|
function on() { return dropOn() || askOn(); }
|
|
95
|
+
// 6.99.16: the COS thread, advertised inside dropIt while Drop it is on.
|
|
96
|
+
function threadOn() { return !!(C.caps && C.caps.dropIt && C.caps.thread); }
|
|
81
97
|
function setCaps(h) {
|
|
82
98
|
var caps = (h && h.capabilities) || {};
|
|
83
99
|
var drop = caps.dropIt;
|
|
84
100
|
var next = {
|
|
85
101
|
dropIt: !!(drop && drop.enabled === true),
|
|
86
|
-
|
|
87
|
-
|
|
102
|
+
// 6.99.13: how many files a drop takes. 0 (an older server): Drop is words only and says so.
|
|
103
|
+
dropFiles: drop && drop.enabled === true && Number.isInteger(drop.attachments) && drop.attachments > 0 ? Math.min(drop.attachments, MAX_ATTS) : 0,
|
|
104
|
+
// 6.99.16: /api/health carries it at features.durableQueryJobs (a boolean); /api/models at capabilities.durableQueryJobs.enabled.
|
|
105
|
+
jobs: caps.durableQueryJobs === true || h && h.durableQueryJobs === true || !!(caps.durableQueryJobs && caps.durableQueryJobs.enabled) ||
|
|
106
|
+
!!(h && h.features && h.features.durableQueryJobs === true),
|
|
107
|
+
steer: !!(C.caps && C.caps.steer),
|
|
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)
|
|
88
111
|
};
|
|
89
|
-
|
|
112
|
+
if (next.prep && next.prep.serverInstanceId) PREP_INST = next.prep.serverInstanceId;
|
|
113
|
+
var changed = !C.caps || C.caps.dropIt !== next.dropIt || C.caps.jobs !== next.jobs || C.caps.thread !== next.thread;
|
|
114
|
+
var filesChanged = !!C.caps && C.caps.dropFiles !== next.dropFiles;
|
|
90
115
|
C.caps = next;
|
|
91
116
|
if (!next.dropIt) { clearRail(); C.summary = null; }
|
|
92
117
|
if (changed && C.onChange) C.onChange();
|
|
118
|
+
else if (filesChanged) paintButtons(CHAT);
|
|
93
119
|
if (next.dropIt) refreshSummary();
|
|
94
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
|
+
}
|
|
95
128
|
// The shell's status read (shell.js checkServer) hands its /api/health answer here.
|
|
96
129
|
function fromHealth(h) { setCaps(h); }
|
|
97
130
|
function ensureCaps() {
|
|
@@ -260,14 +293,16 @@
|
|
|
260
293
|
}
|
|
261
294
|
|
|
262
295
|
// ── the chat ──────────────────────────────────────────
|
|
263
|
-
function chatMode() { return dropOn() ? 'drop' : 'ask'; }
|
|
296
|
+
function chatMode() { return dropOn() ? (threadOn() ? 'drop-thread' : 'drop') : 'ask'; }
|
|
264
297
|
function chatSkeleton() {
|
|
265
298
|
var drop = dropOn();
|
|
266
299
|
return '<section class="cc" data-cos-chat data-mode="' + chatMode() + '" data-tint="messages" aria-label="' + (drop ? 'COS chat' : 'Ask COS') + '">' +
|
|
267
300
|
'<header class="cc-head"><span class="cc-avatar big">' + MARK + '</span><div class="cc-head-text"><h2 class="cc-title">' + (drop ? 'COS' : 'Ask COS') + '</h2>' +
|
|
268
301
|
'<p class="cc-sub">' + (drop ? 'Text it anything: a thought, an ask, a reminder. It texts back.' : 'Ask anything, with photos or files. Each answer lands in Messages with its number, here and on the glasses.') + '</p></div>' +
|
|
269
302
|
'<div class="cc-head-acts"><button type="button" class="link-btn cc-all" data-cos="all">All messages</button>' +
|
|
270
|
-
(drop ? '<button type="button" class="link-btn" data-cos="read-all">Mark all read</button>' : '') +
|
|
303
|
+
(drop ? '<button type="button" class="link-btn" data-cos="read-all">Mark all read</button>' : '') +
|
|
304
|
+
(threadOn() ? '<button type="button" class="link-btn" data-cos="new-thread">New thread</button>' : '') + '</div>' +
|
|
305
|
+
(threadOn() ? '<p class="cc-thread" data-cos-thread role="status">' + esc(threadLine()) + '</p>' : '') + '</header>' +
|
|
271
306
|
'<div class="cc-feed" data-cos-feed aria-live="polite"></div>' +
|
|
272
307
|
composerHtml(CHAT, { drop: drop, placeholder: drop ? 'Ask COS, or drop a thought, a task, a reminder' : 'Ask COS anything', label: 'Message to COS' }) +
|
|
273
308
|
'</section>';
|
|
@@ -280,9 +315,11 @@
|
|
|
280
315
|
if (!el || el.getAttribute('data-mode') !== chatMode()) {
|
|
281
316
|
host.innerHTML = chatSkeleton();
|
|
282
317
|
hydrate(CHAT);
|
|
318
|
+
if (P) paintPrep(true);
|
|
283
319
|
}
|
|
284
320
|
if (!visible) return;
|
|
285
321
|
loadModels();
|
|
322
|
+
if (threadOn()) loadThread();
|
|
286
323
|
// Ask only: no inbox to read; the chat holds this page's Asks. Inbox lines from a Drop it session are not shown.
|
|
287
324
|
if (!dropOn()) { if (C.feedMode !== 'ask') { C.items = []; C.feedState = 'ready'; C.feedMode = 'ask'; } paintFeed(true); return; }
|
|
288
325
|
if (C.items === null || C.feedState === 'error' || C.feedMode !== 'drop') loadFeed(); else paintFeed(true);
|
|
@@ -338,7 +375,8 @@
|
|
|
338
375
|
}).join('') + '</ol>';
|
|
339
376
|
}
|
|
340
377
|
var rest = acts.filter(function (a) { return !/^choose:/.test(a.id); });
|
|
341
|
-
|
|
378
|
+
// A P2 follow-up already carries its own Dismiss (6.99.11); add one only to an item that does not (6.99.15: it showed twice).
|
|
379
|
+
if (item.needsReply && !rest.some(function (a) { return a.id === 'dismiss'; })) rest.push({ id: 'dismiss', label: 'Dismiss' });
|
|
342
380
|
if (rest.length) {
|
|
343
381
|
html += '<div class="cc-acts">' + rest.map(function (a) {
|
|
344
382
|
var cls = PRIMARY[a.id] ? 'cx-btn primary' : 'link-btn cc-text-btn';
|
|
@@ -360,16 +398,30 @@
|
|
|
360
398
|
return '<button type="button" class="cc-choice" data-cos="move-to" data-item="' + esc(item.id) + '" data-dest="' + o.dest + '"' + (o.domain ? ' data-domain="' + esc(o.domain) + '"' : '') + '>' + esc(o.label) + '</button>';
|
|
361
399
|
}).join('') + '<button type="button" class="link-btn cc-text-btn" data-cos="move-cancel">Keep it where it is</button></div>';
|
|
362
400
|
}
|
|
401
|
+
// 6.99.13: a drop's files, each opening from the media store in a new tab. Under your words they are what you sent;
|
|
402
|
+
// on a receipt each says Open. Undo takes the links away with the files. The classes are cc-dfile*, never cc-file:
|
|
403
|
+
// .cc-file is the clip's invisible file input (opacity 0, absolute), and a link wearing it does not show.
|
|
404
|
+
function fileLinksHtml(item, mine) {
|
|
405
|
+
var files = (item.links || []).filter(function (l) { return l.kind === 'file' && MEDIA_ID.test(String(l.id)); });
|
|
406
|
+
if (!files.length) return '';
|
|
407
|
+
return '<div class="cc-dfiles' + (mine ? ' mine' : '') + '">' + files.map(function (f) {
|
|
408
|
+
var url = '/api/media/' + encodeURIComponent(f.id) + '/content';
|
|
409
|
+
var photo = /\.(jpe?g|png|gif|webp|heic|heif)$/i.test(String(f.label)) || f.label === 'Photo';
|
|
410
|
+
return '<a class="cc-dfile" href="' + esc(url + '?variant=phone') + '" target="_blank" rel="noopener" aria-label="Open ' + esc(f.label) + '">' +
|
|
411
|
+
'<span class="cc-dfile-thumb">' + (photo ? '<img src="' + esc(url + '?variant=thumb') + '" alt="" loading="lazy">' : '<svg class="icon" aria-hidden="true"><use href="#i-doc"/></svg>') + '</span>' +
|
|
412
|
+
'<span class="cc-dfile-name">' + esc(f.label) + '</span>' + (mine ? '' : '<span class="cc-dfile-open">Open</span>') + '</a>';
|
|
413
|
+
}).join('') + '</div>';
|
|
414
|
+
}
|
|
363
415
|
function itemHtml(item) {
|
|
364
416
|
if (item.kind === 'drop' || item.kind === 'answer') {
|
|
365
417
|
return '<div class="cc-line you' + (item.kind === 'answer' ? ' answer' : '') + '"' + (item.kind === 'drop' ? ' data-drop-thread="' + esc(item.threadId) + '"' : '') + '><div class="cc-bubble">' + esc(item.line) + '</div>' +
|
|
366
|
-
(item.kind === 'drop' ? '<div class="cc-src">' + esc(SOURCE[item.source] || '') + '</div>' : '') + '</div>';
|
|
418
|
+
(item.kind === 'drop' ? fileLinksHtml(item, true) + '<div class="cc-src">' + esc(SOURCE[item.source] || '') + '</div>' : '') + '</div>';
|
|
367
419
|
}
|
|
368
420
|
var quote = item.kind === 'followup' ? (function () { var d = dropOf(item); return d ? '<button type="button" class="cc-quote" data-cos="quote" data-thread="' + esc(item.threadId) + '">Re: ' + esc(clip(d.line, 70)) + '</button>' : ''; })() : '';
|
|
369
421
|
return '<div class="cc-line cos' + (item.state !== 'live' ? ' done' : '') + (item.needsReply && item.state === 'live' ? ' waits' : '') + '" data-item-id="' + esc(item.id) + '">' +
|
|
370
422
|
'<span class="cc-avatar">' + MARK + '</span><div class="cc-card">' + quote +
|
|
371
423
|
(item.title ? '<p class="cc-card-title">' + esc(item.title) + '</p>' : '') +
|
|
372
|
-
'<p class="cc-card-line">' + esc(item.line) + '</p>' + actionsHtml(item) + '</div></div>';
|
|
424
|
+
'<p class="cc-card-line">' + esc(item.line) + '</p>' + fileLinksHtml(item, false) + actionsHtml(item) + '</div></div>';
|
|
373
425
|
}
|
|
374
426
|
// The finished answer as Markdown; while it streams, plain words, so a half-written "**bold" never shows its stars.
|
|
375
427
|
function answerHtml(a) {
|
|
@@ -458,7 +510,13 @@
|
|
|
458
510
|
var jobs = !C.caps || C.caps.jobs;
|
|
459
511
|
var waiting = uploadsPending(k), failed = uploadsFailed(k);
|
|
460
512
|
if (ask) { ask.disabled = !text || !!k.busy || !jobs || waiting || failed; ask.textContent = k.busy === 'ask' ? 'Asking…' : waiting ? 'Uploading…' : (k.name === 'chat' ? 'Ask' : 'Reply'); }
|
|
461
|
-
if (drop) {
|
|
513
|
+
if (drop) {
|
|
514
|
+
// 6.99.13: with files on a server that takes them, Drop waits for the uploads as Ask does. Files with no words keep
|
|
515
|
+
// Drop tappable so the tap can say what is missing.
|
|
516
|
+
var withFiles = k.atts.length > 0 && dropFiles();
|
|
517
|
+
drop.disabled = !!k.busy || (!text && !withFiles) || (withFiles && (waiting || failed));
|
|
518
|
+
drop.textContent = k.busy === 'drop' ? 'Dropping…' : withFiles && waiting ? 'Uploading…' : 'Drop';
|
|
519
|
+
}
|
|
462
520
|
var attach = form.querySelector('.cc-attach'), picker = form.querySelector('[data-cos-file]');
|
|
463
521
|
if (attach) attach.classList.toggle('full', k.atts.length >= MAX_ATTS);
|
|
464
522
|
if (picker) picker.disabled = k.atts.length >= MAX_ATTS || !!k.busy;
|
|
@@ -476,6 +534,7 @@
|
|
|
476
534
|
|
|
477
535
|
// ── attachments (6.99.12, the Even app's composer) ────
|
|
478
536
|
var MAX_ATTS = 5; // MAX_ATTACHMENTS_PER_PROMPT
|
|
537
|
+
var MEDIA_ID = /^m_[a-f0-9]{24}$/; // shared/media-attachment.ts MEDIA_ID_RE
|
|
479
538
|
var MAX_PHOTO = 2 * 1024 * 1024; // MAX_IMAGE_BYTES, after the resize
|
|
480
539
|
var MAX_DOC = 64 * 1024 * 1024; // MAX_OTHER_MEDIA_BYTES
|
|
481
540
|
var MAX_VIDEO = 100 * 1024 * 1024; // MAX_VIDEO_MEDIA_BYTES
|
|
@@ -618,32 +677,47 @@
|
|
|
618
677
|
k.signedOut = true;
|
|
619
678
|
setNote(k, 'This browser was signed out. Your message and files are kept. Sign in, then tap Retry on each file.');
|
|
620
679
|
} else {
|
|
621
|
-
t.error =
|
|
680
|
+
t.error = uploadErrorText(err);
|
|
622
681
|
}
|
|
623
682
|
paintChips(k); paintButtons(k);
|
|
624
683
|
}
|
|
625
|
-
|
|
626
|
-
|
|
684
|
+
/**
|
|
685
|
+
* One file to the media store, the way every web upload goes: a photo resized to a 1024 px JPEG and posted to /api/media,
|
|
686
|
+
* any other file raw to /api/media/file. Resolves with the media ref ({ id }). `t` is the upload's record ({ file, name,
|
|
687
|
+
* kind, mime }); it gets `xhr` while sending (Remove aborts it) and `thumb` for a photo. hooks.uploading() runs once the
|
|
688
|
+
* bytes start, hooks.progress(0..1) as they go. 6.99.14: the live meeting view's Add file uses this too.
|
|
689
|
+
*/
|
|
690
|
+
function uploadMedia(t, hooks) {
|
|
627
691
|
var at = new Date(t.file.lastModified || Date.now()).toISOString();
|
|
628
|
-
var step;
|
|
629
692
|
if (t.kind === 'photo') {
|
|
630
|
-
|
|
693
|
+
return resizePhoto(t.file).then(function (dataUrl) {
|
|
631
694
|
var data = dataUrl.slice(dataUrl.indexOf(',') + 1);
|
|
632
695
|
var bytes = Math.floor(data.length * 3 / 4);
|
|
633
696
|
if (bytes > MAX_PHOTO) { var big = new Error('Over 2 MB even after resizing.'); big.code = 'photo_too_large'; throw big; }
|
|
634
|
-
t.thumb = dataUrl;
|
|
697
|
+
t.thumb = dataUrl; hooks.uploading();
|
|
635
698
|
var body = JSON.stringify({ images: [{ data: data, label: t.name.slice(0, 120), capturedAt: at }], kind: 'user_photo' });
|
|
636
|
-
return uploadRequest('/api/media', body, { 'Content-Type': 'application/json' }, function (p) {
|
|
699
|
+
return uploadRequest('/api/media', body, { 'Content-Type': 'application/json' }, function (p) { hooks.progress(p); }, t);
|
|
637
700
|
}).then(function (d) { return d && d.attachments && d.attachments[0]; });
|
|
638
|
-
} else {
|
|
639
|
-
t.state = 'uploading';
|
|
640
|
-
step = uploadRequest('/api/media/file', t.file, {
|
|
641
|
-
'Content-Type': t.mime || 'application/octet-stream',
|
|
642
|
-
'X-COS-Filename': encodeURIComponent(t.name),
|
|
643
|
-
'X-COS-Captured-At': at
|
|
644
|
-
}, function (p) { t.progress = p; paintProgress(k, t); }, t).then(function (d) { return d && d.attachment; });
|
|
645
|
-
paintChips(k);
|
|
646
701
|
}
|
|
702
|
+
hooks.uploading();
|
|
703
|
+
return uploadRequest('/api/media/file', t.file, {
|
|
704
|
+
'Content-Type': t.mime || 'application/octet-stream',
|
|
705
|
+
'X-COS-Filename': encodeURIComponent(t.name),
|
|
706
|
+
'X-COS-Captured-At': at
|
|
707
|
+
}, function (p) { hooks.progress(p); }, t).then(function (d) { return d && d.attachment; });
|
|
708
|
+
}
|
|
709
|
+
/** The chip's words for a failed upload (never the sign-in case, which each caller says its own way). */
|
|
710
|
+
function uploadErrorText(err) {
|
|
711
|
+
if (err && err.code === 'photo_too_large') return 'Over 2 MB even after resizing';
|
|
712
|
+
if (err && !err.status && !err.code) return err.message || 'Did not upload';
|
|
713
|
+
return UPLOAD_COPY[err && err.code] || (err && err.status === 413 ? 'Too large for COS.' : 'Did not upload');
|
|
714
|
+
}
|
|
715
|
+
function startUpload(k, t) {
|
|
716
|
+
t.state = 'reading'; t.progress = 0; t.error = '';
|
|
717
|
+
var step = uploadMedia(t, {
|
|
718
|
+
uploading: function () { t.state = 'uploading'; paintChips(k); paintButtons(k); },
|
|
719
|
+
progress: function (p) { t.progress = p; paintProgress(k, t); }
|
|
720
|
+
});
|
|
647
721
|
step.then(function (ref) {
|
|
648
722
|
if (!findAtt(k, t.id)) return;
|
|
649
723
|
if (!ref || !ref.id) { var e = new Error('The Mac did not keep the file.'); e.code = ''; throw e; }
|
|
@@ -651,8 +725,8 @@
|
|
|
651
725
|
paintChips(k); paintButtons(k);
|
|
652
726
|
}).catch(function (err) {
|
|
653
727
|
if (!findAtt(k, t.id)) return;
|
|
654
|
-
if (err && err.code === 'photo_too_large') { t.state = 'failed'; t.error =
|
|
655
|
-
if (err && !err.status && !err.code) { t.state = 'failed'; t.error = err
|
|
728
|
+
if (err && err.code === 'photo_too_large') { t.state = 'failed'; t.error = uploadErrorText(err); paintChips(k); paintButtons(k); return; }
|
|
729
|
+
if (err && !err.status && !err.code) { t.state = 'failed'; t.error = uploadErrorText(err); paintChips(k); paintButtons(k); return; }
|
|
656
730
|
uploadError(k, t, err);
|
|
657
731
|
});
|
|
658
732
|
}
|
|
@@ -663,13 +737,20 @@
|
|
|
663
737
|
function doDrop() {
|
|
664
738
|
var k = CHAT;
|
|
665
739
|
var input = inputOf(k); var text = input ? input.value.trim() : '';
|
|
666
|
-
if (
|
|
667
|
-
//
|
|
668
|
-
if (k.atts.length) { setNote(k, 'Drop takes words only
|
|
740
|
+
if (k.busy) return;
|
|
741
|
+
// An older server files words only (no capabilities.dropIt.attachments): say so, and send nothing.
|
|
742
|
+
if (k.atts.length && !dropFiles()) { setNote(k, 'Drop takes words only on this Mac. Ask sends the files with your words, or remove them to drop.'); return; }
|
|
743
|
+
if (!text) { if (k.atts.length) setNote(k, 'Add a few words with the files so COS knows where they go.'); return; }
|
|
744
|
+
if (uploadsPending(k)) return;
|
|
745
|
+
if (uploadsFailed(k)) { setNote(k, 'A file did not upload. Retry it or remove it, then Drop.'); return; }
|
|
746
|
+
// What is shown as sent is exactly what is sent: never more than the server takes.
|
|
747
|
+
var ready = readyAtts(k).slice(0, dropFiles() || 0);
|
|
669
748
|
var dropId = uuid();
|
|
749
|
+
var body = { dropId: dropId, text: text, source: 'web' };
|
|
750
|
+
if (ready.length) body.attachmentIds = ready.map(function (t) { return t.mediaId; });
|
|
670
751
|
k.busy = 'drop'; setNote(k, ''); paintButtons(k);
|
|
671
752
|
var send = function (retry) {
|
|
672
|
-
return api('/api/drops', { method: 'POST', body:
|
|
753
|
+
return api('/api/drops', { method: 'POST', body: body, timeoutMs: 30000 }).catch(function (err) {
|
|
673
754
|
// The same dropId is safe to send again: the server answers the first result.
|
|
674
755
|
if (retry && err && err.code === 'timeout') return send(false);
|
|
675
756
|
throw err;
|
|
@@ -678,14 +759,78 @@
|
|
|
678
759
|
send(true).then(function (ans) {
|
|
679
760
|
k.busy = ''; k.draft = ''; saveDraft(k);
|
|
680
761
|
var box = inputOf(k); if (box) { box.value = ''; W.autoGrow(box); }
|
|
762
|
+
if (ready.length) clearAtts(k);
|
|
681
763
|
mergeItems(ans.items);
|
|
682
764
|
paintFeed(true); paintButtons(k);
|
|
683
|
-
if (ans.next && ans.next.kind === 'ask' && C.caps && C.caps.jobs) submitAsk(ans.next.prompt, ans.drop.id);
|
|
765
|
+
if (ans.next && ans.next.kind === 'ask' && C.caps && C.caps.jobs) submitAsk(ans.next.prompt, ans.drop.id, null, askFilesOf(ans.next));
|
|
684
766
|
refreshSummary();
|
|
685
767
|
}, function (err) {
|
|
686
768
|
k.busy = ''; paintButtons(k);
|
|
687
|
-
// The words stay in the box until the server has them.
|
|
688
|
-
|
|
769
|
+
// The words and the files stay in the box until the server has them.
|
|
770
|
+
var kept = ready.length ? ' Your words and files are still here.' : ' Your words are still here.';
|
|
771
|
+
// A refused file says why in the server's own words (attachment_expired, too_many_attachments, words_needed...).
|
|
772
|
+
setNote(k, err && err.code === 'drop_it_off' ? 'Drop it is off on this Mac.' : 'Not dropped: ' + ((err && err.message) || 'the Mac did not answer.') + kept);
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// A drop read as a question runs the Ask with the drop's files (the server names them in next.attachmentIds).
|
|
777
|
+
function askFilesOf(next) {
|
|
778
|
+
var ids = next && Array.isArray(next.attachmentIds) ? next.attachmentIds.filter(function (id) { return MEDIA_ID.test(String(id)); }).slice(0, MAX_ATTS) : [];
|
|
779
|
+
return ids.length ? { attachmentIds: ids } : undefined;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ── the COS thread (6.99.16) ──────────────────────────
|
|
783
|
+
var THREAD_REF = 'Summary carried over from earlier in this COS thread';
|
|
784
|
+
function threadLine() {
|
|
785
|
+
var t = C.thread && C.thread.thread, n = C.thread && C.thread.next;
|
|
786
|
+
var carried = C.threadCarried || (n && n.reason === 'manual' ? (n.digest ? 'summary' : 'plain') : '');
|
|
787
|
+
if (carried) return carried === 'summary' ? 'New thread. It carries a short summary of the last one.' : 'New thread.';
|
|
788
|
+
var asks = t && Number(t.asks) > 0 ? Number(t.asks) : 0;
|
|
789
|
+
return asks ? 'Continues your COS thread · ' + asks + (asks === 1 ? ' ask' : ' asks') : '';
|
|
790
|
+
}
|
|
791
|
+
function paintThread() {
|
|
792
|
+
var line = threadLine();
|
|
793
|
+
document.querySelectorAll('[data-cos-thread]').forEach(function (el) { el.textContent = line; });
|
|
794
|
+
}
|
|
795
|
+
function loadThread() {
|
|
796
|
+
return api('/api/cos-thread').then(function (t) { C.thread = t; paintThread(); return t; }, function () { return null; });
|
|
797
|
+
}
|
|
798
|
+
// What the next Ask in the thread sends: a sessionId to continue, or none and the digest to carry.
|
|
799
|
+
function threadPlan() {
|
|
800
|
+
// An Ask in the thread still answering: the next one continues its conversation (its turn is not recorded yet).
|
|
801
|
+
var live = C.asks.filter(function (x) { return x.threaded && x.sessionId && x.turnState !== 'done' && Date.now() - x.at < 20 * 60000; });
|
|
802
|
+
if (live.length) return Promise.resolve({ sessionId: live[live.length - 1].sessionId, digest: null });
|
|
803
|
+
return loadThread().then(function (t) {
|
|
804
|
+
var n = t && t.next;
|
|
805
|
+
if (n && n.mode === 'continue' && n.sessionId) return { sessionId: n.sessionId, digest: null };
|
|
806
|
+
if (n && n.mode === 'fresh') return { sessionId: '', digest: typeof n.digest === 'string' && n.digest ? n.digest : null };
|
|
807
|
+
// The thread could not be read: continue the session it last named, if any.
|
|
808
|
+
var last = C.thread && C.thread.thread && C.thread.thread.sessionId;
|
|
809
|
+
return { sessionId: last || '', digest: null };
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
// Once per Ask, when it has finished: the thread records it, with the answer when there is one.
|
|
813
|
+
function postTurn(a) {
|
|
814
|
+
if (!a.threaded || !a.sessionId || a.turnState) return;
|
|
815
|
+
a.turnState = 'posting';
|
|
816
|
+
var body = { clientId: a.cid, sessionId: a.sessionId, query: String(a.query).slice(0, 20000) };
|
|
817
|
+
if (a.status === 'completed' && typeof a.response === 'string' && a.response) body.answer = a.response.slice(0, 40000);
|
|
818
|
+
if (Number.isInteger(a.no) && a.no > 0) body.messageNo = a.no;
|
|
819
|
+
api('/api/cos-thread/turn', { method: 'POST', body: body }).then(function (t) {
|
|
820
|
+
a.turnState = 'done';
|
|
821
|
+
if (t && t.thread) { C.thread = t; paintThread(); }
|
|
822
|
+
}, function () { a.turnState = 'done'; });
|
|
823
|
+
}
|
|
824
|
+
function newThread() {
|
|
825
|
+
if (C.threadBusy) return;
|
|
826
|
+
C.threadBusy = true;
|
|
827
|
+
api('/api/cos-thread/new', { method: 'POST', body: {} }).then(function (t) {
|
|
828
|
+
C.threadBusy = false;
|
|
829
|
+
C.thread = t; C.threadCarried = '';
|
|
830
|
+
paintThread();
|
|
831
|
+
}, function (err) {
|
|
832
|
+
C.threadBusy = false;
|
|
833
|
+
W.toast((err && err.message) || 'New thread did not go through.');
|
|
689
834
|
});
|
|
690
835
|
}
|
|
691
836
|
|
|
@@ -713,7 +858,9 @@
|
|
|
713
858
|
var body = { clientJobId: a.cid, generation: 1, query: text, source: 'web', activityToolMode: 'status' };
|
|
714
859
|
if (C.model) body.model = C.model;
|
|
715
860
|
if (C.effort) body.effort = C.effort;
|
|
716
|
-
|
|
861
|
+
// 6.99.16: an Ask from the chat's own composer belongs to the COS thread (a Reply keeps its turn's conversation).
|
|
862
|
+
a.threaded = threadOn() && !opts.replyKey;
|
|
863
|
+
if (!a.threaded && C.session && C.session.id) body.sessionId = C.session.id;
|
|
717
864
|
// A reply continues the turn's own conversation (kept when it is still live; else the server starts a new one).
|
|
718
865
|
if (opts.sessionId) body.sessionId = opts.sessionId;
|
|
719
866
|
if (opts.attachmentIds && opts.attachmentIds.length) body.attachmentIds = opts.attachmentIds.slice(0, MAX_ATTS);
|
|
@@ -727,10 +874,16 @@
|
|
|
727
874
|
throw err;
|
|
728
875
|
});
|
|
729
876
|
};
|
|
730
|
-
|
|
877
|
+
var prepare = a.threaded ? threadPlan().then(function (plan) {
|
|
878
|
+
if (plan.sessionId) body.sessionId = plan.sessionId;
|
|
879
|
+
else if (plan.digest) body.reference = { query: THREAD_REF, response: plan.digest };
|
|
880
|
+
a.carried = plan.sessionId ? '' : plan.digest ? 'summary' : 'plain';
|
|
881
|
+
}) : Promise.resolve();
|
|
882
|
+
prepare.then(function () { return admit(true); }).then(function (d) {
|
|
731
883
|
var job = d && d.job; if (!job) throw new Error('The Mac did not take the question.');
|
|
732
884
|
take(a, job);
|
|
733
|
-
if (
|
|
885
|
+
if (a.threaded) { C.threadCarried = a.carried; paintThread(); }
|
|
886
|
+
else if (job.sessionId && !a.replyKey) { C.session = { id: job.sessionId, day: today() }; save(); }
|
|
734
887
|
if (dropId) api('/api/drops/' + encodeURIComponent(dropId) + '/ask', { method: 'POST', body: { clientJobId: a.cid, generation: 1 } }).catch(function () {});
|
|
735
888
|
if (done) done(true);
|
|
736
889
|
poll(a);
|
|
@@ -767,6 +920,7 @@
|
|
|
767
920
|
setTimeout(tick, 600);
|
|
768
921
|
}
|
|
769
922
|
function finished(a) {
|
|
923
|
+
postTurn(a);
|
|
770
924
|
if (a.replyKey) paintReplyFeed(a.replyKey); else paintFeed(false);
|
|
771
925
|
if (C.onAnswered) C.onAnswered();
|
|
772
926
|
// A drop that became a question gets its answer as a follow-up: read the chat again.
|
|
@@ -833,7 +987,8 @@
|
|
|
833
987
|
if (action === 'open_result') return W.go(linkOf('session') ? 'sessions' : linkOf('card') ? 'work' : 'messages');
|
|
834
988
|
if (action === 'open') { if (linkOf('thread')) return W.go('threads'); if (linkOf('meeting')) return W.go('meetings'); return; }
|
|
835
989
|
if (action === 'review') return W.go('work');
|
|
836
|
-
|
|
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; }
|
|
837
992
|
if (action === 'reply') {
|
|
838
993
|
var box = document.getElementById('cos-input');
|
|
839
994
|
if (box) { box.value = 'remind me '; box.focus(); }
|
|
@@ -847,7 +1002,7 @@
|
|
|
847
1002
|
if (ans && ans.item) mergeItems([ans.item]);
|
|
848
1003
|
// A reminder action answers the finished item itself (merged above); dismiss answers counts only.
|
|
849
1004
|
if (action === 'dismiss') item.state = 'done';
|
|
850
|
-
if (ans && ans.next && ans.next.kind === 'ask' && C.caps && C.caps.jobs) submitAsk(ans.next.prompt, ans.drop.id);
|
|
1005
|
+
if (ans && ans.next && ans.next.kind === 'ask' && C.caps && C.caps.jobs) submitAsk(ans.next.prompt, ans.drop.id, null, askFilesOf(ans.next));
|
|
851
1006
|
paintFeed(false); refreshSummary();
|
|
852
1007
|
}, function (err) { W.toast((err && err.message) || 'That did not go through.'); });
|
|
853
1008
|
}
|
|
@@ -876,13 +1031,599 @@
|
|
|
876
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.'); });
|
|
877
1032
|
}
|
|
878
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
|
+
|
|
879
1618
|
// ── events (delegated from the Messages pane) ─────────
|
|
880
1619
|
function onClick(t) {
|
|
881
1620
|
var what = t.getAttribute('data-cos');
|
|
1621
|
+
if (/^prep-/.test(what || '')) return onPrepClick(t, what);
|
|
882
1622
|
if (what === 'ask') return doAsk(kOf(t));
|
|
883
1623
|
if (what === 'drop') return doDrop();
|
|
884
1624
|
if (what === 'all') return W.go('messages');
|
|
885
1625
|
if (what === 'read-all') return markReadIfShown(true);
|
|
1626
|
+
if (what === 'new-thread') return newThread();
|
|
886
1627
|
if (what === 'retry') { C.feedState = 'idle'; return loadFeed(); }
|
|
887
1628
|
if (what === 'act') return act(t.getAttribute('data-item'), t.getAttribute('data-act'));
|
|
888
1629
|
if (what === 'move-to') return moveTo(t.getAttribute('data-item'), t.getAttribute('data-dest'), t.getAttribute('data-domain'));
|
|
@@ -916,6 +1657,7 @@
|
|
|
916
1657
|
doAsk(k);
|
|
917
1658
|
}
|
|
918
1659
|
function onInput(e) {
|
|
1660
|
+
if (e.target && e.target.hasAttribute && e.target.hasAttribute('data-cos-prep-steer')) return onPrepInput(e.target);
|
|
919
1661
|
if (isComposerInput(e.target)) { var k = kOf(e.target); k.draft = e.target.value; saveDraft(k); W.autoGrow(e.target); paintButtons(k); }
|
|
920
1662
|
}
|
|
921
1663
|
function onChangeEvent(e) {
|
|
@@ -924,6 +1666,7 @@
|
|
|
924
1666
|
if (t.hasAttribute('data-cos-model')) { C.model = t.value; save(); syncMenus(); }
|
|
925
1667
|
else if (t.hasAttribute('data-cos-effort')) { C.effort = t.value; save(); syncMenus(); }
|
|
926
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 = ''; }
|
|
927
1670
|
}
|
|
928
1671
|
// Two composers can be on screen (the chat beside a reader is not, but a reload can leave either): keep both menus true.
|
|
929
1672
|
function syncMenus() {
|
|
@@ -934,15 +1677,23 @@
|
|
|
934
1677
|
var f = e.target.closest && e.target.closest('[data-cos-steer]');
|
|
935
1678
|
if (f) { e.preventDefault(); steer(f.getAttribute('data-cos-steer'), (f.querySelector('input') || {}).value || ''); return true; }
|
|
936
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; }
|
|
937
1681
|
return false;
|
|
938
1682
|
}
|
|
939
1683
|
// Paste a photo or a file into the box; drop files on the composer.
|
|
940
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
|
+
}
|
|
941
1690
|
if (!isComposerInput(e.target)) return;
|
|
942
1691
|
var files = e.clipboardData && e.clipboardData.files;
|
|
943
1692
|
if (files && files.length) { e.preventDefault(); addFiles(kOf(e.target), files); }
|
|
944
1693
|
}
|
|
945
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);
|
|
946
1697
|
var form = e.target && e.target.closest && e.target.closest('[data-cos-form]');
|
|
947
1698
|
if (!form) return;
|
|
948
1699
|
var types = e.dataTransfer && e.dataTransfer.types;
|
|
@@ -968,6 +1719,13 @@
|
|
|
968
1719
|
onClick: onClick, onKey: onKey, onInput: onInput, onChange: onChangeEvent, onSubmit: onSubmit, onPaste: onPaste, onDrag: onDrag,
|
|
969
1720
|
setOnChange: function (fn) { C.onChange = fn; }, setOnAnswered: function (fn) { C.onAnswered = fn; },
|
|
970
1721
|
// Test and render hooks: what the chat holds right now.
|
|
971
|
-
|
|
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; },
|
|
1729
|
+
_state: C, _composer: composer, _classify: classify, _modelsFrom: modelsFrom, _addFiles: addFiles, _itemHtml: itemHtml, _setCaps: setCaps, _threadLine: threadLine, _postTurn: postTurn
|
|
972
1730
|
};
|
|
973
1731
|
})();
|