@sphoro/bugcatch-web-sdk 0.14.2 → 0.14.4
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/dist/bugcatch.es.js +1297 -1233
- package/dist/bugcatch.es.js.map +1 -1
- package/dist/bugcatch.umd.js +124 -82
- package/dist/bugcatch.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/widget.js +103 -5
package/package.json
CHANGED
package/src/widget.js
CHANGED
|
@@ -623,6 +623,36 @@ button.primary .spinner { border-color: rgba(255,255,255,.4); border-top-color:
|
|
|
623
623
|
.att .file .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
624
624
|
.att .file .sz { flex: none; color: var(--bc-faint); font-size: 11px; }
|
|
625
625
|
|
|
626
|
+
/* The assistant's live card, above the conversation. */
|
|
627
|
+
.assistant { margin: 8px 12px 4px; padding: 10px 12px; border-radius: 12px; border: 1px solid var(--bc-border); background: var(--bc-subtle); }
|
|
628
|
+
.assistant.analyzing, .assistant.confirming, .assistant.needs_info { border-color: color-mix(in srgb, var(--bc-accent) 45%, transparent); }
|
|
629
|
+
.as-top { display: flex; gap: 10px; align-items: flex-start; }
|
|
630
|
+
.as-orb { flex: none; width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; color: #fff; font-size: 12px; background: var(--bc-accent); }
|
|
631
|
+
.as-spin { width: 14px; height: 14px; border-radius: 50%; border: 2px solid rgba(255,255,255,.4); border-top-color: #fff; animation: as-rot .8s linear infinite; }
|
|
632
|
+
@keyframes as-rot { to { transform: rotate(360deg); } }
|
|
633
|
+
.as-title { font-weight: 600; font-size: 14px; }
|
|
634
|
+
.assistant.analyzing .as-title::after { content: ''; animation: as-dots 1.4s steps(4) infinite; }
|
|
635
|
+
@keyframes as-dots { 0% { content: ''; } 25% { content: '.'; } 50% { content: '..'; } 75% { content: '...'; } }
|
|
636
|
+
.as-detail { font-size: 12.5px; color: var(--bc-muted); margin-top: 2px; }
|
|
637
|
+
.as-steps { list-style: none; display: flex; gap: 6px; margin: 10px 0 0; padding: 0; flex-wrap: wrap; font-size: 11.5px; color: var(--bc-muted); }
|
|
638
|
+
.as-steps li { display: flex; align-items: center; gap: 4px; }
|
|
639
|
+
.as-steps li span { width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid currentColor; }
|
|
640
|
+
.as-steps li.done { color: var(--bc-accent); }
|
|
641
|
+
.as-steps li.done span { background: var(--bc-accent); border-color: var(--bc-accent); }
|
|
642
|
+
.as-steps li.active { color: var(--bc-fg); font-weight: 600; }
|
|
643
|
+
.as-steps li.active span { border-color: var(--bc-accent); animation: as-pulse 1s ease-in-out infinite; }
|
|
644
|
+
@keyframes as-pulse { 50% { box-shadow: 0 0 0 4px color-mix(in srgb, var(--bc-accent) 25%, transparent); } }
|
|
645
|
+
@media (prefers-reduced-motion: reduce) { .as-spin, .as-steps li.active span, .assistant.analyzing .as-title::after { animation: none; } }
|
|
646
|
+
|
|
647
|
+
/* Quick answers under the confirm question — only on the latest message. */
|
|
648
|
+
.msg-row .quick { display: none; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
|
|
649
|
+
.msg-row:last-child .quick { display: flex; }
|
|
650
|
+
.quick button {
|
|
651
|
+
font: inherit; font-size: 13px; padding: 6px 12px; border-radius: 999px; cursor: pointer;
|
|
652
|
+
border: 1px solid var(--bc-accent); background: transparent; color: var(--bc-accent);
|
|
653
|
+
}
|
|
654
|
+
.quick button:hover { background: color-mix(in srgb, var(--bc-accent) 12%, transparent); }
|
|
655
|
+
|
|
626
656
|
/* ---------- the composer ---------- */
|
|
627
657
|
.composer {
|
|
628
658
|
position: sticky; bottom: 0; margin-top: auto;
|
|
@@ -2042,6 +2072,18 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
2042
2072
|
* sentence somebody started reading is the reason they open it again.
|
|
2043
2073
|
*/
|
|
2044
2074
|
function succeed(res, { callFailed = null } = {}) {
|
|
2075
|
+
// The assistant: straight into the report's conversation, where the
|
|
2076
|
+
// investigation shows itself working ("Analyzing the issue…") and asks
|
|
2077
|
+
// what it needs. The plain "Thanks" below stays for when that view cannot
|
|
2078
|
+
// load — a report filed without a way to follow it.
|
|
2079
|
+
if (res?.id && !callFailed) {
|
|
2080
|
+
showReport(res.id, { fresh: true }).then((ok) => { if (!ok) doneScreen(res, { callFailed }); });
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
doneScreen(res, { callFailed });
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
function doneScreen(res, { callFailed = null } = {}) {
|
|
2045
2087
|
const ref = res?.id ? String(res.id).slice(0, 8) : null;
|
|
2046
2088
|
const eta = res?.estimate?.label ? String(res.estimate.label) : null;
|
|
2047
2089
|
// A press of Call that filed the report and then could not ring anybody.
|
|
@@ -2125,14 +2167,14 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
2125
2167
|
* bubbles either side of them; and there is a box at the bottom. Oldest at
|
|
2126
2168
|
* the top, because that is the direction a conversation is read.
|
|
2127
2169
|
*/
|
|
2128
|
-
async function showReport(bugId) {
|
|
2170
|
+
async function showReport(bugId, { fresh = false } = {}) {
|
|
2129
2171
|
const panel = scrim.querySelector('.panel');
|
|
2130
2172
|
panel.innerHTML = `
|
|
2131
2173
|
<div class="head">
|
|
2132
2174
|
<button class="icon-btn back" type="button" data-act="back" aria-label="Back to your reports">${ICONS.back}</button>
|
|
2133
2175
|
<div class="titles">
|
|
2134
|
-
<h2
|
|
2135
|
-
<p
|
|
2176
|
+
<h2>${fresh ? 'Looking into it' : 'Your report'}</h2>
|
|
2177
|
+
<p>${fresh ? 'Our assistant is investigating — it may ask you a question.' : 'Everything that has happened, and a way to answer.'}</p>
|
|
2136
2178
|
</div>
|
|
2137
2179
|
<button class="icon-btn" type="button" data-act="close" aria-label="Close">${ICONS.close}</button>
|
|
2138
2180
|
</div>
|
|
@@ -2150,11 +2192,12 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
2150
2192
|
|
|
2151
2193
|
const body = panel.querySelector('.history');
|
|
2152
2194
|
// The panel can be closed, or navigated away from, while this is in flight.
|
|
2153
|
-
if (!body || !panel.querySelector('[data-act="back"]')) return;
|
|
2195
|
+
if (!body || !panel.querySelector('[data-act="back"]')) return false;
|
|
2154
2196
|
|
|
2155
2197
|
if (!report) {
|
|
2198
|
+
if (fresh) return false;
|
|
2156
2199
|
body.innerHTML = '<div class="empty">Couldn\'t load that report just now.</div>';
|
|
2157
|
-
return;
|
|
2200
|
+
return false;
|
|
2158
2201
|
}
|
|
2159
2202
|
|
|
2160
2203
|
openReportId = bugId;
|
|
@@ -2183,6 +2226,41 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
2183
2226
|
scrollToLatest(thread);
|
|
2184
2227
|
mountVerification(body, report);
|
|
2185
2228
|
if (report.canReply) mountComposer(body, thread, bugId);
|
|
2229
|
+
mountAssistant(body, report, bugId);
|
|
2230
|
+
return true;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
/**
|
|
2234
|
+
* The assistant's live card: where the investigation is, and a gentle
|
|
2235
|
+
* refresh while it works. Analyzing polls every few seconds until the
|
|
2236
|
+
* outcome lands — a question, a found cause, or a hand-off — so the
|
|
2237
|
+
* reporter watches it happen rather than wondering whether anything is.
|
|
2238
|
+
*/
|
|
2239
|
+
let assistantTimer = null;
|
|
2240
|
+
function mountAssistant(body, report, bugId) {
|
|
2241
|
+
clearTimeout(assistantTimer);
|
|
2242
|
+
const a = report.assistant;
|
|
2243
|
+
body.querySelector('.assistant')?.remove();
|
|
2244
|
+
if (!a) return;
|
|
2245
|
+
const card = document.createElement('div');
|
|
2246
|
+
card.className = `assistant ${escapeHtml(a.phase)}`;
|
|
2247
|
+
card.setAttribute('role', 'status');
|
|
2248
|
+
card.setAttribute('aria-live', 'polite');
|
|
2249
|
+
card.innerHTML = `
|
|
2250
|
+
<div class="as-top">
|
|
2251
|
+
<span class="as-orb" aria-hidden="true">${a.phase === 'analyzing' ? '<span class="as-spin"></span>' : '✦'}</span>
|
|
2252
|
+
<div class="as-text">
|
|
2253
|
+
<div class="as-title">${escapeHtml(a.title)}</div>
|
|
2254
|
+
<div class="as-detail">${escapeHtml(a.detail)}</div>
|
|
2255
|
+
</div>
|
|
2256
|
+
</div>
|
|
2257
|
+
<ol class="as-steps">${(a.steps || []).map((st) => `<li class="${escapeHtml(st.state)}"><span></span>${escapeHtml(st.label)}</li>`).join('')}</ol>`;
|
|
2258
|
+
body.querySelector('.report-head')?.after(card);
|
|
2259
|
+
if (a.phase === 'analyzing') {
|
|
2260
|
+
assistantTimer = setTimeout(() => refreshThread(bugId), 3000);
|
|
2261
|
+
} else if (a.phase === 'needs_info') {
|
|
2262
|
+
body.querySelector('.composer textarea')?.focus();
|
|
2263
|
+
}
|
|
2186
2264
|
}
|
|
2187
2265
|
|
|
2188
2266
|
/**
|
|
@@ -2220,6 +2298,7 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
2220
2298
|
if (!report || openReportId !== bugId) return;
|
|
2221
2299
|
|
|
2222
2300
|
mountVerification(body, report);
|
|
2301
|
+
mountAssistant(body, report, bugId);
|
|
2223
2302
|
const pinned = atLatest(thread);
|
|
2224
2303
|
thread.innerHTML = threadHtml(report.timeline);
|
|
2225
2304
|
// Only follow the conversation down if they were already at the bottom.
|
|
@@ -2398,6 +2477,14 @@ function openPanel({ root, host, dock, client, onClose }) {
|
|
|
2398
2477
|
|
|
2399
2478
|
sendBtn.addEventListener('click', send);
|
|
2400
2479
|
|
|
2480
|
+
// The confirm question's quick answers: the same as typing and sending.
|
|
2481
|
+
thread.addEventListener('click', (e) => {
|
|
2482
|
+
const quick = e.target.closest?.('[data-quick]');
|
|
2483
|
+
if (!quick || sending) return;
|
|
2484
|
+
input.value = quick.dataset.quick;
|
|
2485
|
+
send();
|
|
2486
|
+
});
|
|
2487
|
+
|
|
2401
2488
|
async function send() {
|
|
2402
2489
|
const text = input.value.trim();
|
|
2403
2490
|
const ready = files.filter((f) => f.storageKey);
|
|
@@ -2621,10 +2708,21 @@ function messageHtml(message, state = '') {
|
|
|
2621
2708
|
if (automated) who = '<span class="msg-who auto">Automatic reply</span>';
|
|
2622
2709
|
else if (!mine && message.author) who = `<span class="msg-who">${escapeHtml(message.author)}</span>`;
|
|
2623
2710
|
|
|
2711
|
+
// "Is this the problem you are facing?" — the confirm question the
|
|
2712
|
+
// investigation asks (bugcatch-workers check-loop). One tap answers it; the
|
|
2713
|
+
// buttons only show on the latest message, so an answered question has none.
|
|
2714
|
+
const confirm = automated && /reply "yes" or "no"/i.test(body)
|
|
2715
|
+
? `<div class="quick" role="group" aria-label="Answer">
|
|
2716
|
+
<button type="button" data-quick="Yes, this is the problem">Yes, that's it</button>
|
|
2717
|
+
<button type="button" data-quick="No, that is not the problem">No, it's something else</button>
|
|
2718
|
+
</div>`
|
|
2719
|
+
: '';
|
|
2720
|
+
|
|
2624
2721
|
return `
|
|
2625
2722
|
<div class="msg-row ${mine ? 'mine' : 'theirs'} ${automated ? 'auto' : ''} ${state}">
|
|
2626
2723
|
${who}
|
|
2627
2724
|
<div class="bubble ${body ? '' : 'silent'}">${text}${files}</div>
|
|
2725
|
+
${confirm}
|
|
2628
2726
|
<span class="msg-when">${escapeHtml(relativeDay(message.at))}</span>
|
|
2629
2727
|
</div>`;
|
|
2630
2728
|
}
|