@gotcos/glasses-server 6.99.12 → 6.99.16
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 +78 -0
- package/package.json +1 -1
- package/server/index.ts +23 -2
- 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 +22 -0
- package/server/lib/media-store.ts +80 -0
- package/server/lib/meeting-batch-transcribe.ts +15 -2
- package/server/lib/permission-broker.ts +6 -4
- package/server/lib/phone-recording.ts +1 -1
- package/server/routes/cos-thread.ts +79 -0
- package/server/routes/drops.ts +4 -2
- package/server/routes/permission-broker.ts +2 -2
- package/server/routes/transcribe-stream.ts +9 -1
- 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 +17 -0
- package/server/web-control/panes/cos-chat.js +185 -40
- package/server/web-control/panes/meetings.css +61 -4
- package/server/web-control/panes/meetings.js +358 -54
- package/server/web-control/panes/messages.js +25 -18
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
Data: GET /api/meetings?limit=200&domain=all&month=, /api/meetings/search, /api/meetings/detail,
|
|
6
6
|
/api/meetings/live-chunks, /api/meeting/sessions/:id/status, /api/meeting/orphans, /api/meeting-ask/*,
|
|
7
7
|
/api/live-cues/history, /api/context/status. Writes only on a tap: Meeting Ask, Stop and save, End and save,
|
|
8
|
-
Recover, Save, Save as follow-up.
|
|
8
|
+
Recover, Save, Save as follow-up, and (6.99.14) Add file, paste or drop in the live view: /api/media or
|
|
9
|
+
/api/media/file, then POST /api/meeting-ask/files/media.
|
|
9
10
|
Routes: #/meetings, #/meetings/<domain>/<month>/<filename>, #/meetings/live/<sessionId>. */
|
|
10
11
|
(function () {
|
|
11
12
|
'use strict';
|
|
@@ -812,6 +813,7 @@
|
|
|
812
813
|
if (r.startTime != null) f.start = r.startTime;
|
|
813
814
|
if (r.lastActivityAt != null) f.last = Math.max(f.last || 0, r.lastActivityAt);
|
|
814
815
|
if (typeof r.titleSteer === 'string') f.titleSteer = cleanTitle(r.titleSteer) || null;
|
|
816
|
+
setLevels(f, r.levels);
|
|
815
817
|
liveChanged();
|
|
816
818
|
}, function () {});
|
|
817
819
|
}
|
|
@@ -1793,17 +1795,32 @@
|
|
|
1793
1795
|
return html;
|
|
1794
1796
|
}
|
|
1795
1797
|
var FILE_STATE = { pending: 'Reading', unsupported: 'No readable text', failed: 'Could not be read', 'secret-held': 'Held: it appears to contain credentials' };
|
|
1796
|
-
|
|
1798
|
+
// `live`: the live view while it records, where this page adds files itself (Add file, paste, drop). There the row is
|
|
1799
|
+
// always drawn, one line tall whatever it says, so files that arrive with the Ask history, an upload that starts and
|
|
1800
|
+
// one that finishes never move what is under it. Open, the files and the uploads are rows of the same height: a
|
|
1801
|
+
// finished upload becomes its file in place. { items, note } are the uploads and the last refusal.
|
|
1802
|
+
function filesRowHtml(sid, live) {
|
|
1797
1803
|
var a = A(sid), shown = (a.files || []).filter(function (x) { return x && x.status !== 'removed' && String(x.name || '').trim(); });
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
var
|
|
1801
|
-
|
|
1802
|
-
|
|
1804
|
+
var ups = live ? live.items : [], note = live ? live.note : '';
|
|
1805
|
+
if (!shown.length && !(live && live.on) && !ups.length && !note) return '';
|
|
1806
|
+
var busy = ups.filter(function (t) { return t.state !== 'failed'; }).length, failed = ups.length - busy;
|
|
1807
|
+
var title = shown.length ? 'Files \u00b7 ' + shown.length : (live && live.on ? 'No files yet. Add file, paste or drop one here.' : 'Files');
|
|
1808
|
+
var extra = busy ? ' \u00b7 adding ' + busy : '';
|
|
1809
|
+
var bad = failed ? (failed === 1 ? '1 did not upload' : failed + ' did not upload') : '';
|
|
1810
|
+
var openable = shown.length + ups.length > 0 || !!note;
|
|
1811
|
+
var html = '<div class="mt-files' + (live ? ' live' : '') + '">' + (openable
|
|
1812
|
+
? '<button type="button" class="mt-files-head" data-mt="files-toggle" data-sid="' + esc(sid) + '" aria-expanded="' + !!a.filesOpen + '">'
|
|
1813
|
+
: '<div class="mt-files-head quiet">') +
|
|
1814
|
+
icon('clip') + '<span>' + esc(title + extra) + (bad ? '<span class="mt-amber"> \u00b7 ' + esc(bad) + '</span>' : '') + '</span>' + (openable ? icon('chev') + '</button>' : '</div>');
|
|
1815
|
+
if (a.filesOpen && openable) {
|
|
1803
1816
|
html += '<ul class="mt-files-list">' + shown.map(function (x) {
|
|
1804
|
-
var
|
|
1805
|
-
return '<li><span class="mt-files-name">' + esc(x.name) + '</span>' + (
|
|
1806
|
-
}).join('') + '</ul
|
|
1817
|
+
var line = [x.status !== 'ready' ? (x.notice || FILE_STATE[x.status] || '') : (x.notice || ''), x.origin === 'phone' ? 'Added from a phone or browser' : ''].filter(Boolean).join(' \u00b7 ');
|
|
1818
|
+
return '<li><span class="mt-files-name">' + esc(x.name) + '</span>' + (line ? '<span class="mt-muted">' + esc(line) + '</span>' : '') + '</li>';
|
|
1819
|
+
}).join('') + ups.map(uploadRowHtml).join('') + '</ul>' +
|
|
1820
|
+
(note ? '<p class="mt-amber-line mt-files-note" role="status">' + esc(note) + '</p>' : '') +
|
|
1821
|
+
'<p class="mt-muted mt-files-note">' + (live && live.on
|
|
1822
|
+
? 'Meeting Ask reads these files. Add more here; remove them in COS Control on your Mac.'
|
|
1823
|
+
: 'Meeting Ask reads these files. Add or remove them in COS Control on your Mac.') + '</p>';
|
|
1807
1824
|
}
|
|
1808
1825
|
return html + '</div>';
|
|
1809
1826
|
}
|
|
@@ -1901,18 +1918,27 @@
|
|
|
1901
1918
|
}
|
|
1902
1919
|
function openLive(sid) {
|
|
1903
1920
|
S.view = 'live';
|
|
1904
|
-
if (S.liveSid !== sid) { S.liveSid = sid; S.
|
|
1921
|
+
if (S.liveSid !== sid) { S.liveSid = sid; S.confirm = ''; S.liveCopy = ''; S.liveReason = ''; }
|
|
1922
|
+
// A new box starts at its newest line, following.
|
|
1923
|
+
S.follow = true;
|
|
1905
1924
|
var f = feedFor(sid);
|
|
1906
1925
|
A(sid).ctx = { live: true };
|
|
1907
1926
|
W.setCrumb(liveTitle(f));
|
|
1908
|
-
|
|
1909
|
-
|
|
1927
|
+
// 6.99.14: the transcript scrolls inside its own box, as Control's does, so a new line never moves the page: the
|
|
1928
|
+
// title, the recording bar, the files and the Ask panel stay where the reader left them. Everything that changes
|
|
1929
|
+
// height while it records (the stop line, the files, the confirm card) sits under the box, never above it.
|
|
1930
|
+
pane.innerHTML = '<div class="cx mt mt-livev" data-tint="meetings"><div id="mt-lhead"></div><div class="mt-lgrid"><div class="mt-lmain" id="mt-lmain">' +
|
|
1931
|
+
'<div class="mt-tbox"><div id="mt-turns" class="mt-turns" tabindex="0" role="log" aria-live="polite" aria-label="Live transcript"></div>' +
|
|
1932
|
+
'<button type="button" class="cx-btn quiet mt-jump" id="mt-jump" data-mt="jump" hidden>Jump to live</button></div>' +
|
|
1933
|
+
'<div id="mt-lbar" class="mt-lbarslot"></div><div id="mt-lfiles"></div><div id="mt-lfoot"></div>' +
|
|
1934
|
+
'<input type="file" id="mt-lpick" class="mt-pick" multiple accept="' + esc(LIVE_ACCEPT) + '" tabindex="-1" aria-hidden="true"></div>' +
|
|
1910
1935
|
'<aside class="mt-side-panel" id="mt-ask-' + safeId(sid) + '" data-sid="' + esc(sid) + '">' + sidePanelHtml(sid) + '</aside></div></div>';
|
|
1911
|
-
S.headHtml = S.footHtml = null;
|
|
1936
|
+
S.headHtml = S.footHtml = S.barHtml = S.filesHtml = null;
|
|
1912
1937
|
paintLiveHead(); paintTurns(true); paintLiveFoot();
|
|
1913
|
-
clearInterval(S.liveTimer);
|
|
1938
|
+
clearInterval(S.liveTimer); clearInterval(S.clockTimer);
|
|
1914
1939
|
livePoll();
|
|
1915
1940
|
S.liveTimer = setInterval(livePoll, 1500);
|
|
1941
|
+
S.clockTimer = setInterval(tickClock, 1000);
|
|
1916
1942
|
panelOpen(sid);
|
|
1917
1943
|
}
|
|
1918
1944
|
// Control's LiveMeeting.title: the typed name, else a calendar event that started within CALENDAR_MATCH_MS of the
|
|
@@ -1951,7 +1977,9 @@
|
|
|
1951
1977
|
var t = Date.now(), files = a.body.sessions || [];
|
|
1952
1978
|
S.files = files;
|
|
1953
1979
|
var file = files.filter(function (s) { return s.sessionId === f.sid; })[0];
|
|
1954
|
-
var
|
|
1980
|
+
var r = a.body.read;
|
|
1981
|
+
if (r && r.sessionId === f.sid && !r.unchanged) setLevels(f, r.levels);
|
|
1982
|
+
var changed = applyRead(f, r, t);
|
|
1955
1983
|
if (file) { var m = num(file.mtimeMs); if (!f.last || m > f.last) f.last = m; }
|
|
1956
1984
|
else if (!f.endedAt) changed = markEnded(f, t) || changed;
|
|
1957
1985
|
if (!f.endedAt && f.last && t - f.last >= QUIET_MS && t - f.statusAt >= STATUS_QUIET_MS) askStatus(f, t);
|
|
@@ -1992,22 +2020,276 @@
|
|
|
1992
2020
|
: (ended ? '' : (isMac(sid) ? 'Recording on this Mac' : 'Recording on the glasses') + (f.titleSteer && f.start ? ' · started ' + clockOf(new Date(f.start)) : ''));
|
|
1993
2021
|
var quiet = ended ? null : quietLabel(f.last, now);
|
|
1994
2022
|
var sub = f.titleSteer && f.start && !isPhone(sid) ? (durationLabel(f.start, now, f.endedAt) || 'Starting') : liveSubtitle(f.start, now, f.endedAt);
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
else if (!ended && isPhone(sid)) controls = '<span class="mt-side-note">Stop on iPhone</span>';
|
|
1998
|
-
var line = stopLineFor(sid);
|
|
2023
|
+
// Every line here is one line tall whatever it says (the CSS keeps each to one line), so the header never changes
|
|
2024
|
+
// height while it records and the transcript under it never moves.
|
|
1999
2025
|
var html = '<div class="mt-dhead mt-lhead"><div class="mt-ltitle"><span class="mt-livedot big' + (ended ? '' : ' on') + '"></span><h2 class="mt-dtitle">' + esc(liveTitle(f)) + '</h2></div>' +
|
|
2000
2026
|
(status ? '<p class="mt-lstatus">' + esc(status) + '</p>' : '') +
|
|
2001
2027
|
(isPhone(sid) ? '<p class="mt-muted">iPhone microphone recordings are anonymous. Names and voice training are unavailable.</p>' : '') +
|
|
2002
2028
|
'<p class="mt-muted">' + esc(LIVE_HEADER) + '</p>' +
|
|
2003
2029
|
(isMac(sid) ? '<p class="mt-muted">Mac playback · microphone off · voice numbers do not identify people</p>' : '') +
|
|
2004
|
-
'<p class="mt-lsub">' + esc(sub) + (quiet ? '<span class="mt-amber"> · ' + esc(quiet) + '</span>' : '') + '</p>'
|
|
2005
|
-
liveHandoffLine(f) + filesRowHtml(sid) +
|
|
2006
|
-
(line ? '<p class="mt-stopline' + (S.stop && S.stop.nothing ? ' bad' : '') + '">' + esc(line) + '</p>' : '') +
|
|
2007
|
-
(controls ? '<div class="mt-bar">' + controls + '</div>' : '') +
|
|
2008
|
-
(S.confirm === 'stop:' + sid ? confirmCard(STOP_TITLE, STOP_LINE, 'stop-yes', 'Stop and save', 'Cancel', sid) : '') + '</div>';
|
|
2030
|
+
'<p class="mt-lsub">' + esc(sub) + (quiet ? '<span class="mt-amber"> · ' + esc(quiet) + '</span>' : '') + '</p></div>';
|
|
2009
2031
|
if (html !== S.headHtml) { S.headHtml = html; el.innerHTML = html; }
|
|
2010
2032
|
if (W.crumb() !== liveTitle(f)) W.setCrumb(liveTitle(f));
|
|
2033
|
+
paintLiveBar(); paintLiveFiles();
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// ── the recording bar (LiveRecordingBar, 0.5.339) ──────
|
|
2037
|
+
// The red dot, the elapsed time on its own one-second clock, the level strip, Add file and Stop and save, in one row
|
|
2038
|
+
// of fixed height under the transcript. Under it: the stop line and the confirm card. Once it ends: the handoff line.
|
|
2039
|
+
// The row is rebuilt only when what it offers changes; the clock and the strip draw in place.
|
|
2040
|
+
function elapsedLabel(ms) {
|
|
2041
|
+
var t = Math.max(0, Math.floor((isFinite(ms) ? ms : 0) / 1000)), h = Math.floor(t / 3600), m = Math.floor((t % 3600) / 60), s = t % 60;
|
|
2042
|
+
return h > 0 ? h + ':' + pad2(m) + ':' + pad2(s) : m + ':' + pad2(s);
|
|
2043
|
+
}
|
|
2044
|
+
function clockText(f, now) { return f && f.start ? elapsedLabel(now - f.start) : 'Starting'; }
|
|
2045
|
+
// The Meeting Ask files route takes the ids Meeting Ask does (server/lib/ask-session-id.ts ASK_SESSION).
|
|
2046
|
+
var ASK_SESSION = /^(?:meeting_[A-Za-z0-9_-]{1,89}|(?:mac_|phone_(?:[0-9]{13}_)?)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
|
|
2047
|
+
function canAddFiles(f) { return !!f && !f.endedAt && ASK_SESSION.test(f.sid); }
|
|
2048
|
+
function liveStopHtml(sid) {
|
|
2049
|
+
if (isGlasses(sid)) return '<button type="button" class="cx-btn quiet mt-stopbtn" data-mt="stop-ask" data-sid="' + esc(sid) + '"' + (stopping(sid) ? ' disabled' : '') + '>Stop and save</button>';
|
|
2050
|
+
if (isPhone(sid)) return '<span class="mt-side-note">Stop on iPhone</span>';
|
|
2051
|
+
return '<span class="mt-side-note">Stop on your Mac</span>';
|
|
2052
|
+
}
|
|
2053
|
+
function liveBarHtml(f) {
|
|
2054
|
+
var sid = f.sid;
|
|
2055
|
+
if (f.endedAt) return liveHandoffLine(f);
|
|
2056
|
+
var line = stopLineFor(sid);
|
|
2057
|
+
return '<div class="mt-lbar" role="group" aria-label="Recording">' +
|
|
2058
|
+
'<span class="mt-livedot on" aria-hidden="true"></span>' +
|
|
2059
|
+
'<span class="mt-ltime' + (f.start ? '' : ' starting') + '" id="mt-ltime" role="timer" aria-label="Recording time">' + esc(clockText(f, Date.now())) + '</span>' +
|
|
2060
|
+
'<span class="mt-meterwrap"><canvas class="mt-meter" id="mt-meter" role="img" aria-label="' + esc(LEVEL_LABEL) + '"></canvas>' +
|
|
2061
|
+
'<span class="mt-glyph" aria-hidden="true"><i></i><i></i><i></i></span></span>' +
|
|
2062
|
+
(canAddFiles(f) ? '<button type="button" class="cx-btn quiet mt-addfile" data-mt="add-file" aria-label="Add file" title="Add a screenshot or files to this meeting">' + icon('clip') + '<span class="mt-addlabel">Add file</span></button>' : '') +
|
|
2063
|
+
liveStopHtml(sid) + '</div>' +
|
|
2064
|
+
(line ? '<p class="mt-stopline' + (S.stop && S.stop.nothing ? ' bad' : '') + '">' + esc(line) + '</p>' : '') +
|
|
2065
|
+
(S.confirm === 'stop:' + sid ? confirmCard(STOP_TITLE, STOP_LINE, 'stop-yes', 'Stop and save', 'Cancel', sid) : '');
|
|
2066
|
+
}
|
|
2067
|
+
function paintLiveBar() {
|
|
2068
|
+
var el = $('mt-lbar'); if (!el || S.view !== 'live') return;
|
|
2069
|
+
var f = S.feeds[S.liveSid]; if (!f) return;
|
|
2070
|
+
// The clock's own text is left out of the comparison, so a second ticking never rebuilds the row (or the strip).
|
|
2071
|
+
var html = liveBarHtml(f), key = html.replace(/(id="mt-ltime"[^>]*>)[^<]*/, '$1');
|
|
2072
|
+
if (key !== S.barHtml) { S.barHtml = key; el.innerHTML = html; drawMeter(); }
|
|
2073
|
+
}
|
|
2074
|
+
function tickClock() {
|
|
2075
|
+
if (S.view !== 'live' || document.hidden) return;
|
|
2076
|
+
var f = S.feeds[S.liveSid], el = $('mt-ltime');
|
|
2077
|
+
if (!f || !el) return;
|
|
2078
|
+
var text = clockText(f, Date.now());
|
|
2079
|
+
if (el.textContent !== text) el.textContent = text;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// ── the level strip (LiveChunkLevelStripView) ─────────
|
|
2083
|
+
// One bar per chunk from the live read's `levels` (server 6.87.0: the quiet gate's RMS and Silero speech seconds,
|
|
2084
|
+
// kept on the active-session record), newest at the right, a dot where the chunk held almost no speech, the oldest
|
|
2085
|
+
// clipped first when narrow. It trails the room by a chunk. Reduce Motion: one still bar for the newest chunk.
|
|
2086
|
+
var LEVEL_STEP = 5, SPEECH_FLOOR = 0.5, LEVEL_FLOOR = 0.27;
|
|
2087
|
+
var LEVEL_LABEL = 'Audio level, a few seconds behind';
|
|
2088
|
+
function levelSilent(l) { return l.speechSec != null ? l.speechSec < SPEECH_FLOOR : l.level <= LEVEL_FLOOR; }
|
|
2089
|
+
function cleanLevels(v) {
|
|
2090
|
+
if (!Array.isArray(v)) return null;
|
|
2091
|
+
var seen = {}, out = [];
|
|
2092
|
+
v.forEach(function (l) {
|
|
2093
|
+
if (!l || typeof l !== 'object' || !Number.isInteger(l.i) || l.i < 0 || seen[l.i]) return;
|
|
2094
|
+
if (typeof l.level !== 'number' || !(l.level >= 0 && l.level <= 1)) return;
|
|
2095
|
+
if (l.speechSec != null && !(typeof l.speechSec === 'number' && l.speechSec >= 0)) return;
|
|
2096
|
+
seen[l.i] = 1; out.push({ i: l.i, level: l.level, speechSec: l.speechSec == null ? null : l.speechSec });
|
|
2097
|
+
});
|
|
2098
|
+
return out.sort(function (a, b) { return a.i - b.i; }).slice(-120);
|
|
2099
|
+
}
|
|
2100
|
+
function visibleLevels(levels, width) { var fit = Math.floor(width / LEVEL_STEP); return fit < 1 ? [] : levels.slice(-fit); }
|
|
2101
|
+
function reduceMotion() { try { return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (_e) { return false; } }
|
|
2102
|
+
function drawMeter() {
|
|
2103
|
+
var c = $('mt-meter'); if (!c || !c.getContext) return;
|
|
2104
|
+
var f = S.feeds[S.liveSid], levels = (f && f.levels) || [];
|
|
2105
|
+
var wrap = c.parentNode; if (wrap && wrap.classList) wrap.classList.toggle('mt-nolevels', !levels.length);
|
|
2106
|
+
var last = levels[levels.length - 1];
|
|
2107
|
+
c.setAttribute('aria-label', LEVEL_LABEL + ': ' + (!last ? 'waiting for the first chunk' : levelSilent(last) ? 'quiet' : 'speech'));
|
|
2108
|
+
var w = c.clientWidth, h = c.clientHeight; if (!w || !h) return;
|
|
2109
|
+
var dpr = window.devicePixelRatio || 1;
|
|
2110
|
+
if (c.width !== Math.round(w * dpr) || c.height !== Math.round(h * dpr)) { c.width = Math.round(w * dpr); c.height = Math.round(h * dpr); }
|
|
2111
|
+
var g = c.getContext('2d'); if (!g) return;
|
|
2112
|
+
g.setTransform(dpr, 0, 0, dpr, 0, 0); g.clearRect(0, 0, w, h);
|
|
2113
|
+
if (!levels.length) return;
|
|
2114
|
+
var css = getComputedStyle(c), bar = css.getPropertyValue('--mt-bar').trim() || '#c2410c', dot = css.getPropertyValue('--mt-dot').trim() || '#8a8a8a';
|
|
2115
|
+
var mid = h / 2;
|
|
2116
|
+
if (reduceMotion()) {
|
|
2117
|
+
var lv = levelSilent(last) ? 0 : last.level;
|
|
2118
|
+
g.fillStyle = dot; g.globalAlpha = 0.35; g.fillRect(0, mid - 2, w, 4); g.globalAlpha = 1;
|
|
2119
|
+
g.fillStyle = bar; g.fillRect(0, mid - 2, Math.max(2, w * lv), 4);
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
var shown = visibleLevels(levels, w);
|
|
2123
|
+
for (var back = 0; back < shown.length; back++) {
|
|
2124
|
+
var l = shown[shown.length - 1 - back], x = w - (back + 0.5) * LEVEL_STEP;
|
|
2125
|
+
// The oldest fade out at the left edge, so clipped history reads as older, not cut.
|
|
2126
|
+
g.globalAlpha = Math.min(1, Math.max(0.15, x / 28));
|
|
2127
|
+
if (levelSilent(l)) { g.fillStyle = dot; g.beginPath(); g.arc(x, mid, 1, 0, Math.PI * 2); g.fill(); }
|
|
2128
|
+
else { var bh = Math.max(3, l.level * h); g.fillStyle = bar; g.fillRect(x - 1.5, mid - bh / 2, 3, bh); }
|
|
2129
|
+
}
|
|
2130
|
+
g.globalAlpha = 1;
|
|
2131
|
+
}
|
|
2132
|
+
function setLevels(f, raw) {
|
|
2133
|
+
var next = cleanLevels(raw); if (!next) return;
|
|
2134
|
+
var sig = next.map(function (l) { return l.i + ':' + l.level + ':' + l.speechSec; }).join('|');
|
|
2135
|
+
if (sig === f.levelSig) return;
|
|
2136
|
+
f.levelSig = sig; f.levels = next;
|
|
2137
|
+
if (S.view === 'live' && S.liveSid === f.sid) drawMeter();
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// ── files during a live meeting (LiveMeetingFiles) ─────
|
|
2141
|
+
// Add file, paste and drop: each file goes to the media store (W.cosChat.uploadMedia, the composer's own upload) and
|
|
2142
|
+
// then joins the meeting's files through POST /api/meeting-ask/files/media, the route the phone uses, so Meeting Ask
|
|
2143
|
+
// reads it and Copy full context carries it. A file never becomes text: a paste or a drop that holds a file is taken
|
|
2144
|
+
// as the file, and one the browser will not hand over is refused out loud instead of landing as its path.
|
|
2145
|
+
var LIVE_ACCEPT = 'image/*,.heic,.heif,application/pdf,.pdf,text/plain,.txt,text/markdown,.md,text/csv,.csv,application/json,.json,.xlsx';
|
|
2146
|
+
var MAX_LIVE_UPLOADS = 10;
|
|
2147
|
+
function LU() { return S.lup || (S.lup = { sid: '', items: [], note: '' }); }
|
|
2148
|
+
function liveUploads(sid) { var u = LU(); if (u.sid !== sid) { u.items.forEach(abortUpload); S.lup = { sid: sid, items: [], note: '' }; } return S.lup; }
|
|
2149
|
+
function abortUpload(t) { if (t.xhr) { try { t.xhr.abort(); } catch (_e) {} } }
|
|
2150
|
+
/** The files a paste or a drop carries: its File list, else the items of kind file. */
|
|
2151
|
+
function filesOf(dt) {
|
|
2152
|
+
if (!dt) return [];
|
|
2153
|
+
var list = Array.prototype.slice.call(dt.files || []);
|
|
2154
|
+
if (!list.length && dt.items) Array.prototype.forEach.call(dt.items, function (it) { if (it && it.kind === 'file' && it.getAsFile) { var x = it.getAsFile(); if (x) list.push(x); } });
|
|
2155
|
+
return list;
|
|
2156
|
+
}
|
|
2157
|
+
/** True when a paste or a drop is a file (or a file's link) rather than words: it must never be inserted as text. */
|
|
2158
|
+
function carriesFile(dt) {
|
|
2159
|
+
if (!dt) return false;
|
|
2160
|
+
var types = Array.prototype.slice.call(dt.types || []);
|
|
2161
|
+
if (types.indexOf('Files') >= 0 || filesOf(dt).length) return true;
|
|
2162
|
+
var uri = '';
|
|
2163
|
+
try { uri = types.indexOf('text/uri-list') >= 0 ? String(dt.getData('text/uri-list') || '') : ''; } catch (_e) { uri = ''; }
|
|
2164
|
+
return /^\s*file:/i.test(uri);
|
|
2165
|
+
}
|
|
2166
|
+
function liveFileKind(file) {
|
|
2167
|
+
var cc = W.cosChat;
|
|
2168
|
+
if (!cc || !cc.classify || !cc.uploadMedia) return { refuse: 'This page could not load its upload code. Reload the page, then try again.' };
|
|
2169
|
+
var c = cc.classify(file);
|
|
2170
|
+
if (c.refuse) return c;
|
|
2171
|
+
if (c.kind === 'video') return { refuse: (file.name || 'That video') + ' is a video. Meeting Ask reads documents and the text in images, so attach a PDF, a text file or a screenshot.' };
|
|
2172
|
+
return c;
|
|
2173
|
+
}
|
|
2174
|
+
function addLiveFiles(list) {
|
|
2175
|
+
var f = S.feeds[S.liveSid];
|
|
2176
|
+
if (S.view !== 'live' || !f) return;
|
|
2177
|
+
var u = liveUploads(f.sid), files = Array.prototype.slice.call(list || []), refusals = [];
|
|
2178
|
+
if (!canAddFiles(f)) { liveNote(f.endedAt ? 'This recording has ended. Files can be added while it records.' : 'Meeting Ask does not take files for this recording.'); return; }
|
|
2179
|
+
files.forEach(function (file) {
|
|
2180
|
+
if (u.items.filter(function (t) { return t.state !== 'failed'; }).length >= MAX_LIVE_UPLOADS) { refusals.push('Up to 10 files at a time. ' + (file.name || 'A file') + ' was not added.'); return; }
|
|
2181
|
+
var c = liveFileKind(file);
|
|
2182
|
+
if (c.refuse) { refusals.push(c.refuse); return; }
|
|
2183
|
+
var t = { id: W.cosChat.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: '' };
|
|
2184
|
+
u.items.push(t);
|
|
2185
|
+
sendLiveFile(f.sid, t, false);
|
|
2186
|
+
});
|
|
2187
|
+
u.note = refusals.join(' ');
|
|
2188
|
+
// Opened by the person's own add, in the same task as the paste or drop, never later when an upload lands.
|
|
2189
|
+
A(f.sid).filesOpen = true;
|
|
2190
|
+
paintLiveFiles();
|
|
2191
|
+
}
|
|
2192
|
+
var ATTACH_COPY = {
|
|
2193
|
+
file_context_limit: 'This meeting holds as many files as Meeting Ask reads. Remove one in COS Control on your Mac, then retry.',
|
|
2194
|
+
file_context_storage_limit: 'COS is out of room for meeting files. Remove some in COS Control on your Mac, then retry.',
|
|
2195
|
+
file_revision_conflict: 'The meeting’s files changed while this one was read. Retry.',
|
|
2196
|
+
media_not_found: 'The upload expired before it was added. Retry.', media_expired: 'The upload expired before it was added. Retry.',
|
|
2197
|
+
media_conflict: 'The upload is still being read. Retry in a moment.', media_unavailable: 'COS could not read the upload. Retry.',
|
|
2198
|
+
maintenance: 'COS is updating. Retry in a minute.', invalid_session_id: 'Meeting Ask does not take files for this recording.',
|
|
2199
|
+
invalid_media: 'COS did not accept that file. Retry, or attach it from COS Control on your Mac.'
|
|
2200
|
+
};
|
|
2201
|
+
// The media store has it; now the meeting. A changed server (it restarted) is read again once before giving up.
|
|
2202
|
+
function attachLiveFile(sid, t, again) {
|
|
2203
|
+
t.state = 'attaching'; paintLiveFiles();
|
|
2204
|
+
return loadConfig(again).then(function (conf) {
|
|
2205
|
+
if (!conf) { var e = new Error(S.conf.notice || 'Meeting assistance is not available on this server.'); e.code = 'config'; throw e; }
|
|
2206
|
+
return answer('/api/meeting-ask/files/media', { method: 'POST', body: { sessionId: sid, serverInstanceId: conf.serverInstanceId, media: [{ id: t.mediaId, name: t.name.slice(0, 240) }] } });
|
|
2207
|
+
}).then(function (a) {
|
|
2208
|
+
var b = a.body || {}, code = typeof b.error === 'string' ? b.error : (b.error && b.error.code) || b.reason || '';
|
|
2209
|
+
if (a.status === 200 && Array.isArray(b.files)) return b.files;
|
|
2210
|
+
if (a.status === 409 && code === 'server_instance_mismatch' && !again) return attachLiveFile(sid, t, true);
|
|
2211
|
+
var err = new Error(a.status === 0 ? 'COS on your Mac did not answer. Retry when it is awake.' : a.status === 401 ? SIGNED_OUT + ' The file is kept here; sign in, then retry.' : ATTACH_COPY[code] || 'COS could not add the file to this meeting (HTTP ' + a.status + '). Retry.');
|
|
2212
|
+
err.code = 'attach'; throw err;
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
function sendLiveFile(sid, t, retry) {
|
|
2216
|
+
var u = liveUploads(sid);
|
|
2217
|
+
t.state = t.mediaId ? 'attaching' : 'reading'; t.progress = 0; t.error = '';
|
|
2218
|
+
var step = t.mediaId ? Promise.resolve({ id: t.mediaId }) : W.cosChat.uploadMedia(t, {
|
|
2219
|
+
uploading: function () { t.state = 'uploading'; paintLiveFiles(); },
|
|
2220
|
+
progress: function (p) { t.progress = p; paintUploadProgress(t); }
|
|
2221
|
+
});
|
|
2222
|
+
if (retry) paintLiveFiles();
|
|
2223
|
+
step.then(function (ref) {
|
|
2224
|
+
if (u.items.indexOf(t) < 0) return null;
|
|
2225
|
+
if (!ref || !ref.id) { var e = new Error('COS did not keep the file. Retry.'); e.code = 'attach'; throw e; }
|
|
2226
|
+
t.mediaId = ref.id;
|
|
2227
|
+
return attachLiveFile(sid, t, false);
|
|
2228
|
+
}).then(function (files) {
|
|
2229
|
+
if (!files || u.items.indexOf(t) < 0) return;
|
|
2230
|
+
u.items = u.items.filter(function (x) { return x !== t; });
|
|
2231
|
+
var a = A(sid); a.files = files;
|
|
2232
|
+
repaintAsk(sid); paintLiveFiles();
|
|
2233
|
+
}).catch(function (err) {
|
|
2234
|
+
if (u.items.indexOf(t) < 0 || (err && err.code === 'aborted')) return;
|
|
2235
|
+
t.state = 'failed';
|
|
2236
|
+
t.error = err && (err.code === 'attach' || err.code === 'config') ? err.message
|
|
2237
|
+
: err && err.status === 401 ? SIGNED_OUT + ' The file is kept here; sign in, then retry.'
|
|
2238
|
+
: W.cosChat.uploadErrorText(err);
|
|
2239
|
+
paintLiveFiles();
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
function uploadMeta(t) {
|
|
2243
|
+
if (t.state === 'reading') return t.kind === 'photo' ? 'Preparing the image' : 'Preparing';
|
|
2244
|
+
if (t.state === 'uploading') return 'Uploading ' + Math.round(t.progress * 100) + '%';
|
|
2245
|
+
if (t.state === 'attaching') return t.kind === 'photo' ? 'Reading the text in the image' : 'Adding to the meeting';
|
|
2246
|
+
return t.error || 'Did not upload';
|
|
2247
|
+
}
|
|
2248
|
+
function uploadRowHtml(t) {
|
|
2249
|
+
return '<li class="mt-up ' + t.state + '" data-up="' + esc(t.id) + '"><span class="mt-files-name">' + esc(t.name) + '</span>' +
|
|
2250
|
+
'<span class="mt-up-meta ' + (t.state === 'failed' ? 'mt-amber' : 'mt-muted') + '" data-up-meta>' + esc(uploadMeta(t)) + '</span>' +
|
|
2251
|
+
'<span class="mt-up-acts">' + (t.state === 'failed' ? '<button type="button" class="mt-link" data-mt="up-retry" data-up="' + esc(t.id) + '">Retry</button>' : '') +
|
|
2252
|
+
'<button type="button" class="mt-link" data-mt="up-remove" data-up="' + esc(t.id) + '" aria-label="Remove ' + esc(t.name) + '">' + (t.state === 'failed' ? 'Remove' : 'Cancel') + '</button></span>' +
|
|
2253
|
+
(t.state === 'uploading' || t.state === 'reading' ? '<span class="mt-up-bar" data-up-bar style="width:' + Math.round(t.progress * 100) + '%"></span>' : '') + '</li>';
|
|
2254
|
+
}
|
|
2255
|
+
// Progress moves the bar and its words only, so a tap on Cancel never misses.
|
|
2256
|
+
function paintUploadProgress(t) {
|
|
2257
|
+
var row = pane.querySelector('[data-up="' + t.id + '"]'); if (!row) return;
|
|
2258
|
+
var meta = row.querySelector('[data-up-meta]'); if (meta) meta.textContent = uploadMeta(t);
|
|
2259
|
+
var bar = row.querySelector('[data-up-bar]'); if (bar) bar.style.width = Math.round(t.progress * 100) + '%';
|
|
2260
|
+
}
|
|
2261
|
+
function paintLiveFiles() {
|
|
2262
|
+
var el = $('mt-lfiles'); if (!el || S.view !== 'live') return;
|
|
2263
|
+
var f = S.feeds[S.liveSid]; if (!f) return;
|
|
2264
|
+
var u = liveUploads(f.sid);
|
|
2265
|
+
var html = filesRowHtml(f.sid, { on: canAddFiles(f), items: u.items, note: u.note });
|
|
2266
|
+
if (html !== S.filesHtml) { S.filesHtml = html; el.innerHTML = html; }
|
|
2267
|
+
}
|
|
2268
|
+
function liveNote(text) { LU().note = text; if (S.liveSid) A(S.liveSid).filesOpen = true; paintLiveFiles(); }
|
|
2269
|
+
function findUpload(id) { var u = LU(); for (var i = 0; i < u.items.length; i++) if (u.items[i].id === id) return u.items[i]; return null; }
|
|
2270
|
+
function onLivePaste(e) {
|
|
2271
|
+
if (S.view !== 'live') return;
|
|
2272
|
+
var dt = e.clipboardData;
|
|
2273
|
+
if (!carriesFile(dt)) return;
|
|
2274
|
+
// Taken as the file, never as its name or path in a text box.
|
|
2275
|
+
e.preventDefault();
|
|
2276
|
+
var files = filesOf(dt);
|
|
2277
|
+
if (files.length) addLiveFiles(files);
|
|
2278
|
+
else liveNote('This browser did not hand over the file. Use Add file to choose it.');
|
|
2279
|
+
}
|
|
2280
|
+
function onLiveDrag(e) {
|
|
2281
|
+
if (S.view !== 'live') return;
|
|
2282
|
+
var dt = e.dataTransfer;
|
|
2283
|
+
if (!carriesFile(dt)) return;
|
|
2284
|
+
// Every file drag is ours: the browser must not open the file, or drop its path into the Ask box.
|
|
2285
|
+
e.preventDefault();
|
|
2286
|
+
var main = $('mt-lmain');
|
|
2287
|
+
if (e.type === 'dragenter' || e.type === 'dragover') { if (dt) dt.dropEffect = 'copy'; if (main) main.classList.add('dropping'); S.dragAt = Date.now(); return; }
|
|
2288
|
+
if (e.type === 'dragleave') { if (main && (!e.relatedTarget || !pane.contains(e.relatedTarget))) main.classList.remove('dropping'); return; }
|
|
2289
|
+
if (main) main.classList.remove('dropping');
|
|
2290
|
+
var files = filesOf(dt);
|
|
2291
|
+
if (files.length) addLiveFiles(files);
|
|
2292
|
+
else liveNote('This browser did not hand over the file. Use Add file to choose it.');
|
|
2011
2293
|
}
|
|
2012
2294
|
function paintTurns(force) {
|
|
2013
2295
|
var el = $('mt-turns'); if (!el) return;
|
|
@@ -2017,7 +2299,8 @@
|
|
|
2017
2299
|
S.turnsDirty = false;
|
|
2018
2300
|
var f = S.feeds[S.liveSid]; if (!f) return;
|
|
2019
2301
|
var hours = f.turns.some(function (t) { return t.elapsedMs >= 3600000; });
|
|
2020
|
-
// Reading (not following): hold
|
|
2302
|
+
// Reading (not following): hold the first line on screen still while turns above it are refined. A new turn lands
|
|
2303
|
+
// below the reader, inside the box, so it moves nothing and needs no scroll at all.
|
|
2021
2304
|
var anchor = S.follow ? null : turnsAnchor(el);
|
|
2022
2305
|
if (!f.turns.length) {
|
|
2023
2306
|
el.innerHTML = '<p class="mt-lempty">' + esc(f.endedAt ? 'No words were transcribed before the recording ended.' : 'Waiting for the first words. Text appears a few seconds after it is spoken.') + '</p>';
|
|
@@ -2038,45 +2321,52 @@
|
|
|
2038
2321
|
prev = node;
|
|
2039
2322
|
});
|
|
2040
2323
|
}
|
|
2041
|
-
if (S.follow)
|
|
2324
|
+
if (S.follow) pinBottom(el); else holdAnchor(el, anchor);
|
|
2325
|
+
paintJump();
|
|
2042
2326
|
repaintAskEnabled(f.sid);
|
|
2043
2327
|
}
|
|
2044
2328
|
function turnInner(t, hours) {
|
|
2045
2329
|
return '<div class="mt-lwho"><span class="mt-lname">' + esc(t.speaker) + '</span><span class="mt-lstamp">' + esc(stampMs(t.elapsedMs, hours)) + '</span></div>' +
|
|
2046
2330
|
t.text.split(/\n{2,}/).map(function (p) { return '<p>' + esc(p) + '</p>'; }).join('');
|
|
2047
2331
|
}
|
|
2048
|
-
// iOS Safari has no scroll anchoring
|
|
2049
|
-
//
|
|
2050
|
-
function turnsAnchor(
|
|
2051
|
-
var kids =
|
|
2332
|
+
// iOS Safari has no scroll anchoring, and Chrome's would fight this one (the box sets overflow-anchor: none), so the
|
|
2333
|
+
// anchor is ours on every browser: the first turn still showing in the box, and its distance from the box's top.
|
|
2334
|
+
function turnsAnchor(box) {
|
|
2335
|
+
var kids = box.children, top = box.scrollTop;
|
|
2052
2336
|
for (var i = 0; i < kids.length; i++) {
|
|
2053
|
-
var
|
|
2054
|
-
if (
|
|
2337
|
+
var n = kids[i];
|
|
2338
|
+
if (n.offsetTop + n.offsetHeight > top) return { node: n, off: n.offsetTop - top };
|
|
2055
2339
|
}
|
|
2056
|
-
return
|
|
2340
|
+
return null;
|
|
2057
2341
|
}
|
|
2058
|
-
|
|
2342
|
+
// Moves the box only when a turn above the reader changed height; a write to scrollTop during an iOS fling stops it.
|
|
2343
|
+
function holdAnchor(box, a) {
|
|
2059
2344
|
if (!a || !a.node.isConnected) return;
|
|
2060
|
-
var
|
|
2061
|
-
|
|
2062
|
-
if (Math.abs(dy) >= 1) { S.autoScroll = Date.now(); window.scrollBy(0, dy); }
|
|
2345
|
+
var dy = (a.node.offsetTop - box.scrollTop) - a.off;
|
|
2346
|
+
if (Math.abs(dy) >= 1) box.scrollTop += dy;
|
|
2063
2347
|
}
|
|
2348
|
+
function atBottom(box) { return box.scrollHeight - box.scrollTop - box.clientHeight <= 40; }
|
|
2349
|
+
function pinBottom(box) { var end = box.scrollHeight - box.clientHeight; if (end - box.scrollTop > 0.5) box.scrollTop = end; }
|
|
2064
2350
|
// A question needs words: the panel's buttons follow the transcript's first turn.
|
|
2065
2351
|
function repaintAskEnabled(sid) {
|
|
2066
2352
|
var a = A(sid), en = askEnabled(sid);
|
|
2067
2353
|
if (a.lastEnabled !== en) { a.lastEnabled = en; repaintAsk(sid); }
|
|
2068
2354
|
}
|
|
2069
2355
|
function jumpToLive() {
|
|
2070
|
-
var el = $('mt-turns')
|
|
2071
|
-
if (
|
|
2072
|
-
S.follow = true;
|
|
2356
|
+
var el = $('mt-turns');
|
|
2357
|
+
if (el) pinBottom(el);
|
|
2358
|
+
S.follow = true; paintJump();
|
|
2359
|
+
}
|
|
2360
|
+
// Jump to live floats over the box's bottom edge: showing it never moves a line.
|
|
2361
|
+
function paintJump() {
|
|
2362
|
+
var b = $('mt-jump'), f = S.feeds[S.liveSid];
|
|
2363
|
+
if (b) b.hidden = S.follow || !(f && f.turns.length);
|
|
2073
2364
|
}
|
|
2074
2365
|
function paintLiveFoot() {
|
|
2075
2366
|
var el = $('mt-lfoot'); if (!el || S.view !== 'live') return;
|
|
2076
2367
|
var f = S.feeds[S.liveSid], has = !!(f && f.turns.length);
|
|
2077
2368
|
var html = '<div class="mt-bar"><button type="button" class="cx-btn primary" data-mt="live-copy"' + (has ? '' : ' disabled') + '>Copy transcript</button>' +
|
|
2078
|
-
'<button type="button" class="cx-btn quiet" data-mt="live-context">Copy full context</button>' +
|
|
2079
|
-
(!S.follow && has ? '<button type="button" class="cx-btn quiet" data-mt="jump">Jump to live</button>' : '') + '</div>' +
|
|
2369
|
+
'<button type="button" class="cx-btn quiet" data-mt="live-context">Copy full context</button></div>' +
|
|
2080
2370
|
(S.liveCopy ? '<p class="mt-copynote">' + esc(S.liveCopy) + '</p>' : S.liveReason ? '<p class="mt-copynote mt-amber">' + esc('Live transcript paused · ' + S.liveReason.replace(/_/g, ' ')) + '</p>' : '');
|
|
2081
2371
|
if (html !== S.footHtml) { S.footHtml = html; el.innerHTML = html; }
|
|
2082
2372
|
}
|
|
@@ -2098,14 +2388,12 @@
|
|
|
2098
2388
|
return askOpen(f.sid, true).then(function () { return loadCues(f.sid); }).then(function () { done('Ready. Tap Copy full context again.'); });
|
|
2099
2389
|
});
|
|
2100
2390
|
}
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
if (!
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
var b = last.getBoundingClientRect().bottom, atEnd = b <= window.innerHeight + 40 && b >= 0;
|
|
2108
|
-
if (atEnd !== S.follow) { S.follow = atEnd; paintLiveFoot(); if (atEnd && S.turnsDirty) paintTurns(false); }
|
|
2391
|
+
// Following means the end of the transcript shows in its box. Scrolled up is reading: a new line must not pull the
|
|
2392
|
+
// reader down. The page's own scroll is never touched.
|
|
2393
|
+
function onBoxScroll(e) {
|
|
2394
|
+
if (S.view !== 'live' || !e.target || e.target.id !== 'mt-turns') return;
|
|
2395
|
+
var end = atBottom(e.target);
|
|
2396
|
+
if (end !== S.follow) { S.follow = end; paintJump(); if (end && S.turnsDirty) paintTurns(false); }
|
|
2109
2397
|
}
|
|
2110
2398
|
|
|
2111
2399
|
// ── list shell ────────────────────────────────────────
|
|
@@ -2116,7 +2404,7 @@
|
|
|
2116
2404
|
'<div id="mt-cal"></div><div id="mt-toolbar"></div><div id="mt-list"></div></div>';
|
|
2117
2405
|
paintHero(); paintOrphans(); paintLive(); paintCal(); paintToolbar(); paintList();
|
|
2118
2406
|
}
|
|
2119
|
-
function stopTimers() { clearInterval(S.listTimer); clearInterval(S.liveTimer); S.listTimer = null; S.liveTimer = null; }
|
|
2407
|
+
function stopTimers() { clearInterval(S.listTimer); clearInterval(S.liveTimer); clearInterval(S.clockTimer); S.listTimer = null; S.liveTimer = null; S.clockTimer = null; }
|
|
2120
2408
|
|
|
2121
2409
|
// ── events ────────────────────────────────────────────
|
|
2122
2410
|
function exchangeOf(sid, req) { return A(sid).history.filter(function (x) { return x.requestId === req; })[0]; }
|
|
@@ -2165,7 +2453,10 @@
|
|
|
2165
2453
|
return;
|
|
2166
2454
|
}
|
|
2167
2455
|
case 'ask-refresh': loadConfig(true).then(function () { panelOpen(sid); }); return;
|
|
2168
|
-
case 'files-toggle': a = A(sid); a.filesOpen = !a.filesOpen; if (S.view === 'live' && S.liveSid === sid)
|
|
2456
|
+
case 'files-toggle': a = A(sid); a.filesOpen = !a.filesOpen; if (S.view === 'live' && S.liveSid === sid) paintLiveFiles(); else repaintAsk(sid); return;
|
|
2457
|
+
case 'add-file': { var pick = $('mt-lpick'); if (pick) pick.click(); return; }
|
|
2458
|
+
case 'up-retry': { var ru = findUpload(t.getAttribute('data-up')); if (ru && ru.state === 'failed' && S.liveSid) sendLiveFile(S.liveSid, ru, true); return; }
|
|
2459
|
+
case 'up-remove': { var xu = findUpload(t.getAttribute('data-up')); if (xu) { abortUpload(xu); LU().items = LU().items.filter(function (q) { return q !== xu; }); paintLiveFiles(); } return; }
|
|
2169
2460
|
case 'ask-act': submit(sid, t.getAttribute('data-action'), t.getAttribute('data-q')); return;
|
|
2170
2461
|
case 'ask-retry': ex = exchangeOf(sid, req); if (ex) submit(sid, ex.action, ex.question, ex.requestId); return;
|
|
2171
2462
|
case 'ask-stop': cancelAnswer(sid, req); return;
|
|
@@ -2216,6 +2507,7 @@
|
|
|
2216
2507
|
if (sid && A(sid).panelDirty) setTimeout(function () { repaintAsk(sid); }, 0);
|
|
2217
2508
|
});
|
|
2218
2509
|
pane.addEventListener('change', function (e) {
|
|
2510
|
+
if (e.target.id === 'mt-lpick') { addLiveFiles(e.target.files); e.target.value = ''; return; }
|
|
2219
2511
|
if (e.target.id === 'mt-recency') { S.recency = e.target.value; paintBrowse(); }
|
|
2220
2512
|
else if (e.target.id === 'mt-dom') { S.domain = e.target.value; if (queryActive()) runSearch(); else paintBrowse(); }
|
|
2221
2513
|
else if (e.target.hasAttribute && e.target.hasAttribute('data-mt-model')) setModel(e.target.value);
|
|
@@ -2233,7 +2525,13 @@
|
|
|
2233
2525
|
var sid = t.getAttribute('data-sid'); submit(sid, 'ask', A(sid).draft);
|
|
2234
2526
|
}
|
|
2235
2527
|
});
|
|
2236
|
-
|
|
2528
|
+
// scroll does not bubble: the transcript box's own scroll is caught on the way down.
|
|
2529
|
+
pane.addEventListener('scroll', onBoxScroll, true);
|
|
2530
|
+
pane.addEventListener('paste', onLivePaste);
|
|
2531
|
+
// The strip is drawn at the box's own pixel size and in the theme's colours: draw again when either changes.
|
|
2532
|
+
window.addEventListener('resize', function () { if (S.view === 'live') drawMeter(); });
|
|
2533
|
+
if (typeof MutationObserver === 'function') new MutationObserver(function () { if (S.view === 'live') drawMeter(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
2534
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function (type) { pane.addEventListener(type, onLiveDrag); });
|
|
2237
2535
|
document.addEventListener('visibilitychange', function () {
|
|
2238
2536
|
if (document.hidden) return;
|
|
2239
2537
|
if (S.out) return;
|
|
@@ -2242,6 +2540,12 @@
|
|
|
2242
2540
|
});
|
|
2243
2541
|
window.addEventListener('focus', refreshShownAsk);
|
|
2244
2542
|
|
|
2543
|
+
// Test hooks (server/web-control/live-meeting-view.test.ts drives the real pane through these and through its events).
|
|
2544
|
+
W.meetingsLive = {
|
|
2545
|
+
_state: S, _elapsedLabel: elapsedLabel, _cleanLevels: cleanLevels, _visibleLevels: visibleLevels, _levelSilent: levelSilent,
|
|
2546
|
+
_carriesFile: carriesFile, _filesOf: filesOf, _addLiveFiles: addLiveFiles, _drawMeter: drawMeter, _tickClock: tickClock,
|
|
2547
|
+
_livePoll: livePoll, _feed: function (sid) { return S.feeds[sid]; }, _uploads: function () { return LU(); }
|
|
2548
|
+
};
|
|
2245
2549
|
W.registerPane('meetings', {
|
|
2246
2550
|
load: function (args) {
|
|
2247
2551
|
args = args || [];
|