@mhosaic/feedback 0.6.3 → 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
@@ -396,7 +467,35 @@ 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.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"
400
499
  };
401
500
  var FRENCH_STRINGS = {
402
501
  "fab.label": "Envoyer un commentaire",
@@ -441,7 +540,35 @@ var FRENCH_STRINGS = {
441
540
  "annotator.count_suffix": "annotations",
442
541
  "annotator.loading": "Chargement\u2026",
443
542
  "annotator.apply": "Appliquer",
444
- "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"
445
572
  };
446
573
  var LOCALE_PACKS = {
447
574
  fr: FRENCH_STRINGS
@@ -1058,13 +1185,123 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1058
1185
  ] });
1059
1186
  }
1060
1187
 
1061
- // src/widget/Modal.tsx
1062
- 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
1063
1192
  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);
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
+ };
1067
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(() => {
1068
1305
  previouslyFocused.current = document.activeElement;
1069
1306
  const onKey = (e) => {
1070
1307
  if (e.key !== "Escape") return;
@@ -1086,7 +1323,7 @@ function Modal({ onDismiss, children, closeLabel = "Close" }) {
1086
1323
  if (prev && typeof prev.focus === "function") prev.focus();
1087
1324
  };
1088
1325
  }, [onDismiss]);
1089
- return /* @__PURE__ */ jsx4(
1326
+ return /* @__PURE__ */ jsx6(
1090
1327
  "div",
1091
1328
  {
1092
1329
  class: "backdrop",
@@ -1094,8 +1331,8 @@ function Modal({ onDismiss, children, closeLabel = "Close" }) {
1094
1331
  onClick: (e) => {
1095
1332
  if (e.target === e.currentTarget) onDismiss();
1096
1333
  },
1097
- children: /* @__PURE__ */ jsxs3("div", { ref: modalRef, class: "modal", role: "dialog", "aria-modal": "true", children: [
1098
- /* @__PURE__ */ jsx4(
1334
+ children: /* @__PURE__ */ jsxs5("div", { ref: modalRef, class: "modal", role: "dialog", "aria-modal": "true", children: [
1335
+ /* @__PURE__ */ jsx6(
1099
1336
  "button",
1100
1337
  {
1101
1338
  type: "button",
@@ -1111,6 +1348,177 @@ function Modal({ onDismiss, children, closeLabel = "Close" }) {
1111
1348
  );
1112
1349
  }
1113
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
+
1114
1522
  // src/widget/styles.ts
1115
1523
  var WIDGET_STYLES = `
1116
1524
  :host {
@@ -1496,10 +1904,232 @@ var WIDGET_STYLES = `
1496
1904
  padding: 10px 14px;
1497
1905
  border-top: 1px solid var(--mfb-border);
1498
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
+ }
1499
2129
  `;
1500
2130
 
1501
2131
  // src/widget/mount.tsx
1502
- 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";
1503
2133
  function mountWidget(options) {
1504
2134
  const shadow = options.host.attachShadow({ mode: "open" });
1505
2135
  const style = document.createElement("style");
@@ -1507,45 +2137,113 @@ function mountWidget(options) {
1507
2137
  shadow.appendChild(style);
1508
2138
  const mountPoint = document.createElement("div");
1509
2139
  shadow.appendChild(mountPoint);
1510
- let currentState = { open: false, status: "idle" };
2140
+ let currentState = { open: false, status: "idle", tab: "send" };
1511
2141
  function rerender(state) {
1512
2142
  currentState = state;
1513
2143
  render(h(Root, { state }), mountPoint);
1514
2144
  }
2145
+ function clearSelected(s) {
2146
+ const { selectedReportId: _drop, ...rest } = s;
2147
+ void _drop;
2148
+ return rest;
2149
+ }
1515
2150
  function Root({ state }) {
1516
2151
  const handleSubmit = useCallback(async (values) => {
1517
- rerender({ open: true, status: "submitting" });
2152
+ rerender({ ...currentState, status: "submitting" });
1518
2153
  try {
1519
2154
  await options.onSubmit(values);
1520
- rerender({ open: true, status: "success" });
1521
- 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
+ );
1522
2168
  } catch (err) {
1523
- 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
+ });
1524
2174
  }
1525
2175
  }, []);
1526
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
1527
- 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(
1528
2181
  Fab,
1529
2182
  {
1530
2183
  label: options.strings["fab.label"],
1531
2184
  onClick: () => rerender({ ...currentState, open: true })
1532
2185
  }
1533
2186
  ),
1534
- state.open && /* @__PURE__ */ jsx5(
2187
+ state.open && /* @__PURE__ */ jsxs8(
1535
2188
  Modal,
1536
2189
  {
1537
- onDismiss: () => rerender({ open: false, status: "idle" }),
2190
+ onDismiss: () => rerender(clearSelected({ ...currentState, open: false, status: "idle" })),
1538
2191
  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
- )
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
+ ]
1549
2247
  }
1550
2248
  )
1551
2249
  ] });
@@ -1561,6 +2259,9 @@ function mountWidget(options) {
1561
2259
  dispose() {
1562
2260
  render(null, mountPoint);
1563
2261
  options.host.innerHTML = "";
2262
+ },
2263
+ notifyIdentityChanged() {
2264
+ rerender({ ...currentState });
1564
2265
  }
1565
2266
  };
1566
2267
  }
@@ -1615,6 +2316,15 @@ function createFeedback(config) {
1615
2316
  };
1616
2317
  if (screenshot) payload.screenshot = screenshot;
1617
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
+ }
1618
2328
  let finalPayload = payload;
1619
2329
  for (const t of transformers) finalPayload = await t(finalPayload);
1620
2330
  try {
@@ -1634,7 +2344,12 @@ function createFeedback(config) {
1634
2344
  showFAB: config.showFAB ?? true,
1635
2345
  onSubmit: async (values) => {
1636
2346
  await buildAndSubmit(values);
1637
- }
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
1638
2353
  });
1639
2354
  const instance = {
1640
2355
  show() {
@@ -1658,6 +2373,7 @@ function createFeedback(config) {
1658
2373
  },
1659
2374
  identify(u) {
1660
2375
  user = u;
2376
+ handle.notifyIdentityChanged();
1661
2377
  },
1662
2378
  setMetadata(kv) {
1663
2379
  metadata = { ...metadata, ...kv };
@@ -1682,4 +2398,4 @@ function createFeedback(config) {
1682
2398
  export {
1683
2399
  createFeedback
1684
2400
  };
1685
- //# sourceMappingURL=chunk-3RIR3JHF.mjs.map
2401
+ //# sourceMappingURL=chunk-W6JAJT2U.mjs.map