@sphoro/bugcatch-web-sdk 0.14.3 → 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.3",
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,27 @@ 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
+
626
647
  /* Quick answers under the confirm question — only on the latest message. */
627
648
  .msg-row .quick { display: none; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
628
649
  .msg-row:last-child .quick { display: flex; }
@@ -2051,6 +2072,18 @@ function openPanel({ root, host, dock, client, onClose }) {
2051
2072
  * sentence somebody started reading is the reason they open it again.
2052
2073
  */
2053
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 } = {}) {
2054
2087
  const ref = res?.id ? String(res.id).slice(0, 8) : null;
2055
2088
  const eta = res?.estimate?.label ? String(res.estimate.label) : null;
2056
2089
  // A press of Call that filed the report and then could not ring anybody.
@@ -2134,14 +2167,14 @@ function openPanel({ root, host, dock, client, onClose }) {
2134
2167
  * bubbles either side of them; and there is a box at the bottom. Oldest at
2135
2168
  * the top, because that is the direction a conversation is read.
2136
2169
  */
2137
- async function showReport(bugId) {
2170
+ async function showReport(bugId, { fresh = false } = {}) {
2138
2171
  const panel = scrim.querySelector('.panel');
2139
2172
  panel.innerHTML = `
2140
2173
  <div class="head">
2141
2174
  <button class="icon-btn back" type="button" data-act="back" aria-label="Back to your reports">${ICONS.back}</button>
2142
2175
  <div class="titles">
2143
- <h2>Your report</h2>
2144
- <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>
2145
2178
  </div>
2146
2179
  <button class="icon-btn" type="button" data-act="close" aria-label="Close">${ICONS.close}</button>
2147
2180
  </div>
@@ -2159,11 +2192,12 @@ function openPanel({ root, host, dock, client, onClose }) {
2159
2192
 
2160
2193
  const body = panel.querySelector('.history');
2161
2194
  // The panel can be closed, or navigated away from, while this is in flight.
2162
- if (!body || !panel.querySelector('[data-act="back"]')) return;
2195
+ if (!body || !panel.querySelector('[data-act="back"]')) return false;
2163
2196
 
2164
2197
  if (!report) {
2198
+ if (fresh) return false;
2165
2199
  body.innerHTML = '<div class="empty">Couldn\'t load that report just now.</div>';
2166
- return;
2200
+ return false;
2167
2201
  }
2168
2202
 
2169
2203
  openReportId = bugId;
@@ -2192,6 +2226,41 @@ function openPanel({ root, host, dock, client, onClose }) {
2192
2226
  scrollToLatest(thread);
2193
2227
  mountVerification(body, report);
2194
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
+ }
2195
2264
  }
2196
2265
 
2197
2266
  /**
@@ -2229,6 +2298,7 @@ function openPanel({ root, host, dock, client, onClose }) {
2229
2298
  if (!report || openReportId !== bugId) return;
2230
2299
 
2231
2300
  mountVerification(body, report);
2301
+ mountAssistant(body, report, bugId);
2232
2302
  const pinned = atLatest(thread);
2233
2303
  thread.innerHTML = threadHtml(report.timeline);
2234
2304
  // Only follow the conversation down if they were already at the bottom.