@sphoro/bugcatch-web-sdk 0.15.0 → 0.16.0
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/README.md +1 -1
- package/dist/bugcatch.es.js +1849 -1640
- package/dist/bugcatch.es.js.map +1 -1
- package/dist/bugcatch.umd.js +140 -117
- package/dist/bugcatch.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/index.js +83 -8
- package/src/references.js +155 -3
- package/src/updates.js +36 -6
- package/src/widget.js +207 -48
package/src/widget.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { draftKey, draftOperation, readDraft } from './drafts.js';
|
|
15
15
|
import { readSession } from './session.js';
|
|
16
16
|
import { sendMessage } from './updates.js';
|
|
17
|
-
import {
|
|
17
|
+
import { readReferences, validateReference } from './references.js';
|
|
18
18
|
|
|
19
19
|
const POSITIONS = {
|
|
20
20
|
'bottom-right': 'bottom:20px;right:20px;',
|
|
@@ -503,6 +503,14 @@ textarea { min-height: 88px; resize: vertical; }
|
|
|
503
503
|
.refs .err { color: var(--bc-danger); font-weight: 550; }
|
|
504
504
|
.refs .link { margin-top: 10px; font-size: 12px; color: var(--bc-muted); }
|
|
505
505
|
.refs .link:hover { color: var(--bc-fg); }
|
|
506
|
+
.refs .chips.cands button { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 550; }
|
|
507
|
+
/* "We read your ID" — under the description, while they type. */
|
|
508
|
+
.ref-found {
|
|
509
|
+
display: flex; align-items: center; gap: 6px;
|
|
510
|
+
margin: -6px 0 14px; font-size: 12px; line-height: 1.45; color: var(--bc-muted);
|
|
511
|
+
}
|
|
512
|
+
.ref-found svg { width: 14px; height: 14px; flex: none; color: var(--bc-ok); }
|
|
513
|
+
.ref-found b { font-weight: 600; color: var(--bc-fg); }
|
|
506
514
|
|
|
507
515
|
/* ---------- severity ---------- */
|
|
508
516
|
.seg { display: grid; grid-template-columns: repeat(4, 1fr); gap: 3px; padding: 3px; background: var(--bc-subtle); border: 1px solid var(--bc-border); border-radius: var(--bc-r-md); }
|
|
@@ -856,6 +864,10 @@ button.quiet:hover { color: var(--bc-accent); }
|
|
|
856
864
|
background: var(--bc-accent-soft); border-color: var(--bc-accent-line);
|
|
857
865
|
}
|
|
858
866
|
.assistant.resolved { background: color-mix(in srgb, var(--bc-ok) 9%, var(--bc-bg)); border-color: color-mix(in srgb, var(--bc-ok) 35%, var(--bc-border)); }
|
|
867
|
+
/* On a call: the call's own card is what they are watching. This one states
|
|
868
|
+
what is happening and stops moving — no spinner, no travelling bar. */
|
|
869
|
+
.assistant.calling { background: var(--bc-bg); }
|
|
870
|
+
.assistant.calling .as-progress i { transition: none; }
|
|
859
871
|
.as-body.entering { animation: bc-rise 360ms var(--bc-ease) both; }
|
|
860
872
|
.as-top { display: flex; gap: 11px; align-items: center; }
|
|
861
873
|
.as-orb {
|
|
@@ -1049,6 +1061,7 @@ button.quiet:hover { color: var(--bc-accent); }
|
|
|
1049
1061
|
animation: bc-rise 240ms var(--bc-ease) both;
|
|
1050
1062
|
}
|
|
1051
1063
|
.livecall .who { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1064
|
+
.livecall .call-kept { font-size: 11px; opacity: .75; }
|
|
1052
1065
|
.livecall .secs { font-variant-numeric: tabular-nums; color: var(--bc-muted); }
|
|
1053
1066
|
.livecall .act {
|
|
1054
1067
|
padding: 5px 11px; border-radius: 999px; border: 1px solid var(--bc-border);
|
|
@@ -1750,9 +1763,16 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
1750
1763
|
// answer, is a reasonable thing to be asked. Most reporters never see it: the
|
|
1751
1764
|
// loan number was in the sentence.
|
|
1752
1765
|
let refConfig = { policy: 'off', fields: [] };
|
|
1753
|
-
let
|
|
1766
|
+
let refOpen = false;
|
|
1767
|
+
let refChoice = null; // which field they are answering; null = not picked yet
|
|
1754
1768
|
let refValue = '';
|
|
1755
1769
|
let refError = '';
|
|
1770
|
+
// What we read in their text but could not place for sure, offered as taps:
|
|
1771
|
+
// [{ value, kinds: [{ key, label }] }]. `refMode` says why the card is open —
|
|
1772
|
+
// 'confirm' when the text had two answers, 'suggest' when it had an unlabelled
|
|
1773
|
+
// one, 'ask' when it had none.
|
|
1774
|
+
let refCandidates = [];
|
|
1775
|
+
let refMode = 'ask';
|
|
1756
1776
|
// Set once they have answered or explicitly skipped, so Send stops asking.
|
|
1757
1777
|
let refSettled = false;
|
|
1758
1778
|
// Set once we have said "that does not look right" about the current value.
|
|
@@ -1763,13 +1783,18 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
1763
1783
|
let refWarned = false;
|
|
1764
1784
|
|
|
1765
1785
|
client.referenceFields()
|
|
1766
|
-
.then((cfg) => { refConfig = cfg; })
|
|
1786
|
+
.then((cfg) => { refConfig = cfg; showFound(); })
|
|
1767
1787
|
.catch(() => { /* off is the safe answer to every failure */ });
|
|
1768
1788
|
|
|
1769
|
-
/** What the
|
|
1770
|
-
function
|
|
1771
|
-
if (refConfig.policy === 'off' || !refConfig.fields.length) return [];
|
|
1772
|
-
return
|
|
1789
|
+
/** What the text says: found (sure), conflicts (ask which), loose (offer). */
|
|
1790
|
+
function readText() {
|
|
1791
|
+
if (refConfig.policy === 'off' || !refConfig.fields.length) return { found: [], conflicts: [], loose: [] };
|
|
1792
|
+
return readReferences(refConfig.fields, [titleInput.value, descInput.value]);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
/** Sure enough to send without a question: something found, nothing contested. */
|
|
1796
|
+
function textAnswers(read = readText()) {
|
|
1797
|
+
return read.found.length > 0 && read.conflicts.length === 0;
|
|
1773
1798
|
}
|
|
1774
1799
|
|
|
1775
1800
|
/** The answer to send, or an empty list. */
|
|
@@ -1778,50 +1803,127 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
1778
1803
|
return [{ key: refChoice, value: refValue.trim().slice(0, 120) }];
|
|
1779
1804
|
}
|
|
1780
1805
|
|
|
1806
|
+
// A quiet line under the description while they type, so somebody who put
|
|
1807
|
+
// their loan number in the sentence can see it was read and does not go
|
|
1808
|
+
// looking for a box to type it into again.
|
|
1809
|
+
const foundNote = document.createElement('p');
|
|
1810
|
+
foundNote.className = 'ref-found';
|
|
1811
|
+
foundNote.setAttribute('aria-live', 'polite');
|
|
1812
|
+
foundNote.hidden = true;
|
|
1813
|
+
descInput.closest('.field').after(foundNote);
|
|
1814
|
+
let foundTimer;
|
|
1815
|
+
function showFound() {
|
|
1816
|
+
const read = readText();
|
|
1817
|
+
if (textAnswers(read)) {
|
|
1818
|
+
const list = read.found.slice(0, 2).map((r) => `${escapeHtml(r.label)} <b>${escapeHtml(shortId(r.value))}</b>`).join(', ');
|
|
1819
|
+
foundNote.innerHTML = `${ICONS.check}<span>Found your ${list} — no need to add it again.</span>`;
|
|
1820
|
+
foundNote.hidden = false;
|
|
1821
|
+
// They have answered in the text; a card still asking would be not listening.
|
|
1822
|
+
if (refOpen && !answeredReference().length) { refOpen = false; drawRefs(); }
|
|
1823
|
+
} else {
|
|
1824
|
+
foundNote.hidden = true;
|
|
1825
|
+
foundNote.innerHTML = '';
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
const queueFound = () => { clearTimeout(foundTimer); foundTimer = setTimeout(showFound, 250); };
|
|
1829
|
+
titleInput.addEventListener('input', queueFound);
|
|
1830
|
+
descInput.addEventListener('input', queueFound);
|
|
1831
|
+
|
|
1832
|
+
/** A UUID is long; the chip shows its ends, the title shows all of it. */
|
|
1833
|
+
function shortId(value) {
|
|
1834
|
+
return value.length > 20 ? `${value.slice(0, 8)}…${value.slice(-4)}` : value;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
function candidateFor(value) {
|
|
1838
|
+
const v = value.trim().toLowerCase();
|
|
1839
|
+
return refCandidates.find((c) => c.value.toLowerCase() === v) || null;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/** Pick a candidate: its value, and its kind when only one kind fits. */
|
|
1843
|
+
function takeCandidate(c) {
|
|
1844
|
+
refValue = c.value;
|
|
1845
|
+
if (c.kinds.length === 1) refChoice = c.kinds[0].key;
|
|
1846
|
+
else if (!c.kinds.some((k) => k.key === refChoice)) refChoice = null;
|
|
1847
|
+
refError = '';
|
|
1848
|
+
refWarned = false;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1781
1851
|
function drawRefs() {
|
|
1782
|
-
if (!
|
|
1783
|
-
const field = refConfig.fields.find((f) => f.key === refChoice) ||
|
|
1852
|
+
if (!refOpen) { refsBox.hidden = true; refsBox.innerHTML = ''; return; }
|
|
1853
|
+
const field = refConfig.fields.find((f) => f.key === refChoice) || null;
|
|
1784
1854
|
const optional = refConfig.policy !== 'required';
|
|
1855
|
+
const current = candidateFor(refValue);
|
|
1856
|
+
// Kinds worth offering: the ones the picked value could be, when we know;
|
|
1857
|
+
// every kind otherwise. Never zero — a guess of ours must not lock out the
|
|
1858
|
+
// right answer.
|
|
1859
|
+
const kinds = current && current.kinds.length > 1
|
|
1860
|
+
? refConfig.fields.filter((f) => current.kinds.some((k) => k.key === f.key))
|
|
1861
|
+
: refConfig.fields;
|
|
1862
|
+
// Two loan numbers: the kind is not in question, only which one — so the
|
|
1863
|
+
// kind chips would be a second question nobody needs to answer.
|
|
1864
|
+
const sharedKind = refCandidates.length > 1
|
|
1865
|
+
&& refCandidates.every((c) => c.kinds.length === 1 && c.kinds[0].key === refCandidates[0].kinds[0].key);
|
|
1866
|
+
|
|
1867
|
+
const head = {
|
|
1868
|
+
confirm: refCandidates.length > 1
|
|
1869
|
+
? ['Which one is this about?', 'We found more than one ID in your message. Pick the one this problem is about.']
|
|
1870
|
+
: ['Is this the right ID?', 'This could be more than one kind of ID. Tell us which it is.'],
|
|
1871
|
+
suggest: ['Is this your ID?', 'We spotted this in your message. Tell us what it is so we can find your record.'],
|
|
1872
|
+
ask: optional
|
|
1873
|
+
? ['One more thing', 'So we can find your record — this helps us answer faster.']
|
|
1874
|
+
: ['We need one detail', 'We need one of these to look this up.'],
|
|
1875
|
+
}[refMode];
|
|
1785
1876
|
|
|
1786
1877
|
refsBox.hidden = false;
|
|
1787
1878
|
// A handful of kinds read best as chips to tap; a long list goes back to
|
|
1788
1879
|
// a select so the card does not turn into a wall of pills.
|
|
1789
|
-
const many =
|
|
1880
|
+
const many = kinds.length > 6;
|
|
1790
1881
|
refsBox.innerHTML = `
|
|
1791
1882
|
<div class="ask-head">
|
|
1792
1883
|
<span class="avatar" aria-hidden="true">${ICONS.sparkle}</span>
|
|
1793
1884
|
<div>
|
|
1794
|
-
<strong>${
|
|
1795
|
-
<p class="why">${escapeHtml(
|
|
1796
|
-
optional
|
|
1797
|
-
? 'So we can find your record — this helps us answer faster.'
|
|
1798
|
-
: 'We need one of these to look this up.',
|
|
1799
|
-
)}</p>
|
|
1885
|
+
<strong>${escapeHtml(head[0])}</strong>
|
|
1886
|
+
<p class="why">${escapeHtml(head[1])}</p>
|
|
1800
1887
|
</div>
|
|
1801
1888
|
</div>
|
|
1802
|
-
${
|
|
1889
|
+
${refCandidates.length > 1 ? `
|
|
1890
|
+
<div class="chips cands" role="group" aria-label="IDs found in your message">
|
|
1891
|
+
${refCandidates.map((c, i) => `
|
|
1892
|
+
<button type="button" data-ref-cand="${i}" title="${escapeHtml(c.value)}"
|
|
1893
|
+
aria-pressed="${!!current && current === c}">${escapeHtml(shortId(c.value))}</button>
|
|
1894
|
+
`).join('')}
|
|
1895
|
+
</div>` : ''}
|
|
1896
|
+
${kinds.length > 1 && !many && !sharedKind ? `
|
|
1803
1897
|
<div class="chips" role="group" aria-label="Which detail you are giving">
|
|
1804
|
-
${
|
|
1805
|
-
<button type="button" data-ref-kind="${escapeHtml(f.key)}" aria-pressed="${f.key === field.key}">${escapeHtml(f.label)}</button>
|
|
1898
|
+
${kinds.map((f) => `
|
|
1899
|
+
<button type="button" data-ref-kind="${escapeHtml(f.key)}" aria-pressed="${!!field && f.key === field.key}">${escapeHtml(f.label)}</button>
|
|
1806
1900
|
`).join('')}
|
|
1807
1901
|
</div>` : ''}
|
|
1808
1902
|
<div class="row">
|
|
1809
1903
|
${many ? `
|
|
1810
1904
|
<select class="ref-kind" aria-label="Which detail you are giving">
|
|
1811
|
-
${
|
|
1812
|
-
|
|
1905
|
+
${field ? '' : '<option value="" selected disabled>What is it?</option>'}
|
|
1906
|
+
${kinds.map((f) => `
|
|
1907
|
+
<option value="${escapeHtml(f.key)}" ${field && f.key === field.key ? 'selected' : ''}>${escapeHtml(f.label)}</option>
|
|
1813
1908
|
`).join('')}
|
|
1814
1909
|
</select>` : ''}
|
|
1815
|
-
<input class="ref-value${refError ? ' invalid' : ''}" type="${field
|
|
1816
|
-
inputmode="${escapeHtml(field
|
|
1910
|
+
<input class="ref-value${refError ? ' invalid' : ''}" type="${field?.inputMode === 'email' ? 'email' : 'text'}"
|
|
1911
|
+
inputmode="${escapeHtml(field?.inputMode === 'numeric' || field?.inputMode === 'tel' ? 'numeric' : 'text')}"
|
|
1817
1912
|
value="${escapeHtml(refValue)}"
|
|
1818
|
-
placeholder="${escapeHtml(field.placeholder || field.label)}"
|
|
1819
|
-
aria-label="${escapeHtml(field.label)}" ${refError ? 'aria-invalid="true"' : ''} />
|
|
1913
|
+
placeholder="${escapeHtml(field ? (field.placeholder || field.label) : 'Your ID')}"
|
|
1914
|
+
aria-label="${escapeHtml(field ? field.label : 'Your ID')}" ${refError ? 'aria-invalid="true"' : ''} />
|
|
1820
1915
|
</div>
|
|
1821
|
-
${field
|
|
1916
|
+
${field?.hint ? `<p class="hint">${escapeHtml(field.hint)}</p>` : ''}
|
|
1822
1917
|
${refError ? `<p class="field-err" role="alert">${ICONS.alert}<span>${escapeHtml(refError)}</span></p>` : ''}
|
|
1823
|
-
${optional ?
|
|
1918
|
+
${optional ? `<button type="button" class="link" data-act="ref-skip">${
|
|
1919
|
+
refMode === 'ask' ? 'I don\'t have this — send without it' : 'Not sure — send as it is'}</button>` : ''}`;
|
|
1824
1920
|
|
|
1921
|
+
refsBox.querySelectorAll('[data-ref-cand]').forEach((chip) => {
|
|
1922
|
+
chip.addEventListener('click', () => {
|
|
1923
|
+
takeCandidate(refCandidates[Number(chip.dataset.refCand)]);
|
|
1924
|
+
drawRefs();
|
|
1925
|
+
});
|
|
1926
|
+
});
|
|
1825
1927
|
refsBox.querySelectorAll('[data-ref-kind]').forEach((chip) => {
|
|
1826
1928
|
chip.addEventListener('click', () => {
|
|
1827
1929
|
refChoice = chip.dataset.refKind;
|
|
@@ -1851,7 +1953,9 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
1851
1953
|
refsBox.querySelector('[data-act="ref-skip"]')?.addEventListener('click', () => {
|
|
1852
1954
|
// Skipping is answering. Without this the panel asks again on the next
|
|
1853
1955
|
// press and the reporter is in a loop with a button that does nothing.
|
|
1956
|
+
// The text still goes; the server reads it either way.
|
|
1854
1957
|
refSettled = true;
|
|
1958
|
+
refOpen = false;
|
|
1855
1959
|
refChoice = null;
|
|
1856
1960
|
refValue = '';
|
|
1857
1961
|
refError = '';
|
|
@@ -1860,23 +1964,54 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
1860
1964
|
say('');
|
|
1861
1965
|
});
|
|
1862
1966
|
|
|
1863
|
-
|
|
1967
|
+
// Focus where the next decision is: the kind, when that is all that is
|
|
1968
|
+
// missing; the box otherwise.
|
|
1969
|
+
const next = !field && refValue ? refsBox.querySelector('[data-ref-kind], .ref-kind') : null;
|
|
1970
|
+
(next || value).focus();
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
/**
|
|
1974
|
+
* Open the question, pre-filled with the best of what the text offered.
|
|
1975
|
+
*
|
|
1976
|
+
* Conflicts first — the text said two things and only the reporter knows
|
|
1977
|
+
* which — then loose IDs, which are probably the answer and only need a
|
|
1978
|
+
* name. Nothing, and it is the plain question it always was.
|
|
1979
|
+
*/
|
|
1980
|
+
function openRefs(read) {
|
|
1981
|
+
refCandidates = read.conflicts.length ? read.conflicts : read.loose.slice(0, 4);
|
|
1982
|
+
refMode = read.conflicts.length ? 'confirm' : read.loose.length ? 'suggest' : 'ask';
|
|
1983
|
+
refOpen = true;
|
|
1984
|
+
if (refCandidates.length) {
|
|
1985
|
+
// One contested value: pre-fill it so they only choose what it is. Two
|
|
1986
|
+
// or more: pick nothing, because picking is the question.
|
|
1987
|
+
if (refCandidates.length === 1) takeCandidate(refCandidates[0]);
|
|
1988
|
+
else {
|
|
1989
|
+
refValue = '';
|
|
1990
|
+
const [first] = refCandidates;
|
|
1991
|
+
const same = refCandidates.every((c) => c.kinds.length === 1 && c.kinds[0].key === first.kinds[0].key);
|
|
1992
|
+
refChoice = same ? first.kinds[0].key : null;
|
|
1993
|
+
}
|
|
1994
|
+
} else {
|
|
1995
|
+
refChoice = refChoice || refConfig.fields[0].key;
|
|
1996
|
+
}
|
|
1997
|
+
drawRefs();
|
|
1864
1998
|
}
|
|
1865
1999
|
|
|
1866
2000
|
/**
|
|
1867
2001
|
* Whether Send should stop and ask.
|
|
1868
2002
|
*
|
|
1869
|
-
*
|
|
1870
|
-
*
|
|
1871
|
-
*
|
|
1872
|
-
*
|
|
1873
|
-
* question, and being asked again would be the widget not listening.
|
|
2003
|
+
* Never when the text answered it outright. Once when the text said two
|
|
2004
|
+
* things or something unlabelled — on every policy, because sending the
|
|
2005
|
+
* wrong record's id with confidence is worse than one extra tap. And the
|
|
2006
|
+
* plain question on `optional`/`required` projects as before.
|
|
1874
2007
|
*/
|
|
1875
2008
|
function needsReference() {
|
|
1876
2009
|
if (refConfig.policy === 'off' || !refConfig.fields.length) return false;
|
|
1877
|
-
if (referencesInText().length) return false;
|
|
1878
2010
|
if (answeredReference().length) return false;
|
|
1879
|
-
|
|
2011
|
+
const read = readText();
|
|
2012
|
+
if (textAnswers(read)) return false;
|
|
2013
|
+
if (refSettled && refConfig.policy !== 'required') return false;
|
|
2014
|
+
return read;
|
|
1880
2015
|
}
|
|
1881
2016
|
|
|
1882
2017
|
/* ---------- send ---------- */
|
|
@@ -1894,9 +2029,18 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
1894
2029
|
// costs one extra press on projects that want an identifier, and it is the
|
|
1895
2030
|
// only shape that does not put paperwork in front of a person who came here
|
|
1896
2031
|
// to say something is broken.
|
|
1897
|
-
|
|
1898
|
-
|
|
2032
|
+
//
|
|
2033
|
+
// First, though: a value in the box but no kind picked. Asked rather than
|
|
2034
|
+
// guessed — a wrong kind joins on the wrong table.
|
|
2035
|
+
if (refOpen && refValue.trim() && !refChoice) {
|
|
2036
|
+
refError = 'Pick what this ID is.';
|
|
1899
2037
|
drawRefs();
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
const read = needsReference();
|
|
2042
|
+
if (read) {
|
|
2043
|
+
if (!refOpen) openRefs(read); else drawRefs();
|
|
1900
2044
|
say('');
|
|
1901
2045
|
return;
|
|
1902
2046
|
}
|
|
@@ -2639,7 +2783,8 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
2639
2783
|
<div class="as-body${previous && previous !== a.phase ? ' entering' : ''}">
|
|
2640
2784
|
<div class="as-top">
|
|
2641
2785
|
<span class="as-orb" aria-hidden="true">${a.phase === 'analyzing' ? '<span class="as-spin"></span>'
|
|
2642
|
-
: a.phase === '
|
|
2786
|
+
: a.phase === 'calling' ? ICONS.phone
|
|
2787
|
+
: a.phase === 'resolved' ? ICONS.check : ICONS.sparkle}</span>
|
|
2643
2788
|
<div class="as-text">
|
|
2644
2789
|
<div class="as-title">${escapeHtml(a.title)}</div>
|
|
2645
2790
|
<div class="as-detail">${escapeHtml(a.detail)}</div>
|
|
@@ -2664,6 +2809,9 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
2664
2809
|
}
|
|
2665
2810
|
|
|
2666
2811
|
// While it works, a "thinking" bubble sits where its next message will land.
|
|
2812
|
+
// Never during a call: somebody is on the phone about this report, nothing
|
|
2813
|
+
// is being analysed (the analysis waits for the transcript), and a bubble
|
|
2814
|
+
// typing away underneath a live call is the assistant talking over them.
|
|
2667
2815
|
if (thread) {
|
|
2668
2816
|
const pinned = atLatest(thread);
|
|
2669
2817
|
const typing = thread.querySelector('.typing-row');
|
|
@@ -2677,6 +2825,10 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
2677
2825
|
|
|
2678
2826
|
if (a.phase === 'analyzing') {
|
|
2679
2827
|
assistantTimer = setTimeout(() => refreshThread(bugId), 3000);
|
|
2828
|
+
} else if (a.phase === 'calling') {
|
|
2829
|
+
// Slower: the call's own card is what the reporter is watching, and this
|
|
2830
|
+
// only has to notice when the call ends and the analysis begins.
|
|
2831
|
+
assistantTimer = setTimeout(() => refreshThread(bugId), 10000);
|
|
2680
2832
|
} else if (a.phase === 'needs_info' && previous !== 'needs_info') {
|
|
2681
2833
|
body.querySelector('.composer textarea')?.focus();
|
|
2682
2834
|
}
|
|
@@ -2753,8 +2905,9 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
2753
2905
|
<div class="staged" hidden></div>
|
|
2754
2906
|
<div class="row">
|
|
2755
2907
|
<div class="tools">
|
|
2908
|
+
${cfg.attachments === false ? '' : `
|
|
2756
2909
|
<button class="tool" type="button" data-act="attach" title="Attach a file" aria-label="Attach a file">${ICONS.paperclip}</button>
|
|
2757
|
-
<button class="tool" type="button" data-act="voice" title="Record a voice note" aria-label="Record a voice note">${ICONS.mic}</button
|
|
2910
|
+
<button class="tool" type="button" data-act="voice" title="Record a voice note" aria-label="Record a voice note">${ICONS.mic}</button>`}
|
|
2758
2911
|
<button class="tool" type="button" data-act="livecall" title="Call the team" aria-label="Call the team" hidden>${ICONS.phone}</button>
|
|
2759
2912
|
</div>
|
|
2760
2913
|
<textarea rows="1" placeholder="Write a reply…" aria-label="Write a reply"></textarea>
|
|
@@ -2834,7 +2987,11 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
2834
2987
|
}
|
|
2835
2988
|
});
|
|
2836
2989
|
|
|
2837
|
-
|
|
2990
|
+
// Absent when the host set `attachments: false` — which the report form
|
|
2991
|
+
// has always honoured and this composer did not, so a host keeping files
|
|
2992
|
+
// and microphone audio outside its compliance perimeter still had both
|
|
2993
|
+
// offered in the follow-up thread.
|
|
2994
|
+
composer.querySelector('[data-act="attach"]')?.addEventListener('click', () => picker.click());
|
|
2838
2995
|
picker.addEventListener('change', () => {
|
|
2839
2996
|
say('');
|
|
2840
2997
|
for (const file of picker.files) {
|
|
@@ -2847,12 +3004,12 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
2847
3004
|
picker.value = '';
|
|
2848
3005
|
});
|
|
2849
3006
|
|
|
2850
|
-
if (!voiceSupported()) {
|
|
3007
|
+
if (voiceBtn && !voiceSupported()) {
|
|
2851
3008
|
voiceBtn.disabled = true;
|
|
2852
3009
|
voiceBtn.title = 'This browser cannot record audio';
|
|
2853
3010
|
}
|
|
2854
3011
|
|
|
2855
|
-
voiceBtn
|
|
3012
|
+
voiceBtn?.addEventListener('click', async () => {
|
|
2856
3013
|
if (recorder) {
|
|
2857
3014
|
const handle = recorder;
|
|
2858
3015
|
recorder = null;
|
|
@@ -2971,6 +3128,7 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
2971
3128
|
<span class="call-dot" aria-hidden="true"></span>
|
|
2972
3129
|
<span class="who" role="status" aria-live="polite">Calling the team…</span>
|
|
2973
3130
|
<span class="secs" hidden></span>
|
|
3131
|
+
${handle?.recorded ? '<span class="call-kept">This call is recorded.</span>' : ''}
|
|
2974
3132
|
<button class="act" type="button" data-act="mute" hidden>Mute</button>
|
|
2975
3133
|
<button class="act danger" type="button" data-act="hangup">Cancel</button>`;
|
|
2976
3134
|
body.insertBefore(bar, thread);
|
|
@@ -3167,13 +3325,14 @@ function messageHtml(message, state = '') {
|
|
|
3167
3325
|
}
|
|
3168
3326
|
if (state.includes('cont')) who = '';
|
|
3169
3327
|
|
|
3170
|
-
//
|
|
3171
|
-
//
|
|
3172
|
-
//
|
|
3173
|
-
|
|
3328
|
+
// The assistant's one answer ends by inviting a correction (check-rules
|
|
3329
|
+
// confirmMessage). One tap sends it, and what comes back goes to the person
|
|
3330
|
+
// who now owns the report rather than to another automated round — so the
|
|
3331
|
+
// wording is "that's right" and "that's not it", not a yes/no question.
|
|
3332
|
+
const confirm = automated && /it goes to the same person/i.test(body)
|
|
3174
3333
|
? `<div class="quick" role="group" aria-label="Answer">
|
|
3175
|
-
<button type="button" data-quick="Yes, this is the problem">${ICONS.check}
|
|
3176
|
-
<button type="button" data-quick="No, that is not the problem">
|
|
3334
|
+
<button type="button" data-quick="Yes, this is the problem">${ICONS.check}That's right</button>
|
|
3335
|
+
<button type="button" data-quick="No, that is not the problem">That's not it</button>
|
|
3177
3336
|
</div>`
|
|
3178
3337
|
: '';
|
|
3179
3338
|
|