@gotcos/glasses-server 6.99.25 → 6.99.33
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 +224 -0
- package/package.json +1 -1
- package/server/index.ts +46 -4
- package/server/lib/agent-session-output-images.ts +280 -0
- package/server/lib/agent-session-store.ts +3 -1
- package/server/lib/agent-session-turns.ts +18 -7
- package/server/lib/client-liveness.ts +38 -3
- package/server/lib/codex-subagent-activity.ts +36 -0
- package/server/lib/g2-turn-provenance.ts +679 -0
- package/server/lib/media-store.ts +41 -2
- package/server/lib/permission-broker.ts +94 -17
- package/server/lib/queue-turn-evidence.ts +19 -2
- package/server/lib/session-halt.ts +15 -1
- package/server/lib/session-stream-events.ts +4 -0
- package/server/lib/session-subagents.ts +155 -0
- package/server/lib/session-turn-attachments.ts +245 -0
- package/server/lib/thread-turn-queue-deliver.ts +17 -2
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +731 -0
- package/server/lib/turn-file-store.ts +202 -0
- package/server/lib/turn-footer.ts +81 -0
- package/server/routes/agent-session-bindings.ts +164 -16
- package/server/routes/agent-sessions.ts +77 -8
- package/server/routes/client-instance.ts +12 -1
- package/server/routes/cursor-stop-followup.ts +30 -1
- package/server/routes/g2-turns.ts +46 -0
- package/server/routes/health.ts +9 -0
- package/server/routes/media.ts +32 -8
- package/server/routes/permission-broker.ts +47 -4
- package/server/routes/thread-turn-queue.ts +148 -8
- package/server/routes/today-brief.ts +148 -0
- package/server/web-control/app.css +42 -9
- package/server/web-control/app.js +975 -114
- package/server/web-control/control.css +12 -6
- package/server/web-control/index.html +6 -1
- package/server/web-control/panes/cos-chat.css +27 -3
- package/server/web-control/panes/cos-chat.js +240 -46
- package/server/web-control/panes/meetings.css +5 -2
- package/server/web-control/panes/meetings.js +21 -10
- package/server/web-control/panes/memories.css +1 -1
- package/server/web-control/panes/memories.js +3 -8
- package/server/web-control/panes/questions.css +56 -2
- package/server/web-control/panes/questions.js +454 -56
- package/server/web-control/panes/reader-media.js +154 -0
- package/server/web-control/panes/sessions.css +20 -7
- package/server/web-control/panes/settings.js +34 -2
- package/server/web-control/panes/speakers.css +3 -5
- package/server/web-control/panes/speakers.js +7 -4
- package/server/web-control/panes/threads.js +3 -2
- package/server/web-control/panes/today-actions.css +81 -3
- package/server/web-control/panes/today-actions.js +527 -62
- package/server/web-control/panes/work.css +1 -1
- package/server/web-control/shell.js +3 -0
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
:root {
|
|
8
8
|
--tint: var(--accent);
|
|
9
|
-
|
|
9
|
+
/* 6.99.30 (design QA): #8b8378 measured 3.29:1 on the light page; this reads at 4.5:1 or better. Dark is unchanged. */
|
|
10
|
+
--tertiary: #6e675d;
|
|
10
11
|
--gold: #b98a3e;
|
|
11
12
|
--green: #2f7d4f;
|
|
12
13
|
--amber: #a8641c;
|
|
@@ -64,8 +65,9 @@
|
|
|
64
65
|
.rail-settings { min-height: 40px; }
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
/* Full rail (Control's at window width 1100 and up
|
|
68
|
-
|
|
68
|
+
/* Full rail (Control's at window width 1100 and up, on a screen tall enough for it: the same clause as app.css's desktop
|
|
69
|
+
step, so a wide short window keeps the icon rail and its short-screen sizes). */
|
|
70
|
+
@media (min-width: 1100px) and (min-height: 721px) {
|
|
69
71
|
.app-shell { grid-template-columns: 248px minmax(0, 1fr); }
|
|
70
72
|
.rail { align-items: stretch; padding: calc(var(--safe-top) + 18px) 12px 14px; gap: 2px; }
|
|
71
73
|
.rail-head { flex-direction: row; align-items: center; gap: 8px; padding: 0 8px 14px; }
|
|
@@ -145,7 +147,7 @@
|
|
|
145
147
|
.cx-btn:disabled { opacity: 0.45; }
|
|
146
148
|
.cx-btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
|
147
149
|
.cx-btn.quiet { border-color: var(--panel-border); }
|
|
148
|
-
@media (max-width: 599px) { .cx-btn {
|
|
150
|
+
@media (max-width: 599px) { .cx-btn { font-size: 14.5px; } }
|
|
149
151
|
|
|
150
152
|
/* ── text switch (COSBrand: words with a 2pt gold rule, never pills) ── */
|
|
151
153
|
.cx-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; margin: 12px 0 10px; }
|
|
@@ -161,7 +163,6 @@
|
|
|
161
163
|
.cx-check input { width: 16px; height: 16px; accent-color: var(--gold); }
|
|
162
164
|
@media (max-width: 599px) {
|
|
163
165
|
.cx-search { max-width: none; flex-basis: 100%; order: 3; }
|
|
164
|
-
.cx-search .field { min-height: 40px; font-size: 16px; }
|
|
165
166
|
.cx-switch button { font-size: 15px; padding: 8px 0 9px; }
|
|
166
167
|
}
|
|
167
168
|
|
|
@@ -280,7 +281,7 @@
|
|
|
280
281
|
.cx-chip { display: inline-flex; align-items: center; gap: 4px; height: 28px; padding: 0 4px 0 10px; border-radius: 14px; background: var(--panel); border: 1px solid var(--panel-border); font-size: 13px; color: var(--ink); }
|
|
281
282
|
.cx-chip button, .cx-x { width: 24px; height: 24px; border-radius: 12px; color: var(--sub); font-size: 15px; line-height: 1; display: grid; place-items: center; }
|
|
282
283
|
.cx-chip.more { padding-right: 10px; color: var(--sub); }
|
|
283
|
-
@media (max-width: 599px) { .cx-
|
|
284
|
+
@media (max-width: 599px) { .cx-chip { height: 32px; } }
|
|
284
285
|
|
|
285
286
|
/* ── Settings (Control's hub categories) ──────────────── */
|
|
286
287
|
.cx-set-cat { margin-top: 8px; }
|
|
@@ -323,3 +324,8 @@ html, body { overscroll-behavior-y: none; }
|
|
|
323
324
|
.intake-meeting { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); }
|
|
324
325
|
.intake-n { font: 600 12px/1.6 var(--mono); color: var(--sub); }
|
|
325
326
|
.intake-foot { margin: 6px 0 0; font-size: 12.5px; color: var(--tertiary); }
|
|
327
|
+
|
|
328
|
+
/* 6.99.30 (design QA, the unfolded Duo at 1430 px): Settings and Dictation are forms and short reads, not boards, so
|
|
329
|
+
their content stops at 720 px instead of running lines to 137 characters; prose lines stop at about 68 characters. */
|
|
330
|
+
#pane-settings, #pane-dictation { max-width: 720px; }
|
|
331
|
+
.cx-set-desc, .cx-set-blurb, .wk-fine { max-width: 68ch; }
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
<symbol id="i-photo" viewBox="0 0 24 24"><rect x="3.5" y="5" width="17" height="14" rx="2.5"/><circle cx="9" cy="10" r="1.6"/><path d="m4 17 5-4.5 3.5 3 3-2.5 4.5 4"/></symbol>
|
|
41
41
|
<symbol id="i-video" viewBox="0 0 24 24"><rect x="3.5" y="6" width="12.5" height="12" rx="2.5"/><path d="m16 10.5 4.5-2.5v8L16 13.5"/></symbol>
|
|
42
42
|
<symbol id="i-doc" viewBox="0 0 24 24"><path d="M7 3.5h6.5L18 8v12a.5.5 0 0 1-.5.5h-10.5A.5.5 0 0 1 6.5 20V4a.5.5 0 0 1 .5-.5z"/><path d="M13.5 3.5V8H18M9 12.5h6M9 15.5h6"/></symbol>
|
|
43
|
+
<symbol id="i-camera" viewBox="0 0 24 24"><path d="M4.5 8h3l1.6-2.2h5.8L16.5 8h3a1.5 1.5 0 0 1 1.5 1.5v8A1.5 1.5 0 0 1 19.5 19h-15A1.5 1.5 0 0 1 3 17.5v-8A1.5 1.5 0 0 1 4.5 8z"/><circle cx="12" cy="13.2" r="3.4"/></symbol>
|
|
43
44
|
<symbol id="i-clip" viewBox="0 0 24 24"><path d="m15.5 7.5-6.8 6.8a1.8 1.8 0 0 0 2.5 2.5l7.3-7.3a3.5 3.5 0 0 0-5-5l-7.6 7.6a5.2 5.2 0 0 0 7.4 7.4l5.7-5.7"/></symbol>
|
|
44
45
|
<symbol id="i-messages" viewBox="0 0 24 24"><path d="M4 7a2.5 2.5 0 0 1 2.5-2.5h11A2.5 2.5 0 0 1 20 7v7a2.5 2.5 0 0 1-2.5 2.5H10L5.5 20v-3.6A2.5 2.5 0 0 1 4 14z"/></symbol>
|
|
45
46
|
<symbol id="i-usage" viewBox="0 0 24 24"><path d="M4 20h16M7 16v-4M12 16V7M17 16v-7"/></symbol>
|
|
@@ -121,6 +122,7 @@
|
|
|
121
122
|
<div>
|
|
122
123
|
<h1 class="pane-title">Today</h1>
|
|
123
124
|
<p id="today-date" class="today-date"></p>
|
|
125
|
+
<p id="today-weather" class="today-weather hidden"></p>
|
|
124
126
|
</div>
|
|
125
127
|
<button type="button" class="ask-bar" data-open-ask>
|
|
126
128
|
<svg class="icon"><use href="#i-ask"/></svg>
|
|
@@ -253,8 +255,10 @@
|
|
|
253
255
|
<div class="sheet-grip"></div>
|
|
254
256
|
<form class="sheet-head" id="ask-form">
|
|
255
257
|
<input id="ask-q" class="field" type="search" placeholder="Ask COS or find a session" autocomplete="off" enterkeyhint="go">
|
|
258
|
+
<span id="ask-clip" class="ask-clip"></span>
|
|
256
259
|
<button type="button" class="link-btn" id="ask-close">Cancel</button>
|
|
257
260
|
</form>
|
|
261
|
+
<div id="ask-files" class="cc-chips ask-files hidden" aria-live="polite"></div>
|
|
258
262
|
<div id="ask-results"></div>
|
|
259
263
|
</div>
|
|
260
264
|
</div>
|
|
@@ -276,7 +280,8 @@
|
|
|
276
280
|
"dictation": "/control/panes/dictation.js",
|
|
277
281
|
"settings": "/control/panes/settings.js",
|
|
278
282
|
"memories": "/control/panes/memories.js",
|
|
279
|
-
"todayActions": "/control/panes/today-actions.js"
|
|
283
|
+
"todayActions": "/control/panes/today-actions.js",
|
|
284
|
+
"readerMedia": "/control/panes/reader-media.js"
|
|
280
285
|
}</script>
|
|
281
286
|
<!-- The Knowledge graph explorer, loaded by panes/memories.js when the graph first opens. -->
|
|
282
287
|
<script type="application/json" id="memories-graph-assets">{
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
.msg-split[data-show] > .msg-left, .msg-split[data-show] > .msg-right { display: block; }
|
|
11
11
|
/* The list column is narrower than the pane: its header's buttons go under the title instead of over it. */
|
|
12
12
|
.msg-left .cx-hero .cx-actions { grid-column: 1 / -1; justify-content: flex-start; }
|
|
13
|
-
.msg-right { position: sticky; top: calc(56px + var(--safe-top, 0px)); height: calc(100vh - 72px - var(--safe-top, 0px)); min-height: 420px; }
|
|
13
|
+
.msg-right { position: sticky; top: calc(56px + var(--safe-top, 0px)); height: calc(100vh - 72px - var(--safe-top, 0px)); height: calc(100dvh - 72px - var(--safe-top, 0px)); min-height: 420px; }
|
|
14
14
|
.msg-right .cc { height: 100%; display: flex; flex-direction: column; background: var(--panel); border: 1px solid var(--panel-border); border-radius: 16px; overflow: hidden; }
|
|
15
15
|
.msg-right .cc-head { padding: 14px 16px 12px; }
|
|
16
16
|
.msg-right .cc-feed { flex: 1; overflow-y: auto; padding: 12px 16px 8px; overscroll-behavior: contain; }
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
/* Below 860 px the chat fills the screen, so the composer sits at the bottom even when the chat is short. */
|
|
21
21
|
@media (max-width: 859px) {
|
|
22
|
-
.msg-split[data-show='chat'] .cc { min-height: calc(100vh - 64px - var(--safe-top, 0px)); display: flex; flex-direction: column; }
|
|
22
|
+
.msg-split[data-show='chat'] .cc { min-height: calc(100vh - 64px - var(--safe-top, 0px)); min-height: calc(100dvh - 64px - var(--safe-top, 0px)); display: flex; flex-direction: column; }
|
|
23
23
|
.msg-split[data-show='chat'] .cc-feed { flex: 1; }
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
.cc-prep-add:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
191
191
|
.cc-prep-acts .cx-btn { height: 40px; }
|
|
192
192
|
@media (max-width: 480px) {
|
|
193
|
-
.cc-prep-acts .cx-btn {
|
|
193
|
+
.cc-prep-acts .cx-btn { min-width: 44px; }
|
|
194
194
|
.cc-prep-head-acts button { min-height: 44px; }
|
|
195
195
|
}
|
|
196
196
|
.cc-prep-note { margin: 0; font-size: 13px; }
|
|
@@ -198,3 +198,27 @@
|
|
|
198
198
|
.cc-prep.dragging .cc-prep-panel { outline: 2px dashed var(--accent); outline-offset: 4px; }
|
|
199
199
|
.cc-prep { scroll-margin-top: calc(64px + var(--safe-top, 0px)); }
|
|
200
200
|
@media (min-width: 860px) { .msg-right .cc-prep { padding: 10px 16px 0; max-height: 55%; overflow-y: auto; flex: none; } }
|
|
201
|
+
|
|
202
|
+
/* The Ask sheet's files (11/07, app.js askFiles): the attach kit's chips and clip beside the Ask box. */
|
|
203
|
+
.sheet-head .cc-attach { flex: none; height: 44px; border-radius: 12px; }
|
|
204
|
+
.ask-clip:empty { display: none; }
|
|
205
|
+
.ask-files { margin-top: 10px; }
|
|
206
|
+
.ask-files .ask-files-note { flex-basis: 100%; margin: 0 2px; font-size: 13px; line-height: 1.35; color: var(--sub); }
|
|
207
|
+
#ask-sheet .sheet.dragging { outline: 2px dashed var(--accent); outline-offset: -6px; }
|
|
208
|
+
|
|
209
|
+
/* 6.99.27: a session's reply box with files (app.js paintReaderFiles, panes/today-actions.js): the kit's chips on a row of
|
|
210
|
+
their own above the words, Take photo and the clip beside Fork and Send, and the limits in plain words under the chips. */
|
|
211
|
+
.att-composer { flex-wrap: wrap; }
|
|
212
|
+
.att-composer .att-files { flex: 1 1 100%; padding: 2px 0 4px; }
|
|
213
|
+
/* 6.99.30 (canary): the folded Sessions reader squeezed the box to about 101 px beside Fork, the clip and Send. Under
|
|
214
|
+
430 px the words get their own full row and the buttons sit on the row below, at its right. */
|
|
215
|
+
@media (max-width: 429px) {
|
|
216
|
+
.att-composer > textarea { flex: 1 1 100%; }
|
|
217
|
+
.att-composer > .composer-actions { margin-left: auto; }
|
|
218
|
+
}
|
|
219
|
+
.att-composer .att-files.hidden { display: none; }
|
|
220
|
+
.att-clip, .ta-clip { display: inline-flex; gap: 6px; }
|
|
221
|
+
.att-clip:empty { display: none; }
|
|
222
|
+
.att-clip .cc-attach { height: 38px; width: 42px; border-radius: var(--radius-sm); }
|
|
223
|
+
.att-composer.dragging { outline: 2px dashed var(--accent); outline-offset: 4px; }
|
|
224
|
+
.cc-chips .att-limit { flex-basis: 100%; min-width: 0; max-width: 100%; margin: 0 2px; font-size: 12.5px; line-height: 1.35; color: var(--sub); white-space: normal; overflow-wrap: anywhere; }
|
|
@@ -107,7 +107,12 @@
|
|
|
107
107
|
steer: !!(C.caps && C.caps.steer),
|
|
108
108
|
thread: !!(drop && drop.enabled === true && drop.thread && drop.thread.protocol === 1),
|
|
109
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)
|
|
110
|
+
prep: prepCapsOf(caps.prep),
|
|
111
|
+
// 11/07: how many files a session turn takes (threadTurnAttachments). 0 (an older server): words only, and Today's
|
|
112
|
+
// reply box shows no clip, so a file can never be dropped from a turn in silence.
|
|
113
|
+
turnFiles: h && Number.isInteger(h.threadTurnAttachments) && h.threadTurnAttachments > 0 ? Math.min(h.threadTurnAttachments, MAX_ATTS) : 0,
|
|
114
|
+
// 6.99.27: any file for a session turn, up to this many bytes (threadTurnFileBytes). 0: the chat's list only.
|
|
115
|
+
turnFileBytes: h && Number.isInteger(h.threadTurnFileBytes) && h.threadTurnFileBytes > 0 ? Math.min(h.threadTurnFileBytes, MAX_DOC) : 0
|
|
111
116
|
};
|
|
112
117
|
if (next.prep && next.prep.serverInstanceId) PREP_INST = next.prep.serverInstanceId;
|
|
113
118
|
var changed = !C.caps || C.caps.dropIt !== next.dropIt || C.caps.jobs !== next.jobs || C.caps.thread !== next.thread;
|
|
@@ -276,8 +281,7 @@
|
|
|
276
281
|
'<div class="cc-chips hidden" data-cos-chips></div>' +
|
|
277
282
|
'<textarea class="field cc-input" id="' + esc(k.inputId) + '" rows="1" maxlength="2000" placeholder="' + esc(o.placeholder) + '" aria-label="' + esc(o.label) + '"></textarea>' +
|
|
278
283
|
'<div class="cc-buttons">' +
|
|
279
|
-
'
|
|
280
|
-
'<input type="file" class="cc-file" multiple accept="' + ACCEPT + '" data-cos-file aria-label="Attach photos or files"></label>' +
|
|
284
|
+
cameraHtml('data-cos-file') + clipHtml('data-cos-file') +
|
|
281
285
|
'<button type="button" class="cx-btn primary cc-ask" data-cos="ask">Ask</button>' +
|
|
282
286
|
(drop ? '<button type="button" class="cx-btn cc-drop" data-cos="drop">Drop</button>' : '') + '</div>' +
|
|
283
287
|
'<div class="cc-foot"><span class="cc-menus" data-cos-model-slot>' + (C.models ? modelMenuHtml() : '') + '</span>' +
|
|
@@ -549,11 +553,19 @@
|
|
|
549
553
|
var ua = (window.navigator && window.navigator.userAgent) || '';
|
|
550
554
|
return /safari/i.test(ua) && !/chrome|chromium|crios|fxios|edg|android|opr/i.test(ua);
|
|
551
555
|
}
|
|
556
|
+
// QA 6.99.27 (User 3): every browser on an iPhone or iPad is WebKit and decodes HEIC, including the Home Screen web
|
|
557
|
+
// app, whose user agent has no "Safari" in it. iPadOS asks for the desktop site by default, so it reads as a Mac
|
|
558
|
+
// with a touch screen.
|
|
559
|
+
function appleMobile() {
|
|
560
|
+
var nav = window.navigator || {}, ua = nav.userAgent || '';
|
|
561
|
+
return /iPhone|iPad|iPod/.test(ua) || (/Macintosh/.test(ua) && Number(nav.maxTouchPoints) > 1);
|
|
562
|
+
}
|
|
563
|
+
function heicReadable() { return isSafari() || appleMobile(); }
|
|
552
564
|
/** What a file is to COS: 'photo', 'doc' or 'video', with a refusal sentence when it cannot be attached. */
|
|
553
565
|
function classify(file) {
|
|
554
566
|
var type = String(file.type || '').toLowerCase(), ext = extOf(file.name);
|
|
555
567
|
var heic = /^image\/hei[cf]/.test(type) || ext === 'heic' || ext === 'heif';
|
|
556
|
-
if (heic && !
|
|
568
|
+
if (heic && !heicReadable()) return { refuse: file.name + ' is a HEIC photo, which only Safari can read here. Attach it from Safari, or save it as a JPEG first.' };
|
|
557
569
|
if (/^image\//.test(type) || PHOTO_EXT[ext]) return { kind: 'photo' };
|
|
558
570
|
if (/^video\//.test(type) || VIDEO_EXT[ext]) {
|
|
559
571
|
if (!VIDEO_EXT[ext] && type !== 'video/mp4' && type !== 'video/quicktime') return { refuse: file.name + ' is a video COS cannot read. Use MP4 or MOV.' };
|
|
@@ -566,19 +578,80 @@
|
|
|
566
578
|
}
|
|
567
579
|
return { refuse: file.name + ' cannot be attached. COS takes photos, PDF, TXT, MD, CSV, JSON, XLSX, MP4 and MOV.' };
|
|
568
580
|
}
|
|
569
|
-
|
|
581
|
+
// ── the attach kit (11/07: "attach files and images from the dashboard view as well") ──
|
|
582
|
+
// The clip, the chips, the limits and the upload above, for any place that sends files with words: the chat's own
|
|
583
|
+
// composers, Today's reply and note boxes (panes/today-actions.js) and the Ask sheet (app.js). One copy, used by all.
|
|
584
|
+
// A box is { atts: [], signedOut: false, ui } (W.cosChat.attach.box): ui.changed(box) repaints after any change, ui.removed(box) runs after a
|
|
585
|
+
// Remove, ui.signedOut(box) says the browser was signed out mid-upload. Progress moves only the chip's own bar
|
|
586
|
+
// (paintProgress, by the chip's id). Nothing here sends: each caller sends the ready ids its own way, and only once
|
|
587
|
+
// pending() and failed() are both false.
|
|
588
|
+
// opts.any (a session turn): no accept list, so any file, and the label says the limits.
|
|
589
|
+
function clipHtml(fileAttr, opts) {
|
|
590
|
+
var any = !!(opts && opts.any);
|
|
591
|
+
var label = any ? 'Attach photos or files, up to 5, ' + mb(opts.any) + ' each' : 'Attach photos or files';
|
|
592
|
+
return '<label class="cx-btn cc-attach" title="' + esc(label) + '"><svg class="icon" aria-hidden="true"><use href="#i-clip"/></svg><span class="sr-only">' + esc(label) + '</span>' +
|
|
593
|
+
// No capture attribute: iOS then offers Photo Library, Take Photo and Choose File.
|
|
594
|
+
'<input type="file" class="cc-file" multiple' + (any ? '' : ' accept="' + ACCEPT + '"') + ' ' + fileAttr + ' aria-label="' + esc(label) + '"></label>';
|
|
595
|
+
}
|
|
596
|
+
// 6.99.27 (Miles: "take picture"): the camera itself, one tap. On a phone or tablet only: a desktop browser ignores
|
|
597
|
+
// `capture` and would open a second file picker labelled Take photo.
|
|
598
|
+
function cameraOn() { return !!(window.matchMedia && window.matchMedia('(pointer: coarse)').matches); }
|
|
599
|
+
function cameraHtml(fileAttr) {
|
|
600
|
+
if (!cameraOn()) return '';
|
|
601
|
+
return '<label class="cx-btn cc-attach cc-camera" title="Take photo"><svg class="icon" aria-hidden="true"><use href="#i-camera"/></svg><span class="sr-only">Take photo</span>' +
|
|
602
|
+
'<input type="file" class="cc-file" accept="image/*" capture="environment" ' + fileAttr + ' aria-label="Take photo"></label>';
|
|
603
|
+
}
|
|
604
|
+
// The limits in plain words, under the chips of a session turn.
|
|
605
|
+
function limitLine(box) {
|
|
606
|
+
return box && box.anyBytes ? 'Up to 5 files, ' + mb(box.anyBytes) + ' each (videos up to ' + mb(MAX_VIDEO) + '). The session gets where each one is on your Mac.' : 'Up to 5 photos or files.';
|
|
607
|
+
}
|
|
608
|
+
// A composer here has no ui of its own (it is made before COMPOSER_UI below exists): it is the chat's.
|
|
609
|
+
function uiOf(box) { return box.ui || COMPOSER_UI; }
|
|
610
|
+
function boxChanged(box) { var ui = uiOf(box); if (ui.changed) ui.changed(box); }
|
|
611
|
+
/**
|
|
612
|
+
* 6.99.27: what a SESSION turn takes, which is more than the chat does. The agent gets each file's place on the Mac and
|
|
613
|
+
* opens it with its own tools, so any type goes, up to `maxBytes` (the Mac's threadTurnFileBytes, 64 MB) and up to
|
|
614
|
+
* 100 MB for an MP4 or MOV (the media store's video cap). Photos go as taken: kind 'photo' only for the chip.
|
|
615
|
+
* QA 6.99.27 (Skeptic W3): the 100 MB is only for a file whose type is video/mp4 or video/quicktime (or, with no type,
|
|
616
|
+
* whose name says so). Any other video (WebM, MKV) is a turn file on the Mac, whose cap is 64 MB, so it is refused
|
|
617
|
+
* here before the upload rather than after it.
|
|
618
|
+
*/
|
|
619
|
+
function classifyForSession(file, maxBytes) {
|
|
620
|
+
var type = String(file.type || '').toLowerCase(), ext = extOf(file.name);
|
|
621
|
+
var kindOf = type || VIDEO_EXT[ext] || '';
|
|
622
|
+
var video = kindOf === 'video/mp4' || kindOf === 'video/quicktime';
|
|
623
|
+
var cap = video ? MAX_VIDEO : maxBytes;
|
|
624
|
+
if (file.size > cap) return { refuse: file.name + ' is ' + mb(file.size) + '. A session takes ' + (video ? 'a video' : 'a file') + ' up to ' + mb(cap) + '.' };
|
|
625
|
+
if (!file.size) return { refuse: file.name + ' is empty.' };
|
|
626
|
+
if (/^image\//.test(type) || PHOTO_EXT[ext]) {
|
|
627
|
+
var heic = /^image\/hei[cf]/.test(type) || ext === 'heic' || ext === 'heif';
|
|
628
|
+
return { kind: 'photo', mime: type || 'application/octet-stream', showable: !heic || heicReadable() };
|
|
629
|
+
}
|
|
630
|
+
if (video) return { kind: 'video', mime: kindOf };
|
|
631
|
+
return { kind: DOC_EXT[ext] ? 'doc' : 'file', mime: type && type !== 'application/octet-stream' ? type : DOC_EXT[ext] || 'application/octet-stream' };
|
|
632
|
+
}
|
|
633
|
+
function dropThumb(t) { if (t.raw && /^blob:/.test(t.thumb || '')) { try { URL.revokeObjectURL(t.thumb); } catch (_e) {} } }
|
|
634
|
+
function objectUrl(file) { try { return URL.createObjectURL(file); } catch (_e) { return ''; } }
|
|
635
|
+
function boxAdd(box, files) {
|
|
570
636
|
var list = Array.prototype.slice.call(files || []);
|
|
571
|
-
if (!list.length) return;
|
|
572
637
|
var refusals = [];
|
|
573
638
|
list.forEach(function (file) {
|
|
574
|
-
if (
|
|
575
|
-
var c = classify(file);
|
|
639
|
+
if (box.atts.length >= MAX_ATTS) { refusals.push('Up to 5 attachments per message. ' + file.name + ' was not added.'); return; }
|
|
640
|
+
var c = box.anyBytes ? classifyForSession(file, box.anyBytes) : classify(file);
|
|
576
641
|
if (c.refuse) { refusals.push(c.refuse); return; }
|
|
577
642
|
var t = { id: uuid(), file: file, name: file.name || (c.kind === 'photo' ? 'Photo' : 'File'), size: file.size, kind: c.kind, mime: c.mime || '', state: 'reading', progress: 0, mediaId: '', thumb: '', error: '' };
|
|
578
|
-
|
|
579
|
-
|
|
643
|
+
// 6.99.27, a session turn: the file as it is, never resized or converted (the agent reads the original), and the
|
|
644
|
+
// upload says so, so the Mac keeps a format the media store does not read (lib/turn-file-store.ts).
|
|
645
|
+
if (box.anyBytes) { t.raw = true; t.purpose = 'session-turn'; if (c.kind === 'photo' && c.showable) t.thumb = objectUrl(file); }
|
|
646
|
+
box.atts.push(t);
|
|
647
|
+
startUpload(box, t);
|
|
580
648
|
});
|
|
581
|
-
|
|
649
|
+
if (list.length) box.signedOut = false;
|
|
650
|
+
return refusals;
|
|
651
|
+
}
|
|
652
|
+
function addFiles(k, files) {
|
|
653
|
+
if (!files || !files.length) return;
|
|
654
|
+
var refusals = boxAdd(k, files);
|
|
582
655
|
setNote(k, refusals.join(' '));
|
|
583
656
|
paintChips(k); paintButtons(k);
|
|
584
657
|
}
|
|
@@ -588,18 +661,21 @@
|
|
|
588
661
|
if (t.state === 'failed') return t.error || 'Did not upload';
|
|
589
662
|
return sizeLabel(t.size);
|
|
590
663
|
}
|
|
591
|
-
|
|
664
|
+
// attr: the click attribute the box's own handler reads ('data-cos' here, 'data-ta' on Today, 'data-action' in the
|
|
665
|
+
// Ask sheet), so a chip's Remove never reaches another box's handler.
|
|
666
|
+
function chipHtml(t, attr) {
|
|
667
|
+
attr = attr || 'data-cos';
|
|
592
668
|
var icon = t.thumb ? '<img src="' + esc(t.thumb) + '" alt="">' : '<svg class="icon" aria-hidden="true"><use href="#i-' + (t.kind === 'video' ? 'video' : t.kind === 'photo' ? 'photo' : 'doc') + '"/></svg>';
|
|
593
669
|
return '<div class="cc-chip ' + t.state + '" data-chip="' + t.id + '"><span class="cc-chip-thumb">' + icon + '</span>' +
|
|
594
670
|
'<span class="cc-chip-text"><span class="cc-chip-name">' + esc(t.name) + '</span><span class="cc-chip-meta" data-chip-meta>' + esc(chipMeta(t)) + '</span></span>' +
|
|
595
|
-
(t.state === 'failed' ? '<button type="button" class="link-btn cc-chip-retry"
|
|
596
|
-
'<button type="button" class="cc-chip-x"
|
|
671
|
+
(t.state === 'failed' ? '<button type="button" class="link-btn cc-chip-retry" ' + attr + '="att-retry" data-att="' + t.id + '">Retry</button>' : '') +
|
|
672
|
+
'<button type="button" class="cc-chip-x" ' + attr + '="att-remove" data-att="' + t.id + '" aria-label="Remove ' + esc(t.name) + '"><span aria-hidden="true">×</span></button>' +
|
|
597
673
|
(t.state === 'uploading' || t.state === 'reading' ? '<span class="cc-chip-bar" data-chip-bar style="width:' + Math.round(t.progress * 100) + '%"></span>' : '') + '</div>';
|
|
598
674
|
}
|
|
599
675
|
function paintChips(k) {
|
|
600
676
|
var form = formOf(k), box = form && form.querySelector('[data-cos-chips]');
|
|
601
677
|
if (!box) return;
|
|
602
|
-
box.innerHTML = k.atts.map(chipHtml).join('');
|
|
678
|
+
box.innerHTML = k.atts.map(function (t) { return chipHtml(t); }).join('');
|
|
603
679
|
box.classList.toggle('hidden', !k.atts.length);
|
|
604
680
|
}
|
|
605
681
|
// Progress moves the bar and the words only, never the chip row (a tap on Remove must not miss).
|
|
@@ -610,11 +686,60 @@
|
|
|
610
686
|
var bar = chip.querySelector('[data-chip-bar]'); if (bar) bar.style.width = Math.round(t.progress * 100) + '%';
|
|
611
687
|
}
|
|
612
688
|
function findAtt(k, id) { for (var i = 0; i < k.atts.length; i++) if (k.atts[i].id === id) return k.atts[i]; return null; }
|
|
613
|
-
function
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
689
|
+
function boxRemove(box, id) {
|
|
690
|
+
box.atts = box.atts.filter(function (t) { if (t.id === id) { if (t.xhr) { try { t.xhr.abort(); } catch (_e) {} } dropThumb(t); } return t.id !== id; });
|
|
691
|
+
var ui = uiOf(box); if (ui.removed) ui.removed(box);
|
|
692
|
+
boxChanged(box);
|
|
693
|
+
}
|
|
694
|
+
function removeAtt(k, id) { boxRemove(k, id); }
|
|
695
|
+
function boxRetry(box, id) {
|
|
696
|
+
var t = findAtt(box, id);
|
|
697
|
+
if (!t) return false;
|
|
698
|
+
box.signedOut = false;
|
|
699
|
+
startUpload(box, t);
|
|
700
|
+
boxChanged(box);
|
|
701
|
+
return true;
|
|
617
702
|
}
|
|
703
|
+
// QA 6.99.27: the files that went with a message leave the box. That is not a Remove, so ui.removed is not told (the
|
|
704
|
+
// reader's clears the status line, which may be telling the person about the message that just took them). Answers
|
|
705
|
+
// what left, enough to put each back as a kept chip (boxRestore) if the message does not go after all.
|
|
706
|
+
function boxTake(box, ids) {
|
|
707
|
+
var went = [];
|
|
708
|
+
box.atts = box.atts.filter(function (t) {
|
|
709
|
+
if (!t.mediaId || ids.indexOf(t.mediaId) < 0) return true;
|
|
710
|
+
went.push({ mediaId: t.mediaId, name: t.name, size: t.size, kind: t.kind, mime: t.mime, keptAt: t.keptAt || 0 });
|
|
711
|
+
dropThumb(t);
|
|
712
|
+
return false;
|
|
713
|
+
});
|
|
714
|
+
if (went.length) boxChanged(box);
|
|
715
|
+
return went;
|
|
716
|
+
}
|
|
717
|
+
// Files the Mac already keeps, back on the box as ready chips (no upload), at most five in all. Answers how many are
|
|
718
|
+
// on the box afterwards out of those asked for.
|
|
719
|
+
function boxRestore(box, files) {
|
|
720
|
+
var back = 0;
|
|
721
|
+
(files || []).forEach(function (f) {
|
|
722
|
+
if (!f || !f.mediaId) return;
|
|
723
|
+
if (box.atts.some(function (t) { return t.mediaId === f.mediaId; })) { back++; return; }
|
|
724
|
+
if (box.atts.length >= MAX_ATTS) return;
|
|
725
|
+
box.atts.push({ id: uuid(), file: null, name: f.name || 'File', size: f.size || 0, kind: f.kind || 'file', mime: f.mime || '', state: 'ready', progress: 1, mediaId: f.mediaId, thumb: '', error: '', keptAt: f.keptAt || 0 });
|
|
726
|
+
back++;
|
|
727
|
+
});
|
|
728
|
+
if (back) boxChanged(box);
|
|
729
|
+
return back;
|
|
730
|
+
}
|
|
731
|
+
// Everything goes, an upload still running included (its request is stopped, and its late answer finds no record).
|
|
732
|
+
function boxClear(box) {
|
|
733
|
+
box.atts.forEach(function (t) { if (t.xhr) { try { t.xhr.abort(); } catch (_e) {} } dropThumb(t); });
|
|
734
|
+
box.atts = [];
|
|
735
|
+
boxChanged(box);
|
|
736
|
+
}
|
|
737
|
+
// The chat's composers, as boxes.
|
|
738
|
+
var COMPOSER_UI = {
|
|
739
|
+
changed: function (k) { paintChips(k); paintButtons(k); },
|
|
740
|
+
removed: function (k) { if (!uploadsFailed(k) && /did not upload/i.test(k.note)) setNote(k, ''); },
|
|
741
|
+
signedOut: function (k) { setNote(k, 'This browser was signed out. Your message and files are kept. Sign in, then tap Retry on each file.'); }
|
|
742
|
+
};
|
|
618
743
|
/** A photo, drawn into a canvas at most 1024 px on its longest edge, as a JPEG at quality 0.85. */
|
|
619
744
|
function resizePhoto(file) {
|
|
620
745
|
return new Promise(function (resolve, reject) {
|
|
@@ -640,46 +765,82 @@
|
|
|
640
765
|
* with credentials 'same-origin' (the cookie goes with any same-origin XHR) plus X-COS-Web: 1, which a cookie write
|
|
641
766
|
* needs. A 401 here never opens the sign-in screen: the chip says so and the message stays (see uploadError).
|
|
642
767
|
*/
|
|
643
|
-
|
|
768
|
+
// extra: more headers (6.99.27: X-COS-Purpose for a session turn's file).
|
|
769
|
+
// QA 6.99.27 (User 8): an upload that makes no progress for a minute stops and says so, and one that stopped while
|
|
770
|
+
// the page was hidden (the phone locked, the browser in the background) says that, never that the Mac did not answer.
|
|
771
|
+
var UPLOAD_STALL_MS = 60000;
|
|
772
|
+
function uploadRequest(path, body, headers, onProgress, t, extra) {
|
|
644
773
|
return new Promise(function (resolve, reject) {
|
|
645
774
|
var xhr = new XMLHttpRequest();
|
|
646
775
|
t.xhr = xhr;
|
|
776
|
+
var stall = null, stalled = false, wasHidden = !!document.hidden;
|
|
777
|
+
var onVisibility = function () { if (document.hidden) wasHidden = true; };
|
|
778
|
+
var watch = function () { clearTimeout(stall); stall = setTimeout(function () { stalled = true; try { xhr.abort(); } catch (_e) {} }, UPLOAD_STALL_MS); };
|
|
779
|
+
var settle = function () {
|
|
780
|
+
clearTimeout(stall); stall = null; t.xhr = null;
|
|
781
|
+
if (document.removeEventListener) document.removeEventListener('visibilitychange', onVisibility);
|
|
782
|
+
};
|
|
783
|
+
// The bytes are all sent: the Mac is reading the file, which is not a stall.
|
|
784
|
+
var sent = function () { clearTimeout(stall); stall = null; };
|
|
785
|
+
var stopped = function () {
|
|
786
|
+
var err = wasHidden || document.hidden ? new Error('The upload stopped while the phone was locked. Retry it.') : new Error('The upload stopped. Nothing moved for a minute. Retry it.');
|
|
787
|
+
err.code = wasHidden || document.hidden ? 'upload_locked' : 'upload_stalled';
|
|
788
|
+
return err;
|
|
789
|
+
};
|
|
790
|
+
if (document.addEventListener) document.addEventListener('visibilitychange', onVisibility);
|
|
647
791
|
xhr.open('POST', path, true);
|
|
648
792
|
xhr.setRequestHeader('X-COS-Web', '1');
|
|
649
793
|
Object.keys(headers).forEach(function (h) { xhr.setRequestHeader(h, headers[h]); });
|
|
650
|
-
if (
|
|
794
|
+
if (extra) Object.keys(extra).forEach(function (h) { xhr.setRequestHeader(h, extra[h]); });
|
|
795
|
+
if (xhr.upload) {
|
|
796
|
+
xhr.upload.onprogress = function (e) {
|
|
797
|
+
if (e.lengthComputable && e.total) { onProgress(e.loaded / e.total); if (e.loaded >= e.total) { sent(); return; } }
|
|
798
|
+
watch();
|
|
799
|
+
};
|
|
800
|
+
xhr.upload.onload = sent;
|
|
801
|
+
}
|
|
651
802
|
xhr.onload = function () {
|
|
652
|
-
|
|
803
|
+
settle();
|
|
653
804
|
var data = {}; try { data = JSON.parse(xhr.responseText || '{}'); } catch (_e) { data = {}; }
|
|
654
805
|
if (xhr.status >= 200 && xhr.status < 300) return resolve(data);
|
|
655
806
|
var err = new Error('The Mac answered ' + xhr.status + '.'); err.status = xhr.status;
|
|
656
807
|
err.code = data && (typeof data.error === 'string' ? data.error : data.error && data.error.code) || '';
|
|
657
808
|
reject(err);
|
|
658
809
|
};
|
|
659
|
-
xhr.onerror = function () {
|
|
660
|
-
|
|
810
|
+
xhr.onerror = function () {
|
|
811
|
+
settle();
|
|
812
|
+
if (wasHidden || document.hidden) return reject(stopped());
|
|
813
|
+
var err = new Error('The Mac did not answer.'); err.code = 'network'; reject(err);
|
|
814
|
+
};
|
|
815
|
+
xhr.onabort = function () {
|
|
816
|
+
settle();
|
|
817
|
+
if (stalled) return reject(stopped());
|
|
818
|
+
var err = new Error('Removed.'); err.code = 'aborted'; reject(err);
|
|
819
|
+
};
|
|
661
820
|
xhr.send(body);
|
|
821
|
+
watch();
|
|
662
822
|
});
|
|
663
823
|
}
|
|
664
824
|
var UPLOAD_COPY = {
|
|
665
825
|
batch_too_large: 'Too large for one upload.', too_many_images: 'Up to 5 attachments per message.',
|
|
666
826
|
media_processing_unavailable: 'The Mac cannot read photos right now.', unsupported_kind: 'COS cannot read this kind of file.',
|
|
667
827
|
unsupported_media_type: 'COS cannot read this kind of file.', file_too_large: 'Too large for COS.', attachment_too_large: 'Too large for COS.',
|
|
668
|
-
server_maintenance: 'The Mac is updating. Retry in a minute.', network: 'The Mac did not answer. Retry when it is awake.'
|
|
828
|
+
server_maintenance: 'The Mac is updating. Retry in a minute.', network: 'The Mac did not answer. Retry when it is awake.',
|
|
829
|
+
upload_locked: 'The upload stopped while the phone was locked. Retry it.', upload_stalled: 'The upload stopped. Nothing moved for a minute. Retry it.'
|
|
669
830
|
};
|
|
670
|
-
function uploadError(
|
|
831
|
+
function uploadError(box, t, err) {
|
|
671
832
|
if (err && err.code === 'aborted') return;
|
|
672
833
|
t.state = 'failed';
|
|
673
834
|
if (err && err.status === 401) {
|
|
674
835
|
// Signed out mid-message (the browser session expired or every browser was signed out). Nothing is thrown away:
|
|
675
836
|
// the words and the files stay, and Sign in again opens the sign-in screen on purpose, not by surprise.
|
|
676
837
|
t.error = 'Signed out';
|
|
677
|
-
|
|
678
|
-
|
|
838
|
+
box.signedOut = true;
|
|
839
|
+
var ui = uiOf(box); if (ui.signedOut) ui.signedOut(box);
|
|
679
840
|
} else {
|
|
680
841
|
t.error = uploadErrorText(err);
|
|
681
842
|
}
|
|
682
|
-
|
|
843
|
+
boxChanged(box);
|
|
683
844
|
}
|
|
684
845
|
/**
|
|
685
846
|
* 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,
|
|
@@ -689,7 +850,7 @@
|
|
|
689
850
|
*/
|
|
690
851
|
function uploadMedia(t, hooks) {
|
|
691
852
|
var at = new Date(t.file.lastModified || Date.now()).toISOString();
|
|
692
|
-
if (t.kind === 'photo') {
|
|
853
|
+
if (t.kind === 'photo' && !t.raw) {
|
|
693
854
|
return resizePhoto(t.file).then(function (dataUrl) {
|
|
694
855
|
var data = dataUrl.slice(dataUrl.indexOf(',') + 1);
|
|
695
856
|
var bytes = Math.floor(data.length * 3 / 4);
|
|
@@ -704,7 +865,7 @@
|
|
|
704
865
|
'Content-Type': t.mime || 'application/octet-stream',
|
|
705
866
|
'X-COS-Filename': encodeURIComponent(t.name),
|
|
706
867
|
'X-COS-Captured-At': at
|
|
707
|
-
}, function (p) { hooks.progress(p); }, t).then(function (d) { return d && d.attachment; });
|
|
868
|
+
}, function (p) { hooks.progress(p); }, t, t.purpose ? { 'X-COS-Purpose': t.purpose } : null).then(function (d) { return d && d.attachment; });
|
|
708
869
|
}
|
|
709
870
|
/** The chip's words for a failed upload (never the sign-in case, which each caller says its own way). */
|
|
710
871
|
function uploadErrorText(err) {
|
|
@@ -712,22 +873,22 @@
|
|
|
712
873
|
if (err && !err.status && !err.code) return err.message || 'Did not upload';
|
|
713
874
|
return UPLOAD_COPY[err && err.code] || (err && err.status === 413 ? 'Too large for COS.' : 'Did not upload');
|
|
714
875
|
}
|
|
715
|
-
function startUpload(
|
|
716
|
-
t.state = 'reading'; t.progress = 0; t.error = '';
|
|
876
|
+
function startUpload(box, t) {
|
|
877
|
+
t.state = 'reading'; t.progress = 0; t.error = ''; t.mediaId = '';
|
|
717
878
|
var step = uploadMedia(t, {
|
|
718
|
-
uploading: function () { t.state = 'uploading';
|
|
719
|
-
progress: function (p) { t.progress = p; paintProgress(
|
|
879
|
+
uploading: function () { t.state = 'uploading'; boxChanged(box); },
|
|
880
|
+
progress: function (p) { t.progress = p; paintProgress(box, t); }
|
|
720
881
|
});
|
|
721
882
|
step.then(function (ref) {
|
|
722
|
-
if (!findAtt(
|
|
883
|
+
if (!findAtt(box, t.id)) return;
|
|
723
884
|
if (!ref || !ref.id) { var e = new Error('The Mac did not keep the file.'); e.code = ''; throw e; }
|
|
724
|
-
t.mediaId = ref.id; t.state = 'ready'; t.progress = 1;
|
|
725
|
-
|
|
885
|
+
t.mediaId = ref.id; t.state = 'ready'; t.progress = 1; t.keptAt = Date.now();
|
|
886
|
+
boxChanged(box);
|
|
726
887
|
}).catch(function (err) {
|
|
727
|
-
if (!findAtt(
|
|
728
|
-
if (err && err.code === 'photo_too_large') { t.state = 'failed'; t.error = uploadErrorText(err);
|
|
729
|
-
if (err && !err.status && !err.code) { t.state = 'failed'; t.error = uploadErrorText(err);
|
|
730
|
-
uploadError(
|
|
888
|
+
if (!findAtt(box, t.id)) return;
|
|
889
|
+
if (err && err.code === 'photo_too_large') { t.state = 'failed'; t.error = uploadErrorText(err); boxChanged(box); return; }
|
|
890
|
+
if (err && !err.status && !err.code) { t.state = 'failed'; t.error = uploadErrorText(err); boxChanged(box); return; }
|
|
891
|
+
uploadError(box, t, err);
|
|
731
892
|
});
|
|
732
893
|
}
|
|
733
894
|
function readyAtts(k) { return k.atts.filter(function (t) { return t.state === 'ready' && t.mediaId; }); }
|
|
@@ -945,8 +1106,13 @@
|
|
|
945
1106
|
return ensureCaps().then(function (caps) {
|
|
946
1107
|
if (!caps || !caps.jobs) { var off = new Error('Ask needs the durable query jobs switch on your Mac.'); off.code = 'durable_query_jobs_disabled'; throw off; }
|
|
947
1108
|
return new Promise(function (resolve, reject) {
|
|
1109
|
+
// 11/07: files attached in the Ask sheet go with the question, as from the chat's own clip (ready ids only:
|
|
1110
|
+
// the sheet never asks while one is uploading or failed).
|
|
1111
|
+
var o = {};
|
|
1112
|
+
if (extra && extra.context) o.context = extra.context;
|
|
1113
|
+
if (extra && Array.isArray(extra.attachmentIds) && extra.attachmentIds.length) { o.attachmentIds = extra.attachmentIds.slice(0, MAX_ATTS); o.atts = Array.isArray(extra.atts) ? extra.atts.slice(0, MAX_ATTS) : []; }
|
|
948
1114
|
submitAsk(String(text || '').trim(), null, function (ok) { if (ok) resolve(['cos']); else reject(new Error('Not sent. The question is in Ask COS; try again there.')); },
|
|
949
|
-
|
|
1115
|
+
o.context || o.attachmentIds ? o : undefined);
|
|
950
1116
|
});
|
|
951
1117
|
});
|
|
952
1118
|
}
|
|
@@ -1647,8 +1813,8 @@
|
|
|
1647
1813
|
if (what === 'cancel') return cancel(t.getAttribute('data-cid'));
|
|
1648
1814
|
if (what === 'att-remove') return removeAtt(kOf(t), t.getAttribute('data-att'));
|
|
1649
1815
|
if (what === 'att-retry') {
|
|
1650
|
-
var k = kOf(t)
|
|
1651
|
-
if (att) {
|
|
1816
|
+
var k = kOf(t);
|
|
1817
|
+
if (findAtt(k, t.getAttribute('data-att'))) { setNote(k, ''); boxRetry(k, t.getAttribute('data-att')); }
|
|
1652
1818
|
return;
|
|
1653
1819
|
}
|
|
1654
1820
|
if (what === 'sign-in') { if (W.showAuth) W.showAuth('Sign in again. Your message and files are kept; tap Retry on each file after.'); return; }
|
|
@@ -1660,7 +1826,7 @@
|
|
|
1660
1826
|
}
|
|
1661
1827
|
function isComposerInput(el) { return !!(el && el.classList && el.classList.contains('cc-input')); }
|
|
1662
1828
|
function onKey(e) {
|
|
1663
|
-
if (!isComposerInput(e.target) || e.key !== 'Enter' || e.isComposing || e.shiftKey) return;
|
|
1829
|
+
if (!isComposerInput(e.target) || e.key !== 'Enter' || e.isComposing || e.keyCode === 229 || e.shiftKey) return;
|
|
1664
1830
|
e.preventDefault();
|
|
1665
1831
|
var k = kOf(e.target);
|
|
1666
1832
|
if (e.altKey) { if (k === CHAT && dropOn()) doDrop(); return; }
|
|
@@ -1730,6 +1896,34 @@
|
|
|
1730
1896
|
setOnChange: function (fn) { C.onChange = fn; }, setOnAnswered: function (fn) { C.onAnswered = fn; },
|
|
1731
1897
|
// Test and render hooks: what the chat holds right now.
|
|
1732
1898
|
classify: classify, uploadMedia: uploadMedia, uploadErrorText: uploadErrorText, uuid: uuid,
|
|
1899
|
+
// 11/07: the attach kit, for Today's reply and note boxes and the Ask sheet (see "the attach kit" above).
|
|
1900
|
+
attach: {
|
|
1901
|
+
max: MAX_ATTS, accept: ACCEPT,
|
|
1902
|
+
// opts.anyBytes (a session turn): any file up to that size, sent as it is (classifyForSession).
|
|
1903
|
+
box: function (ui, opts) { return { atts: [], signedOut: false, ui: ui || {}, anyBytes: opts && opts.anyBytes > 0 ? opts.anyBytes : 0 }; },
|
|
1904
|
+
add: boxAdd, remove: boxRemove, retry: boxRetry, clear: boxClear,
|
|
1905
|
+
take: boxTake, restore: boxRestore,
|
|
1906
|
+
pending: uploadsPending, failed: uploadsFailed,
|
|
1907
|
+
ids: function (box) { return readyAtts(box).slice(0, MAX_ATTS).map(function (t) { return t.mediaId; }); },
|
|
1908
|
+
sent: function (box) { return readyAtts(box).slice(0, MAX_ATTS).map(function (t) { return { name: t.name, kind: t.kind, thumb: t.thumb }; }); },
|
|
1909
|
+
chipsHtml: function (box, attr) { return box.atts.map(function (t) { return chipHtml(t, attr); }).join(''); },
|
|
1910
|
+
clipHtml: clipHtml, cameraHtml: cameraHtml, limitLine: limitLine,
|
|
1911
|
+
// Files on the clipboard (a pasted screenshot), or none.
|
|
1912
|
+
pasted: function (e) { var f = e && e.clipboardData && e.clipboardData.files; return f && f.length ? f : null; },
|
|
1913
|
+
// Files dragged over `zone`: marks it while they hover, and answers the files on the drop. Null for anything else.
|
|
1914
|
+
dragged: function (e, zone) {
|
|
1915
|
+
var types = e && e.dataTransfer && e.dataTransfer.types;
|
|
1916
|
+
if (!types || Array.prototype.indexOf.call(types, 'Files') < 0) return null;
|
|
1917
|
+
e.preventDefault();
|
|
1918
|
+
if (e.type === 'dragover' || e.type === 'dragenter') { zone.classList.add('dragging'); return null; }
|
|
1919
|
+
zone.classList.remove('dragging');
|
|
1920
|
+
return e.type === 'drop' ? e.dataTransfer.files : null;
|
|
1921
|
+
},
|
|
1922
|
+
// How many files a session turn takes on this Mac (threadTurnAttachments); 0 means words only.
|
|
1923
|
+
turnFiles: function () { return C.caps && C.caps.turnFiles > 0 ? C.caps.turnFiles : 0; },
|
|
1924
|
+
turnFileBytes: function () { return C.caps && C.caps.turnFiles > 0 && C.caps.turnFileBytes > 0 ? C.caps.turnFileBytes : 0; },
|
|
1925
|
+
askOn: askOn, ensureCaps: ensureCaps
|
|
1926
|
+
},
|
|
1733
1927
|
// 6.99.19: Prep. meetings.js reads the capability from here (one /api/health read for the page).
|
|
1734
1928
|
prepCaps: prepCaps,
|
|
1735
1929
|
// Today's Next up (app.js prepNext): Prep me for that meeting, as the chat's own Prep me does. With Prep on the Mac
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
.mt-wd { text-align: center; font: 600 10px/1 var(--mono); letter-spacing: 0.04em; color: var(--tertiary); padding: 2px 0 4px; }
|
|
22
22
|
.mt-day, .mt-blank { min-height: 42px; }
|
|
23
23
|
.mt-day { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; border-radius: 8px; color: var(--ink); font: 400 14px/1 var(--body); }
|
|
24
|
-
.mt-day:disabled { opacity:
|
|
24
|
+
.mt-day:disabled { opacity: 1; }
|
|
25
|
+
/* 6.99.30 (design QA): a day with nothing to open read at 2.35:1 at 38% ink; the secondary colour keeps it quiet and
|
|
26
|
+
readable. */
|
|
27
|
+
.mt-day:disabled .mt-dnum { color: var(--sub); }
|
|
25
28
|
.mt-day:not(:disabled):hover { background: var(--qopen); }
|
|
26
29
|
.mt-day[aria-pressed='true'] { background: color-mix(in srgb, var(--tint) 18%, transparent); font-weight: 600; }
|
|
27
30
|
.mt-day:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
@@ -353,5 +356,5 @@
|
|
|
353
356
|
@media (min-width: 860px) {
|
|
354
357
|
.mt-turns { height: clamp(320px, calc(100dvh - 400px), 1100px); }
|
|
355
358
|
.mt-lgrid { grid-template-columns: minmax(0, 1fr) minmax(280px, min(380px, 38%)); align-items: start; column-gap: 24px; }
|
|
356
|
-
.mt-lgrid > .mt-side-panel { position: sticky; top: calc(62px + var(--safe-top, 0px)); max-height: calc(100vh - 80px - var(--safe-top, 0px)); overflow: auto; }
|
|
359
|
+
.mt-lgrid > .mt-side-panel { position: sticky; top: calc(62px + var(--safe-top, 0px)); max-height: calc(100vh - 80px - var(--safe-top, 0px)); max-height: calc(100dvh - 80px - var(--safe-top, 0px)); overflow: auto; }
|
|
357
360
|
}
|