@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sphoro/bugcatch-web-sdk",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "description": "Drop-in browser SDK — report bugs with console, network and environment context, without the extension.",
5
5
  "type": "module",
6
6
  "main": "dist/bugcatch.es.js",
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>Your report</h2>
2135
- <p>Everything that has happened, and a way to answer.</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
  }