@sphoro/bugcatch-web-sdk 0.19.0 → 0.20.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 +16 -1
- package/dist/bugcatch-call.umd.js +1 -1
- package/dist/bugcatch-core.es.js +1 -1
- package/dist/bugcatch-panel.es.js +1057 -916
- package/dist/bugcatch-panel.es.js.map +1 -1
- package/dist/bugcatch-panel.umd.js +192 -151
- package/dist/bugcatch-panel.umd.js.map +1 -1
- package/dist/bugcatch.standalone.umd.js +178 -137
- package/dist/bugcatch.standalone.umd.js.map +1 -1
- package/dist/bugcatch.umd.js +2 -2
- package/package.json +1 -1
- package/src/index.d.ts +9 -0
- package/src/panel/call.js +3 -2
- package/src/panel/compose.js +7 -3
- package/src/panel/index.js +9 -0
- package/src/panel/rate-due.js +155 -0
- package/src/panel/rating.js +87 -0
- package/src/panel/signin.js +1 -1
- package/src/panel/styles.js +28 -0
- package/src/panel/thread.js +3 -72
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// "How did we do?" — five stars and, after them, an optional note. Drawn under
|
|
2
|
+
// a settled report's conversation (thread.js) and above the form on the first
|
|
3
|
+
// screen (rate-due.js), so both ask the same question the same way.
|
|
4
|
+
|
|
5
|
+
import * as ICONS from './icons.js';
|
|
6
|
+
import { escapeHtml } from './render.js';
|
|
7
|
+
|
|
8
|
+
/** What a drawn rating box was drawn from, so a refresh can tell if it moved. */
|
|
9
|
+
export function ratingKey(rating) {
|
|
10
|
+
return JSON.stringify(rating ? [rating.score, rating.current, rating.comment || null] : null);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Draw the stars into `box` for `report`, with the reader's `rating` so far.
|
|
15
|
+
*
|
|
16
|
+
* One tap sends the score; a note is offered after it rather than before,
|
|
17
|
+
* because a form that asks for a sentence first is a form people close, and
|
|
18
|
+
* the score is the part that counts. `onRated` hears each saved rating and
|
|
19
|
+
* `onError` each refusal, for a caller that holds something until it lands.
|
|
20
|
+
*/
|
|
21
|
+
export function drawRating(box, {
|
|
22
|
+
client, report, rating, justRated = false, question = null, onRated = null, onError = null,
|
|
23
|
+
}) {
|
|
24
|
+
box.dataset.key = ratingKey(rating);
|
|
25
|
+
// A rating given for an earlier fix stands, but it is not this fix's: the
|
|
26
|
+
// stars start empty and the sentence says what the old score was.
|
|
27
|
+
const shown = rating?.current ? rating.score : 0;
|
|
28
|
+
const asked = !rating
|
|
29
|
+
? (question || 'How did we do?')
|
|
30
|
+
: !rating.current
|
|
31
|
+
? `You rated an earlier fix ${rating.score} out of 5. How did this one go?`
|
|
32
|
+
: justRated
|
|
33
|
+
? 'Thanks — the team will see that.'
|
|
34
|
+
: `You rated this ${rating.score} out of 5.`;
|
|
35
|
+
const askNote = justRated && rating?.current && !rating.comment;
|
|
36
|
+
box.innerHTML = `
|
|
37
|
+
<p class="fr-q">${escapeHtml(asked)}</p>
|
|
38
|
+
<div class="stars" role="group" aria-label="Rate how this was handled, 1 to 5">${[1, 2, 3, 4, 5].map((n) => `
|
|
39
|
+
<button type="button" data-score="${n}" aria-pressed="${n === shown}" aria-label="${n} out of 5${n === 1 ? ', poor' : n === 5 ? ', great' : ''}" class="${n <= shown ? 'on' : ''}">${ICONS.star}</button>`).join('')}
|
|
40
|
+
</div>
|
|
41
|
+
${rating?.current && rating.comment ? `<p class="fr-said">“${escapeHtml(rating.comment)}”</p>` : ''}
|
|
42
|
+
${askNote ? `
|
|
43
|
+
<div class="fr-note">
|
|
44
|
+
<textarea maxlength="2000" aria-label="Add a note to your rating" placeholder="${rating.score <= 2 ? 'What went wrong? (optional)' : 'Anything to add? (optional)'}"></textarea>
|
|
45
|
+
<button type="button" data-act="rating-note">Add note</button>
|
|
46
|
+
</div>` : ''}
|
|
47
|
+
${rating ? '' : '<p class="fr-hint">1 is poor, 5 is great.</p>'}
|
|
48
|
+
<p class="feedback-status" role="status"></p>`;
|
|
49
|
+
|
|
50
|
+
const stars = [...box.querySelectorAll('[data-score]')];
|
|
51
|
+
const paint = (upTo) => stars.forEach((b) => b.classList.toggle('on', Number(b.dataset.score) <= upTo));
|
|
52
|
+
const lock = (on) => box.querySelectorAll('button, textarea').forEach((el) => { el.disabled = on; });
|
|
53
|
+
const status = box.querySelector('.feedback-status');
|
|
54
|
+
|
|
55
|
+
async function send(score, comment) {
|
|
56
|
+
lock(true);
|
|
57
|
+
status.textContent = '';
|
|
58
|
+
try {
|
|
59
|
+
const saved = await client.rate(report.id, { score, comment });
|
|
60
|
+
drawRating(box, {
|
|
61
|
+
client, report, rating: saved, justRated: true, question, onRated, onError,
|
|
62
|
+
});
|
|
63
|
+
box.querySelector('.fr-note textarea')?.focus();
|
|
64
|
+
onRated?.(saved);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
status.textContent = err.message;
|
|
67
|
+
lock(false);
|
|
68
|
+
paint(shown);
|
|
69
|
+
onError?.(err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const btn of stars) {
|
|
74
|
+
const n = Number(btn.dataset.score);
|
|
75
|
+
btn.addEventListener('mouseenter', () => paint(n));
|
|
76
|
+
btn.addEventListener('focus', () => paint(n));
|
|
77
|
+
btn.addEventListener('blur', () => paint(shown));
|
|
78
|
+
btn.addEventListener('click', () => send(n));
|
|
79
|
+
}
|
|
80
|
+
box.querySelector('.stars').addEventListener('mouseleave', () => paint(shown));
|
|
81
|
+
|
|
82
|
+
box.querySelector('[data-act="rating-note"]')?.addEventListener('click', () => {
|
|
83
|
+
const note = box.querySelector('.fr-note textarea').value.trim();
|
|
84
|
+
if (!note) { box.querySelector('.fr-note')?.remove(); return; }
|
|
85
|
+
send(rating.score, note);
|
|
86
|
+
});
|
|
87
|
+
}
|
package/src/panel/signin.js
CHANGED
|
@@ -37,7 +37,7 @@ export function mountSignIn(ctx) {
|
|
|
37
37
|
// The call offer is the same errand as the form, so it goes with it — and
|
|
38
38
|
// comes back only if the API had already said this project can take one.
|
|
39
39
|
if (shut) callBtn.hidden = true;
|
|
40
|
-
else if (ctx.callOffered()) callBtn.hidden = false;
|
|
40
|
+
else if (ctx.callOffered() && !ctx.mustRate()) callBtn.hidden = false;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function drawWhoami() {
|
package/src/panel/styles.js
CHANGED
|
@@ -831,6 +831,34 @@ button.quiet:hover { color: var(--bc-fg); text-decoration-color: color-mix(in sr
|
|
|
831
831
|
.history .sub .rated svg { width: 11px; height: 11px; color: #f0a81c; }
|
|
832
832
|
.history .sub .rate-ask { color: var(--bc-fg); font-weight: 600; }
|
|
833
833
|
|
|
834
|
+
/* ---------- a settled report waiting for its rating ----------
|
|
835
|
+
Above the form on the first screen: the same stars as under a report's
|
|
836
|
+
conversation, with the report they are about named on top. */
|
|
837
|
+
.rate-due { margin: 0 0 14px; }
|
|
838
|
+
.rate-due:focus { outline: none; }
|
|
839
|
+
.rate-due:focus-visible { box-shadow: var(--bc-ring); }
|
|
840
|
+
.rate-due .rd-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 0 8px; }
|
|
841
|
+
.rate-due .rd-eyebrow {
|
|
842
|
+
font-size: 11px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; color: var(--bc-faint);
|
|
843
|
+
}
|
|
844
|
+
.rate-due.is-required .rd-eyebrow { color: var(--bc-fg); }
|
|
845
|
+
.rate-due .rd-later { font-size: var(--bc-fs-xs); color: var(--bc-muted); }
|
|
846
|
+
.rate-due .rd-report {
|
|
847
|
+
display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 9px; align-items: center;
|
|
848
|
+
margin: 0 0 10px; padding: 0 0 10px; border-bottom: 1px solid var(--bc-border);
|
|
849
|
+
}
|
|
850
|
+
.rate-due .rd-ico {
|
|
851
|
+
grid-row: span 2; display: inline-grid; place-items: center; width: 24px; height: 24px; border-radius: 999px;
|
|
852
|
+
background: color-mix(in srgb, var(--bc-ok) 14%, transparent); color: var(--bc-ok);
|
|
853
|
+
}
|
|
854
|
+
.rate-due .rd-ico svg { width: 13px; height: 13px; }
|
|
855
|
+
.rate-due .rd-ttl { font-weight: 620; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
856
|
+
.rate-due .rd-sub { font-size: var(--bc-fs-xs); color: var(--bc-muted); }
|
|
857
|
+
|
|
858
|
+
/* What a required rating is holding: still in sight, plainly not yet. */
|
|
859
|
+
.held { opacity: .42; transition: opacity 200ms ease; }
|
|
860
|
+
@media (prefers-reduced-motion: reduce) { .held { transition: none; } }
|
|
861
|
+
|
|
834
862
|
/* A call in progress, in place of the form.
|
|
835
863
|
---------------------------------------------------------------------------
|
|
836
864
|
Deliberately not styled as a chat. Nothing arrives here to read — the
|
package/src/panel/thread.js
CHANGED
|
@@ -11,16 +11,12 @@ import {
|
|
|
11
11
|
atLatest, scrollToLatest,
|
|
12
12
|
} from './render.js';
|
|
13
13
|
import { mountComposer } from './composer.js';
|
|
14
|
+
import { drawRating, ratingKey } from './rating.js';
|
|
14
15
|
|
|
15
16
|
// What each open thread was last drawn from, so a poll that brings nothing new
|
|
16
17
|
// leaves the DOM (and the reader's place in it) alone.
|
|
17
18
|
const threadDrawn = new WeakMap();
|
|
18
19
|
|
|
19
|
-
/** What a drawn rating box was drawn from, so a refresh can tell if it moved. */
|
|
20
|
-
function ratingKey(rating) {
|
|
21
|
-
return JSON.stringify(rating ? [rating.score, rating.current, rating.comment || null] : null);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
20
|
/**
|
|
25
21
|
* The rating's word on a history row: the score they gave, or a nudge on a
|
|
26
22
|
* settled report that is waiting for one. Nothing on an open report.
|
|
@@ -336,10 +332,7 @@ export function mountThread(ctx) {
|
|
|
336
332
|
* "How did we do?" — five stars under a settled report.
|
|
337
333
|
*
|
|
338
334
|
* Only where the API says a rating is being asked for: the report is settled
|
|
339
|
-
* and the project has not switched ratings off.
|
|
340
|
-
* note is offered after it rather than before, because a form that asks for
|
|
341
|
-
* a sentence first is a form people close, and the score is the part that
|
|
342
|
-
* counts.
|
|
335
|
+
* and the project has not switched ratings off. The box itself is rating.js.
|
|
343
336
|
*
|
|
344
337
|
* Redrawn only when the rating itself changed. The thread refreshes on every
|
|
345
338
|
* update and every few seconds while the assistant works, and a box rebuilt
|
|
@@ -358,69 +351,7 @@ export function mountThread(ctx) {
|
|
|
358
351
|
box.className = 'fix-rating';
|
|
359
352
|
(body.querySelector('.fix-verification') || body.querySelector('.report-head'))?.after(box);
|
|
360
353
|
}
|
|
361
|
-
drawRating(box, report, report.rating || null);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function drawRating(box, report, rating, { justRated = false } = {}) {
|
|
365
|
-
box.dataset.key = ratingKey(rating);
|
|
366
|
-
// A rating given for an earlier fix stands, but it is not this fix's: the
|
|
367
|
-
// stars start empty and the sentence says what the old score was.
|
|
368
|
-
const shown = rating?.current ? rating.score : 0;
|
|
369
|
-
const question = !rating
|
|
370
|
-
? 'How did we do?'
|
|
371
|
-
: !rating.current
|
|
372
|
-
? `You rated an earlier fix ${rating.score} out of 5. How did this one go?`
|
|
373
|
-
: justRated
|
|
374
|
-
? 'Thanks — the team will see that.'
|
|
375
|
-
: `You rated this ${rating.score} out of 5.`;
|
|
376
|
-
const askNote = justRated && rating?.current && !rating.comment;
|
|
377
|
-
box.innerHTML = `
|
|
378
|
-
<p class="fr-q">${escapeHtml(question)}</p>
|
|
379
|
-
<div class="stars" role="group" aria-label="Rate how this was handled, 1 to 5">${[1, 2, 3, 4, 5].map((n) => `
|
|
380
|
-
<button type="button" data-score="${n}" aria-pressed="${n === shown}" aria-label="${n} out of 5${n === 1 ? ', poor' : n === 5 ? ', great' : ''}" class="${n <= shown ? 'on' : ''}">${ICONS.star}</button>`).join('')}
|
|
381
|
-
</div>
|
|
382
|
-
${rating?.current && rating.comment ? `<p class="fr-said">“${escapeHtml(rating.comment)}”</p>` : ''}
|
|
383
|
-
${askNote ? `
|
|
384
|
-
<div class="fr-note">
|
|
385
|
-
<textarea maxlength="2000" aria-label="Add a note to your rating" placeholder="${rating.score <= 2 ? 'What went wrong? (optional)' : 'Anything to add? (optional)'}"></textarea>
|
|
386
|
-
<button type="button" data-act="rating-note">Add note</button>
|
|
387
|
-
</div>` : ''}
|
|
388
|
-
${rating ? '' : '<p class="fr-hint">1 is poor, 5 is great.</p>'}
|
|
389
|
-
<p class="feedback-status" role="status"></p>`;
|
|
390
|
-
|
|
391
|
-
const stars = [...box.querySelectorAll('[data-score]')];
|
|
392
|
-
const paint = (upTo) => stars.forEach((b) => b.classList.toggle('on', Number(b.dataset.score) <= upTo));
|
|
393
|
-
const lock = (on) => box.querySelectorAll('button, textarea').forEach((el) => { el.disabled = on; });
|
|
394
|
-
const status = box.querySelector('.feedback-status');
|
|
395
|
-
|
|
396
|
-
async function send(score, comment) {
|
|
397
|
-
lock(true);
|
|
398
|
-
status.textContent = '';
|
|
399
|
-
try {
|
|
400
|
-
const saved = await client.rate(report.id, { score, comment });
|
|
401
|
-
drawRating(box, report, saved, { justRated: true });
|
|
402
|
-
box.querySelector('.fr-note textarea')?.focus();
|
|
403
|
-
} catch (err) {
|
|
404
|
-
status.textContent = err.message;
|
|
405
|
-
lock(false);
|
|
406
|
-
paint(shown);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
for (const btn of stars) {
|
|
411
|
-
const n = Number(btn.dataset.score);
|
|
412
|
-
btn.addEventListener('mouseenter', () => paint(n));
|
|
413
|
-
btn.addEventListener('focus', () => paint(n));
|
|
414
|
-
btn.addEventListener('blur', () => paint(shown));
|
|
415
|
-
btn.addEventListener('click', () => send(n));
|
|
416
|
-
}
|
|
417
|
-
box.querySelector('.stars').addEventListener('mouseleave', () => paint(shown));
|
|
418
|
-
|
|
419
|
-
box.querySelector('[data-act="rating-note"]')?.addEventListener('click', () => {
|
|
420
|
-
const note = box.querySelector('.fr-note textarea').value.trim();
|
|
421
|
-
if (!note) { box.querySelector('.fr-note')?.remove(); return; }
|
|
422
|
-
send(rating.score, note);
|
|
423
|
-
});
|
|
354
|
+
drawRating(box, { client, report, rating: report.rating || null });
|
|
424
355
|
}
|
|
425
356
|
|
|
426
357
|
/**
|