spaps-issue-reporting-react 0.1.2 → 0.1.4

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.mjs CHANGED
@@ -1,15 +1,18 @@
1
1
  // src/components.tsx
2
2
  import * as Dialog from "@radix-ui/react-dialog";
3
3
  import * as Popover from "@radix-ui/react-popover";
4
+ import { useScribe } from "@elevenlabs/react";
4
5
  import {
5
6
  BugBeetle,
6
7
  CheckCircle,
7
8
  Circle,
9
+ Microphone,
8
10
  Spinner,
11
+ TextT,
9
12
  X
10
13
  } from "@phosphor-icons/react";
11
14
  import { formatDistanceToNow } from "date-fns";
12
- import { useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
15
+ import React2, { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
13
16
 
14
17
  // src/provider.tsx
15
18
  import {
@@ -18,6 +21,7 @@ import {
18
21
  useContext as useContext2,
19
22
  useEffect,
20
23
  useMemo,
24
+ useRef,
21
25
  useState
22
26
  } from "react";
23
27
  import {
@@ -40,6 +44,9 @@ import { jsx } from "react/jsx-runtime";
40
44
  var LIST_LIMIT = 200;
41
45
  var NOTE_MIN_LENGTH = 10;
42
46
  var NOTE_MAX_LENGTH = 2e3;
47
+ var DEFAULT_INPUT_MODES = ["text"];
48
+ var DEFAULT_VOICE_PROVIDER = "elevenlabs_scribe_realtime";
49
+ var DEFAULT_VOICE_MODEL_ID = "scribe_v2_realtime";
43
50
  var initialModalState = {
44
51
  isOpen: false,
45
52
  mode: "create",
@@ -52,10 +59,24 @@ var initialModalState = {
52
59
  var IssueReportingContext = createContext2(
53
60
  void 0
54
61
  );
62
+ var IssueReportingPageConfigRegistryContext = createContext2(void 0);
63
+ var hiddenMarkerStyle = {
64
+ position: "absolute",
65
+ width: "1px",
66
+ height: "1px",
67
+ padding: 0,
68
+ margin: "-1px",
69
+ overflow: "hidden",
70
+ clip: "rect(0, 0, 0, 0)",
71
+ whiteSpace: "nowrap",
72
+ border: 0
73
+ };
55
74
  var defaultIssueReportingCopy = {
56
75
  entryAriaLabel: "Report issue",
57
76
  popoverTitle: "Issue Reports",
58
77
  reportNewAction: "Report New Issue",
78
+ reportPageAction: "Report This Page",
79
+ reportSpecificAction: "Pick Specific Section",
59
80
  filtersAll: "All",
60
81
  filtersUnresolved: "Unresolved",
61
82
  filtersResolved: "Resolved",
@@ -72,6 +93,9 @@ var defaultIssueReportingCopy = {
72
93
  reportModeTitle: "Report mode is active",
73
94
  reportModeDescription: "Click a highlighted section to capture the broken surface.",
74
95
  reportModeCancelAction: "Cancel",
96
+ generalPageTargetLabel: "Current Page",
97
+ surfaceRequiredDescription: "This page requires selecting a specific section before you submit a report.",
98
+ specificSectionUnavailableDescription: "No specific sections are registered on this page right now.",
75
99
  createTitlePrefix: "Report Issue",
76
100
  editTitlePrefix: "Edit Issue",
77
101
  replyTitlePrefix: "Reply to",
@@ -115,6 +139,25 @@ function resolveInitialScope(defaultScope, allowTenantScope) {
115
139
  }
116
140
  return "mine";
117
141
  }
142
+ function normalizeInputModes(inputModes) {
143
+ if (!inputModes || inputModes.length === 0) {
144
+ return DEFAULT_INPUT_MODES;
145
+ }
146
+ const unique = Array.from(new Set(inputModes));
147
+ const supported = unique.filter(
148
+ (mode) => mode === "text" || mode === "voice"
149
+ );
150
+ if (supported.length === 0) {
151
+ return DEFAULT_INPUT_MODES;
152
+ }
153
+ return supported;
154
+ }
155
+ function resolveDefaultInputMode(defaultInputMode, inputModes) {
156
+ if (defaultInputMode && inputModes.includes(defaultInputMode)) {
157
+ return defaultInputMode;
158
+ }
159
+ return inputModes[0] ?? "text";
160
+ }
118
161
  function normalizeTarget(target, getPageUrl) {
119
162
  if (typeof target === "string") {
120
163
  const normalized = target.trim();
@@ -136,6 +179,44 @@ function normalizeTarget(target, getPageUrl) {
136
179
  metadata: target.metadata ?? {}
137
180
  };
138
181
  }
182
+ function summarizeTarget(target) {
183
+ if (typeof target === "string") {
184
+ const normalized = target.trim();
185
+ return {
186
+ component_key: normalized,
187
+ component_label: normalized,
188
+ surface_ref: null,
189
+ metadata: {}
190
+ };
191
+ }
192
+ const key = target.componentKey.trim();
193
+ return {
194
+ component_key: key,
195
+ component_label: (target.componentLabel ?? key).trim(),
196
+ surface_ref: target.surfaceRef ?? null,
197
+ metadata: target.metadata ?? {}
198
+ };
199
+ }
200
+ function isElementVisibleForReporting(element) {
201
+ if (!element || typeof window === "undefined") {
202
+ return true;
203
+ }
204
+ let current = element;
205
+ while (current) {
206
+ if (current.getAttribute("aria-hidden") === "true") {
207
+ return false;
208
+ }
209
+ if (current instanceof HTMLElement && (current.hidden || current.inert)) {
210
+ return false;
211
+ }
212
+ const computedStyle = window.getComputedStyle(current);
213
+ if (computedStyle.display === "none" || computedStyle.visibility === "hidden" || computedStyle.visibility === "collapse") {
214
+ return false;
215
+ }
216
+ current = current.parentElement;
217
+ }
218
+ return true;
219
+ }
139
220
  function normalizeIssueTarget(issue) {
140
221
  return {
141
222
  component_key: issue.target.component_key,
@@ -217,6 +298,23 @@ function getIssueNoteLengthMessage(note, copy) {
217
298
  }
218
299
  return `${note.length}/${NOTE_MAX_LENGTH}`;
219
300
  }
301
+ function buildGeneralPageTarget(copy, registeredTargets, getPageUrl) {
302
+ return {
303
+ component_key: "general_page",
304
+ component_label: copy.generalPageTargetLabel,
305
+ page_url: resolvePageUrl(getPageUrl),
306
+ surface_ref: null,
307
+ metadata: {
308
+ capture_mode: "general_page",
309
+ registered_target_count: registeredTargets.length,
310
+ ...registeredTargets.length > 0 ? {
311
+ registered_targets: registeredTargets.map(
312
+ ({ target }) => summarizeTarget(target)
313
+ )
314
+ } : {}
315
+ }
316
+ };
317
+ }
220
318
  function useIssueReporting() {
221
319
  const context = useContext2(IssueReportingContext);
222
320
  if (!context) {
@@ -226,6 +324,25 @@ function useIssueReporting() {
226
324
  }
227
325
  return context;
228
326
  }
327
+ function IssueReportingPageConfig({
328
+ createMode
329
+ }) {
330
+ const registry = useContext2(IssueReportingPageConfigRegistryContext);
331
+ const configId = useRef(/* @__PURE__ */ Symbol("issue-reporting-page-config"));
332
+ const markerRef = useRef(null);
333
+ if (!registry) {
334
+ throw new Error(
335
+ "IssueReportingPageConfig must be used within an IssueReportingProvider"
336
+ );
337
+ }
338
+ useEffect(() => {
339
+ registry.registerPageConfig(configId.current, createMode, () => markerRef.current);
340
+ return () => {
341
+ registry.unregisterPageConfig(configId.current);
342
+ };
343
+ }, [createMode, registry]);
344
+ return /* @__PURE__ */ jsx("span", { ref: markerRef, style: hiddenMarkerStyle });
345
+ }
229
346
  function useIssueReportingStatus() {
230
347
  const { client, isEligible, scope } = useIssueReporting();
231
348
  return useQuery({
@@ -298,6 +415,10 @@ function IssueReportingProvider({
298
415
  getPageUrl,
299
416
  defaultScope,
300
417
  allowTenantScope = false,
418
+ defaultCreateMode = "general_page",
419
+ inputModes,
420
+ defaultInputMode,
421
+ voice,
301
422
  copy,
302
423
  children
303
424
  }) {
@@ -315,6 +436,27 @@ function IssueReportingProvider({
315
436
  const [scope, setScope] = useState(
316
437
  () => resolveInitialScope(defaultScope, allowTenantScope)
317
438
  );
439
+ const [pageConfigs, setPageConfigs] = useState([]);
440
+ const [registeredTargets, setRegisteredTargets] = useState(
441
+ []
442
+ );
443
+ const resolvedInputModes = useMemo(
444
+ () => normalizeInputModes(inputModes),
445
+ [inputModes]
446
+ );
447
+ const resolvedDefaultInputMode = useMemo(
448
+ () => resolveDefaultInputMode(defaultInputMode, resolvedInputModes),
449
+ [defaultInputMode, resolvedInputModes]
450
+ );
451
+ const resolvedVoiceConfig = useMemo(
452
+ () => ({
453
+ provider: voice?.provider ?? DEFAULT_VOICE_PROVIDER,
454
+ modelId: voice?.modelId ?? DEFAULT_VOICE_MODEL_ID,
455
+ requireMicrophonePermission: voice?.requireMicrophonePermission ?? true,
456
+ microphone: voice?.microphone
457
+ }),
458
+ [voice]
459
+ );
318
460
  useEffect(() => {
319
461
  setScope(resolveInitialScope(defaultScope, allowTenantScope));
320
462
  }, [allowTenantScope, defaultScope]);
@@ -324,6 +466,51 @@ function IssueReportingProvider({
324
466
  const openPopover = useCallback(() => {
325
467
  setIsPopoverOpen(true);
326
468
  }, []);
469
+ const openCreateModal = useCallback((target) => {
470
+ setIsPopoverOpen(false);
471
+ setIsReportMode(false);
472
+ setModalState({
473
+ isOpen: true,
474
+ mode: "create",
475
+ issueReportId: null,
476
+ issue: null,
477
+ target,
478
+ error: null,
479
+ isHydrating: false
480
+ });
481
+ }, []);
482
+ const registerPageConfig = useCallback(
483
+ (id, createMode2, getElement) => {
484
+ setPageConfigs((current) => [
485
+ ...current.filter((entry) => entry.id !== id),
486
+ { id, createMode: createMode2, getElement }
487
+ ]);
488
+ },
489
+ []
490
+ );
491
+ const unregisterPageConfig = useCallback((id) => {
492
+ setPageConfigs((current) => current.filter((entry) => entry.id !== id));
493
+ }, []);
494
+ const registerTarget = useCallback(
495
+ (id, target, getElement) => {
496
+ setRegisteredTargets((current) => [
497
+ ...current.filter((entry) => entry.id !== id),
498
+ { id, target, getElement }
499
+ ]);
500
+ },
501
+ []
502
+ );
503
+ const unregisterTarget = useCallback((id) => {
504
+ setRegisteredTargets((current) => current.filter((entry) => entry.id !== id));
505
+ }, []);
506
+ const visiblePageConfigs = pageConfigs.filter(
507
+ ({ getElement }) => isElementVisibleForReporting(getElement())
508
+ );
509
+ const visibleRegisteredTargets = registeredTargets.filter(
510
+ ({ getElement }) => isElementVisibleForReporting(getElement())
511
+ );
512
+ const createMode = visiblePageConfigs.length > 0 ? visiblePageConfigs[visiblePageConfigs.length - 1].createMode : defaultCreateMode;
513
+ const hasRegisteredTargets = visibleRegisteredTargets.length > 0;
327
514
  const enterReportMode = useCallback(() => {
328
515
  setIsReportMode(true);
329
516
  setIsPopoverOpen(false);
@@ -335,6 +522,42 @@ function IssueReportingProvider({
335
522
  setModalState(initialModalState);
336
523
  setIsPopoverOpen(true);
337
524
  }, []);
525
+ const openPageIssueModal = useCallback(() => {
526
+ if (!isEligible) {
527
+ return;
528
+ }
529
+ openCreateModal(
530
+ buildGeneralPageTarget(mergedCopy, visibleRegisteredTargets, getPageUrl)
531
+ );
532
+ }, [
533
+ getPageUrl,
534
+ isEligible,
535
+ mergedCopy,
536
+ openCreateModal,
537
+ visibleRegisteredTargets
538
+ ]);
539
+ const startNewIssue = useCallback(() => {
540
+ if (!isEligible) {
541
+ return;
542
+ }
543
+ if (createMode === "surface_required") {
544
+ if (hasRegisteredTargets) {
545
+ enterReportMode();
546
+ }
547
+ return;
548
+ }
549
+ if (createMode === "surface_preferred" && hasRegisteredTargets) {
550
+ enterReportMode();
551
+ return;
552
+ }
553
+ openPageIssueModal();
554
+ }, [
555
+ createMode,
556
+ enterReportMode,
557
+ hasRegisteredTargets,
558
+ isEligible,
559
+ openPageIssueModal
560
+ ]);
338
561
  const hydrateIssueIntoModal = useCallback(
339
562
  async (issueReportId, mode) => {
340
563
  setModalState({
@@ -393,18 +616,9 @@ function IssueReportingProvider({
393
616
  return;
394
617
  }
395
618
  const normalizedTarget = normalizeTarget(target, getPageUrl);
396
- setIsReportMode(false);
397
- setModalState({
398
- isOpen: true,
399
- mode: "create",
400
- issueReportId: null,
401
- issue: null,
402
- target: normalizedTarget,
403
- error: null,
404
- isHydrating: false
405
- });
619
+ openCreateModal(normalizedTarget);
406
620
  },
407
- [getPageUrl, isEligible]
621
+ [getPageUrl, isEligible, openCreateModal]
408
622
  );
409
623
  const contextValue = useMemo(
410
624
  () => ({
@@ -414,9 +628,12 @@ function IssueReportingProvider({
414
628
  principalId: principalId ?? null,
415
629
  copy: mergedCopy,
416
630
  isReportMode,
631
+ hasRegisteredTargets,
417
632
  enterReportMode,
418
633
  cancelReportMode,
419
634
  selectPanel,
635
+ openPageIssueModal,
636
+ startNewIssue,
420
637
  isPopoverOpen,
421
638
  openPopover,
422
639
  closePopover,
@@ -426,7 +643,11 @@ function IssueReportingProvider({
426
643
  retryModalHydration,
427
644
  scope,
428
645
  setScope,
429
- allowTenantScope
646
+ allowTenantScope,
647
+ createMode,
648
+ inputModes: resolvedInputModes,
649
+ defaultInputMode: resolvedDefaultInputMode,
650
+ voice: resolvedVoiceConfig
430
651
  }),
431
652
  [
432
653
  allowTenantScope,
@@ -434,30 +655,60 @@ function IssueReportingProvider({
434
655
  client,
435
656
  closeModal,
436
657
  closePopover,
658
+ createMode,
437
659
  enterReportMode,
660
+ hasRegisteredTargets,
438
661
  isEligible,
439
662
  isPopoverOpen,
440
663
  isReportMode,
441
664
  mergedCopy,
442
665
  modalState,
443
666
  openExistingIssueModal,
667
+ openPageIssueModal,
444
668
  openPopover,
445
669
  principalId,
446
670
  reporterRoleHint,
447
671
  retryModalHydration,
672
+ resolvedDefaultInputMode,
673
+ resolvedInputModes,
674
+ resolvedVoiceConfig,
448
675
  scope,
449
- selectPanel
676
+ selectPanel,
677
+ startNewIssue
450
678
  ]
451
679
  );
452
680
  const reportModeValue = useMemo(
453
681
  () => ({
454
682
  isReportMode,
455
683
  selectPanel,
456
- cancelReportMode
684
+ cancelReportMode,
685
+ hasRegisteredTargets,
686
+ registerTarget,
687
+ unregisterTarget
457
688
  }),
458
- [cancelReportMode, isReportMode, selectPanel]
689
+ [
690
+ cancelReportMode,
691
+ hasRegisteredTargets,
692
+ isReportMode,
693
+ registerTarget,
694
+ selectPanel,
695
+ unregisterTarget
696
+ ]
697
+ );
698
+ const pageConfigRegistryValue = useMemo(
699
+ () => ({
700
+ registerPageConfig,
701
+ unregisterPageConfig
702
+ }),
703
+ [registerPageConfig, unregisterPageConfig]
704
+ );
705
+ return /* @__PURE__ */ jsx(
706
+ IssueReportingPageConfigRegistryContext.Provider,
707
+ {
708
+ value: pageConfigRegistryValue,
709
+ children: /* @__PURE__ */ jsx(ReportModeContext.Provider, { value: reportModeValue, children: /* @__PURE__ */ jsx(IssueReportingContext.Provider, { value: contextValue, children }) })
710
+ }
459
711
  );
460
- return /* @__PURE__ */ jsx(ReportModeContext.Provider, { value: reportModeValue, children: /* @__PURE__ */ jsx(IssueReportingContext.Provider, { value: contextValue, children }) });
461
712
  }
462
713
 
463
714
  // src/components.tsx
@@ -465,6 +716,18 @@ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
465
716
  function cn(...values) {
466
717
  return values.filter(Boolean).join(" ");
467
718
  }
719
+ var Z_FLOATING_BUTTON = "z-[65]";
720
+ var Z_BANNER = "z-[70]";
721
+ var Z_POPOVER = "z-[70]";
722
+ var Z_MODAL_OVERLAY = "z-[80]";
723
+ var Z_MODAL_CONTENT = "z-[81]";
724
+ var POPOVER_WIDTH = "w-[360px]";
725
+ var MODAL_WIDTH = "w-[calc(100vw-2rem)]";
726
+ var MODAL_RADIUS = "rounded-[28px]";
727
+ var POPOVER_SHADOW = "shadow-[0_18px_48px_rgba(15,23,42,0.18)]";
728
+ var MODAL_SHADOW = "shadow-[0_28px_80px_rgba(15,23,42,0.24)]";
729
+ var BADGE_TEXT = "text-[11px]";
730
+ var LABEL_TEXT = "text-[11px]";
468
731
  function truncate(value, max = 80) {
469
732
  if (value.length <= max) {
470
733
  return value;
@@ -485,6 +748,16 @@ function resolveErrorMessage(error, fallback) {
485
748
  }
486
749
  return fallback;
487
750
  }
751
+ function getCommittedTranscriptText(committedTranscripts) {
752
+ return committedTranscripts.map((segment) => segment.text.trim()).filter(Boolean).join(" ").trim();
753
+ }
754
+ function appendTranscriptToNote(current, transcript) {
755
+ const normalizedCurrent = current.trim();
756
+ if (!normalizedCurrent) {
757
+ return transcript;
758
+ }
759
+ return `${normalizedCurrent} ${transcript}`;
760
+ }
488
761
  function resolveReporterId(issue) {
489
762
  return issue.reporter_principal_id ?? issue.reporter_user_id ?? null;
490
763
  }
@@ -517,13 +790,390 @@ function getIssueOriginText(issue, copy) {
517
790
  const humanName = issue.reporter_display_name?.trim();
518
791
  return humanName ? `${copy.originHumanLabel} \xB7 ${humanName}` : copy.originHumanLabel;
519
792
  }
793
+ function IssueReportVoicePanel({
794
+ canUseText,
795
+ effectiveInputMode,
796
+ isSubmitting,
797
+ isVoiceActive,
798
+ isVoiceConnecting,
799
+ voice,
800
+ voiceTokenResult,
801
+ committedTranscript,
802
+ voiceError,
803
+ scribeError,
804
+ onSelectText,
805
+ onSelectVoice,
806
+ onStartVoiceInput,
807
+ onStopVoiceInput,
808
+ onAppendTranscript
809
+ }) {
810
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
811
+ canUseText ? /* @__PURE__ */ jsxs("div", { className: "mt-5 flex gap-2", children: [
812
+ /* @__PURE__ */ jsxs(
813
+ "button",
814
+ {
815
+ type: "button",
816
+ className: cn(
817
+ "inline-flex items-center gap-1 rounded-full border px-3 py-1.5 text-xs font-semibold transition",
818
+ effectiveInputMode === "text" ? "border-slate-900 bg-slate-900 text-white" : "border-slate-200 text-slate-700 hover:bg-slate-50"
819
+ ),
820
+ onClick: onSelectText,
821
+ disabled: isSubmitting,
822
+ children: [
823
+ /* @__PURE__ */ jsx2(TextT, { className: "h-3.5 w-3.5" }),
824
+ "Text Input"
825
+ ]
826
+ }
827
+ ),
828
+ /* @__PURE__ */ jsxs(
829
+ "button",
830
+ {
831
+ type: "button",
832
+ className: cn(
833
+ "inline-flex items-center gap-1 rounded-full border px-3 py-1.5 text-xs font-semibold transition",
834
+ effectiveInputMode === "voice" ? "border-slate-900 bg-slate-900 text-white" : "border-slate-200 text-slate-700 hover:bg-slate-50"
835
+ ),
836
+ onClick: onSelectVoice,
837
+ disabled: isSubmitting,
838
+ children: [
839
+ /* @__PURE__ */ jsx2(Microphone, { className: "h-3.5 w-3.5" }),
840
+ "Voice Input"
841
+ ]
842
+ }
843
+ )
844
+ ] }) : null,
845
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3", children: [
846
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
847
+ /* @__PURE__ */ jsxs("div", { children: [
848
+ /* @__PURE__ */ jsx2("div", { className: "text-xs font-medium uppercase tracking-wide text-slate-500", children: "Voice Input" }),
849
+ /* @__PURE__ */ jsxs("p", { className: "mt-1 text-xs text-slate-600", children: [
850
+ "Provider: ",
851
+ voiceTokenResult?.provider ?? voice.provider,
852
+ " \xB7 Model:",
853
+ " ",
854
+ voiceTokenResult?.model_id ?? voice.modelId
855
+ ] }),
856
+ /* @__PURE__ */ jsx2("p", { className: "mt-1 text-xs text-slate-500", children: voice.requireMicrophonePermission ? "Microphone access is required to transcribe." : "Microphone access policy is optional." })
857
+ ] }),
858
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
859
+ isVoiceActive || isVoiceConnecting ? /* @__PURE__ */ jsx2(
860
+ "button",
861
+ {
862
+ type: "button",
863
+ className: "rounded-full border border-slate-300 px-3 py-1.5 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
864
+ onClick: onStopVoiceInput,
865
+ disabled: isSubmitting,
866
+ children: "Stop Voice Input"
867
+ }
868
+ ) : /* @__PURE__ */ jsx2(
869
+ "button",
870
+ {
871
+ type: "button",
872
+ className: "rounded-full bg-slate-900 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-slate-800",
873
+ onClick: onStartVoiceInput,
874
+ disabled: isSubmitting,
875
+ children: "Start Voice Input"
876
+ }
877
+ ),
878
+ canUseText ? /* @__PURE__ */ jsx2(
879
+ "button",
880
+ {
881
+ type: "button",
882
+ className: "rounded-full border border-slate-300 px-3 py-1.5 text-xs font-medium text-slate-700 transition hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-60",
883
+ onClick: onAppendTranscript,
884
+ disabled: isSubmitting || !committedTranscript.trim(),
885
+ children: "Append Transcript"
886
+ }
887
+ ) : null
888
+ ] })
889
+ ] }),
890
+ /* @__PURE__ */ jsx2("div", { className: "mt-3 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-700", children: committedTranscript ? committedTranscript : /* @__PURE__ */ jsx2("span", { className: "text-slate-500", children: "No committed transcript yet." }) }),
891
+ voiceError || scribeError ? /* @__PURE__ */ jsx2("div", { className: "mt-2 rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700", children: voiceError ?? scribeError }) : isVoiceConnecting ? /* @__PURE__ */ jsx2("div", { className: "mt-2 text-xs text-slate-500", children: "Connecting voice transcription..." }) : null
892
+ ] })
893
+ ] });
894
+ }
895
+ function IssueReportNoteEditor({
896
+ canUseText,
897
+ note,
898
+ normalizedNote,
899
+ isSubmitting,
900
+ copy,
901
+ onNoteChange,
902
+ onSubmit
903
+ }) {
904
+ if (!canUseText) {
905
+ return /* @__PURE__ */ jsxs("div", { className: "mt-5 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-xs text-slate-600", children: [
906
+ /* @__PURE__ */ jsx2("div", { children: getIssueNoteLengthMessage(normalizedNote, copy) }),
907
+ /* @__PURE__ */ jsx2("div", { className: "mt-1", children: "Submit is enabled after a committed transcript reaches 10-2000 characters." })
908
+ ] });
909
+ }
910
+ return /* @__PURE__ */ jsxs("div", { className: "mt-5 space-y-2", children: [
911
+ /* @__PURE__ */ jsx2(
912
+ "textarea",
913
+ {
914
+ value: note,
915
+ onChange: (event) => onNoteChange(event.target.value),
916
+ onKeyDown: (event) => {
917
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
918
+ event.preventDefault();
919
+ onSubmit();
920
+ }
921
+ },
922
+ placeholder: copy.notePlaceholder,
923
+ className: "h-36 w-full resize-none rounded-2xl border border-slate-300 px-4 py-3 text-sm text-slate-800 outline-none transition focus:border-slate-500 focus:ring-2 focus:ring-slate-200",
924
+ disabled: isSubmitting,
925
+ autoFocus: true
926
+ }
927
+ ),
928
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs text-slate-500", children: [
929
+ /* @__PURE__ */ jsx2("span", { children: getIssueNoteLengthMessage(note, copy) }),
930
+ /* @__PURE__ */ jsx2("span", { children: copy.keyboardShortcutHint })
931
+ ] })
932
+ ] });
933
+ }
934
+ function IssueReportModalDescription({
935
+ copy,
936
+ mode,
937
+ target
938
+ }) {
939
+ if (mode === "create" && target) {
940
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
941
+ copy.createDescriptionPrefix,
942
+ " ",
943
+ /* @__PURE__ */ jsx2("code", { className: "rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-700", children: target.page_url })
944
+ ] });
945
+ }
946
+ return mode === "edit" ? copy.editDescription : copy.replyDescription;
947
+ }
948
+ function IssueReportModalBody({
949
+ copy,
950
+ mode,
951
+ issue,
952
+ isHydrating,
953
+ error,
954
+ canUseVoice,
955
+ canUseText,
956
+ effectiveInputMode,
957
+ note,
958
+ normalizedNote,
959
+ isValid,
960
+ isSubmitting,
961
+ isVoiceActive,
962
+ isVoiceConnecting,
963
+ voice,
964
+ voiceTokenResult,
965
+ committedTranscript,
966
+ voiceError,
967
+ scribeError,
968
+ submitError,
969
+ onRetryHydration,
970
+ onClose,
971
+ onSelectText,
972
+ onSelectVoice,
973
+ onStartVoiceInput,
974
+ onStopVoiceInput,
975
+ onAppendTranscript,
976
+ onNoteChange,
977
+ onSubmit
978
+ }) {
979
+ if (isHydrating) {
980
+ return /* @__PURE__ */ 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: [
981
+ /* @__PURE__ */ jsx2(Spinner, { className: "h-4 w-4 animate-spin" }),
982
+ /* @__PURE__ */ jsx2("span", { children: copy.hydrateLoading })
983
+ ] });
984
+ }
985
+ if (error) {
986
+ return /* @__PURE__ */ jsxs("div", { className: "mt-5 space-y-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-4 text-sm text-rose-700", children: [
987
+ /* @__PURE__ */ jsx2("div", { children: error }),
988
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
989
+ /* @__PURE__ */ jsx2(
990
+ "button",
991
+ {
992
+ type: "button",
993
+ className: "rounded-full border border-rose-300 px-3 py-1 font-medium transition hover:bg-rose-100",
994
+ onClick: onRetryHydration,
995
+ children: copy.retryAction
996
+ }
997
+ ),
998
+ /* @__PURE__ */ jsx2(
999
+ "button",
1000
+ {
1001
+ type: "button",
1002
+ className: "rounded-full border border-slate-300 px-3 py-1 font-medium text-slate-700 transition hover:bg-slate-50",
1003
+ onClick: onClose,
1004
+ children: copy.cancelAction
1005
+ }
1006
+ )
1007
+ ] })
1008
+ ] });
1009
+ }
1010
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1011
+ mode === "create" && canUseVoice ? /* @__PURE__ */ jsx2(
1012
+ IssueReportVoicePanel,
1013
+ {
1014
+ canUseText,
1015
+ effectiveInputMode,
1016
+ isSubmitting,
1017
+ isVoiceActive,
1018
+ isVoiceConnecting,
1019
+ voice,
1020
+ voiceTokenResult,
1021
+ committedTranscript,
1022
+ voiceError,
1023
+ scribeError,
1024
+ onSelectText,
1025
+ onSelectVoice,
1026
+ onStartVoiceInput,
1027
+ onStopVoiceInput,
1028
+ onAppendTranscript
1029
+ }
1030
+ ) : null,
1031
+ mode === "reply" && issue ? /* @__PURE__ */ jsxs("div", { className: "mt-5 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3", children: [
1032
+ /* @__PURE__ */ jsx2("div", { className: "text-xs font-medium uppercase tracking-wide text-slate-500", children: copy.originalIssueLabel }),
1033
+ /* @__PURE__ */ jsx2("p", { className: "mt-1 text-sm text-slate-700", children: issue.note })
1034
+ ] }) : null,
1035
+ /* @__PURE__ */ jsx2(
1036
+ IssueReportNoteEditor,
1037
+ {
1038
+ canUseText,
1039
+ note,
1040
+ normalizedNote,
1041
+ isSubmitting,
1042
+ copy,
1043
+ onNoteChange,
1044
+ onSubmit
1045
+ }
1046
+ ),
1047
+ submitError ? /* @__PURE__ */ jsx2("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,
1048
+ /* @__PURE__ */ jsxs("div", { className: "mt-6 flex justify-end gap-3", children: [
1049
+ /* @__PURE__ */ jsx2(
1050
+ "button",
1051
+ {
1052
+ type: "button",
1053
+ className: "rounded-full border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50",
1054
+ onClick: onClose,
1055
+ disabled: isSubmitting,
1056
+ children: copy.cancelAction
1057
+ }
1058
+ ),
1059
+ /* @__PURE__ */ jsx2(
1060
+ "button",
1061
+ {
1062
+ type: "button",
1063
+ className: cn(
1064
+ "rounded-full px-4 py-2 text-sm font-semibold text-white transition",
1065
+ isValid && !isSubmitting ? "bg-slate-900 hover:bg-slate-800" : "cursor-not-allowed bg-slate-300"
1066
+ ),
1067
+ onClick: onSubmit,
1068
+ disabled: !isValid || isSubmitting,
1069
+ children: isSubmitting ? copy.submittingAction : copy.submitAction
1070
+ }
1071
+ )
1072
+ ] })
1073
+ ] });
1074
+ }
1075
+ function useIssueReportVoiceCapture({
1076
+ canUseVoice,
1077
+ defaultInputMode,
1078
+ isSubmitting,
1079
+ voice
1080
+ }) {
1081
+ const [inputMode, setInputMode] = useState2(defaultInputMode);
1082
+ const [voiceTokenResult, setVoiceTokenResult] = useState2(null);
1083
+ const [voiceSubmitMetadata, setVoiceSubmitMetadata] = useState2(null);
1084
+ const [voiceError, setVoiceError] = useState2(null);
1085
+ const {
1086
+ status: scribeStatus,
1087
+ isConnected,
1088
+ isTranscribing,
1089
+ committedTranscripts,
1090
+ error: scribeError,
1091
+ connect: connectScribe,
1092
+ disconnect: disconnectScribe
1093
+ } = useScribe({
1094
+ autoConnect: false,
1095
+ modelId: voice.modelId,
1096
+ microphone: voice.microphone
1097
+ });
1098
+ const committedTranscript = useMemo2(
1099
+ () => getCommittedTranscriptText(committedTranscripts),
1100
+ [committedTranscripts]
1101
+ );
1102
+ const isVoiceConnecting = scribeStatus === "connecting";
1103
+ const isVoiceActive = isConnected || isTranscribing;
1104
+ const resetVoiceCapture = useCallback2(() => {
1105
+ disconnectScribe();
1106
+ setInputMode(defaultInputMode);
1107
+ setVoiceTokenResult(null);
1108
+ setVoiceSubmitMetadata(null);
1109
+ setVoiceError(null);
1110
+ }, [defaultInputMode, disconnectScribe]);
1111
+ const startVoiceInput = useCallback2(
1112
+ async (createVoiceToken) => {
1113
+ if (!canUseVoice || isSubmitting || isVoiceActive || isVoiceConnecting) {
1114
+ return;
1115
+ }
1116
+ if (!createVoiceToken) {
1117
+ setVoiceError("Voice input is unavailable for this client.");
1118
+ return;
1119
+ }
1120
+ setVoiceError(null);
1121
+ try {
1122
+ const tokenResult = await createVoiceToken();
1123
+ setVoiceTokenResult(tokenResult);
1124
+ setVoiceSubmitMetadata(tokenResult);
1125
+ await connectScribe({
1126
+ token: tokenResult.token,
1127
+ modelId: tokenResult.model_id,
1128
+ microphone: voice.microphone
1129
+ });
1130
+ setInputMode("voice");
1131
+ } catch (startError) {
1132
+ setVoiceError(resolveErrorMessage(startError, "Failed to start voice input."));
1133
+ }
1134
+ },
1135
+ [
1136
+ canUseVoice,
1137
+ connectScribe,
1138
+ isSubmitting,
1139
+ isVoiceActive,
1140
+ isVoiceConnecting,
1141
+ voice.microphone
1142
+ ]
1143
+ );
1144
+ const appendTranscript = useCallback2(
1145
+ (setNote) => {
1146
+ if (!committedTranscript.trim()) {
1147
+ return;
1148
+ }
1149
+ setNote((current) => appendTranscriptToNote(current, committedTranscript));
1150
+ setInputMode("text");
1151
+ },
1152
+ [committedTranscript]
1153
+ );
1154
+ return {
1155
+ inputMode,
1156
+ setInputMode,
1157
+ voiceTokenResult,
1158
+ voiceSubmitMetadata,
1159
+ committedTranscript,
1160
+ voiceError,
1161
+ scribeError,
1162
+ isVoiceConnecting,
1163
+ isVoiceActive,
1164
+ resetVoiceCapture,
1165
+ startVoiceInput,
1166
+ stopVoiceInput: disconnectScribe,
1167
+ appendTranscript
1168
+ };
1169
+ }
520
1170
  function IssueReportModeBanner() {
521
1171
  const { copy } = useIssueReporting();
522
1172
  const reportMode = useReportMode();
523
1173
  if (!reportMode?.isReportMode) {
524
1174
  return null;
525
1175
  }
526
- return /* @__PURE__ */ jsx2("div", { className: "fixed inset-x-4 top-4 z-[70] flex justify-center", children: /* @__PURE__ */ jsx2("div", { className: "max-w-xl rounded-full border border-amber-300 bg-amber-50/95 px-4 py-3 text-sm text-amber-950 shadow-lg backdrop-blur", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-3", children: [
1176
+ return /* @__PURE__ */ jsx2("div", { className: cn("fixed inset-x-4 top-4 flex justify-center", Z_BANNER), children: /* @__PURE__ */ jsx2("div", { className: "max-w-xl rounded-full border border-amber-300 bg-amber-50/95 px-4 py-3 text-sm text-amber-950 shadow-lg backdrop-blur", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-3", children: [
527
1177
  /* @__PURE__ */ jsx2("div", { className: "font-medium", children: copy.reportModeTitle }),
528
1178
  /* @__PURE__ */ jsx2("div", { className: "text-amber-900/80", children: copy.reportModeDescription }),
529
1179
  /* @__PURE__ */ jsx2(
@@ -586,13 +1236,14 @@ function IssueList({
586
1236
  "span",
587
1237
  {
588
1238
  className: cn(
589
- "rounded-full px-2 py-0.5 text-[11px] font-medium",
1239
+ "rounded-full px-2 py-0.5 font-medium",
1240
+ BADGE_TEXT,
590
1241
  getIssueStatusClassName(issue.status)
591
1242
  ),
592
1243
  children: getIssueStatusBadgeLabel(issue.status)
593
1244
  }
594
1245
  ),
595
- /* @__PURE__ */ jsx2("span", { className: "rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-600", children: getIssueOriginText(issue, copy) }),
1246
+ /* @__PURE__ */ jsx2("span", { className: cn("rounded-full bg-slate-100 px-2 py-0.5 font-medium text-slate-600", BADGE_TEXT), children: getIssueOriginText(issue, copy) }),
596
1247
  /* @__PURE__ */ jsx2("span", { className: "text-xs text-slate-400", children: formatRelativeTime(issue.updated_at) })
597
1248
  ] })
598
1249
  ] }),
@@ -623,10 +1274,13 @@ function IssueReportPopover({ children }) {
623
1274
  openPopover,
624
1275
  closePopover,
625
1276
  enterReportMode,
1277
+ openPageIssueModal,
626
1278
  openExistingIssueModal,
627
1279
  scope,
628
1280
  setScope,
629
- allowTenantScope
1281
+ allowTenantScope,
1282
+ createMode,
1283
+ hasRegisteredTargets
630
1284
  } = useIssueReporting();
631
1285
  const history = useIssueReportingHistory("all");
632
1286
  const status = useIssueReportingStatus();
@@ -640,6 +1294,9 @@ function IssueReportPopover({ children }) {
640
1294
  [allItems]
641
1295
  );
642
1296
  const statusSummary = status.data ? `${status.data.open_count} open \xB7 ${status.data.recent_resolved_count} recently resolved` : "Status reflects the active scope.";
1297
+ const showSectionFirst = createMode === "surface_required" || createMode === "surface_preferred" && hasRegisteredTargets;
1298
+ const sectionActionDisabled = !hasRegisteredTargets;
1299
+ const helperText = createMode === "surface_required" ? hasRegisteredTargets ? copy.surfaceRequiredDescription : copy.specificSectionUnavailableDescription : createMode === "surface_preferred" && !hasRegisteredTargets ? copy.specificSectionUnavailableDescription : null;
643
1300
  return /* @__PURE__ */ jsxs(
644
1301
  Popover.Root,
645
1302
  {
@@ -653,26 +1310,78 @@ function IssueReportPopover({ children }) {
653
1310
  align: "end",
654
1311
  side: "top",
655
1312
  sideOffset: 12,
656
- className: "z-[70] w-[360px] rounded-3xl border border-slate-200 bg-white p-4 shadow-[0_18px_48px_rgba(15,23,42,0.18)]",
1313
+ className: cn(
1314
+ "rounded-3xl border border-slate-200 bg-white p-4",
1315
+ Z_POPOVER,
1316
+ POPOVER_WIDTH,
1317
+ POPOVER_SHADOW
1318
+ ),
657
1319
  children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
658
1320
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
659
1321
  /* @__PURE__ */ jsxs("div", { children: [
660
1322
  /* @__PURE__ */ jsx2("h3", { className: "text-sm font-semibold text-slate-900", children: copy.popoverTitle }),
661
1323
  /* @__PURE__ */ jsx2("p", { className: "mt-1 text-xs text-slate-500", children: statusSummary })
662
1324
  ] }),
663
- /* @__PURE__ */ jsx2(
664
- "button",
665
- {
666
- type: "button",
667
- className: "rounded-full bg-slate-900 px-3 py-2 text-xs font-semibold text-white transition hover:bg-slate-800",
668
- onClick: () => {
669
- closePopover();
670
- enterReportMode();
671
- },
672
- children: copy.reportNewAction
673
- }
674
- )
1325
+ /* @__PURE__ */ jsx2("div", { className: "flex flex-wrap justify-end gap-2", children: showSectionFirst ? /* @__PURE__ */ jsxs(Fragment, { children: [
1326
+ /* @__PURE__ */ jsx2(
1327
+ "button",
1328
+ {
1329
+ type: "button",
1330
+ className: cn(
1331
+ "rounded-full px-3 py-2 text-xs font-semibold transition",
1332
+ sectionActionDisabled ? "cursor-not-allowed bg-slate-200 text-slate-500" : "bg-slate-900 text-white hover:bg-slate-800"
1333
+ ),
1334
+ onClick: () => {
1335
+ if (sectionActionDisabled) {
1336
+ return;
1337
+ }
1338
+ closePopover();
1339
+ enterReportMode();
1340
+ },
1341
+ disabled: sectionActionDisabled,
1342
+ children: createMode === "surface_required" ? copy.reportNewAction : copy.reportSpecificAction
1343
+ }
1344
+ ),
1345
+ createMode !== "surface_required" ? /* @__PURE__ */ jsx2(
1346
+ "button",
1347
+ {
1348
+ type: "button",
1349
+ className: "rounded-full border border-slate-200 px-3 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-50",
1350
+ onClick: () => {
1351
+ closePopover();
1352
+ openPageIssueModal();
1353
+ },
1354
+ children: copy.reportPageAction
1355
+ }
1356
+ ) : null
1357
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1358
+ /* @__PURE__ */ jsx2(
1359
+ "button",
1360
+ {
1361
+ type: "button",
1362
+ className: "rounded-full bg-slate-900 px-3 py-2 text-xs font-semibold text-white transition hover:bg-slate-800",
1363
+ onClick: () => {
1364
+ closePopover();
1365
+ openPageIssueModal();
1366
+ },
1367
+ children: createMode === "general_page" ? copy.reportPageAction : copy.reportNewAction
1368
+ }
1369
+ ),
1370
+ hasRegisteredTargets ? /* @__PURE__ */ jsx2(
1371
+ "button",
1372
+ {
1373
+ type: "button",
1374
+ className: "rounded-full border border-slate-200 px-3 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-50",
1375
+ onClick: () => {
1376
+ closePopover();
1377
+ enterReportMode();
1378
+ },
1379
+ children: copy.reportSpecificAction
1380
+ }
1381
+ ) : null
1382
+ ] }) })
675
1383
  ] }),
1384
+ helperText ? /* @__PURE__ */ jsx2("div", { className: "rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-900", children: helperText }) : null,
676
1385
  status.error ? /* @__PURE__ */ jsxs("div", { className: "space-y-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-4 text-sm text-rose-700", children: [
677
1386
  /* @__PURE__ */ jsx2("div", { children: status.error.message || copy.statusLoadFailed }),
678
1387
  /* @__PURE__ */ jsx2(
@@ -686,7 +1395,7 @@ function IssueReportPopover({ children }) {
686
1395
  )
687
1396
  ] }) : null,
688
1397
  allowTenantScope ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
689
- /* @__PURE__ */ jsx2("div", { className: "text-[11px] font-medium uppercase tracking-wide text-slate-500", children: copy.scopeLabel }),
1398
+ /* @__PURE__ */ jsx2("div", { className: cn("font-medium uppercase tracking-wide text-slate-500", LABEL_TEXT), children: copy.scopeLabel }),
690
1399
  /* @__PURE__ */ jsx2("div", { className: "flex gap-2", children: [
691
1400
  ["tenant", copy.scopeTenant],
692
1401
  ["mine", copy.scopeMine]
@@ -742,18 +1451,46 @@ function IssueReportPopover({ children }) {
742
1451
  }
743
1452
  function IssueReportModal() {
744
1453
  const {
1454
+ client,
745
1455
  copy,
746
1456
  reporterRoleHint,
747
1457
  modalState,
748
1458
  closeModal,
749
- retryModalHydration
1459
+ retryModalHydration,
1460
+ inputModes,
1461
+ defaultInputMode,
1462
+ voice
750
1463
  } = useIssueReporting();
751
1464
  const { createMutation, updateMutation, replyMutation } = useIssueReportingMutations();
752
1465
  const [note, setNote] = useState2("");
753
1466
  const [submitError, setSubmitError] = useState2(null);
754
1467
  const { isOpen, mode, issue, target, isHydrating, error } = modalState;
1468
+ const canUseVoice = mode === "create" && inputModes.includes("voice");
1469
+ const canUseText = mode !== "create" || inputModes.includes("text");
1470
+ const isSubmitting = createMutation.isPending || updateMutation.isPending || replyMutation.isPending;
1471
+ const {
1472
+ inputMode,
1473
+ setInputMode,
1474
+ voiceTokenResult,
1475
+ voiceSubmitMetadata,
1476
+ committedTranscript,
1477
+ voiceError,
1478
+ scribeError,
1479
+ isVoiceConnecting,
1480
+ isVoiceActive,
1481
+ resetVoiceCapture,
1482
+ startVoiceInput,
1483
+ stopVoiceInput,
1484
+ appendTranscript
1485
+ } = useIssueReportVoiceCapture({
1486
+ canUseVoice,
1487
+ defaultInputMode,
1488
+ isSubmitting,
1489
+ voice
1490
+ });
755
1491
  useEffect2(() => {
756
1492
  if (!isOpen) {
1493
+ resetVoiceCapture();
757
1494
  setNote("");
758
1495
  setSubmitError(null);
759
1496
  return;
@@ -763,140 +1500,145 @@ function IssueReportModal() {
763
1500
  } else {
764
1501
  setNote("");
765
1502
  }
1503
+ resetVoiceCapture();
766
1504
  setSubmitError(null);
767
- }, [isOpen, mode, issue]);
768
- const isValid = note.trim().length >= 10 && note.trim().length <= 2e3;
769
- const isSubmitting = createMutation.isPending || updateMutation.isPending || replyMutation.isPending;
1505
+ }, [isOpen, issue, mode, resetVoiceCapture]);
1506
+ const effectiveInputMode = mode !== "create" ? "text" : canUseVoice && inputMode === "voice" ? "voice" : "text";
1507
+ const normalizedNote = effectiveInputMode === "voice" ? committedTranscript.trim() : note.trim();
1508
+ const isValid = normalizedNote.length >= 10 && normalizedNote.length <= 2e3;
770
1509
  const title = mode === "create" ? `${copy.createTitlePrefix}: ${target?.component_label ?? ""}` : mode === "edit" ? `${copy.editTitlePrefix}: ${target?.component_label ?? ""}` : `${copy.replyTitlePrefix}: ${target?.component_label ?? ""}`;
1510
+ const handleCloseModal = () => {
1511
+ resetVoiceCapture();
1512
+ closeModal();
1513
+ };
1514
+ const handleStartVoiceInput = async () => {
1515
+ await startVoiceInput(client.issueReporting.createVoiceToken);
1516
+ };
1517
+ const handleStopVoiceInput = () => {
1518
+ stopVoiceInput();
1519
+ };
1520
+ const handleAppendTranscript = () => {
1521
+ appendTranscript(setNote);
1522
+ };
771
1523
  const handleSubmit = async () => {
772
1524
  if (!target || !isValid || isSubmitting) {
773
1525
  return;
774
1526
  }
775
1527
  setSubmitError(null);
776
1528
  try {
777
- const normalizedNote = note.trim();
1529
+ const noteForSubmit = normalizedNote;
1530
+ const effectiveVoiceMetadata = effectiveInputMode === "voice" ? voiceSubmitMetadata ?? {
1531
+ provider: voice.provider,
1532
+ token: "",
1533
+ model_id: voice.modelId,
1534
+ expires_in_seconds: 0,
1535
+ retain_audio: false,
1536
+ attach_transcript: true
1537
+ } : null;
778
1538
  if (mode === "create") {
779
1539
  await createMutation.mutateAsync({
780
- target,
781
- note: normalizedNote,
1540
+ target: effectiveVoiceMetadata && effectiveInputMode === "voice" ? {
1541
+ ...target,
1542
+ metadata: {
1543
+ ...target.metadata ?? {},
1544
+ capture: {
1545
+ input_mode: "voice",
1546
+ provider: effectiveVoiceMetadata.provider,
1547
+ model_id: effectiveVoiceMetadata.model_id,
1548
+ retain_audio: effectiveVoiceMetadata.retain_audio,
1549
+ attach_transcript: effectiveVoiceMetadata.attach_transcript
1550
+ }
1551
+ }
1552
+ } : target,
1553
+ note: noteForSubmit,
782
1554
  reporter_role_hint: reporterRoleHint
783
1555
  });
784
1556
  } else if (mode === "edit" && issue) {
785
1557
  await updateMutation.mutateAsync({
786
1558
  issueReportId: issue.id,
787
- note: normalizedNote
1559
+ note: noteForSubmit
788
1560
  });
789
1561
  } else if (mode === "reply" && issue) {
790
1562
  await replyMutation.mutateAsync({
791
1563
  issueReportId: issue.id,
792
- note: normalizedNote,
1564
+ note: noteForSubmit,
793
1565
  reporterRoleHint
794
1566
  });
795
1567
  }
1568
+ resetVoiceCapture();
796
1569
  closeModal();
797
1570
  } catch (submissionError) {
798
1571
  setSubmitError(resolveErrorMessage(submissionError, "Failed to submit issue report"));
799
1572
  }
800
1573
  };
801
- return /* @__PURE__ */ jsx2(Dialog.Root, { open: isOpen, onOpenChange: (open) => !open && closeModal(), children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
802
- /* @__PURE__ */ jsx2(Dialog.Overlay, { className: "fixed inset-0 z-[80] bg-slate-950/45 backdrop-blur-sm" }),
803
- /* @__PURE__ */ jsxs(Dialog.Content, { className: "fixed left-1/2 top-1/2 z-[81] w-[calc(100vw-2rem)] max-w-xl -translate-x-1/2 -translate-y-1/2 rounded-[28px] border border-slate-200 bg-white p-6 shadow-[0_28px_80px_rgba(15,23,42,0.24)] focus:outline-none", children: [
804
- /* @__PURE__ */ jsx2(Dialog.Title, { className: "text-lg font-semibold text-slate-950", children: title }),
805
- /* @__PURE__ */ jsx2(Dialog.Description, { className: "mt-2 text-sm text-slate-600", children: mode === "create" && target ? /* @__PURE__ */ jsxs(Fragment, { children: [
806
- copy.createDescriptionPrefix,
807
- " ",
808
- /* @__PURE__ */ jsx2("code", { className: "rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-700", children: target.page_url })
809
- ] }) : mode === "edit" ? copy.editDescription : copy.replyDescription }),
810
- isHydrating ? /* @__PURE__ */ 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: [
811
- /* @__PURE__ */ jsx2(Spinner, { className: "h-4 w-4 animate-spin" }),
812
- /* @__PURE__ */ jsx2("span", { children: copy.hydrateLoading })
813
- ] }) : error ? /* @__PURE__ */ jsxs("div", { className: "mt-5 space-y-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-4 text-sm text-rose-700", children: [
814
- /* @__PURE__ */ jsx2("div", { children: error }),
815
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
816
- /* @__PURE__ */ jsx2(
817
- "button",
818
- {
819
- type: "button",
820
- className: "rounded-full border border-rose-300 px-3 py-1 font-medium transition hover:bg-rose-100",
821
- onClick: () => retryModalHydration(),
822
- children: copy.retryAction
823
- }
824
- ),
825
- /* @__PURE__ */ jsx2(
826
- "button",
1574
+ return /* @__PURE__ */ jsx2(Dialog.Root, { open: isOpen, onOpenChange: (open) => !open && handleCloseModal(), children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
1575
+ /* @__PURE__ */ jsx2(Dialog.Overlay, { className: cn("fixed inset-0 bg-slate-950/45 backdrop-blur-sm", Z_MODAL_OVERLAY) }),
1576
+ /* @__PURE__ */ jsxs(
1577
+ Dialog.Content,
1578
+ {
1579
+ className: cn(
1580
+ "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",
1581
+ Z_MODAL_CONTENT,
1582
+ MODAL_WIDTH,
1583
+ MODAL_RADIUS,
1584
+ MODAL_SHADOW
1585
+ ),
1586
+ children: [
1587
+ /* @__PURE__ */ jsx2(Dialog.Title, { className: "text-lg font-semibold text-slate-950", children: title }),
1588
+ /* @__PURE__ */ jsx2(Dialog.Description, { className: "mt-2 text-sm text-slate-600", children: /* @__PURE__ */ jsx2(
1589
+ IssueReportModalDescription,
827
1590
  {
828
- type: "button",
829
- className: "rounded-full border border-slate-300 px-3 py-1 font-medium text-slate-700 transition hover:bg-slate-50",
830
- onClick: closeModal,
831
- children: copy.cancelAction
1591
+ copy,
1592
+ mode,
1593
+ target
832
1594
  }
833
- )
834
- ] })
835
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
836
- mode === "reply" && issue ? /* @__PURE__ */ jsxs("div", { className: "mt-5 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3", children: [
837
- /* @__PURE__ */ jsx2("div", { className: "text-xs font-medium uppercase tracking-wide text-slate-500", children: copy.originalIssueLabel }),
838
- /* @__PURE__ */ jsx2("p", { className: "mt-1 text-sm text-slate-700", children: issue.note })
839
- ] }) : null,
840
- /* @__PURE__ */ jsxs("div", { className: "mt-5 space-y-2", children: [
1595
+ ) }),
841
1596
  /* @__PURE__ */ jsx2(
842
- "textarea",
1597
+ IssueReportModalBody,
843
1598
  {
844
- value: note,
845
- onChange: (event) => setNote(event.target.value),
846
- onKeyDown: (event) => {
847
- if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
848
- event.preventDefault();
849
- void handleSubmit();
850
- }
851
- },
852
- placeholder: copy.notePlaceholder,
853
- className: "h-36 w-full resize-none rounded-2xl border border-slate-300 px-4 py-3 text-sm text-slate-800 outline-none transition focus:border-slate-500 focus:ring-2 focus:ring-slate-200",
854
- disabled: isSubmitting,
855
- autoFocus: true
1599
+ copy,
1600
+ mode,
1601
+ issue,
1602
+ isHydrating,
1603
+ error,
1604
+ canUseVoice,
1605
+ canUseText,
1606
+ effectiveInputMode,
1607
+ note,
1608
+ normalizedNote,
1609
+ isValid,
1610
+ isSubmitting,
1611
+ isVoiceActive,
1612
+ isVoiceConnecting,
1613
+ voice,
1614
+ voiceTokenResult,
1615
+ committedTranscript,
1616
+ voiceError,
1617
+ scribeError,
1618
+ submitError,
1619
+ onRetryHydration: () => void retryModalHydration(),
1620
+ onClose: handleCloseModal,
1621
+ onSelectText: () => setInputMode("text"),
1622
+ onSelectVoice: () => setInputMode("voice"),
1623
+ onStartVoiceInput: () => void handleStartVoiceInput(),
1624
+ onStopVoiceInput: handleStopVoiceInput,
1625
+ onAppendTranscript: handleAppendTranscript,
1626
+ onNoteChange: setNote,
1627
+ onSubmit: () => void handleSubmit()
856
1628
  }
857
1629
  ),
858
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs text-slate-500", children: [
859
- /* @__PURE__ */ jsx2("span", { children: getIssueNoteLengthMessage(note, copy) }),
860
- /* @__PURE__ */ jsx2("span", { children: copy.keyboardShortcutHint })
861
- ] })
862
- ] }),
863
- submitError ? /* @__PURE__ */ jsx2("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,
864
- /* @__PURE__ */ jsxs("div", { className: "mt-6 flex justify-end gap-3", children: [
865
- /* @__PURE__ */ jsx2(
866
- "button",
867
- {
868
- type: "button",
869
- className: "rounded-full border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50",
870
- onClick: closeModal,
871
- disabled: isSubmitting,
872
- children: copy.cancelAction
873
- }
874
- ),
875
- /* @__PURE__ */ jsx2(
1630
+ /* @__PURE__ */ jsx2(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx2(
876
1631
  "button",
877
1632
  {
878
1633
  type: "button",
879
- className: cn(
880
- "rounded-full px-4 py-2 text-sm font-semibold text-white transition",
881
- isValid && !isSubmitting ? "bg-slate-900 hover:bg-slate-800" : "cursor-not-allowed bg-slate-300"
882
- ),
883
- onClick: () => void handleSubmit(),
884
- disabled: !isValid || isSubmitting,
885
- children: isSubmitting ? copy.submittingAction : copy.submitAction
1634
+ className: "absolute right-4 top-4 inline-flex h-9 w-9 items-center justify-center rounded-full border border-slate-200 text-slate-500 transition hover:bg-slate-50 hover:text-slate-900",
1635
+ "aria-label": "Close issue report modal",
1636
+ children: /* @__PURE__ */ jsx2(X, { className: "h-4 w-4" })
886
1637
  }
887
- )
888
- ] })
889
- ] }),
890
- /* @__PURE__ */ jsx2(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx2(
891
- "button",
892
- {
893
- type: "button",
894
- className: "absolute right-4 top-4 inline-flex h-9 w-9 items-center justify-center rounded-full border border-slate-200 text-slate-500 transition hover:bg-slate-50 hover:text-slate-900",
895
- "aria-label": "Close issue report modal",
896
- children: /* @__PURE__ */ jsx2(X, { className: "h-4 w-4" })
897
- }
898
- ) })
899
- ] })
1638
+ ) })
1639
+ ]
1640
+ }
1641
+ )
900
1642
  ] }) });
901
1643
  }
902
1644
  function FloatingIssueReportButton({
@@ -918,7 +1660,7 @@ function FloatingIssueReportButton({
918
1660
  }
919
1661
  return /* @__PURE__ */ jsxs(Fragment, { children: [
920
1662
  /* @__PURE__ */ jsx2(IssueReportModeBanner, {}),
921
- !isReportMode ? /* @__PURE__ */ jsx2("div", { className: cn("fixed bottom-12 right-4 z-[65]", positionClassName), children: /* @__PURE__ */ jsx2(IssueReportPopover, { children: /* @__PURE__ */ jsx2(
1663
+ !isReportMode ? /* @__PURE__ */ jsx2("div", { className: cn("fixed bottom-12 right-4", Z_FLOATING_BUTTON, positionClassName), children: /* @__PURE__ */ jsx2(IssueReportPopover, { children: /* @__PURE__ */ jsx2(
922
1664
  "button",
923
1665
  {
924
1666
  type: "button",
@@ -949,9 +1691,23 @@ function ReportableSection({
949
1691
  }) {
950
1692
  const reportMode = useReportMode();
951
1693
  const isSelectable = Boolean(reportMode?.isReportMode);
952
- return /* @__PURE__ */ jsx2(
1694
+ const targetId = React2.useRef(/* @__PURE__ */ Symbol("reportable-section"));
1695
+ const elementRef = React2.useRef(null);
1696
+ useEffect2(() => {
1697
+ if (!reportMode) {
1698
+ return;
1699
+ }
1700
+ reportMode.registerTarget(targetId.current, reportableName, () => elementRef.current);
1701
+ return () => {
1702
+ reportMode.unregisterTarget(targetId.current);
1703
+ };
1704
+ }, [reportMode, reportableName]);
1705
+ return React2.createElement(
953
1706
  Component,
954
1707
  {
1708
+ ref: (node) => {
1709
+ elementRef.current = node;
1710
+ },
955
1711
  className: cn(
956
1712
  className,
957
1713
  isSelectable && "cursor-pointer ring-2 ring-amber-400 transition hover:ring-amber-500"
@@ -966,13 +1722,14 @@ function ReportableSection({
966
1722
  }
967
1723
  } : void 0,
968
1724
  role: isSelectable ? "button" : void 0,
969
- tabIndex: isSelectable ? 0 : void 0,
970
- children
971
- }
1725
+ tabIndex: isSelectable ? 0 : void 0
1726
+ },
1727
+ children
972
1728
  );
973
1729
  }
974
1730
  export {
975
1731
  FloatingIssueReportButton,
1732
+ IssueReportingPageConfig,
976
1733
  IssueReportingProvider,
977
1734
  ReportModeContext,
978
1735
  ReportableSection,