@mhosaic/feedback 0.6.2 → 0.7.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.
@@ -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
@@ -366,6 +437,7 @@ var DEFAULT_STRINGS = {
366
437
  "form.submitting": "Sending\u2026",
367
438
  "form.success": "Thanks \u2014 your feedback was sent.",
368
439
  "form.error": "Could not send. Please try again.",
440
+ "form.description.required": "Please describe the issue before sending.",
369
441
  "form.screenshot.label": "Screenshot",
370
442
  "form.screenshot.cta_click": "Click",
371
443
  "form.screenshot.cta_rest": "drop, or paste an image",
@@ -395,7 +467,35 @@ var DEFAULT_STRINGS = {
395
467
  "annotator.count_suffix": "annotations",
396
468
  "annotator.loading": "Loading\u2026",
397
469
  "annotator.apply": "Apply",
398
- "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.author.staff": "Operator",
490
+ "detail.author.mcp": "Mhosaic Team",
491
+ "detail.author.system": "System",
492
+ "status.new": "New",
493
+ "status.in_progress": "In progress",
494
+ "status.awaiting_validation": "Awaiting your validation",
495
+ "status.closed": "Closed",
496
+ "status.rejected": "Rejected",
497
+ "status.duplicate": "Duplicate",
498
+ "status.wontfix": "Won\u2019t fix"
399
499
  };
400
500
  var FRENCH_STRINGS = {
401
501
  "fab.label": "Envoyer un commentaire",
@@ -410,6 +510,7 @@ var FRENCH_STRINGS = {
410
510
  "form.submitting": "Envoi\u2026",
411
511
  "form.success": "Merci \u2014 votre commentaire a \xE9t\xE9 envoy\xE9.",
412
512
  "form.error": "\xC9chec d\u2019envoi. Veuillez r\xE9essayer.",
513
+ "form.description.required": "D\xE9crivez le probl\xE8me avant d\u2019envoyer.",
413
514
  "form.screenshot.label": "Capture d\u2019\xE9cran",
414
515
  "form.screenshot.cta_click": "Cliquez",
415
516
  "form.screenshot.cta_rest": "d\xE9posez ou collez une image",
@@ -439,7 +540,35 @@ var FRENCH_STRINGS = {
439
540
  "annotator.count_suffix": "annotations",
440
541
  "annotator.loading": "Chargement\u2026",
441
542
  "annotator.apply": "Appliquer",
442
- "annotator.applying": "Application\u2026"
543
+ "annotator.applying": "Application\u2026",
544
+ "tab.send": "Envoyer",
545
+ "tab.mine": "Mes rapports",
546
+ "mine.empty.title": "Aucun rapport",
547
+ "mine.empty.body": "Apr\xE8s votre premier envoi vous pourrez suivre la conversation ici.",
548
+ "mine.refresh": "Actualiser",
549
+ "mine.loading": "Chargement\u2026",
550
+ "mine.error": "Impossible de charger vos rapports.",
551
+ "mine.replies_one": "1 r\xE9ponse",
552
+ "mine.replies_many": "{count} r\xE9ponses",
553
+ "detail.back": "Retour",
554
+ "detail.thread": "Conversation",
555
+ "detail.no_replies": "Pas encore de r\xE9ponse \u2014 vous serez notifi\xE9 d\xE8s qu\u2019un op\xE9rateur r\xE9pondra.",
556
+ "detail.compose_placeholder": "Ajouter une r\xE9ponse\u2026",
557
+ "detail.compose_send": "R\xE9pondre",
558
+ "detail.compose_sending": "Envoi\u2026",
559
+ "detail.close_cta": "Marquer comme r\xE9solu",
560
+ "detail.close_busy": "Validation\u2026",
561
+ "detail.history": "Historique du statut",
562
+ "detail.author.staff": "Op\xE9rateur",
563
+ "detail.author.mcp": "\xC9quipe Mhosaic",
564
+ "detail.author.system": "Syst\xE8me",
565
+ "status.new": "Nouveau",
566
+ "status.in_progress": "En cours",
567
+ "status.awaiting_validation": "En attente de validation",
568
+ "status.closed": "Ferm\xE9",
569
+ "status.rejected": "Rejet\xE9",
570
+ "status.duplicate": "Doublon",
571
+ "status.wontfix": "Non corrig\xE9"
443
572
  };
444
573
  var LOCALE_PACKS = {
445
574
  fr: FRENCH_STRINGS
@@ -917,7 +1046,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
917
1046
  const handleSubmit = (e) => {
918
1047
  e.preventDefault();
919
1048
  if (!description.trim()) {
920
- setLocalError(strings["form.description.placeholder"]);
1049
+ setLocalError(strings["form.description.required"]);
921
1050
  return;
922
1051
  }
923
1052
  setLocalError("");
@@ -1056,13 +1185,123 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1056
1185
  ] });
1057
1186
  }
1058
1187
 
1059
- // src/widget/Modal.tsx
1060
- import { useEffect as useEffect3, useRef as useRef3 } from "preact/hooks";
1188
+ // src/widget/MineList.tsx
1189
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "preact/hooks";
1190
+
1191
+ // src/widget/ReportRow.tsx
1061
1192
  import { jsx as jsx4, jsxs as jsxs3 } from "preact/jsx-runtime";
1062
- function Modal({ onDismiss, children, closeLabel = "Close" }) {
1063
- const modalRef = useRef3(null);
1064
- const previouslyFocused = useRef3(null);
1193
+ function statusClassName(status) {
1194
+ return `pill pill-status pill-status--${status}`;
1195
+ }
1196
+ function severityClassName(severity) {
1197
+ return `pill pill-severity pill-severity--${severity}`;
1198
+ }
1199
+ function typeClassName() {
1200
+ return "pill pill-type";
1201
+ }
1202
+ function formatRelative(iso) {
1203
+ const then = Date.parse(iso);
1204
+ if (!Number.isFinite(then)) return "";
1205
+ const seconds = Math.max(1, Math.round((Date.now() - then) / 1e3));
1206
+ if (seconds < 60) return `${seconds}s`;
1207
+ const minutes = Math.round(seconds / 60);
1208
+ if (minutes < 60) return `${minutes}m`;
1209
+ const hours = Math.round(minutes / 60);
1210
+ if (hours < 48) return `${hours}h`;
1211
+ const days = Math.round(hours / 24);
1212
+ return `${days}d`;
1213
+ }
1214
+ function repliesLabel(count, strings) {
1215
+ if (count === 1) return strings["mine.replies_one"];
1216
+ return strings["mine.replies_many"].replace("{count}", String(count));
1217
+ }
1218
+ function ReportRow({ row, strings, onClick }) {
1219
+ const preview = row.description.length > 120 ? row.description.slice(0, 117) + "\u2026" : row.description;
1220
+ return /* @__PURE__ */ jsxs3("button", { type: "button", class: "mine-row", onClick, children: [
1221
+ /* @__PURE__ */ jsxs3("div", { class: "mine-row-pills", children: [
1222
+ /* @__PURE__ */ jsx4("span", { class: statusClassName(row.status), children: strings[`status.${row.status}`] ?? row.status }),
1223
+ /* @__PURE__ */ jsx4("span", { class: typeClassName(), children: strings[`type.${row.feedback_type}`] }),
1224
+ /* @__PURE__ */ jsx4("span", { class: severityClassName(row.severity), children: strings[`severity.${row.severity}`] })
1225
+ ] }),
1226
+ /* @__PURE__ */ jsx4("div", { class: "mine-row-preview", children: preview }),
1227
+ /* @__PURE__ */ jsxs3("div", { class: "mine-row-meta", children: [
1228
+ /* @__PURE__ */ jsx4("span", { children: formatRelative(row.updated_at || row.created_at) }),
1229
+ row.comment_count > 0 && /* @__PURE__ */ jsxs3("span", { children: [
1230
+ "\xB7 ",
1231
+ repliesLabel(row.comment_count, strings)
1232
+ ] })
1233
+ ] })
1234
+ ] });
1235
+ }
1236
+
1237
+ // src/widget/MineList.tsx
1238
+ import { jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
1239
+ var POLL_MS = 3e4;
1240
+ function MineList({ api, externalId, strings, onSelect }) {
1241
+ const [rows, setRows] = useState3(null);
1242
+ const [error, setError] = useState3(null);
1243
+ const [refreshing, setRefreshing] = useState3(false);
1244
+ const mountedRef = useRef3(true);
1245
+ const fetchRows = async () => {
1246
+ setRefreshing(true);
1247
+ setError(null);
1248
+ try {
1249
+ const next = await api.listMine(externalId);
1250
+ if (!mountedRef.current) return;
1251
+ setRows(next);
1252
+ } catch (err) {
1253
+ if (!mountedRef.current) return;
1254
+ setError(err instanceof Error ? err.message : strings["mine.error"]);
1255
+ } finally {
1256
+ if (mountedRef.current) setRefreshing(false);
1257
+ }
1258
+ };
1065
1259
  useEffect3(() => {
1260
+ mountedRef.current = true;
1261
+ void fetchRows();
1262
+ const timer = setInterval(() => {
1263
+ void fetchRows();
1264
+ }, POLL_MS);
1265
+ return () => {
1266
+ mountedRef.current = false;
1267
+ clearInterval(timer);
1268
+ };
1269
+ }, [externalId]);
1270
+ const isEmpty = rows !== null && rows.length === 0;
1271
+ const isLoading = rows === null && !error;
1272
+ return /* @__PURE__ */ jsxs4("div", { class: "mine-list", children: [
1273
+ /* @__PURE__ */ jsxs4("div", { class: "mine-list-header", children: [
1274
+ /* @__PURE__ */ jsx5("h2", { children: strings["tab.mine"] }),
1275
+ /* @__PURE__ */ jsx5(
1276
+ "button",
1277
+ {
1278
+ type: "button",
1279
+ class: "btn",
1280
+ onClick: () => {
1281
+ void fetchRows();
1282
+ },
1283
+ disabled: refreshing,
1284
+ children: refreshing ? strings["mine.loading"] : strings["mine.refresh"]
1285
+ }
1286
+ )
1287
+ ] }),
1288
+ isLoading && /* @__PURE__ */ jsx5("div", { class: "mine-loading", children: strings["mine.loading"] }),
1289
+ error && /* @__PURE__ */ jsx5("div", { class: "error", children: error }),
1290
+ isEmpty && /* @__PURE__ */ jsxs4("div", { class: "mine-empty", children: [
1291
+ /* @__PURE__ */ jsx5("strong", { children: strings["mine.empty.title"] }),
1292
+ /* @__PURE__ */ jsx5("p", { children: strings["mine.empty.body"] })
1293
+ ] }),
1294
+ 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) }) })) })
1295
+ ] });
1296
+ }
1297
+
1298
+ // src/widget/Modal.tsx
1299
+ import { useEffect as useEffect4, useRef as useRef4 } from "preact/hooks";
1300
+ import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
1301
+ function Modal({ onDismiss, children, closeLabel = "Close" }) {
1302
+ const modalRef = useRef4(null);
1303
+ const previouslyFocused = useRef4(null);
1304
+ useEffect4(() => {
1066
1305
  previouslyFocused.current = document.activeElement;
1067
1306
  const onKey = (e) => {
1068
1307
  if (e.key !== "Escape") return;
@@ -1084,7 +1323,7 @@ function Modal({ onDismiss, children, closeLabel = "Close" }) {
1084
1323
  if (prev && typeof prev.focus === "function") prev.focus();
1085
1324
  };
1086
1325
  }, [onDismiss]);
1087
- return /* @__PURE__ */ jsx4(
1326
+ return /* @__PURE__ */ jsx6(
1088
1327
  "div",
1089
1328
  {
1090
1329
  class: "backdrop",
@@ -1092,8 +1331,8 @@ function Modal({ onDismiss, children, closeLabel = "Close" }) {
1092
1331
  onClick: (e) => {
1093
1332
  if (e.target === e.currentTarget) onDismiss();
1094
1333
  },
1095
- children: /* @__PURE__ */ jsxs3("div", { ref: modalRef, class: "modal", role: "dialog", "aria-modal": "true", children: [
1096
- /* @__PURE__ */ jsx4(
1334
+ children: /* @__PURE__ */ jsxs5("div", { ref: modalRef, class: "modal", role: "dialog", "aria-modal": "true", children: [
1335
+ /* @__PURE__ */ jsx6(
1097
1336
  "button",
1098
1337
  {
1099
1338
  type: "button",
@@ -1109,6 +1348,177 @@ function Modal({ onDismiss, children, closeLabel = "Close" }) {
1109
1348
  );
1110
1349
  }
1111
1350
 
1351
+ // src/widget/ReportDetailView.tsx
1352
+ import { useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "preact/hooks";
1353
+
1354
+ // src/widget/CommentBubble.tsx
1355
+ import { jsx as jsx7, jsxs as jsxs6 } from "preact/jsx-runtime";
1356
+ function CommentBubble({ comment, strings }) {
1357
+ const isMine = comment.is_mine;
1358
+ const isAgent = !isMine && comment.author_source === "mcp";
1359
+ const isSystem = !isMine && comment.author_source === "system";
1360
+ const variant = isAgent ? "mcp" : isSystem ? "system" : "staff";
1361
+ const labelKey = variant === "mcp" ? "detail.author.mcp" : variant === "system" ? "detail.author.system" : "detail.author.staff";
1362
+ const label = comment.author_label || strings[labelKey];
1363
+ return /* @__PURE__ */ jsxs6("div", { class: `comment-bubble ${isMine ? "is-mine" : "is-other"}`, children: [
1364
+ !isMine && label && /* @__PURE__ */ jsx7("div", { class: `comment-author comment-author--${variant}`, children: label }),
1365
+ /* @__PURE__ */ jsx7("div", { class: "comment-body", children: comment.body }),
1366
+ /* @__PURE__ */ jsx7("div", { class: "comment-time", children: formatTime(comment.created_at) })
1367
+ ] });
1368
+ }
1369
+ function formatTime(iso) {
1370
+ try {
1371
+ return new Date(iso).toLocaleString(void 0, {
1372
+ dateStyle: "short",
1373
+ timeStyle: "short"
1374
+ });
1375
+ } catch {
1376
+ return iso;
1377
+ }
1378
+ }
1379
+
1380
+ // src/widget/ReportDetailView.tsx
1381
+ import { jsx as jsx8, jsxs as jsxs7 } from "preact/jsx-runtime";
1382
+ var POLL_MS2 = 3e4;
1383
+ function ReportDetailView({
1384
+ api,
1385
+ externalId,
1386
+ reportId,
1387
+ strings,
1388
+ onBack
1389
+ }) {
1390
+ const [detail, setDetail] = useState4(null);
1391
+ const [error, setError] = useState4(null);
1392
+ const [composeBody, setComposeBody] = useState4("");
1393
+ const [sending, setSending] = useState4(false);
1394
+ const [closing, setClosing] = useState4(false);
1395
+ const mountedRef = useRef5(true);
1396
+ const fetchDetail = async () => {
1397
+ try {
1398
+ const next = await api.getReport(reportId, externalId);
1399
+ if (!mountedRef.current) return;
1400
+ setDetail(next);
1401
+ setError(null);
1402
+ } catch (err) {
1403
+ if (!mountedRef.current) return;
1404
+ setError(err instanceof Error ? err.message : "load_failed");
1405
+ }
1406
+ };
1407
+ useEffect5(() => {
1408
+ mountedRef.current = true;
1409
+ void fetchDetail();
1410
+ const timer = setInterval(() => {
1411
+ void fetchDetail();
1412
+ }, POLL_MS2);
1413
+ return () => {
1414
+ mountedRef.current = false;
1415
+ clearInterval(timer);
1416
+ };
1417
+ }, [reportId, externalId]);
1418
+ const handleSend = async () => {
1419
+ if (!composeBody.trim() || sending) return;
1420
+ setSending(true);
1421
+ try {
1422
+ const nonce = `${reportId}:${Date.now()}`;
1423
+ const created = await api.addComment(reportId, externalId, composeBody.trim(), nonce);
1424
+ if (!mountedRef.current) return;
1425
+ setComposeBody("");
1426
+ setDetail(
1427
+ (prev) => prev ? { ...prev, comments: appendComment(prev.comments, created) } : prev
1428
+ );
1429
+ void fetchDetail();
1430
+ } catch (err) {
1431
+ if (!mountedRef.current) return;
1432
+ setError(err instanceof Error ? err.message : "comment_failed");
1433
+ } finally {
1434
+ if (mountedRef.current) setSending(false);
1435
+ }
1436
+ };
1437
+ const handleClose = async () => {
1438
+ if (closing) return;
1439
+ setClosing(true);
1440
+ try {
1441
+ const next = await api.closeAsResolved(reportId, externalId);
1442
+ if (!mountedRef.current) return;
1443
+ setDetail(next);
1444
+ } catch (err) {
1445
+ if (!mountedRef.current) return;
1446
+ setError(err instanceof Error ? err.message : "close_failed");
1447
+ } finally {
1448
+ if (mountedRef.current) setClosing(false);
1449
+ }
1450
+ };
1451
+ if (!detail && !error) {
1452
+ return /* @__PURE__ */ jsx8("div", { class: "mine-loading", children: strings["mine.loading"] });
1453
+ }
1454
+ if (!detail) {
1455
+ return /* @__PURE__ */ jsx8("div", { class: "error", role: "alert", children: error });
1456
+ }
1457
+ const showCloseCta = detail.status === "awaiting_validation";
1458
+ return /* @__PURE__ */ jsxs7("div", { class: "report-detail", children: [
1459
+ /* @__PURE__ */ jsxs7("div", { class: "report-detail-header", children: [
1460
+ /* @__PURE__ */ jsxs7("button", { type: "button", class: "btn", onClick: onBack, children: [
1461
+ "\u2190 ",
1462
+ strings["detail.back"]
1463
+ ] }),
1464
+ /* @__PURE__ */ jsx8("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status })
1465
+ ] }),
1466
+ /* @__PURE__ */ jsxs7("div", { class: "report-detail-body", children: [
1467
+ /* @__PURE__ */ jsx8("p", { class: "report-detail-description", children: detail.description }),
1468
+ detail.screenshot_url && /* @__PURE__ */ jsx8(
1469
+ "a",
1470
+ {
1471
+ class: "report-detail-screenshot",
1472
+ href: detail.screenshot_url,
1473
+ target: "_blank",
1474
+ rel: "noopener noreferrer",
1475
+ children: /* @__PURE__ */ jsx8("img", { src: detail.screenshot_url, alt: "", loading: "lazy" })
1476
+ }
1477
+ ),
1478
+ /* @__PURE__ */ jsx8("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
1479
+ 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 }) })) }),
1480
+ /* @__PURE__ */ jsxs7("div", { class: "report-compose", children: [
1481
+ /* @__PURE__ */ jsx8(
1482
+ "textarea",
1483
+ {
1484
+ value: composeBody,
1485
+ placeholder: strings["detail.compose_placeholder"],
1486
+ onInput: (e) => setComposeBody(e.target.value),
1487
+ disabled: sending
1488
+ }
1489
+ ),
1490
+ /* @__PURE__ */ jsxs7("div", { class: "report-compose-actions", children: [
1491
+ showCloseCta && /* @__PURE__ */ jsx8(
1492
+ "button",
1493
+ {
1494
+ type: "button",
1495
+ class: "btn",
1496
+ onClick: handleClose,
1497
+ disabled: closing,
1498
+ children: closing ? strings["detail.close_busy"] : strings["detail.close_cta"]
1499
+ }
1500
+ ),
1501
+ /* @__PURE__ */ jsx8(
1502
+ "button",
1503
+ {
1504
+ type: "button",
1505
+ class: "btn btn--primary",
1506
+ onClick: handleSend,
1507
+ disabled: !composeBody.trim() || sending,
1508
+ children: sending ? strings["detail.compose_sending"] : strings["detail.compose_send"]
1509
+ }
1510
+ )
1511
+ ] })
1512
+ ] }),
1513
+ error && /* @__PURE__ */ jsx8("div", { class: "error", children: error })
1514
+ ] })
1515
+ ] });
1516
+ }
1517
+ function appendComment(current, next) {
1518
+ if (current.some((c) => c.id === next.id)) return current;
1519
+ return [...current, next];
1520
+ }
1521
+
1112
1522
  // src/widget/styles.ts
1113
1523
  var WIDGET_STYLES = `
1114
1524
  :host {
@@ -1494,10 +1904,232 @@ var WIDGET_STYLES = `
1494
1904
  padding: 10px 14px;
1495
1905
  border-top: 1px solid var(--mfb-border);
1496
1906
  }
1907
+
1908
+ /* ---- v0.7: tabs + reader UI -------------------------------------- */
1909
+
1910
+ .tab-strip {
1911
+ display: flex;
1912
+ gap: 4px;
1913
+ border-bottom: 1px solid var(--mfb-border);
1914
+ margin: 0 -4px 4px;
1915
+ padding: 0 4px;
1916
+ }
1917
+ .tab-button {
1918
+ appearance: none;
1919
+ background: transparent;
1920
+ border: 0;
1921
+ border-bottom: 2px solid transparent;
1922
+ padding: 8px 12px;
1923
+ font: inherit;
1924
+ font-size: 13px;
1925
+ font-weight: 500;
1926
+ color: var(--mfb-text-muted);
1927
+ cursor: pointer;
1928
+ }
1929
+ .tab-button:hover { color: var(--mfb-text); }
1930
+ .tab-button.is-active {
1931
+ color: var(--mfb-text);
1932
+ border-bottom-color: var(--mfb-accent);
1933
+ }
1934
+ .tab-button[aria-selected="true"] { font-weight: 600; }
1935
+
1936
+ .mine-list { display: flex; flex-direction: column; gap: 10px; }
1937
+ .mine-list-header {
1938
+ display: flex;
1939
+ align-items: center;
1940
+ justify-content: space-between;
1941
+ }
1942
+ .mine-list-header h2 { margin: 0; font-size: 15px; font-weight: 600; }
1943
+ .mine-loading { color: var(--mfb-text-muted); font-size: 13px; }
1944
+ .mine-empty {
1945
+ text-align: center;
1946
+ padding: 24px 12px;
1947
+ color: var(--mfb-text-muted);
1948
+ font-size: 13px;
1949
+ }
1950
+ .mine-empty strong { display: block; color: var(--mfb-text); margin-bottom: 4px; }
1951
+
1952
+ .mine-rows {
1953
+ list-style: none;
1954
+ margin: 0;
1955
+ padding: 0;
1956
+ display: flex;
1957
+ flex-direction: column;
1958
+ gap: 6px;
1959
+ max-height: 380px;
1960
+ overflow-y: auto;
1961
+ }
1962
+ .mine-row {
1963
+ appearance: none;
1964
+ text-align: left;
1965
+ background: var(--mfb-surface);
1966
+ border: 1px solid var(--mfb-border);
1967
+ border-radius: var(--mfb-radius);
1968
+ padding: 10px 12px;
1969
+ font: inherit;
1970
+ color: inherit;
1971
+ cursor: pointer;
1972
+ display: flex;
1973
+ flex-direction: column;
1974
+ gap: 4px;
1975
+ width: 100%;
1976
+ transition: border-color 120ms ease, background 120ms ease;
1977
+ }
1978
+ .mine-row:hover { border-color: var(--mfb-text-muted); }
1979
+ .mine-row:focus-visible {
1980
+ outline: 2px solid var(--mfb-accent);
1981
+ outline-offset: 2px;
1982
+ }
1983
+ .mine-row-pills { display: flex; gap: 4px; flex-wrap: wrap; }
1984
+ .mine-row-preview {
1985
+ font-size: 13px;
1986
+ color: var(--mfb-text);
1987
+ display: -webkit-box;
1988
+ -webkit-line-clamp: 2;
1989
+ -webkit-box-orient: vertical;
1990
+ overflow: hidden;
1991
+ }
1992
+ .mine-row-meta {
1993
+ display: flex;
1994
+ gap: 6px;
1995
+ font-size: 11px;
1996
+ color: var(--mfb-text-muted);
1997
+ }
1998
+
1999
+ .pill {
2000
+ display: inline-block;
2001
+ font-size: 10px;
2002
+ font-weight: 600;
2003
+ text-transform: uppercase;
2004
+ letter-spacing: 0.04em;
2005
+ padding: 2px 8px;
2006
+ border-radius: 999px;
2007
+ border: 1px solid transparent;
2008
+ }
2009
+ .pill-status { background: #eff6ff; color: #1e40af; border-color: #dbeafe; }
2010
+ .pill-status--in_progress { background: #fffbeb; color: #92400e; border-color: #fde68a; }
2011
+ .pill-status--awaiting_validation { background: #faf5ff; color: #6b21a8; border-color: #e9d5ff; }
2012
+ .pill-status--closed { background: #ecfdf5; color: #065f46; border-color: #a7f3d0; }
2013
+ .pill-status--rejected, .pill-status--wontfix, .pill-status--duplicate { background: #f3f4f6; color: #374151; border-color: #e5e7eb; }
2014
+ .pill-type { background: var(--mfb-surface); color: var(--mfb-text-muted); border-color: var(--mfb-border); }
2015
+ .pill-severity { background: var(--mfb-surface); color: var(--mfb-text-muted); border-color: var(--mfb-border); }
2016
+ .pill-severity--blocker { background: #fef2f2; color: #991b1b; border-color: #fecaca; }
2017
+ .pill-severity--high { background: #fff7ed; color: #9a3412; border-color: #fed7aa; }
2018
+ .pill-severity--medium { background: #fefce8; color: #854d0e; border-color: #fef08a; }
2019
+ .pill-severity--low { background: var(--mfb-surface); color: var(--mfb-text-muted); }
2020
+
2021
+ .report-detail { display: flex; flex-direction: column; gap: 12px; }
2022
+ .report-detail-header {
2023
+ display: flex;
2024
+ align-items: center;
2025
+ justify-content: space-between;
2026
+ gap: 8px;
2027
+ }
2028
+ .report-detail-body { display: flex; flex-direction: column; gap: 10px; }
2029
+ .report-detail-description {
2030
+ font-size: 14px;
2031
+ white-space: pre-wrap;
2032
+ margin: 0;
2033
+ }
2034
+ .report-detail-screenshot {
2035
+ display: block;
2036
+ border: 1px solid var(--mfb-border);
2037
+ border-radius: var(--mfb-radius);
2038
+ overflow: hidden;
2039
+ background: #1a1a1a;
2040
+ }
2041
+ .report-detail-screenshot img {
2042
+ display: block;
2043
+ width: 100%;
2044
+ max-height: 200px;
2045
+ object-fit: contain;
2046
+ }
2047
+ .report-detail-section {
2048
+ font-size: 12px;
2049
+ text-transform: uppercase;
2050
+ letter-spacing: 0.04em;
2051
+ color: var(--mfb-text-muted);
2052
+ margin: 8px 0 4px;
2053
+ font-weight: 600;
2054
+ }
2055
+ .report-detail-empty {
2056
+ font-size: 12px;
2057
+ color: var(--mfb-text-muted);
2058
+ margin: 0;
2059
+ }
2060
+
2061
+ .report-comments {
2062
+ list-style: none;
2063
+ margin: 0;
2064
+ padding: 0;
2065
+ display: flex;
2066
+ flex-direction: column;
2067
+ gap: 6px;
2068
+ max-height: 300px;
2069
+ overflow-y: auto;
2070
+ }
2071
+
2072
+ .comment-bubble {
2073
+ padding: 8px 10px;
2074
+ border-radius: 12px;
2075
+ max-width: 88%;
2076
+ font-size: 13px;
2077
+ line-height: 1.4;
2078
+ }
2079
+ .comment-bubble.is-mine {
2080
+ align-self: flex-end;
2081
+ background: var(--mfb-accent);
2082
+ color: var(--mfb-accent-contrast);
2083
+ }
2084
+ .comment-bubble.is-other {
2085
+ align-self: flex-start;
2086
+ background: var(--mfb-surface);
2087
+ border: 1px solid var(--mfb-border);
2088
+ color: var(--mfb-text);
2089
+ }
2090
+ .comment-author {
2091
+ font-size: 10px;
2092
+ text-transform: uppercase;
2093
+ letter-spacing: 0.04em;
2094
+ font-weight: 600;
2095
+ margin-bottom: 2px;
2096
+ }
2097
+ .comment-author--mcp { color: #6b21a8; }
2098
+ .comment-author--staff { color: #1e40af; }
2099
+ .comment-author--system { color: var(--mfb-text-muted); }
2100
+ .comment-body { white-space: pre-wrap; }
2101
+ .comment-time {
2102
+ font-size: 10px;
2103
+ margin-top: 4px;
2104
+ opacity: 0.7;
2105
+ }
2106
+
2107
+ .report-compose {
2108
+ display: flex;
2109
+ flex-direction: column;
2110
+ gap: 6px;
2111
+ margin-top: 4px;
2112
+ }
2113
+ .report-compose textarea {
2114
+ font: inherit;
2115
+ font-size: 13px;
2116
+ padding: 8px 10px;
2117
+ border: 1px solid var(--mfb-border);
2118
+ border-radius: var(--mfb-radius);
2119
+ background: var(--mfb-surface);
2120
+ color: inherit;
2121
+ min-height: 64px;
2122
+ resize: vertical;
2123
+ }
2124
+ .report-compose-actions {
2125
+ display: flex;
2126
+ justify-content: flex-end;
2127
+ gap: 6px;
2128
+ }
1497
2129
  `;
1498
2130
 
1499
2131
  // src/widget/mount.tsx
1500
- import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
2132
+ import { Fragment, jsx as jsx9, jsxs as jsxs8 } from "preact/jsx-runtime";
1501
2133
  function mountWidget(options) {
1502
2134
  const shadow = options.host.attachShadow({ mode: "open" });
1503
2135
  const style = document.createElement("style");
@@ -1505,45 +2137,113 @@ function mountWidget(options) {
1505
2137
  shadow.appendChild(style);
1506
2138
  const mountPoint = document.createElement("div");
1507
2139
  shadow.appendChild(mountPoint);
1508
- let currentState = { open: false, status: "idle" };
2140
+ let currentState = { open: false, status: "idle", tab: "send" };
1509
2141
  function rerender(state) {
1510
2142
  currentState = state;
1511
2143
  render(h(Root, { state }), mountPoint);
1512
2144
  }
2145
+ function clearSelected(s) {
2146
+ const { selectedReportId: _drop, ...rest } = s;
2147
+ void _drop;
2148
+ return rest;
2149
+ }
1513
2150
  function Root({ state }) {
1514
2151
  const handleSubmit = useCallback(async (values) => {
1515
- rerender({ open: true, status: "submitting" });
2152
+ rerender({ ...currentState, status: "submitting" });
1516
2153
  try {
1517
2154
  await options.onSubmit(values);
1518
- rerender({ open: true, status: "success" });
1519
- setTimeout(() => rerender({ open: false, status: "idle" }), 1200);
2155
+ rerender({ ...currentState, status: "success" });
2156
+ setTimeout(
2157
+ () => rerender({
2158
+ ...currentState,
2159
+ open: false,
2160
+ status: "idle",
2161
+ // After a successful submit, jump the user to "My reports" so
2162
+ // they immediately see their just-filed report in the list
2163
+ // (and the conversation that's about to start).
2164
+ tab: options.api ? "mine" : "send"
2165
+ }),
2166
+ 1200
2167
+ );
1520
2168
  } catch (err) {
1521
- rerender({ open: true, status: "error", error: err instanceof Error ? err.message : String(err) });
2169
+ rerender({
2170
+ ...currentState,
2171
+ status: "error",
2172
+ error: err instanceof Error ? err.message : String(err)
2173
+ });
1522
2174
  }
1523
2175
  }, []);
1524
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
1525
- options.showFAB && /* @__PURE__ */ jsx5(
2176
+ const externalId = options.getExternalId?.();
2177
+ const fabVisible = options.showFAB && (options.getExternalId === void 0 ? true : Boolean(externalId));
2178
+ const showMineTab = Boolean(options.api && externalId);
2179
+ return /* @__PURE__ */ jsxs8(Fragment, { children: [
2180
+ fabVisible && /* @__PURE__ */ jsx9(
1526
2181
  Fab,
1527
2182
  {
1528
2183
  label: options.strings["fab.label"],
1529
2184
  onClick: () => rerender({ ...currentState, open: true })
1530
2185
  }
1531
2186
  ),
1532
- state.open && /* @__PURE__ */ jsx5(
2187
+ state.open && /* @__PURE__ */ jsxs8(
1533
2188
  Modal,
1534
2189
  {
1535
- onDismiss: () => rerender({ open: false, status: "idle" }),
2190
+ onDismiss: () => rerender(clearSelected({ ...currentState, open: false, status: "idle" })),
1536
2191
  closeLabel: options.strings["form.close"],
1537
- children: /* @__PURE__ */ jsx5(
1538
- Form,
1539
- {
1540
- strings: options.strings,
1541
- onSubmit: handleSubmit,
1542
- onCancel: () => rerender({ open: false, status: "idle" }),
1543
- status: state.status,
1544
- ...state.error !== void 0 && { errorMessage: state.error }
1545
- }
1546
- )
2192
+ children: [
2193
+ showMineTab && /* @__PURE__ */ jsxs8("div", { class: "tab-strip", role: "tablist", children: [
2194
+ /* @__PURE__ */ jsx9(
2195
+ "button",
2196
+ {
2197
+ type: "button",
2198
+ role: "tab",
2199
+ "aria-selected": state.tab === "send",
2200
+ class: `tab-button ${state.tab === "send" ? "is-active" : ""}`,
2201
+ onClick: () => rerender(clearSelected({ ...currentState, tab: "send" })),
2202
+ children: options.strings["tab.send"]
2203
+ }
2204
+ ),
2205
+ /* @__PURE__ */ jsx9(
2206
+ "button",
2207
+ {
2208
+ type: "button",
2209
+ role: "tab",
2210
+ "aria-selected": state.tab === "mine",
2211
+ class: `tab-button ${state.tab === "mine" ? "is-active" : ""}`,
2212
+ onClick: () => rerender(clearSelected({ ...currentState, tab: "mine" })),
2213
+ children: options.strings["tab.mine"]
2214
+ }
2215
+ )
2216
+ ] }),
2217
+ state.tab === "send" && /* @__PURE__ */ jsx9(
2218
+ Form,
2219
+ {
2220
+ strings: options.strings,
2221
+ onSubmit: handleSubmit,
2222
+ onCancel: () => rerender({ ...currentState, open: false, status: "idle" }),
2223
+ status: state.status,
2224
+ ...state.error !== void 0 && { errorMessage: state.error }
2225
+ }
2226
+ ),
2227
+ state.tab === "mine" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx9(
2228
+ MineList,
2229
+ {
2230
+ api: options.api,
2231
+ externalId,
2232
+ strings: options.strings,
2233
+ onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
2234
+ }
2235
+ ),
2236
+ state.tab === "mine" && options.api && externalId && state.selectedReportId && /* @__PURE__ */ jsx9(
2237
+ ReportDetailView,
2238
+ {
2239
+ api: options.api,
2240
+ externalId,
2241
+ reportId: state.selectedReportId,
2242
+ strings: options.strings,
2243
+ onBack: () => rerender(clearSelected({ ...currentState }))
2244
+ }
2245
+ )
2246
+ ]
1547
2247
  }
1548
2248
  )
1549
2249
  ] });
@@ -1559,6 +2259,9 @@ function mountWidget(options) {
1559
2259
  dispose() {
1560
2260
  render(null, mountPoint);
1561
2261
  options.host.innerHTML = "";
2262
+ },
2263
+ notifyIdentityChanged() {
2264
+ rerender({ ...currentState });
1562
2265
  }
1563
2266
  };
1564
2267
  }
@@ -1613,6 +2316,15 @@ function createFeedback(config) {
1613
2316
  };
1614
2317
  if (screenshot) payload.screenshot = screenshot;
1615
2318
  if (values.synthetic) payload.synthetic = true;
2319
+ if (user?.id !== void 0 && user.id !== null && user.id !== "") {
2320
+ payload.user = {
2321
+ // The host can pass `id` as a string or number; the backend
2322
+ // stores it as an opaque string. Coerce here to a stable shape.
2323
+ id: String(user.id),
2324
+ ...user.email !== void 0 && { email: user.email },
2325
+ ...user.name !== void 0 && { name: user.name }
2326
+ };
2327
+ }
1616
2328
  let finalPayload = payload;
1617
2329
  for (const t of transformers) finalPayload = await t(finalPayload);
1618
2330
  try {
@@ -1632,7 +2344,12 @@ function createFeedback(config) {
1632
2344
  showFAB: config.showFAB ?? true,
1633
2345
  onSubmit: async (values) => {
1634
2346
  await buildAndSubmit(values);
1635
- }
2347
+ },
2348
+ api,
2349
+ // Keep this a callback (not a snapshot) so the mount picks up identity
2350
+ // changes that happen after createFeedback() — `notifyIdentityChanged()`
2351
+ // is the trigger for a re-render.
2352
+ getExternalId: () => user?.id !== void 0 && user.id !== null && user.id !== "" ? String(user.id) : void 0
1636
2353
  });
1637
2354
  const instance = {
1638
2355
  show() {
@@ -1656,6 +2373,7 @@ function createFeedback(config) {
1656
2373
  },
1657
2374
  identify(u) {
1658
2375
  user = u;
2376
+ handle.notifyIdentityChanged();
1659
2377
  },
1660
2378
  setMetadata(kv) {
1661
2379
  metadata = { ...metadata, ...kv };
@@ -1680,4 +2398,4 @@ function createFeedback(config) {
1680
2398
  export {
1681
2399
  createFeedback
1682
2400
  };
1683
- //# sourceMappingURL=chunk-KXLX4IGN.mjs.map
2401
+ //# sourceMappingURL=chunk-W6JAJT2U.mjs.map