spaps-issue-reporting-react 0.1.3 → 0.1.5

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 React2, { 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 {
@@ -41,6 +44,9 @@ import { jsx } from "react/jsx-runtime";
41
44
  var LIST_LIMIT = 200;
42
45
  var NOTE_MIN_LENGTH = 10;
43
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";
44
50
  var initialModalState = {
45
51
  isOpen: false,
46
52
  mode: "create",
@@ -133,6 +139,25 @@ function resolveInitialScope(defaultScope, allowTenantScope) {
133
139
  }
134
140
  return "mine";
135
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
+ }
136
161
  function normalizeTarget(target, getPageUrl) {
137
162
  if (typeof target === "string") {
138
163
  const normalized = target.trim();
@@ -391,6 +416,9 @@ function IssueReportingProvider({
391
416
  defaultScope,
392
417
  allowTenantScope = false,
393
418
  defaultCreateMode = "general_page",
419
+ inputModes,
420
+ defaultInputMode,
421
+ voice,
394
422
  copy,
395
423
  children
396
424
  }) {
@@ -412,6 +440,23 @@ function IssueReportingProvider({
412
440
  const [registeredTargets, setRegisteredTargets] = useState(
413
441
  []
414
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
+ );
415
460
  useEffect(() => {
416
461
  setScope(resolveInitialScope(defaultScope, allowTenantScope));
417
462
  }, [allowTenantScope, defaultScope]);
@@ -599,7 +644,10 @@ function IssueReportingProvider({
599
644
  scope,
600
645
  setScope,
601
646
  allowTenantScope,
602
- createMode
647
+ createMode,
648
+ inputModes: resolvedInputModes,
649
+ defaultInputMode: resolvedDefaultInputMode,
650
+ voice: resolvedVoiceConfig
603
651
  }),
604
652
  [
605
653
  allowTenantScope,
@@ -621,6 +669,9 @@ function IssueReportingProvider({
621
669
  principalId,
622
670
  reporterRoleHint,
623
671
  retryModalHydration,
672
+ resolvedDefaultInputMode,
673
+ resolvedInputModes,
674
+ resolvedVoiceConfig,
624
675
  scope,
625
676
  selectPanel,
626
677
  startNewIssue
@@ -665,6 +716,18 @@ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
665
716
  function cn(...values) {
666
717
  return values.filter(Boolean).join(" ");
667
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]";
668
731
  function truncate(value, max = 80) {
669
732
  if (value.length <= max) {
670
733
  return value;
@@ -685,6 +748,16 @@ function resolveErrorMessage(error, fallback) {
685
748
  }
686
749
  return fallback;
687
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
+ }
688
761
  function resolveReporterId(issue) {
689
762
  return issue.reporter_principal_id ?? issue.reporter_user_id ?? null;
690
763
  }
@@ -717,13 +790,390 @@ function getIssueOriginText(issue, copy) {
717
790
  const humanName = issue.reporter_display_name?.trim();
718
791
  return humanName ? `${copy.originHumanLabel} \xB7 ${humanName}` : copy.originHumanLabel;
719
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
+ }
720
1170
  function IssueReportModeBanner() {
721
1171
  const { copy } = useIssueReporting();
722
1172
  const reportMode = useReportMode();
723
1173
  if (!reportMode?.isReportMode) {
724
1174
  return null;
725
1175
  }
726
- 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: [
727
1177
  /* @__PURE__ */ jsx2("div", { className: "font-medium", children: copy.reportModeTitle }),
728
1178
  /* @__PURE__ */ jsx2("div", { className: "text-amber-900/80", children: copy.reportModeDescription }),
729
1179
  /* @__PURE__ */ jsx2(
@@ -786,13 +1236,14 @@ function IssueList({
786
1236
  "span",
787
1237
  {
788
1238
  className: cn(
789
- "rounded-full px-2 py-0.5 text-[11px] font-medium",
1239
+ "rounded-full px-2 py-0.5 font-medium",
1240
+ BADGE_TEXT,
790
1241
  getIssueStatusClassName(issue.status)
791
1242
  ),
792
1243
  children: getIssueStatusBadgeLabel(issue.status)
793
1244
  }
794
1245
  ),
795
- /* @__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) }),
796
1247
  /* @__PURE__ */ jsx2("span", { className: "text-xs text-slate-400", children: formatRelativeTime(issue.updated_at) })
797
1248
  ] })
798
1249
  ] }),
@@ -859,7 +1310,12 @@ function IssueReportPopover({ children }) {
859
1310
  align: "end",
860
1311
  side: "top",
861
1312
  sideOffset: 12,
862
- 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
+ ),
863
1319
  children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
864
1320
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
865
1321
  /* @__PURE__ */ jsxs("div", { children: [
@@ -939,7 +1395,7 @@ function IssueReportPopover({ children }) {
939
1395
  )
940
1396
  ] }) : null,
941
1397
  allowTenantScope ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
942
- /* @__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 }),
943
1399
  /* @__PURE__ */ jsx2("div", { className: "flex gap-2", children: [
944
1400
  ["tenant", copy.scopeTenant],
945
1401
  ["mine", copy.scopeMine]
@@ -995,18 +1451,46 @@ function IssueReportPopover({ children }) {
995
1451
  }
996
1452
  function IssueReportModal() {
997
1453
  const {
1454
+ client,
998
1455
  copy,
999
1456
  reporterRoleHint,
1000
1457
  modalState,
1001
1458
  closeModal,
1002
- retryModalHydration
1459
+ retryModalHydration,
1460
+ inputModes,
1461
+ defaultInputMode,
1462
+ voice
1003
1463
  } = useIssueReporting();
1004
1464
  const { createMutation, updateMutation, replyMutation } = useIssueReportingMutations();
1005
1465
  const [note, setNote] = useState2("");
1006
1466
  const [submitError, setSubmitError] = useState2(null);
1007
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
+ });
1008
1491
  useEffect2(() => {
1009
1492
  if (!isOpen) {
1493
+ resetVoiceCapture();
1010
1494
  setNote("");
1011
1495
  setSubmitError(null);
1012
1496
  return;
@@ -1016,140 +1500,145 @@ function IssueReportModal() {
1016
1500
  } else {
1017
1501
  setNote("");
1018
1502
  }
1503
+ resetVoiceCapture();
1019
1504
  setSubmitError(null);
1020
- }, [isOpen, mode, issue]);
1021
- const isValid = note.trim().length >= 10 && note.trim().length <= 2e3;
1022
- 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;
1023
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
+ };
1024
1523
  const handleSubmit = async () => {
1025
1524
  if (!target || !isValid || isSubmitting) {
1026
1525
  return;
1027
1526
  }
1028
1527
  setSubmitError(null);
1029
1528
  try {
1030
- 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;
1031
1538
  if (mode === "create") {
1032
1539
  await createMutation.mutateAsync({
1033
- target,
1034
- 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,
1035
1554
  reporter_role_hint: reporterRoleHint
1036
1555
  });
1037
1556
  } else if (mode === "edit" && issue) {
1038
1557
  await updateMutation.mutateAsync({
1039
1558
  issueReportId: issue.id,
1040
- note: normalizedNote
1559
+ note: noteForSubmit
1041
1560
  });
1042
1561
  } else if (mode === "reply" && issue) {
1043
1562
  await replyMutation.mutateAsync({
1044
1563
  issueReportId: issue.id,
1045
- note: normalizedNote,
1564
+ note: noteForSubmit,
1046
1565
  reporterRoleHint
1047
1566
  });
1048
1567
  }
1568
+ resetVoiceCapture();
1049
1569
  closeModal();
1050
1570
  } catch (submissionError) {
1051
1571
  setSubmitError(resolveErrorMessage(submissionError, "Failed to submit issue report"));
1052
1572
  }
1053
1573
  };
1054
- return /* @__PURE__ */ jsx2(Dialog.Root, { open: isOpen, onOpenChange: (open) => !open && closeModal(), children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
1055
- /* @__PURE__ */ jsx2(Dialog.Overlay, { className: "fixed inset-0 z-[80] bg-slate-950/45 backdrop-blur-sm" }),
1056
- /* @__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: [
1057
- /* @__PURE__ */ jsx2(Dialog.Title, { className: "text-lg font-semibold text-slate-950", children: title }),
1058
- /* @__PURE__ */ jsx2(Dialog.Description, { className: "mt-2 text-sm text-slate-600", children: mode === "create" && target ? /* @__PURE__ */ jsxs(Fragment, { children: [
1059
- copy.createDescriptionPrefix,
1060
- " ",
1061
- /* @__PURE__ */ jsx2("code", { className: "rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-700", children: target.page_url })
1062
- ] }) : mode === "edit" ? copy.editDescription : copy.replyDescription }),
1063
- 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: [
1064
- /* @__PURE__ */ jsx2(Spinner, { className: "h-4 w-4 animate-spin" }),
1065
- /* @__PURE__ */ jsx2("span", { children: copy.hydrateLoading })
1066
- ] }) : 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: [
1067
- /* @__PURE__ */ jsx2("div", { children: error }),
1068
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1069
- /* @__PURE__ */ jsx2(
1070
- "button",
1071
- {
1072
- type: "button",
1073
- className: "rounded-full border border-rose-300 px-3 py-1 font-medium transition hover:bg-rose-100",
1074
- onClick: () => retryModalHydration(),
1075
- children: copy.retryAction
1076
- }
1077
- ),
1078
- /* @__PURE__ */ jsx2(
1079
- "button",
1080
- {
1081
- type: "button",
1082
- className: "rounded-full border border-slate-300 px-3 py-1 font-medium text-slate-700 transition hover:bg-slate-50",
1083
- onClick: closeModal,
1084
- children: copy.cancelAction
1085
- }
1086
- )
1087
- ] })
1088
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1089
- mode === "reply" && issue ? /* @__PURE__ */ jsxs("div", { className: "mt-5 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3", children: [
1090
- /* @__PURE__ */ jsx2("div", { className: "text-xs font-medium uppercase tracking-wide text-slate-500", children: copy.originalIssueLabel }),
1091
- /* @__PURE__ */ jsx2("p", { className: "mt-1 text-sm text-slate-700", children: issue.note })
1092
- ] }) : null,
1093
- /* @__PURE__ */ jsxs("div", { className: "mt-5 space-y-2", children: [
1094
- /* @__PURE__ */ jsx2(
1095
- "textarea",
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,
1096
1590
  {
1097
- value: note,
1098
- onChange: (event) => setNote(event.target.value),
1099
- onKeyDown: (event) => {
1100
- if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
1101
- event.preventDefault();
1102
- void handleSubmit();
1103
- }
1104
- },
1105
- placeholder: copy.notePlaceholder,
1106
- 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",
1107
- disabled: isSubmitting,
1108
- autoFocus: true
1591
+ copy,
1592
+ mode,
1593
+ target
1109
1594
  }
1110
- ),
1111
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs text-slate-500", children: [
1112
- /* @__PURE__ */ jsx2("span", { children: getIssueNoteLengthMessage(note, copy) }),
1113
- /* @__PURE__ */ jsx2("span", { children: copy.keyboardShortcutHint })
1114
- ] })
1115
- ] }),
1116
- 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,
1117
- /* @__PURE__ */ jsxs("div", { className: "mt-6 flex justify-end gap-3", children: [
1595
+ ) }),
1118
1596
  /* @__PURE__ */ jsx2(
1119
- "button",
1597
+ IssueReportModalBody,
1120
1598
  {
1121
- type: "button",
1122
- className: "rounded-full border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50",
1123
- onClick: closeModal,
1124
- disabled: isSubmitting,
1125
- children: copy.cancelAction
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()
1126
1628
  }
1127
1629
  ),
1128
- /* @__PURE__ */ jsx2(
1630
+ /* @__PURE__ */ jsx2(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx2(
1129
1631
  "button",
1130
1632
  {
1131
1633
  type: "button",
1132
- className: cn(
1133
- "rounded-full px-4 py-2 text-sm font-semibold text-white transition",
1134
- isValid && !isSubmitting ? "bg-slate-900 hover:bg-slate-800" : "cursor-not-allowed bg-slate-300"
1135
- ),
1136
- onClick: () => void handleSubmit(),
1137
- disabled: !isValid || isSubmitting,
1138
- 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" })
1139
1637
  }
1140
- )
1141
- ] })
1142
- ] }),
1143
- /* @__PURE__ */ jsx2(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx2(
1144
- "button",
1145
- {
1146
- type: "button",
1147
- 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",
1148
- "aria-label": "Close issue report modal",
1149
- children: /* @__PURE__ */ jsx2(X, { className: "h-4 w-4" })
1150
- }
1151
- ) })
1152
- ] })
1638
+ ) })
1639
+ ]
1640
+ }
1641
+ )
1153
1642
  ] }) });
1154
1643
  }
1155
1644
  function FloatingIssueReportButton({
@@ -1171,7 +1660,7 @@ function FloatingIssueReportButton({
1171
1660
  }
1172
1661
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1173
1662
  /* @__PURE__ */ jsx2(IssueReportModeBanner, {}),
1174
- !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(
1175
1664
  "button",
1176
1665
  {
1177
1666
  type: "button",