spaps-issue-reporting-react 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  FloatingIssueReportButton: () => FloatingIssueReportButton,
34
+ IssueReportMessageThread: () => IssueReportMessageThread,
34
35
  IssueReportingPageConfig: () => IssueReportingPageConfig,
35
36
  IssueReportingProvider: () => IssueReportingProvider,
36
37
  ReportModeContext: () => ReportModeContext,
@@ -44,9 +45,13 @@ __export(index_exports, {
44
45
  getIssueStatusClassName: () => getIssueStatusClassName,
45
46
  isClosedIssueStatus: () => isClosedIssueStatus,
46
47
  isOpenIssueStatus: () => isOpenIssueStatus,
48
+ isReporterMessageConflict: () => isReporterMessageConflict,
47
49
  issueReportingKeys: () => issueReportingKeys,
50
+ selectReporterVisibleMessages: () => selectReporterVisibleMessages,
48
51
  useIssueReporting: () => useIssueReporting,
49
52
  useIssueReportingHistory: () => useIssueReportingHistory,
53
+ useIssueReportingMessageMutation: () => useIssueReportingMessageMutation,
54
+ useIssueReportingMessages: () => useIssueReportingMessages,
50
55
  useIssueReportingMutations: () => useIssueReportingMutations,
51
56
  useIssueReportingStatus: () => useIssueReportingStatus,
52
57
  useReportMode: () => useReportMode
@@ -79,6 +84,13 @@ var import_jsx_runtime = require("react/jsx-runtime");
79
84
  var LIST_LIMIT = 200;
80
85
  var NOTE_MIN_LENGTH = 10;
81
86
  var NOTE_MAX_LENGTH = 2e3;
87
+ var ATTACHMENT_MAX_COUNT = 5;
88
+ var ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
89
+ var ATTACHMENT_ALLOWED_MIMES = /* @__PURE__ */ new Set([
90
+ "image/png",
91
+ "image/jpeg",
92
+ "image/webp"
93
+ ]);
82
94
  var DEFAULT_INPUT_MODES = ["text"];
83
95
  var DEFAULT_VOICE_PROVIDER = "elevenlabs_scribe_realtime";
84
96
  var DEFAULT_VOICE_MODEL_ID = "scribe_v2_realtime";
@@ -151,13 +163,31 @@ var defaultIssueReportingCopy = {
151
163
  retryAction: "Retry",
152
164
  originHumanLabel: "Human",
153
165
  originMachineLabel: "Machine",
154
- machineOriginFallback: "system"
166
+ machineOriginFallback: "system",
167
+ threadTitle: "Conversation",
168
+ threadDescription: "Messages from the support team about this report, and your replies.",
169
+ threadLoading: "Loading conversation...",
170
+ threadLoadFailed: "Failed to load the conversation",
171
+ threadEmpty: "No messages on this report yet.",
172
+ threadNeedsResponseBadge: "Needs your response",
173
+ threadAuthorOperator: "Support",
174
+ threadAuthorReporter: "You",
175
+ threadKindClarificationRequest: "Question",
176
+ threadKindReporterResponse: "Your reply",
177
+ threadKindFinalResponse: "Resolution",
178
+ threadResponsePlaceholder: "Write your response...",
179
+ threadResponseLabel: "Respond to the support team",
180
+ threadResponseSubmitAction: "Send response",
181
+ threadResponseSubmittingAction: "Sending...",
182
+ threadResponseConflict: "That response was already sent, or the message changed. Refresh and try again.",
183
+ threadResponseFailed: "Failed to send your response"
155
184
  };
156
185
  var issueReportingKeys = {
157
186
  all: ["spaps-issue-reporting"],
158
187
  status: (scope) => [...issueReportingKeys.all, "status", scope],
159
188
  history: (scope) => [...issueReportingKeys.all, "history", scope],
160
- detail: (issueReportId) => [...issueReportingKeys.all, "detail", issueReportId]
189
+ detail: (issueReportId) => [...issueReportingKeys.all, "detail", issueReportId],
190
+ messages: (issueReportId) => [...issueReportingKeys.all, "messages", issueReportId]
161
191
  };
162
192
  function resolvePageUrl(getPageUrl) {
163
193
  if (getPageUrl) {
@@ -327,6 +357,27 @@ function getEntryPointClassName(state) {
327
357
  }
328
358
  return "text-slate-500";
329
359
  }
360
+ function selectReporterVisibleMessages(messages) {
361
+ return messages.filter(
362
+ (message) => message.reporter_visible && message.state === "active"
363
+ ).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
364
+ }
365
+ function isReporterMessageConflict(error) {
366
+ if (!error || typeof error !== "object") {
367
+ return false;
368
+ }
369
+ const record = error;
370
+ const code = record.code ?? record.error?.code;
371
+ if (typeof code === "string" && code === "ISSUE_REPORT_MESSAGE_CONFLICT") {
372
+ return true;
373
+ }
374
+ const status = record.status ?? record.statusCode ?? record.status_code ?? void 0;
375
+ if (status === 409) {
376
+ return true;
377
+ }
378
+ const message = error instanceof Error ? error.message : typeof record.message === "string" ? record.message : "";
379
+ return /ISSUE_REPORT_MESSAGE_CONFLICT/i.test(message) || /\b409\b/.test(message);
380
+ }
330
381
  function getIssueNoteLengthMessage(note, copy) {
331
382
  if (note.length < NOTE_MIN_LENGTH) {
332
383
  return `${NOTE_MIN_LENGTH - note.length} ${copy.noteMinimumSuffix}`;
@@ -421,18 +472,26 @@ function useIssueReportingMutations() {
421
472
  const updateMutation = (0, import_react_query.useMutation)({
422
473
  mutationFn: ({
423
474
  issueReportId,
424
- note
425
- }) => client.issueReporting.update(issueReportId, { note }),
475
+ note,
476
+ add_attachment_ids,
477
+ remove_attachment_ids
478
+ }) => client.issueReporting.update(issueReportId, {
479
+ note,
480
+ add_attachment_ids,
481
+ remove_attachment_ids
482
+ }),
426
483
  onSuccess: invalidateAll
427
484
  });
428
485
  const replyMutation = (0, import_react_query.useMutation)({
429
486
  mutationFn: ({
430
487
  issueReportId,
431
488
  note,
432
- reporterRoleHint
489
+ reporterRoleHint,
490
+ attachment_ids
433
491
  }) => client.issueReporting.reply(issueReportId, {
434
492
  note,
435
- reporter_role_hint: reporterRoleHint
493
+ reporter_role_hint: reporterRoleHint,
494
+ attachment_ids
436
495
  }),
437
496
  onSuccess: invalidateAll
438
497
  });
@@ -442,6 +501,39 @@ function useIssueReportingMutations() {
442
501
  replyMutation
443
502
  };
444
503
  }
504
+ function useIssueReportingMessages(issueReportId) {
505
+ const { client, isEligible } = useIssueReporting();
506
+ const listMessages = client.issueReporting.listMessages;
507
+ return (0, import_react_query.useQuery)({
508
+ queryKey: issueReportingKeys.messages(issueReportId ?? "none"),
509
+ queryFn: () => listMessages(issueReportId),
510
+ enabled: isEligible && Boolean(issueReportId) && Boolean(listMessages),
511
+ retry: false
512
+ });
513
+ }
514
+ function useIssueReportingMessageMutation(issueReportId) {
515
+ const queryClient = (0, import_react_query.useQueryClient)();
516
+ const { client } = useIssueReporting();
517
+ return (0, import_react_query.useMutation)({
518
+ mutationFn: (payload) => {
519
+ const submit = client.issueReporting.submitMessage;
520
+ if (!submit || !issueReportId) {
521
+ return Promise.reject(
522
+ new Error("This client does not support submitting messages.")
523
+ );
524
+ }
525
+ return submit(issueReportId, payload);
526
+ },
527
+ onSuccess: async () => {
528
+ if (!issueReportId) {
529
+ return;
530
+ }
531
+ await queryClient.invalidateQueries({
532
+ queryKey: issueReportingKeys.messages(issueReportId)
533
+ });
534
+ }
535
+ });
536
+ }
445
537
  function IssueReportingProvider({
446
538
  client,
447
539
  isEligible,
@@ -471,6 +563,7 @@ function IssueReportingProvider({
471
563
  const [scope, setScope] = (0, import_react2.useState)(
472
564
  () => resolveInitialScope(defaultScope, allowTenantScope)
473
565
  );
566
+ const [needsResponseMap, setNeedsResponseMap] = (0, import_react2.useState)({});
474
567
  const [pageConfigs, setPageConfigs] = (0, import_react2.useState)([]);
475
568
  const [registeredTargets, setRegisteredTargets] = (0, import_react2.useState)(
476
569
  []
@@ -495,6 +588,21 @@ function IssueReportingProvider({
495
588
  (0, import_react2.useEffect)(() => {
496
589
  setScope(resolveInitialScope(defaultScope, allowTenantScope));
497
590
  }, [allowTenantScope, defaultScope]);
591
+ const setNeedsResponse = (0, import_react2.useCallback)(
592
+ (issueReportId, needsResponse) => {
593
+ setNeedsResponseMap((current) => {
594
+ if (Boolean(current[issueReportId]) === needsResponse) {
595
+ return current;
596
+ }
597
+ return { ...current, [issueReportId]: needsResponse };
598
+ });
599
+ },
600
+ []
601
+ );
602
+ const needsResponseIssueIds = (0, import_react2.useMemo)(
603
+ () => Object.entries(needsResponseMap).filter(([, needs]) => needs).map(([id]) => id),
604
+ [needsResponseMap]
605
+ );
498
606
  const closePopover = (0, import_react2.useCallback)(() => {
499
607
  setIsPopoverOpen(false);
500
608
  }, []);
@@ -682,7 +790,9 @@ function IssueReportingProvider({
682
790
  createMode,
683
791
  inputModes: resolvedInputModes,
684
792
  defaultInputMode: resolvedDefaultInputMode,
685
- voice: resolvedVoiceConfig
793
+ voice: resolvedVoiceConfig,
794
+ needsResponseIssueIds,
795
+ setNeedsResponse
686
796
  }),
687
797
  [
688
798
  allowTenantScope,
@@ -698,6 +808,7 @@ function IssueReportingProvider({
698
808
  isReportMode,
699
809
  mergedCopy,
700
810
  modalState,
811
+ needsResponseIssueIds,
701
812
  openExistingIssueModal,
702
813
  openPageIssueModal,
703
814
  openPopover,
@@ -709,6 +820,7 @@ function IssueReportingProvider({
709
820
  resolvedVoiceConfig,
710
821
  scope,
711
822
  selectPanel,
823
+ setNeedsResponse,
712
824
  startNewIssue
713
825
  ]
714
826
  );
@@ -988,6 +1100,7 @@ function IssueReportModalBody({
988
1100
  error,
989
1101
  canUseVoice,
990
1102
  canUseText,
1103
+ canUseAttachments,
991
1104
  effectiveInputMode,
992
1105
  note,
993
1106
  normalizedNote,
@@ -1001,6 +1114,11 @@ function IssueReportModalBody({
1001
1114
  voiceError,
1002
1115
  scribeError,
1003
1116
  submitError,
1117
+ existingAttachments,
1118
+ removedExistingIds,
1119
+ pendingFiles,
1120
+ uploadProgress,
1121
+ attachmentValidationErrors,
1004
1122
  onRetryHydration,
1005
1123
  onClose,
1006
1124
  onSelectText,
@@ -1009,7 +1127,10 @@ function IssueReportModalBody({
1009
1127
  onStopVoiceInput,
1010
1128
  onAppendTranscript,
1011
1129
  onNoteChange,
1012
- onSubmit
1130
+ onSubmit,
1131
+ onAddFiles,
1132
+ onRemoveExistingAttachment,
1133
+ onRemovePendingAttachment
1013
1134
  }) {
1014
1135
  if (isHydrating) {
1015
1136
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-5 flex items-center gap-2 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4 text-sm text-slate-600", children: [
@@ -1079,6 +1200,20 @@ function IssueReportModalBody({
1079
1200
  onSubmit
1080
1201
  }
1081
1202
  ),
1203
+ canUseAttachments && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1204
+ AttachmentPicker,
1205
+ {
1206
+ existingAttachments,
1207
+ removedExistingIds,
1208
+ pendingFiles,
1209
+ uploadProgress,
1210
+ validationErrors: attachmentValidationErrors,
1211
+ disabled: isSubmitting,
1212
+ onAddFiles,
1213
+ onRemoveExisting: onRemoveExistingAttachment,
1214
+ onRemovePending: onRemovePendingAttachment
1215
+ }
1216
+ ),
1082
1217
  submitError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-4 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700", children: submitError }) : null,
1083
1218
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-6 flex justify-end gap-3", children: [
1084
1219
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -1104,7 +1239,8 @@ function IssueReportModalBody({
1104
1239
  children: isSubmitting ? copy.submittingAction : copy.submitAction
1105
1240
  }
1106
1241
  )
1107
- ] })
1242
+ ] }),
1243
+ mode !== "create" && issue ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IssueReportMessageThread, { issueReportId: issue.id }) : null
1108
1244
  ] });
1109
1245
  }
1110
1246
  function useIssueReportVoiceCapture({
@@ -1202,6 +1338,493 @@ function useIssueReportVoiceCapture({
1202
1338
  appendTranscript
1203
1339
  };
1204
1340
  }
1341
+ var INITIAL_UPLOAD_PROGRESS = {
1342
+ phase: "idle",
1343
+ uploaded: 0,
1344
+ total: 0
1345
+ };
1346
+ var pendingFileCounter = 0;
1347
+ function nextPendingId() {
1348
+ pendingFileCounter += 1;
1349
+ return `pending-${pendingFileCounter}`;
1350
+ }
1351
+ function formatFileSize(bytes) {
1352
+ if (bytes < 1024) return `${bytes} B`;
1353
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1354
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1355
+ }
1356
+ function validateAttachmentFile(file) {
1357
+ if (!ATTACHMENT_ALLOWED_MIMES.has(file.type)) {
1358
+ return `"${file.name}" is not a supported image type (PNG, JPEG, or WebP).`;
1359
+ }
1360
+ if (file.size > ATTACHMENT_MAX_BYTES) {
1361
+ return `"${file.name}" exceeds the 10 MB limit.`;
1362
+ }
1363
+ return null;
1364
+ }
1365
+ function AttachmentPicker({
1366
+ existingAttachments,
1367
+ removedExistingIds,
1368
+ pendingFiles,
1369
+ uploadProgress,
1370
+ validationErrors,
1371
+ disabled,
1372
+ onAddFiles,
1373
+ onRemoveExisting,
1374
+ onRemovePending
1375
+ }) {
1376
+ const fileInputRef = (0, import_react5.useRef)(null);
1377
+ const retainedExisting = existingAttachments.filter(
1378
+ (a) => !removedExistingIds.has(a.id)
1379
+ );
1380
+ const totalCount = retainedExisting.length + pendingFiles.length;
1381
+ const canAdd = totalCount < ATTACHMENT_MAX_COUNT && !disabled;
1382
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-4 space-y-3", children: [
1383
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center justify-between", children: [
1384
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-slate-500", children: [
1385
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Image, { className: "h-3.5 w-3.5" }),
1386
+ "Screenshots (",
1387
+ totalCount,
1388
+ "/",
1389
+ ATTACHMENT_MAX_COUNT,
1390
+ ")"
1391
+ ] }),
1392
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1393
+ "input",
1394
+ {
1395
+ ref: fileInputRef,
1396
+ type: "file",
1397
+ accept: "image/png,image/jpeg,image/webp",
1398
+ multiple: true,
1399
+ className: "hidden",
1400
+ onChange: (e) => {
1401
+ if (e.target.files && e.target.files.length > 0) {
1402
+ onAddFiles(e.target.files);
1403
+ }
1404
+ e.target.value = "";
1405
+ },
1406
+ disabled: !canAdd,
1407
+ "aria-label": "Select screenshot files"
1408
+ }
1409
+ ),
1410
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1411
+ "button",
1412
+ {
1413
+ type: "button",
1414
+ className: cn(
1415
+ "rounded-full border px-3 py-1 text-xs font-medium transition",
1416
+ canAdd ? "border-slate-300 text-slate-700 hover:bg-slate-50" : "cursor-not-allowed border-slate-200 text-slate-400"
1417
+ ),
1418
+ onClick: () => fileInputRef.current?.click(),
1419
+ disabled: !canAdd,
1420
+ "aria-label": "Add screenshots",
1421
+ children: "Add"
1422
+ }
1423
+ )
1424
+ ] }),
1425
+ validationErrors.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "space-y-1", children: validationErrors.map((err, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1426
+ "div",
1427
+ {
1428
+ className: "rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700",
1429
+ role: "alert",
1430
+ children: err
1431
+ },
1432
+ i
1433
+ )) }),
1434
+ uploadProgress.phase === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-600", children: [
1435
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Spinner, { className: "h-3.5 w-3.5 animate-spin" }),
1436
+ "Uploading ",
1437
+ uploadProgress.uploaded,
1438
+ " of ",
1439
+ uploadProgress.total,
1440
+ "..."
1441
+ ] }),
1442
+ uploadProgress.phase === "error" && uploadProgress.error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1443
+ "div",
1444
+ {
1445
+ className: "rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700",
1446
+ role: "alert",
1447
+ children: uploadProgress.error
1448
+ }
1449
+ ),
1450
+ (retainedExisting.length > 0 || pendingFiles.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-wrap gap-2", children: [
1451
+ retainedExisting.map((att) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1452
+ "div",
1453
+ {
1454
+ className: "group relative flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2",
1455
+ children: [
1456
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Image, { className: "h-4 w-4 flex-shrink-0 text-slate-400" }),
1457
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "min-w-0", children: [
1458
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "max-w-[140px] truncate text-xs font-medium text-slate-700", children: att.original_filename }),
1459
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-xs text-slate-400", children: formatFileSize(att.byte_size) })
1460
+ ] }),
1461
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1462
+ "button",
1463
+ {
1464
+ type: "button",
1465
+ className: "ml-1 rounded-full p-0.5 text-slate-400 transition hover:bg-slate-100 hover:text-slate-700",
1466
+ onClick: () => onRemoveExisting(att.id),
1467
+ disabled,
1468
+ "aria-label": `Remove ${att.original_filename}`,
1469
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Trash, { className: "h-3.5 w-3.5" })
1470
+ }
1471
+ )
1472
+ ]
1473
+ },
1474
+ att.id
1475
+ )),
1476
+ pendingFiles.map((pf) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1477
+ "div",
1478
+ {
1479
+ className: "group relative flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2",
1480
+ children: [
1481
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1482
+ "img",
1483
+ {
1484
+ src: pf.previewUrl,
1485
+ alt: pf.file.name,
1486
+ className: "h-8 w-8 flex-shrink-0 rounded object-cover"
1487
+ }
1488
+ ),
1489
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "min-w-0", children: [
1490
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "max-w-[140px] truncate text-xs font-medium text-slate-700", children: pf.file.name }),
1491
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-xs text-slate-400", children: formatFileSize(pf.file.size) })
1492
+ ] }),
1493
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1494
+ "button",
1495
+ {
1496
+ type: "button",
1497
+ className: "ml-1 rounded-full p-0.5 text-slate-400 transition hover:bg-slate-100 hover:text-slate-700",
1498
+ onClick: () => onRemovePending(pf.clientId),
1499
+ disabled,
1500
+ "aria-label": `Remove ${pf.file.name}`,
1501
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Trash, { className: "h-3.5 w-3.5" })
1502
+ }
1503
+ )
1504
+ ]
1505
+ },
1506
+ pf.clientId
1507
+ ))
1508
+ ] })
1509
+ ] });
1510
+ }
1511
+ function useAttachmentState(existingAttachments) {
1512
+ const [pendingFiles, setPendingFiles] = (0, import_react5.useState)([]);
1513
+ const [removedExistingIds, setRemovedExistingIds] = (0, import_react5.useState)(
1514
+ /* @__PURE__ */ new Set()
1515
+ );
1516
+ const [validationErrors, setValidationErrors] = (0, import_react5.useState)([]);
1517
+ const [uploadProgress, setUploadProgress] = (0, import_react5.useState)(INITIAL_UPLOAD_PROGRESS);
1518
+ const retainedExistingCount = existingAttachments.filter(
1519
+ (a) => !removedExistingIds.has(a.id)
1520
+ ).length;
1521
+ const reset = (0, import_react5.useCallback)(() => {
1522
+ for (const pf of pendingFiles) {
1523
+ URL.revokeObjectURL(pf.previewUrl);
1524
+ }
1525
+ setPendingFiles([]);
1526
+ setRemovedExistingIds(/* @__PURE__ */ new Set());
1527
+ setValidationErrors([]);
1528
+ setUploadProgress(INITIAL_UPLOAD_PROGRESS);
1529
+ }, [pendingFiles]);
1530
+ const addFiles = (0, import_react5.useCallback)(
1531
+ (fileList) => {
1532
+ const currentTotal = retainedExistingCount + pendingFiles.length;
1533
+ const errors = [];
1534
+ const accepted = [];
1535
+ let count = currentTotal;
1536
+ for (let i = 0; i < fileList.length; i++) {
1537
+ const file = fileList[i];
1538
+ if (count >= ATTACHMENT_MAX_COUNT) {
1539
+ errors.push(
1540
+ `"${file.name}" was not added; maximum ${ATTACHMENT_MAX_COUNT} screenshots reached.`
1541
+ );
1542
+ continue;
1543
+ }
1544
+ const err = validateAttachmentFile(file);
1545
+ if (err) {
1546
+ errors.push(err);
1547
+ continue;
1548
+ }
1549
+ accepted.push({
1550
+ clientId: nextPendingId(),
1551
+ file,
1552
+ previewUrl: URL.createObjectURL(file)
1553
+ });
1554
+ count += 1;
1555
+ }
1556
+ if (accepted.length > 0) {
1557
+ setPendingFiles((prev) => [...prev, ...accepted]);
1558
+ }
1559
+ setValidationErrors(errors);
1560
+ setUploadProgress(INITIAL_UPLOAD_PROGRESS);
1561
+ },
1562
+ [pendingFiles.length, retainedExistingCount]
1563
+ );
1564
+ const removeExisting = (0, import_react5.useCallback)((id) => {
1565
+ setRemovedExistingIds((prev) => /* @__PURE__ */ new Set([...prev, id]));
1566
+ }, []);
1567
+ const removePending = (0, import_react5.useCallback)((clientId) => {
1568
+ setPendingFiles((prev) => {
1569
+ const removed = prev.find((pf) => pf.clientId === clientId);
1570
+ if (removed) {
1571
+ URL.revokeObjectURL(removed.previewUrl);
1572
+ }
1573
+ return prev.filter((pf) => pf.clientId !== clientId);
1574
+ });
1575
+ setUploadProgress(INITIAL_UPLOAD_PROGRESS);
1576
+ }, []);
1577
+ const markPendingUploaded = (0, import_react5.useCallback)((clientId, attachmentId) => {
1578
+ setPendingFiles(
1579
+ (prev) => prev.map(
1580
+ (pf) => pf.clientId === clientId ? { ...pf, uploadedAttachmentId: attachmentId } : pf
1581
+ )
1582
+ );
1583
+ }, []);
1584
+ return {
1585
+ pendingFiles,
1586
+ removedExistingIds,
1587
+ validationErrors,
1588
+ uploadProgress,
1589
+ setUploadProgress,
1590
+ addFiles,
1591
+ removeExisting,
1592
+ removePending,
1593
+ markPendingUploaded,
1594
+ reset
1595
+ };
1596
+ }
1597
+ var REPORTER_MESSAGE_MIN_LENGTH = 1;
1598
+ var REPORTER_MESSAGE_MAX_LENGTH = 2e3;
1599
+ function generateIdempotencyKey() {
1600
+ const cryptoObj = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
1601
+ if (cryptoObj && typeof cryptoObj.randomUUID === "function") {
1602
+ return `reporter-msg-${cryptoObj.randomUUID()}`;
1603
+ }
1604
+ return `reporter-msg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
1605
+ }
1606
+ function getMessageKindLabel(kind, copy) {
1607
+ switch (kind) {
1608
+ case "clarification_request":
1609
+ return copy.threadKindClarificationRequest;
1610
+ case "reporter_response":
1611
+ return copy.threadKindReporterResponse;
1612
+ case "final_response":
1613
+ return copy.threadKindFinalResponse;
1614
+ default:
1615
+ return kind;
1616
+ }
1617
+ }
1618
+ function getMessageKindClassName(kind) {
1619
+ switch (kind) {
1620
+ case "clarification_request":
1621
+ return "bg-amber-100 text-amber-700";
1622
+ case "final_response":
1623
+ return "bg-emerald-100 text-emerald-700";
1624
+ default:
1625
+ return "bg-slate-100 text-slate-600";
1626
+ }
1627
+ }
1628
+ function MessageBubble({
1629
+ message,
1630
+ copy
1631
+ }) {
1632
+ const isReporter = message.actor.author_type === "reporter";
1633
+ const isFinal = message.kind === "final_response";
1634
+ const authorLabel = isReporter ? copy.threadAuthorReporter : copy.threadAuthorOperator;
1635
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1636
+ "li",
1637
+ {
1638
+ className: cn(
1639
+ "flex flex-col gap-1",
1640
+ isReporter ? "items-end" : "items-start"
1641
+ ),
1642
+ children: [
1643
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-2", children: [
1644
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1645
+ "span",
1646
+ {
1647
+ className: cn(
1648
+ "rounded-full px-2 py-0.5 font-medium",
1649
+ BADGE_TEXT,
1650
+ getMessageKindClassName(message.kind)
1651
+ ),
1652
+ children: getMessageKindLabel(message.kind, copy)
1653
+ }
1654
+ ),
1655
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: cn("font-medium text-slate-600", LABEL_TEXT), children: authorLabel }),
1656
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1657
+ "time",
1658
+ {
1659
+ className: "text-xs text-slate-400",
1660
+ dateTime: message.created_at,
1661
+ children: formatRelativeTime(message.created_at)
1662
+ }
1663
+ )
1664
+ ] }),
1665
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1666
+ "div",
1667
+ {
1668
+ className: cn(
1669
+ "max-w-[85%] whitespace-pre-wrap rounded-2xl border px-3 py-2 text-sm",
1670
+ isReporter ? "border-slate-900 bg-slate-900 text-white" : isFinal ? "border-emerald-200 bg-emerald-50 text-emerald-950" : "border-slate-200 bg-white text-slate-800"
1671
+ ),
1672
+ children: message.body
1673
+ }
1674
+ )
1675
+ ]
1676
+ }
1677
+ );
1678
+ }
1679
+ function ReporterResponseComposer({
1680
+ issueReportId,
1681
+ copy
1682
+ }) {
1683
+ const mutation = useIssueReportingMessageMutation(issueReportId);
1684
+ const idempotencyKeyRef = (0, import_react5.useRef)(generateIdempotencyKey());
1685
+ const [body, setBody] = (0, import_react5.useState)("");
1686
+ const [submitError, setSubmitError] = (0, import_react5.useState)(null);
1687
+ const normalized = body.trim();
1688
+ const isValid = normalized.length >= REPORTER_MESSAGE_MIN_LENGTH && normalized.length <= REPORTER_MESSAGE_MAX_LENGTH;
1689
+ const isSubmitting = mutation.isPending;
1690
+ const handleSubmit = async () => {
1691
+ if (!isValid || isSubmitting) {
1692
+ return;
1693
+ }
1694
+ setSubmitError(null);
1695
+ try {
1696
+ await mutation.mutateAsync({
1697
+ body: normalized,
1698
+ idempotency_key: idempotencyKeyRef.current
1699
+ });
1700
+ setBody("");
1701
+ idempotencyKeyRef.current = generateIdempotencyKey();
1702
+ } catch (error) {
1703
+ if (isReporterMessageConflict(error)) {
1704
+ setSubmitError(copy.threadResponseConflict);
1705
+ idempotencyKeyRef.current = generateIdempotencyKey();
1706
+ return;
1707
+ }
1708
+ setSubmitError(resolveErrorMessage(error, copy.threadResponseFailed));
1709
+ }
1710
+ };
1711
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-3 space-y-2", children: [
1712
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1713
+ "label",
1714
+ {
1715
+ htmlFor: "issue-report-thread-response",
1716
+ className: cn(
1717
+ "block font-medium uppercase tracking-wide text-slate-500",
1718
+ LABEL_TEXT
1719
+ ),
1720
+ children: copy.threadResponseLabel
1721
+ }
1722
+ ),
1723
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1724
+ "textarea",
1725
+ {
1726
+ id: "issue-report-thread-response",
1727
+ value: body,
1728
+ onChange: (event) => setBody(event.target.value),
1729
+ onKeyDown: (event) => {
1730
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
1731
+ event.preventDefault();
1732
+ void handleSubmit();
1733
+ }
1734
+ },
1735
+ placeholder: copy.threadResponsePlaceholder,
1736
+ className: "h-24 w-full resize-none rounded-2xl border border-slate-300 px-3 py-2 text-sm text-slate-800 outline-none transition focus:border-slate-500 focus:ring-2 focus:ring-slate-200",
1737
+ disabled: isSubmitting
1738
+ }
1739
+ ),
1740
+ submitError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1741
+ "div",
1742
+ {
1743
+ className: "rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700",
1744
+ role: "alert",
1745
+ children: submitError
1746
+ }
1747
+ ) : null,
1748
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1749
+ "button",
1750
+ {
1751
+ type: "button",
1752
+ className: cn(
1753
+ "rounded-full px-4 py-2 text-sm font-semibold text-white transition",
1754
+ isValid && !isSubmitting ? "bg-slate-900 hover:bg-slate-800" : "cursor-not-allowed bg-slate-300"
1755
+ ),
1756
+ onClick: () => void handleSubmit(),
1757
+ disabled: !isValid || isSubmitting,
1758
+ children: isSubmitting ? copy.threadResponseSubmittingAction : copy.threadResponseSubmitAction
1759
+ }
1760
+ ) })
1761
+ ] });
1762
+ }
1763
+ function IssueReportMessageThread({
1764
+ issueReportId
1765
+ }) {
1766
+ const { copy, client, setNeedsResponse } = useIssueReporting();
1767
+ const supportsMessages = Boolean(client.issueReporting.listMessages);
1768
+ const supportsSubmit = Boolean(client.issueReporting.submitMessage);
1769
+ const query = useIssueReportingMessages(issueReportId);
1770
+ const needsResponse = query.data?.needs_response ?? false;
1771
+ (0, import_react5.useEffect)(() => {
1772
+ setNeedsResponse(issueReportId, needsResponse);
1773
+ return () => {
1774
+ setNeedsResponse(issueReportId, false);
1775
+ };
1776
+ }, [issueReportId, needsResponse, setNeedsResponse]);
1777
+ if (!supportsMessages) {
1778
+ return null;
1779
+ }
1780
+ const visibleMessages = selectReporterVisibleMessages(query.data?.items ?? []);
1781
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1782
+ "section",
1783
+ {
1784
+ "aria-label": copy.threadTitle,
1785
+ className: "mt-6 border-t border-slate-100 pt-5",
1786
+ children: [
1787
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center justify-between gap-2", children: [
1788
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: "text-sm font-semibold text-slate-900", children: copy.threadTitle }),
1789
+ needsResponse ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1790
+ "span",
1791
+ {
1792
+ className: cn(
1793
+ "rounded-full px-2 py-0.5 font-medium",
1794
+ BADGE_TEXT,
1795
+ "bg-amber-100 text-amber-700"
1796
+ ),
1797
+ children: copy.threadNeedsResponseBadge
1798
+ }
1799
+ ) : null
1800
+ ] }),
1801
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "mt-1 text-xs text-slate-500", children: copy.threadDescription }),
1802
+ query.isPending ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-4 flex items-center gap-2 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4 text-sm text-slate-600", children: [
1803
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Spinner, { className: "h-4 w-4 animate-spin" }),
1804
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: copy.threadLoading })
1805
+ ] }) : query.error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-4 space-y-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-4 text-sm text-rose-700", children: [
1806
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: resolveErrorMessage(query.error, copy.threadLoadFailed) }),
1807
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1808
+ "button",
1809
+ {
1810
+ type: "button",
1811
+ className: "rounded-full border border-rose-300 px-3 py-1 font-medium transition hover:bg-rose-100",
1812
+ onClick: () => void query.refetch(),
1813
+ children: copy.retryAction
1814
+ }
1815
+ )
1816
+ ] }) : visibleMessages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-4 rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-6 text-center text-sm text-slate-500", children: copy.threadEmpty }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { className: "mt-4 space-y-3", children: visibleMessages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MessageBubble, { message, copy }, message.id)) }),
1817
+ supportsSubmit ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1818
+ ReporterResponseComposer,
1819
+ {
1820
+ issueReportId,
1821
+ copy
1822
+ }
1823
+ ) : null
1824
+ ]
1825
+ }
1826
+ );
1827
+ }
1205
1828
  function IssueReportModeBanner() {
1206
1829
  const { copy } = useIssueReporting();
1207
1830
  const reportMode = useReportMode();
@@ -1502,7 +2125,13 @@ function IssueReportModal() {
1502
2125
  const { isOpen, mode, issue, target, isHydrating, error } = modalState;
1503
2126
  const canUseVoice = mode === "create" && inputModes.includes("voice");
1504
2127
  const canUseText = mode !== "create" || inputModes.includes("text");
1505
- const isSubmitting = createMutation.isPending || updateMutation.isPending || replyMutation.isPending;
2128
+ const canUseAttachments = Boolean(client.issueReporting.uploadAttachment);
2129
+ const existingAttachments = (0, import_react5.useMemo)(
2130
+ () => mode === "edit" && issue ? issue.attachments ?? [] : [],
2131
+ [issue, mode]
2132
+ );
2133
+ const attachmentState = useAttachmentState(existingAttachments);
2134
+ const isSubmitting = createMutation.isPending || updateMutation.isPending || replyMutation.isPending || attachmentState.uploadProgress.phase === "uploading";
1506
2135
  const {
1507
2136
  inputMode,
1508
2137
  setInputMode,
@@ -1526,6 +2155,7 @@ function IssueReportModal() {
1526
2155
  (0, import_react5.useEffect)(() => {
1527
2156
  if (!isOpen) {
1528
2157
  resetVoiceCapture();
2158
+ attachmentState.reset();
1529
2159
  setNote("");
1530
2160
  setSubmitError(null);
1531
2161
  return;
@@ -1536,6 +2166,7 @@ function IssueReportModal() {
1536
2166
  setNote("");
1537
2167
  }
1538
2168
  resetVoiceCapture();
2169
+ attachmentState.reset();
1539
2170
  setSubmitError(null);
1540
2171
  }, [isOpen, issue, mode, resetVoiceCapture]);
1541
2172
  const effectiveInputMode = mode !== "create" ? "text" : canUseVoice && inputMode === "voice" ? "voice" : "text";
@@ -1544,6 +2175,7 @@ function IssueReportModal() {
1544
2175
  const title = mode === "create" ? `${copy.createTitlePrefix}: ${target?.component_label ?? ""}` : mode === "edit" ? `${copy.editTitlePrefix}: ${target?.component_label ?? ""}` : `${copy.replyTitlePrefix}: ${target?.component_label ?? ""}`;
1545
2176
  const handleCloseModal = () => {
1546
2177
  resetVoiceCapture();
2178
+ attachmentState.reset();
1547
2179
  closeModal();
1548
2180
  };
1549
2181
  const handleStartVoiceInput = async () => {
@@ -1555,6 +2187,38 @@ function IssueReportModal() {
1555
2187
  const handleAppendTranscript = () => {
1556
2188
  appendTranscript(setNote);
1557
2189
  };
2190
+ const uploadPendingFiles = async () => {
2191
+ const upload = client.issueReporting.uploadAttachment;
2192
+ if (!upload || attachmentState.pendingFiles.length === 0) {
2193
+ return [];
2194
+ }
2195
+ const files = attachmentState.pendingFiles;
2196
+ attachmentState.setUploadProgress({
2197
+ phase: "uploading",
2198
+ uploaded: 0,
2199
+ total: files.length
2200
+ });
2201
+ const ids = [];
2202
+ for (let i = 0; i < files.length; i++) {
2203
+ const pf = files[i];
2204
+ const attachmentId = pf.uploadedAttachmentId ?? (await upload(pf.file, { filename: pf.file.name })).id;
2205
+ if (!pf.uploadedAttachmentId) {
2206
+ attachmentState.markPendingUploaded(pf.clientId, attachmentId);
2207
+ }
2208
+ ids.push(attachmentId);
2209
+ attachmentState.setUploadProgress({
2210
+ phase: "uploading",
2211
+ uploaded: i + 1,
2212
+ total: files.length
2213
+ });
2214
+ }
2215
+ attachmentState.setUploadProgress({
2216
+ phase: "done",
2217
+ uploaded: files.length,
2218
+ total: files.length
2219
+ });
2220
+ return ids;
2221
+ };
1558
2222
  const handleSubmit = async () => {
1559
2223
  if (!target || !isValid || isSubmitting) {
1560
2224
  return;
@@ -1562,6 +2226,10 @@ function IssueReportModal() {
1562
2226
  setSubmitError(null);
1563
2227
  try {
1564
2228
  const noteForSubmit = normalizedNote;
2229
+ let newAttachmentIds = [];
2230
+ if (canUseAttachments && attachmentState.pendingFiles.length > 0) {
2231
+ newAttachmentIds = await uploadPendingFiles();
2232
+ }
1565
2233
  const effectiveVoiceMetadata = effectiveInputMode === "voice" ? voiceSubmitMetadata ?? {
1566
2234
  provider: voice.provider,
1567
2235
  token: "",
@@ -1586,24 +2254,37 @@ function IssueReportModal() {
1586
2254
  }
1587
2255
  } : target,
1588
2256
  note: noteForSubmit,
1589
- reporter_role_hint: reporterRoleHint
2257
+ reporter_role_hint: reporterRoleHint,
2258
+ attachment_ids: newAttachmentIds.length > 0 ? newAttachmentIds : void 0
1590
2259
  });
1591
2260
  } else if (mode === "edit" && issue) {
2261
+ const removedIds = [...attachmentState.removedExistingIds];
1592
2262
  await updateMutation.mutateAsync({
1593
2263
  issueReportId: issue.id,
1594
- note: noteForSubmit
2264
+ note: noteForSubmit,
2265
+ add_attachment_ids: newAttachmentIds.length > 0 ? newAttachmentIds : void 0,
2266
+ remove_attachment_ids: removedIds.length > 0 ? removedIds : void 0
1595
2267
  });
1596
2268
  } else if (mode === "reply" && issue) {
1597
2269
  await replyMutation.mutateAsync({
1598
2270
  issueReportId: issue.id,
1599
2271
  note: noteForSubmit,
1600
- reporterRoleHint
2272
+ reporterRoleHint,
2273
+ attachment_ids: newAttachmentIds.length > 0 ? newAttachmentIds : void 0
1601
2274
  });
1602
2275
  }
1603
2276
  resetVoiceCapture();
2277
+ attachmentState.reset();
1604
2278
  closeModal();
1605
2279
  } catch (submissionError) {
1606
- setSubmitError(resolveErrorMessage(submissionError, "Failed to submit issue report"));
2280
+ const message = resolveErrorMessage(
2281
+ submissionError,
2282
+ "Failed to submit issue report"
2283
+ );
2284
+ attachmentState.setUploadProgress(
2285
+ (current) => current.phase === "uploading" ? { ...current, phase: "error", error: message } : current
2286
+ );
2287
+ setSubmitError(message);
1607
2288
  }
1608
2289
  };
1609
2290
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dialog.Root, { open: isOpen, onOpenChange: (open) => !open && handleCloseModal(), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Dialog.Portal, { children: [
@@ -1612,7 +2293,7 @@ function IssueReportModal() {
1612
2293
  Dialog.Content,
1613
2294
  {
1614
2295
  className: cn(
1615
- "fixed left-1/2 top-1/2 max-w-xl -translate-x-1/2 -translate-y-1/2 border border-slate-200 bg-white p-6 focus:outline-none",
2296
+ "fixed left-1/2 top-1/2 max-h-[90vh] max-w-xl -translate-x-1/2 -translate-y-1/2 overflow-y-auto border border-slate-200 bg-white p-6 focus:outline-none",
1616
2297
  Z_MODAL_CONTENT,
1617
2298
  MODAL_WIDTH,
1618
2299
  MODAL_RADIUS,
@@ -1638,6 +2319,7 @@ function IssueReportModal() {
1638
2319
  error,
1639
2320
  canUseVoice,
1640
2321
  canUseText,
2322
+ canUseAttachments,
1641
2323
  effectiveInputMode,
1642
2324
  note,
1643
2325
  normalizedNote,
@@ -1651,6 +2333,11 @@ function IssueReportModal() {
1651
2333
  voiceError,
1652
2334
  scribeError,
1653
2335
  submitError,
2336
+ existingAttachments,
2337
+ removedExistingIds: attachmentState.removedExistingIds,
2338
+ pendingFiles: attachmentState.pendingFiles,
2339
+ uploadProgress: attachmentState.uploadProgress,
2340
+ attachmentValidationErrors: attachmentState.validationErrors,
1654
2341
  onRetryHydration: () => void retryModalHydration(),
1655
2342
  onClose: handleCloseModal,
1656
2343
  onSelectText: () => setInputMode("text"),
@@ -1659,7 +2346,10 @@ function IssueReportModal() {
1659
2346
  onStopVoiceInput: handleStopVoiceInput,
1660
2347
  onAppendTranscript: handleAppendTranscript,
1661
2348
  onNoteChange: setNote,
1662
- onSubmit: () => void handleSubmit()
2349
+ onSubmit: () => void handleSubmit(),
2350
+ onAddFiles: attachmentState.addFiles,
2351
+ onRemoveExistingAttachment: attachmentState.removeExisting,
2352
+ onRemovePendingAttachment: attachmentState.removePending
1663
2353
  }
1664
2354
  ),
1665
2355
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dialog.Close, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -1686,33 +2376,44 @@ function FloatingIssueReportButton({
1686
2376
  isReportMode,
1687
2377
  isPopoverOpen,
1688
2378
  openPopover,
1689
- closePopover
2379
+ closePopover,
2380
+ needsResponseIssueIds
1690
2381
  } = useIssueReporting();
1691
2382
  const status = useIssueReportingStatus();
1692
2383
  const entryPointState = getEntryPointState(status.data);
2384
+ const needsResponse = needsResponseIssueIds.length > 0;
1693
2385
  if (!isEligible) {
1694
2386
  return null;
1695
2387
  }
1696
2388
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
1697
2389
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IssueReportModeBanner, {}),
1698
- !isReportMode ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: cn("fixed bottom-12 right-4", Z_FLOATING_BUTTON, positionClassName), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IssueReportPopover, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2390
+ !isReportMode ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: cn("fixed bottom-12 right-4", Z_FLOATING_BUTTON, positionClassName), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IssueReportPopover, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1699
2391
  "button",
1700
2392
  {
1701
2393
  type: "button",
1702
- "aria-label": copy.entryAriaLabel,
2394
+ "aria-label": needsResponse ? `${copy.entryAriaLabel} (${copy.threadNeedsResponseBadge})` : copy.entryAriaLabel,
1703
2395
  onClick: () => isPopoverOpen ? closePopover() : openPopover(),
1704
2396
  className: cn(
1705
- "flex h-12 w-12 items-center justify-center rounded-full border border-slate-200 bg-white shadow-lg transition hover:-translate-y-0.5 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-slate-300",
2397
+ "relative flex h-12 w-12 items-center justify-center rounded-full border border-slate-200 bg-white shadow-lg transition hover:-translate-y-0.5 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-slate-300",
1706
2398
  status.isPending && "animate-pulse",
1707
2399
  className
1708
2400
  ),
1709
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1710
- import_react4.BugBeetle,
1711
- {
1712
- className: cn("h-6 w-6", getEntryPointClassName(entryPointState)),
1713
- weight: "fill"
1714
- }
1715
- )
2401
+ children: [
2402
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2403
+ import_react4.BugBeetle,
2404
+ {
2405
+ className: cn("h-6 w-6", getEntryPointClassName(entryPointState)),
2406
+ weight: "fill"
2407
+ }
2408
+ ),
2409
+ needsResponse ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2410
+ "span",
2411
+ {
2412
+ "data-testid": "issue-report-needs-response-badge",
2413
+ className: "absolute -right-0.5 -top-0.5 h-3 w-3 rounded-full border-2 border-white bg-amber-500"
2414
+ }
2415
+ ) : null
2416
+ ]
1716
2417
  }
1717
2418
  ) }) }) : null,
1718
2419
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IssueReportModal, {})
@@ -1765,6 +2466,7 @@ function ReportableSection({
1765
2466
  // Annotate the CommonJS export names for ESM import in node:
1766
2467
  0 && (module.exports = {
1767
2468
  FloatingIssueReportButton,
2469
+ IssueReportMessageThread,
1768
2470
  IssueReportingPageConfig,
1769
2471
  IssueReportingProvider,
1770
2472
  ReportModeContext,
@@ -1778,9 +2480,13 @@ function ReportableSection({
1778
2480
  getIssueStatusClassName,
1779
2481
  isClosedIssueStatus,
1780
2482
  isOpenIssueStatus,
2483
+ isReporterMessageConflict,
1781
2484
  issueReportingKeys,
2485
+ selectReporterVisibleMessages,
1782
2486
  useIssueReporting,
1783
2487
  useIssueReportingHistory,
2488
+ useIssueReportingMessageMutation,
2489
+ useIssueReportingMessages,
1784
2490
  useIssueReportingMutations,
1785
2491
  useIssueReportingStatus,
1786
2492
  useReportMode