glitchgrab 1.58.1 → 1.58.3

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
@@ -178,7 +178,7 @@ function argsToString(args) {
178
178
  function previewJson(value, max) {
179
179
  let out = "";
180
180
  const path = /* @__PURE__ */ new WeakSet();
181
- const write = (input, inArray) => {
181
+ const write2 = (input, inArray) => {
182
182
  if (out.length > max) return;
183
183
  let v = input;
184
184
  if (v !== null && typeof v === "object" && typeof v.toJSON === "function") {
@@ -215,7 +215,7 @@ function previewJson(value, max) {
215
215
  out += "[";
216
216
  for (let i = 0; i < obj.length && out.length <= max; i++) {
217
217
  if (i > 0) out += ",";
218
- write(obj[i], true);
218
+ write2(obj[i], true);
219
219
  }
220
220
  out += "]";
221
221
  } else {
@@ -228,13 +228,13 @@ function previewJson(value, max) {
228
228
  if (child === void 0 || typeof child === "function" || typeof child === "symbol") continue;
229
229
  out += `${first ? "" : ","}${JSON.stringify(key)}:`;
230
230
  first = false;
231
- write(child, false);
231
+ write2(child, false);
232
232
  }
233
233
  out += "}";
234
234
  }
235
235
  path.delete(obj);
236
236
  };
237
- write(value, false);
237
+ write2(value, false);
238
238
  return out;
239
239
  }
240
240
  async function readErrorBody(response, url) {
@@ -587,6 +587,9 @@ function contextMetadata(context) {
587
587
  var DEFAULT_BASE_URL = "https://glitchgrab.dev";
588
588
  var MAX_RETRIES = 3;
589
589
  var RETRY_BASE_MS = 500;
590
+ function reportAttemptTimeoutMs(bodyLength) {
591
+ return 1e4 + Math.ceil(bodyLength / 1e5) * 1e3;
592
+ }
590
593
  async function sendReport(payload, baseUrl) {
591
594
  var _a;
592
595
  try {
@@ -597,13 +600,18 @@ async function sendReport(payload, baseUrl) {
597
600
  Authorization: `Bearer ${payload.token}`
598
601
  };
599
602
  const useKeepalive = body.length < 6e4;
603
+ const timeoutMs = reportAttemptTimeoutMs(body.length);
600
604
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
605
+ const isLast = attempt === MAX_RETRIES - 1;
606
+ const controller = !isLast && typeof AbortController === "function" ? new AbortController() : null;
607
+ const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
601
608
  try {
602
609
  const response = await fetch(url, {
603
610
  method: "POST",
604
611
  headers,
605
612
  body,
606
- ...useKeepalive ? { keepalive: true } : {}
613
+ ...useKeepalive ? { keepalive: true } : {},
614
+ ...controller ? { signal: controller.signal } : {}
607
615
  });
608
616
  if (response.ok) {
609
617
  const envelope = await response.json();
@@ -613,6 +621,8 @@ async function sendReport(payload, baseUrl) {
613
621
  return null;
614
622
  }
615
623
  } catch (e) {
624
+ } finally {
625
+ if (timer) clearTimeout(timer);
616
626
  }
617
627
  if (attempt < MAX_RETRIES - 1) {
618
628
  await new Promise(
@@ -729,7 +739,7 @@ async function enhanceText(text, token, baseUrl, screenshot, context, identity)
729
739
  return text;
730
740
  }
731
741
  }
732
- async function findSimilarIssues(description, token, baseUrl, identity) {
742
+ async function findSimilarIssues(description, token, baseUrl, identity, files) {
733
743
  var _a;
734
744
  try {
735
745
  const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/reports/similar`;
@@ -739,7 +749,11 @@ async function findSimilarIssues(description, token, baseUrl, identity) {
739
749
  "Content-Type": "application/json",
740
750
  Authorization: `Bearer ${token}`
741
751
  },
742
- body: JSON.stringify({ text: description, ...signedFields(identity) })
752
+ body: JSON.stringify({
753
+ text: description,
754
+ ...(files == null ? void 0 : files.length) ? { files } : {},
755
+ ...signedFields(identity)
756
+ })
743
757
  });
744
758
  const envelope = await response.json().catch(() => null);
745
759
  if (!response.ok || !(envelope == null ? void 0 : envelope.success)) return null;
@@ -749,6 +763,25 @@ async function findSimilarIssues(description, token, baseUrl, identity) {
749
763
  return null;
750
764
  }
751
765
  }
766
+ async function readAttachment(file, token, baseUrl, identity) {
767
+ var _a;
768
+ try {
769
+ const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/ai/read-file`;
770
+ const response = await fetch(url, {
771
+ method: "POST",
772
+ headers: {
773
+ "Content-Type": "application/json",
774
+ Authorization: `Bearer ${token}`
775
+ },
776
+ body: JSON.stringify({ name: file.name, data: file.dataUrl, ...signedFields(identity) })
777
+ });
778
+ const envelope = await response.json().catch(() => null);
779
+ if (!response.ok || !(envelope == null ? void 0 : envelope.success) || !((_a = envelope.data) == null ? void 0 : _a.enabled)) return null;
780
+ return typeof envelope.data.text === "string" ? envelope.data.text : null;
781
+ } catch (e) {
782
+ return null;
783
+ }
784
+ }
752
785
  async function assistTurn(params, token, baseUrl) {
753
786
  const offline = assistOffline();
754
787
  try {
@@ -862,6 +895,292 @@ function clearDedupCache() {
862
895
  seen.clear();
863
896
  }
864
897
 
898
+ // src/hydration.ts
899
+ var HYDRATION_CODE_RE = /Minified React error #(418|419|422|423|425)\b/;
900
+ var HYDRATION_DEV_RE = /Hydration failed|hydration mismatch|Text content does not match|did not match\. Server|There was an error while hydrating|error occurred during hydration|server rendered HTML didn't match/i;
901
+ var BAILOUT_DIGEST = "BAILOUT_TO_CLIENT_SIDE_RENDERING";
902
+ var DOM_SNIPPET_MAX = 3e3;
903
+ var STACK_FRAMES_MAX = 40;
904
+ function messageOf(error) {
905
+ if (error && typeof error === "object" && typeof error.message === "string") {
906
+ return error.message;
907
+ }
908
+ return typeof error === "string" ? error : "";
909
+ }
910
+ function hydrationCode(message) {
911
+ if (!message) return null;
912
+ const m = HYDRATION_CODE_RE.exec(message);
913
+ if (m) return m[1];
914
+ return HYDRATION_DEV_RE.test(message) ? "dev" : null;
915
+ }
916
+ function isHydrationError(error) {
917
+ if (hydrationCode(messageOf(error))) return true;
918
+ const cause = error && typeof error === "object" ? error.cause : void 0;
919
+ return cause !== void 0 && cause !== error && hydrationCode(messageOf(cause)) !== null;
920
+ }
921
+ function isBailout(error) {
922
+ const e = error;
923
+ if (!e || typeof e !== "object") return false;
924
+ if (e.digest === BAILOUT_DIGEST) return true;
925
+ const c = e.cause;
926
+ return !!c && typeof c === "object" && c.digest === BAILOUT_DIGEST;
927
+ }
928
+ function componentStackNames(stack) {
929
+ if (!stack) return [];
930
+ const names = [];
931
+ for (const raw of stack.split("\n")) {
932
+ const line = raw.trim();
933
+ if (!line) continue;
934
+ let name = "";
935
+ if (line.startsWith("at ")) {
936
+ name = line.slice(3).split(" (")[0].trim();
937
+ } else if (line.includes("@")) {
938
+ name = line.slice(0, line.indexOf("@")).trim();
939
+ } else {
940
+ name = line;
941
+ }
942
+ if (name) names.push(name);
943
+ if (names.length >= STACK_FRAMES_MAX) break;
944
+ }
945
+ return names;
946
+ }
947
+ function componentStackKey(stack) {
948
+ return componentStackNames(stack).join(">");
949
+ }
950
+ var observer = null;
951
+ var observerStarted = false;
952
+ var observerStopAt = 0;
953
+ var OBSERVE_MS = 3e4;
954
+ function startObserver() {
955
+ if (observerStarted || typeof MutationObserver === "undefined" || typeof document === "undefined") {
956
+ return;
957
+ }
958
+ observerStarted = true;
959
+ try {
960
+ observer = new MutationObserver(() => {
961
+ if (Date.now() > observerStopAt) stopObserver();
962
+ });
963
+ observer.observe(document, { childList: true, subtree: true });
964
+ observerStopAt = Date.now() + OBSERVE_MS;
965
+ setTimeout(stopObserver, OBSERVE_MS);
966
+ } catch (e) {
967
+ observer = null;
968
+ }
969
+ }
970
+ function stopObserver() {
971
+ try {
972
+ observer == null ? void 0 : observer.disconnect();
973
+ } catch (e) {
974
+ }
975
+ observer = null;
976
+ }
977
+ function describeNode(node) {
978
+ if (!node) return "";
979
+ if (node.nodeType === 9) return "#document";
980
+ const el = node;
981
+ if (!el.tagName) return node.nodeName;
982
+ const id = el.id ? `#${el.id}` : "";
983
+ const cls = typeof el.className === "string" && el.className.trim() ? "." + el.className.trim().split(/\s+/).slice(0, 3).join(".") : "";
984
+ return `${el.tagName.toLowerCase()}${id}${cls}`;
985
+ }
986
+ function pathOf(node) {
987
+ const parts = [];
988
+ let cur = node;
989
+ while (cur && parts.length < 6) {
990
+ parts.unshift(describeNode(cur));
991
+ if (cur.nodeType === 9) break;
992
+ cur = cur.parentNode;
993
+ }
994
+ return parts.join(" > ");
995
+ }
996
+ var NON_CONTENT_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "LINK", "META", "NOSCRIPT", "TEMPLATE", "TITLE", "HEAD"]);
997
+ function isInHead(node) {
998
+ for (let cur = node; cur; cur = cur.parentNode) {
999
+ if (cur.nodeName === "HEAD") return true;
1000
+ if (cur.nodeName === "BODY") return false;
1001
+ }
1002
+ return false;
1003
+ }
1004
+ var GSTIN_RE = /\b\d{2}[A-Z]{5}\d{4}[A-Z][A-Z0-9]Z[A-Z0-9]\b/g;
1005
+ var PAN_RE = /\b[A-Z]{5}\d{4}[A-Z]\b/g;
1006
+ function scrubHtml(html) {
1007
+ return redactString(
1008
+ html.replace(/\svalue="[^"]*"/gi, ' value="[REDACTED]"').replace(GSTIN_RE, "[REDACTED_GSTIN]").replace(PAN_RE, "[REDACTED_PAN]").replace(/\d{6,}/g, (m) => "#".repeat(Math.min(m.length, 6)))
1009
+ );
1010
+ }
1011
+ function takeRemovedServerHtml() {
1012
+ var _a, _b;
1013
+ if (!observer) return { html: "", where: "" };
1014
+ try {
1015
+ const records = observer.takeRecords();
1016
+ let html = "";
1017
+ let where = "";
1018
+ for (const rec of records) {
1019
+ if (rec.type !== "childList" || rec.removedNodes.length === 0) continue;
1020
+ if (isInHead(rec.target)) continue;
1021
+ if (!where) where = pathOf(rec.target);
1022
+ for (const node of Array.from(rec.removedNodes)) {
1023
+ if (node.nodeType === 1 && NON_CONTENT_TAGS.has(node.tagName)) continue;
1024
+ if (node.nodeType === 3 && !((_a = node.textContent) != null ? _a : "").trim()) continue;
1025
+ const piece = node.nodeType === 1 ? node.outerHTML : node.nodeType === 3 ? `#text "${(_b = node.textContent) != null ? _b : ""}"` : "";
1026
+ if (!piece) continue;
1027
+ html += (html ? "\n" : "") + piece;
1028
+ if (html.length >= DOM_SNIPPET_MAX) break;
1029
+ }
1030
+ if (html.length >= DOM_SNIPPET_MAX) break;
1031
+ }
1032
+ const scrubbed = scrubHtml(html);
1033
+ return {
1034
+ html: scrubbed.length > DOM_SNIPPET_MAX ? `${scrubbed.slice(0, DOM_SNIPPET_MAX)}\u2026[truncated]` : scrubbed,
1035
+ where
1036
+ };
1037
+ } catch (e) {
1038
+ return { html: "", where: "" };
1039
+ }
1040
+ }
1041
+ function detectForeignNodes(doc = document) {
1042
+ var _a;
1043
+ const found = [];
1044
+ try {
1045
+ const html = doc.documentElement;
1046
+ if ((html == null ? void 0 : html.classList.contains("translated-ltr")) || (html == null ? void 0 : html.classList.contains("translated-rtl"))) {
1047
+ found.push("Google Translate is active (html.translated-*)");
1048
+ }
1049
+ const fonts = doc.getElementsByTagName("font").length;
1050
+ if (fonts > 0) found.push(`<font> \xD7 ${fonts} (Google Translate or a translation extension)`);
1051
+ const detectors = doc.querySelectorAll("a[x-apple-data-detectors]").length;
1052
+ if (detectors > 0) {
1053
+ found.push(`<a x-apple-data-detectors> \xD7 ${detectors} (iOS format detection turned text into links)`);
1054
+ }
1055
+ if (doc.querySelector("[data-gr-ext-installed],[data-new-gr-c-s-check-loaded],grammarly-desktop-integration")) {
1056
+ found.push("Grammarly extension attributes");
1057
+ }
1058
+ if (doc.querySelector("[data-lt-installed]")) found.push("LanguageTool extension attributes");
1059
+ if ((_a = doc.body) == null ? void 0 : _a.hasAttribute("cz-shortcut-listen")) found.push("ColorZilla extension attribute on <body>");
1060
+ const custom = /* @__PURE__ */ new Set();
1061
+ for (const parent of [html, doc.body]) {
1062
+ if (!parent) continue;
1063
+ for (const child of Array.from(parent.children)) {
1064
+ const tag = child.tagName.toLowerCase();
1065
+ if (tag.includes("-")) custom.add(tag);
1066
+ }
1067
+ }
1068
+ if (custom.size > 0) {
1069
+ found.push(`custom elements at top level: ${Array.from(custom).slice(0, 5).join(", ")} (often an extension)`);
1070
+ }
1071
+ } catch (e) {
1072
+ }
1073
+ return found;
1074
+ }
1075
+ function hydrationTiming() {
1076
+ var _a, _b, _c, _d;
1077
+ try {
1078
+ if (typeof performance === "undefined") return "";
1079
+ const at2 = Math.round(performance.now());
1080
+ const parts = [`error at ${at2}ms after navigation start`];
1081
+ const nav = (_b = (_a = performance.getEntriesByType) == null ? void 0 : _a.call(performance, "navigation")) == null ? void 0 : _b[0];
1082
+ if (nav == null ? void 0 : nav.domContentLoadedEventEnd) {
1083
+ parts.push(`DOMContentLoaded ${Math.round(nav.domContentLoadedEventEnd)}ms`);
1084
+ }
1085
+ const fetches = ((_d = (_c = performance.getEntriesByType) == null ? void 0 : _c.call(performance, "resource")) != null ? _d : []).filter((e) => {
1086
+ const t = e.initiatorType;
1087
+ return t === "fetch" || t === "xmlhttprequest";
1088
+ }).sort((a, b) => a.responseEnd - b.responseEnd).slice(0, 8).map((e) => {
1089
+ let path = e.name;
1090
+ try {
1091
+ const u = new URL(e.name);
1092
+ path = u.origin === window.location.origin ? u.pathname : `${u.host}${u.pathname}`;
1093
+ } catch (e2) {
1094
+ }
1095
+ return `${path} ${Math.round(e.responseEnd)}ms`;
1096
+ });
1097
+ if (fetches.length > 0) parts.push(`fetches finished before it: ${fetches.join(", ")}`);
1098
+ else parts.push("no fetch had finished yet");
1099
+ return parts.join("; ");
1100
+ } catch (e) {
1101
+ return "";
1102
+ }
1103
+ }
1104
+ function hydrationMetadata(error, extra) {
1105
+ var _a, _b, _c;
1106
+ const code = (_a = hydrationCode(messageOf(error))) != null ? _a : hydrationCode(messageOf(error == null ? void 0 : error.cause));
1107
+ const meta = { errorKind: "hydration" };
1108
+ if (code) meta.hydrationCode = code;
1109
+ const cause = error == null ? void 0 : error.cause;
1110
+ if (cause && cause !== error && messageOf(cause)) {
1111
+ meta.hydrationCause = messageOf(cause).slice(0, 500);
1112
+ }
1113
+ if ((_b = extra == null ? void 0 : extra.removed) == null ? void 0 : _b.html) meta.hydrationServerHtml = extra.removed.html;
1114
+ if ((_c = extra == null ? void 0 : extra.removed) == null ? void 0 : _c.where) meta.hydrationReplacedIn = extra.removed.where;
1115
+ const foreign = detectForeignNodes();
1116
+ if (foreign.length > 0) meta.hydrationForeignNodes = foreign.join("\n");
1117
+ const timing = hydrationTiming();
1118
+ if (timing) meta.hydrationTiming = timing;
1119
+ return meta;
1120
+ }
1121
+ var handled = /* @__PURE__ */ new WeakSet();
1122
+ function isHandledHydrationError(error) {
1123
+ return !!error && typeof error === "object" && handled.has(error);
1124
+ }
1125
+ function markHandled(error) {
1126
+ if (error && typeof error === "object") {
1127
+ handled.add(error);
1128
+ const cause = error.cause;
1129
+ if (cause && typeof cause === "object") handled.add(cause);
1130
+ }
1131
+ }
1132
+ var reporter = null;
1133
+ var PATCHED = "__glitchgrabHydrationPatched";
1134
+ function findFiberRoot(container) {
1135
+ if (!container || typeof container !== "object") return null;
1136
+ try {
1137
+ const key = Object.keys(container).find((k) => k.startsWith("__reactContainer$"));
1138
+ if (!key) return null;
1139
+ const hostRoot = container[key];
1140
+ const root = hostRoot == null ? void 0 : hostRoot.stateNode;
1141
+ return root && typeof root === "object" ? root : null;
1142
+ } catch (e) {
1143
+ return null;
1144
+ }
1145
+ }
1146
+ function patchRoot(root) {
1147
+ if (root[PATCHED]) return true;
1148
+ const original = root.onRecoverableError;
1149
+ if (typeof original !== "function") return false;
1150
+ root.onRecoverableError = function glitchgrabOnRecoverableError(error, info) {
1151
+ var _a;
1152
+ try {
1153
+ if (reporter && isHydrationError(error) && !isBailout(error)) {
1154
+ const removed = takeRemovedServerHtml();
1155
+ markHandled(error);
1156
+ reporter(error, (_a = info == null ? void 0 : info.componentStack) != null ? _a : "", hydrationMetadata(error, { removed }));
1157
+ }
1158
+ } catch (e) {
1159
+ }
1160
+ return original.call(this, error, info);
1161
+ };
1162
+ root[PATCHED] = true;
1163
+ return true;
1164
+ }
1165
+ function patchKnownRoots() {
1166
+ if (typeof document === "undefined") return false;
1167
+ let patched = false;
1168
+ for (const container of [document, document.getElementById("__next"), document.getElementById("root")]) {
1169
+ const root = findFiberRoot(container);
1170
+ if (root && patchRoot(root)) patched = true;
1171
+ }
1172
+ return patched;
1173
+ }
1174
+ function installHydrationCapture(report) {
1175
+ try {
1176
+ if (typeof window === "undefined") return;
1177
+ reporter = report;
1178
+ startObserver();
1179
+ patchKnownRoots();
1180
+ } catch (e) {
1181
+ }
1182
+ }
1183
+
865
1184
  // src/capture.ts
866
1185
  var config = null;
867
1186
  function registerCaptureConfig(next) {
@@ -881,7 +1200,7 @@ function sessionMetadata(session) {
881
1200
  ...(session == null ? void 0 : session.phone) ? { sessionUserPhone: String(session.phone) } : {}
882
1201
  };
883
1202
  }
884
- function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureError") {
1203
+ function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureError", dedupKey) {
885
1204
  var _a, _b, _c;
886
1205
  const err = error instanceof Error ? error : void 0;
887
1206
  const errorMessage = err ? err.message : String(error);
@@ -893,7 +1212,7 @@ function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureErr
893
1212
  const sig = computeSignature({
894
1213
  errorMessage,
895
1214
  pageUrl: context.url,
896
- errorStack: err == null ? void 0 : err.stack,
1215
+ errorStack: dedupKey ? `at ${dedupKey}` : err == null ? void 0 : err.stack,
897
1216
  digest
898
1217
  });
899
1218
  if (shouldSkipDuplicate(sig)) return null;
@@ -933,6 +1252,25 @@ function captureError(error, options) {
933
1252
  } catch (e) {
934
1253
  }
935
1254
  }
1255
+ function reportHydrationError(error, componentStack, metadata) {
1256
+ try {
1257
+ const cfg = config;
1258
+ if (!(cfg == null ? void 0 : cfg.token)) return;
1259
+ const payload = buildCapturedErrorPayload(
1260
+ error,
1261
+ { componentStack: componentStack || void 0, metadata },
1262
+ cfg,
1263
+ "onRecoverableError",
1264
+ componentStackKey(componentStack) || void 0
1265
+ );
1266
+ if (!payload) return;
1267
+ void sendReport(payload, cfg.baseUrl).then((result) => {
1268
+ if (result && cfg.onReportSent) cfg.onReportSent(result);
1269
+ });
1270
+ if (cfg.onError && error instanceof Error) cfg.onError(error);
1271
+ } catch (e) {
1272
+ }
1273
+ }
936
1274
 
937
1275
  // src/error-boundary.tsx
938
1276
  var GlitchgrabErrorBoundary = class extends React.Component {
@@ -1355,6 +1693,9 @@ var MAX_ASSIST_IMAGE_CHARS = 35e5;
1355
1693
  var MAX_ASSIST_FILES = 3;
1356
1694
  var MAX_ASSIST_FILE_CHARS = 3e5;
1357
1695
  var MAX_ASSIST_FILE_CHARS_EACH = 2e5;
1696
+ var MAX_SIMILAR_FILE_CHARS = 2e4;
1697
+ var MAX_SIMILAR_FILES_TOTAL_CHARS = 4e4;
1698
+ var MAX_READABLE_PDF_BYTES = 3 * 1024 * 1024;
1358
1699
  var TEXT_ATTACHMENT_EXTENSIONS = [
1359
1700
  ".html",
1360
1701
  ".htm",
@@ -1385,6 +1726,10 @@ var ATTACHMENT_ACCEPT = [
1385
1726
  ...BINARY_ATTACHMENT_EXTENSIONS
1386
1727
  ].join(",");
1387
1728
  var ASSIST_ATTACHMENT_ACCEPT = ["image/*", ...TEXT_ATTACHMENT_EXTENSIONS].join(",");
1729
+ var ASSIST_ATTACHMENT_ACCEPT_WITH_PDF = [ASSIST_ATTACHMENT_ACCEPT, ".pdf"].join(",");
1730
+ function isPdfFile(name) {
1731
+ return name.toLowerCase().endsWith(".pdf");
1732
+ }
1388
1733
  function isAssistReadableFile(name) {
1389
1734
  const lower = name.toLowerCase();
1390
1735
  return TEXT_ATTACHMENT_EXTENSIONS.some((ext) => lower.endsWith(ext));
@@ -1405,6 +1750,27 @@ function decodeTextDataUrl(dataUrl) {
1405
1750
  return null;
1406
1751
  }
1407
1752
  }
1753
+ function heldFileKey(file) {
1754
+ return `${file.name}:${file.size}`;
1755
+ }
1756
+ function heldFileText(file) {
1757
+ if (isPdfFile(file.name)) return typeof file.text === "string" && file.text.trim() ? file.text : null;
1758
+ if (!isAssistReadableFile(file.name)) return null;
1759
+ const text = decodeTextDataUrl(file.dataUrl);
1760
+ return text && text.trim() ? text : null;
1761
+ }
1762
+ function filesForSimilar(files, each, total) {
1763
+ const picked = [];
1764
+ let used = 0;
1765
+ for (let i = files.length - 1; i >= 0 && used < total; i--) {
1766
+ const text = heldFileText(files[i]);
1767
+ if (!text) continue;
1768
+ const content = text.slice(0, Math.min(each, total - used));
1769
+ picked.unshift({ name: files[i].name, content });
1770
+ used += content.length;
1771
+ }
1772
+ return picked;
1773
+ }
1408
1774
  function getTypeLabel(type) {
1409
1775
  switch (type) {
1410
1776
  case "RATING":
@@ -1831,6 +2197,79 @@ function renderBlocks(lines, theme, key) {
1831
2197
  function ChatMarkdown({ text, theme }) {
1832
2198
  return /* @__PURE__ */ jsx2("div", { style: STACK, children: renderBlocks(text.replace(/\r\n?/g, "\n").split("\n"), theme, "md") });
1833
2199
  }
2200
+ var MAX_SAVED_CHATS = 20;
2201
+ var SAVED_CHAT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
2202
+ var TITLE_CHARS = 80;
2203
+ var PREFIX = "glitchgrab:ai-chats:";
2204
+ function chatHistoryKey(...parts) {
2205
+ const input = parts.map((p) => p != null ? p : "").join("\0");
2206
+ let hash = 2166136261;
2207
+ for (let i = 0; i < input.length; i++) {
2208
+ hash ^= input.charCodeAt(i);
2209
+ hash = Math.imul(hash, 16777619);
2210
+ }
2211
+ return (hash >>> 0).toString(36);
2212
+ }
2213
+ function chatTitle(messages) {
2214
+ var _a, _b;
2215
+ const first = (_b = (_a = messages.find((m) => m.role === "user")) == null ? void 0 : _a.content.trim()) != null ? _b : "";
2216
+ const line = first.replace(/\s+/g, " ");
2217
+ return line.length > TITLE_CHARS ? `${line.slice(0, TITLE_CHARS - 1)}\u2026` : line;
2218
+ }
2219
+ function storage() {
2220
+ try {
2221
+ return typeof window !== "undefined" && window.localStorage ? window.localStorage : null;
2222
+ } catch (e) {
2223
+ return null;
2224
+ }
2225
+ }
2226
+ function isSavedChat(value) {
2227
+ if (!value || typeof value !== "object") return false;
2228
+ const c = value;
2229
+ return typeof c.id === "string" && typeof c.title === "string" && typeof c.updatedAt === "number" && (c.status === "open" || c.status === "filed") && Array.isArray(c.messages);
2230
+ }
2231
+ function loadChats(key, now = Date.now()) {
2232
+ const store = storage();
2233
+ if (!store) return [];
2234
+ try {
2235
+ const raw = store.getItem(PREFIX + key);
2236
+ if (!raw) return [];
2237
+ const parsed = JSON.parse(raw);
2238
+ if (!Array.isArray(parsed)) return [];
2239
+ return parsed.filter(isSavedChat).filter((c) => now - c.updatedAt < SAVED_CHAT_MAX_AGE_MS).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_SAVED_CHATS);
2240
+ } catch (e) {
2241
+ return [];
2242
+ }
2243
+ }
2244
+ function write(key, chats) {
2245
+ const store = storage();
2246
+ if (!store) return;
2247
+ try {
2248
+ if (chats.length === 0) store.removeItem(PREFIX + key);
2249
+ else store.setItem(PREFIX + key, JSON.stringify(chats));
2250
+ } catch (e) {
2251
+ }
2252
+ }
2253
+ function saveChat(key, chat, now = Date.now()) {
2254
+ const rest = loadChats(key, now).filter((c) => c.id !== chat.id);
2255
+ const next = [chat, ...rest].slice(0, MAX_SAVED_CHATS);
2256
+ write(key, next);
2257
+ return next;
2258
+ }
2259
+ function markChatFiled(key, conversationId, label, now = Date.now()) {
2260
+ const chats = loadChats(key, now);
2261
+ const hit = chats.find((c) => c.conversationId === conversationId);
2262
+ if (!hit || hit.status === "filed") return;
2263
+ write(
2264
+ key,
2265
+ chats.map((c) => c === hit ? { ...c, status: "filed", filedLabel: label, updatedAt: now } : c)
2266
+ );
2267
+ }
2268
+ function removeChat(key, id, now = Date.now()) {
2269
+ const next = loadChats(key, now).filter((c) => c.id !== id);
2270
+ write(key, next);
2271
+ return next;
2272
+ }
1834
2273
  function pickAssistImages(all) {
1835
2274
  const picked = [];
1836
2275
  let total = 0;
@@ -1848,9 +2287,8 @@ function pickAssistFiles(files) {
1848
2287
  let total = 0;
1849
2288
  for (let i = files.length - 1; i >= 0 && picked.length < MAX_ASSIST_FILES; i--) {
1850
2289
  const attached = files[i];
1851
- if (!isAssistReadableFile(attached.name)) continue;
1852
- const text = decodeTextDataUrl(attached.dataUrl);
1853
- if (!text || !text.trim()) continue;
2290
+ const text = heldFileText(attached);
2291
+ if (!text) continue;
1854
2292
  const content = text.slice(0, MAX_ASSIST_FILE_CHARS_EACH);
1855
2293
  if (total + content.length > MAX_ASSIST_FILE_CHARS) continue;
1856
2294
  picked.unshift({ index: i, file: { name: attached.name, content } });
@@ -1858,6 +2296,18 @@ function pickAssistFiles(files) {
1858
2296
  }
1859
2297
  return picked;
1860
2298
  }
2299
+ function newChatId() {
2300
+ return `local-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
2301
+ }
2302
+ function chatAge(at2, now = Date.now()) {
2303
+ const min = Math.round((now - at2) / 6e4);
2304
+ if (min < 1) return "just now";
2305
+ if (min < 60) return `${min} min ago`;
2306
+ const hours = Math.round(min / 60);
2307
+ if (hours < 24) return `${hours} h ago`;
2308
+ if (hours < 48) return "yesterday";
2309
+ return new Date(at2).toLocaleDateString(void 0, { day: "numeric", month: "short" });
2310
+ }
1861
2311
  var STARTERS = [
1862
2312
  "Something on this page is broken",
1863
2313
  "It did the wrong thing",
@@ -1940,6 +2390,33 @@ function Avatar({
1940
2390
  }
1941
2391
  );
1942
2392
  }
2393
+ var THINKING_SLOW_SECONDS = 20;
2394
+ function ThinkingClock({ color }) {
2395
+ const [seconds, setSeconds] = useState2(0);
2396
+ useEffect2(() => {
2397
+ const started = Date.now();
2398
+ const id = setInterval(() => setSeconds(Math.floor((Date.now() - started) / 1e3)), 1e3);
2399
+ return () => clearInterval(id);
2400
+ }, []);
2401
+ return (
2402
+ // Hidden from screen readers: the bubble already says "Thinking", and a
2403
+ // number changing every second would be read out every second.
2404
+ /* @__PURE__ */ jsxs3(
2405
+ "span",
2406
+ {
2407
+ "data-gg-thinking-clock": "",
2408
+ "aria-hidden": "true",
2409
+ style: { marginLeft: "6px", fontSize: "12px", color, fontVariantNumeric: "tabular-nums" },
2410
+ children: [
2411
+ "Thinking\u2026 ",
2412
+ seconds,
2413
+ "s",
2414
+ seconds >= THINKING_SLOW_SECONDS ? " \xB7 taking longer than usual, still working" : ""
2415
+ ]
2416
+ }
2417
+ )
2418
+ );
2419
+ }
1943
2420
  function AssistSheet({
1944
2421
  assist,
1945
2422
  theme: t,
@@ -1947,6 +2424,7 @@ function AssistSheet({
1947
2424
  pageShot = null,
1948
2425
  onAddFiles,
1949
2426
  files = NO_FILES,
2427
+ canReadPdf = false,
1950
2428
  onRemoveFile,
1951
2429
  onRemoveImage,
1952
2430
  attachmentCount,
@@ -1974,17 +2452,27 @@ function AssistSheet({
1974
2452
  isSubmitting,
1975
2453
  submitted,
1976
2454
  onSend,
2455
+ similarSlot,
2456
+ submittedText,
1977
2457
  onDegrade,
1978
2458
  onClose,
1979
2459
  onFinish,
1980
- onReportGlitchgrabProblem
2460
+ onReportGlitchgrabProblem,
2461
+ historyKey = null
1981
2462
  }) {
2463
+ var _a;
1982
2464
  const narrow = useIsNarrow();
1983
2465
  const severityAsked = showSeverity && currentTile === "BUG";
1984
2466
  const [messages, setMessages] = useState2([]);
1985
2467
  const [input, setInput] = useState2("");
1986
2468
  const [busy, setBusy] = useState2(false);
1987
2469
  const [conversationId, setConversationId] = useState2(null);
2470
+ const chatIdRef = useRef2(newChatId());
2471
+ const [savedChats, setSavedChats] = useState2(
2472
+ () => historyKey ? loadChats(historyKey) : []
2473
+ );
2474
+ const [showChats, setShowChats] = useState2(false);
2475
+ const [viewingChat, setViewingChat] = useState2(null);
1988
2476
  const [hoveredStar, setHoveredStar] = useState2(0);
1989
2477
  const [options, setOptions] = useState2([]);
1990
2478
  const [duplicate, setDuplicate] = useState2(null);
@@ -1997,6 +2485,7 @@ function AssistSheet({
1997
2485
  const [autoFileLeft, setAutoFileLeft] = useState2(null);
1998
2486
  const scrollRef = useRef2(null);
1999
2487
  const draftRef = useRef2(null);
2488
+ const composerRef = useRef2(null);
2000
2489
  const seededRef = useRef2(false);
2001
2490
  const seedPendingRef = useRef2(false);
2002
2491
  const eventsRef = useRef2([]);
@@ -2035,9 +2524,15 @@ function AssistSheet({
2035
2524
  if (el) el.scrollTop = el.scrollHeight;
2036
2525
  }, [messages, busy, phase]);
2037
2526
  useEffect2(() => {
2038
- var _a;
2039
- if (phase === "draft") (_a = draftRef.current) == null ? void 0 : _a.focus();
2527
+ var _a2;
2528
+ if (phase === "draft") (_a2 = draftRef.current) == null ? void 0 : _a2.focus();
2040
2529
  }, [phase]);
2530
+ useEffect2(() => {
2531
+ const el = composerRef.current;
2532
+ if (!el) return;
2533
+ el.style.height = "auto";
2534
+ el.style.height = `${el.scrollHeight + 2}px`;
2535
+ }, [input]);
2041
2536
  useEffect2(() => {
2042
2537
  if (autoFileLeft === null) return;
2043
2538
  if (isSubmitting || submitted || phase !== "draft") {
@@ -2062,7 +2557,8 @@ function AssistSheet({
2062
2557
  onDegrade(message, retryable);
2063
2558
  }
2064
2559
  async function runTurn(history2, autoFile = false) {
2065
- var _a, _b, _c;
2560
+ var _a2, _b, _c;
2561
+ const turnChat = chatIdRef.current;
2066
2562
  setBusy(true);
2067
2563
  setAutoFileLeft(null);
2068
2564
  let result;
@@ -2099,6 +2595,7 @@ function AssistSheet({
2099
2595
  } finally {
2100
2596
  setBusy(false);
2101
2597
  }
2598
+ if (chatIdRef.current !== turnChat) return;
2102
2599
  if (result.degraded) {
2103
2600
  degradeKeepingEvents(events, result.degraded, result.retryable === true);
2104
2601
  return;
@@ -2109,7 +2606,7 @@ function AssistSheet({
2109
2606
  }
2110
2607
  const attach = result.duplicate ? { ...result.duplicate, kind: "duplicate" } : result.related ? { ...result.related, kind: "related" } : null;
2111
2608
  setDuplicate(attach);
2112
- onDuplicateChange == null ? void 0 : onDuplicateChange((_a = attach == null ? void 0 : attach.number) != null ? _a : null, attach == null ? void 0 : attach.kind);
2609
+ onDuplicateChange == null ? void 0 : onDuplicateChange((_a2 = attach == null ? void 0 : attach.number) != null ? _a2 : null, attach == null ? void 0 : attach.kind);
2113
2610
  setGlitchgrabOffer(!!result.aboutGlitchgrab && !!onReportGlitchgrabProblem && !!result.question);
2114
2611
  if (result.solved) {
2115
2612
  setSolved(result.solved);
@@ -2163,9 +2660,9 @@ function AssistSheet({
2163
2660
  }
2164
2661
  }
2165
2662
  function sendTyped(text) {
2166
- var _a;
2663
+ var _a2;
2167
2664
  const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
2168
- const checkingBack = options.includes(CONFIRM_OPTION) || (lastAssistant == null ? void 0 : lastAssistant.kind) !== "report" && isCheckBackQuestion((_a = lastAssistant == null ? void 0 : lastAssistant.content) != null ? _a : "");
2665
+ const checkingBack = options.includes(CONFIRM_OPTION) || (lastAssistant == null ? void 0 : lastAssistant.kind) !== "report" && isCheckBackQuestion((_a2 = lastAssistant == null ? void 0 : lastAssistant.content) != null ? _a2 : "");
2169
2666
  send(text, checkingBack && isAffirmativeReply(text));
2170
2667
  }
2171
2668
  function send(text, autoFile = false) {
@@ -2207,7 +2704,215 @@ function AssistSheet({
2207
2704
  flushEvents();
2208
2705
  onSend();
2209
2706
  }
2707
+ useEffect2(() => {
2708
+ if (!historyKey || !messages.some((m) => m.role === "user")) return;
2709
+ const done = submitted || phase === "solved";
2710
+ const kept = submitted || phase === "draft" ? withCurrentDraft(messages) : messages;
2711
+ setSavedChats(
2712
+ saveChat(historyKey, {
2713
+ id: chatIdRef.current,
2714
+ conversationId,
2715
+ title: chatTitle(kept),
2716
+ tile: currentTile,
2717
+ messages: kept,
2718
+ ...issueTitle ? { issueTitle } : {},
2719
+ status: done ? "filed" : "open",
2720
+ ...done ? {
2721
+ filedLabel: submitted ? submittedText != null ? submittedText : `${reportTypeLabel} sent` : "Answered \u2014 nothing was filed"
2722
+ } : {},
2723
+ updatedAt: Date.now()
2724
+ })
2725
+ );
2726
+ }, [
2727
+ historyKey,
2728
+ messages,
2729
+ conversationId,
2730
+ submitted,
2731
+ phase === "solved",
2732
+ issueTitle,
2733
+ phase === "draft" ? description : null
2734
+ ]);
2735
+ function resumeChat(chat) {
2736
+ var _a2;
2737
+ flushEvents();
2738
+ chatIdRef.current = chat.id;
2739
+ setMessages(chat.messages);
2740
+ setConversationId(chat.conversationId);
2741
+ onConversationChange == null ? void 0 : onConversationChange(chat.conversationId);
2742
+ setOptions([]);
2743
+ setDuplicate(null);
2744
+ onDuplicateChange == null ? void 0 : onDuplicateChange(null);
2745
+ setGlitchgrabOffer(false);
2746
+ setAutoFileLeft(null);
2747
+ setInput("");
2748
+ if (chat.tile !== currentTile && tiles.includes(chat.tile)) {
2749
+ onPickType(chat.tile);
2750
+ }
2751
+ const last = chat.messages[chat.messages.length - 1];
2752
+ if ((last == null ? void 0 : last.kind) === "report") {
2753
+ onDescriptionChange(last.content);
2754
+ onIssueTitleChange((_a2 = chat.issueTitle) != null ? _a2 : "");
2755
+ aiDraftRef.current = last.content;
2756
+ setPhase("draft");
2757
+ } else {
2758
+ setPhase("chat");
2759
+ }
2760
+ setShowChats(false);
2761
+ setViewingChat(null);
2762
+ }
2763
+ function startNewChat() {
2764
+ flushEvents();
2765
+ chatIdRef.current = newChatId();
2766
+ setMessages([]);
2767
+ setConversationId(null);
2768
+ onConversationChange == null ? void 0 : onConversationChange(null);
2769
+ setOptions([]);
2770
+ setDuplicate(null);
2771
+ onDuplicateChange == null ? void 0 : onDuplicateChange(null);
2772
+ setGlitchgrabOffer(false);
2773
+ setAutoFileLeft(null);
2774
+ setInput("");
2775
+ onDescriptionChange("");
2776
+ onIssueTitleChange("");
2777
+ aiDraftRef.current = null;
2778
+ setPhase("chat");
2779
+ setShowChats(false);
2780
+ setViewingChat(null);
2781
+ }
2782
+ function forgetChat(id) {
2783
+ if (!historyKey) return;
2784
+ setSavedChats(removeChat(historyKey, id));
2785
+ if (viewingChat === id) setViewingChat(null);
2786
+ }
2787
+ const otherChats = savedChats.filter((c) => c.id !== chatIdRef.current);
2788
+ const resumable = (_a = otherChats.find((c) => c.status === "open" && c.messages.length > 0)) != null ? _a : null;
2210
2789
  const empty = phase === "chat" && messages.length === 0 && !busy;
2790
+ function renderChatList() {
2791
+ return /* @__PURE__ */ jsxs3("div", { "data-gg-chat-list": "", style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [
2792
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
2793
+ /* @__PURE__ */ jsx3("span", { style: { fontSize: "13px", fontWeight: 600 }, children: "Your chats" }),
2794
+ messages.length > 0 && /* @__PURE__ */ jsx3(
2795
+ "button",
2796
+ {
2797
+ type: "button",
2798
+ "data-gg-new-chat": "",
2799
+ onClick: startNewChat,
2800
+ style: {
2801
+ border: `1px solid ${t.inputBorder}`,
2802
+ borderRadius: "999px",
2803
+ background: "transparent",
2804
+ color: t.text,
2805
+ padding: "7px 12px",
2806
+ fontSize: "12px",
2807
+ fontFamily: "inherit",
2808
+ cursor: "pointer"
2809
+ },
2810
+ children: "+ New chat"
2811
+ }
2812
+ )
2813
+ ] }),
2814
+ /* @__PURE__ */ jsx3("span", { style: { fontSize: "11.5px", color: t.textMuted, lineHeight: 1.5 }, children: "Kept in this browser only, for 30 days." }),
2815
+ otherChats.length === 0 && /* @__PURE__ */ jsx3("span", { style: { fontSize: "12.5px", color: t.textMuted, padding: "12px 0" }, children: "No other chats yet." }),
2816
+ otherChats.map((chat) => {
2817
+ var _a2;
2818
+ const filed = chat.status === "filed";
2819
+ const open = viewingChat === chat.id;
2820
+ return /* @__PURE__ */ jsxs3(
2821
+ "div",
2822
+ {
2823
+ "data-gg-saved-chat": chat.status,
2824
+ style: {
2825
+ border: `1px solid ${t.border}`,
2826
+ borderRadius: "10px",
2827
+ padding: "10px 12px",
2828
+ display: "flex",
2829
+ flexDirection: "column",
2830
+ gap: "6px",
2831
+ minWidth: 0
2832
+ },
2833
+ children: [
2834
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px", alignItems: "flex-start", minWidth: 0 }, children: [
2835
+ /* @__PURE__ */ jsxs3(
2836
+ "button",
2837
+ {
2838
+ type: "button",
2839
+ onClick: () => filed ? setViewingChat(open ? null : chat.id) : resumeChat(chat),
2840
+ style: {
2841
+ flex: 1,
2842
+ minWidth: 0,
2843
+ textAlign: "left",
2844
+ border: "none",
2845
+ background: "transparent",
2846
+ color: t.text,
2847
+ padding: 0,
2848
+ fontFamily: "inherit",
2849
+ cursor: "pointer",
2850
+ display: "flex",
2851
+ flexDirection: "column",
2852
+ gap: "4px"
2853
+ },
2854
+ children: [
2855
+ /* @__PURE__ */ jsx3("span", { style: { fontSize: "13px", lineHeight: 1.45, overflowWrap: "anywhere" }, children: chat.title || "Untitled chat" }),
2856
+ /* @__PURE__ */ jsxs3("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
2857
+ filed ? (_a2 = chat.filedLabel) != null ? _a2 : "Sent" : "Not sent yet \u2014 tap to continue",
2858
+ " \xB7",
2859
+ " ",
2860
+ chatAge(chat.updatedAt)
2861
+ ] })
2862
+ ]
2863
+ }
2864
+ ),
2865
+ /* @__PURE__ */ jsx3(
2866
+ "button",
2867
+ {
2868
+ type: "button",
2869
+ "aria-label": `Delete chat: ${chat.title}`,
2870
+ onClick: () => forgetChat(chat.id),
2871
+ style: {
2872
+ flexShrink: 0,
2873
+ border: "none",
2874
+ background: "transparent",
2875
+ color: t.textMuted,
2876
+ fontSize: "16px",
2877
+ lineHeight: 1,
2878
+ padding: "4px 6px",
2879
+ cursor: "pointer",
2880
+ fontFamily: "inherit"
2881
+ },
2882
+ children: "\xD7"
2883
+ }
2884
+ )
2885
+ ] }),
2886
+ open && /* @__PURE__ */ jsx3(
2887
+ "div",
2888
+ {
2889
+ "data-gg-saved-transcript": "",
2890
+ style: {
2891
+ display: "flex",
2892
+ flexDirection: "column",
2893
+ gap: "6px",
2894
+ borderTop: `1px solid ${t.border}`,
2895
+ paddingTop: "8px",
2896
+ fontSize: "12.5px",
2897
+ lineHeight: 1.5
2898
+ },
2899
+ children: chat.messages.map((m, i) => /* @__PURE__ */ jsxs3("div", { style: { minWidth: 0, overflowWrap: "anywhere" }, children: [
2900
+ /* @__PURE__ */ jsxs3("span", { style: { color: t.textMuted, fontWeight: 600 }, children: [
2901
+ m.role === "user" ? "You" : m.kind === "report" ? "Report" : "Assistant",
2902
+ ":",
2903
+ " "
2904
+ ] }),
2905
+ /* @__PURE__ */ jsx3("span", { style: { whiteSpace: "pre-wrap" }, children: m.content })
2906
+ ] }, i))
2907
+ }
2908
+ )
2909
+ ]
2910
+ },
2911
+ chat.id
2912
+ );
2913
+ })
2914
+ ] });
2915
+ }
2211
2916
  function renderKnownIssue({ title, footer }) {
2212
2917
  if (!duplicate) return null;
2213
2918
  return /* @__PURE__ */ jsxs3(
@@ -2241,8 +2946,8 @@ function AssistSheet({
2241
2946
  );
2242
2947
  }
2243
2948
  const initials2 = (reporterName != null ? reporterName : "").split(/\s+/).filter(Boolean).slice(0, 2).map((w) => {
2244
- var _a, _b;
2245
- return (_b = (_a = w[0]) == null ? void 0 : _a.toUpperCase()) != null ? _b : "";
2949
+ var _a2, _b;
2950
+ return (_b = (_a2 = w[0]) == null ? void 0 : _a2.toUpperCase()) != null ? _b : "";
2246
2951
  }).join("");
2247
2952
  const panelStyle = narrow ? {
2248
2953
  width: "100%",
@@ -2264,6 +2969,7 @@ function AssistSheet({
2264
2969
  @keyframes gg-sheet-up{from{transform:translateY(24px);opacity:0}to{transform:translateY(0);opacity:1}}
2265
2970
  @keyframes gg-sheet-fade{from{opacity:0}to{opacity:1}}
2266
2971
  @keyframes gg-autofile{from{transform:scaleX(0)}to{transform:scaleX(1)}}
2972
+ @keyframes gg-spin{to{transform:rotate(360deg)}}
2267
2973
  @keyframes gg-dot{0%,80%,100%{transform:translateY(0);opacity:.4}40%{transform:translateY(-3px);opacity:1}}
2268
2974
  [data-gg-sheet-send]:focus-visible,[data-gg-chip]:focus-visible,[data-gg-assist-remove]:focus-visible,[data-gg-stop-auto-file]:focus-visible{outline:2px solid currentColor;outline-offset:2px}
2269
2975
  ` }),
@@ -2332,7 +3038,9 @@ function AssistSheet({
2332
3038
  alignItems: "center",
2333
3039
  gap: "7px",
2334
3040
  fontSize: "14px",
2335
- fontWeight: 600
3041
+ fontWeight: 600,
3042
+ // One line even with the Chats pill beside it (#567).
3043
+ whiteSpace: "nowrap"
2336
3044
  },
2337
3045
  children: [
2338
3046
  /* @__PURE__ */ jsx3(Sparkle, { color: t.accent }),
@@ -2362,34 +3070,64 @@ function AssistSheet({
2362
3070
  projectSlot ? /* @__PURE__ */ jsx3("span", { style: { marginLeft: "6px" }, children: projectSlot }) : null
2363
3071
  ] })
2364
3072
  ] }),
2365
- /* @__PURE__ */ jsx3(
2366
- "button",
2367
- {
2368
- type: "button",
2369
- "data-gg-write-myself": "",
2370
- onClick: () => leave("write_myself", `header, ${phase}`),
2371
- "aria-label": "Write it myself instead",
2372
- style: {
2373
- flexShrink: 0,
2374
- border: `1px solid ${t.inputBorder}`,
2375
- borderRadius: "999px",
2376
- background: "transparent",
2377
- color: t.textMuted,
2378
- cursor: "pointer",
2379
- fontSize: "11.5px",
2380
- lineHeight: 1,
2381
- fontFamily: "inherit",
2382
- whiteSpace: "nowrap",
2383
- // ~32px tall either way — this is the escape hatch, not a hint.
2384
- padding: "9px 11px"
2385
- },
2386
- children: "Write it myself"
2387
- }
2388
- )
3073
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "6px", flexShrink: 0 }, children: [
3074
+ historyKey && (showChats || otherChats.length > 0) && /* @__PURE__ */ jsx3(
3075
+ "button",
3076
+ {
3077
+ type: "button",
3078
+ "data-gg-chats-toggle": "",
3079
+ disabled: busy,
3080
+ "aria-pressed": showChats,
3081
+ onClick: () => {
3082
+ setShowChats((v) => !v);
3083
+ setViewingChat(null);
3084
+ },
3085
+ style: {
3086
+ flexShrink: 0,
3087
+ border: `1px solid ${showChats ? t.accent : t.inputBorder}`,
3088
+ borderRadius: "999px",
3089
+ background: "transparent",
3090
+ color: showChats ? t.accent : t.textMuted,
3091
+ cursor: busy ? "default" : "pointer",
3092
+ opacity: busy ? 0.5 : 1,
3093
+ fontSize: "11.5px",
3094
+ lineHeight: 1,
3095
+ fontFamily: "inherit",
3096
+ whiteSpace: "nowrap",
3097
+ padding: "9px 11px"
3098
+ },
3099
+ children: showChats ? "\u2190 Chat" : `Chats \xB7 ${otherChats.length}`
3100
+ }
3101
+ ),
3102
+ /* @__PURE__ */ jsx3(
3103
+ "button",
3104
+ {
3105
+ type: "button",
3106
+ "data-gg-write-myself": "",
3107
+ onClick: () => leave("write_myself", `header, ${phase}`),
3108
+ "aria-label": "Write it myself instead",
3109
+ style: {
3110
+ flexShrink: 0,
3111
+ border: `1px solid ${t.inputBorder}`,
3112
+ borderRadius: "999px",
3113
+ background: "transparent",
3114
+ color: t.textMuted,
3115
+ cursor: "pointer",
3116
+ fontSize: "11.5px",
3117
+ lineHeight: 1,
3118
+ fontFamily: "inherit",
3119
+ whiteSpace: "nowrap",
3120
+ // ~32px tall either way — this is the escape hatch, not a hint.
3121
+ padding: "9px 11px"
3122
+ },
3123
+ children: "Write it myself"
3124
+ }
3125
+ )
3126
+ ] })
2389
3127
  ]
2390
3128
  }
2391
3129
  ),
2392
- /* @__PURE__ */ jsx3(
3130
+ /* @__PURE__ */ jsxs3(
2393
3131
  "div",
2394
3132
  {
2395
3133
  ref: scrollRef,
@@ -2401,765 +3139,845 @@ function AssistSheet({
2401
3139
  display: "flex",
2402
3140
  flexDirection: "column"
2403
3141
  },
2404
- children: /* @__PURE__ */ jsxs3(
2405
- "div",
2406
- {
2407
- style: {
2408
- marginTop: "auto",
2409
- display: "flex",
2410
- flexDirection: "column",
2411
- gap: "12px",
2412
- minWidth: 0
2413
- },
2414
- children: [
2415
- phase === "type" && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2416
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
2417
- /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
2418
- /* @__PURE__ */ jsxs3(
3142
+ children: [
3143
+ showChats && renderChatList(),
3144
+ /* @__PURE__ */ jsx3(
3145
+ "div",
3146
+ {
3147
+ style: {
3148
+ marginTop: "auto",
3149
+ display: "flex",
3150
+ flexDirection: "column",
3151
+ gap: "12px",
3152
+ minWidth: 0
3153
+ },
3154
+ children: /* @__PURE__ */ jsxs3("div", { style: { display: showChats ? "none" : "contents" }, children: [
3155
+ phase === "type" && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3156
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
3157
+ /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
3158
+ /* @__PURE__ */ jsxs3(
3159
+ "div",
3160
+ {
3161
+ style: {
3162
+ padding: "9px 12px",
3163
+ borderRadius: "12px 12px 12px 4px",
3164
+ backgroundColor: t.bgSecondary,
3165
+ fontSize: "13px",
3166
+ lineHeight: 1.55,
3167
+ maxWidth: "88%"
3168
+ },
3169
+ children: [
3170
+ reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 w` : "W",
3171
+ "hat do you want to do? Pick one and I'll take it from there."
3172
+ ]
3173
+ }
3174
+ )
3175
+ ] }),
3176
+ /* @__PURE__ */ jsx3(
2419
3177
  "div",
2420
3178
  {
2421
3179
  style: {
2422
- padding: "9px 12px",
2423
- borderRadius: "12px 12px 12px 4px",
2424
- backgroundColor: t.bgSecondary,
2425
- fontSize: "13px",
2426
- lineHeight: 1.55,
2427
- maxWidth: "88%"
3180
+ display: "flex",
3181
+ flexWrap: "wrap",
3182
+ gap: "6px",
3183
+ paddingLeft: "32px"
2428
3184
  },
2429
- children: [
2430
- reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 w` : "W",
2431
- "hat do you want to do? Pick one and I'll take it from there."
2432
- ]
3185
+ children: tiles.map((tile) => {
3186
+ const rate = tile === "RATING";
3187
+ return /* @__PURE__ */ jsx3(
3188
+ "button",
3189
+ {
3190
+ type: "button",
3191
+ "data-gg-chip": "",
3192
+ "data-gg-tile": tile,
3193
+ onClick: () => pickType(tile),
3194
+ style: {
3195
+ display: "inline-flex",
3196
+ alignItems: "center",
3197
+ gap: "5px",
3198
+ // The rating is the only tile that isn't a report, so
3199
+ // it wears the star colour the dialog's hero row uses
3200
+ // rather than hiding among the report types.
3201
+ border: `1px solid ${rate ? "rgba(245,158,11,0.5)" : t.inputBorder}`,
3202
+ background: rate ? "rgba(245,158,11,0.08)" : "transparent",
3203
+ color: rate ? "#f59e0b" : t.textMuted,
3204
+ borderRadius: "999px",
3205
+ // 8px vertical keeps the tap target at ~32px.
3206
+ padding: "8px 12px",
3207
+ fontSize: "12px",
3208
+ fontWeight: rate ? 600 : 400,
3209
+ fontFamily: "inherit",
3210
+ cursor: "pointer"
3211
+ },
3212
+ children: rate ? "\u2605 Rate us" : getTypeLabel(tile)
3213
+ },
3214
+ tile
3215
+ );
3216
+ })
2433
3217
  }
2434
3218
  )
2435
3219
  ] }),
2436
- /* @__PURE__ */ jsx3(
2437
- "div",
2438
- {
2439
- style: {
2440
- display: "flex",
2441
- flexWrap: "wrap",
2442
- gap: "6px",
2443
- paddingLeft: "32px"
2444
- },
2445
- children: tiles.map((tile) => {
2446
- const rate = tile === "RATING";
2447
- return /* @__PURE__ */ jsx3(
3220
+ phase === "severity" && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3221
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
3222
+ /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
3223
+ /* @__PURE__ */ jsx3(
3224
+ "div",
3225
+ {
3226
+ style: {
3227
+ padding: "9px 12px",
3228
+ borderRadius: "12px 12px 12px 4px",
3229
+ backgroundColor: t.bgSecondary,
3230
+ fontSize: "13px",
3231
+ lineHeight: 1.55,
3232
+ maxWidth: "88%"
3233
+ },
3234
+ children: "How bad is it?"
3235
+ }
3236
+ )
3237
+ ] }),
3238
+ /* @__PURE__ */ jsx3(
3239
+ "div",
3240
+ {
3241
+ role: "radiogroup",
3242
+ "aria-label": "Severity",
3243
+ style: {
3244
+ display: "flex",
3245
+ flexWrap: "wrap",
3246
+ gap: "6px",
3247
+ paddingLeft: "32px"
3248
+ },
3249
+ children: SEVERITY_LEVELS.map((s) => /* @__PURE__ */ jsx3(
2448
3250
  "button",
2449
3251
  {
2450
3252
  type: "button",
3253
+ role: "radio",
3254
+ "aria-checked": severity === s,
2451
3255
  "data-gg-chip": "",
2452
- "data-gg-tile": tile,
2453
- onClick: () => pickType(tile),
3256
+ "data-gg-severity": s,
3257
+ onClick: () => answerSeverity(s),
2454
3258
  style: {
2455
- display: "inline-flex",
2456
- alignItems: "center",
2457
- gap: "5px",
2458
- // The rating is the only tile that isn't a report, so
2459
- // it wears the star colour the dialog's hero row uses
2460
- // rather than hiding among the report types.
2461
- border: `1px solid ${rate ? "rgba(245,158,11,0.5)" : t.inputBorder}`,
2462
- background: rate ? "rgba(245,158,11,0.08)" : "transparent",
2463
- color: rate ? "#f59e0b" : t.textMuted,
3259
+ border: `1px solid ${t.inputBorder}`,
3260
+ background: "transparent",
3261
+ color: t.textMuted,
2464
3262
  borderRadius: "999px",
2465
- // 8px vertical keeps the tap target at ~32px.
3263
+ // Same as the picker and option chips — one chip size.
2466
3264
  padding: "8px 12px",
2467
3265
  fontSize: "12px",
2468
- fontWeight: rate ? 600 : 400,
2469
3266
  fontFamily: "inherit",
2470
3267
  cursor: "pointer"
2471
3268
  },
2472
- children: rate ? "\u2605 Rate us" : getTypeLabel(tile)
3269
+ children: SEVERITY_LABELS[s]
2473
3270
  },
2474
- tile
2475
- );
2476
- })
2477
- }
2478
- )
2479
- ] }),
2480
- phase === "severity" && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2481
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
2482
- /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
2483
- /* @__PURE__ */ jsx3(
2484
- "div",
2485
- {
2486
- style: {
2487
- padding: "9px 12px",
2488
- borderRadius: "12px 12px 12px 4px",
2489
- backgroundColor: t.bgSecondary,
2490
- fontSize: "13px",
2491
- lineHeight: 1.55,
2492
- maxWidth: "88%"
2493
- },
2494
- children: "How bad is it?"
3271
+ s
3272
+ ))
2495
3273
  }
2496
3274
  )
2497
3275
  ] }),
2498
- /* @__PURE__ */ jsx3(
2499
- "div",
2500
- {
2501
- role: "radiogroup",
2502
- "aria-label": "Severity",
2503
- style: {
2504
- display: "flex",
2505
- flexWrap: "wrap",
2506
- gap: "6px",
2507
- paddingLeft: "32px"
2508
- },
2509
- children: SEVERITY_LEVELS.map((s) => /* @__PURE__ */ jsx3(
2510
- "button",
3276
+ phase === "rating" && !submitted && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3277
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
3278
+ /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
3279
+ /* @__PURE__ */ jsx3(
3280
+ "div",
2511
3281
  {
2512
- type: "button",
2513
- role: "radio",
2514
- "aria-checked": severity === s,
2515
- "data-gg-chip": "",
2516
- "data-gg-severity": s,
2517
- onClick: () => answerSeverity(s),
2518
3282
  style: {
2519
- border: `1px solid ${t.inputBorder}`,
2520
- background: "transparent",
2521
- color: t.textMuted,
2522
- borderRadius: "999px",
2523
- // Same as the picker and option chips — one chip size.
2524
- padding: "8px 12px",
2525
- fontSize: "12px",
2526
- fontFamily: "inherit",
2527
- cursor: "pointer"
3283
+ padding: "9px 12px",
3284
+ borderRadius: "12px 12px 12px 4px",
3285
+ backgroundColor: t.bgSecondary,
3286
+ fontSize: "13px",
3287
+ lineHeight: 1.55,
3288
+ maxWidth: "88%"
2528
3289
  },
2529
- children: SEVERITY_LABELS[s]
2530
- },
2531
- s
2532
- ))
2533
- }
2534
- )
2535
- ] }),
2536
- phase === "rating" && !submitted && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2537
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
2538
- /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
2539
- /* @__PURE__ */ jsx3(
3290
+ children: "How are we doing? Tap a star \u2014 the words are optional."
3291
+ }
3292
+ )
3293
+ ] }),
3294
+ /* @__PURE__ */ jsxs3(
2540
3295
  "div",
2541
3296
  {
2542
3297
  style: {
2543
- padding: "9px 12px",
2544
- borderRadius: "12px 12px 12px 4px",
2545
- backgroundColor: t.bgSecondary,
2546
- fontSize: "13px",
2547
- lineHeight: 1.55,
2548
- maxWidth: "88%"
3298
+ display: "flex",
3299
+ flexDirection: "column",
3300
+ gap: "10px",
3301
+ marginLeft: "32px",
3302
+ padding: "12px",
3303
+ borderRadius: "12px",
3304
+ border: "1px solid rgba(245,158,11,0.4)",
3305
+ backgroundColor: "rgba(245,158,11,0.06)"
2549
3306
  },
2550
- children: "How are we doing? Tap a star \u2014 the words are optional."
2551
- }
2552
- )
2553
- ] }),
2554
- /* @__PURE__ */ jsxs3(
2555
- "div",
2556
- {
2557
- style: {
2558
- display: "flex",
2559
- flexDirection: "column",
2560
- gap: "10px",
2561
- marginLeft: "32px",
2562
- padding: "12px",
2563
- borderRadius: "12px",
2564
- border: "1px solid rgba(245,158,11,0.4)",
2565
- backgroundColor: "rgba(245,158,11,0.06)"
2566
- },
2567
- children: [
2568
- /* @__PURE__ */ jsx3("div", { style: { display: "flex", gap: "4px" }, role: "group", "aria-label": "Rating", children: [1, 2, 3, 4, 5].map((star) => {
2569
- const filled = star <= (hoveredStar || rating);
2570
- return /* @__PURE__ */ jsx3(
3307
+ children: [
3308
+ /* @__PURE__ */ jsx3("div", { style: { display: "flex", gap: "4px" }, role: "group", "aria-label": "Rating", children: [1, 2, 3, 4, 5].map((star) => {
3309
+ const filled = star <= (hoveredStar || rating);
3310
+ return /* @__PURE__ */ jsx3(
3311
+ "button",
3312
+ {
3313
+ type: "button",
3314
+ "aria-label": `${star} star${star > 1 ? "s" : ""}`,
3315
+ "aria-pressed": star <= rating,
3316
+ onClick: () => onRatingChange(star),
3317
+ onMouseEnter: () => setHoveredStar(star),
3318
+ onMouseLeave: () => setHoveredStar(0),
3319
+ style: {
3320
+ border: "none",
3321
+ background: "transparent",
3322
+ padding: 0,
3323
+ cursor: "pointer",
3324
+ lineHeight: 0
3325
+ },
3326
+ children: /* @__PURE__ */ jsx3("svg", { width: "30", height: "30", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
3327
+ "path",
3328
+ {
3329
+ d: "M12 3l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.4 6.8 19.1l1-5.8L3.5 9.2l5.9-.9L12 3z",
3330
+ fill: filled ? "#f59e0b" : "transparent",
3331
+ stroke: filled ? "#f59e0b" : t.inputBorder,
3332
+ strokeWidth: "1.5",
3333
+ strokeLinejoin: "round"
3334
+ }
3335
+ ) })
3336
+ },
3337
+ star
3338
+ );
3339
+ }) }),
3340
+ /* @__PURE__ */ jsx3(
3341
+ "textarea",
3342
+ {
3343
+ value: description,
3344
+ onChange: (e) => onDescriptionChange(e.target.value),
3345
+ rows: 3,
3346
+ placeholder: "What made it good \u2014 or what let you down? (optional)",
3347
+ style: {
3348
+ width: "100%",
3349
+ padding: "10px",
3350
+ borderRadius: "8px",
3351
+ border: `1px solid ${t.inputBorder}`,
3352
+ backgroundColor: t.inputBg,
3353
+ color: t.text,
3354
+ fontSize: "13px",
3355
+ lineHeight: 1.5,
3356
+ fontFamily: "inherit",
3357
+ resize: "vertical",
3358
+ outline: "none",
3359
+ boxSizing: "border-box"
3360
+ }
3361
+ }
3362
+ ),
3363
+ /* @__PURE__ */ jsx3(
2571
3364
  "button",
2572
3365
  {
2573
3366
  type: "button",
2574
- "aria-label": `${star} star${star > 1 ? "s" : ""}`,
2575
- "aria-pressed": star <= rating,
2576
- onClick: () => onRatingChange(star),
2577
- onMouseEnter: () => setHoveredStar(star),
2578
- onMouseLeave: () => setHoveredStar(0),
3367
+ onClick: () => setPhase("type"),
2579
3368
  style: {
3369
+ alignSelf: "flex-start",
2580
3370
  border: "none",
2581
3371
  background: "transparent",
2582
- padding: 0,
3372
+ color: t.textMuted,
3373
+ fontSize: "12px",
3374
+ fontFamily: "inherit",
3375
+ // A bare text link is ~14px tall — half a thumb.
3376
+ padding: "9px 0",
2583
3377
  cursor: "pointer",
2584
- lineHeight: 0
3378
+ textDecoration: "underline"
2585
3379
  },
2586
- children: /* @__PURE__ */ jsx3("svg", { width: "30", height: "30", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
2587
- "path",
2588
- {
2589
- d: "M12 3l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.4 6.8 19.1l1-5.8L3.5 9.2l5.9-.9L12 3z",
2590
- fill: filled ? "#f59e0b" : "transparent",
2591
- stroke: filled ? "#f59e0b" : t.inputBorder,
2592
- strokeWidth: "1.5",
2593
- strokeLinejoin: "round"
2594
- }
2595
- ) })
2596
- },
2597
- star
2598
- );
2599
- }) }),
2600
- /* @__PURE__ */ jsx3(
2601
- "textarea",
2602
- {
2603
- value: description,
2604
- onChange: (e) => onDescriptionChange(e.target.value),
2605
- rows: 3,
2606
- placeholder: "What made it good \u2014 or what let you down? (optional)",
2607
- style: {
2608
- width: "100%",
2609
- padding: "10px",
2610
- borderRadius: "8px",
2611
- border: `1px solid ${t.inputBorder}`,
2612
- backgroundColor: t.inputBg,
2613
- color: t.text,
2614
- fontSize: "13px",
2615
- lineHeight: 1.5,
2616
- fontFamily: "inherit",
2617
- resize: "vertical",
2618
- outline: "none",
2619
- boxSizing: "border-box"
3380
+ children: "Report something instead"
2620
3381
  }
2621
- }
2622
- ),
2623
- /* @__PURE__ */ jsx3(
2624
- "button",
2625
- {
2626
- type: "button",
2627
- onClick: () => setPhase("type"),
2628
- style: {
2629
- alignSelf: "flex-start",
2630
- border: "none",
2631
- background: "transparent",
2632
- color: t.textMuted,
2633
- fontSize: "12px",
2634
- fontFamily: "inherit",
2635
- // A bare text link is ~14px tall — half a thumb.
2636
- padding: "9px 0",
2637
- cursor: "pointer",
2638
- textDecoration: "underline"
2639
- },
2640
- children: "Report something instead"
2641
- }
2642
- )
2643
- ]
2644
- }
2645
- )
2646
- ] }),
2647
- empty && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2648
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
2649
- /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
2650
- /* @__PURE__ */ jsxs3(
3382
+ )
3383
+ ]
3384
+ }
3385
+ )
3386
+ ] }),
3387
+ empty && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3388
+ resumable && /* @__PURE__ */ jsxs3(
2651
3389
  "div",
2652
3390
  {
3391
+ "data-gg-resume-chat": "",
2653
3392
  style: {
2654
- padding: "9px 12px",
2655
- borderRadius: "12px 12px 12px 4px",
2656
- backgroundColor: t.bgSecondary,
2657
- fontSize: "13px",
2658
- lineHeight: 1.55,
2659
- maxWidth: "88%"
3393
+ display: "flex",
3394
+ flexDirection: "column",
3395
+ gap: "8px",
3396
+ padding: "10px 12px",
3397
+ borderRadius: "10px",
3398
+ border: `1px solid ${t.accent}55`,
3399
+ backgroundColor: t.bgSecondary,
3400
+ minWidth: 0
2660
3401
  },
2661
3402
  children: [
2662
- reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 t` : "T",
2663
- "ell me what went wrong in your own words. I can already see your screenshot and what you clicked, so skip the boring parts."
2664
- ]
2665
- }
2666
- )
2667
- ] }),
2668
- /* @__PURE__ */ jsx3(
2669
- "div",
2670
- {
2671
- style: {
2672
- display: "flex",
2673
- flexWrap: "wrap",
2674
- gap: "6px",
2675
- paddingLeft: "32px"
2676
- },
2677
- children: STARTERS.map((s) => /* @__PURE__ */ jsx3(
2678
- "button",
2679
- {
2680
- type: "button",
2681
- "data-gg-chip": "",
2682
- onClick: () => {
2683
- logEvent("starter_tap", s);
2684
- send(s);
2685
- },
2686
- style: {
2687
- border: `1px solid ${t.inputBorder}`,
2688
- background: "transparent",
2689
- color: t.textMuted,
2690
- borderRadius: "999px",
2691
- padding: "6px 11px",
2692
- fontSize: "12px",
2693
- fontFamily: "inherit",
2694
- cursor: "pointer"
2695
- },
2696
- children: s
2697
- },
2698
- s
2699
- ))
2700
- }
2701
- )
2702
- ] }),
2703
- messages.map((m, i) => {
2704
- const mine = m.role === "user";
2705
- if (m.kind === "report") {
2706
- const live = phase === "draft" && i === messages.length - 1;
2707
- return /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, children: [
2708
- /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
2709
- /* @__PURE__ */ jsx3(
2710
- "div",
2711
- {
2712
- "data-gg-draft-bubble": "",
2713
- style: {
2714
- maxWidth: "82%",
2715
- minWidth: 0,
2716
- padding: "9px 12px",
2717
- borderRadius: "12px 12px 12px 4px",
2718
- fontSize: "13px",
2719
- lineHeight: 1.55,
2720
- backgroundColor: t.bgSecondary,
2721
- color: t.text,
2722
- border: live ? "none" : `1px solid ${t.accent}66`
2723
- },
2724
- children: live ? (
2725
- // #402: "have a read" beside a countdown reads as a
2726
- // contradiction. Say what is about to happen instead.
2727
- autoFileLeft !== null ? "Here's your report \u2014 I'll file it unless you want to change something." : "Here's your report \u2014 have a read."
2728
- ) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
3403
+ /* @__PURE__ */ jsxs3("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
3404
+ "Pick up where you left off \xB7 ",
3405
+ chatAge(resumable.updatedAt)
3406
+ ] }),
3407
+ /* @__PURE__ */ jsx3(
3408
+ "span",
3409
+ {
3410
+ style: {
3411
+ fontSize: "13px",
3412
+ lineHeight: 1.45,
3413
+ overflowWrap: "anywhere"
3414
+ },
3415
+ children: resumable.title
3416
+ }
3417
+ ),
3418
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap" }, children: [
2729
3419
  /* @__PURE__ */ jsx3(
2730
- "span",
3420
+ "button",
2731
3421
  {
3422
+ type: "button",
3423
+ "data-gg-resume-continue": "",
3424
+ onClick: () => resumeChat(resumable),
2732
3425
  style: {
2733
- display: "block",
2734
- fontSize: "10px",
2735
- fontWeight: 700,
2736
- letterSpacing: ".06em",
2737
- textTransform: "uppercase",
2738
- color: t.accent,
2739
- marginBottom: "4px"
3426
+ border: "none",
3427
+ borderRadius: "999px",
3428
+ backgroundColor: t.accent,
3429
+ color: t.accentText,
3430
+ padding: "7px 13px",
3431
+ fontSize: "12px",
3432
+ fontWeight: 600,
3433
+ fontFamily: "inherit",
3434
+ cursor: "pointer"
2740
3435
  },
2741
- children: "Draft so far"
3436
+ children: "Continue this chat"
2742
3437
  }
2743
3438
  ),
2744
3439
  /* @__PURE__ */ jsx3(
2745
- "span",
3440
+ "button",
2746
3441
  {
3442
+ type: "button",
3443
+ onClick: () => setShowChats(true),
2747
3444
  style: {
2748
- display: "block",
2749
- whiteSpace: "pre-wrap",
2750
- wordBreak: "break-word",
2751
- maxHeight: "160px",
2752
- overflowY: "auto",
2753
- color: t.textMuted
3445
+ border: `1px solid ${t.inputBorder}`,
3446
+ borderRadius: "999px",
3447
+ background: "transparent",
3448
+ color: t.textMuted,
3449
+ padding: "7px 13px",
3450
+ fontSize: "12px",
3451
+ fontFamily: "inherit",
3452
+ cursor: "pointer"
2754
3453
  },
2755
- children: m.content
3454
+ children: "All chats"
2756
3455
  }
2757
3456
  )
2758
3457
  ] })
3458
+ ]
3459
+ }
3460
+ ),
3461
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
3462
+ /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
3463
+ /* @__PURE__ */ jsxs3(
3464
+ "div",
3465
+ {
3466
+ style: {
3467
+ padding: "9px 12px",
3468
+ borderRadius: "12px 12px 12px 4px",
3469
+ backgroundColor: t.bgSecondary,
3470
+ fontSize: "13px",
3471
+ lineHeight: 1.55,
3472
+ maxWidth: "88%"
3473
+ },
3474
+ children: [
3475
+ reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 t` : "T",
3476
+ "ell me what went wrong in your own words. I can already see your screenshot and what you clicked, so skip the boring parts."
3477
+ ]
2759
3478
  }
2760
3479
  )
2761
- ] }, i);
2762
- }
2763
- return /* @__PURE__ */ jsxs3(
2764
- "div",
2765
- {
2766
- style: {
2767
- display: "flex",
2768
- gap: "8px",
2769
- minWidth: 0,
2770
- flexDirection: mine ? "row-reverse" : "row"
2771
- },
2772
- children: [
2773
- /* @__PURE__ */ jsx3(Avatar, { who: m.role, theme: t, initials: initials2 }),
2774
- /* @__PURE__ */ jsx3(
2775
- "div",
2776
- {
2777
- style: {
2778
- maxWidth: "82%",
2779
- minWidth: 0,
2780
- padding: "9px 12px",
2781
- borderRadius: mine ? "12px 12px 4px 12px" : "12px 12px 12px 4px",
2782
- fontSize: "13px",
2783
- lineHeight: 1.55,
2784
- // Markdown owns the assistant's line breaks and lists.
2785
- whiteSpace: mine ? "pre-wrap" : "normal",
2786
- wordBreak: "break-word",
2787
- backgroundColor: mine ? t.accent : t.bgSecondary,
2788
- color: mine ? t.accentText : t.text
2789
- },
2790
- children: mine ? m.content : /* @__PURE__ */ jsx3(ChatMarkdown, { text: m.content, theme: t })
2791
- }
2792
- )
2793
- ]
2794
- },
2795
- i
2796
- );
2797
- }),
2798
- duplicate && !busy && phase === "chat" && /* @__PURE__ */ jsx3("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: renderKnownIssue({ title: false }) }),
2799
- glitchgrabOffer && onReportGlitchgrabProblem && !busy && phase === "chat" && /* @__PURE__ */ jsx3("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: /* @__PURE__ */ jsxs3(
2800
- "div",
2801
- {
2802
- "data-gg-glitchgrab-offer": "",
2803
- style: {
2804
- display: "flex",
2805
- flexDirection: "column",
2806
- gap: "8px",
2807
- padding: "10px",
2808
- borderRadius: "8px",
2809
- border: `1px solid ${t.accent}55`,
2810
- backgroundColor: `${t.accent}14`,
2811
- fontSize: "12px",
2812
- lineHeight: 1.5
2813
- },
2814
- children: [
2815
- /* @__PURE__ */ jsx3("span", { style: { color: t.text, fontWeight: 600 }, children: "This is about Glitchgrab, not this app" }),
2816
- /* @__PURE__ */ jsx3("span", { style: { color: t.textMuted }, children: "It goes to the team that builds this reporter, with your chat attached." }),
2817
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: [
2818
- /* @__PURE__ */ jsx3(
3480
+ ] }),
3481
+ /* @__PURE__ */ jsx3(
3482
+ "div",
3483
+ {
3484
+ style: {
3485
+ display: "flex",
3486
+ flexWrap: "wrap",
3487
+ gap: "6px",
3488
+ paddingLeft: "32px"
3489
+ },
3490
+ children: STARTERS.map((s) => /* @__PURE__ */ jsx3(
2819
3491
  "button",
2820
3492
  {
2821
3493
  type: "button",
2822
- "data-gg-glitchgrab-offer-send": "",
3494
+ "data-gg-chip": "",
2823
3495
  onClick: () => {
2824
- logEvent("glitchgrab_offer_tap");
2825
- setGlitchgrabOffer(false);
2826
- onReportGlitchgrabProblem(
2827
- messages.filter((m) => m.role === "user").map((m) => m.content.trim()).filter(Boolean).join("\n\n")
2828
- );
3496
+ logEvent("starter_tap", s);
3497
+ send(s);
2829
3498
  },
2830
3499
  style: {
2831
- padding: "8px 12px",
3500
+ border: `1px solid ${t.inputBorder}`,
3501
+ background: "transparent",
3502
+ color: t.textMuted,
2832
3503
  borderRadius: "999px",
2833
- border: "none",
2834
- backgroundColor: t.accent,
2835
- color: t.accentText,
3504
+ padding: "6px 11px",
2836
3505
  fontSize: "12px",
2837
- fontWeight: 700,
2838
3506
  fontFamily: "inherit",
2839
- cursor: "pointer",
2840
- // Both stretch, so on a phone each wrapped line reads as
2841
- // a choice rather than one button left stranded.
2842
- flex: "1 1 auto"
3507
+ cursor: "pointer"
2843
3508
  },
2844
- children: "Send to the Glitchgrab team"
2845
- }
2846
- ),
3509
+ children: s
3510
+ },
3511
+ s
3512
+ ))
3513
+ }
3514
+ )
3515
+ ] }),
3516
+ messages.map((m, i) => {
3517
+ const mine = m.role === "user";
3518
+ if (m.kind === "report") {
3519
+ const live = phase === "draft" && i === messages.length - 1;
3520
+ return /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, children: [
3521
+ /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
2847
3522
  /* @__PURE__ */ jsx3(
2848
- "button",
3523
+ "div",
2849
3524
  {
2850
- type: "button",
2851
- "data-gg-glitchgrab-offer-no": "",
2852
- onClick: () => {
2853
- logEvent("glitchgrab_offer_declined");
2854
- send("No \u2014 it's about this app, not the reporting tool");
2855
- },
3525
+ "data-gg-draft-bubble": "",
2856
3526
  style: {
2857
- padding: "8px 12px",
2858
- borderRadius: "999px",
2859
- border: `1px solid ${t.inputBorder}`,
2860
- background: "transparent",
2861
- color: t.textMuted,
2862
- fontSize: "12px",
2863
- fontFamily: "inherit",
2864
- cursor: "pointer",
2865
- flex: "1 1 auto"
3527
+ maxWidth: "82%",
3528
+ minWidth: 0,
3529
+ padding: "9px 12px",
3530
+ borderRadius: "12px 12px 12px 4px",
3531
+ fontSize: "13px",
3532
+ lineHeight: 1.55,
3533
+ backgroundColor: t.bgSecondary,
3534
+ color: t.text,
3535
+ border: live ? "none" : `1px solid ${t.accent}66`
2866
3536
  },
2867
- children: "No, it's about this app"
3537
+ children: live ? (
3538
+ // #402: "have a read" beside a countdown reads as a
3539
+ // contradiction. Say what is about to happen instead.
3540
+ autoFileLeft !== null ? "Here's your report \u2014 I'll file it unless you want to change something." : "Here's your report \u2014 have a read."
3541
+ ) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
3542
+ /* @__PURE__ */ jsx3(
3543
+ "span",
3544
+ {
3545
+ style: {
3546
+ display: "block",
3547
+ fontSize: "10px",
3548
+ fontWeight: 700,
3549
+ letterSpacing: ".06em",
3550
+ textTransform: "uppercase",
3551
+ color: t.accent,
3552
+ marginBottom: "4px"
3553
+ },
3554
+ children: "Draft so far"
3555
+ }
3556
+ ),
3557
+ /* @__PURE__ */ jsx3(
3558
+ "span",
3559
+ {
3560
+ style: {
3561
+ display: "block",
3562
+ whiteSpace: "pre-wrap",
3563
+ wordBreak: "break-word",
3564
+ maxHeight: "160px",
3565
+ overflowY: "auto",
3566
+ color: t.textMuted
3567
+ },
3568
+ children: m.content
3569
+ }
3570
+ )
3571
+ ] })
2868
3572
  }
2869
3573
  )
2870
- ] })
2871
- ]
2872
- }
2873
- ) }),
2874
- options.length > 0 && !busy && phase === "chat" && !glitchgrabOffer && /* @__PURE__ */ jsx3(
2875
- "div",
2876
- {
2877
- style: {
2878
- display: "flex",
2879
- // Long labels wrapped into a ragged row that always stranded
2880
- // the last chip on a line of its own. Past ~24 characters
2881
- // these stop being chips and become a choice list, so they
2882
- // stack full width and read like one.
2883
- flexDirection: options.some((o) => o.length > 24) ? "column" : "row",
2884
- alignItems: options.some((o) => o.length > 24) ? "stretch" : "flex-start",
2885
- flexWrap: "wrap",
2886
- gap: "6px",
2887
- paddingLeft: "32px"
2888
- },
2889
- children: options.map((option) => /* @__PURE__ */ jsx3(
2890
- "button",
3574
+ ] }, i);
3575
+ }
3576
+ return /* @__PURE__ */ jsxs3(
3577
+ "div",
2891
3578
  {
2892
- type: "button",
2893
- "data-gg-chip": "",
2894
- "data-gg-option": "",
2895
- onClick: () => {
2896
- logEvent("option_tap", option);
2897
- send(option, option === CONFIRM_OPTION);
2898
- },
2899
3579
  style: {
2900
- border: `1px solid ${t.inputBorder}`,
2901
- background: "transparent",
2902
- color: t.textMuted,
2903
- borderRadius: "999px",
2904
- // Same 8px as the picker chips — one tap target size in
2905
- // this sheet, not two.
2906
- padding: "8px 12px",
2907
- fontSize: "12px",
2908
- fontFamily: "inherit",
2909
- cursor: "pointer",
2910
- textAlign: "left"
3580
+ display: "flex",
3581
+ gap: "8px",
3582
+ minWidth: 0,
3583
+ flexDirection: mine ? "row-reverse" : "row"
2911
3584
  },
2912
- children: option
3585
+ children: [
3586
+ /* @__PURE__ */ jsx3(Avatar, { who: m.role, theme: t, initials: initials2 }),
3587
+ /* @__PURE__ */ jsx3(
3588
+ "div",
3589
+ {
3590
+ style: {
3591
+ maxWidth: "82%",
3592
+ minWidth: 0,
3593
+ padding: "9px 12px",
3594
+ borderRadius: mine ? "12px 12px 4px 12px" : "12px 12px 12px 4px",
3595
+ fontSize: "13px",
3596
+ lineHeight: 1.55,
3597
+ // Markdown owns the assistant's line breaks and lists.
3598
+ whiteSpace: mine ? "pre-wrap" : "normal",
3599
+ wordBreak: "break-word",
3600
+ backgroundColor: mine ? t.accent : t.bgSecondary,
3601
+ color: mine ? t.accentText : t.text
3602
+ },
3603
+ children: mine ? m.content : /* @__PURE__ */ jsx3(ChatMarkdown, { text: m.content, theme: t })
3604
+ }
3605
+ )
3606
+ ]
2913
3607
  },
2914
- option
2915
- ))
2916
- }
2917
- ),
2918
- busy && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, role: "status", "aria-label": "Thinking", children: [
2919
- /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
2920
- /* @__PURE__ */ jsx3(
2921
- "span",
3608
+ i
3609
+ );
3610
+ }),
3611
+ duplicate && !busy && phase === "chat" && /* @__PURE__ */ jsx3("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: renderKnownIssue({ title: false }) }),
3612
+ glitchgrabOffer && onReportGlitchgrabProblem && !busy && phase === "chat" && /* @__PURE__ */ jsx3("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: /* @__PURE__ */ jsxs3(
3613
+ "div",
2922
3614
  {
3615
+ "data-gg-glitchgrab-offer": "",
2923
3616
  style: {
2924
- display: "inline-flex",
2925
- alignItems: "center",
2926
- gap: "4px",
2927
- padding: "12px",
2928
- borderRadius: "12px 12px 12px 4px",
2929
- backgroundColor: t.bgSecondary
3617
+ display: "flex",
3618
+ flexDirection: "column",
3619
+ gap: "8px",
3620
+ padding: "10px",
3621
+ borderRadius: "8px",
3622
+ border: `1px solid ${t.accent}55`,
3623
+ backgroundColor: `${t.accent}14`,
3624
+ fontSize: "12px",
3625
+ lineHeight: 1.5
2930
3626
  },
2931
- children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx3(
2932
- "span",
2933
- {
2934
- style: {
2935
- width: "5px",
2936
- height: "5px",
2937
- borderRadius: "50%",
2938
- backgroundColor: t.textMuted,
2939
- animation: `gg-dot 1.1s ease-in-out ${i * 0.15}s infinite`
2940
- }
2941
- },
2942
- i
2943
- ))
3627
+ children: [
3628
+ /* @__PURE__ */ jsx3("span", { style: { color: t.text, fontWeight: 600 }, children: "This is about Glitchgrab, not this app" }),
3629
+ /* @__PURE__ */ jsx3("span", { style: { color: t.textMuted }, children: "It goes to the team that builds this reporter, with your chat attached." }),
3630
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: [
3631
+ /* @__PURE__ */ jsx3(
3632
+ "button",
3633
+ {
3634
+ type: "button",
3635
+ "data-gg-glitchgrab-offer-send": "",
3636
+ onClick: () => {
3637
+ logEvent("glitchgrab_offer_tap");
3638
+ setGlitchgrabOffer(false);
3639
+ onReportGlitchgrabProblem(
3640
+ messages.filter((m) => m.role === "user").map((m) => m.content.trim()).filter(Boolean).join("\n\n")
3641
+ );
3642
+ },
3643
+ style: {
3644
+ padding: "8px 12px",
3645
+ borderRadius: "999px",
3646
+ border: "none",
3647
+ backgroundColor: t.accent,
3648
+ color: t.accentText,
3649
+ fontSize: "12px",
3650
+ fontWeight: 700,
3651
+ fontFamily: "inherit",
3652
+ cursor: "pointer",
3653
+ // Both stretch, so on a phone each wrapped line reads as
3654
+ // a choice rather than one button left stranded.
3655
+ flex: "1 1 auto"
3656
+ },
3657
+ children: "Send to the Glitchgrab team"
3658
+ }
3659
+ ),
3660
+ /* @__PURE__ */ jsx3(
3661
+ "button",
3662
+ {
3663
+ type: "button",
3664
+ "data-gg-glitchgrab-offer-no": "",
3665
+ onClick: () => {
3666
+ logEvent("glitchgrab_offer_declined");
3667
+ send("No \u2014 it's about this app, not the reporting tool");
3668
+ },
3669
+ style: {
3670
+ padding: "8px 12px",
3671
+ borderRadius: "999px",
3672
+ border: `1px solid ${t.inputBorder}`,
3673
+ background: "transparent",
3674
+ color: t.textMuted,
3675
+ fontSize: "12px",
3676
+ fontFamily: "inherit",
3677
+ cursor: "pointer",
3678
+ flex: "1 1 auto"
3679
+ },
3680
+ children: "No, it's about this app"
3681
+ }
3682
+ )
3683
+ ] })
3684
+ ]
2944
3685
  }
2945
- )
2946
- ] }),
2947
- phase === "draft" && !submitted && /* @__PURE__ */ jsxs3(
2948
- "div",
2949
- {
2950
- style: {
2951
- border: `1px solid ${t.accent}`,
2952
- borderRadius: "12px",
2953
- backgroundColor: t.bgSecondary,
2954
- padding: "12px",
2955
- display: "flex",
2956
- flexDirection: "column",
2957
- gap: "10px",
2958
- minWidth: 0,
2959
- boxSizing: "border-box"
2960
- },
2961
- children: [
2962
- /* @__PURE__ */ jsx3(
2963
- "span",
2964
- {
2965
- style: {
2966
- fontSize: "10px",
2967
- fontWeight: 700,
2968
- letterSpacing: ".06em",
2969
- textTransform: "uppercase",
2970
- color: t.accent
2971
- },
2972
- children: duplicate ? `What we'll add to #${duplicate.number}${duplicate.kind === "related" ? " as a related request" : ""} \xB7 edit anything` : "Your report \xB7 edit anything"
2973
- }
2974
- ),
2975
- renderKnownIssue({
2976
- title: true,
2977
- footer: "Your details will be added to it instead of opening a new issue."
2978
- }),
2979
- !duplicate && issueTitle && // Wraps rather than scrolls sideways: a title cut off at the
2980
- // box edge is the very thing #543 is about. One line of text,
2981
- // so Enter is swallowed.
2982
- /* @__PURE__ */ jsx3(
2983
- "textarea",
3686
+ ) }),
3687
+ options.length > 0 && !busy && phase === "chat" && !glitchgrabOffer && /* @__PURE__ */ jsx3(
3688
+ "div",
3689
+ {
3690
+ style: {
3691
+ display: "flex",
3692
+ // Long labels wrapped into a ragged row that always stranded
3693
+ // the last chip on a line of its own. Past ~24 characters
3694
+ // these stop being chips and become a choice list, so they
3695
+ // stack full width and read like one.
3696
+ flexDirection: options.some((o) => o.length > 24) ? "column" : "row",
3697
+ alignItems: options.some((o) => o.length > 24) ? "stretch" : "flex-start",
3698
+ flexWrap: "wrap",
3699
+ gap: "6px",
3700
+ paddingLeft: "32px"
3701
+ },
3702
+ children: options.map((option) => /* @__PURE__ */ jsx3(
3703
+ "button",
2984
3704
  {
2985
- "aria-label": "Title",
2986
- value: issueTitle,
2987
- maxLength: 120,
2988
- rows: 2,
2989
- onKeyDown: (e) => {
2990
- if (e.key === "Enter") e.preventDefault();
2991
- },
2992
- onChange: (e) => {
2993
- stopAutoFile("edited the title");
2994
- onIssueTitleChange(e.target.value.replace(/[\r\n]+/g, " "));
3705
+ type: "button",
3706
+ "data-gg-chip": "",
3707
+ "data-gg-option": "",
3708
+ onClick: () => {
3709
+ logEvent("option_tap", option);
3710
+ send(option, option === CONFIRM_OPTION);
2995
3711
  },
2996
3712
  style: {
2997
- width: "100%",
2998
- padding: "8px 10px",
2999
- lineHeight: 1.45,
3000
- resize: "none",
3001
- borderRadius: "8px",
3002
3713
  border: `1px solid ${t.inputBorder}`,
3003
- backgroundColor: t.inputBg,
3004
- color: t.text,
3005
- fontSize: "13px",
3006
- fontWeight: 600,
3714
+ background: "transparent",
3715
+ color: t.textMuted,
3716
+ borderRadius: "999px",
3717
+ // Same 8px as the picker chips — one tap target size in
3718
+ // this sheet, not two.
3719
+ padding: "8px 12px",
3720
+ fontSize: "12px",
3007
3721
  fontFamily: "inherit",
3008
- outline: "none",
3009
- boxSizing: "border-box"
3010
- }
3011
- }
3012
- ),
3013
- /* @__PURE__ */ jsx3(
3014
- "textarea",
3015
- {
3016
- ref: draftRef,
3017
- value: description,
3018
- onChange: (e) => {
3019
- stopAutoFile("edited the draft");
3020
- onDescriptionChange(e.target.value);
3722
+ cursor: "pointer",
3723
+ textAlign: "left"
3021
3724
  },
3022
- rows: 6,
3023
- style: {
3024
- width: "100%",
3025
- minHeight: "120px",
3026
- padding: "10px",
3027
- borderRadius: "8px",
3028
- border: `1px solid ${t.inputBorder}`,
3029
- backgroundColor: t.inputBg,
3030
- color: t.text,
3031
- fontSize: "13px",
3032
- lineHeight: 1.55,
3033
- fontFamily: "inherit",
3034
- resize: "vertical",
3035
- outline: "none",
3036
- boxSizing: "border-box"
3037
- }
3038
- }
3039
- ),
3040
- severityAsked && autoFileLeft === null && /* @__PURE__ */ jsxs3("div", { children: [
3041
- /* @__PURE__ */ jsxs3(
3725
+ children: option
3726
+ },
3727
+ option
3728
+ ))
3729
+ }
3730
+ ),
3731
+ busy && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, role: "status", "aria-label": "Thinking", children: [
3732
+ /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
3733
+ /* @__PURE__ */ jsxs3(
3734
+ "span",
3735
+ {
3736
+ style: {
3737
+ display: "inline-flex",
3738
+ alignItems: "center",
3739
+ // The "still working" line must wrap on a phone, not push
3740
+ // the bubble past the sheet's edge.
3741
+ flexWrap: "wrap",
3742
+ minWidth: 0,
3743
+ gap: "4px",
3744
+ padding: "12px",
3745
+ borderRadius: "12px 12px 12px 4px",
3746
+ backgroundColor: t.bgSecondary
3747
+ },
3748
+ children: [
3749
+ [0, 1, 2].map((i) => /* @__PURE__ */ jsx3(
3750
+ "span",
3751
+ {
3752
+ style: {
3753
+ width: "5px",
3754
+ height: "5px",
3755
+ borderRadius: "50%",
3756
+ backgroundColor: t.textMuted,
3757
+ animation: `gg-dot 1.1s ease-in-out ${i * 0.15}s infinite`
3758
+ }
3759
+ },
3760
+ i
3761
+ )),
3762
+ /* @__PURE__ */ jsx3(ThinkingClock, { color: t.textMuted })
3763
+ ]
3764
+ }
3765
+ )
3766
+ ] }),
3767
+ phase === "draft" && !submitted && /* @__PURE__ */ jsxs3(
3768
+ "div",
3769
+ {
3770
+ style: {
3771
+ border: `1px solid ${t.accent}`,
3772
+ borderRadius: "12px",
3773
+ backgroundColor: t.bgSecondary,
3774
+ padding: "12px",
3775
+ display: "flex",
3776
+ flexDirection: "column",
3777
+ gap: "10px",
3778
+ minWidth: 0,
3779
+ boxSizing: "border-box"
3780
+ },
3781
+ children: [
3782
+ /* @__PURE__ */ jsx3(
3042
3783
  "span",
3043
3784
  {
3044
- style: {
3045
- fontSize: "11.5px",
3046
- color: t.textMuted,
3047
- display: "block",
3048
- marginBottom: "6px"
3785
+ style: {
3786
+ fontSize: "10px",
3787
+ fontWeight: 700,
3788
+ letterSpacing: ".06em",
3789
+ textTransform: "uppercase",
3790
+ color: t.accent
3791
+ },
3792
+ children: duplicate ? `What we'll add to #${duplicate.number}${duplicate.kind === "related" ? " as a related request" : ""} \xB7 edit anything` : "Your report \xB7 edit anything"
3793
+ }
3794
+ ),
3795
+ renderKnownIssue({
3796
+ title: true,
3797
+ footer: "Your details will be added to it instead of opening a new issue."
3798
+ }),
3799
+ !duplicate && issueTitle && // Wraps rather than scrolls sideways: a title cut off at the
3800
+ // box edge is the very thing #543 is about. One line of text,
3801
+ // so Enter is swallowed.
3802
+ /* @__PURE__ */ jsx3(
3803
+ "textarea",
3804
+ {
3805
+ "aria-label": "Title",
3806
+ value: issueTitle,
3807
+ maxLength: 120,
3808
+ rows: 2,
3809
+ onKeyDown: (e) => {
3810
+ if (e.key === "Enter") e.preventDefault();
3049
3811
  },
3050
- children: [
3051
- "How bad is it?",
3052
- !severity && /* @__PURE__ */ jsx3(
3053
- "span",
3054
- {
3055
- style: {
3056
- marginLeft: "6px",
3057
- fontSize: "11px",
3058
- color: severityRefused ? "#ef4444" : t.textMuted
3059
- },
3060
- children: "\xB7 required"
3061
- }
3062
- )
3063
- ]
3812
+ onChange: (e) => {
3813
+ stopAutoFile("edited the title");
3814
+ onIssueTitleChange(e.target.value.replace(/[\r\n]+/g, " "));
3815
+ },
3816
+ style: {
3817
+ width: "100%",
3818
+ padding: "8px 10px",
3819
+ lineHeight: 1.45,
3820
+ resize: "none",
3821
+ borderRadius: "8px",
3822
+ border: `1px solid ${t.inputBorder}`,
3823
+ backgroundColor: t.inputBg,
3824
+ color: t.text,
3825
+ fontSize: "13px",
3826
+ fontWeight: 600,
3827
+ fontFamily: "inherit",
3828
+ outline: "none",
3829
+ boxSizing: "border-box"
3830
+ }
3064
3831
  }
3065
3832
  ),
3066
3833
  /* @__PURE__ */ jsx3(
3067
- "div",
3834
+ "textarea",
3068
3835
  {
3069
- role: "radiogroup",
3070
- "aria-label": "Severity",
3071
- style: { display: "flex", gap: "6px" },
3072
- children: SEVERITY_LEVELS.map((s) => {
3073
- const on = severity === s;
3074
- return /* @__PURE__ */ jsx3(
3075
- "button",
3076
- {
3077
- type: "button",
3078
- onClick: () => onSeverityChange(s),
3079
- role: "radio",
3080
- "aria-checked": on,
3081
- style: {
3082
- flex: 1,
3083
- minWidth: 0,
3084
- padding: "8px 2px",
3085
- borderRadius: "8px",
3086
- border: `1px solid ${on ? t.accent : severityRefused ? "#ef4444" : t.inputBorder}`,
3087
- backgroundColor: on ? t.accent : "transparent",
3088
- color: on ? t.accentText : t.textMuted,
3089
- fontSize: "12px",
3090
- fontWeight: 600,
3091
- fontFamily: "inherit",
3092
- whiteSpace: "nowrap",
3093
- overflow: "hidden",
3094
- textOverflow: "ellipsis",
3095
- cursor: "pointer"
3096
- },
3097
- children: SEVERITY_LABELS[s]
3098
- },
3099
- s
3100
- );
3101
- })
3836
+ ref: draftRef,
3837
+ value: description,
3838
+ onChange: (e) => {
3839
+ stopAutoFile("edited the draft");
3840
+ onDescriptionChange(e.target.value);
3841
+ },
3842
+ rows: 6,
3843
+ style: {
3844
+ width: "100%",
3845
+ minHeight: "120px",
3846
+ padding: "10px",
3847
+ borderRadius: "8px",
3848
+ border: `1px solid ${t.inputBorder}`,
3849
+ backgroundColor: t.inputBg,
3850
+ color: t.text,
3851
+ fontSize: "13px",
3852
+ lineHeight: 1.55,
3853
+ fontFamily: "inherit",
3854
+ resize: "vertical",
3855
+ outline: "none",
3856
+ boxSizing: "border-box"
3857
+ }
3102
3858
  }
3103
- )
3104
- ] }),
3105
- attachmentCount > 0 && /* @__PURE__ */ jsxs3("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
3106
- attachmentCount,
3107
- " ",
3108
- attachmentCount === 1 ? "attachment" : "attachments",
3109
- " will be sent with this."
3110
- ] })
3111
- ]
3112
- }
3113
- ),
3114
- phase === "solved" && solved && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3115
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
3116
- /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
3859
+ ),
3860
+ severityAsked && autoFileLeft === null && /* @__PURE__ */ jsxs3("div", { children: [
3861
+ /* @__PURE__ */ jsxs3(
3862
+ "span",
3863
+ {
3864
+ style: {
3865
+ fontSize: "11.5px",
3866
+ color: t.textMuted,
3867
+ display: "block",
3868
+ marginBottom: "6px"
3869
+ },
3870
+ children: [
3871
+ "How bad is it?",
3872
+ !severity && /* @__PURE__ */ jsx3(
3873
+ "span",
3874
+ {
3875
+ style: {
3876
+ marginLeft: "6px",
3877
+ fontSize: "11px",
3878
+ color: severityRefused ? "#ef4444" : t.textMuted
3879
+ },
3880
+ children: "\xB7 required"
3881
+ }
3882
+ )
3883
+ ]
3884
+ }
3885
+ ),
3886
+ /* @__PURE__ */ jsx3(
3887
+ "div",
3888
+ {
3889
+ role: "radiogroup",
3890
+ "aria-label": "Severity",
3891
+ style: { display: "flex", gap: "6px" },
3892
+ children: SEVERITY_LEVELS.map((s) => {
3893
+ const on = severity === s;
3894
+ return /* @__PURE__ */ jsx3(
3895
+ "button",
3896
+ {
3897
+ type: "button",
3898
+ onClick: () => onSeverityChange(s),
3899
+ role: "radio",
3900
+ "aria-checked": on,
3901
+ style: {
3902
+ flex: 1,
3903
+ minWidth: 0,
3904
+ padding: "8px 2px",
3905
+ borderRadius: "8px",
3906
+ border: `1px solid ${on ? t.accent : severityRefused ? "#ef4444" : t.inputBorder}`,
3907
+ backgroundColor: on ? t.accent : "transparent",
3908
+ color: on ? t.accentText : t.textMuted,
3909
+ fontSize: "12px",
3910
+ fontWeight: 600,
3911
+ fontFamily: "inherit",
3912
+ whiteSpace: "nowrap",
3913
+ overflow: "hidden",
3914
+ textOverflow: "ellipsis",
3915
+ cursor: "pointer"
3916
+ },
3917
+ children: SEVERITY_LABELS[s]
3918
+ },
3919
+ s
3920
+ );
3921
+ })
3922
+ }
3923
+ )
3924
+ ] }),
3925
+ attachmentCount > 0 && /* @__PURE__ */ jsxs3("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
3926
+ attachmentCount,
3927
+ " ",
3928
+ attachmentCount === 1 ? "attachment" : "attachments",
3929
+ " will be sent with this."
3930
+ ] })
3931
+ ]
3932
+ }
3933
+ ),
3934
+ phase === "solved" && solved && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3935
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
3936
+ /* @__PURE__ */ jsx3(Avatar, { who: "assistant", theme: t }),
3937
+ /* @__PURE__ */ jsx3(
3938
+ "div",
3939
+ {
3940
+ style: {
3941
+ padding: "9px 12px",
3942
+ borderRadius: "12px 12px 12px 4px",
3943
+ backgroundColor: t.bgSecondary,
3944
+ fontSize: "13px",
3945
+ lineHeight: 1.55,
3946
+ maxWidth: "88%",
3947
+ whiteSpace: "pre-wrap"
3948
+ },
3949
+ children: solved
3950
+ }
3951
+ )
3952
+ ] }),
3117
3953
  /* @__PURE__ */ jsx3(
3118
- "div",
3954
+ "span",
3119
3955
  {
3120
- style: {
3121
- padding: "9px 12px",
3122
- borderRadius: "12px 12px 12px 4px",
3123
- backgroundColor: t.bgSecondary,
3124
- fontSize: "13px",
3125
- lineHeight: 1.55,
3126
- maxWidth: "88%",
3127
- whiteSpace: "pre-wrap"
3128
- },
3129
- children: solved
3956
+ style: { fontSize: "11.5px", color: t.textMuted, paddingLeft: "32px" },
3957
+ children: "Nothing was filed \u2014 you are all set."
3130
3958
  }
3131
3959
  )
3132
3960
  ] }),
3133
- /* @__PURE__ */ jsx3(
3134
- "span",
3961
+ submitted && /* @__PURE__ */ jsx3(
3962
+ "div",
3135
3963
  {
3136
- style: { fontSize: "11.5px", color: t.textMuted, paddingLeft: "32px" },
3137
- children: "Nothing was filed \u2014 you are all set."
3964
+ style: {
3965
+ textAlign: "center",
3966
+ padding: "24px 0",
3967
+ color: t.accent,
3968
+ fontSize: "14px",
3969
+ fontWeight: 600
3970
+ },
3971
+ children: submittedText != null ? submittedText : `${reportTypeLabel} sent. Thank you!`
3138
3972
  }
3139
3973
  )
3140
- ] }),
3141
- submitted && /* @__PURE__ */ jsxs3(
3142
- "div",
3143
- {
3144
- style: {
3145
- textAlign: "center",
3146
- padding: "24px 0",
3147
- color: t.accent,
3148
- fontSize: "14px",
3149
- fontWeight: 600
3150
- },
3151
- children: [
3152
- reportTypeLabel,
3153
- " sent. Thank you!"
3154
- ]
3155
- }
3156
- )
3157
- ]
3158
- }
3159
- )
3974
+ ] })
3975
+ }
3976
+ )
3977
+ ]
3160
3978
  }
3161
3979
  ),
3162
- !submitted && phase !== "type" && phase !== "severity" && /* @__PURE__ */ jsxs3(
3980
+ !submitted && !showChats && phase !== "type" && phase !== "severity" && /* @__PURE__ */ jsxs3(
3163
3981
  "div",
3164
3982
  {
3165
3983
  style: {
@@ -3217,342 +4035,363 @@ function AssistSheet({
3217
4035
  // instead of remounting, and a draft is something you can
3218
4036
  // still reply to (#360), not a form that replaced the chat.
3219
4037
  /* @__PURE__ */ jsxs3(Fragment2, { children: [
3220
- (screenshots.length > 0 || files.length > 0 || onAddFiles) && /* @__PURE__ */ jsxs3(
3221
- "div",
3222
- {
3223
- style: {
3224
- display: "flex",
3225
- gap: "8px",
3226
- alignItems: "center",
3227
- flexWrap: "wrap",
3228
- marginBottom: "8px"
3229
- },
3230
- children: [
3231
- screenshots.map((shot, i) => {
3232
- const read = readShots.has(shot);
3233
- return /* @__PURE__ */ jsxs3(
3234
- "div",
3235
- {
3236
- style: {
3237
- position: "relative",
3238
- width: "40px",
3239
- height: "40px",
3240
- flexShrink: 0
3241
- },
3242
- children: [
3243
- /* @__PURE__ */ jsx3(
3244
- "img",
3245
- {
3246
- src: shot,
3247
- alt: read ? "Attached image the assistant reads" : "Attached image",
3248
- "data-gg-assist-thumb": "",
3249
- style: {
3250
- display: "block",
3251
- width: "100%",
3252
- height: "100%",
3253
- objectFit: "cover",
3254
- borderRadius: "8px",
3255
- border: `1px solid ${read ? t.accent : t.inputBorder}`,
3256
- opacity: read ? 1 : 0.45,
3257
- boxSizing: "border-box"
4038
+ /* @__PURE__ */ jsxs3("div", { style: { display: similarSlot ? "none" : "contents" }, children: [
4039
+ (screenshots.length > 0 || files.length > 0 || onAddFiles) && /* @__PURE__ */ jsxs3(
4040
+ "div",
4041
+ {
4042
+ style: {
4043
+ display: "flex",
4044
+ gap: "8px",
4045
+ alignItems: "center",
4046
+ flexWrap: "wrap",
4047
+ marginBottom: "8px"
4048
+ },
4049
+ children: [
4050
+ screenshots.map((shot, i) => {
4051
+ const read = readShots.has(shot);
4052
+ return /* @__PURE__ */ jsxs3(
4053
+ "div",
4054
+ {
4055
+ style: {
4056
+ position: "relative",
4057
+ width: "40px",
4058
+ height: "40px",
4059
+ flexShrink: 0
4060
+ },
4061
+ children: [
4062
+ /* @__PURE__ */ jsx3(
4063
+ "img",
4064
+ {
4065
+ src: shot,
4066
+ alt: read ? "Attached image the assistant reads" : "Attached image",
4067
+ "data-gg-assist-thumb": "",
4068
+ style: {
4069
+ display: "block",
4070
+ width: "100%",
4071
+ height: "100%",
4072
+ objectFit: "cover",
4073
+ borderRadius: "8px",
4074
+ border: `1px solid ${read ? t.accent : t.inputBorder}`,
4075
+ opacity: read ? 1 : 0.45,
4076
+ boxSizing: "border-box"
4077
+ }
3258
4078
  }
3259
- }
3260
- ),
3261
- onRemoveImage && /* @__PURE__ */ jsx3(
3262
- "button",
3263
- {
3264
- type: "button",
3265
- "data-gg-assist-remove": "",
3266
- "aria-label": `Remove image ${i + 1}`,
3267
- title: "Remove this image",
3268
- onClick: () => {
3269
- stopAutoFile("changed attachments");
3270
- logEvent(
3271
- "image_removed",
3272
- pageShot && shot === pageShot ? "page capture" : "attached"
3273
- );
3274
- onRemoveImage(i);
3275
- },
3276
- style: {
3277
- position: "absolute",
3278
- top: "-13px",
3279
- right: "-13px",
3280
- width: "32px",
3281
- height: "32px",
3282
- border: "none",
3283
- background: "transparent",
3284
- padding: 0,
3285
- cursor: "pointer",
3286
- display: "flex",
3287
- alignItems: "center",
3288
- justifyContent: "center",
3289
- borderRadius: "50%"
3290
- },
3291
- children: /* @__PURE__ */ jsx3(
3292
- "span",
3293
- {
3294
- "aria-hidden": "true",
3295
- style: {
3296
- width: "20px",
3297
- height: "20px",
3298
- boxSizing: "border-box",
3299
- borderRadius: "50%",
3300
- border: `2px solid ${t.bg}`,
3301
- background: "#ef4444",
3302
- color: "#fff",
3303
- display: "flex",
3304
- alignItems: "center",
3305
- justifyContent: "center"
3306
- },
3307
- children: /* @__PURE__ */ jsx3("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ jsx3(
3308
- "path",
3309
- {
3310
- d: "M1 1L9 9M9 1L1 9",
3311
- stroke: "currentColor",
3312
- strokeWidth: "1.6",
3313
- strokeLinecap: "round"
3314
- }
3315
- ) })
4079
+ ),
4080
+ onRemoveImage && /* @__PURE__ */ jsx3(
4081
+ "button",
4082
+ {
4083
+ type: "button",
4084
+ "data-gg-assist-remove": "",
4085
+ "aria-label": `Remove image ${i + 1}`,
4086
+ title: "Remove this image",
4087
+ onClick: () => {
4088
+ stopAutoFile("changed attachments");
4089
+ logEvent(
4090
+ "image_removed",
4091
+ pageShot && shot === pageShot ? "page capture" : "attached"
4092
+ );
4093
+ onRemoveImage(i);
4094
+ },
4095
+ style: {
4096
+ position: "absolute",
4097
+ top: "-13px",
4098
+ right: "-13px",
4099
+ width: "32px",
4100
+ height: "32px",
4101
+ border: "none",
4102
+ background: "transparent",
4103
+ padding: 0,
4104
+ cursor: "pointer",
4105
+ display: "flex",
4106
+ alignItems: "center",
4107
+ justifyContent: "center",
4108
+ borderRadius: "50%"
4109
+ },
4110
+ children: /* @__PURE__ */ jsx3(
4111
+ "span",
4112
+ {
4113
+ "aria-hidden": "true",
4114
+ style: {
4115
+ width: "20px",
4116
+ height: "20px",
4117
+ boxSizing: "border-box",
4118
+ borderRadius: "50%",
4119
+ border: `2px solid ${t.bg}`,
4120
+ background: "#ef4444",
4121
+ color: "#fff",
4122
+ display: "flex",
4123
+ alignItems: "center",
4124
+ justifyContent: "center"
4125
+ },
4126
+ children: /* @__PURE__ */ jsx3("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ jsx3(
4127
+ "path",
4128
+ {
4129
+ d: "M1 1L9 9M9 1L1 9",
4130
+ stroke: "currentColor",
4131
+ strokeWidth: "1.6",
4132
+ strokeLinecap: "round"
4133
+ }
4134
+ ) })
4135
+ }
4136
+ )
4137
+ }
4138
+ )
4139
+ ]
4140
+ },
4141
+ `${i}-${shot.slice(-16)}`
4142
+ );
4143
+ }),
4144
+ files.map((file, i) => {
4145
+ const read = readFiles.has(i);
4146
+ const pdf = isPdfFile(file.name);
4147
+ const why = read ? "the assistant reads this file" : pdf && file.reading ? "reading the text in this PDF\u2026" : pdf && file.text !== void 0 ? file.text === null ? file.tooLarge ? "attached to the report \u2014 too large for the assistant to read (over 3 MB)" : "attached to the report \u2014 couldn't read this PDF" : file.text.trim() ? "attached, but more than one message can carry, so the assistant is not reading it" : "attached to the report \u2014 this PDF has no text to read (a scan?)" : isAssistReadableFile(file.name) ? "attached, but more than one message can carry, so the assistant is not reading it" : "attached to the report \u2014 the assistant can't read this kind of file";
4148
+ return /* @__PURE__ */ jsxs3(
4149
+ "div",
4150
+ {
4151
+ "data-gg-assist-file": "",
4152
+ "data-gg-assist-file-read": read ? "" : void 0,
4153
+ "data-gg-assist-file-reading": pdf && file.reading ? "" : void 0,
4154
+ title: `${file.name} \u2014 ${why}`,
4155
+ style: {
4156
+ position: "relative",
4157
+ height: "40px",
4158
+ maxWidth: "170px",
4159
+ display: "flex",
4160
+ alignItems: "center",
4161
+ gap: "6px",
4162
+ padding: "0 10px",
4163
+ borderRadius: "8px",
4164
+ border: `1px solid ${read ? t.accent : t.inputBorder}`,
4165
+ opacity: read || pdf && file.reading ? 1 : 0.55,
4166
+ boxSizing: "border-box",
4167
+ flexShrink: 0
4168
+ },
4169
+ children: [
4170
+ pdf && file.reading ? /* @__PURE__ */ jsx3(
4171
+ "span",
4172
+ {
4173
+ role: "status",
4174
+ "aria-label": "Reading this PDF",
4175
+ style: {
4176
+ width: "12px",
4177
+ height: "12px",
4178
+ flexShrink: 0,
4179
+ borderRadius: "50%",
4180
+ border: `1.5px solid ${t.inputBorder}`,
4181
+ borderTopColor: t.accent,
4182
+ boxSizing: "border-box",
4183
+ animation: "gg-spin .8s linear infinite"
3316
4184
  }
3317
- )
3318
- }
3319
- )
3320
- ]
3321
- },
3322
- `${i}-${shot.slice(-16)}`
3323
- );
3324
- }),
3325
- files.map((file, i) => {
3326
- const read = readFiles.has(i);
3327
- const why = read ? "the assistant reads this file" : isAssistReadableFile(file.name) ? "attached, but more than one message can carry, so the assistant is not reading it" : "attached to the report \u2014 the assistant can't read this kind of file";
3328
- return /* @__PURE__ */ jsxs3(
3329
- "div",
4185
+ }
4186
+ ) : /* @__PURE__ */ jsx3("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsx3(
4187
+ "path",
4188
+ {
4189
+ d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z M14 2v6h6",
4190
+ stroke: t.textMuted,
4191
+ strokeWidth: "1.5",
4192
+ strokeLinecap: "round",
4193
+ strokeLinejoin: "round"
4194
+ }
4195
+ ) }),
4196
+ /* @__PURE__ */ jsx3(
4197
+ "span",
4198
+ {
4199
+ style: {
4200
+ fontSize: "11.5px",
4201
+ color: t.text,
4202
+ overflow: "hidden",
4203
+ textOverflow: "ellipsis",
4204
+ whiteSpace: "nowrap"
4205
+ },
4206
+ children: file.name
4207
+ }
4208
+ ),
4209
+ onRemoveFile && /* @__PURE__ */ jsx3(
4210
+ "button",
4211
+ {
4212
+ type: "button",
4213
+ "data-gg-assist-remove-file": "",
4214
+ "aria-label": `Remove file ${file.name}`,
4215
+ title: "Remove this file",
4216
+ onClick: () => {
4217
+ stopAutoFile("changed attachments");
4218
+ logEvent("file_removed", read ? "read" : "not read");
4219
+ onRemoveFile(i);
4220
+ },
4221
+ style: {
4222
+ position: "absolute",
4223
+ top: "-13px",
4224
+ right: "-13px",
4225
+ width: "32px",
4226
+ height: "32px",
4227
+ border: "none",
4228
+ background: "transparent",
4229
+ padding: 0,
4230
+ cursor: "pointer",
4231
+ display: "flex",
4232
+ alignItems: "center",
4233
+ justifyContent: "center",
4234
+ borderRadius: "50%"
4235
+ },
4236
+ children: /* @__PURE__ */ jsx3(
4237
+ "span",
4238
+ {
4239
+ "aria-hidden": "true",
4240
+ style: {
4241
+ width: "20px",
4242
+ height: "20px",
4243
+ boxSizing: "border-box",
4244
+ borderRadius: "50%",
4245
+ border: `2px solid ${t.bg}`,
4246
+ background: "#ef4444",
4247
+ color: "#fff",
4248
+ display: "flex",
4249
+ alignItems: "center",
4250
+ justifyContent: "center"
4251
+ },
4252
+ children: /* @__PURE__ */ jsx3("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ jsx3(
4253
+ "path",
4254
+ {
4255
+ d: "M1 1L9 9M9 1L1 9",
4256
+ stroke: "currentColor",
4257
+ strokeWidth: "1.6",
4258
+ strokeLinecap: "round"
4259
+ }
4260
+ ) })
4261
+ }
4262
+ )
4263
+ }
4264
+ )
4265
+ ]
4266
+ },
4267
+ `file-${i}-${file.name}`
4268
+ );
4269
+ }),
4270
+ onAddFiles && /* @__PURE__ */ jsxs3(
4271
+ "label",
3330
4272
  {
3331
- "data-gg-assist-file": "",
3332
- "data-gg-assist-file-read": read ? "" : void 0,
3333
- title: `${file.name} \u2014 ${why}`,
4273
+ "data-gg-assist-attach": "",
4274
+ title: "Attach an image or a file (html, json, log\u2026) \u2014 or paste one",
3334
4275
  style: {
3335
- position: "relative",
4276
+ width: "40px",
3336
4277
  height: "40px",
3337
- maxWidth: "170px",
3338
- display: "flex",
3339
- alignItems: "center",
3340
- gap: "6px",
3341
- padding: "0 10px",
3342
4278
  borderRadius: "8px",
3343
- border: `1px solid ${read ? t.accent : t.inputBorder}`,
3344
- opacity: read ? 1 : 0.55,
3345
- boxSizing: "border-box",
3346
- flexShrink: 0
3347
- },
3348
- children: [
3349
- /* @__PURE__ */ jsx3("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsx3(
3350
- "path",
3351
- {
3352
- d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z M14 2v6h6",
3353
- stroke: t.textMuted,
3354
- strokeWidth: "1.5",
3355
- strokeLinecap: "round",
3356
- strokeLinejoin: "round"
3357
- }
3358
- ) }),
3359
- /* @__PURE__ */ jsx3(
3360
- "span",
3361
- {
3362
- style: {
3363
- fontSize: "11.5px",
3364
- color: t.text,
3365
- overflow: "hidden",
3366
- textOverflow: "ellipsis",
3367
- whiteSpace: "nowrap"
3368
- },
3369
- children: file.name
3370
- }
3371
- ),
3372
- onRemoveFile && /* @__PURE__ */ jsx3(
3373
- "button",
3374
- {
3375
- type: "button",
3376
- "data-gg-assist-remove-file": "",
3377
- "aria-label": `Remove file ${file.name}`,
3378
- title: "Remove this file",
3379
- onClick: () => {
3380
- stopAutoFile("changed attachments");
3381
- logEvent("file_removed", read ? "read" : "not read");
3382
- onRemoveFile(i);
3383
- },
3384
- style: {
3385
- position: "absolute",
3386
- top: "-13px",
3387
- right: "-13px",
3388
- width: "32px",
3389
- height: "32px",
3390
- border: "none",
3391
- background: "transparent",
3392
- padding: 0,
3393
- cursor: "pointer",
3394
- display: "flex",
3395
- alignItems: "center",
3396
- justifyContent: "center",
3397
- borderRadius: "50%"
3398
- },
3399
- children: /* @__PURE__ */ jsx3(
3400
- "span",
3401
- {
3402
- "aria-hidden": "true",
3403
- style: {
3404
- width: "20px",
3405
- height: "20px",
3406
- boxSizing: "border-box",
3407
- borderRadius: "50%",
3408
- border: `2px solid ${t.bg}`,
3409
- background: "#ef4444",
3410
- color: "#fff",
3411
- display: "flex",
3412
- alignItems: "center",
3413
- justifyContent: "center"
3414
- },
3415
- children: /* @__PURE__ */ jsx3("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ jsx3(
3416
- "path",
3417
- {
3418
- d: "M1 1L9 9M9 1L1 9",
3419
- stroke: "currentColor",
3420
- strokeWidth: "1.6",
3421
- strokeLinecap: "round"
3422
- }
3423
- ) })
3424
- }
3425
- )
3426
- }
3427
- )
3428
- ]
3429
- },
3430
- `file-${i}-${file.name}`
3431
- );
3432
- }),
3433
- onAddFiles && /* @__PURE__ */ jsxs3(
3434
- "label",
3435
- {
3436
- "data-gg-assist-attach": "",
3437
- title: "Attach an image or a file (html, json, log\u2026) \u2014 or paste one",
3438
- style: {
3439
- width: "40px",
3440
- height: "40px",
3441
- borderRadius: "8px",
3442
- border: `1px dashed ${t.inputBorder}`,
3443
- display: "inline-flex",
3444
- alignItems: "center",
3445
- justifyContent: "center",
3446
- color: t.textMuted,
3447
- fontSize: "18px",
3448
- cursor: "pointer",
3449
- flexShrink: 0
3450
- },
3451
- children: [
3452
- "+",
3453
- /* @__PURE__ */ jsx3(
3454
- "input",
3455
- {
3456
- type: "file",
3457
- accept: ASSIST_ATTACHMENT_ACCEPT,
3458
- multiple: true,
3459
- onChange: (e) => {
3460
- var _a;
3461
- const picked = Array.from((_a = e.target.files) != null ? _a : []);
3462
- if (picked.length) {
3463
- stopAutoFile("changed attachments");
3464
- const images = picked.filter((f) => f.type.startsWith("image/")).length;
3465
- if (images) logEvent("image_added", String(images));
3466
- if (picked.length > images) {
3467
- logEvent("file_added", String(picked.length - images));
4279
+ border: `1px dashed ${t.inputBorder}`,
4280
+ display: "inline-flex",
4281
+ alignItems: "center",
4282
+ justifyContent: "center",
4283
+ color: t.textMuted,
4284
+ fontSize: "18px",
4285
+ cursor: "pointer",
4286
+ flexShrink: 0
4287
+ },
4288
+ children: [
4289
+ "+",
4290
+ /* @__PURE__ */ jsx3(
4291
+ "input",
4292
+ {
4293
+ type: "file",
4294
+ accept: canReadPdf ? ASSIST_ATTACHMENT_ACCEPT_WITH_PDF : ASSIST_ATTACHMENT_ACCEPT,
4295
+ multiple: true,
4296
+ onChange: (e) => {
4297
+ var _a2;
4298
+ const picked = Array.from((_a2 = e.target.files) != null ? _a2 : []);
4299
+ if (picked.length) {
4300
+ stopAutoFile("changed attachments");
4301
+ const images = picked.filter((f) => f.type.startsWith("image/")).length;
4302
+ if (images) logEvent("image_added", String(images));
4303
+ if (picked.length > images) {
4304
+ logEvent("file_added", String(picked.length - images));
4305
+ }
4306
+ onAddFiles(picked);
3468
4307
  }
3469
- onAddFiles(picked);
3470
- }
3471
- e.target.value = "";
3472
- },
3473
- style: { display: "none" }
3474
- }
3475
- )
3476
- ]
3477
- }
3478
- ),
3479
- readShots.size < screenshots.length && /* @__PURE__ */ jsxs3("span", { style: { color: t.textMuted, fontSize: "11px" }, children: [
3480
- readShots.size,
3481
- " of ",
3482
- screenshots.length,
3483
- " read"
3484
- ] })
3485
- ]
3486
- }
3487
- ),
3488
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px", alignItems: "flex-end" }, children: [
3489
- /* @__PURE__ */ jsx3(
3490
- "textarea",
3491
- {
3492
- value: input,
3493
- onChange: (e) => {
3494
- stopAutoFile("typing a change");
3495
- setInput(e.target.value);
3496
- },
3497
- onKeyDown: (e) => {
3498
- if (e.key === "Enter" && !e.shiftKey) {
3499
- e.preventDefault();
3500
- sendTyped(input);
3501
- }
3502
- },
3503
- placeholder: phase === "draft" ? "Anything to change? Tell me here\u2026" : "Type your answer\u2026",
3504
- rows: 1,
3505
- disabled: busy,
3506
- autoFocus: true,
3507
- style: {
3508
- flex: 1,
3509
- minHeight: "42px",
3510
- maxHeight: "120px",
3511
- padding: "11px 12px",
3512
- borderRadius: "10px",
3513
- border: `1px solid ${t.inputBorder}`,
3514
- backgroundColor: t.inputBg,
3515
- color: t.text,
3516
- fontSize: "13px",
3517
- lineHeight: 1.4,
3518
- fontFamily: "inherit",
3519
- resize: "none",
3520
- outline: "none",
3521
- boxSizing: "border-box"
3522
- }
4308
+ e.target.value = "";
4309
+ },
4310
+ style: { display: "none" }
4311
+ }
4312
+ )
4313
+ ]
4314
+ }
4315
+ ),
4316
+ readShots.size < screenshots.length && /* @__PURE__ */ jsxs3("span", { style: { color: t.textMuted, fontSize: "11px" }, children: [
4317
+ readShots.size,
4318
+ " of ",
4319
+ screenshots.length,
4320
+ " read"
4321
+ ] })
4322
+ ]
3523
4323
  }
3524
4324
  ),
3525
- /* @__PURE__ */ jsx3(
3526
- "button",
3527
- {
3528
- type: "button",
3529
- "data-gg-sheet-send": "",
3530
- onClick: () => sendTyped(input),
3531
- disabled: busy || !input.trim(),
3532
- "aria-label": "Send message",
3533
- "data-gg-send-message": "",
3534
- style: {
3535
- flexShrink: 0,
3536
- width: "42px",
3537
- height: "42px",
3538
- borderRadius: "10px",
3539
- border: "none",
3540
- display: "inline-flex",
3541
- alignItems: "center",
3542
- justifyContent: "center",
3543
- backgroundColor: busy || !input.trim() ? t.bgSecondary : t.accent,
3544
- color: busy || !input.trim() ? t.textMuted : t.accentText,
3545
- cursor: busy || !input.trim() ? "default" : "pointer"
3546
- },
3547
- children: /* @__PURE__ */ jsx3("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
3548
- "path",
3549
- {
3550
- d: "M3 20.5L21.5 12 3 3.5V10l12 2-12 2v6.5z",
3551
- fill: "currentColor"
4325
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px", alignItems: "flex-end" }, children: [
4326
+ /* @__PURE__ */ jsx3(
4327
+ "textarea",
4328
+ {
4329
+ ref: composerRef,
4330
+ value: input,
4331
+ onChange: (e) => {
4332
+ stopAutoFile("typing a change");
4333
+ setInput(e.target.value);
4334
+ },
4335
+ onKeyDown: (e) => {
4336
+ if (e.key === "Enter" && !e.shiftKey) {
4337
+ e.preventDefault();
4338
+ sendTyped(input);
4339
+ }
4340
+ },
4341
+ placeholder: phase === "draft" ? "Anything to change? Tell me here\u2026" : "Type your answer\u2026",
4342
+ rows: 3,
4343
+ disabled: busy,
4344
+ autoFocus: true,
4345
+ style: {
4346
+ flex: 1,
4347
+ minHeight: "72px",
4348
+ maxHeight: "220px",
4349
+ padding: "11px 12px",
4350
+ borderRadius: "10px",
4351
+ border: `1px solid ${t.inputBorder}`,
4352
+ backgroundColor: t.inputBg,
4353
+ color: t.text,
4354
+ fontSize: "13px",
4355
+ lineHeight: 1.4,
4356
+ fontFamily: "inherit",
4357
+ resize: "none",
4358
+ outline: "none",
4359
+ boxSizing: "border-box"
3552
4360
  }
3553
- ) })
3554
- }
3555
- )
4361
+ }
4362
+ ),
4363
+ /* @__PURE__ */ jsx3(
4364
+ "button",
4365
+ {
4366
+ type: "button",
4367
+ "data-gg-sheet-send": "",
4368
+ onClick: () => sendTyped(input),
4369
+ disabled: busy || !input.trim(),
4370
+ "aria-label": "Send message",
4371
+ "data-gg-send-message": "",
4372
+ style: {
4373
+ flexShrink: 0,
4374
+ width: "42px",
4375
+ height: "42px",
4376
+ borderRadius: "10px",
4377
+ border: "none",
4378
+ display: "inline-flex",
4379
+ alignItems: "center",
4380
+ justifyContent: "center",
4381
+ backgroundColor: busy || !input.trim() ? t.bgSecondary : t.accent,
4382
+ color: busy || !input.trim() ? t.textMuted : t.accentText,
4383
+ cursor: busy || !input.trim() ? "default" : "pointer"
4384
+ },
4385
+ children: /* @__PURE__ */ jsx3("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
4386
+ "path",
4387
+ {
4388
+ d: "M3 20.5L21.5 12 3 3.5V10l12 2-12 2v6.5z",
4389
+ fill: "currentColor"
4390
+ }
4391
+ ) })
4392
+ }
4393
+ )
4394
+ ] })
3556
4395
  ] }),
3557
4396
  phase === "draft" && /* @__PURE__ */ jsxs3(Fragment2, { children: [
3558
4397
  validationError && /* @__PURE__ */ jsx3(
@@ -3566,7 +4405,7 @@ function AssistSheet({
3566
4405
  children: validationError
3567
4406
  }
3568
4407
  ),
3569
- autoFileLeft !== null ? (
4408
+ similarSlot ? similarSlot : autoFileLeft !== null ? (
3570
4409
  // #402: "Yes, that's it" was the confirmation. The draft
3571
4410
  // files itself unless they stop it — the bar shows how
3572
4411
  // long is left, the button sends now, the × on its right
@@ -4434,20 +5273,20 @@ function initialsOf(name) {
4434
5273
  }).join("");
4435
5274
  }
4436
5275
  function ReporterChip({
4437
- reporter,
5276
+ reporter: reporter2,
4438
5277
  t
4439
5278
  }) {
4440
5279
  var _a;
4441
5280
  const [avatarFailed, setAvatarFailed] = useState4(false);
4442
- const avatarUrl = (reporter == null ? void 0 : reporter.avatarUrl) || "";
5281
+ const avatarUrl = (reporter2 == null ? void 0 : reporter2.avatarUrl) || "";
4443
5282
  useEffect4(() => {
4444
5283
  setAvatarFailed(false);
4445
5284
  }, [avatarUrl]);
4446
- const name = ((_a = reporter == null ? void 0 : reporter.name) == null ? void 0 : _a.trim()) || "";
5285
+ const name = ((_a = reporter2 == null ? void 0 : reporter2.name) == null ? void 0 : _a.trim()) || "";
4447
5286
  const isAnonymous = !name;
4448
5287
  const initials2 = isAnonymous ? "" : initialsOf(name);
4449
5288
  const label = isAnonymous ? "Anonymous" : name;
4450
- const title = isAnonymous ? "No reporter identity was passed to Glitchgrab \u2014 this report won't be attributed to anyone" : [name, reporter == null ? void 0 : reporter.email, reporter == null ? void 0 : reporter.role].filter(Boolean).join(" \xB7 ");
5289
+ const title = isAnonymous ? "No reporter identity was passed to Glitchgrab \u2014 this report won't be attributed to anyone" : [name, reporter2 == null ? void 0 : reporter2.email, reporter2 == null ? void 0 : reporter2.role].filter(Boolean).join(" \xB7 ");
4451
5290
  const showAvatarImage = !!avatarUrl && !avatarFailed;
4452
5291
  return /* @__PURE__ */ jsxs5(
4453
5292
  "span",
@@ -4511,7 +5350,7 @@ function ReporterChip({
4511
5350
  },
4512
5351
  children: [
4513
5352
  label,
4514
- (reporter == null ? void 0 : reporter.role) ? ` \xB7 ${reporter.role}` : ""
5353
+ (reporter2 == null ? void 0 : reporter2.role) ? ` \xB7 ${reporter2.role}` : ""
4515
5354
  ]
4516
5355
  }
4517
5356
  )
@@ -4622,16 +5461,18 @@ function ReportDialog({
4622
5461
  enhanceText: enhanceText2,
4623
5462
  assist,
4624
5463
  findSimilarIssues: findSimilarIssues2,
5464
+ readFile,
4625
5465
  assistContext = null,
4626
5466
  transcribeAudio: transcribeAudio2,
4627
5467
  types,
4628
5468
  showSeverity = true,
4629
5469
  captureScreenshot = captureDefaultScreenshot,
4630
- reporter,
5470
+ reporter: reporter2,
4631
5471
  onClose,
4632
5472
  headerSlot,
4633
5473
  reportGlitchgrabProblem,
4634
- layout = "modal"
5474
+ layout = "modal",
5475
+ chatHistoryKey: chatHistoryKey2 = null
4635
5476
  }) {
4636
5477
  var _a;
4637
5478
  const sheet = layout === "sheet";
@@ -4675,6 +5516,15 @@ function ReportDialog({
4675
5516
  const pendingShotRef = useRef4(null);
4676
5517
  const openGenRef = useRef4(0);
4677
5518
  const [attachments, setAttachments] = useState4([]);
5519
+ const [pdfTexts, setPdfTexts] = useState4({});
5520
+ const heldFiles = useMemo2(
5521
+ () => attachments.map((a) => {
5522
+ const read = pdfTexts[heldFileKey(a)];
5523
+ if (!read) return a;
5524
+ return "reading" in read ? { ...a, reading: true } : { ...a, text: read.text, ...read.tooLarge ? { tooLarge: true } : {} };
5525
+ }),
5526
+ [attachments, pdfTexts]
5527
+ );
4678
5528
  const [previewIndex, setPreviewIndex] = useState4(null);
4679
5529
  const [annotatingIndex, setAnnotatingIndex] = useState4(null);
4680
5530
  const [previewZoomed, setPreviewZoomed] = useState4(false);
@@ -4727,15 +5577,15 @@ function ReportDialog({
4727
5577
  });
4728
5578
  };
4729
5579
  applyInert();
4730
- const observer = new MutationObserver((mutations) => {
5580
+ const observer2 = new MutationObserver((mutations) => {
4731
5581
  const structural = mutations.some(
4732
5582
  (m) => m.addedNodes.length > 0 || m.removedNodes.length > 0
4733
5583
  );
4734
5584
  if (structural) applyInert();
4735
5585
  });
4736
- observer.observe(document.body, { childList: true, subtree: true });
5586
+ observer2.observe(document.body, { childList: true, subtree: true });
4737
5587
  return () => {
4738
- observer.disconnect();
5588
+ observer2.disconnect();
4739
5589
  inerted.forEach((el) => el.removeAttribute("inert"));
4740
5590
  };
4741
5591
  }, [isOpen]);
@@ -4835,10 +5685,16 @@ function ReportDialog({
4835
5685
  files.filter((file) => !file.type.startsWith("image/")).forEach((file) => {
4836
5686
  const reader = new FileReader();
4837
5687
  reader.onload = () => {
4838
- setAttachments((prev) => [
4839
- ...prev,
4840
- { name: file.name, size: file.size, dataUrl: reader.result }
4841
- ]);
5688
+ const dataUrl = reader.result;
5689
+ setAttachments((prev) => [...prev, { name: file.name, size: file.size, dataUrl }]);
5690
+ const key = heldFileKey(file);
5691
+ if (!readFile || !isPdfFile(file.name) || pdfTexts[key]) return;
5692
+ if (file.size > MAX_READABLE_PDF_BYTES) {
5693
+ setPdfTexts((prev) => ({ ...prev, [key]: { text: null, tooLarge: true } }));
5694
+ return;
5695
+ }
5696
+ setPdfTexts((prev) => ({ ...prev, [key]: { reading: true } }));
5697
+ void Promise.resolve().then(() => readFile({ name: file.name, dataUrl })).catch(() => null).then((text) => setPdfTexts((prev) => ({ ...prev, [key]: { text } })));
4842
5698
  };
4843
5699
  reader.readAsDataURL(file);
4844
5700
  });
@@ -5274,6 +6130,10 @@ function ReportDialog({
5274
6130
  stopListeningAndTranscribe();
5275
6131
  }
5276
6132
  };
6133
+ useEffect4(() => {
6134
+ setSimilarIssues(null);
6135
+ setPickedSimilar(null);
6136
+ }, [description]);
5277
6137
  const handleSubmit = async (opts) => {
5278
6138
  var _a2, _b, _c;
5279
6139
  try {
@@ -5314,11 +6174,17 @@ function ReportDialog({
5314
6174
  }
5315
6175
  const duplicateOf = (_a2 = opts == null ? void 0 : opts.duplicateOf) != null ? _a2 : duplicateIssueNumber;
5316
6176
  const attachKind = (opts == null ? void 0 : opts.duplicateOf) ? (_b = opts.kind) != null ? _b : "duplicate" : duplicateKind;
5317
- if (findSimilarIssues2 && !(opts == null ? void 0 : opts.skipSimilar) && !duplicateOf && !aiConversationId) {
6177
+ if (findSimilarIssues2 && !(opts == null ? void 0 : opts.skipSimilar) && !duplicateOf) {
6178
+ const similarFiles = filesForSimilar(
6179
+ heldFiles,
6180
+ MAX_SIMILAR_FILE_CHARS,
6181
+ MAX_SIMILAR_FILES_TOTAL_CHARS
6182
+ );
5318
6183
  setIsSubmitting(true);
5319
6184
  setCheckingSimilar(true);
5320
6185
  const matches = await Promise.race([
5321
- findSimilarIssues2(description.trim()).catch(() => null),
6186
+ // A PDF still being read is left out rather than waited for.
6187
+ (similarFiles.length ? findSimilarIssues2(description.trim(), similarFiles) : findSimilarIssues2(description.trim())).catch(() => null),
5322
6188
  new Promise((resolve) => setTimeout(() => resolve(null), SIMILAR_WAIT_MS))
5323
6189
  ]);
5324
6190
  setCheckingSimilar(false);
@@ -5350,6 +6216,8 @@ function ReportDialog({
5350
6216
  if (duplicateOf) {
5351
6217
  metadata.duplicateIssueNumber = String(duplicateOf);
5352
6218
  if (attachKind === "related") metadata.duplicateKind = "related";
6219
+ } else if (opts == null ? void 0 : opts.possiblyRelated) {
6220
+ metadata.possiblyRelatedIssue = String(opts.possiblyRelated);
5353
6221
  }
5354
6222
  if (aiConversationId) {
5355
6223
  metadata.aiConversationId = aiConversationId;
@@ -5363,14 +6231,20 @@ function ReportDialog({
5363
6231
  Object.keys(metadata).length > 0 ? metadata : void 0
5364
6232
  );
5365
6233
  if (result) {
5366
- setAddedToIssue(duplicateOf && result.issueNumber === duplicateOf ? duplicateOf : null);
6234
+ setAddedToIssue(
6235
+ duplicateOf && result.issueNumber === duplicateOf ? duplicateOf : result.status === "DUPLICATE" && result.issueNumber ? result.issueNumber : null
6236
+ );
5367
6237
  setAddedAsRelated(!!duplicateOf && result.issueNumber === duplicateOf && attachKind === "related");
5368
6238
  const queued = result.status === "QUEUED" && !!result.message;
5369
6239
  setQueuedMessage(queued ? (_c = result.message) != null ? _c : null : null);
6240
+ if (chatHistoryKey2 && aiConversationId) {
6241
+ markChatFiled(chatHistoryKey2, aiConversationId, `${getTypeLabel(reportType)} sent`);
6242
+ }
5370
6243
  setSubmitted(true);
5371
6244
  setDescription("");
5372
6245
  setScreenshots([]);
5373
6246
  setAttachments([]);
6247
+ setPdfTexts({});
5374
6248
  setAiConversationId(null);
5375
6249
  setIssueTitle("");
5376
6250
  setDuplicateIssueNumber(null);
@@ -5391,6 +6265,220 @@ function ReportDialog({
5391
6265
  setAttachingTo(null);
5392
6266
  }
5393
6267
  };
6268
+ const similarCard = similarIssues ? /* @__PURE__ */ jsxs5(
6269
+ "div",
6270
+ {
6271
+ "data-gg-similar": "",
6272
+ style: {
6273
+ marginTop: "12px",
6274
+ display: "flex",
6275
+ flexDirection: "column",
6276
+ gap: "10px",
6277
+ padding: "12px",
6278
+ borderRadius: "10px",
6279
+ border: "1px solid rgba(245,158,11,0.4)",
6280
+ backgroundColor: "rgba(245,158,11,0.08)"
6281
+ },
6282
+ children: [
6283
+ /* @__PURE__ */ jsxs5("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [
6284
+ /* @__PURE__ */ jsx5("span", { style: { color: t.text, fontWeight: 600, fontSize: "13px" }, children: "Our team may already have this, or something close" }),
6285
+ /* @__PURE__ */ jsx5("span", { style: { color: t.textMuted, fontSize: "12px", lineHeight: 1.5 }, children: similarIssues.length > 1 ? "Pick the one that matches. Same problem, or a different request for the same feature \u2014 it's added to that issue, so the team has it in one place." : "Same problem, or a different request for the same feature? Pick it \u2014 it's added to that issue, so the team has it in one place." })
6286
+ ] }),
6287
+ /* @__PURE__ */ jsx5(
6288
+ "div",
6289
+ {
6290
+ role: "radiogroup",
6291
+ "aria-label": "Existing issues",
6292
+ style: { display: "flex", flexDirection: "column", gap: "6px" },
6293
+ children: similarIssues.map((issue) => {
6294
+ const picked = pickedSimilar === issue.number;
6295
+ return /* @__PURE__ */ jsxs5(
6296
+ "button",
6297
+ {
6298
+ type: "button",
6299
+ role: "radio",
6300
+ "aria-checked": picked,
6301
+ disabled: isSubmitting,
6302
+ onClick: () => setPickedSimilar(issue.number),
6303
+ style: {
6304
+ display: "flex",
6305
+ alignItems: "flex-start",
6306
+ gap: "10px",
6307
+ width: "100%",
6308
+ minHeight: "44px",
6309
+ padding: "9px 10px",
6310
+ borderRadius: "8px",
6311
+ border: `1px solid ${picked ? t.accent : t.border}`,
6312
+ boxShadow: picked ? `0 0 0 1px ${t.accent}` : "none",
6313
+ backgroundColor: t.bg,
6314
+ color: t.text,
6315
+ textAlign: "left",
6316
+ cursor: isSubmitting ? "not-allowed" : "pointer",
6317
+ fontFamily: "inherit"
6318
+ },
6319
+ children: [
6320
+ /* @__PURE__ */ jsx5(
6321
+ "span",
6322
+ {
6323
+ "aria-hidden": "true",
6324
+ style: {
6325
+ flexShrink: 0,
6326
+ marginTop: "2px",
6327
+ width: "14px",
6328
+ height: "14px",
6329
+ boxSizing: "border-box",
6330
+ borderRadius: "50%",
6331
+ border: `2px solid ${picked ? t.accent : t.textMuted}`,
6332
+ backgroundColor: picked ? t.accent : "transparent",
6333
+ boxShadow: picked ? `inset 0 0 0 2px ${t.bg}` : "none"
6334
+ }
6335
+ }
6336
+ ),
6337
+ /* @__PURE__ */ jsxs5(
6338
+ "span",
6339
+ {
6340
+ style: {
6341
+ display: "flex",
6342
+ flexDirection: "column",
6343
+ gap: "3px",
6344
+ minWidth: 0
6345
+ },
6346
+ children: [
6347
+ /* @__PURE__ */ jsxs5(
6348
+ "span",
6349
+ {
6350
+ title: issue.title,
6351
+ style: {
6352
+ fontSize: "12.5px",
6353
+ lineHeight: 1.45,
6354
+ wordBreak: "break-word",
6355
+ overflow: "hidden",
6356
+ display: "-webkit-box",
6357
+ WebkitLineClamp: 3,
6358
+ WebkitBoxOrient: "vertical"
6359
+ },
6360
+ children: [
6361
+ /* @__PURE__ */ jsxs5("span", { style: { color: t.textMuted, fontWeight: 600 }, children: [
6362
+ "#",
6363
+ issue.number
6364
+ ] }),
6365
+ " ",
6366
+ issue.title
6367
+ ]
6368
+ }
6369
+ ),
6370
+ issue.status && /* @__PURE__ */ jsx5("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: issue.status })
6371
+ ]
6372
+ }
6373
+ )
6374
+ ]
6375
+ },
6376
+ issue.number
6377
+ );
6378
+ })
6379
+ }
6380
+ ),
6381
+ /* @__PURE__ */ jsx5(
6382
+ "button",
6383
+ {
6384
+ type: "button",
6385
+ disabled: isSubmitting || !pickedSimilar,
6386
+ onClick: () => {
6387
+ if (!pickedSimilar) return;
6388
+ setAttachingTo(pickedSimilar);
6389
+ setDuplicateIssueNumber(pickedSimilar);
6390
+ setDuplicateKind("duplicate");
6391
+ void handleSubmit({ duplicateOf: pickedSimilar, kind: "duplicate" });
6392
+ },
6393
+ style: {
6394
+ width: "100%",
6395
+ minHeight: "40px",
6396
+ padding: "10px",
6397
+ borderRadius: "8px",
6398
+ border: "none",
6399
+ backgroundColor: isSubmitting || !pickedSimilar ? t.bgSecondary : t.accent,
6400
+ color: isSubmitting || !pickedSimilar ? t.textMuted : t.accentText,
6401
+ fontSize: "14px",
6402
+ fontWeight: 600,
6403
+ cursor: isSubmitting || !pickedSimilar ? "not-allowed" : "pointer",
6404
+ fontFamily: "inherit"
6405
+ },
6406
+ children: typeof attachingTo === "number" && duplicateKind !== "related" ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
6407
+ /* @__PURE__ */ jsx5(ButtonSpinner, {}),
6408
+ "Adding to #",
6409
+ attachingTo,
6410
+ "\u2026"
6411
+ ] }) : pickedSimilar ? `Add to #${pickedSimilar}` : "Pick the matching issue"
6412
+ }
6413
+ ),
6414
+ pickedSimilar && /* @__PURE__ */ jsx5(
6415
+ "button",
6416
+ {
6417
+ type: "button",
6418
+ disabled: isSubmitting,
6419
+ onClick: () => {
6420
+ setAttachingTo(pickedSimilar);
6421
+ setDuplicateIssueNumber(pickedSimilar);
6422
+ setDuplicateKind("related");
6423
+ void handleSubmit({ duplicateOf: pickedSimilar, kind: "related" });
6424
+ },
6425
+ style: {
6426
+ width: "100%",
6427
+ minHeight: "40px",
6428
+ marginTop: "-4px",
6429
+ padding: "9px 10px",
6430
+ borderRadius: "8px",
6431
+ border: "1px solid rgba(245,158,11,0.4)",
6432
+ backgroundColor: "transparent",
6433
+ color: isSubmitting ? t.textMuted : t.text,
6434
+ fontSize: "13px",
6435
+ fontWeight: 600,
6436
+ cursor: isSubmitting ? "not-allowed" : "pointer",
6437
+ fontFamily: "inherit"
6438
+ },
6439
+ children: typeof attachingTo === "number" && duplicateKind === "related" ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
6440
+ /* @__PURE__ */ jsx5(ButtonSpinner, {}),
6441
+ "Adding to #",
6442
+ attachingTo,
6443
+ "\u2026"
6444
+ ] }) : `Related request \u2014 add to #${pickedSimilar}`
6445
+ }
6446
+ ),
6447
+ /* @__PURE__ */ jsx5(
6448
+ "button",
6449
+ {
6450
+ type: "button",
6451
+ disabled: isSubmitting,
6452
+ onClick: () => {
6453
+ var _a2;
6454
+ setAttachingTo("new");
6455
+ void handleSubmit({ skipSimilar: true, possiblyRelated: (_a2 = similarIssues[0]) == null ? void 0 : _a2.number });
6456
+ },
6457
+ style: {
6458
+ width: "100%",
6459
+ minHeight: "40px",
6460
+ marginTop: "-4px",
6461
+ padding: "8px",
6462
+ borderRadius: "8px",
6463
+ border: "none",
6464
+ backgroundColor: "transparent",
6465
+ color: isSubmitting ? t.textMuted : t.text,
6466
+ fontSize: "13px",
6467
+ fontWeight: 500,
6468
+ textDecoration: "underline",
6469
+ textUnderlineOffset: "3px",
6470
+ cursor: isSubmitting ? "not-allowed" : "pointer",
6471
+ fontFamily: "inherit"
6472
+ },
6473
+ children: attachingTo === "new" ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
6474
+ /* @__PURE__ */ jsx5(ButtonSpinner, {}),
6475
+ "Sending as new\u2026"
6476
+ ] }) : "No, mine is different \u2014 send as new"
6477
+ }
6478
+ )
6479
+ ]
6480
+ }
6481
+ ) : null;
5394
6482
  if (!mounted) return null;
5395
6483
  return /* @__PURE__ */ jsxs5(Fragment4, { children: [
5396
6484
  isOpen && createPortal3(
@@ -6929,224 +8017,7 @@ function ReportDialog({
6929
8017
  children: validationError
6930
8018
  }
6931
8019
  ),
6932
- similarIssues ? (
6933
- // Same visual language as the assistant's "Our team is
6934
- // already on this" card, so both routes to a duplicate
6935
- // read as one feature.
6936
- /* @__PURE__ */ jsxs5(
6937
- "div",
6938
- {
6939
- "data-gg-similar": "",
6940
- style: {
6941
- marginTop: "12px",
6942
- display: "flex",
6943
- flexDirection: "column",
6944
- gap: "10px",
6945
- padding: "12px",
6946
- borderRadius: "10px",
6947
- border: "1px solid rgba(245,158,11,0.4)",
6948
- backgroundColor: "rgba(245,158,11,0.08)"
6949
- },
6950
- children: [
6951
- /* @__PURE__ */ jsxs5("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [
6952
- /* @__PURE__ */ jsx5("span", { style: { color: t.text, fontWeight: 600, fontSize: "13px" }, children: "Our team may already have this, or something close" }),
6953
- /* @__PURE__ */ jsx5("span", { style: { color: t.textMuted, fontSize: "12px", lineHeight: 1.5 }, children: similarIssues.length > 1 ? "Pick the one that matches. Same problem, or a different request for the same feature \u2014 it's added to that issue, so the team has it in one place." : "Same problem, or a different request for the same feature? Pick it \u2014 it's added to that issue, so the team has it in one place." })
6954
- ] }),
6955
- /* @__PURE__ */ jsx5(
6956
- "div",
6957
- {
6958
- role: "radiogroup",
6959
- "aria-label": "Existing issues",
6960
- style: { display: "flex", flexDirection: "column", gap: "6px" },
6961
- children: similarIssues.map((issue) => {
6962
- const picked = pickedSimilar === issue.number;
6963
- return /* @__PURE__ */ jsxs5(
6964
- "button",
6965
- {
6966
- type: "button",
6967
- role: "radio",
6968
- "aria-checked": picked,
6969
- disabled: isSubmitting,
6970
- onClick: () => setPickedSimilar(issue.number),
6971
- style: {
6972
- display: "flex",
6973
- alignItems: "flex-start",
6974
- gap: "10px",
6975
- width: "100%",
6976
- minHeight: "44px",
6977
- padding: "9px 10px",
6978
- borderRadius: "8px",
6979
- border: `1px solid ${picked ? t.accent : t.border}`,
6980
- boxShadow: picked ? `0 0 0 1px ${t.accent}` : "none",
6981
- backgroundColor: t.bg,
6982
- color: t.text,
6983
- textAlign: "left",
6984
- cursor: isSubmitting ? "not-allowed" : "pointer",
6985
- fontFamily: "inherit"
6986
- },
6987
- children: [
6988
- /* @__PURE__ */ jsx5(
6989
- "span",
6990
- {
6991
- "aria-hidden": "true",
6992
- style: {
6993
- flexShrink: 0,
6994
- marginTop: "2px",
6995
- width: "14px",
6996
- height: "14px",
6997
- boxSizing: "border-box",
6998
- borderRadius: "50%",
6999
- border: `2px solid ${picked ? t.accent : t.textMuted}`,
7000
- backgroundColor: picked ? t.accent : "transparent",
7001
- boxShadow: picked ? `inset 0 0 0 2px ${t.bg}` : "none"
7002
- }
7003
- }
7004
- ),
7005
- /* @__PURE__ */ jsxs5(
7006
- "span",
7007
- {
7008
- style: {
7009
- display: "flex",
7010
- flexDirection: "column",
7011
- gap: "3px",
7012
- minWidth: 0
7013
- },
7014
- children: [
7015
- /* @__PURE__ */ jsxs5(
7016
- "span",
7017
- {
7018
- title: issue.title,
7019
- style: {
7020
- fontSize: "12.5px",
7021
- lineHeight: 1.45,
7022
- wordBreak: "break-word",
7023
- overflow: "hidden",
7024
- display: "-webkit-box",
7025
- WebkitLineClamp: 3,
7026
- WebkitBoxOrient: "vertical"
7027
- },
7028
- children: [
7029
- /* @__PURE__ */ jsxs5("span", { style: { color: t.textMuted, fontWeight: 600 }, children: [
7030
- "#",
7031
- issue.number
7032
- ] }),
7033
- " ",
7034
- issue.title
7035
- ]
7036
- }
7037
- ),
7038
- issue.status && /* @__PURE__ */ jsx5("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: issue.status })
7039
- ]
7040
- }
7041
- )
7042
- ]
7043
- },
7044
- issue.number
7045
- );
7046
- })
7047
- }
7048
- ),
7049
- /* @__PURE__ */ jsx5(
7050
- "button",
7051
- {
7052
- type: "button",
7053
- disabled: isSubmitting || !pickedSimilar,
7054
- onClick: () => {
7055
- if (!pickedSimilar) return;
7056
- setAttachingTo(pickedSimilar);
7057
- setDuplicateIssueNumber(pickedSimilar);
7058
- setDuplicateKind("duplicate");
7059
- void handleSubmit({ duplicateOf: pickedSimilar, kind: "duplicate" });
7060
- },
7061
- style: {
7062
- width: "100%",
7063
- minHeight: "40px",
7064
- padding: "10px",
7065
- borderRadius: "8px",
7066
- border: "none",
7067
- backgroundColor: isSubmitting || !pickedSimilar ? t.bgSecondary : t.accent,
7068
- color: isSubmitting || !pickedSimilar ? t.textMuted : t.accentText,
7069
- fontSize: "14px",
7070
- fontWeight: 600,
7071
- cursor: isSubmitting || !pickedSimilar ? "not-allowed" : "pointer",
7072
- fontFamily: "inherit"
7073
- },
7074
- children: typeof attachingTo === "number" && duplicateKind !== "related" ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
7075
- /* @__PURE__ */ jsx5(ButtonSpinner, {}),
7076
- "Adding to #",
7077
- attachingTo,
7078
- "\u2026"
7079
- ] }) : pickedSimilar ? `Add to #${pickedSimilar}` : "Pick the matching issue"
7080
- }
7081
- ),
7082
- pickedSimilar && /* @__PURE__ */ jsx5(
7083
- "button",
7084
- {
7085
- type: "button",
7086
- disabled: isSubmitting,
7087
- onClick: () => {
7088
- setAttachingTo(pickedSimilar);
7089
- setDuplicateIssueNumber(pickedSimilar);
7090
- setDuplicateKind("related");
7091
- void handleSubmit({ duplicateOf: pickedSimilar, kind: "related" });
7092
- },
7093
- style: {
7094
- width: "100%",
7095
- minHeight: "40px",
7096
- marginTop: "-4px",
7097
- padding: "9px 10px",
7098
- borderRadius: "8px",
7099
- border: "1px solid rgba(245,158,11,0.4)",
7100
- backgroundColor: "transparent",
7101
- color: isSubmitting ? t.textMuted : t.text,
7102
- fontSize: "13px",
7103
- fontWeight: 600,
7104
- cursor: isSubmitting ? "not-allowed" : "pointer",
7105
- fontFamily: "inherit"
7106
- },
7107
- children: typeof attachingTo === "number" && duplicateKind === "related" ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
7108
- /* @__PURE__ */ jsx5(ButtonSpinner, {}),
7109
- "Adding to #",
7110
- attachingTo,
7111
- "\u2026"
7112
- ] }) : `Related request \u2014 add to #${pickedSimilar}`
7113
- }
7114
- ),
7115
- /* @__PURE__ */ jsx5(
7116
- "button",
7117
- {
7118
- type: "button",
7119
- disabled: isSubmitting,
7120
- onClick: () => {
7121
- setAttachingTo("new");
7122
- void handleSubmit({ skipSimilar: true });
7123
- },
7124
- style: {
7125
- width: "100%",
7126
- minHeight: "40px",
7127
- marginTop: "-4px",
7128
- padding: "8px",
7129
- borderRadius: "8px",
7130
- border: "none",
7131
- backgroundColor: "transparent",
7132
- color: isSubmitting ? t.textMuted : t.text,
7133
- fontSize: "13px",
7134
- fontWeight: 500,
7135
- textDecoration: "underline",
7136
- textUnderlineOffset: "3px",
7137
- cursor: isSubmitting ? "not-allowed" : "pointer",
7138
- fontFamily: "inherit"
7139
- },
7140
- children: attachingTo === "new" ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
7141
- /* @__PURE__ */ jsx5(ButtonSpinner, {}),
7142
- "Sending as new\u2026"
7143
- ] }) : "No, mine is different \u2014 send as new"
7144
- }
7145
- )
7146
- ]
7147
- }
7148
- )
7149
- ) : /* @__PURE__ */ jsx5(
8020
+ similarCard != null ? similarCard : /* @__PURE__ */ jsx5(
7150
8021
  "button",
7151
8022
  {
7152
8023
  type: "button",
@@ -7188,7 +8059,7 @@ function ReportDialog({
7188
8059
  gap: "12px"
7189
8060
  },
7190
8061
  children: [
7191
- /* @__PURE__ */ jsx5(ReporterChip, { reporter, t }),
8062
+ /* @__PURE__ */ jsx5(ReporterChip, { reporter: reporter2, t }),
7192
8063
  reportGlitchgrabProblem ? /* @__PURE__ */ jsx5(
7193
8064
  "button",
7194
8065
  {
@@ -7445,7 +8316,8 @@ function ReportDialog({
7445
8316
  pageShot,
7446
8317
  onAddFiles: addFiles,
7447
8318
  onRemoveImage: (index) => setScreenshots((prev) => prev.filter((_, i) => i !== index)),
7448
- files: attachments,
8319
+ files: heldFiles,
8320
+ canReadPdf: !!readFile,
7449
8321
  onRemoveFile: (index) => setAttachments((prev) => prev.filter((_, i) => i !== index)),
7450
8322
  attachmentCount: screenshots.length + attachments.length,
7451
8323
  context: { ...assistContext != null ? assistContext : {}, reportType },
@@ -7459,10 +8331,15 @@ function ReportDialog({
7459
8331
  setStep(2);
7460
8332
  },
7461
8333
  onDuplicateChange: (issueNumber, kind) => {
8334
+ if (issueNumber) {
8335
+ setSimilarIssues(null);
8336
+ setPickedSimilar(null);
8337
+ }
7462
8338
  setDuplicateIssueNumber(issueNumber);
7463
8339
  setDuplicateKind(issueNumber && kind === "related" ? "related" : "duplicate");
7464
8340
  },
7465
8341
  onConversationChange: setAiConversationId,
8342
+ historyKey: chatHistoryKey2,
7466
8343
  onFinish: handleClose,
7467
8344
  rating,
7468
8345
  onRatingChange: (value) => {
@@ -7470,7 +8347,7 @@ function ReportDialog({
7470
8347
  if (validationError) setValidationError(null);
7471
8348
  },
7472
8349
  projectSlot: headerSlot,
7473
- reporterName: (_a = reporter == null ? void 0 : reporter.name) != null ? _a : null,
8350
+ reporterName: (_a = reporter2 == null ? void 0 : reporter2.name) != null ? _a : null,
7474
8351
  description,
7475
8352
  onDescriptionChange: (value) => {
7476
8353
  setDescription(value);
@@ -7488,7 +8365,9 @@ function ReportDialog({
7488
8365
  severityRefused: needsSeverity,
7489
8366
  isSubmitting,
7490
8367
  submitted,
7491
- onSend: () => void handleSubmit({ skipSimilar: true }),
8368
+ onSend: () => void handleSubmit(),
8369
+ similarSlot: similarCard,
8370
+ submittedText: addedToIssue ? `Added to #${addedToIssue}${addedAsRelated ? " as a related request" : ""}. Thank you!` : null,
7492
8371
  onReportGlitchgrabProblem: reportGlitchgrabProblem ? (prefill) => {
7493
8372
  setSelfReportPrefill(prefill != null ? prefill : "");
7494
8373
  setSelfReportFrom("assist");
@@ -7782,7 +8661,9 @@ function BookingDialog({
7782
8661
  token,
7783
8662
  baseUrl,
7784
8663
  open,
7785
- onClose
8664
+ onClose,
8665
+ preview = false,
8666
+ availabilityUrl
7786
8667
  }) {
7787
8668
  var _a, _b, _c, _d, _e;
7788
8669
  const wide = useMediaQuery(WIDE_QUERY, true);
@@ -7816,10 +8697,12 @@ function BookingDialog({
7816
8697
  const [meetUrl, setMeetUrl] = useState5(null);
7817
8698
  const [busy, setBusy] = useState5(false);
7818
8699
  const [error, setError] = useState5(null);
7819
- const headers = useMemo3(
7820
- () => ({ Authorization: `Bearer ${token}`, "Content-Type": "application/json" }),
7821
- [token]
7822
- );
8700
+ const [previewEnded, setPreviewEnded] = useState5(false);
8701
+ const headers = useMemo3(() => {
8702
+ const out = { "Content-Type": "application/json" };
8703
+ if (token) out.Authorization = `Bearer ${token}`;
8704
+ return out;
8705
+ }, [token]);
7823
8706
  const loadMonth = useCallback2(
7824
8707
  async (target) => {
7825
8708
  var _a2, _b2, _c2;
@@ -7831,8 +8714,9 @@ function BookingDialog({
7831
8714
  return;
7832
8715
  }
7833
8716
  try {
8717
+ const source = availabilityUrl != null ? availabilityUrl : `${baseUrl}/api/v1/sdk/booking/availability`;
7834
8718
  const res = await fetch(
7835
- `${baseUrl}/api/v1/sdk/booking/availability?from=${encodeURIComponent(new Date(from).toISOString())}&to=${encodeURIComponent(new Date(to).toISOString())}`,
8719
+ `${source}${source.includes("?") ? "&" : "?"}from=${encodeURIComponent(new Date(from).toISOString())}&to=${encodeURIComponent(new Date(to).toISOString())}${preview ? "&preview=1" : ""}`,
7836
8720
  { headers }
7837
8721
  );
7838
8722
  const json = await res.json();
@@ -7861,7 +8745,7 @@ function BookingDialog({
7861
8745
  setPageLoaded(true);
7862
8746
  }
7863
8747
  },
7864
- [baseUrl, headers]
8748
+ [baseUrl, headers, availabilityUrl, preview]
7865
8749
  );
7866
8750
  useEffect5(() => {
7867
8751
  if (!open) return;
@@ -7875,6 +8759,7 @@ function BookingDialog({
7875
8759
  setCode("");
7876
8760
  setBookingId(null);
7877
8761
  setError(null);
8762
+ setPreviewEnded(false);
7878
8763
  setAvatarFailed(false);
7879
8764
  setPageLoaded(false);
7880
8765
  setAsked(false);
@@ -7913,6 +8798,10 @@ function BookingDialog({
7913
8798
  async function startBooking() {
7914
8799
  var _a2, _b2;
7915
8800
  if (!selected) return;
8801
+ if (preview) {
8802
+ setPreviewEnded(true);
8803
+ return;
8804
+ }
7916
8805
  setBusy(true);
7917
8806
  setError(null);
7918
8807
  try {
@@ -8106,6 +8995,22 @@ function BookingDialog({
8106
8995
  ) : null,
8107
8996
  page.hostName && /* @__PURE__ */ jsx6("div", { style: { fontSize: 14, fontWeight: 500, ...muted }, children: page.hostName })
8108
8997
  ] }),
8998
+ preview && /* @__PURE__ */ jsx6(
8999
+ "div",
9000
+ {
9001
+ style: {
9002
+ alignSelf: "flex-start",
9003
+ padding: "3px 9px",
9004
+ borderRadius: 999,
9005
+ border: `1px dashed ${t.border}`,
9006
+ fontSize: 11.5,
9007
+ fontWeight: 600,
9008
+ letterSpacing: "0.02em",
9009
+ color: t.muted
9010
+ },
9011
+ children: "Preview \u2014 nothing is booked"
9012
+ }
9013
+ ),
8109
9014
  /* @__PURE__ */ jsx6("div", { style: { fontSize: 20, fontWeight: 600, lineHeight: 1.25, paddingRight: 32, overflowWrap: "anywhere" }, children: page.title }),
8110
9015
  page.description && /* @__PURE__ */ jsx6("div", { style: { fontSize: 13, lineHeight: 1.5, overflowWrap: "anywhere", ...muted }, children: page.description }),
8111
9016
  /* @__PURE__ */ jsxs6("div", { style: { display: "flex", flexDirection: "column", gap: 10, marginTop: 4, fontSize: 14 }, children: [
@@ -8543,6 +9448,7 @@ function BookingDialog({
8543
9448
  },
8544
9449
  children: /* @__PURE__ */ jsxs6("div", { style: { maxWidth: 440, display: "flex", flexDirection: "column", gap: 14 }, children: [
8545
9450
  error && /* @__PURE__ */ jsx6("div", { style: { color: t.danger, fontSize: 13, lineHeight: 1.5 }, children: error }),
9451
+ previewEnded && /* @__PURE__ */ jsx6("div", { style: { fontSize: 13, lineHeight: 1.5, ...muted }, children: "End of the preview \u2014 a prospect would be booked here. Nothing was booked and no code was sent." }),
8546
9452
  step === "details" && /* @__PURE__ */ jsxs6(Fragment5, { children: [
8547
9453
  /* @__PURE__ */ jsx6(Field, { t, label: "Your name", value: name, onChange: setName, autoComplete: "name" }),
8548
9454
  /* @__PURE__ */ jsx6(Field, { t, label: "Email", value: email, onChange: setEmail, type: "email", autoComplete: "email" }),
@@ -9028,6 +9934,9 @@ function GlitchgrabProviderInner({
9028
9934
  onError,
9029
9935
  onReportSent
9030
9936
  });
9937
+ if (process.env.NODE_ENV !== "development") {
9938
+ installHydrationCapture(reportHydrationError);
9939
+ }
9031
9940
  useEffect6(() => {
9032
9941
  if (enableBreadcrumbs) {
9033
9942
  initBreadcrumbs(maxBreadcrumbs2, { responseBodyOrigins: responseBodyOrigins2 });
@@ -9083,7 +9992,7 @@ function GlitchgrabProviderInner({
9083
9992
  return matchesIgnorePatterns(message, ignoreErrors);
9084
9993
  };
9085
9994
  const handleError = (event) => {
9086
- var _a, _b, _c, _d, _e, _f, _g;
9995
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
9087
9996
  try {
9088
9997
  const isOpaqueCrossOrigin = event.message === "Script error." || event.message === "Script error" || !event.error && !event.filename;
9089
9998
  if (isOpaqueCrossOrigin) {
@@ -9098,6 +10007,7 @@ function GlitchgrabProviderInner({
9098
10007
  if (event.filename && EXTENSION_ORIGIN_RE.test(event.filename) || ((_a = event.error) == null ? void 0 : _a.stack) && EXTENSION_ORIGIN_RE.test(event.error.stack)) {
9099
10008
  return;
9100
10009
  }
10010
+ if (isHandledHydrationError(event.error)) return;
9101
10011
  incrementErrorCount();
9102
10012
  const context = captureContext(visitedPagesRef.current);
9103
10013
  const sig = computeSignature({
@@ -9123,6 +10033,9 @@ function GlitchgrabProviderInner({
9123
10033
  filename: (_e = event.filename) != null ? _e : "",
9124
10034
  lineno: String((_f = event.lineno) != null ? _f : ""),
9125
10035
  colno: String((_g = event.colno) != null ? _g : ""),
10036
+ // The root patch could not attach (React internals moved, or a
10037
+ // root it does not know): still label it and send the DOM context.
10038
+ ...isHydrationError((_h = event.error) != null ? _h : event.message) ? hydrationMetadata((_i = event.error) != null ? _i : event.message) : {},
9126
10039
  ...(session == null ? void 0 : session.userId) ? { sessionUserId: session.userId } : {},
9127
10040
  ...(session == null ? void 0 : session.name) ? { sessionUserName: String(session.name) } : {},
9128
10041
  ...(session == null ? void 0 : session.email) ? { sessionUserEmail: String(session.email) } : {},
@@ -9383,7 +10296,11 @@ function GlitchgrabProviderInner({
9383
10296
  [token, baseUrl, session]
9384
10297
  );
9385
10298
  const findSimilar = useCallback3(
9386
- (description) => findSimilarIssues(description, token, baseUrl, identity),
10299
+ (description, files) => findSimilarIssues(description, token, baseUrl, identity, files),
10300
+ [token, baseUrl, identity]
10301
+ );
10302
+ const readFile = useCallback3(
10303
+ (file) => readAttachment(file, token, baseUrl, identity),
9387
10304
  [token, baseUrl, identity]
9388
10305
  );
9389
10306
  const transcribe = useCallback3(
@@ -9414,8 +10331,9 @@ function GlitchgrabProviderInner({
9414
10331
  const [glitchgrabReport, setGlitchgrabReport] = useState6(false);
9415
10332
  const [signedUsers, setSignedUsers] = useState6("OFF");
9416
10333
  const aiBlockedUnsigned = signedUsers === "ENFORCE" && !signature;
9417
- const [bookingOpen, setBookingOpen] = useState6(false);
9418
- const openBookingDialog = useCallback3(() => setBookingOpen(true), []);
10334
+ const [booking, setBooking] = useState6("closed");
10335
+ const openBookingDialog = useCallback3(() => setBooking("live"), []);
10336
+ const openBookingPreview = useCallback3(() => setBooking("preview"), []);
9419
10337
  useEffect6(() => {
9420
10338
  let cancelled = false;
9421
10339
  fetch(`${baseUrl}/api/v1/sdk/project`, {
@@ -9459,7 +10377,7 @@ function GlitchgrabProviderInner({
9459
10377
  } catch (e) {
9460
10378
  }
9461
10379
  }, [openReportDialog]);
9462
- const reporter = useMemo4(() => {
10380
+ const reporter2 = useMemo4(() => {
9463
10381
  var _a;
9464
10382
  if (!session) return null;
9465
10383
  const avatar = (_a = session.avatarUrl) != null ? _a : session.image;
@@ -9490,6 +10408,7 @@ function GlitchgrabProviderInner({
9490
10408
  addBreadcrumb: addBreadcrumb2,
9491
10409
  openReportDialog,
9492
10410
  openBookingDialog,
10411
+ openBookingPreview,
9493
10412
  enhanceText: enhance,
9494
10413
  shortcutLabel
9495
10414
  },
@@ -9511,8 +10430,9 @@ function GlitchgrabProviderInner({
9511
10430
  {
9512
10431
  token,
9513
10432
  baseUrl: baseUrl != null ? baseUrl : DEFAULT_BASE_URL2,
9514
- open: bookingOpen,
9515
- onClose: () => setBookingOpen(false)
10433
+ open: booking !== "closed",
10434
+ preview: booking === "preview",
10435
+ onClose: () => setBooking("closed")
9516
10436
  }
9517
10437
  ),
9518
10438
  /* @__PURE__ */ jsx7(
@@ -9523,11 +10443,13 @@ function GlitchgrabProviderInner({
9523
10443
  enhanceText: aiAssist && !aiBlockedUnsigned ? enhance : void 0,
9524
10444
  assist: aiAssist && !aiBlockedUnsigned ? assist : void 0,
9525
10445
  findSimilarIssues: aiAssist && !aiBlockedUnsigned ? findSimilar : void 0,
10446
+ readFile: aiAssist && !aiBlockedUnsigned ? readFile : void 0,
9526
10447
  reportGlitchgrabProblem: glitchgrabReport ? reportGlitchgrabProblem : void 0,
9527
10448
  transcribeAudio: aiBlockedUnsigned ? void 0 : transcribe,
9528
10449
  types,
9529
10450
  showSeverity,
9530
- reporter,
10451
+ reporter: reporter2,
10452
+ chatHistoryKey: chatHistoryKey("sdk", token, session == null ? void 0 : session.userId),
9531
10453
  headerSlot: projectName ? /* @__PURE__ */ jsxs7("div", { style: { fontSize: 11, color: "#9aa0a6" }, children: [
9532
10454
  "Reporting to ",
9533
10455
  /* @__PURE__ */ jsx7("strong", { style: { color: "#e5e5e5" }, children: projectName })
@@ -9926,6 +10848,7 @@ function useGlitchgrabFeedback(query) {
9926
10848
  }
9927
10849
  export {
9928
10850
  BookDemoButton,
10851
+ BookingDialog,
9929
10852
  FeedbackButton,
9930
10853
  GLITCHGRAB_SHORTCUT2 as GLITCHGRAB_SHORTCUT,
9931
10854
  GLITCHGRAB_SHORTCUT_MAC2 as GLITCHGRAB_SHORTCUT_MAC,