glitchgrab 1.22.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,185 @@ 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 (e) {
857
+ if (!SpeechRec) {
858
+ setVoiceError("Microphone access denied");
859
+ return;
860
+ }
861
+ }
862
+ if (stream && transcribeAudio2) {
863
+ streamRef.current = stream;
864
+ sarvamChunksRef.current = [];
865
+ textBeforeVoiceRef.current = description;
866
+ const rec = new MediaRecorder(stream);
867
+ mediaRecorderRef.current = rec;
868
+ rec.ondataavailable = (e) => {
869
+ if (e.data.size > 0) sarvamChunksRef.current.push(e.data);
870
+ };
871
+ rec.onstop = async () => {
872
+ stream.getTracks().forEach((t2) => t2.stop());
873
+ if (streamRef.current === stream) streamRef.current = null;
874
+ mediaRecorderRef.current = null;
875
+ const chunks = sarvamChunksRef.current;
876
+ sarvamChunksRef.current = [];
877
+ if (!chunks.length || !transcribeAudio2) return;
878
+ setIsTranscribing(true);
879
+ try {
880
+ const blob = new Blob(chunks, { type: rec.mimeType || "audio/webm" });
881
+ const text = await transcribeAudio2(blob);
882
+ if (text.trim()) {
883
+ const sep = textBeforeVoiceRef.current.trim() ? " " : "";
884
+ setDescription(textBeforeVoiceRef.current + sep + text.trim());
885
+ setValidationError(null);
886
+ setIsEnhanced(false);
887
+ setOriginalDescription(null);
888
+ }
889
+ } catch (e) {
890
+ }
891
+ setIsTranscribing(false);
892
+ };
893
+ rec.start();
894
+ }
895
+ if (SpeechRec) {
896
+ setVoiceError(null);
897
+ usingWebSpeechRef.current = true;
898
+ voiceBaseRef.current = description;
899
+ textBeforeVoiceRef.current = description;
900
+ const recognition = new SpeechRec();
901
+ recognition.lang = "en-IN";
902
+ recognition.interimResults = true;
903
+ recognition.continuous = true;
904
+ recognitionRef.current = recognition;
905
+ recognition.onstart = () => setIsListening(true);
906
+ recognition.onresult = (event) => {
907
+ let finalText = "";
908
+ let interimText = "";
909
+ for (let i = event.resultIndex; i < event.results.length; i++) {
910
+ const text = event.results[i][0].transcript;
911
+ if (event.results[i].isFinal) finalText += text;
912
+ else interimText += text;
913
+ }
914
+ if (finalText) {
915
+ const sep = voiceBaseRef.current.trim() ? " " : "";
916
+ voiceBaseRef.current += sep + finalText.trim();
917
+ }
918
+ const liveText = interimText ? voiceBaseRef.current + (voiceBaseRef.current.trim() ? " " : "") + interimText : voiceBaseRef.current;
919
+ setDescription(liveText);
920
+ setValidationError(null);
921
+ };
922
+ recognition.onend = () => {
923
+ if (usingWebSpeechRef.current && recognitionRef.current === recognition) {
924
+ try {
925
+ recognition.start();
926
+ } catch (e) {
927
+ }
928
+ } else {
929
+ setIsListening(false);
930
+ }
931
+ };
932
+ recognition.onerror = (event) => {
933
+ if (event.error === "aborted" || event.error === "no-speech") return;
934
+ setVoiceError("Speech recognition not available \u2014 try Chrome");
935
+ usingWebSpeechRef.current = false;
936
+ recognitionRef.current = null;
937
+ setIsListening(false);
938
+ };
939
+ recognition.start();
940
+ } else if (stream) {
941
+ setVoiceError(null);
942
+ setIsListening(true);
943
+ }
944
+ };
945
+ const handleSpaceDown = (e) => {
946
+ if (e.code !== "Space") return;
947
+ if (spaceTimerRef.current || isListening || isPushToTalkRef.current) {
948
+ e.preventDefault();
949
+ return;
950
+ }
951
+ if (e.repeat || !transcribeAudio2 || isTranscribing) return;
952
+ e.preventDefault();
953
+ spaceTimerRef.current = setTimeout(() => {
954
+ isPushToTalkRef.current = true;
955
+ void toggleVoice();
956
+ }, 400);
957
+ };
958
+ const handleSpaceUp = (e) => {
959
+ var _a;
960
+ if (e.code !== "Space") return;
961
+ if (spaceTimerRef.current) {
962
+ clearTimeout(spaceTimerRef.current);
963
+ spaceTimerRef.current = null;
964
+ if (!isPushToTalkRef.current) {
965
+ const ta = textareaRef.current;
966
+ const pos = ta ? (_a = ta.selectionStart) != null ? _a : description.length : description.length;
967
+ setDescription((prev) => prev.slice(0, pos) + " " + prev.slice(pos));
968
+ requestAnimationFrame(() => {
969
+ if (textareaRef.current) {
970
+ textareaRef.current.selectionStart = pos + 1;
971
+ textareaRef.current.selectionEnd = pos + 1;
972
+ }
973
+ });
974
+ }
975
+ }
976
+ if (isPushToTalkRef.current) {
977
+ isPushToTalkRef.current = false;
978
+ stopListeningAndTranscribe();
979
+ }
706
980
  };
707
981
  const handleSubmit = async () => {
708
982
  try {
@@ -719,7 +993,11 @@ function ReportDialog({
719
993
  if (showSeverity && reportType === "BUG") {
720
994
  metadata.severity = severity;
721
995
  }
722
- const result = await report(reportType, description.trim(), Object.keys(metadata).length > 0 ? metadata : void 0);
996
+ const result = await report(
997
+ reportType,
998
+ description.trim(),
999
+ Object.keys(metadata).length > 0 ? metadata : void 0
1000
+ );
723
1001
  if (result) {
724
1002
  setSubmitted(true);
725
1003
  setDescription("");
@@ -736,6 +1014,16 @@ function ReportDialog({
736
1014
  };
737
1015
  if (!mounted) return null;
738
1016
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1017
+ isOpen && createPortal(
1018
+ /* @__PURE__ */ jsx("style", { children: `
1019
+ @keyframes gg-pulse{0%,100%{opacity:1}50%{opacity:0.35}}
1020
+ @keyframes gg-b1{0%,100%{height:3px}40%{height:13px}}
1021
+ @keyframes gg-b2{0%,100%{height:8px}50%{height:3px}}
1022
+ @keyframes gg-b3{0%,100%{height:4px}30%{height:14px}70%{height:5px}}
1023
+ @keyframes gg-b4{0%,100%{height:6px}60%{height:13px}}
1024
+ ` }),
1025
+ document.body
1026
+ ),
739
1027
  isOpen && createPortal(
740
1028
  /* @__PURE__ */ jsx(
741
1029
  "div",
@@ -792,36 +1080,64 @@ function ReportDialog({
792
1080
  alignItems: "center"
793
1081
  },
794
1082
  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
- ] }),
1083
+ /* @__PURE__ */ jsxs(
1084
+ "div",
1085
+ {
1086
+ style: {
1087
+ display: "flex",
1088
+ alignItems: "center",
1089
+ gap: "8px"
1090
+ },
1091
+ children: [
1092
+ step > 1 && /* @__PURE__ */ jsx(
1093
+ "button",
1094
+ {
1095
+ type: "button",
1096
+ onClick: () => setStep((s) => s - 1),
1097
+ style: {
1098
+ background: "none",
1099
+ border: "none",
1100
+ cursor: "pointer",
1101
+ padding: "2px",
1102
+ display: "flex",
1103
+ alignItems: "center"
1104
+ },
1105
+ "aria-label": "Back",
1106
+ children: /* @__PURE__ */ jsx(
1107
+ "svg",
1108
+ {
1109
+ width: "16",
1110
+ height: "16",
1111
+ viewBox: "0 0 16 16",
1112
+ fill: "none",
1113
+ children: /* @__PURE__ */ jsx(
1114
+ "path",
1115
+ {
1116
+ d: "M10 3L5 8L10 13",
1117
+ stroke: t.textMuted,
1118
+ strokeWidth: "2",
1119
+ strokeLinecap: "round",
1120
+ strokeLinejoin: "round"
1121
+ }
1122
+ )
1123
+ }
1124
+ )
1125
+ }
1126
+ ),
1127
+ /* @__PURE__ */ jsx(
1128
+ "span",
1129
+ {
1130
+ style: {
1131
+ fontSize: "15px",
1132
+ fontWeight: 600,
1133
+ color: t.text
1134
+ },
1135
+ children: step === 1 ? "What's on your mind?" : "Tell us more"
1136
+ }
1137
+ )
1138
+ ]
1139
+ }
1140
+ ),
825
1141
  /* @__PURE__ */ jsx(
826
1142
  "button",
827
1143
  {
@@ -837,27 +1153,74 @@ function ReportDialog({
837
1153
  justifyContent: "center"
838
1154
  },
839
1155
  "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" }) })
1156
+ children: /* @__PURE__ */ jsx(
1157
+ "svg",
1158
+ {
1159
+ width: "16",
1160
+ height: "16",
1161
+ viewBox: "0 0 16 16",
1162
+ fill: "none",
1163
+ style: { display: "block" },
1164
+ children: /* @__PURE__ */ jsx(
1165
+ "path",
1166
+ {
1167
+ d: "M4 4L12 12M12 4L4 12",
1168
+ stroke: t.textMuted,
1169
+ strokeWidth: "2",
1170
+ strokeLinecap: "round"
1171
+ }
1172
+ )
1173
+ }
1174
+ )
841
1175
  }
842
1176
  )
843
1177
  ]
844
1178
  }
845
1179
  ),
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)) })
1180
+ /* @__PURE__ */ jsx(
1181
+ "div",
1182
+ {
1183
+ style: {
1184
+ display: "flex",
1185
+ alignItems: "center",
1186
+ justifyContent: "center",
1187
+ gap: "0",
1188
+ marginTop: "12px"
1189
+ },
1190
+ children: [1, 2].map((s, i) => /* @__PURE__ */ jsxs(
1191
+ "div",
1192
+ {
1193
+ style: { display: "flex", alignItems: "center" },
1194
+ children: [
1195
+ /* @__PURE__ */ jsx(
1196
+ "div",
1197
+ {
1198
+ style: {
1199
+ width: "8px",
1200
+ height: "8px",
1201
+ borderRadius: "50%",
1202
+ backgroundColor: s <= step ? t.accent : t.inputBorder,
1203
+ transition: "background-color 0.2s ease"
1204
+ }
1205
+ }
1206
+ ),
1207
+ i < 1 && /* @__PURE__ */ jsx(
1208
+ "div",
1209
+ {
1210
+ style: {
1211
+ width: "40px",
1212
+ height: "2px",
1213
+ backgroundColor: s < step ? t.accent : t.inputBorder,
1214
+ transition: "background-color 0.2s ease"
1215
+ }
1216
+ }
1217
+ )
1218
+ ]
1219
+ },
1220
+ s
1221
+ ))
1222
+ }
1223
+ )
861
1224
  ]
862
1225
  }
863
1226
  ),
@@ -877,76 +1240,185 @@ function ReportDialog({
877
1240
  ]
878
1241
  }
879
1242
  ) : /* @__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",
1243
+ step === 1 && /* @__PURE__ */ jsx(
1244
+ "div",
882
1245
  {
883
- type: "button",
884
- onClick: () => {
885
- setReportType(type);
886
- setStep(2);
887
- },
888
1246
  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;
1247
+ display: "grid",
1248
+ gridTemplateColumns: "1fr 1fr",
1249
+ gap: "8px"
904
1250
  },
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
- )) }),
1251
+ children: availableTypes.map((type) => /* @__PURE__ */ jsxs(
1252
+ "button",
1253
+ {
1254
+ type: "button",
1255
+ onClick: () => {
1256
+ setReportType(type);
1257
+ setStep(2);
1258
+ },
1259
+ style: {
1260
+ display: "flex",
1261
+ flexDirection: "column",
1262
+ alignItems: "center",
1263
+ gap: "8px",
1264
+ padding: "16px 8px",
1265
+ borderRadius: "8px",
1266
+ border: `1px solid ${t.inputBorder}`,
1267
+ background: t.inputBg,
1268
+ cursor: "pointer",
1269
+ color: t.text,
1270
+ fontFamily: "inherit",
1271
+ transition: "border-color 0.15s ease"
1272
+ },
1273
+ onMouseEnter: (e) => {
1274
+ e.currentTarget.style.borderColor = t.accent;
1275
+ },
1276
+ onMouseLeave: (e) => {
1277
+ e.currentTarget.style.borderColor = t.inputBorder;
1278
+ },
1279
+ children: [
1280
+ getTypeIcon(type, t.accent),
1281
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "13px", fontWeight: 600 }, children: getTypeLabel(type) }),
1282
+ /* @__PURE__ */ jsx(
1283
+ "span",
1284
+ {
1285
+ style: {
1286
+ fontSize: "11px",
1287
+ color: t.textMuted,
1288
+ lineHeight: "1.3"
1289
+ },
1290
+ children: getTypeSubtitle(type)
1291
+ }
1292
+ )
1293
+ ]
1294
+ },
1295
+ type
1296
+ ))
1297
+ }
1298
+ ),
916
1299
  step === 2 && /* @__PURE__ */ jsxs(Fragment, { children: [
917
1300
  /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
918
1301
  /* @__PURE__ */ jsx(
919
1302
  "textarea",
920
1303
  {
1304
+ ref: textareaRef,
921
1305
  value: description,
922
1306
  onChange: (e) => {
923
1307
  setDescription(e.target.value);
924
1308
  if (validationError) setValidationError(null);
1309
+ if (isEnhanced) {
1310
+ setIsEnhanced(false);
1311
+ setOriginalDescription(null);
1312
+ }
925
1313
  },
926
- placeholder: getPlaceholder(reportType),
1314
+ placeholder: isTranscribing ? "Transcribing your speech\u2026" : isListening ? "Listening\u2026 speak now" : getPlaceholder(reportType, placeholderIdx, !!transcribeAudio2),
927
1315
  style: {
928
1316
  width: "100%",
929
1317
  minHeight: "100px",
930
- padding: enhanceText2 ? "28px 12px 10px" : "10px 12px",
1318
+ padding: enhanceText2 ? "28px 12px 36px" : transcribeAudio2 ? "10px 12px 36px" : "10px 12px",
931
1319
  borderRadius: "8px",
932
- border: `1px solid ${t.inputBorder}`,
1320
+ border: `1px solid ${isTranscribing ? "#f59e0b" : isListening ? t.accent : t.inputBorder}`,
933
1321
  fontSize: "14px",
934
1322
  fontFamily: "inherit",
935
- resize: "vertical",
1323
+ resize: "none",
936
1324
  outline: "none",
937
1325
  boxSizing: "border-box",
938
1326
  color: t.text,
939
- backgroundColor: t.inputBg
1327
+ backgroundColor: t.inputBg,
1328
+ transition: "border-color 0.2s ease"
940
1329
  },
1330
+ onKeyDown: handleSpaceDown,
1331
+ onKeyUp: handleSpaceUp,
941
1332
  onFocus: (e) => {
942
1333
  e.target.style.borderColor = t.accent;
943
1334
  },
944
1335
  onBlur: (e) => {
945
- e.target.style.borderColor = t.inputBorder;
1336
+ if (!isListening && !isTranscribing)
1337
+ e.target.style.borderColor = t.inputBorder;
946
1338
  },
947
1339
  autoFocus: true
948
1340
  }
949
1341
  ),
1342
+ (isListening || isTranscribing) && /* @__PURE__ */ jsxs(
1343
+ "span",
1344
+ {
1345
+ style: {
1346
+ position: "absolute",
1347
+ bottom: "10px",
1348
+ left: "10px",
1349
+ display: "inline-flex",
1350
+ alignItems: "center",
1351
+ gap: "4px",
1352
+ fontSize: "10px",
1353
+ fontWeight: 700,
1354
+ letterSpacing: "0.04em",
1355
+ color: isTranscribing ? "#f59e0b" : "#ef4444",
1356
+ pointerEvents: "none",
1357
+ zIndex: 1
1358
+ },
1359
+ children: [
1360
+ /* @__PURE__ */ jsx(
1361
+ "span",
1362
+ {
1363
+ style: {
1364
+ width: "6px",
1365
+ height: "6px",
1366
+ borderRadius: "50%",
1367
+ backgroundColor: "currentColor",
1368
+ animation: "gg-pulse 1.2s ease-in-out infinite",
1369
+ display: "inline-block",
1370
+ flexShrink: 0
1371
+ }
1372
+ }
1373
+ ),
1374
+ isTranscribing ? "Transcribing\u2026" : "REC",
1375
+ isListening && !isTranscribing && /* @__PURE__ */ jsx(
1376
+ "span",
1377
+ {
1378
+ style: {
1379
+ display: "inline-flex",
1380
+ alignItems: "flex-end",
1381
+ gap: "2px",
1382
+ height: "14px",
1383
+ marginLeft: "3px"
1384
+ },
1385
+ children: [
1386
+ { anim: "gg-b1", dur: "0.6s", delay: "0s" },
1387
+ {
1388
+ anim: "gg-b2",
1389
+ dur: "0.5s",
1390
+ delay: "0.1s"
1391
+ },
1392
+ {
1393
+ anim: "gg-b3",
1394
+ dur: "0.7s",
1395
+ delay: "0.05s"
1396
+ },
1397
+ {
1398
+ anim: "gg-b4",
1399
+ dur: "0.55s",
1400
+ delay: "0.15s"
1401
+ }
1402
+ ].map((b, i) => /* @__PURE__ */ jsx(
1403
+ "span",
1404
+ {
1405
+ style: {
1406
+ width: "2.5px",
1407
+ height: "4px",
1408
+ borderRadius: "1px",
1409
+ backgroundColor: "#ef4444",
1410
+ animation: `${b.anim} ${b.dur} ease-in-out ${b.delay} infinite`,
1411
+ display: "inline-block",
1412
+ alignSelf: "center"
1413
+ }
1414
+ },
1415
+ i
1416
+ ))
1417
+ }
1418
+ )
1419
+ ]
1420
+ }
1421
+ ),
950
1422
  enhanceText2 && /* @__PURE__ */ jsxs(
951
1423
  "button",
952
1424
  {
@@ -955,16 +1427,22 @@ function ReportDialog({
955
1427
  if (!description.trim() || isEnhancing) return;
956
1428
  setIsEnhancing(true);
957
1429
  try {
958
- const polished = await enhanceText2(description);
1430
+ const polished = await enhanceText2(
1431
+ description,
1432
+ screenshot
1433
+ );
959
1434
  if (polished && polished !== description) {
1435
+ setOriginalDescription(description);
960
1436
  setDescription(polished);
961
- if (validationError) setValidationError(null);
1437
+ setIsEnhanced(true);
1438
+ if (validationError)
1439
+ setValidationError(null);
962
1440
  }
963
1441
  } finally {
964
1442
  setIsEnhancing(false);
965
1443
  }
966
1444
  },
967
- disabled: !description.trim() || isEnhancing,
1445
+ disabled: !description.trim() || isEnhancing || isEnhanced,
968
1446
  style: {
969
1447
  position: "absolute",
970
1448
  top: "6px",
@@ -985,105 +1463,393 @@ function ReportDialog({
985
1463
  },
986
1464
  title: "Polish grammar \u2014 preserves your meaning",
987
1465
  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" }) }),
1466
+ /* @__PURE__ */ jsx(
1467
+ "svg",
1468
+ {
1469
+ width: "11",
1470
+ height: "11",
1471
+ viewBox: "0 0 24 24",
1472
+ fill: "none",
1473
+ "aria-hidden": "true",
1474
+ children: /* @__PURE__ */ jsx(
1475
+ "path",
1476
+ {
1477
+ 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",
1478
+ stroke: t.textMuted,
1479
+ strokeWidth: "1.5",
1480
+ strokeLinejoin: "round"
1481
+ }
1482
+ )
1483
+ }
1484
+ ),
989
1485
  isEnhancing ? "Enhancing..." : "AI enhance"
990
1486
  ]
991
1487
  }
1488
+ ),
1489
+ transcribeAudio2 && /* @__PURE__ */ jsx(
1490
+ "button",
1491
+ {
1492
+ type: "button",
1493
+ onClick: () => {
1494
+ void toggleVoice();
1495
+ },
1496
+ title: isListening ? "Stop recording" : "Speak your report",
1497
+ style: {
1498
+ position: "absolute",
1499
+ bottom: "14px",
1500
+ right: "7px",
1501
+ width: "26px",
1502
+ height: "26px",
1503
+ display: "flex",
1504
+ alignItems: "center",
1505
+ justifyContent: "center",
1506
+ background: "transparent",
1507
+ border: `1.5px solid ${isListening ? "#ef4444" : t.inputBorder}`,
1508
+ borderRadius: "6px",
1509
+ color: isListening ? "#ef4444" : t.textMuted,
1510
+ cursor: "pointer",
1511
+ zIndex: 1,
1512
+ padding: 0,
1513
+ transition: "border-color 0.2s ease, color 0.2s ease"
1514
+ },
1515
+ children: isListening ? /* @__PURE__ */ jsx(
1516
+ "svg",
1517
+ {
1518
+ width: "12",
1519
+ height: "12",
1520
+ viewBox: "0 0 24 24",
1521
+ fill: "currentColor",
1522
+ "aria-hidden": "true",
1523
+ children: /* @__PURE__ */ jsx(
1524
+ "rect",
1525
+ {
1526
+ x: "6",
1527
+ y: "6",
1528
+ width: "12",
1529
+ height: "12",
1530
+ rx: "2"
1531
+ }
1532
+ )
1533
+ }
1534
+ ) : /* @__PURE__ */ jsxs(
1535
+ "svg",
1536
+ {
1537
+ width: "13",
1538
+ height: "13",
1539
+ viewBox: "0 0 24 24",
1540
+ fill: "none",
1541
+ "aria-hidden": "true",
1542
+ children: [
1543
+ /* @__PURE__ */ jsx(
1544
+ "rect",
1545
+ {
1546
+ x: "9",
1547
+ y: "2",
1548
+ width: "6",
1549
+ height: "12",
1550
+ rx: "3",
1551
+ stroke: "currentColor",
1552
+ strokeWidth: "1.8"
1553
+ }
1554
+ ),
1555
+ /* @__PURE__ */ jsx(
1556
+ "path",
1557
+ {
1558
+ d: "M5 10a7 7 0 0014 0M12 19v3M9 22h6",
1559
+ stroke: "currentColor",
1560
+ strokeWidth: "1.8",
1561
+ strokeLinecap: "round"
1562
+ }
1563
+ )
1564
+ ]
1565
+ }
1566
+ )
1567
+ }
992
1568
  )
993
1569
  ] }),
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: [
1570
+ transcribeAudio2 && !isListening && !isTranscribing && /* @__PURE__ */ jsxs(
1571
+ "div",
1572
+ {
1573
+ style: {
1574
+ display: "flex",
1575
+ alignItems: "center",
1576
+ gap: "5px",
1577
+ marginTop: "5px",
1578
+ fontSize: "11px",
1579
+ color: t.text,
1580
+ opacity: 0.55,
1581
+ userSelect: "none"
1582
+ },
1583
+ children: [
1584
+ /* @__PURE__ */ jsxs("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1585
+ /* @__PURE__ */ jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }),
1586
+ /* @__PURE__ */ jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
1587
+ /* @__PURE__ */ jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })
1588
+ ] }),
1589
+ "Hold Space to speak \u2014 we'll transcribe it"
1590
+ ]
1591
+ }
1592
+ ),
1593
+ isEnhanced && originalDescription !== null && /* @__PURE__ */ jsxs(
1594
+ "div",
1595
+ {
1596
+ style: {
1597
+ display: "flex",
1598
+ alignItems: "center",
1599
+ gap: "6px",
1600
+ marginTop: "6px",
1601
+ padding: "6px 8px",
1602
+ borderRadius: "6px",
1603
+ background: isDark ? "rgba(34,211,238,0.06)" : "rgba(8,145,178,0.05)",
1604
+ border: `1px solid ${t.accent}40`
1605
+ },
1606
+ children: [
998
1607
  /* @__PURE__ */ jsx(
999
- "img",
1608
+ "svg",
1609
+ {
1610
+ width: "12",
1611
+ height: "12",
1612
+ viewBox: "0 0 24 24",
1613
+ fill: "none",
1614
+ style: { flexShrink: 0 },
1615
+ children: /* @__PURE__ */ jsx(
1616
+ "path",
1617
+ {
1618
+ d: "M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3z",
1619
+ stroke: t.accent,
1620
+ strokeWidth: "1.5",
1621
+ strokeLinejoin: "round"
1622
+ }
1623
+ )
1624
+ }
1625
+ ),
1626
+ /* @__PURE__ */ jsx(
1627
+ "span",
1000
1628
  {
1001
- src: screenshot,
1002
- alt: "Page screenshot",
1003
- onClick: () => setPreviewOpen(true),
1004
1629
  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
- }
1630
+ fontSize: "11px",
1631
+ color: t.textMuted,
1632
+ flex: 1
1633
+ },
1634
+ children: "AI enhanced"
1014
1635
  }
1015
1636
  ),
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(
1637
+ /* @__PURE__ */ jsx(
1030
1638
  "button",
1031
1639
  {
1032
1640
  type: "button",
1033
- onClick: () => setScreenshot(null),
1641
+ onClick: () => {
1642
+ setIsEnhanced(false);
1643
+ setOriginalDescription(null);
1644
+ },
1034
1645
  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,
1646
+ padding: "2px 8px",
1647
+ borderRadius: "4px",
1648
+ border: `1px solid ${t.accent}`,
1649
+ background: t.accent,
1650
+ color: t.accentText,
1651
+ fontSize: "11px",
1652
+ fontWeight: 600,
1043
1653
  cursor: "pointer",
1044
- fontFamily: "inherit",
1045
- display: "flex",
1046
- alignItems: "center",
1047
- justifyContent: "center",
1048
- gap: "4px"
1654
+ fontFamily: "inherit"
1049
1655
  },
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
- ]
1656
+ children: "Keep"
1054
1657
  }
1055
1658
  ),
1056
- /* @__PURE__ */ jsxs(
1659
+ /* @__PURE__ */ jsx(
1057
1660
  "button",
1058
1661
  {
1059
1662
  type: "button",
1060
1663
  onClick: () => {
1061
- var _a;
1062
- return (_a = fileInputRef.current) == null ? void 0 : _a.click();
1664
+ setDescription(originalDescription);
1665
+ setIsEnhanced(false);
1666
+ setOriginalDescription(null);
1063
1667
  },
1064
1668
  style: {
1065
- flex: 1,
1066
- padding: "6px 0",
1067
- borderRadius: "6px",
1669
+ padding: "2px 8px",
1670
+ borderRadius: "4px",
1068
1671
  border: `1px solid ${t.inputBorder}`,
1069
1672
  background: "transparent",
1070
1673
  color: t.textMuted,
1071
- fontSize: "12px",
1072
- fontWeight: 500,
1674
+ fontSize: "11px",
1675
+ fontWeight: 600,
1073
1676
  cursor: "pointer",
1074
- fontFamily: "inherit",
1075
- display: "flex",
1076
- alignItems: "center",
1077
- justifyContent: "center",
1078
- gap: "4px"
1677
+ fontFamily: "inherit"
1079
1678
  },
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
- ]
1679
+ children: "Restore"
1680
+ }
1681
+ )
1682
+ ]
1683
+ }
1684
+ ),
1685
+ voiceError && /* @__PURE__ */ jsx(
1686
+ "p",
1687
+ {
1688
+ style: {
1689
+ color: "#ef4444",
1690
+ fontSize: "11px",
1691
+ marginTop: "4px",
1692
+ marginBottom: 0
1693
+ },
1694
+ children: voiceError
1695
+ }
1696
+ ),
1697
+ /* @__PURE__ */ jsxs("div", { style: { marginTop: "10px" }, children: [
1698
+ /* @__PURE__ */ jsx(
1699
+ "span",
1700
+ {
1701
+ style: {
1702
+ fontSize: "12px",
1703
+ color: t.textMuted,
1704
+ marginBottom: "6px",
1705
+ display: "block",
1706
+ fontWeight: 500
1707
+ },
1708
+ children: "Screenshot"
1709
+ }
1710
+ ),
1711
+ screenshot ? /* @__PURE__ */ jsxs("div", { children: [
1712
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
1713
+ /* @__PURE__ */ jsx(
1714
+ "img",
1715
+ {
1716
+ src: screenshot,
1717
+ alt: "Page screenshot",
1718
+ onClick: () => setPreviewOpen(true),
1719
+ style: {
1720
+ width: "100%",
1721
+ borderRadius: "6px",
1722
+ border: `1px solid ${t.border}`,
1723
+ maxHeight: "64px",
1724
+ objectFit: "cover",
1725
+ objectPosition: "top",
1726
+ cursor: "zoom-in",
1727
+ display: "block"
1728
+ }
1729
+ }
1730
+ ),
1731
+ /* @__PURE__ */ jsx(
1732
+ "span",
1733
+ {
1734
+ style: {
1735
+ position: "absolute",
1736
+ bottom: "5px",
1737
+ left: "6px",
1738
+ background: "rgba(0,0,0,0.55)",
1739
+ color: "#fff",
1740
+ fontSize: "10px",
1741
+ padding: "1px 5px",
1742
+ borderRadius: "3px",
1743
+ pointerEvents: "none"
1744
+ },
1745
+ children: "click to preview"
1084
1746
  }
1085
1747
  )
1086
- ] })
1748
+ ] }),
1749
+ /* @__PURE__ */ jsxs(
1750
+ "div",
1751
+ {
1752
+ style: {
1753
+ display: "flex",
1754
+ gap: "6px",
1755
+ marginTop: "6px"
1756
+ },
1757
+ children: [
1758
+ /* @__PURE__ */ jsxs(
1759
+ "button",
1760
+ {
1761
+ type: "button",
1762
+ onClick: () => setScreenshot(null),
1763
+ style: {
1764
+ flex: 1,
1765
+ padding: "6px 0",
1766
+ borderRadius: "6px",
1767
+ border: `1px solid ${t.inputBorder}`,
1768
+ background: "transparent",
1769
+ color: "#ef4444",
1770
+ fontSize: "12px",
1771
+ fontWeight: 500,
1772
+ cursor: "pointer",
1773
+ fontFamily: "inherit",
1774
+ display: "flex",
1775
+ alignItems: "center",
1776
+ justifyContent: "center",
1777
+ gap: "4px"
1778
+ },
1779
+ children: [
1780
+ /* @__PURE__ */ jsx(
1781
+ "svg",
1782
+ {
1783
+ width: "11",
1784
+ height: "11",
1785
+ viewBox: "0 0 10 10",
1786
+ fill: "none",
1787
+ children: /* @__PURE__ */ jsx(
1788
+ "path",
1789
+ {
1790
+ d: "M1 1L9 9M9 1L1 9",
1791
+ stroke: "currentColor",
1792
+ strokeWidth: "1.5",
1793
+ strokeLinecap: "round"
1794
+ }
1795
+ )
1796
+ }
1797
+ ),
1798
+ "Remove"
1799
+ ]
1800
+ }
1801
+ ),
1802
+ /* @__PURE__ */ jsxs(
1803
+ "button",
1804
+ {
1805
+ type: "button",
1806
+ onClick: () => {
1807
+ var _a;
1808
+ return (_a = fileInputRef.current) == null ? void 0 : _a.click();
1809
+ },
1810
+ style: {
1811
+ flex: 1,
1812
+ padding: "6px 0",
1813
+ borderRadius: "6px",
1814
+ border: `1px solid ${t.inputBorder}`,
1815
+ background: "transparent",
1816
+ color: t.textMuted,
1817
+ fontSize: "12px",
1818
+ fontWeight: 500,
1819
+ cursor: "pointer",
1820
+ fontFamily: "inherit",
1821
+ display: "flex",
1822
+ alignItems: "center",
1823
+ justifyContent: "center",
1824
+ gap: "4px"
1825
+ },
1826
+ children: [
1827
+ /* @__PURE__ */ jsx(
1828
+ "svg",
1829
+ {
1830
+ width: "11",
1831
+ height: "11",
1832
+ viewBox: "0 0 24 24",
1833
+ fill: "none",
1834
+ children: /* @__PURE__ */ jsx(
1835
+ "path",
1836
+ {
1837
+ d: "M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12",
1838
+ stroke: "currentColor",
1839
+ strokeWidth: "2",
1840
+ strokeLinecap: "round",
1841
+ strokeLinejoin: "round"
1842
+ }
1843
+ )
1844
+ }
1845
+ ),
1846
+ "Replace"
1847
+ ]
1848
+ }
1849
+ )
1850
+ ]
1851
+ }
1852
+ )
1087
1853
  ] }) : (
1088
1854
  /* Drop zone */
1089
1855
  /* @__PURE__ */ jsxs(
@@ -1096,7 +1862,9 @@ function ReportDialog({
1096
1862
  onDrop: (e) => {
1097
1863
  e.preventDefault();
1098
1864
  setScreenshotDragOver(false);
1099
- const file = Array.from(e.dataTransfer.files).find((f) => f.type.startsWith("image/"));
1865
+ const file = Array.from(
1866
+ e.dataTransfer.files
1867
+ ).find((f) => f.type.startsWith("image/"));
1100
1868
  if (!file) return;
1101
1869
  const reader = new FileReader();
1102
1870
  reader.onload = () => setScreenshot(reader.result);
@@ -1117,20 +1885,95 @@ function ReportDialog({
1117
1885
  transition: "border-color 0.15s ease, background 0.15s ease"
1118
1886
  },
1119
1887
  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" })
1888
+ /* @__PURE__ */ jsxs(
1889
+ "svg",
1890
+ {
1891
+ width: "22",
1892
+ height: "22",
1893
+ viewBox: "0 0 24 24",
1894
+ fill: "none",
1895
+ style: {
1896
+ margin: "0 auto 6px",
1897
+ display: "block",
1898
+ opacity: 0.45
1899
+ },
1900
+ children: [
1901
+ /* @__PURE__ */ jsx(
1902
+ "rect",
1903
+ {
1904
+ x: "3",
1905
+ y: "3",
1906
+ width: "18",
1907
+ height: "18",
1908
+ rx: "2",
1909
+ stroke: t.textMuted,
1910
+ strokeWidth: "1.5"
1911
+ }
1912
+ ),
1913
+ /* @__PURE__ */ jsx(
1914
+ "circle",
1915
+ {
1916
+ cx: "8.5",
1917
+ cy: "8.5",
1918
+ r: "1.5",
1919
+ stroke: t.textMuted,
1920
+ strokeWidth: "1.5"
1921
+ }
1922
+ ),
1923
+ /* @__PURE__ */ jsx(
1924
+ "path",
1925
+ {
1926
+ d: "M21 15l-5-5L5 21",
1927
+ stroke: t.textMuted,
1928
+ strokeWidth: "1.5",
1929
+ strokeLinecap: "round",
1930
+ strokeLinejoin: "round"
1931
+ }
1932
+ )
1933
+ ]
1934
+ }
1935
+ ),
1936
+ /* @__PURE__ */ jsx(
1937
+ "p",
1938
+ {
1939
+ style: {
1940
+ margin: 0,
1941
+ fontSize: "12px",
1942
+ fontWeight: 600,
1943
+ color: t.text
1944
+ },
1945
+ children: screenshotDragOver ? "Drop to attach" : "Add a screenshot"
1946
+ }
1947
+ ),
1948
+ /* @__PURE__ */ jsx(
1949
+ "p",
1950
+ {
1951
+ style: {
1952
+ margin: "3px 0 0",
1953
+ fontSize: "11px",
1954
+ color: t.textMuted
1955
+ },
1956
+ children: "Drag & drop \xB7 Paste \u2318V / Ctrl+V \xB7 Click to browse"
1957
+ }
1958
+ )
1127
1959
  ]
1128
1960
  }
1129
1961
  )
1130
1962
  )
1131
1963
  ] }),
1132
1964
  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" }),
1965
+ /* @__PURE__ */ jsx(
1966
+ "span",
1967
+ {
1968
+ style: {
1969
+ fontSize: "12px",
1970
+ color: t.textMuted,
1971
+ marginBottom: "6px",
1972
+ display: "block"
1973
+ },
1974
+ children: "Severity"
1975
+ }
1976
+ ),
1134
1977
  /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: "6px" }, children: ["low", "medium", "high"].map((s) => /* @__PURE__ */ jsx(
1135
1978
  "button",
1136
1979
  {
@@ -1155,114 +1998,34 @@ function ReportDialog({
1155
1998
  s
1156
1999
  )) })
1157
2000
  ] }),
1158
- validationError && /* @__PURE__ */ jsx("p", { style: { color: "#ef4444", fontSize: "12px", marginTop: "6px" }, children: validationError }),
1159
- /* @__PURE__ */ jsx(
1160
- "button",
2001
+ validationError && /* @__PURE__ */ jsx(
2002
+ "p",
1161
2003
  {
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
2004
  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"
2005
+ color: "#ef4444",
2006
+ fontSize: "12px",
2007
+ marginTop: "6px"
1186
2008
  },
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
- }
2009
+ children: validationError
1248
2010
  }
1249
- ) }),
2011
+ ),
1250
2012
  /* @__PURE__ */ jsx(
1251
2013
  "button",
1252
2014
  {
1253
2015
  type: "button",
1254
2016
  onClick: handleSubmit,
1255
- disabled: isSubmitting,
2017
+ disabled: !description.trim() || isSubmitting,
1256
2018
  style: {
2019
+ marginTop: "12px",
1257
2020
  width: "100%",
1258
2021
  padding: "10px",
1259
2022
  borderRadius: "8px",
1260
2023
  border: "none",
1261
- backgroundColor: isSubmitting ? t.bgSecondary : t.accent,
1262
- color: isSubmitting ? t.textMuted : t.accentText,
2024
+ backgroundColor: !description.trim() || isSubmitting ? t.bgSecondary : t.accent,
2025
+ color: !description.trim() || isSubmitting ? t.textMuted : t.accentText,
1263
2026
  fontSize: "14px",
1264
2027
  fontWeight: 600,
1265
- cursor: isSubmitting ? "not-allowed" : "pointer",
2028
+ cursor: !description.trim() || isSubmitting ? "not-allowed" : "pointer",
1266
2029
  fontFamily: "inherit",
1267
2030
  transition: "background-color 0.15s ease"
1268
2031
  },
@@ -1378,10 +2141,11 @@ function getTypeSubtitle(type) {
1378
2141
  return "General feedback";
1379
2142
  }
1380
2143
  }
1381
- function getPlaceholder(type) {
2144
+ function getPlaceholder(type, idx = 0, hasVoice = false) {
2145
+ if (hasVoice && idx === 1) return "Hold Space to speak \u2014 we'll transcribe it";
1382
2146
  switch (type) {
1383
2147
  case "BUG":
1384
- return "What went wrong?";
2148
+ return "What went wrong? Describe it or paste an error...";
1385
2149
  case "FEATURE_REQUEST":
1386
2150
  return "Describe the feature you'd like...";
1387
2151
  case "QUESTION":
@@ -1391,19 +2155,58 @@ function getPlaceholder(type) {
1391
2155
  }
1392
2156
  }
1393
2157
  function getTypeIcon(type, color, size = 24) {
1394
- const props = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0 } };
2158
+ const props = {
2159
+ width: size,
2160
+ height: size,
2161
+ viewBox: "0 0 24 24",
2162
+ fill: "none",
2163
+ style: { flexShrink: 0 }
2164
+ };
1395
2165
  switch (type) {
1396
2166
  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" }) });
2167
+ return /* @__PURE__ */ jsx("svg", { ...props, children: /* @__PURE__ */ jsx(
2168
+ "path",
2169
+ {
2170
+ 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",
2171
+ stroke: color,
2172
+ strokeWidth: "1.5",
2173
+ strokeLinecap: "round"
2174
+ }
2175
+ ) });
1398
2176
  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" }) });
2177
+ return /* @__PURE__ */ jsx("svg", { ...props, children: /* @__PURE__ */ jsx(
2178
+ "path",
2179
+ {
2180
+ 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",
2181
+ stroke: color,
2182
+ strokeWidth: "1.5",
2183
+ strokeLinejoin: "round"
2184
+ }
2185
+ ) });
1400
2186
  case "QUESTION":
1401
2187
  return /* @__PURE__ */ jsxs("svg", { ...props, children: [
1402
2188
  /* @__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" })
2189
+ /* @__PURE__ */ jsx(
2190
+ "path",
2191
+ {
2192
+ d: "M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3M12 17h.01",
2193
+ stroke: color,
2194
+ strokeWidth: "1.5",
2195
+ strokeLinecap: "round",
2196
+ strokeLinejoin: "round"
2197
+ }
2198
+ )
1404
2199
  ] });
1405
2200
  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" }) });
2201
+ return /* @__PURE__ */ jsx("svg", { ...props, children: /* @__PURE__ */ jsx(
2202
+ "path",
2203
+ {
2204
+ d: "M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2v10z",
2205
+ stroke: color,
2206
+ strokeWidth: "1.5",
2207
+ strokeLinejoin: "round"
2208
+ }
2209
+ ) });
1407
2210
  }
1408
2211
  }
1409
2212
 
@@ -1625,15 +2428,31 @@ function GlitchgrabProviderInner({
1625
2428
  []
1626
2429
  );
1627
2430
  const enhance = useCallback(
1628
- async (text) => {
2431
+ async (text, screenshot) => {
1629
2432
  try {
1630
- return await enhanceText(text, token, baseUrl);
2433
+ const ctx = captureContext(visitedPagesRef.current);
2434
+ const context = {
2435
+ url: ctx.url,
2436
+ visitedPages: ctx.visitedPages.slice(-5),
2437
+ breadcrumbs: ctx.breadcrumbs.slice(-10).map((b) => ({ type: b.type, message: b.message }))
2438
+ };
2439
+ return await enhanceText(text, token, baseUrl, screenshot, context);
1631
2440
  } catch (e) {
1632
2441
  return text;
1633
2442
  }
1634
2443
  },
1635
2444
  [token, baseUrl]
1636
2445
  );
2446
+ const transcribe = useCallback(
2447
+ async (blob) => {
2448
+ try {
2449
+ return await transcribeAudio(blob, token, baseUrl);
2450
+ } catch (e) {
2451
+ return "";
2452
+ }
2453
+ },
2454
+ [token, baseUrl]
2455
+ );
1637
2456
  const openReportDialog = useCallback((options) => {
1638
2457
  if (typeof window !== "undefined") {
1639
2458
  window.dispatchEvent(new CustomEvent("glitchgrab:open-report", { detail: options }));
@@ -1678,7 +2497,7 @@ function GlitchgrabProviderInner({
1678
2497
  children
1679
2498
  }
1680
2499
  ),
1681
- /* @__PURE__ */ jsx2(ReportDialog, { report, enhanceText: enhance, types, showSeverity })
2500
+ /* @__PURE__ */ jsx2(ReportDialog, { report, enhanceText: enhance, transcribeAudio: transcribe, types, showSeverity })
1682
2501
  ]
1683
2502
  }
1684
2503
  );