glitchgrab 1.22.0 → 1.23.1

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
@@ -272,7 +272,7 @@ async function sendReport(payload, baseUrl) {
272
272
  return null;
273
273
  }
274
274
  }
275
- async function enhanceText(text, token, baseUrl) {
275
+ async function enhanceText(text, token, baseUrl, screenshot, context) {
276
276
  var _a;
277
277
  try {
278
278
  const trimmed = text.trim();
@@ -284,7 +284,11 @@ async function enhanceText(text, token, baseUrl) {
284
284
  "Content-Type": "application/json",
285
285
  Authorization: `Bearer ${token}`
286
286
  },
287
- body: JSON.stringify({ text: trimmed })
287
+ body: JSON.stringify({
288
+ text: trimmed,
289
+ ...screenshot ? { screenshot } : {},
290
+ ...context ? { context } : {}
291
+ })
288
292
  });
289
293
  if (!response.ok) return text;
290
294
  const envelope = await response.json();
@@ -296,6 +300,28 @@ async function enhanceText(text, token, baseUrl) {
296
300
  return text;
297
301
  }
298
302
  }
303
+ async function transcribeAudio(blob, token, baseUrl) {
304
+ var _a;
305
+ try {
306
+ if (blob.size === 0) return "";
307
+ const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/sdk/stt`;
308
+ const form = new FormData();
309
+ form.append("file", blob, "audio.webm");
310
+ const response = await fetch(url, {
311
+ method: "POST",
312
+ headers: { Authorization: `Bearer ${token}` },
313
+ body: form
314
+ });
315
+ if (!response.ok) return "";
316
+ const envelope = await response.json();
317
+ if ((envelope == null ? void 0 : envelope.success) && typeof ((_a = envelope.data) == null ? void 0 : _a.transcript) === "string") {
318
+ return envelope.data.transcript;
319
+ }
320
+ return "";
321
+ } catch (e) {
322
+ return "";
323
+ }
324
+ }
299
325
 
300
326
  // src/dedup.ts
301
327
  var DEFAULT_WINDOW_MS = 5 * 60 * 1e3;
@@ -426,7 +452,11 @@ var GlitchgrabErrorBoundary = class extends React.Component {
426
452
  };
427
453
 
428
454
  // src/report-dialog.tsx
429
- import { useState, useRef, useEffect } from "react";
455
+ import {
456
+ useState,
457
+ useRef,
458
+ useEffect
459
+ } from "react";
430
460
  import { createPortal } from "react-dom";
431
461
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
432
462
  function useIsDark() {
@@ -434,11 +464,17 @@ function useIsDark() {
434
464
  try {
435
465
  for (const el of [document.body, document.documentElement]) {
436
466
  const bg = getComputedStyle(el).backgroundColor;
437
- const match = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
467
+ const match = bg.match(
468
+ /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/
469
+ );
438
470
  if (match) {
439
471
  const alpha = match[4] !== void 0 ? parseFloat(match[4]) : 1;
440
472
  if (alpha < 0.05) continue;
441
- const [r, g, b] = [Number(match[1]), Number(match[2]), Number(match[3])];
473
+ const [r, g, b] = [
474
+ Number(match[1]),
475
+ Number(match[2]),
476
+ Number(match[3])
477
+ ];
442
478
  return (r * 299 + g * 587 + b * 114) / 1e3 < 128;
443
479
  }
444
480
  }
@@ -464,14 +500,26 @@ function parseColorToRgb(value) {
464
500
  const hexMatch = trimmed.match(/^#([0-9a-f]{3,8})$/i);
465
501
  if (hexMatch) {
466
502
  let hex = hexMatch[1];
467
- if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
468
- return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
503
+ if (hex.length === 3)
504
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
505
+ return [
506
+ parseInt(hex.slice(0, 2), 16),
507
+ parseInt(hex.slice(2, 4), 16),
508
+ parseInt(hex.slice(4, 6), 16)
509
+ ];
469
510
  }
470
511
  const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
471
- if (rgbMatch) return [Number(rgbMatch[1]), Number(rgbMatch[2]), Number(rgbMatch[3])];
472
- const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%/);
512
+ if (rgbMatch)
513
+ return [Number(rgbMatch[1]), Number(rgbMatch[2]), Number(rgbMatch[3])];
514
+ const hslMatch = trimmed.match(
515
+ /hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%/
516
+ );
473
517
  if (hslMatch) {
474
- const hex = hslToHex(Number(hslMatch[1]), Number(hslMatch[2]), Number(hslMatch[3]));
518
+ const hex = hslToHex(
519
+ Number(hslMatch[1]),
520
+ Number(hslMatch[2]),
521
+ Number(hslMatch[3])
522
+ );
475
523
  return parseColorToRgb(hex);
476
524
  }
477
525
  return null;
@@ -501,9 +549,16 @@ function detectHostAccent() {
501
549
  if (!raw) continue;
502
550
  const hslSpaceMatch = raw.match(/^([\d.]+)\s+([\d.]+)%\s+([\d.]+)%$/);
503
551
  if (hslSpaceMatch) {
504
- const hex = hslToHex(Number(hslSpaceMatch[1]), Number(hslSpaceMatch[2]), Number(hslSpaceMatch[3]));
552
+ const hex = hslToHex(
553
+ Number(hslSpaceMatch[1]),
554
+ Number(hslSpaceMatch[2]),
555
+ Number(hslSpaceMatch[3])
556
+ );
505
557
  const rgb2 = parseColorToRgb(hex);
506
- return { accent: hex, accentText: rgb2 ? getContrastText(...rgb2) : "#ffffff" };
558
+ return {
559
+ accent: hex,
560
+ accentText: rgb2 ? getContrastText(...rgb2) : "#ffffff"
561
+ };
507
562
  }
508
563
  const rgb = parseColorToRgb(raw);
509
564
  if (rgb) {
@@ -518,7 +573,27 @@ function detectHostAccent() {
518
573
  }
519
574
  function getTheme(dark) {
520
575
  const hostAccent = detectHostAccent();
521
- const defaults = dark ? { bg: "#1c1c1e", bgSecondary: "#27272a", border: "#2c2c2e", text: "#fafafa", textMuted: "#a1a1aa", accent: "#22d3ee", accentText: "#09090b", inputBg: "#27272a", inputBorder: "#3f3f46" } : { bg: "#ffffff", bgSecondary: "#f4f4f5", border: "#e4e4e7", text: "#18181b", textMuted: "#71717a", accent: "#0891b2", accentText: "#ffffff", inputBg: "#fafafa", inputBorder: "#d4d4d8" };
576
+ const defaults = dark ? {
577
+ bg: "#1c1c1e",
578
+ bgSecondary: "#27272a",
579
+ border: "#2c2c2e",
580
+ text: "#fafafa",
581
+ textMuted: "#a1a1aa",
582
+ accent: "#22d3ee",
583
+ accentText: "#09090b",
584
+ inputBg: "#27272a",
585
+ inputBorder: "#3f3f46"
586
+ } : {
587
+ bg: "#ffffff",
588
+ bgSecondary: "#f4f4f5",
589
+ border: "#e4e4e7",
590
+ text: "#18181b",
591
+ textMuted: "#71717a",
592
+ accent: "#0891b2",
593
+ accentText: "#ffffff",
594
+ inputBg: "#fafafa",
595
+ inputBorder: "#d4d4d8"
596
+ };
522
597
  if (hostAccent) {
523
598
  defaults.accent = hostAccent.accent;
524
599
  defaults.accentText = hostAccent.accentText;
@@ -568,10 +643,18 @@ function isLowQualityText(text) {
568
643
  function ReportDialog({
569
644
  report,
570
645
  enhanceText: enhanceText2,
646
+ transcribeAudio: transcribeAudio2,
571
647
  types,
572
648
  showSeverity = true
573
649
  }) {
574
650
  const [isEnhancing, setIsEnhancing] = useState(false);
651
+ const [isEnhanced, setIsEnhanced] = useState(false);
652
+ const [originalDescription, setOriginalDescription] = useState(
653
+ null
654
+ );
655
+ const [isListening, setIsListening] = useState(false);
656
+ const [isTranscribing, setIsTranscribing] = useState(false);
657
+ const [voiceError, setVoiceError] = useState(null);
575
658
  const [screenshotDragOver, setScreenshotDragOver] = useState(false);
576
659
  const [mounted, setMounted] = useState(false);
577
660
  useEffect(() => {
@@ -585,10 +668,22 @@ function ReportDialog({
585
668
  const [previewOpen, setPreviewOpen] = useState(false);
586
669
  const fileInputRef = useRef(null);
587
670
  const modalRef = useRef(null);
671
+ const textareaRef = useRef(null);
672
+ const recognitionRef = useRef(null);
673
+ const voiceBaseRef = useRef("");
674
+ const usingWebSpeechRef = useRef(false);
675
+ const textBeforeVoiceRef = useRef("");
676
+ const mediaRecorderRef = useRef(null);
677
+ const streamRef = useRef(null);
678
+ const sarvamChunksRef = useRef([]);
679
+ const spaceTimerRef = useRef(null);
680
+ const isPushToTalkRef = useRef(false);
588
681
  useEffect(() => {
589
682
  if (!isOpen) return;
590
683
  const dialogs = Array.from(
591
- document.querySelectorAll('[role="dialog"][data-state="open"]')
684
+ document.querySelectorAll(
685
+ '[role="dialog"][data-state="open"]'
686
+ )
592
687
  );
593
688
  dialogs.forEach((d) => d.setAttribute("inert", ""));
594
689
  return () => dialogs.forEach((d) => d.removeAttribute("inert"));
@@ -597,7 +692,13 @@ function ReportDialog({
597
692
  const [reportType, setReportType] = useState("BUG");
598
693
  const [severity, setSeverity] = useState("medium");
599
694
  const [validationError, setValidationError] = useState(null);
600
- const availableTypes = types != null ? types : ["BUG", "FEATURE_REQUEST", "QUESTION", "OTHER"];
695
+ const [placeholderIdx, setPlaceholderIdx] = useState(0);
696
+ const availableTypes = types != null ? types : [
697
+ "BUG",
698
+ "FEATURE_REQUEST",
699
+ "QUESTION",
700
+ "OTHER"
701
+ ];
601
702
  const isDark = useIsDark();
602
703
  const t = getTheme(isDark);
603
704
  const handleFileUpload = (e) => {
@@ -697,12 +798,187 @@ function ReportDialog({
697
798
  window.addEventListener("paste", handlePaste);
698
799
  return () => window.removeEventListener("paste", handlePaste);
699
800
  }, [isOpen]);
801
+ useEffect(() => {
802
+ if (!isOpen || !transcribeAudio2) return;
803
+ const id = setInterval(() => {
804
+ setPlaceholderIdx((i) => (i + 1) % 2);
805
+ }, 3e3);
806
+ return () => clearInterval(id);
807
+ }, [isOpen, transcribeAudio2]);
808
+ const stopVoice = () => {
809
+ var _a, _b;
810
+ usingWebSpeechRef.current = false;
811
+ (_a = recognitionRef.current) == null ? void 0 : _a.stop();
812
+ recognitionRef.current = null;
813
+ sarvamChunksRef.current = [];
814
+ const stream = streamRef.current;
815
+ streamRef.current = null;
816
+ stream == null ? void 0 : stream.getTracks().forEach((t2) => t2.stop());
817
+ try {
818
+ (_b = mediaRecorderRef.current) == null ? void 0 : _b.stop();
819
+ } catch (e) {
820
+ }
821
+ mediaRecorderRef.current = null;
822
+ setIsListening(false);
823
+ setIsTranscribing(false);
824
+ };
825
+ const stopListeningAndTranscribe = () => {
826
+ var _a, _b;
827
+ usingWebSpeechRef.current = false;
828
+ (_a = recognitionRef.current) == null ? void 0 : _a.stop();
829
+ recognitionRef.current = null;
830
+ setIsListening(false);
831
+ try {
832
+ (_b = mediaRecorderRef.current) == null ? void 0 : _b.stop();
833
+ } catch (e) {
834
+ }
835
+ };
700
836
  const handleClose = () => {
837
+ stopVoice();
701
838
  setIsOpen(false);
702
839
  setStep(1);
703
840
  setReportType("BUG");
704
841
  setSeverity("medium");
705
842
  setValidationError(null);
843
+ setVoiceError(null);
844
+ setIsEnhanced(false);
845
+ setOriginalDescription(null);
846
+ };
847
+ const toggleVoice = async () => {
848
+ if (isListening) {
849
+ stopListeningAndTranscribe();
850
+ return;
851
+ }
852
+ const SpeechRec = typeof window !== "undefined" ? window.SpeechRecognition || window.webkitSpeechRecognition : void 0;
853
+ let stream = null;
854
+ try {
855
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
856
+ } catch (err) {
857
+ const isDenied = err instanceof DOMException && (err.name === "NotAllowedError" || err.name === "PermissionDeniedError");
858
+ if (!SpeechRec || isDenied) {
859
+ setVoiceError("Microphone access denied");
860
+ return;
861
+ }
862
+ }
863
+ if (stream && transcribeAudio2) {
864
+ streamRef.current = stream;
865
+ sarvamChunksRef.current = [];
866
+ textBeforeVoiceRef.current = description;
867
+ const rec = new MediaRecorder(stream);
868
+ mediaRecorderRef.current = rec;
869
+ rec.ondataavailable = (e) => {
870
+ if (e.data.size > 0) sarvamChunksRef.current.push(e.data);
871
+ };
872
+ rec.onstop = async () => {
873
+ stream.getTracks().forEach((t2) => t2.stop());
874
+ if (streamRef.current === stream) streamRef.current = null;
875
+ mediaRecorderRef.current = null;
876
+ const chunks = sarvamChunksRef.current;
877
+ sarvamChunksRef.current = [];
878
+ if (!chunks.length || !transcribeAudio2) return;
879
+ setIsTranscribing(true);
880
+ try {
881
+ const blob = new Blob(chunks, { type: rec.mimeType || "audio/webm" });
882
+ const text = await transcribeAudio2(blob);
883
+ if (text.trim()) {
884
+ const sep = textBeforeVoiceRef.current.trim() ? " " : "";
885
+ setDescription(textBeforeVoiceRef.current + sep + text.trim());
886
+ setValidationError(null);
887
+ setIsEnhanced(false);
888
+ setOriginalDescription(null);
889
+ }
890
+ } catch (e) {
891
+ }
892
+ setIsTranscribing(false);
893
+ };
894
+ rec.start();
895
+ }
896
+ if (SpeechRec) {
897
+ setVoiceError(null);
898
+ usingWebSpeechRef.current = true;
899
+ voiceBaseRef.current = description;
900
+ textBeforeVoiceRef.current = description;
901
+ const recognition = new SpeechRec();
902
+ recognition.lang = "en-IN";
903
+ recognition.interimResults = true;
904
+ recognition.continuous = true;
905
+ recognitionRef.current = recognition;
906
+ recognition.onstart = () => setIsListening(true);
907
+ recognition.onresult = (event) => {
908
+ let finalText = "";
909
+ let interimText = "";
910
+ for (let i = event.resultIndex; i < event.results.length; i++) {
911
+ const text = event.results[i][0].transcript;
912
+ if (event.results[i].isFinal) finalText += text;
913
+ else interimText += text;
914
+ }
915
+ if (finalText) {
916
+ const sep = voiceBaseRef.current.trim() ? " " : "";
917
+ voiceBaseRef.current += sep + finalText.trim();
918
+ }
919
+ const liveText = interimText ? voiceBaseRef.current + (voiceBaseRef.current.trim() ? " " : "") + interimText : voiceBaseRef.current;
920
+ setDescription(liveText);
921
+ setValidationError(null);
922
+ };
923
+ recognition.onend = () => {
924
+ if (usingWebSpeechRef.current && recognitionRef.current === recognition) {
925
+ try {
926
+ recognition.start();
927
+ } catch (e) {
928
+ }
929
+ } else {
930
+ setIsListening(false);
931
+ }
932
+ };
933
+ recognition.onerror = (event) => {
934
+ if (event.error === "aborted" || event.error === "no-speech") return;
935
+ const isDenied = event.error === "not-allowed" || event.error === "audio-capture";
936
+ setVoiceError(isDenied ? "Microphone access denied" : "Speech recognition not available \u2014 try Chrome");
937
+ usingWebSpeechRef.current = false;
938
+ recognitionRef.current = null;
939
+ setIsListening(false);
940
+ };
941
+ recognition.start();
942
+ } else if (stream) {
943
+ setVoiceError(null);
944
+ setIsListening(true);
945
+ }
946
+ };
947
+ const handleSpaceDown = (e) => {
948
+ if (e.code !== "Space") return;
949
+ if (spaceTimerRef.current || isListening || isPushToTalkRef.current) {
950
+ e.preventDefault();
951
+ return;
952
+ }
953
+ if (e.repeat || !transcribeAudio2 || isTranscribing) return;
954
+ e.preventDefault();
955
+ spaceTimerRef.current = setTimeout(() => {
956
+ isPushToTalkRef.current = true;
957
+ void toggleVoice();
958
+ }, 400);
959
+ };
960
+ const handleSpaceUp = (e) => {
961
+ var _a;
962
+ if (e.code !== "Space") return;
963
+ if (spaceTimerRef.current) {
964
+ clearTimeout(spaceTimerRef.current);
965
+ spaceTimerRef.current = null;
966
+ if (!isPushToTalkRef.current) {
967
+ const ta = textareaRef.current;
968
+ const pos = ta ? (_a = ta.selectionStart) != null ? _a : description.length : description.length;
969
+ setDescription((prev) => prev.slice(0, pos) + " " + prev.slice(pos));
970
+ requestAnimationFrame(() => {
971
+ if (textareaRef.current) {
972
+ textareaRef.current.selectionStart = pos + 1;
973
+ textareaRef.current.selectionEnd = pos + 1;
974
+ }
975
+ });
976
+ }
977
+ }
978
+ if (isPushToTalkRef.current) {
979
+ isPushToTalkRef.current = false;
980
+ stopListeningAndTranscribe();
981
+ }
706
982
  };
707
983
  const handleSubmit = async () => {
708
984
  try {
@@ -719,7 +995,11 @@ function ReportDialog({
719
995
  if (showSeverity && reportType === "BUG") {
720
996
  metadata.severity = severity;
721
997
  }
722
- const result = await report(reportType, description.trim(), Object.keys(metadata).length > 0 ? metadata : void 0);
998
+ const result = await report(
999
+ reportType,
1000
+ description.trim(),
1001
+ Object.keys(metadata).length > 0 ? metadata : void 0
1002
+ );
723
1003
  if (result) {
724
1004
  setSubmitted(true);
725
1005
  setDescription("");
@@ -736,6 +1016,16 @@ function ReportDialog({
736
1016
  };
737
1017
  if (!mounted) return null;
738
1018
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1019
+ isOpen && createPortal(
1020
+ /* @__PURE__ */ jsx("style", { children: `
1021
+ @keyframes gg-pulse{0%,100%{opacity:1}50%{opacity:0.35}}
1022
+ @keyframes gg-b1{0%,100%{height:3px}40%{height:13px}}
1023
+ @keyframes gg-b2{0%,100%{height:8px}50%{height:3px}}
1024
+ @keyframes gg-b3{0%,100%{height:4px}30%{height:14px}70%{height:5px}}
1025
+ @keyframes gg-b4{0%,100%{height:6px}60%{height:13px}}
1026
+ ` }),
1027
+ document.body
1028
+ ),
739
1029
  isOpen && createPortal(
740
1030
  /* @__PURE__ */ jsx(
741
1031
  "div",
@@ -792,36 +1082,64 @@ function ReportDialog({
792
1082
  alignItems: "center"
793
1083
  },
794
1084
  children: [
795
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
796
- step > 1 && /* @__PURE__ */ jsx(
797
- "button",
798
- {
799
- type: "button",
800
- onClick: () => setStep((s) => s - 1),
801
- style: {
802
- background: "none",
803
- border: "none",
804
- cursor: "pointer",
805
- padding: "2px",
806
- display: "flex",
807
- alignItems: "center"
808
- },
809
- "aria-label": "Back",
810
- children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M10 3L5 8L10 13", stroke: t.textMuted, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
811
- }
812
- ),
813
- /* @__PURE__ */ jsx(
814
- "span",
815
- {
816
- style: {
817
- fontSize: "15px",
818
- fontWeight: 600,
819
- color: t.text
820
- },
821
- children: step === 1 ? "What's on your mind?" : step === 2 ? "Tell us more" : "Review & Send"
822
- }
823
- )
824
- ] }),
1085
+ /* @__PURE__ */ jsxs(
1086
+ "div",
1087
+ {
1088
+ style: {
1089
+ display: "flex",
1090
+ alignItems: "center",
1091
+ gap: "8px"
1092
+ },
1093
+ children: [
1094
+ step > 1 && /* @__PURE__ */ jsx(
1095
+ "button",
1096
+ {
1097
+ type: "button",
1098
+ onClick: () => setStep((s) => s - 1),
1099
+ style: {
1100
+ background: "none",
1101
+ border: "none",
1102
+ cursor: "pointer",
1103
+ padding: "2px",
1104
+ display: "flex",
1105
+ alignItems: "center"
1106
+ },
1107
+ "aria-label": "Back",
1108
+ children: /* @__PURE__ */ jsx(
1109
+ "svg",
1110
+ {
1111
+ width: "16",
1112
+ height: "16",
1113
+ viewBox: "0 0 16 16",
1114
+ fill: "none",
1115
+ children: /* @__PURE__ */ jsx(
1116
+ "path",
1117
+ {
1118
+ d: "M10 3L5 8L10 13",
1119
+ stroke: t.textMuted,
1120
+ strokeWidth: "2",
1121
+ strokeLinecap: "round",
1122
+ strokeLinejoin: "round"
1123
+ }
1124
+ )
1125
+ }
1126
+ )
1127
+ }
1128
+ ),
1129
+ /* @__PURE__ */ jsx(
1130
+ "span",
1131
+ {
1132
+ style: {
1133
+ fontSize: "15px",
1134
+ fontWeight: 600,
1135
+ color: t.text
1136
+ },
1137
+ children: step === 1 ? "What's on your mind?" : "Tell us more"
1138
+ }
1139
+ )
1140
+ ]
1141
+ }
1142
+ ),
825
1143
  /* @__PURE__ */ jsx(
826
1144
  "button",
827
1145
  {
@@ -837,27 +1155,74 @@ function ReportDialog({
837
1155
  justifyContent: "center"
838
1156
  },
839
1157
  "aria-label": "Close",
840
- children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", style: { display: "block" }, children: /* @__PURE__ */ jsx("path", { d: "M4 4L12 12M12 4L4 12", stroke: t.textMuted, strokeWidth: "2", strokeLinecap: "round" }) })
1158
+ children: /* @__PURE__ */ jsx(
1159
+ "svg",
1160
+ {
1161
+ width: "16",
1162
+ height: "16",
1163
+ viewBox: "0 0 16 16",
1164
+ fill: "none",
1165
+ style: { display: "block" },
1166
+ children: /* @__PURE__ */ jsx(
1167
+ "path",
1168
+ {
1169
+ d: "M4 4L12 12M12 4L4 12",
1170
+ stroke: t.textMuted,
1171
+ strokeWidth: "2",
1172
+ strokeLinecap: "round"
1173
+ }
1174
+ )
1175
+ }
1176
+ )
841
1177
  }
842
1178
  )
843
1179
  ]
844
1180
  }
845
1181
  ),
846
- /* @__PURE__ */ jsx("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", gap: "0", marginTop: "12px" }, children: [1, 2, 3].map((s, i) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center" }, children: [
847
- /* @__PURE__ */ jsx("div", { style: {
848
- width: "8px",
849
- height: "8px",
850
- borderRadius: "50%",
851
- backgroundColor: s <= step ? t.accent : t.inputBorder,
852
- transition: "background-color 0.2s ease"
853
- } }),
854
- i < 2 && /* @__PURE__ */ jsx("div", { style: {
855
- width: "40px",
856
- height: "2px",
857
- backgroundColor: s < step ? t.accent : t.inputBorder,
858
- transition: "background-color 0.2s ease"
859
- } })
860
- ] }, s)) })
1182
+ /* @__PURE__ */ jsx(
1183
+ "div",
1184
+ {
1185
+ style: {
1186
+ display: "flex",
1187
+ alignItems: "center",
1188
+ justifyContent: "center",
1189
+ gap: "0",
1190
+ marginTop: "12px"
1191
+ },
1192
+ children: [1, 2].map((s, i) => /* @__PURE__ */ jsxs(
1193
+ "div",
1194
+ {
1195
+ style: { display: "flex", alignItems: "center" },
1196
+ children: [
1197
+ /* @__PURE__ */ jsx(
1198
+ "div",
1199
+ {
1200
+ style: {
1201
+ width: "8px",
1202
+ height: "8px",
1203
+ borderRadius: "50%",
1204
+ backgroundColor: s <= step ? t.accent : t.inputBorder,
1205
+ transition: "background-color 0.2s ease"
1206
+ }
1207
+ }
1208
+ ),
1209
+ i < 1 && /* @__PURE__ */ jsx(
1210
+ "div",
1211
+ {
1212
+ style: {
1213
+ width: "40px",
1214
+ height: "2px",
1215
+ backgroundColor: s < step ? t.accent : t.inputBorder,
1216
+ transition: "background-color 0.2s ease"
1217
+ }
1218
+ }
1219
+ )
1220
+ ]
1221
+ },
1222
+ s
1223
+ ))
1224
+ }
1225
+ )
861
1226
  ]
862
1227
  }
863
1228
  ),
@@ -877,76 +1242,185 @@ function ReportDialog({
877
1242
  ]
878
1243
  }
879
1244
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [
880
- step === 1 && /* @__PURE__ */ jsx("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px" }, children: availableTypes.map((type) => /* @__PURE__ */ jsxs(
881
- "button",
1245
+ step === 1 && /* @__PURE__ */ jsx(
1246
+ "div",
882
1247
  {
883
- type: "button",
884
- onClick: () => {
885
- setReportType(type);
886
- setStep(2);
887
- },
888
1248
  style: {
889
- display: "flex",
890
- flexDirection: "column",
891
- alignItems: "center",
892
- gap: "8px",
893
- padding: "16px 8px",
894
- borderRadius: "8px",
895
- border: `1px solid ${t.inputBorder}`,
896
- background: t.inputBg,
897
- cursor: "pointer",
898
- color: t.text,
899
- fontFamily: "inherit",
900
- transition: "border-color 0.15s ease"
901
- },
902
- onMouseEnter: (e) => {
903
- e.currentTarget.style.borderColor = t.accent;
1249
+ display: "grid",
1250
+ gridTemplateColumns: "1fr 1fr",
1251
+ gap: "8px"
904
1252
  },
905
- onMouseLeave: (e) => {
906
- e.currentTarget.style.borderColor = t.inputBorder;
907
- },
908
- children: [
909
- getTypeIcon(type, t.accent),
910
- /* @__PURE__ */ jsx("span", { style: { fontSize: "13px", fontWeight: 600 }, children: getTypeLabel(type) }),
911
- /* @__PURE__ */ jsx("span", { style: { fontSize: "11px", color: t.textMuted, lineHeight: "1.3" }, children: getTypeSubtitle(type) })
912
- ]
913
- },
914
- type
915
- )) }),
1253
+ children: availableTypes.map((type) => /* @__PURE__ */ jsxs(
1254
+ "button",
1255
+ {
1256
+ type: "button",
1257
+ onClick: () => {
1258
+ setReportType(type);
1259
+ setStep(2);
1260
+ },
1261
+ style: {
1262
+ display: "flex",
1263
+ flexDirection: "column",
1264
+ alignItems: "center",
1265
+ gap: "8px",
1266
+ padding: "16px 8px",
1267
+ borderRadius: "8px",
1268
+ border: `1px solid ${t.inputBorder}`,
1269
+ background: t.inputBg,
1270
+ cursor: "pointer",
1271
+ color: t.text,
1272
+ fontFamily: "inherit",
1273
+ transition: "border-color 0.15s ease"
1274
+ },
1275
+ onMouseEnter: (e) => {
1276
+ e.currentTarget.style.borderColor = t.accent;
1277
+ },
1278
+ onMouseLeave: (e) => {
1279
+ e.currentTarget.style.borderColor = t.inputBorder;
1280
+ },
1281
+ children: [
1282
+ getTypeIcon(type, t.accent),
1283
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "13px", fontWeight: 600 }, children: getTypeLabel(type) }),
1284
+ /* @__PURE__ */ jsx(
1285
+ "span",
1286
+ {
1287
+ style: {
1288
+ fontSize: "11px",
1289
+ color: t.textMuted,
1290
+ lineHeight: "1.3"
1291
+ },
1292
+ children: getTypeSubtitle(type)
1293
+ }
1294
+ )
1295
+ ]
1296
+ },
1297
+ type
1298
+ ))
1299
+ }
1300
+ ),
916
1301
  step === 2 && /* @__PURE__ */ jsxs(Fragment, { children: [
917
1302
  /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
918
1303
  /* @__PURE__ */ jsx(
919
1304
  "textarea",
920
1305
  {
1306
+ ref: textareaRef,
921
1307
  value: description,
922
1308
  onChange: (e) => {
923
1309
  setDescription(e.target.value);
924
1310
  if (validationError) setValidationError(null);
1311
+ if (isEnhanced) {
1312
+ setIsEnhanced(false);
1313
+ setOriginalDescription(null);
1314
+ }
925
1315
  },
926
- placeholder: getPlaceholder(reportType),
1316
+ placeholder: isTranscribing ? "Transcribing your speech\u2026" : isListening ? "Listening\u2026 speak now" : getPlaceholder(reportType, placeholderIdx, !!transcribeAudio2),
927
1317
  style: {
928
1318
  width: "100%",
929
1319
  minHeight: "100px",
930
- padding: enhanceText2 ? "28px 12px 10px" : "10px 12px",
1320
+ padding: enhanceText2 ? "28px 12px 36px" : transcribeAudio2 ? "10px 12px 36px" : "10px 12px",
931
1321
  borderRadius: "8px",
932
- border: `1px solid ${t.inputBorder}`,
1322
+ border: `1px solid ${isTranscribing ? "#f59e0b" : isListening ? t.accent : t.inputBorder}`,
933
1323
  fontSize: "14px",
934
1324
  fontFamily: "inherit",
935
- resize: "vertical",
1325
+ resize: "none",
936
1326
  outline: "none",
937
1327
  boxSizing: "border-box",
938
1328
  color: t.text,
939
- backgroundColor: t.inputBg
1329
+ backgroundColor: t.inputBg,
1330
+ transition: "border-color 0.2s ease"
940
1331
  },
1332
+ onKeyDown: handleSpaceDown,
1333
+ onKeyUp: handleSpaceUp,
941
1334
  onFocus: (e) => {
942
1335
  e.target.style.borderColor = t.accent;
943
1336
  },
944
1337
  onBlur: (e) => {
945
- e.target.style.borderColor = t.inputBorder;
1338
+ if (!isListening && !isTranscribing)
1339
+ e.target.style.borderColor = t.inputBorder;
946
1340
  },
947
1341
  autoFocus: true
948
1342
  }
949
1343
  ),
1344
+ (isListening || isTranscribing) && /* @__PURE__ */ jsxs(
1345
+ "span",
1346
+ {
1347
+ style: {
1348
+ position: "absolute",
1349
+ bottom: "10px",
1350
+ left: "10px",
1351
+ display: "inline-flex",
1352
+ alignItems: "center",
1353
+ gap: "4px",
1354
+ fontSize: "10px",
1355
+ fontWeight: 700,
1356
+ letterSpacing: "0.04em",
1357
+ color: isTranscribing ? "#f59e0b" : "#ef4444",
1358
+ pointerEvents: "none",
1359
+ zIndex: 1
1360
+ },
1361
+ children: [
1362
+ /* @__PURE__ */ jsx(
1363
+ "span",
1364
+ {
1365
+ style: {
1366
+ width: "6px",
1367
+ height: "6px",
1368
+ borderRadius: "50%",
1369
+ backgroundColor: "currentColor",
1370
+ animation: "gg-pulse 1.2s ease-in-out infinite",
1371
+ display: "inline-block",
1372
+ flexShrink: 0
1373
+ }
1374
+ }
1375
+ ),
1376
+ isTranscribing ? "Transcribing\u2026" : "REC",
1377
+ isListening && !isTranscribing && /* @__PURE__ */ jsx(
1378
+ "span",
1379
+ {
1380
+ style: {
1381
+ display: "inline-flex",
1382
+ alignItems: "flex-end",
1383
+ gap: "2px",
1384
+ height: "14px",
1385
+ marginLeft: "3px"
1386
+ },
1387
+ children: [
1388
+ { anim: "gg-b1", dur: "0.6s", delay: "0s" },
1389
+ {
1390
+ anim: "gg-b2",
1391
+ dur: "0.5s",
1392
+ delay: "0.1s"
1393
+ },
1394
+ {
1395
+ anim: "gg-b3",
1396
+ dur: "0.7s",
1397
+ delay: "0.05s"
1398
+ },
1399
+ {
1400
+ anim: "gg-b4",
1401
+ dur: "0.55s",
1402
+ delay: "0.15s"
1403
+ }
1404
+ ].map((b, i) => /* @__PURE__ */ jsx(
1405
+ "span",
1406
+ {
1407
+ style: {
1408
+ width: "2.5px",
1409
+ height: "4px",
1410
+ borderRadius: "1px",
1411
+ backgroundColor: "#ef4444",
1412
+ animation: `${b.anim} ${b.dur} ease-in-out ${b.delay} infinite`,
1413
+ display: "inline-block",
1414
+ alignSelf: "center"
1415
+ }
1416
+ },
1417
+ i
1418
+ ))
1419
+ }
1420
+ )
1421
+ ]
1422
+ }
1423
+ ),
950
1424
  enhanceText2 && /* @__PURE__ */ jsxs(
951
1425
  "button",
952
1426
  {
@@ -955,16 +1429,22 @@ function ReportDialog({
955
1429
  if (!description.trim() || isEnhancing) return;
956
1430
  setIsEnhancing(true);
957
1431
  try {
958
- const polished = await enhanceText2(description);
1432
+ const polished = await enhanceText2(
1433
+ description,
1434
+ screenshot
1435
+ );
959
1436
  if (polished && polished !== description) {
1437
+ setOriginalDescription(description);
960
1438
  setDescription(polished);
961
- if (validationError) setValidationError(null);
1439
+ setIsEnhanced(true);
1440
+ if (validationError)
1441
+ setValidationError(null);
962
1442
  }
963
1443
  } finally {
964
1444
  setIsEnhancing(false);
965
1445
  }
966
1446
  },
967
- disabled: !description.trim() || isEnhancing,
1447
+ disabled: !description.trim() || isEnhancing || isEnhanced,
968
1448
  style: {
969
1449
  position: "absolute",
970
1450
  top: "6px",
@@ -985,105 +1465,393 @@ function ReportDialog({
985
1465
  },
986
1466
  title: "Polish grammar \u2014 preserves your meaning",
987
1467
  children: [
988
- /* @__PURE__ */ jsx("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3zM19 15l.75 2.25L22 18l-2.25.75L19 21l-.75-2.25L16 18l2.25-.75L19 15z", stroke: t.textMuted, strokeWidth: "1.5", strokeLinejoin: "round" }) }),
1468
+ /* @__PURE__ */ jsx(
1469
+ "svg",
1470
+ {
1471
+ width: "11",
1472
+ height: "11",
1473
+ viewBox: "0 0 24 24",
1474
+ fill: "none",
1475
+ "aria-hidden": "true",
1476
+ children: /* @__PURE__ */ jsx(
1477
+ "path",
1478
+ {
1479
+ d: "M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3zM19 15l.75 2.25L22 18l-2.25.75L19 21l-.75-2.25L16 18l2.25-.75L19 15z",
1480
+ stroke: t.textMuted,
1481
+ strokeWidth: "1.5",
1482
+ strokeLinejoin: "round"
1483
+ }
1484
+ )
1485
+ }
1486
+ ),
989
1487
  isEnhancing ? "Enhancing..." : "AI enhance"
990
1488
  ]
991
1489
  }
1490
+ ),
1491
+ transcribeAudio2 && /* @__PURE__ */ jsx(
1492
+ "button",
1493
+ {
1494
+ type: "button",
1495
+ onClick: () => {
1496
+ void toggleVoice();
1497
+ },
1498
+ title: isListening ? "Stop recording" : "Speak your report",
1499
+ style: {
1500
+ position: "absolute",
1501
+ bottom: "14px",
1502
+ right: "7px",
1503
+ width: "26px",
1504
+ height: "26px",
1505
+ display: "flex",
1506
+ alignItems: "center",
1507
+ justifyContent: "center",
1508
+ background: "transparent",
1509
+ border: `1.5px solid ${isListening ? "#ef4444" : t.inputBorder}`,
1510
+ borderRadius: "6px",
1511
+ color: isListening ? "#ef4444" : t.textMuted,
1512
+ cursor: "pointer",
1513
+ zIndex: 1,
1514
+ padding: 0,
1515
+ transition: "border-color 0.2s ease, color 0.2s ease"
1516
+ },
1517
+ children: isListening ? /* @__PURE__ */ jsx(
1518
+ "svg",
1519
+ {
1520
+ width: "12",
1521
+ height: "12",
1522
+ viewBox: "0 0 24 24",
1523
+ fill: "currentColor",
1524
+ "aria-hidden": "true",
1525
+ children: /* @__PURE__ */ jsx(
1526
+ "rect",
1527
+ {
1528
+ x: "6",
1529
+ y: "6",
1530
+ width: "12",
1531
+ height: "12",
1532
+ rx: "2"
1533
+ }
1534
+ )
1535
+ }
1536
+ ) : /* @__PURE__ */ jsxs(
1537
+ "svg",
1538
+ {
1539
+ width: "13",
1540
+ height: "13",
1541
+ viewBox: "0 0 24 24",
1542
+ fill: "none",
1543
+ "aria-hidden": "true",
1544
+ children: [
1545
+ /* @__PURE__ */ jsx(
1546
+ "rect",
1547
+ {
1548
+ x: "9",
1549
+ y: "2",
1550
+ width: "6",
1551
+ height: "12",
1552
+ rx: "3",
1553
+ stroke: "currentColor",
1554
+ strokeWidth: "1.8"
1555
+ }
1556
+ ),
1557
+ /* @__PURE__ */ jsx(
1558
+ "path",
1559
+ {
1560
+ d: "M5 10a7 7 0 0014 0M12 19v3M9 22h6",
1561
+ stroke: "currentColor",
1562
+ strokeWidth: "1.8",
1563
+ strokeLinecap: "round"
1564
+ }
1565
+ )
1566
+ ]
1567
+ }
1568
+ )
1569
+ }
992
1570
  )
993
1571
  ] }),
994
- /* @__PURE__ */ jsxs("div", { style: { marginTop: "10px" }, children: [
995
- /* @__PURE__ */ jsx("span", { style: { fontSize: "12px", color: t.textMuted, marginBottom: "6px", display: "block", fontWeight: 500 }, children: "Screenshot" }),
996
- screenshot ? /* @__PURE__ */ jsxs("div", { children: [
997
- /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
1572
+ transcribeAudio2 && !isListening && !isTranscribing && /* @__PURE__ */ jsxs(
1573
+ "div",
1574
+ {
1575
+ style: {
1576
+ display: "flex",
1577
+ alignItems: "center",
1578
+ gap: "5px",
1579
+ marginTop: "5px",
1580
+ fontSize: "11px",
1581
+ color: t.text,
1582
+ opacity: 0.55,
1583
+ userSelect: "none"
1584
+ },
1585
+ children: [
1586
+ /* @__PURE__ */ jsxs("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1587
+ /* @__PURE__ */ jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }),
1588
+ /* @__PURE__ */ jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
1589
+ /* @__PURE__ */ jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })
1590
+ ] }),
1591
+ "Hold Space to speak \u2014 we'll transcribe it"
1592
+ ]
1593
+ }
1594
+ ),
1595
+ isEnhanced && originalDescription !== null && /* @__PURE__ */ jsxs(
1596
+ "div",
1597
+ {
1598
+ style: {
1599
+ display: "flex",
1600
+ alignItems: "center",
1601
+ gap: "6px",
1602
+ marginTop: "6px",
1603
+ padding: "6px 8px",
1604
+ borderRadius: "6px",
1605
+ background: isDark ? "rgba(34,211,238,0.06)" : "rgba(8,145,178,0.05)",
1606
+ border: `1px solid ${t.accent}40`
1607
+ },
1608
+ children: [
998
1609
  /* @__PURE__ */ jsx(
999
- "img",
1610
+ "svg",
1611
+ {
1612
+ width: "12",
1613
+ height: "12",
1614
+ viewBox: "0 0 24 24",
1615
+ fill: "none",
1616
+ style: { flexShrink: 0 },
1617
+ children: /* @__PURE__ */ jsx(
1618
+ "path",
1619
+ {
1620
+ d: "M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3z",
1621
+ stroke: t.accent,
1622
+ strokeWidth: "1.5",
1623
+ strokeLinejoin: "round"
1624
+ }
1625
+ )
1626
+ }
1627
+ ),
1628
+ /* @__PURE__ */ jsx(
1629
+ "span",
1000
1630
  {
1001
- src: screenshot,
1002
- alt: "Page screenshot",
1003
- onClick: () => setPreviewOpen(true),
1004
1631
  style: {
1005
- width: "100%",
1006
- borderRadius: "6px",
1007
- border: `1px solid ${t.border}`,
1008
- maxHeight: "64px",
1009
- objectFit: "cover",
1010
- objectPosition: "top",
1011
- cursor: "zoom-in",
1012
- display: "block"
1013
- }
1632
+ fontSize: "11px",
1633
+ color: t.textMuted,
1634
+ flex: 1
1635
+ },
1636
+ children: "AI enhanced"
1014
1637
  }
1015
1638
  ),
1016
- /* @__PURE__ */ jsx("span", { style: {
1017
- position: "absolute",
1018
- bottom: "5px",
1019
- left: "6px",
1020
- background: "rgba(0,0,0,0.55)",
1021
- color: "#fff",
1022
- fontSize: "10px",
1023
- padding: "1px 5px",
1024
- borderRadius: "3px",
1025
- pointerEvents: "none"
1026
- }, children: "click to preview" })
1027
- ] }),
1028
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "6px", marginTop: "6px" }, children: [
1029
- /* @__PURE__ */ jsxs(
1639
+ /* @__PURE__ */ jsx(
1030
1640
  "button",
1031
1641
  {
1032
1642
  type: "button",
1033
- onClick: () => setScreenshot(null),
1643
+ onClick: () => {
1644
+ setIsEnhanced(false);
1645
+ setOriginalDescription(null);
1646
+ },
1034
1647
  style: {
1035
- flex: 1,
1036
- padding: "6px 0",
1037
- borderRadius: "6px",
1038
- border: `1px solid ${t.inputBorder}`,
1039
- background: "transparent",
1040
- color: "#ef4444",
1041
- fontSize: "12px",
1042
- fontWeight: 500,
1648
+ padding: "2px 8px",
1649
+ borderRadius: "4px",
1650
+ border: `1px solid ${t.accent}`,
1651
+ background: t.accent,
1652
+ color: t.accentText,
1653
+ fontSize: "11px",
1654
+ fontWeight: 600,
1043
1655
  cursor: "pointer",
1044
- fontFamily: "inherit",
1045
- display: "flex",
1046
- alignItems: "center",
1047
- justifyContent: "center",
1048
- gap: "4px"
1656
+ fontFamily: "inherit"
1049
1657
  },
1050
- children: [
1051
- /* @__PURE__ */ jsx("svg", { width: "11", height: "11", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M1 1L9 9M9 1L1 9", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }) }),
1052
- "Remove"
1053
- ]
1658
+ children: "Keep"
1054
1659
  }
1055
1660
  ),
1056
- /* @__PURE__ */ jsxs(
1661
+ /* @__PURE__ */ jsx(
1057
1662
  "button",
1058
1663
  {
1059
1664
  type: "button",
1060
1665
  onClick: () => {
1061
- var _a;
1062
- return (_a = fileInputRef.current) == null ? void 0 : _a.click();
1666
+ setDescription(originalDescription);
1667
+ setIsEnhanced(false);
1668
+ setOriginalDescription(null);
1063
1669
  },
1064
1670
  style: {
1065
- flex: 1,
1066
- padding: "6px 0",
1067
- borderRadius: "6px",
1671
+ padding: "2px 8px",
1672
+ borderRadius: "4px",
1068
1673
  border: `1px solid ${t.inputBorder}`,
1069
1674
  background: "transparent",
1070
1675
  color: t.textMuted,
1071
- fontSize: "12px",
1072
- fontWeight: 500,
1676
+ fontSize: "11px",
1677
+ fontWeight: 600,
1073
1678
  cursor: "pointer",
1074
- fontFamily: "inherit",
1075
- display: "flex",
1076
- alignItems: "center",
1077
- justifyContent: "center",
1078
- gap: "4px"
1679
+ fontFamily: "inherit"
1079
1680
  },
1080
- children: [
1081
- /* @__PURE__ */ jsx("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }),
1082
- "Replace"
1083
- ]
1681
+ children: "Restore"
1682
+ }
1683
+ )
1684
+ ]
1685
+ }
1686
+ ),
1687
+ voiceError && /* @__PURE__ */ jsx(
1688
+ "p",
1689
+ {
1690
+ style: {
1691
+ color: "#ef4444",
1692
+ fontSize: "11px",
1693
+ marginTop: "4px",
1694
+ marginBottom: 0
1695
+ },
1696
+ children: voiceError
1697
+ }
1698
+ ),
1699
+ /* @__PURE__ */ jsxs("div", { style: { marginTop: "10px" }, children: [
1700
+ /* @__PURE__ */ jsx(
1701
+ "span",
1702
+ {
1703
+ style: {
1704
+ fontSize: "12px",
1705
+ color: t.textMuted,
1706
+ marginBottom: "6px",
1707
+ display: "block",
1708
+ fontWeight: 500
1709
+ },
1710
+ children: "Screenshot"
1711
+ }
1712
+ ),
1713
+ screenshot ? /* @__PURE__ */ jsxs("div", { children: [
1714
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
1715
+ /* @__PURE__ */ jsx(
1716
+ "img",
1717
+ {
1718
+ src: screenshot,
1719
+ alt: "Page screenshot",
1720
+ onClick: () => setPreviewOpen(true),
1721
+ style: {
1722
+ width: "100%",
1723
+ borderRadius: "6px",
1724
+ border: `1px solid ${t.border}`,
1725
+ maxHeight: "64px",
1726
+ objectFit: "cover",
1727
+ objectPosition: "top",
1728
+ cursor: "zoom-in",
1729
+ display: "block"
1730
+ }
1731
+ }
1732
+ ),
1733
+ /* @__PURE__ */ jsx(
1734
+ "span",
1735
+ {
1736
+ style: {
1737
+ position: "absolute",
1738
+ bottom: "5px",
1739
+ left: "6px",
1740
+ background: "rgba(0,0,0,0.55)",
1741
+ color: "#fff",
1742
+ fontSize: "10px",
1743
+ padding: "1px 5px",
1744
+ borderRadius: "3px",
1745
+ pointerEvents: "none"
1746
+ },
1747
+ children: "click to preview"
1084
1748
  }
1085
1749
  )
1086
- ] })
1750
+ ] }),
1751
+ /* @__PURE__ */ jsxs(
1752
+ "div",
1753
+ {
1754
+ style: {
1755
+ display: "flex",
1756
+ gap: "6px",
1757
+ marginTop: "6px"
1758
+ },
1759
+ children: [
1760
+ /* @__PURE__ */ jsxs(
1761
+ "button",
1762
+ {
1763
+ type: "button",
1764
+ onClick: () => setScreenshot(null),
1765
+ style: {
1766
+ flex: 1,
1767
+ padding: "6px 0",
1768
+ borderRadius: "6px",
1769
+ border: `1px solid ${t.inputBorder}`,
1770
+ background: "transparent",
1771
+ color: "#ef4444",
1772
+ fontSize: "12px",
1773
+ fontWeight: 500,
1774
+ cursor: "pointer",
1775
+ fontFamily: "inherit",
1776
+ display: "flex",
1777
+ alignItems: "center",
1778
+ justifyContent: "center",
1779
+ gap: "4px"
1780
+ },
1781
+ children: [
1782
+ /* @__PURE__ */ jsx(
1783
+ "svg",
1784
+ {
1785
+ width: "11",
1786
+ height: "11",
1787
+ viewBox: "0 0 10 10",
1788
+ fill: "none",
1789
+ children: /* @__PURE__ */ jsx(
1790
+ "path",
1791
+ {
1792
+ d: "M1 1L9 9M9 1L1 9",
1793
+ stroke: "currentColor",
1794
+ strokeWidth: "1.5",
1795
+ strokeLinecap: "round"
1796
+ }
1797
+ )
1798
+ }
1799
+ ),
1800
+ "Remove"
1801
+ ]
1802
+ }
1803
+ ),
1804
+ /* @__PURE__ */ jsxs(
1805
+ "button",
1806
+ {
1807
+ type: "button",
1808
+ onClick: () => {
1809
+ var _a;
1810
+ return (_a = fileInputRef.current) == null ? void 0 : _a.click();
1811
+ },
1812
+ style: {
1813
+ flex: 1,
1814
+ padding: "6px 0",
1815
+ borderRadius: "6px",
1816
+ border: `1px solid ${t.inputBorder}`,
1817
+ background: "transparent",
1818
+ color: t.textMuted,
1819
+ fontSize: "12px",
1820
+ fontWeight: 500,
1821
+ cursor: "pointer",
1822
+ fontFamily: "inherit",
1823
+ display: "flex",
1824
+ alignItems: "center",
1825
+ justifyContent: "center",
1826
+ gap: "4px"
1827
+ },
1828
+ children: [
1829
+ /* @__PURE__ */ jsx(
1830
+ "svg",
1831
+ {
1832
+ width: "11",
1833
+ height: "11",
1834
+ viewBox: "0 0 24 24",
1835
+ fill: "none",
1836
+ children: /* @__PURE__ */ jsx(
1837
+ "path",
1838
+ {
1839
+ d: "M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12",
1840
+ stroke: "currentColor",
1841
+ strokeWidth: "2",
1842
+ strokeLinecap: "round",
1843
+ strokeLinejoin: "round"
1844
+ }
1845
+ )
1846
+ }
1847
+ ),
1848
+ "Replace"
1849
+ ]
1850
+ }
1851
+ )
1852
+ ]
1853
+ }
1854
+ )
1087
1855
  ] }) : (
1088
1856
  /* Drop zone */
1089
1857
  /* @__PURE__ */ jsxs(
@@ -1096,7 +1864,9 @@ function ReportDialog({
1096
1864
  onDrop: (e) => {
1097
1865
  e.preventDefault();
1098
1866
  setScreenshotDragOver(false);
1099
- const file = Array.from(e.dataTransfer.files).find((f) => f.type.startsWith("image/"));
1867
+ const file = Array.from(
1868
+ e.dataTransfer.files
1869
+ ).find((f) => f.type.startsWith("image/"));
1100
1870
  if (!file) return;
1101
1871
  const reader = new FileReader();
1102
1872
  reader.onload = () => setScreenshot(reader.result);
@@ -1117,20 +1887,95 @@ function ReportDialog({
1117
1887
  transition: "border-color 0.15s ease, background 0.15s ease"
1118
1888
  },
1119
1889
  children: [
1120
- /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", style: { margin: "0 auto 6px", display: "block", opacity: 0.45 }, children: [
1121
- /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", stroke: t.textMuted, strokeWidth: "1.5" }),
1122
- /* @__PURE__ */ jsx("circle", { cx: "8.5", cy: "8.5", r: "1.5", stroke: t.textMuted, strokeWidth: "1.5" }),
1123
- /* @__PURE__ */ jsx("path", { d: "M21 15l-5-5L5 21", stroke: t.textMuted, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })
1124
- ] }),
1125
- /* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: "12px", fontWeight: 600, color: t.text }, children: screenshotDragOver ? "Drop to attach" : "Add a screenshot" }),
1126
- /* @__PURE__ */ jsx("p", { style: { margin: "3px 0 0", fontSize: "11px", color: t.textMuted }, children: "Drag & drop \xB7 Paste \u2318V / Ctrl+V \xB7 Click to browse" })
1890
+ /* @__PURE__ */ jsxs(
1891
+ "svg",
1892
+ {
1893
+ width: "22",
1894
+ height: "22",
1895
+ viewBox: "0 0 24 24",
1896
+ fill: "none",
1897
+ style: {
1898
+ margin: "0 auto 6px",
1899
+ display: "block",
1900
+ opacity: 0.45
1901
+ },
1902
+ children: [
1903
+ /* @__PURE__ */ jsx(
1904
+ "rect",
1905
+ {
1906
+ x: "3",
1907
+ y: "3",
1908
+ width: "18",
1909
+ height: "18",
1910
+ rx: "2",
1911
+ stroke: t.textMuted,
1912
+ strokeWidth: "1.5"
1913
+ }
1914
+ ),
1915
+ /* @__PURE__ */ jsx(
1916
+ "circle",
1917
+ {
1918
+ cx: "8.5",
1919
+ cy: "8.5",
1920
+ r: "1.5",
1921
+ stroke: t.textMuted,
1922
+ strokeWidth: "1.5"
1923
+ }
1924
+ ),
1925
+ /* @__PURE__ */ jsx(
1926
+ "path",
1927
+ {
1928
+ d: "M21 15l-5-5L5 21",
1929
+ stroke: t.textMuted,
1930
+ strokeWidth: "1.5",
1931
+ strokeLinecap: "round",
1932
+ strokeLinejoin: "round"
1933
+ }
1934
+ )
1935
+ ]
1936
+ }
1937
+ ),
1938
+ /* @__PURE__ */ jsx(
1939
+ "p",
1940
+ {
1941
+ style: {
1942
+ margin: 0,
1943
+ fontSize: "12px",
1944
+ fontWeight: 600,
1945
+ color: t.text
1946
+ },
1947
+ children: screenshotDragOver ? "Drop to attach" : "Add a screenshot"
1948
+ }
1949
+ ),
1950
+ /* @__PURE__ */ jsx(
1951
+ "p",
1952
+ {
1953
+ style: {
1954
+ margin: "3px 0 0",
1955
+ fontSize: "11px",
1956
+ color: t.textMuted
1957
+ },
1958
+ children: "Drag & drop \xB7 Paste \u2318V / Ctrl+V \xB7 Click to browse"
1959
+ }
1960
+ )
1127
1961
  ]
1128
1962
  }
1129
1963
  )
1130
1964
  )
1131
1965
  ] }),
1132
1966
  showSeverity && reportType === "BUG" && /* @__PURE__ */ jsxs("div", { style: { marginTop: "10px" }, children: [
1133
- /* @__PURE__ */ jsx("span", { style: { fontSize: "12px", color: t.textMuted, marginBottom: "6px", display: "block" }, children: "Severity" }),
1967
+ /* @__PURE__ */ jsx(
1968
+ "span",
1969
+ {
1970
+ style: {
1971
+ fontSize: "12px",
1972
+ color: t.textMuted,
1973
+ marginBottom: "6px",
1974
+ display: "block"
1975
+ },
1976
+ children: "Severity"
1977
+ }
1978
+ ),
1134
1979
  /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: "6px" }, children: ["low", "medium", "high"].map((s) => /* @__PURE__ */ jsx(
1135
1980
  "button",
1136
1981
  {
@@ -1155,114 +2000,34 @@ function ReportDialog({
1155
2000
  s
1156
2001
  )) })
1157
2002
  ] }),
1158
- validationError && /* @__PURE__ */ jsx("p", { style: { color: "#ef4444", fontSize: "12px", marginTop: "6px" }, children: validationError }),
1159
- /* @__PURE__ */ jsx(
1160
- "button",
2003
+ validationError && /* @__PURE__ */ jsx(
2004
+ "p",
1161
2005
  {
1162
- type: "button",
1163
- onClick: () => {
1164
- const err = isLowQualityText(description);
1165
- if (err) {
1166
- setValidationError(err);
1167
- return;
1168
- }
1169
- setValidationError(null);
1170
- setStep(3);
1171
- },
1172
- disabled: !description.trim(),
1173
2006
  style: {
1174
- marginTop: "12px",
1175
- width: "100%",
1176
- padding: "10px",
1177
- borderRadius: "8px",
1178
- border: "none",
1179
- backgroundColor: !description.trim() ? t.bgSecondary : t.accent,
1180
- color: !description.trim() ? t.textMuted : t.accentText,
1181
- fontSize: "14px",
1182
- fontWeight: 600,
1183
- cursor: !description.trim() ? "not-allowed" : "pointer",
1184
- fontFamily: "inherit",
1185
- transition: "background-color 0.15s ease"
2007
+ color: "#ef4444",
2008
+ fontSize: "12px",
2009
+ marginTop: "6px"
1186
2010
  },
1187
- children: "Next"
1188
- }
1189
- )
1190
- ] }),
1191
- step === 3 && /* @__PURE__ */ jsxs(Fragment, { children: [
1192
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "6px", flexWrap: "wrap", marginBottom: "12px" }, children: [
1193
- /* @__PURE__ */ jsxs("span", { style: {
1194
- display: "inline-flex",
1195
- alignItems: "center",
1196
- gap: "4px",
1197
- padding: "4px 10px",
1198
- borderRadius: "12px",
1199
- fontSize: "12px",
1200
- fontWeight: 600,
1201
- backgroundColor: t.bgSecondary,
1202
- color: t.text,
1203
- border: `1px solid ${t.border}`
1204
- }, children: [
1205
- getTypeIcon(reportType, t.accent, 12),
1206
- getTypeLabel(reportType)
1207
- ] }),
1208
- showSeverity && reportType === "BUG" && /* @__PURE__ */ jsx("span", { style: {
1209
- display: "inline-flex",
1210
- alignItems: "center",
1211
- padding: "4px 10px",
1212
- borderRadius: "12px",
1213
- fontSize: "12px",
1214
- fontWeight: 600,
1215
- backgroundColor: severity === "high" ? "#fef2f2" : severity === "medium" ? "#fffbeb" : "#f0fdf4",
1216
- color: severity === "high" ? "#dc2626" : severity === "medium" ? "#d97706" : "#16a34a",
1217
- border: `1px solid ${severity === "high" ? "#fecaca" : severity === "medium" ? "#fde68a" : "#bbf7d0"}`,
1218
- textTransform: "capitalize"
1219
- }, children: severity })
1220
- ] }),
1221
- /* @__PURE__ */ jsx("div", { style: {
1222
- padding: "10px 12px",
1223
- borderRadius: "8px",
1224
- border: `1px solid ${t.inputBorder}`,
1225
- backgroundColor: t.inputBg,
1226
- fontSize: "13px",
1227
- color: t.text,
1228
- marginBottom: "10px",
1229
- maxHeight: "80px",
1230
- overflow: "hidden",
1231
- lineHeight: "1.4"
1232
- }, children: description.length > 200 ? description.slice(0, 200) + "..." : description }),
1233
- screenshot && /* @__PURE__ */ jsx("div", { style: { marginBottom: "10px", position: "relative" }, children: /* @__PURE__ */ jsx(
1234
- "img",
1235
- {
1236
- src: screenshot,
1237
- alt: "Page screenshot",
1238
- onClick: () => setPreviewOpen(true),
1239
- style: {
1240
- width: "100%",
1241
- borderRadius: "6px",
1242
- border: `1px solid ${t.border}`,
1243
- maxHeight: "80px",
1244
- objectFit: "cover",
1245
- objectPosition: "top",
1246
- cursor: "pointer"
1247
- }
2011
+ children: validationError
1248
2012
  }
1249
- ) }),
2013
+ ),
1250
2014
  /* @__PURE__ */ jsx(
1251
2015
  "button",
1252
2016
  {
1253
2017
  type: "button",
1254
2018
  onClick: handleSubmit,
1255
- disabled: isSubmitting,
2019
+ disabled: !description.trim() || isSubmitting,
1256
2020
  style: {
2021
+ marginTop: "12px",
1257
2022
  width: "100%",
1258
2023
  padding: "10px",
1259
2024
  borderRadius: "8px",
1260
2025
  border: "none",
1261
- backgroundColor: isSubmitting ? t.bgSecondary : t.accent,
1262
- color: isSubmitting ? t.textMuted : t.accentText,
2026
+ backgroundColor: !description.trim() || isSubmitting ? t.bgSecondary : t.accent,
2027
+ color: !description.trim() || isSubmitting ? t.textMuted : t.accentText,
1263
2028
  fontSize: "14px",
1264
2029
  fontWeight: 600,
1265
- cursor: isSubmitting ? "not-allowed" : "pointer",
2030
+ cursor: !description.trim() || isSubmitting ? "not-allowed" : "pointer",
1266
2031
  fontFamily: "inherit",
1267
2032
  transition: "background-color 0.15s ease"
1268
2033
  },
@@ -1378,10 +2143,11 @@ function getTypeSubtitle(type) {
1378
2143
  return "General feedback";
1379
2144
  }
1380
2145
  }
1381
- function getPlaceholder(type) {
2146
+ function getPlaceholder(type, idx = 0, hasVoice = false) {
2147
+ if (hasVoice && idx === 1) return "Hold Space to speak \u2014 we'll transcribe it";
1382
2148
  switch (type) {
1383
2149
  case "BUG":
1384
- return "What went wrong?";
2150
+ return "What went wrong? Describe it or paste an error...";
1385
2151
  case "FEATURE_REQUEST":
1386
2152
  return "Describe the feature you'd like...";
1387
2153
  case "QUESTION":
@@ -1391,19 +2157,58 @@ function getPlaceholder(type) {
1391
2157
  }
1392
2158
  }
1393
2159
  function getTypeIcon(type, color, size = 24) {
1394
- const props = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0 } };
2160
+ const props = {
2161
+ width: size,
2162
+ height: size,
2163
+ viewBox: "0 0 24 24",
2164
+ fill: "none",
2165
+ style: { flexShrink: 0 }
2166
+ };
1395
2167
  switch (type) {
1396
2168
  case "BUG":
1397
- return /* @__PURE__ */ jsx("svg", { ...props, children: /* @__PURE__ */ jsx("path", { d: "M8 2L6.5 3.5M16 2L17.5 3.5M3 9H7M17 9H21M12 2a5 5 0 015 5v4a5 5 0 01-10 0V7a5 5 0 015-5zM7 16l-2 3M17 16l2 3", stroke: color, strokeWidth: "1.5", strokeLinecap: "round" }) });
2169
+ return /* @__PURE__ */ jsx("svg", { ...props, children: /* @__PURE__ */ jsx(
2170
+ "path",
2171
+ {
2172
+ d: "M8 2L6.5 3.5M16 2L17.5 3.5M3 9H7M17 9H21M12 2a5 5 0 015 5v4a5 5 0 01-10 0V7a5 5 0 015-5zM7 16l-2 3M17 16l2 3",
2173
+ stroke: color,
2174
+ strokeWidth: "1.5",
2175
+ strokeLinecap: "round"
2176
+ }
2177
+ ) });
1398
2178
  case "FEATURE_REQUEST":
1399
- return /* @__PURE__ */ jsx("svg", { ...props, children: /* @__PURE__ */ jsx("path", { d: "M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2z", stroke: color, strokeWidth: "1.5", strokeLinejoin: "round" }) });
2179
+ return /* @__PURE__ */ jsx("svg", { ...props, children: /* @__PURE__ */ jsx(
2180
+ "path",
2181
+ {
2182
+ d: "M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2z",
2183
+ stroke: color,
2184
+ strokeWidth: "1.5",
2185
+ strokeLinejoin: "round"
2186
+ }
2187
+ ) });
1400
2188
  case "QUESTION":
1401
2189
  return /* @__PURE__ */ jsxs("svg", { ...props, children: [
1402
2190
  /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10", stroke: color, strokeWidth: "1.5" }),
1403
- /* @__PURE__ */ jsx("path", { d: "M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3M12 17h.01", stroke: color, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })
2191
+ /* @__PURE__ */ jsx(
2192
+ "path",
2193
+ {
2194
+ d: "M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3M12 17h.01",
2195
+ stroke: color,
2196
+ strokeWidth: "1.5",
2197
+ strokeLinecap: "round",
2198
+ strokeLinejoin: "round"
2199
+ }
2200
+ )
1404
2201
  ] });
1405
2202
  case "OTHER":
1406
- return /* @__PURE__ */ jsx("svg", { ...props, children: /* @__PURE__ */ jsx("path", { d: "M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2v10z", stroke: color, strokeWidth: "1.5", strokeLinejoin: "round" }) });
2203
+ return /* @__PURE__ */ jsx("svg", { ...props, children: /* @__PURE__ */ jsx(
2204
+ "path",
2205
+ {
2206
+ d: "M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2v10z",
2207
+ stroke: color,
2208
+ strokeWidth: "1.5",
2209
+ strokeLinejoin: "round"
2210
+ }
2211
+ ) });
1407
2212
  }
1408
2213
  }
1409
2214
 
@@ -1625,15 +2430,31 @@ function GlitchgrabProviderInner({
1625
2430
  []
1626
2431
  );
1627
2432
  const enhance = useCallback(
1628
- async (text) => {
2433
+ async (text, screenshot) => {
1629
2434
  try {
1630
- return await enhanceText(text, token, baseUrl);
2435
+ const ctx = captureContext(visitedPagesRef.current);
2436
+ const context = {
2437
+ url: ctx.url,
2438
+ visitedPages: ctx.visitedPages.slice(-5),
2439
+ breadcrumbs: ctx.breadcrumbs.slice(-10).map((b) => ({ type: b.type, message: b.message }))
2440
+ };
2441
+ return await enhanceText(text, token, baseUrl, screenshot, context);
1631
2442
  } catch (e) {
1632
2443
  return text;
1633
2444
  }
1634
2445
  },
1635
2446
  [token, baseUrl]
1636
2447
  );
2448
+ const transcribe = useCallback(
2449
+ async (blob) => {
2450
+ try {
2451
+ return await transcribeAudio(blob, token, baseUrl);
2452
+ } catch (e) {
2453
+ return "";
2454
+ }
2455
+ },
2456
+ [token, baseUrl]
2457
+ );
1637
2458
  const openReportDialog = useCallback((options) => {
1638
2459
  if (typeof window !== "undefined") {
1639
2460
  window.dispatchEvent(new CustomEvent("glitchgrab:open-report", { detail: options }));
@@ -1678,7 +2499,7 @@ function GlitchgrabProviderInner({
1678
2499
  children
1679
2500
  }
1680
2501
  ),
1681
- /* @__PURE__ */ jsx2(ReportDialog, { report, enhanceText: enhance, types, showSeverity })
2502
+ /* @__PURE__ */ jsx2(ReportDialog, { report, enhanceText: enhance, transcribeAudio: transcribe, types, showSeverity })
1682
2503
  ]
1683
2504
  }
1684
2505
  );