@mhosaic/feedback 0.6.3 → 0.7.1

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.
@@ -27,6 +27,9 @@ function createApiClient(options) {
27
27
  form.append("technical_context", JSON.stringify(payload.technical_context));
28
28
  if (payload.screenshot) form.append("screenshot", payload.screenshot, "screenshot.png");
29
29
  if (payload.synthetic) form.append("synthetic", "true");
30
+ if (payload.user?.id) {
31
+ form.append("user", JSON.stringify(payload.user));
32
+ }
30
33
  const response = await fetcher(`${endpoint}/api/feedback/v1/reports/`, {
31
34
  method: "POST",
32
35
  headers: { Authorization: `Bearer ${options.apiKey}` },
@@ -38,7 +41,75 @@ function createApiClient(options) {
38
41
  }
39
42
  return response.json();
40
43
  }
41
- return { submitReport };
44
+ function widgetHeaders(externalId) {
45
+ return {
46
+ Authorization: `Bearer ${options.apiKey}`,
47
+ "X-Mhosaic-User": externalId
48
+ };
49
+ }
50
+ async function listMine(externalId) {
51
+ const response = await fetcher(`${endpoint}/api/feedback/v1/reports/widget/mine/`, {
52
+ method: "GET",
53
+ headers: widgetHeaders(externalId)
54
+ });
55
+ if (response.status === 404) return [];
56
+ if (!response.ok) {
57
+ const text = await response.text().catch(() => "");
58
+ throw new Error(`listMine failed: ${response.status} ${text}`);
59
+ }
60
+ return response.json();
61
+ }
62
+ async function getReport(reportId, externalId) {
63
+ const response = await fetcher(
64
+ `${endpoint}/api/feedback/v1/reports/widget/${reportId}/`,
65
+ { method: "GET", headers: widgetHeaders(externalId) }
66
+ );
67
+ if (!response.ok) {
68
+ const text = await response.text().catch(() => "");
69
+ throw new Error(`getReport failed: ${response.status} ${text}`);
70
+ }
71
+ return response.json();
72
+ }
73
+ async function addComment(reportId, externalId, body, clientNonce) {
74
+ const response = await fetcher(
75
+ `${endpoint}/api/feedback/v1/reports/widget/${reportId}/comments/`,
76
+ {
77
+ method: "POST",
78
+ headers: {
79
+ ...widgetHeaders(externalId),
80
+ "Content-Type": "application/json"
81
+ },
82
+ body: JSON.stringify({
83
+ body,
84
+ ...clientNonce !== void 0 && { client_nonce: clientNonce }
85
+ })
86
+ }
87
+ );
88
+ if (!response.ok) {
89
+ const text = await response.text().catch(() => "");
90
+ throw new Error(`addComment failed: ${response.status} ${text}`);
91
+ }
92
+ return response.json();
93
+ }
94
+ async function closeAsResolved(reportId, externalId) {
95
+ const response = await fetcher(
96
+ `${endpoint}/api/feedback/v1/reports/widget/${reportId}/`,
97
+ {
98
+ method: "PATCH",
99
+ headers: {
100
+ ...widgetHeaders(externalId),
101
+ "Content-Type": "application/json"
102
+ },
103
+ body: JSON.stringify({ status: "closed" })
104
+ }
105
+ );
106
+ if (!response.ok) {
107
+ const text = await response.text().catch(() => "");
108
+ throw new Error(`closeAsResolved failed: ${response.status} ${text}`);
109
+ }
110
+ return response.json();
111
+ }
112
+ return { submitReport, listMine, getReport, addComment, closeAsResolved };
42
113
  }
43
114
 
44
115
  // src/capture/urlSanitizer.ts
@@ -396,7 +467,41 @@ var DEFAULT_STRINGS = {
396
467
  "annotator.count_suffix": "annotations",
397
468
  "annotator.loading": "Loading\u2026",
398
469
  "annotator.apply": "Apply",
399
- "annotator.applying": "Applying\u2026"
470
+ "annotator.applying": "Applying\u2026",
471
+ "tab.send": "Send",
472
+ "tab.mine": "My reports",
473
+ "mine.empty.title": "No reports yet",
474
+ "mine.empty.body": "Once you send feedback you can follow the thread here.",
475
+ "mine.refresh": "Refresh",
476
+ "mine.loading": "Loading\u2026",
477
+ "mine.error": "Could not load your reports.",
478
+ "mine.replies_one": "1 reply",
479
+ "mine.replies_many": "{count} replies",
480
+ "detail.back": "Back",
481
+ "detail.thread": "Conversation",
482
+ "detail.no_replies": "No replies yet \u2014 we\u2019ll let you know when an operator responds.",
483
+ "detail.compose_placeholder": "Add a follow-up reply\u2026",
484
+ "detail.compose_send": "Reply",
485
+ "detail.compose_sending": "Sending\u2026",
486
+ "detail.close_cta": "Mark as resolved",
487
+ "detail.close_busy": "Marking\u2026",
488
+ "detail.history": "Status history",
489
+ "detail.context.submitted_at": "Submitted",
490
+ "detail.context.page": "Page",
491
+ "detail.context.capture.manual": "Manual capture",
492
+ "detail.context.capture.html2canvas": "Auto capture",
493
+ "detail.context.capture.display_media": "Screen share",
494
+ "detail.context.capture.none": "No screenshot",
495
+ "detail.author.staff": "Operator",
496
+ "detail.author.mcp": "Mhosaic Team",
497
+ "detail.author.system": "System",
498
+ "status.new": "New",
499
+ "status.in_progress": "In progress",
500
+ "status.awaiting_validation": "Awaiting your validation",
501
+ "status.closed": "Closed",
502
+ "status.rejected": "Rejected",
503
+ "status.duplicate": "Duplicate",
504
+ "status.wontfix": "Won\u2019t fix"
400
505
  };
401
506
  var FRENCH_STRINGS = {
402
507
  "fab.label": "Envoyer un commentaire",
@@ -441,7 +546,41 @@ var FRENCH_STRINGS = {
441
546
  "annotator.count_suffix": "annotations",
442
547
  "annotator.loading": "Chargement\u2026",
443
548
  "annotator.apply": "Appliquer",
444
- "annotator.applying": "Application\u2026"
549
+ "annotator.applying": "Application\u2026",
550
+ "tab.send": "Envoyer",
551
+ "tab.mine": "Mes rapports",
552
+ "mine.empty.title": "Aucun rapport",
553
+ "mine.empty.body": "Apr\xE8s votre premier envoi vous pourrez suivre la conversation ici.",
554
+ "mine.refresh": "Actualiser",
555
+ "mine.loading": "Chargement\u2026",
556
+ "mine.error": "Impossible de charger vos rapports.",
557
+ "mine.replies_one": "1 r\xE9ponse",
558
+ "mine.replies_many": "{count} r\xE9ponses",
559
+ "detail.back": "Retour",
560
+ "detail.thread": "Conversation",
561
+ "detail.no_replies": "Pas encore de r\xE9ponse \u2014 vous serez notifi\xE9 d\xE8s qu\u2019un op\xE9rateur r\xE9pondra.",
562
+ "detail.compose_placeholder": "Ajouter une r\xE9ponse\u2026",
563
+ "detail.compose_send": "R\xE9pondre",
564
+ "detail.compose_sending": "Envoi\u2026",
565
+ "detail.close_cta": "Marquer comme r\xE9solu",
566
+ "detail.close_busy": "Validation\u2026",
567
+ "detail.history": "Historique du statut",
568
+ "detail.context.submitted_at": "Envoy\xE9",
569
+ "detail.context.page": "Page",
570
+ "detail.context.capture.manual": "Capture manuelle",
571
+ "detail.context.capture.html2canvas": "Capture automatique",
572
+ "detail.context.capture.display_media": "Partage d\u2019\xE9cran",
573
+ "detail.context.capture.none": "Sans capture",
574
+ "detail.author.staff": "Op\xE9rateur",
575
+ "detail.author.mcp": "\xC9quipe Mhosaic",
576
+ "detail.author.system": "Syst\xE8me",
577
+ "status.new": "Nouveau",
578
+ "status.in_progress": "En cours",
579
+ "status.awaiting_validation": "En attente de validation",
580
+ "status.closed": "Ferm\xE9",
581
+ "status.rejected": "Rejet\xE9",
582
+ "status.duplicate": "Doublon",
583
+ "status.wontfix": "Non corrig\xE9"
445
584
  };
446
585
  var LOCALE_PACKS = {
447
586
  fr: FRENCH_STRINGS
@@ -1058,13 +1197,123 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1058
1197
  ] });
1059
1198
  }
1060
1199
 
1061
- // src/widget/Modal.tsx
1062
- import { useEffect as useEffect3, useRef as useRef3 } from "preact/hooks";
1200
+ // src/widget/MineList.tsx
1201
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "preact/hooks";
1202
+
1203
+ // src/widget/ReportRow.tsx
1063
1204
  import { jsx as jsx4, jsxs as jsxs3 } from "preact/jsx-runtime";
1064
- function Modal({ onDismiss, children, closeLabel = "Close" }) {
1065
- const modalRef = useRef3(null);
1066
- const previouslyFocused = useRef3(null);
1205
+ function statusClassName(status) {
1206
+ return `pill pill-status pill-status--${status}`;
1207
+ }
1208
+ function severityClassName(severity) {
1209
+ return `pill pill-severity pill-severity--${severity}`;
1210
+ }
1211
+ function typeClassName() {
1212
+ return "pill pill-type";
1213
+ }
1214
+ function formatRelative(iso) {
1215
+ const then = Date.parse(iso);
1216
+ if (!Number.isFinite(then)) return "";
1217
+ const seconds = Math.max(1, Math.round((Date.now() - then) / 1e3));
1218
+ if (seconds < 60) return `${seconds}s`;
1219
+ const minutes = Math.round(seconds / 60);
1220
+ if (minutes < 60) return `${minutes}m`;
1221
+ const hours = Math.round(minutes / 60);
1222
+ if (hours < 48) return `${hours}h`;
1223
+ const days = Math.round(hours / 24);
1224
+ return `${days}d`;
1225
+ }
1226
+ function repliesLabel(count, strings) {
1227
+ if (count === 1) return strings["mine.replies_one"];
1228
+ return strings["mine.replies_many"].replace("{count}", String(count));
1229
+ }
1230
+ function ReportRow({ row, strings, onClick }) {
1231
+ const preview = row.description.length > 120 ? row.description.slice(0, 117) + "\u2026" : row.description;
1232
+ return /* @__PURE__ */ jsxs3("button", { type: "button", class: "mine-row", onClick, children: [
1233
+ /* @__PURE__ */ jsxs3("div", { class: "mine-row-pills", children: [
1234
+ /* @__PURE__ */ jsx4("span", { class: statusClassName(row.status), children: strings[`status.${row.status}`] ?? row.status }),
1235
+ /* @__PURE__ */ jsx4("span", { class: typeClassName(), children: strings[`type.${row.feedback_type}`] }),
1236
+ /* @__PURE__ */ jsx4("span", { class: severityClassName(row.severity), children: strings[`severity.${row.severity}`] })
1237
+ ] }),
1238
+ /* @__PURE__ */ jsx4("div", { class: "mine-row-preview", children: preview }),
1239
+ /* @__PURE__ */ jsxs3("div", { class: "mine-row-meta", children: [
1240
+ /* @__PURE__ */ jsx4("span", { children: formatRelative(row.updated_at || row.created_at) }),
1241
+ row.comment_count > 0 && /* @__PURE__ */ jsxs3("span", { children: [
1242
+ "\xB7 ",
1243
+ repliesLabel(row.comment_count, strings)
1244
+ ] })
1245
+ ] })
1246
+ ] });
1247
+ }
1248
+
1249
+ // src/widget/MineList.tsx
1250
+ import { jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
1251
+ var POLL_MS = 3e4;
1252
+ function MineList({ api, externalId, strings, onSelect }) {
1253
+ const [rows, setRows] = useState3(null);
1254
+ const [error, setError] = useState3(null);
1255
+ const [refreshing, setRefreshing] = useState3(false);
1256
+ const mountedRef = useRef3(true);
1257
+ const fetchRows = async () => {
1258
+ setRefreshing(true);
1259
+ setError(null);
1260
+ try {
1261
+ const next = await api.listMine(externalId);
1262
+ if (!mountedRef.current) return;
1263
+ setRows(next);
1264
+ } catch (err) {
1265
+ if (!mountedRef.current) return;
1266
+ setError(err instanceof Error ? err.message : strings["mine.error"]);
1267
+ } finally {
1268
+ if (mountedRef.current) setRefreshing(false);
1269
+ }
1270
+ };
1067
1271
  useEffect3(() => {
1272
+ mountedRef.current = true;
1273
+ void fetchRows();
1274
+ const timer = setInterval(() => {
1275
+ void fetchRows();
1276
+ }, POLL_MS);
1277
+ return () => {
1278
+ mountedRef.current = false;
1279
+ clearInterval(timer);
1280
+ };
1281
+ }, [externalId]);
1282
+ const isEmpty = rows !== null && rows.length === 0;
1283
+ const isLoading = rows === null && !error;
1284
+ return /* @__PURE__ */ jsxs4("div", { class: "mine-list", children: [
1285
+ /* @__PURE__ */ jsxs4("div", { class: "mine-list-header", children: [
1286
+ /* @__PURE__ */ jsx5("h2", { children: strings["tab.mine"] }),
1287
+ /* @__PURE__ */ jsx5(
1288
+ "button",
1289
+ {
1290
+ type: "button",
1291
+ class: "btn",
1292
+ onClick: () => {
1293
+ void fetchRows();
1294
+ },
1295
+ disabled: refreshing,
1296
+ children: refreshing ? strings["mine.loading"] : strings["mine.refresh"]
1297
+ }
1298
+ )
1299
+ ] }),
1300
+ isLoading && /* @__PURE__ */ jsx5("div", { class: "mine-loading", children: strings["mine.loading"] }),
1301
+ error && /* @__PURE__ */ jsx5("div", { class: "error", children: error }),
1302
+ isEmpty && /* @__PURE__ */ jsxs4("div", { class: "mine-empty", children: [
1303
+ /* @__PURE__ */ jsx5("strong", { children: strings["mine.empty.title"] }),
1304
+ /* @__PURE__ */ jsx5("p", { children: strings["mine.empty.body"] })
1305
+ ] }),
1306
+ rows && rows.length > 0 && /* @__PURE__ */ jsx5("ul", { class: "mine-rows", children: rows.map((row) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5(ReportRow, { row, strings, onClick: () => onSelect(row) }) })) })
1307
+ ] });
1308
+ }
1309
+
1310
+ // src/widget/Modal.tsx
1311
+ import { useEffect as useEffect4, useRef as useRef4 } from "preact/hooks";
1312
+ import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
1313
+ function Modal({ onDismiss, children, closeLabel = "Close" }) {
1314
+ const modalRef = useRef4(null);
1315
+ const previouslyFocused = useRef4(null);
1316
+ useEffect4(() => {
1068
1317
  previouslyFocused.current = document.activeElement;
1069
1318
  const onKey = (e) => {
1070
1319
  if (e.key !== "Escape") return;
@@ -1086,7 +1335,7 @@ function Modal({ onDismiss, children, closeLabel = "Close" }) {
1086
1335
  if (prev && typeof prev.focus === "function") prev.focus();
1087
1336
  };
1088
1337
  }, [onDismiss]);
1089
- return /* @__PURE__ */ jsx4(
1338
+ return /* @__PURE__ */ jsx6(
1090
1339
  "div",
1091
1340
  {
1092
1341
  class: "backdrop",
@@ -1094,8 +1343,8 @@ function Modal({ onDismiss, children, closeLabel = "Close" }) {
1094
1343
  onClick: (e) => {
1095
1344
  if (e.target === e.currentTarget) onDismiss();
1096
1345
  },
1097
- children: /* @__PURE__ */ jsxs3("div", { ref: modalRef, class: "modal", role: "dialog", "aria-modal": "true", children: [
1098
- /* @__PURE__ */ jsx4(
1346
+ children: /* @__PURE__ */ jsxs5("div", { ref: modalRef, class: "modal", role: "dialog", "aria-modal": "true", children: [
1347
+ /* @__PURE__ */ jsx6(
1099
1348
  "button",
1100
1349
  {
1101
1350
  type: "button",
@@ -1111,6 +1360,216 @@ function Modal({ onDismiss, children, closeLabel = "Close" }) {
1111
1360
  );
1112
1361
  }
1113
1362
 
1363
+ // src/widget/ReportDetailView.tsx
1364
+ import { useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "preact/hooks";
1365
+
1366
+ // src/widget/CommentBubble.tsx
1367
+ import { jsx as jsx7, jsxs as jsxs6 } from "preact/jsx-runtime";
1368
+ function CommentBubble({ comment, strings }) {
1369
+ const isMine = comment.is_mine;
1370
+ const isAgent = !isMine && comment.author_source === "mcp";
1371
+ const isSystem = !isMine && comment.author_source === "system";
1372
+ const variant = isAgent ? "mcp" : isSystem ? "system" : "staff";
1373
+ const labelKey = variant === "mcp" ? "detail.author.mcp" : variant === "system" ? "detail.author.system" : "detail.author.staff";
1374
+ const label = comment.author_label || strings[labelKey];
1375
+ return /* @__PURE__ */ jsxs6("div", { class: `comment-bubble ${isMine ? "is-mine" : "is-other"}`, children: [
1376
+ !isMine && label && /* @__PURE__ */ jsx7("div", { class: `comment-author comment-author--${variant}`, children: label }),
1377
+ /* @__PURE__ */ jsx7("div", { class: "comment-body", children: comment.body }),
1378
+ /* @__PURE__ */ jsx7("div", { class: "comment-time", children: formatTime(comment.created_at) })
1379
+ ] });
1380
+ }
1381
+ function formatTime(iso) {
1382
+ try {
1383
+ return new Date(iso).toLocaleString(void 0, {
1384
+ dateStyle: "short",
1385
+ timeStyle: "short"
1386
+ });
1387
+ } catch {
1388
+ return iso;
1389
+ }
1390
+ }
1391
+
1392
+ // src/widget/ReportDetailView.tsx
1393
+ import { jsx as jsx8, jsxs as jsxs7 } from "preact/jsx-runtime";
1394
+ var POLL_MS2 = 3e4;
1395
+ function ReportDetailView({
1396
+ api,
1397
+ externalId,
1398
+ reportId,
1399
+ strings,
1400
+ onBack
1401
+ }) {
1402
+ const [detail, setDetail] = useState4(null);
1403
+ const [error, setError] = useState4(null);
1404
+ const [composeBody, setComposeBody] = useState4("");
1405
+ const [sending, setSending] = useState4(false);
1406
+ const [closing, setClosing] = useState4(false);
1407
+ const mountedRef = useRef5(true);
1408
+ const fetchDetail = async () => {
1409
+ try {
1410
+ const next = await api.getReport(reportId, externalId);
1411
+ if (!mountedRef.current) return;
1412
+ setDetail(next);
1413
+ setError(null);
1414
+ } catch (err) {
1415
+ if (!mountedRef.current) return;
1416
+ setError(err instanceof Error ? err.message : "load_failed");
1417
+ }
1418
+ };
1419
+ useEffect5(() => {
1420
+ mountedRef.current = true;
1421
+ void fetchDetail();
1422
+ const timer = setInterval(() => {
1423
+ void fetchDetail();
1424
+ }, POLL_MS2);
1425
+ return () => {
1426
+ mountedRef.current = false;
1427
+ clearInterval(timer);
1428
+ };
1429
+ }, [reportId, externalId]);
1430
+ const handleSend = async () => {
1431
+ if (!composeBody.trim() || sending) return;
1432
+ setSending(true);
1433
+ try {
1434
+ const nonce = `${reportId}:${Date.now()}`;
1435
+ const created = await api.addComment(reportId, externalId, composeBody.trim(), nonce);
1436
+ if (!mountedRef.current) return;
1437
+ setComposeBody("");
1438
+ setDetail(
1439
+ (prev) => prev ? { ...prev, comments: appendComment(prev.comments, created) } : prev
1440
+ );
1441
+ void fetchDetail();
1442
+ } catch (err) {
1443
+ if (!mountedRef.current) return;
1444
+ setError(err instanceof Error ? err.message : "comment_failed");
1445
+ } finally {
1446
+ if (mountedRef.current) setSending(false);
1447
+ }
1448
+ };
1449
+ const handleClose = async () => {
1450
+ if (closing) return;
1451
+ setClosing(true);
1452
+ try {
1453
+ const next = await api.closeAsResolved(reportId, externalId);
1454
+ if (!mountedRef.current) return;
1455
+ setDetail(next);
1456
+ } catch (err) {
1457
+ if (!mountedRef.current) return;
1458
+ setError(err instanceof Error ? err.message : "close_failed");
1459
+ } finally {
1460
+ if (mountedRef.current) setClosing(false);
1461
+ }
1462
+ };
1463
+ if (!detail && !error) {
1464
+ return /* @__PURE__ */ jsx8("div", { class: "mine-loading", children: strings["mine.loading"] });
1465
+ }
1466
+ if (!detail) {
1467
+ return /* @__PURE__ */ jsx8("div", { class: "error", role: "alert", children: error });
1468
+ }
1469
+ const showCloseCta = detail.status === "awaiting_validation";
1470
+ return /* @__PURE__ */ jsxs7("div", { class: "report-detail", children: [
1471
+ /* @__PURE__ */ jsxs7("div", { class: "report-detail-header", children: [
1472
+ /* @__PURE__ */ jsxs7("button", { type: "button", class: "btn", onClick: onBack, children: [
1473
+ "\u2190 ",
1474
+ strings["detail.back"]
1475
+ ] }),
1476
+ /* @__PURE__ */ jsx8("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status })
1477
+ ] }),
1478
+ /* @__PURE__ */ jsxs7("div", { class: "report-detail-body", children: [
1479
+ /* @__PURE__ */ jsx8(ContextBlock, { detail, strings }),
1480
+ /* @__PURE__ */ jsx8("p", { class: "report-detail-description", children: detail.description }),
1481
+ detail.screenshot_url && /* @__PURE__ */ jsx8(
1482
+ "a",
1483
+ {
1484
+ class: "report-detail-screenshot",
1485
+ href: detail.screenshot_url,
1486
+ target: "_blank",
1487
+ rel: "noopener noreferrer",
1488
+ children: /* @__PURE__ */ jsx8("img", { src: detail.screenshot_url, alt: "", loading: "lazy" })
1489
+ }
1490
+ ),
1491
+ /* @__PURE__ */ jsx8("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
1492
+ detail.comments.length === 0 ? /* @__PURE__ */ jsx8("p", { class: "report-detail-empty", children: strings["detail.no_replies"] }) : /* @__PURE__ */ jsx8("ul", { class: "report-comments", children: detail.comments.map((c) => /* @__PURE__ */ jsx8("li", { children: /* @__PURE__ */ jsx8(CommentBubble, { comment: c, strings }) })) }),
1493
+ /* @__PURE__ */ jsxs7("div", { class: "report-compose", children: [
1494
+ /* @__PURE__ */ jsx8(
1495
+ "textarea",
1496
+ {
1497
+ value: composeBody,
1498
+ placeholder: strings["detail.compose_placeholder"],
1499
+ onInput: (e) => setComposeBody(e.target.value),
1500
+ disabled: sending
1501
+ }
1502
+ ),
1503
+ /* @__PURE__ */ jsxs7("div", { class: "report-compose-actions", children: [
1504
+ showCloseCta && /* @__PURE__ */ jsx8(
1505
+ "button",
1506
+ {
1507
+ type: "button",
1508
+ class: "btn",
1509
+ onClick: handleClose,
1510
+ disabled: closing,
1511
+ children: closing ? strings["detail.close_busy"] : strings["detail.close_cta"]
1512
+ }
1513
+ ),
1514
+ /* @__PURE__ */ jsx8(
1515
+ "button",
1516
+ {
1517
+ type: "button",
1518
+ class: "btn btn--primary",
1519
+ onClick: handleSend,
1520
+ disabled: !composeBody.trim() || sending,
1521
+ children: sending ? strings["detail.compose_sending"] : strings["detail.compose_send"]
1522
+ }
1523
+ )
1524
+ ] })
1525
+ ] }),
1526
+ error && /* @__PURE__ */ jsx8("div", { class: "error", children: error })
1527
+ ] })
1528
+ ] });
1529
+ }
1530
+ function appendComment(current, next) {
1531
+ if (current.some((c) => c.id === next.id)) return current;
1532
+ return [...current, next];
1533
+ }
1534
+ function ContextBlock({ detail, strings }) {
1535
+ const captureKey = `detail.context.capture.${detail.capture_method}`;
1536
+ const captureLabel = strings[captureKey] ?? detail.capture_method;
1537
+ return /* @__PURE__ */ jsxs7("div", { class: "report-detail-context", children: [
1538
+ /* @__PURE__ */ jsxs7("div", { class: "report-detail-context-pills", children: [
1539
+ /* @__PURE__ */ jsx8("span", { class: "pill pill-type", children: strings[`type.${detail.feedback_type}`] }),
1540
+ /* @__PURE__ */ jsx8("span", { class: `pill pill-severity pill-severity--${detail.severity}`, children: strings[`severity.${detail.severity}`] }),
1541
+ /* @__PURE__ */ jsx8("span", { class: "pill pill-capture", children: captureLabel })
1542
+ ] }),
1543
+ /* @__PURE__ */ jsxs7("div", { class: "report-detail-context-line", children: [
1544
+ /* @__PURE__ */ jsx8("span", { class: "report-detail-context-label", children: strings["detail.context.submitted_at"] }),
1545
+ /* @__PURE__ */ jsx8("span", { children: formatSubmittedAt(detail.created_at) })
1546
+ ] }),
1547
+ detail.page_url && /* @__PURE__ */ jsxs7("div", { class: "report-detail-context-line", title: detail.page_url, children: [
1548
+ /* @__PURE__ */ jsx8("span", { class: "report-detail-context-label", children: strings["detail.context.page"] }),
1549
+ /* @__PURE__ */ jsx8(
1550
+ "a",
1551
+ {
1552
+ class: "report-detail-context-url",
1553
+ href: detail.page_url,
1554
+ target: "_blank",
1555
+ rel: "noopener noreferrer",
1556
+ children: truncateUrl(detail.page_url, 64)
1557
+ }
1558
+ )
1559
+ ] })
1560
+ ] });
1561
+ }
1562
+ function formatSubmittedAt(iso) {
1563
+ try {
1564
+ return new Date(iso).toLocaleString(void 0, {
1565
+ dateStyle: "medium",
1566
+ timeStyle: "short"
1567
+ });
1568
+ } catch {
1569
+ return iso;
1570
+ }
1571
+ }
1572
+
1114
1573
  // src/widget/styles.ts
1115
1574
  var WIDGET_STYLES = `
1116
1575
  :host {
@@ -1496,10 +1955,269 @@ var WIDGET_STYLES = `
1496
1955
  padding: 10px 14px;
1497
1956
  border-top: 1px solid var(--mfb-border);
1498
1957
  }
1958
+
1959
+ /* ---- v0.7: tabs + reader UI -------------------------------------- */
1960
+
1961
+ .tab-strip {
1962
+ display: flex;
1963
+ gap: 4px;
1964
+ border-bottom: 1px solid var(--mfb-border);
1965
+ margin: 0 -4px 4px;
1966
+ padding: 0 4px;
1967
+ }
1968
+ .tab-button {
1969
+ appearance: none;
1970
+ background: transparent;
1971
+ border: 0;
1972
+ border-bottom: 2px solid transparent;
1973
+ padding: 8px 12px;
1974
+ font: inherit;
1975
+ font-size: 13px;
1976
+ font-weight: 500;
1977
+ color: var(--mfb-text-muted);
1978
+ cursor: pointer;
1979
+ }
1980
+ .tab-button:hover { color: var(--mfb-text); }
1981
+ .tab-button.is-active {
1982
+ color: var(--mfb-text);
1983
+ border-bottom-color: var(--mfb-accent);
1984
+ }
1985
+ .tab-button[aria-selected="true"] { font-weight: 600; }
1986
+
1987
+ .mine-list { display: flex; flex-direction: column; gap: 10px; }
1988
+ .mine-list-header {
1989
+ display: flex;
1990
+ align-items: center;
1991
+ justify-content: space-between;
1992
+ }
1993
+ .mine-list-header h2 { margin: 0; font-size: 15px; font-weight: 600; }
1994
+ .mine-loading { color: var(--mfb-text-muted); font-size: 13px; }
1995
+ .mine-empty {
1996
+ text-align: center;
1997
+ padding: 24px 12px;
1998
+ color: var(--mfb-text-muted);
1999
+ font-size: 13px;
2000
+ }
2001
+ .mine-empty strong { display: block; color: var(--mfb-text); margin-bottom: 4px; }
2002
+
2003
+ .mine-rows {
2004
+ list-style: none;
2005
+ margin: 0;
2006
+ padding: 0;
2007
+ display: flex;
2008
+ flex-direction: column;
2009
+ gap: 6px;
2010
+ max-height: 380px;
2011
+ overflow-y: auto;
2012
+ }
2013
+ .mine-row {
2014
+ appearance: none;
2015
+ text-align: left;
2016
+ background: var(--mfb-surface);
2017
+ border: 1px solid var(--mfb-border);
2018
+ border-radius: var(--mfb-radius);
2019
+ padding: 10px 12px;
2020
+ font: inherit;
2021
+ color: inherit;
2022
+ cursor: pointer;
2023
+ display: flex;
2024
+ flex-direction: column;
2025
+ gap: 4px;
2026
+ width: 100%;
2027
+ transition: border-color 120ms ease, background 120ms ease;
2028
+ }
2029
+ .mine-row:hover { border-color: var(--mfb-text-muted); }
2030
+ .mine-row:focus-visible {
2031
+ outline: 2px solid var(--mfb-accent);
2032
+ outline-offset: 2px;
2033
+ }
2034
+ .mine-row-pills { display: flex; gap: 4px; flex-wrap: wrap; }
2035
+ .mine-row-preview {
2036
+ font-size: 13px;
2037
+ color: var(--mfb-text);
2038
+ display: -webkit-box;
2039
+ -webkit-line-clamp: 2;
2040
+ -webkit-box-orient: vertical;
2041
+ overflow: hidden;
2042
+ }
2043
+ .mine-row-meta {
2044
+ display: flex;
2045
+ gap: 6px;
2046
+ font-size: 11px;
2047
+ color: var(--mfb-text-muted);
2048
+ }
2049
+
2050
+ .pill {
2051
+ display: inline-block;
2052
+ font-size: 10px;
2053
+ font-weight: 600;
2054
+ text-transform: uppercase;
2055
+ letter-spacing: 0.04em;
2056
+ padding: 2px 8px;
2057
+ border-radius: 999px;
2058
+ border: 1px solid transparent;
2059
+ }
2060
+ .pill-status { background: #eff6ff; color: #1e40af; border-color: #dbeafe; }
2061
+ .pill-status--in_progress { background: #fffbeb; color: #92400e; border-color: #fde68a; }
2062
+ .pill-status--awaiting_validation { background: #faf5ff; color: #6b21a8; border-color: #e9d5ff; }
2063
+ .pill-status--closed { background: #ecfdf5; color: #065f46; border-color: #a7f3d0; }
2064
+ .pill-status--rejected, .pill-status--wontfix, .pill-status--duplicate { background: #f3f4f6; color: #374151; border-color: #e5e7eb; }
2065
+ .pill-type { background: var(--mfb-surface); color: var(--mfb-text-muted); border-color: var(--mfb-border); }
2066
+ .pill-severity { background: var(--mfb-surface); color: var(--mfb-text-muted); border-color: var(--mfb-border); }
2067
+ .pill-severity--blocker { background: #fef2f2; color: #991b1b; border-color: #fecaca; }
2068
+ .pill-severity--high { background: #fff7ed; color: #9a3412; border-color: #fed7aa; }
2069
+ .pill-severity--medium { background: #fefce8; color: #854d0e; border-color: #fef08a; }
2070
+ .pill-severity--low { background: var(--mfb-surface); color: var(--mfb-text-muted); }
2071
+
2072
+ .report-detail { display: flex; flex-direction: column; gap: 12px; }
2073
+ .report-detail-header {
2074
+ display: flex;
2075
+ align-items: center;
2076
+ justify-content: space-between;
2077
+ gap: 8px;
2078
+ }
2079
+ .report-detail-body { display: flex; flex-direction: column; gap: 10px; }
2080
+ .report-detail-description {
2081
+ font-size: 14px;
2082
+ white-space: pre-wrap;
2083
+ margin: 0;
2084
+ }
2085
+ .report-detail-screenshot {
2086
+ display: block;
2087
+ border: 1px solid var(--mfb-border);
2088
+ border-radius: var(--mfb-radius);
2089
+ overflow: hidden;
2090
+ background: #1a1a1a;
2091
+ }
2092
+ .report-detail-screenshot img {
2093
+ display: block;
2094
+ width: 100%;
2095
+ max-height: 200px;
2096
+ object-fit: contain;
2097
+ }
2098
+ .report-detail-section {
2099
+ font-size: 12px;
2100
+ text-transform: uppercase;
2101
+ letter-spacing: 0.04em;
2102
+ color: var(--mfb-text-muted);
2103
+ margin: 8px 0 4px;
2104
+ font-weight: 600;
2105
+ }
2106
+
2107
+ .report-detail-context {
2108
+ display: flex;
2109
+ flex-direction: column;
2110
+ gap: 6px;
2111
+ padding: 10px 12px;
2112
+ background: var(--mfb-surface);
2113
+ border-radius: var(--mfb-radius);
2114
+ border: 1px solid var(--mfb-border);
2115
+ }
2116
+ .report-detail-context-pills { display: flex; gap: 4px; flex-wrap: wrap; }
2117
+ .report-detail-context-line {
2118
+ display: flex;
2119
+ align-items: baseline;
2120
+ gap: 8px;
2121
+ font-size: 12px;
2122
+ color: var(--mfb-text);
2123
+ }
2124
+ .report-detail-context-label {
2125
+ text-transform: uppercase;
2126
+ letter-spacing: 0.04em;
2127
+ font-size: 10px;
2128
+ font-weight: 600;
2129
+ color: var(--mfb-text-muted);
2130
+ min-width: 56px;
2131
+ }
2132
+ .report-detail-context-url {
2133
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
2134
+ color: var(--mfb-accent);
2135
+ text-decoration: none;
2136
+ font-size: 11px;
2137
+ overflow: hidden;
2138
+ text-overflow: ellipsis;
2139
+ white-space: nowrap;
2140
+ }
2141
+ .report-detail-context-url:hover { text-decoration: underline; }
2142
+ .pill-capture { background: var(--mfb-bg); color: var(--mfb-text-muted); border-color: var(--mfb-border); }
2143
+ .report-detail-empty {
2144
+ font-size: 12px;
2145
+ color: var(--mfb-text-muted);
2146
+ margin: 0;
2147
+ }
2148
+
2149
+ .report-comments {
2150
+ list-style: none;
2151
+ margin: 0;
2152
+ padding: 0;
2153
+ display: flex;
2154
+ flex-direction: column;
2155
+ gap: 6px;
2156
+ max-height: 300px;
2157
+ overflow-y: auto;
2158
+ }
2159
+
2160
+ .comment-bubble {
2161
+ padding: 8px 10px;
2162
+ border-radius: 12px;
2163
+ max-width: 88%;
2164
+ font-size: 13px;
2165
+ line-height: 1.4;
2166
+ }
2167
+ .comment-bubble.is-mine {
2168
+ align-self: flex-end;
2169
+ background: var(--mfb-accent);
2170
+ color: var(--mfb-accent-contrast);
2171
+ }
2172
+ .comment-bubble.is-other {
2173
+ align-self: flex-start;
2174
+ background: var(--mfb-surface);
2175
+ border: 1px solid var(--mfb-border);
2176
+ color: var(--mfb-text);
2177
+ }
2178
+ .comment-author {
2179
+ font-size: 10px;
2180
+ text-transform: uppercase;
2181
+ letter-spacing: 0.04em;
2182
+ font-weight: 600;
2183
+ margin-bottom: 2px;
2184
+ }
2185
+ .comment-author--mcp { color: #6b21a8; }
2186
+ .comment-author--staff { color: #1e40af; }
2187
+ .comment-author--system { color: var(--mfb-text-muted); }
2188
+ .comment-body { white-space: pre-wrap; }
2189
+ .comment-time {
2190
+ font-size: 10px;
2191
+ margin-top: 4px;
2192
+ opacity: 0.7;
2193
+ }
2194
+
2195
+ .report-compose {
2196
+ display: flex;
2197
+ flex-direction: column;
2198
+ gap: 6px;
2199
+ margin-top: 4px;
2200
+ }
2201
+ .report-compose textarea {
2202
+ font: inherit;
2203
+ font-size: 13px;
2204
+ padding: 8px 10px;
2205
+ border: 1px solid var(--mfb-border);
2206
+ border-radius: var(--mfb-radius);
2207
+ background: var(--mfb-surface);
2208
+ color: inherit;
2209
+ min-height: 64px;
2210
+ resize: vertical;
2211
+ }
2212
+ .report-compose-actions {
2213
+ display: flex;
2214
+ justify-content: flex-end;
2215
+ gap: 6px;
2216
+ }
1499
2217
  `;
1500
2218
 
1501
2219
  // src/widget/mount.tsx
1502
- import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
2220
+ import { Fragment, jsx as jsx9, jsxs as jsxs8 } from "preact/jsx-runtime";
1503
2221
  function mountWidget(options) {
1504
2222
  const shadow = options.host.attachShadow({ mode: "open" });
1505
2223
  const style = document.createElement("style");
@@ -1507,45 +2225,113 @@ function mountWidget(options) {
1507
2225
  shadow.appendChild(style);
1508
2226
  const mountPoint = document.createElement("div");
1509
2227
  shadow.appendChild(mountPoint);
1510
- let currentState = { open: false, status: "idle" };
2228
+ let currentState = { open: false, status: "idle", tab: "send" };
1511
2229
  function rerender(state) {
1512
2230
  currentState = state;
1513
2231
  render(h(Root, { state }), mountPoint);
1514
2232
  }
2233
+ function clearSelected(s) {
2234
+ const { selectedReportId: _drop, ...rest } = s;
2235
+ void _drop;
2236
+ return rest;
2237
+ }
1515
2238
  function Root({ state }) {
1516
2239
  const handleSubmit = useCallback(async (values) => {
1517
- rerender({ open: true, status: "submitting" });
2240
+ rerender({ ...currentState, status: "submitting" });
1518
2241
  try {
1519
2242
  await options.onSubmit(values);
1520
- rerender({ open: true, status: "success" });
1521
- setTimeout(() => rerender({ open: false, status: "idle" }), 1200);
2243
+ rerender({ ...currentState, status: "success" });
2244
+ setTimeout(
2245
+ () => rerender({
2246
+ ...currentState,
2247
+ open: false,
2248
+ status: "idle",
2249
+ // After a successful submit, jump the user to "My reports" so
2250
+ // they immediately see their just-filed report in the list
2251
+ // (and the conversation that's about to start).
2252
+ tab: options.api ? "mine" : "send"
2253
+ }),
2254
+ 1200
2255
+ );
1522
2256
  } catch (err) {
1523
- rerender({ open: true, status: "error", error: err instanceof Error ? err.message : String(err) });
2257
+ rerender({
2258
+ ...currentState,
2259
+ status: "error",
2260
+ error: err instanceof Error ? err.message : String(err)
2261
+ });
1524
2262
  }
1525
2263
  }, []);
1526
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
1527
- options.showFAB && /* @__PURE__ */ jsx5(
2264
+ const externalId = options.getExternalId?.();
2265
+ const fabVisible = options.showFAB && (options.getExternalId === void 0 ? true : Boolean(externalId));
2266
+ const showMineTab = Boolean(options.api && externalId);
2267
+ return /* @__PURE__ */ jsxs8(Fragment, { children: [
2268
+ fabVisible && /* @__PURE__ */ jsx9(
1528
2269
  Fab,
1529
2270
  {
1530
2271
  label: options.strings["fab.label"],
1531
2272
  onClick: () => rerender({ ...currentState, open: true })
1532
2273
  }
1533
2274
  ),
1534
- state.open && /* @__PURE__ */ jsx5(
2275
+ state.open && /* @__PURE__ */ jsxs8(
1535
2276
  Modal,
1536
2277
  {
1537
- onDismiss: () => rerender({ open: false, status: "idle" }),
2278
+ onDismiss: () => rerender(clearSelected({ ...currentState, open: false, status: "idle" })),
1538
2279
  closeLabel: options.strings["form.close"],
1539
- children: /* @__PURE__ */ jsx5(
1540
- Form,
1541
- {
1542
- strings: options.strings,
1543
- onSubmit: handleSubmit,
1544
- onCancel: () => rerender({ open: false, status: "idle" }),
1545
- status: state.status,
1546
- ...state.error !== void 0 && { errorMessage: state.error }
1547
- }
1548
- )
2280
+ children: [
2281
+ showMineTab && /* @__PURE__ */ jsxs8("div", { class: "tab-strip", role: "tablist", children: [
2282
+ /* @__PURE__ */ jsx9(
2283
+ "button",
2284
+ {
2285
+ type: "button",
2286
+ role: "tab",
2287
+ "aria-selected": state.tab === "send",
2288
+ class: `tab-button ${state.tab === "send" ? "is-active" : ""}`,
2289
+ onClick: () => rerender(clearSelected({ ...currentState, tab: "send" })),
2290
+ children: options.strings["tab.send"]
2291
+ }
2292
+ ),
2293
+ /* @__PURE__ */ jsx9(
2294
+ "button",
2295
+ {
2296
+ type: "button",
2297
+ role: "tab",
2298
+ "aria-selected": state.tab === "mine",
2299
+ class: `tab-button ${state.tab === "mine" ? "is-active" : ""}`,
2300
+ onClick: () => rerender(clearSelected({ ...currentState, tab: "mine" })),
2301
+ children: options.strings["tab.mine"]
2302
+ }
2303
+ )
2304
+ ] }),
2305
+ state.tab === "send" && /* @__PURE__ */ jsx9(
2306
+ Form,
2307
+ {
2308
+ strings: options.strings,
2309
+ onSubmit: handleSubmit,
2310
+ onCancel: () => rerender({ ...currentState, open: false, status: "idle" }),
2311
+ status: state.status,
2312
+ ...state.error !== void 0 && { errorMessage: state.error }
2313
+ }
2314
+ ),
2315
+ state.tab === "mine" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx9(
2316
+ MineList,
2317
+ {
2318
+ api: options.api,
2319
+ externalId,
2320
+ strings: options.strings,
2321
+ onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
2322
+ }
2323
+ ),
2324
+ state.tab === "mine" && options.api && externalId && state.selectedReportId && /* @__PURE__ */ jsx9(
2325
+ ReportDetailView,
2326
+ {
2327
+ api: options.api,
2328
+ externalId,
2329
+ reportId: state.selectedReportId,
2330
+ strings: options.strings,
2331
+ onBack: () => rerender(clearSelected({ ...currentState }))
2332
+ }
2333
+ )
2334
+ ]
1549
2335
  }
1550
2336
  )
1551
2337
  ] });
@@ -1561,6 +2347,9 @@ function mountWidget(options) {
1561
2347
  dispose() {
1562
2348
  render(null, mountPoint);
1563
2349
  options.host.innerHTML = "";
2350
+ },
2351
+ notifyIdentityChanged() {
2352
+ rerender({ ...currentState });
1564
2353
  }
1565
2354
  };
1566
2355
  }
@@ -1615,6 +2404,15 @@ function createFeedback(config) {
1615
2404
  };
1616
2405
  if (screenshot) payload.screenshot = screenshot;
1617
2406
  if (values.synthetic) payload.synthetic = true;
2407
+ if (user?.id !== void 0 && user.id !== null && user.id !== "") {
2408
+ payload.user = {
2409
+ // The host can pass `id` as a string or number; the backend
2410
+ // stores it as an opaque string. Coerce here to a stable shape.
2411
+ id: String(user.id),
2412
+ ...user.email !== void 0 && { email: user.email },
2413
+ ...user.name !== void 0 && { name: user.name }
2414
+ };
2415
+ }
1618
2416
  let finalPayload = payload;
1619
2417
  for (const t of transformers) finalPayload = await t(finalPayload);
1620
2418
  try {
@@ -1634,7 +2432,12 @@ function createFeedback(config) {
1634
2432
  showFAB: config.showFAB ?? true,
1635
2433
  onSubmit: async (values) => {
1636
2434
  await buildAndSubmit(values);
1637
- }
2435
+ },
2436
+ api,
2437
+ // Keep this a callback (not a snapshot) so the mount picks up identity
2438
+ // changes that happen after createFeedback() — `notifyIdentityChanged()`
2439
+ // is the trigger for a re-render.
2440
+ getExternalId: () => user?.id !== void 0 && user.id !== null && user.id !== "" ? String(user.id) : void 0
1638
2441
  });
1639
2442
  const instance = {
1640
2443
  show() {
@@ -1658,6 +2461,7 @@ function createFeedback(config) {
1658
2461
  },
1659
2462
  identify(u) {
1660
2463
  user = u;
2464
+ handle.notifyIdentityChanged();
1661
2465
  },
1662
2466
  setMetadata(kv) {
1663
2467
  metadata = { ...metadata, ...kv };
@@ -1682,4 +2486,4 @@ function createFeedback(config) {
1682
2486
  export {
1683
2487
  createFeedback
1684
2488
  };
1685
- //# sourceMappingURL=chunk-3RIR3JHF.mjs.map
2489
+ //# sourceMappingURL=chunk-F3FVKCBE.mjs.map