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.js CHANGED
@@ -32,6 +32,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
32
32
  var index_exports = {};
33
33
  __export(index_exports, {
34
34
  BookDemoButton: () => BookDemoButton,
35
+ BookingDialog: () => BookingDialog,
35
36
  FeedbackButton: () => FeedbackButton,
36
37
  GLITCHGRAB_SHORTCUT: () => GLITCHGRAB_SHORTCUT2,
37
38
  GLITCHGRAB_SHORTCUT_MAC: () => GLITCHGRAB_SHORTCUT_MAC2,
@@ -238,7 +239,7 @@ function argsToString(args) {
238
239
  function previewJson(value, max) {
239
240
  let out = "";
240
241
  const path = /* @__PURE__ */ new WeakSet();
241
- const write = (input, inArray) => {
242
+ const write2 = (input, inArray) => {
242
243
  if (out.length > max) return;
243
244
  let v = input;
244
245
  if (v !== null && typeof v === "object" && typeof v.toJSON === "function") {
@@ -275,7 +276,7 @@ function previewJson(value, max) {
275
276
  out += "[";
276
277
  for (let i = 0; i < obj.length && out.length <= max; i++) {
277
278
  if (i > 0) out += ",";
278
- write(obj[i], true);
279
+ write2(obj[i], true);
279
280
  }
280
281
  out += "]";
281
282
  } else {
@@ -288,13 +289,13 @@ function previewJson(value, max) {
288
289
  if (child === void 0 || typeof child === "function" || typeof child === "symbol") continue;
289
290
  out += `${first ? "" : ","}${JSON.stringify(key)}:`;
290
291
  first = false;
291
- write(child, false);
292
+ write2(child, false);
292
293
  }
293
294
  out += "}";
294
295
  }
295
296
  path.delete(obj);
296
297
  };
297
- write(value, false);
298
+ write2(value, false);
298
299
  return out;
299
300
  }
300
301
  async function readErrorBody(response, url) {
@@ -647,6 +648,9 @@ function contextMetadata(context) {
647
648
  var DEFAULT_BASE_URL = "https://glitchgrab.dev";
648
649
  var MAX_RETRIES = 3;
649
650
  var RETRY_BASE_MS = 500;
651
+ function reportAttemptTimeoutMs(bodyLength) {
652
+ return 1e4 + Math.ceil(bodyLength / 1e5) * 1e3;
653
+ }
650
654
  async function sendReport(payload, baseUrl) {
651
655
  var _a;
652
656
  try {
@@ -657,13 +661,18 @@ async function sendReport(payload, baseUrl) {
657
661
  Authorization: `Bearer ${payload.token}`
658
662
  };
659
663
  const useKeepalive = body.length < 6e4;
664
+ const timeoutMs = reportAttemptTimeoutMs(body.length);
660
665
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
666
+ const isLast = attempt === MAX_RETRIES - 1;
667
+ const controller = !isLast && typeof AbortController === "function" ? new AbortController() : null;
668
+ const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
661
669
  try {
662
670
  const response = await fetch(url, {
663
671
  method: "POST",
664
672
  headers,
665
673
  body,
666
- ...useKeepalive ? { keepalive: true } : {}
674
+ ...useKeepalive ? { keepalive: true } : {},
675
+ ...controller ? { signal: controller.signal } : {}
667
676
  });
668
677
  if (response.ok) {
669
678
  const envelope = await response.json();
@@ -673,6 +682,8 @@ async function sendReport(payload, baseUrl) {
673
682
  return null;
674
683
  }
675
684
  } catch (e) {
685
+ } finally {
686
+ if (timer) clearTimeout(timer);
676
687
  }
677
688
  if (attempt < MAX_RETRIES - 1) {
678
689
  await new Promise(
@@ -789,7 +800,7 @@ async function enhanceText(text, token, baseUrl, screenshot, context, identity)
789
800
  return text;
790
801
  }
791
802
  }
792
- async function findSimilarIssues(description, token, baseUrl, identity) {
803
+ async function findSimilarIssues(description, token, baseUrl, identity, files) {
793
804
  var _a;
794
805
  try {
795
806
  const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/reports/similar`;
@@ -799,7 +810,11 @@ async function findSimilarIssues(description, token, baseUrl, identity) {
799
810
  "Content-Type": "application/json",
800
811
  Authorization: `Bearer ${token}`
801
812
  },
802
- body: JSON.stringify({ text: description, ...signedFields(identity) })
813
+ body: JSON.stringify({
814
+ text: description,
815
+ ...(files == null ? void 0 : files.length) ? { files } : {},
816
+ ...signedFields(identity)
817
+ })
803
818
  });
804
819
  const envelope = await response.json().catch(() => null);
805
820
  if (!response.ok || !(envelope == null ? void 0 : envelope.success)) return null;
@@ -809,6 +824,25 @@ async function findSimilarIssues(description, token, baseUrl, identity) {
809
824
  return null;
810
825
  }
811
826
  }
827
+ async function readAttachment(file, token, baseUrl, identity) {
828
+ var _a;
829
+ try {
830
+ const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/ai/read-file`;
831
+ const response = await fetch(url, {
832
+ method: "POST",
833
+ headers: {
834
+ "Content-Type": "application/json",
835
+ Authorization: `Bearer ${token}`
836
+ },
837
+ body: JSON.stringify({ name: file.name, data: file.dataUrl, ...signedFields(identity) })
838
+ });
839
+ const envelope = await response.json().catch(() => null);
840
+ if (!response.ok || !(envelope == null ? void 0 : envelope.success) || !((_a = envelope.data) == null ? void 0 : _a.enabled)) return null;
841
+ return typeof envelope.data.text === "string" ? envelope.data.text : null;
842
+ } catch (e) {
843
+ return null;
844
+ }
845
+ }
812
846
  async function assistTurn(params, token, baseUrl) {
813
847
  const offline = assistOffline();
814
848
  try {
@@ -922,6 +956,292 @@ function clearDedupCache() {
922
956
  seen.clear();
923
957
  }
924
958
 
959
+ // src/hydration.ts
960
+ var HYDRATION_CODE_RE = /Minified React error #(418|419|422|423|425)\b/;
961
+ 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;
962
+ var BAILOUT_DIGEST = "BAILOUT_TO_CLIENT_SIDE_RENDERING";
963
+ var DOM_SNIPPET_MAX = 3e3;
964
+ var STACK_FRAMES_MAX = 40;
965
+ function messageOf(error) {
966
+ if (error && typeof error === "object" && typeof error.message === "string") {
967
+ return error.message;
968
+ }
969
+ return typeof error === "string" ? error : "";
970
+ }
971
+ function hydrationCode(message) {
972
+ if (!message) return null;
973
+ const m = HYDRATION_CODE_RE.exec(message);
974
+ if (m) return m[1];
975
+ return HYDRATION_DEV_RE.test(message) ? "dev" : null;
976
+ }
977
+ function isHydrationError(error) {
978
+ if (hydrationCode(messageOf(error))) return true;
979
+ const cause = error && typeof error === "object" ? error.cause : void 0;
980
+ return cause !== void 0 && cause !== error && hydrationCode(messageOf(cause)) !== null;
981
+ }
982
+ function isBailout(error) {
983
+ const e = error;
984
+ if (!e || typeof e !== "object") return false;
985
+ if (e.digest === BAILOUT_DIGEST) return true;
986
+ const c = e.cause;
987
+ return !!c && typeof c === "object" && c.digest === BAILOUT_DIGEST;
988
+ }
989
+ function componentStackNames(stack) {
990
+ if (!stack) return [];
991
+ const names = [];
992
+ for (const raw of stack.split("\n")) {
993
+ const line = raw.trim();
994
+ if (!line) continue;
995
+ let name = "";
996
+ if (line.startsWith("at ")) {
997
+ name = line.slice(3).split(" (")[0].trim();
998
+ } else if (line.includes("@")) {
999
+ name = line.slice(0, line.indexOf("@")).trim();
1000
+ } else {
1001
+ name = line;
1002
+ }
1003
+ if (name) names.push(name);
1004
+ if (names.length >= STACK_FRAMES_MAX) break;
1005
+ }
1006
+ return names;
1007
+ }
1008
+ function componentStackKey(stack) {
1009
+ return componentStackNames(stack).join(">");
1010
+ }
1011
+ var observer = null;
1012
+ var observerStarted = false;
1013
+ var observerStopAt = 0;
1014
+ var OBSERVE_MS = 3e4;
1015
+ function startObserver() {
1016
+ if (observerStarted || typeof MutationObserver === "undefined" || typeof document === "undefined") {
1017
+ return;
1018
+ }
1019
+ observerStarted = true;
1020
+ try {
1021
+ observer = new MutationObserver(() => {
1022
+ if (Date.now() > observerStopAt) stopObserver();
1023
+ });
1024
+ observer.observe(document, { childList: true, subtree: true });
1025
+ observerStopAt = Date.now() + OBSERVE_MS;
1026
+ setTimeout(stopObserver, OBSERVE_MS);
1027
+ } catch (e) {
1028
+ observer = null;
1029
+ }
1030
+ }
1031
+ function stopObserver() {
1032
+ try {
1033
+ observer == null ? void 0 : observer.disconnect();
1034
+ } catch (e) {
1035
+ }
1036
+ observer = null;
1037
+ }
1038
+ function describeNode(node) {
1039
+ if (!node) return "";
1040
+ if (node.nodeType === 9) return "#document";
1041
+ const el = node;
1042
+ if (!el.tagName) return node.nodeName;
1043
+ const id = el.id ? `#${el.id}` : "";
1044
+ const cls = typeof el.className === "string" && el.className.trim() ? "." + el.className.trim().split(/\s+/).slice(0, 3).join(".") : "";
1045
+ return `${el.tagName.toLowerCase()}${id}${cls}`;
1046
+ }
1047
+ function pathOf(node) {
1048
+ const parts = [];
1049
+ let cur = node;
1050
+ while (cur && parts.length < 6) {
1051
+ parts.unshift(describeNode(cur));
1052
+ if (cur.nodeType === 9) break;
1053
+ cur = cur.parentNode;
1054
+ }
1055
+ return parts.join(" > ");
1056
+ }
1057
+ var NON_CONTENT_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "LINK", "META", "NOSCRIPT", "TEMPLATE", "TITLE", "HEAD"]);
1058
+ function isInHead(node) {
1059
+ for (let cur = node; cur; cur = cur.parentNode) {
1060
+ if (cur.nodeName === "HEAD") return true;
1061
+ if (cur.nodeName === "BODY") return false;
1062
+ }
1063
+ return false;
1064
+ }
1065
+ var GSTIN_RE = /\b\d{2}[A-Z]{5}\d{4}[A-Z][A-Z0-9]Z[A-Z0-9]\b/g;
1066
+ var PAN_RE = /\b[A-Z]{5}\d{4}[A-Z]\b/g;
1067
+ function scrubHtml(html) {
1068
+ return redactString(
1069
+ 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)))
1070
+ );
1071
+ }
1072
+ function takeRemovedServerHtml() {
1073
+ var _a, _b;
1074
+ if (!observer) return { html: "", where: "" };
1075
+ try {
1076
+ const records = observer.takeRecords();
1077
+ let html = "";
1078
+ let where = "";
1079
+ for (const rec of records) {
1080
+ if (rec.type !== "childList" || rec.removedNodes.length === 0) continue;
1081
+ if (isInHead(rec.target)) continue;
1082
+ if (!where) where = pathOf(rec.target);
1083
+ for (const node of Array.from(rec.removedNodes)) {
1084
+ if (node.nodeType === 1 && NON_CONTENT_TAGS.has(node.tagName)) continue;
1085
+ if (node.nodeType === 3 && !((_a = node.textContent) != null ? _a : "").trim()) continue;
1086
+ const piece = node.nodeType === 1 ? node.outerHTML : node.nodeType === 3 ? `#text "${(_b = node.textContent) != null ? _b : ""}"` : "";
1087
+ if (!piece) continue;
1088
+ html += (html ? "\n" : "") + piece;
1089
+ if (html.length >= DOM_SNIPPET_MAX) break;
1090
+ }
1091
+ if (html.length >= DOM_SNIPPET_MAX) break;
1092
+ }
1093
+ const scrubbed = scrubHtml(html);
1094
+ return {
1095
+ html: scrubbed.length > DOM_SNIPPET_MAX ? `${scrubbed.slice(0, DOM_SNIPPET_MAX)}\u2026[truncated]` : scrubbed,
1096
+ where
1097
+ };
1098
+ } catch (e) {
1099
+ return { html: "", where: "" };
1100
+ }
1101
+ }
1102
+ function detectForeignNodes(doc = document) {
1103
+ var _a;
1104
+ const found = [];
1105
+ try {
1106
+ const html = doc.documentElement;
1107
+ if ((html == null ? void 0 : html.classList.contains("translated-ltr")) || (html == null ? void 0 : html.classList.contains("translated-rtl"))) {
1108
+ found.push("Google Translate is active (html.translated-*)");
1109
+ }
1110
+ const fonts = doc.getElementsByTagName("font").length;
1111
+ if (fonts > 0) found.push(`<font> \xD7 ${fonts} (Google Translate or a translation extension)`);
1112
+ const detectors = doc.querySelectorAll("a[x-apple-data-detectors]").length;
1113
+ if (detectors > 0) {
1114
+ found.push(`<a x-apple-data-detectors> \xD7 ${detectors} (iOS format detection turned text into links)`);
1115
+ }
1116
+ if (doc.querySelector("[data-gr-ext-installed],[data-new-gr-c-s-check-loaded],grammarly-desktop-integration")) {
1117
+ found.push("Grammarly extension attributes");
1118
+ }
1119
+ if (doc.querySelector("[data-lt-installed]")) found.push("LanguageTool extension attributes");
1120
+ if ((_a = doc.body) == null ? void 0 : _a.hasAttribute("cz-shortcut-listen")) found.push("ColorZilla extension attribute on <body>");
1121
+ const custom = /* @__PURE__ */ new Set();
1122
+ for (const parent of [html, doc.body]) {
1123
+ if (!parent) continue;
1124
+ for (const child of Array.from(parent.children)) {
1125
+ const tag = child.tagName.toLowerCase();
1126
+ if (tag.includes("-")) custom.add(tag);
1127
+ }
1128
+ }
1129
+ if (custom.size > 0) {
1130
+ found.push(`custom elements at top level: ${Array.from(custom).slice(0, 5).join(", ")} (often an extension)`);
1131
+ }
1132
+ } catch (e) {
1133
+ }
1134
+ return found;
1135
+ }
1136
+ function hydrationTiming() {
1137
+ var _a, _b, _c, _d;
1138
+ try {
1139
+ if (typeof performance === "undefined") return "";
1140
+ const at2 = Math.round(performance.now());
1141
+ const parts = [`error at ${at2}ms after navigation start`];
1142
+ const nav = (_b = (_a = performance.getEntriesByType) == null ? void 0 : _a.call(performance, "navigation")) == null ? void 0 : _b[0];
1143
+ if (nav == null ? void 0 : nav.domContentLoadedEventEnd) {
1144
+ parts.push(`DOMContentLoaded ${Math.round(nav.domContentLoadedEventEnd)}ms`);
1145
+ }
1146
+ const fetches = ((_d = (_c = performance.getEntriesByType) == null ? void 0 : _c.call(performance, "resource")) != null ? _d : []).filter((e) => {
1147
+ const t = e.initiatorType;
1148
+ return t === "fetch" || t === "xmlhttprequest";
1149
+ }).sort((a, b) => a.responseEnd - b.responseEnd).slice(0, 8).map((e) => {
1150
+ let path = e.name;
1151
+ try {
1152
+ const u = new URL(e.name);
1153
+ path = u.origin === window.location.origin ? u.pathname : `${u.host}${u.pathname}`;
1154
+ } catch (e2) {
1155
+ }
1156
+ return `${path} ${Math.round(e.responseEnd)}ms`;
1157
+ });
1158
+ if (fetches.length > 0) parts.push(`fetches finished before it: ${fetches.join(", ")}`);
1159
+ else parts.push("no fetch had finished yet");
1160
+ return parts.join("; ");
1161
+ } catch (e) {
1162
+ return "";
1163
+ }
1164
+ }
1165
+ function hydrationMetadata(error, extra) {
1166
+ var _a, _b, _c;
1167
+ const code = (_a = hydrationCode(messageOf(error))) != null ? _a : hydrationCode(messageOf(error == null ? void 0 : error.cause));
1168
+ const meta = { errorKind: "hydration" };
1169
+ if (code) meta.hydrationCode = code;
1170
+ const cause = error == null ? void 0 : error.cause;
1171
+ if (cause && cause !== error && messageOf(cause)) {
1172
+ meta.hydrationCause = messageOf(cause).slice(0, 500);
1173
+ }
1174
+ if ((_b = extra == null ? void 0 : extra.removed) == null ? void 0 : _b.html) meta.hydrationServerHtml = extra.removed.html;
1175
+ if ((_c = extra == null ? void 0 : extra.removed) == null ? void 0 : _c.where) meta.hydrationReplacedIn = extra.removed.where;
1176
+ const foreign = detectForeignNodes();
1177
+ if (foreign.length > 0) meta.hydrationForeignNodes = foreign.join("\n");
1178
+ const timing = hydrationTiming();
1179
+ if (timing) meta.hydrationTiming = timing;
1180
+ return meta;
1181
+ }
1182
+ var handled = /* @__PURE__ */ new WeakSet();
1183
+ function isHandledHydrationError(error) {
1184
+ return !!error && typeof error === "object" && handled.has(error);
1185
+ }
1186
+ function markHandled(error) {
1187
+ if (error && typeof error === "object") {
1188
+ handled.add(error);
1189
+ const cause = error.cause;
1190
+ if (cause && typeof cause === "object") handled.add(cause);
1191
+ }
1192
+ }
1193
+ var reporter = null;
1194
+ var PATCHED = "__glitchgrabHydrationPatched";
1195
+ function findFiberRoot(container) {
1196
+ if (!container || typeof container !== "object") return null;
1197
+ try {
1198
+ const key = Object.keys(container).find((k) => k.startsWith("__reactContainer$"));
1199
+ if (!key) return null;
1200
+ const hostRoot = container[key];
1201
+ const root = hostRoot == null ? void 0 : hostRoot.stateNode;
1202
+ return root && typeof root === "object" ? root : null;
1203
+ } catch (e) {
1204
+ return null;
1205
+ }
1206
+ }
1207
+ function patchRoot(root) {
1208
+ if (root[PATCHED]) return true;
1209
+ const original = root.onRecoverableError;
1210
+ if (typeof original !== "function") return false;
1211
+ root.onRecoverableError = function glitchgrabOnRecoverableError(error, info) {
1212
+ var _a;
1213
+ try {
1214
+ if (reporter && isHydrationError(error) && !isBailout(error)) {
1215
+ const removed = takeRemovedServerHtml();
1216
+ markHandled(error);
1217
+ reporter(error, (_a = info == null ? void 0 : info.componentStack) != null ? _a : "", hydrationMetadata(error, { removed }));
1218
+ }
1219
+ } catch (e) {
1220
+ }
1221
+ return original.call(this, error, info);
1222
+ };
1223
+ root[PATCHED] = true;
1224
+ return true;
1225
+ }
1226
+ function patchKnownRoots() {
1227
+ if (typeof document === "undefined") return false;
1228
+ let patched = false;
1229
+ for (const container of [document, document.getElementById("__next"), document.getElementById("root")]) {
1230
+ const root = findFiberRoot(container);
1231
+ if (root && patchRoot(root)) patched = true;
1232
+ }
1233
+ return patched;
1234
+ }
1235
+ function installHydrationCapture(report) {
1236
+ try {
1237
+ if (typeof window === "undefined") return;
1238
+ reporter = report;
1239
+ startObserver();
1240
+ patchKnownRoots();
1241
+ } catch (e) {
1242
+ }
1243
+ }
1244
+
925
1245
  // src/capture.ts
926
1246
  var config = null;
927
1247
  function registerCaptureConfig(next) {
@@ -941,7 +1261,7 @@ function sessionMetadata(session) {
941
1261
  ...(session == null ? void 0 : session.phone) ? { sessionUserPhone: String(session.phone) } : {}
942
1262
  };
943
1263
  }
944
- function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureError") {
1264
+ function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureError", dedupKey) {
945
1265
  var _a, _b, _c;
946
1266
  const err = error instanceof Error ? error : void 0;
947
1267
  const errorMessage = err ? err.message : String(error);
@@ -953,7 +1273,7 @@ function buildCapturedErrorPayload(error, options, cfg, capturedBy = "captureErr
953
1273
  const sig = computeSignature({
954
1274
  errorMessage,
955
1275
  pageUrl: context.url,
956
- errorStack: err == null ? void 0 : err.stack,
1276
+ errorStack: dedupKey ? `at ${dedupKey}` : err == null ? void 0 : err.stack,
957
1277
  digest
958
1278
  });
959
1279
  if (shouldSkipDuplicate(sig)) return null;
@@ -993,6 +1313,25 @@ function captureError(error, options) {
993
1313
  } catch (e) {
994
1314
  }
995
1315
  }
1316
+ function reportHydrationError(error, componentStack, metadata) {
1317
+ try {
1318
+ const cfg = config;
1319
+ if (!(cfg == null ? void 0 : cfg.token)) return;
1320
+ const payload = buildCapturedErrorPayload(
1321
+ error,
1322
+ { componentStack: componentStack || void 0, metadata },
1323
+ cfg,
1324
+ "onRecoverableError",
1325
+ componentStackKey(componentStack) || void 0
1326
+ );
1327
+ if (!payload) return;
1328
+ void sendReport(payload, cfg.baseUrl).then((result) => {
1329
+ if (result && cfg.onReportSent) cfg.onReportSent(result);
1330
+ });
1331
+ if (cfg.onError && error instanceof Error) cfg.onError(error);
1332
+ } catch (e) {
1333
+ }
1334
+ }
996
1335
 
997
1336
  // src/error-boundary.tsx
998
1337
  var GlitchgrabErrorBoundary = class extends React.Component {
@@ -1410,6 +1749,9 @@ var MAX_ASSIST_IMAGE_CHARS = 35e5;
1410
1749
  var MAX_ASSIST_FILES = 3;
1411
1750
  var MAX_ASSIST_FILE_CHARS = 3e5;
1412
1751
  var MAX_ASSIST_FILE_CHARS_EACH = 2e5;
1752
+ var MAX_SIMILAR_FILE_CHARS = 2e4;
1753
+ var MAX_SIMILAR_FILES_TOTAL_CHARS = 4e4;
1754
+ var MAX_READABLE_PDF_BYTES = 3 * 1024 * 1024;
1413
1755
  var TEXT_ATTACHMENT_EXTENSIONS = [
1414
1756
  ".html",
1415
1757
  ".htm",
@@ -1440,6 +1782,10 @@ var ATTACHMENT_ACCEPT = [
1440
1782
  ...BINARY_ATTACHMENT_EXTENSIONS
1441
1783
  ].join(",");
1442
1784
  var ASSIST_ATTACHMENT_ACCEPT = ["image/*", ...TEXT_ATTACHMENT_EXTENSIONS].join(",");
1785
+ var ASSIST_ATTACHMENT_ACCEPT_WITH_PDF = [ASSIST_ATTACHMENT_ACCEPT, ".pdf"].join(",");
1786
+ function isPdfFile(name) {
1787
+ return name.toLowerCase().endsWith(".pdf");
1788
+ }
1443
1789
  function isAssistReadableFile(name) {
1444
1790
  const lower = name.toLowerCase();
1445
1791
  return TEXT_ATTACHMENT_EXTENSIONS.some((ext) => lower.endsWith(ext));
@@ -1460,6 +1806,27 @@ function decodeTextDataUrl(dataUrl) {
1460
1806
  return null;
1461
1807
  }
1462
1808
  }
1809
+ function heldFileKey(file) {
1810
+ return `${file.name}:${file.size}`;
1811
+ }
1812
+ function heldFileText(file) {
1813
+ if (isPdfFile(file.name)) return typeof file.text === "string" && file.text.trim() ? file.text : null;
1814
+ if (!isAssistReadableFile(file.name)) return null;
1815
+ const text = decodeTextDataUrl(file.dataUrl);
1816
+ return text && text.trim() ? text : null;
1817
+ }
1818
+ function filesForSimilar(files, each, total) {
1819
+ const picked = [];
1820
+ let used = 0;
1821
+ for (let i = files.length - 1; i >= 0 && used < total; i--) {
1822
+ const text = heldFileText(files[i]);
1823
+ if (!text) continue;
1824
+ const content = text.slice(0, Math.min(each, total - used));
1825
+ picked.unshift({ name: files[i].name, content });
1826
+ used += content.length;
1827
+ }
1828
+ return picked;
1829
+ }
1463
1830
  function getTypeLabel(type) {
1464
1831
  switch (type) {
1465
1832
  case "RATING":
@@ -1886,6 +2253,79 @@ function renderBlocks(lines, theme, key) {
1886
2253
  function ChatMarkdown({ text, theme }) {
1887
2254
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: STACK, children: renderBlocks(text.replace(/\r\n?/g, "\n").split("\n"), theme, "md") });
1888
2255
  }
2256
+ var MAX_SAVED_CHATS = 20;
2257
+ var SAVED_CHAT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
2258
+ var TITLE_CHARS = 80;
2259
+ var PREFIX = "glitchgrab:ai-chats:";
2260
+ function chatHistoryKey(...parts) {
2261
+ const input = parts.map((p) => p != null ? p : "").join("\0");
2262
+ let hash = 2166136261;
2263
+ for (let i = 0; i < input.length; i++) {
2264
+ hash ^= input.charCodeAt(i);
2265
+ hash = Math.imul(hash, 16777619);
2266
+ }
2267
+ return (hash >>> 0).toString(36);
2268
+ }
2269
+ function chatTitle(messages) {
2270
+ var _a, _b;
2271
+ const first = (_b = (_a = messages.find((m) => m.role === "user")) == null ? void 0 : _a.content.trim()) != null ? _b : "";
2272
+ const line = first.replace(/\s+/g, " ");
2273
+ return line.length > TITLE_CHARS ? `${line.slice(0, TITLE_CHARS - 1)}\u2026` : line;
2274
+ }
2275
+ function storage() {
2276
+ try {
2277
+ return typeof window !== "undefined" && window.localStorage ? window.localStorage : null;
2278
+ } catch (e) {
2279
+ return null;
2280
+ }
2281
+ }
2282
+ function isSavedChat(value) {
2283
+ if (!value || typeof value !== "object") return false;
2284
+ const c = value;
2285
+ return typeof c.id === "string" && typeof c.title === "string" && typeof c.updatedAt === "number" && (c.status === "open" || c.status === "filed") && Array.isArray(c.messages);
2286
+ }
2287
+ function loadChats(key, now = Date.now()) {
2288
+ const store = storage();
2289
+ if (!store) return [];
2290
+ try {
2291
+ const raw = store.getItem(PREFIX + key);
2292
+ if (!raw) return [];
2293
+ const parsed = JSON.parse(raw);
2294
+ if (!Array.isArray(parsed)) return [];
2295
+ 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);
2296
+ } catch (e) {
2297
+ return [];
2298
+ }
2299
+ }
2300
+ function write(key, chats) {
2301
+ const store = storage();
2302
+ if (!store) return;
2303
+ try {
2304
+ if (chats.length === 0) store.removeItem(PREFIX + key);
2305
+ else store.setItem(PREFIX + key, JSON.stringify(chats));
2306
+ } catch (e) {
2307
+ }
2308
+ }
2309
+ function saveChat(key, chat, now = Date.now()) {
2310
+ const rest = loadChats(key, now).filter((c) => c.id !== chat.id);
2311
+ const next = [chat, ...rest].slice(0, MAX_SAVED_CHATS);
2312
+ write(key, next);
2313
+ return next;
2314
+ }
2315
+ function markChatFiled(key, conversationId, label, now = Date.now()) {
2316
+ const chats = loadChats(key, now);
2317
+ const hit = chats.find((c) => c.conversationId === conversationId);
2318
+ if (!hit || hit.status === "filed") return;
2319
+ write(
2320
+ key,
2321
+ chats.map((c) => c === hit ? { ...c, status: "filed", filedLabel: label, updatedAt: now } : c)
2322
+ );
2323
+ }
2324
+ function removeChat(key, id, now = Date.now()) {
2325
+ const next = loadChats(key, now).filter((c) => c.id !== id);
2326
+ write(key, next);
2327
+ return next;
2328
+ }
1889
2329
  function pickAssistImages(all) {
1890
2330
  const picked = [];
1891
2331
  let total = 0;
@@ -1903,9 +2343,8 @@ function pickAssistFiles(files) {
1903
2343
  let total = 0;
1904
2344
  for (let i = files.length - 1; i >= 0 && picked.length < MAX_ASSIST_FILES; i--) {
1905
2345
  const attached = files[i];
1906
- if (!isAssistReadableFile(attached.name)) continue;
1907
- const text = decodeTextDataUrl(attached.dataUrl);
1908
- if (!text || !text.trim()) continue;
2346
+ const text = heldFileText(attached);
2347
+ if (!text) continue;
1909
2348
  const content = text.slice(0, MAX_ASSIST_FILE_CHARS_EACH);
1910
2349
  if (total + content.length > MAX_ASSIST_FILE_CHARS) continue;
1911
2350
  picked.unshift({ index: i, file: { name: attached.name, content } });
@@ -1913,6 +2352,18 @@ function pickAssistFiles(files) {
1913
2352
  }
1914
2353
  return picked;
1915
2354
  }
2355
+ function newChatId() {
2356
+ return `local-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
2357
+ }
2358
+ function chatAge(at2, now = Date.now()) {
2359
+ const min = Math.round((now - at2) / 6e4);
2360
+ if (min < 1) return "just now";
2361
+ if (min < 60) return `${min} min ago`;
2362
+ const hours = Math.round(min / 60);
2363
+ if (hours < 24) return `${hours} h ago`;
2364
+ if (hours < 48) return "yesterday";
2365
+ return new Date(at2).toLocaleDateString(void 0, { day: "numeric", month: "short" });
2366
+ }
1916
2367
  var STARTERS = [
1917
2368
  "Something on this page is broken",
1918
2369
  "It did the wrong thing",
@@ -1995,6 +2446,33 @@ function Avatar({
1995
2446
  }
1996
2447
  );
1997
2448
  }
2449
+ var THINKING_SLOW_SECONDS = 20;
2450
+ function ThinkingClock({ color }) {
2451
+ const [seconds, setSeconds] = (0, import_react3.useState)(0);
2452
+ (0, import_react3.useEffect)(() => {
2453
+ const started = Date.now();
2454
+ const id = setInterval(() => setSeconds(Math.floor((Date.now() - started) / 1e3)), 1e3);
2455
+ return () => clearInterval(id);
2456
+ }, []);
2457
+ return (
2458
+ // Hidden from screen readers: the bubble already says "Thinking", and a
2459
+ // number changing every second would be read out every second.
2460
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2461
+ "span",
2462
+ {
2463
+ "data-gg-thinking-clock": "",
2464
+ "aria-hidden": "true",
2465
+ style: { marginLeft: "6px", fontSize: "12px", color, fontVariantNumeric: "tabular-nums" },
2466
+ children: [
2467
+ "Thinking\u2026 ",
2468
+ seconds,
2469
+ "s",
2470
+ seconds >= THINKING_SLOW_SECONDS ? " \xB7 taking longer than usual, still working" : ""
2471
+ ]
2472
+ }
2473
+ )
2474
+ );
2475
+ }
1998
2476
  function AssistSheet({
1999
2477
  assist,
2000
2478
  theme: t,
@@ -2002,6 +2480,7 @@ function AssistSheet({
2002
2480
  pageShot = null,
2003
2481
  onAddFiles,
2004
2482
  files = NO_FILES,
2483
+ canReadPdf = false,
2005
2484
  onRemoveFile,
2006
2485
  onRemoveImage,
2007
2486
  attachmentCount,
@@ -2029,17 +2508,27 @@ function AssistSheet({
2029
2508
  isSubmitting,
2030
2509
  submitted,
2031
2510
  onSend,
2511
+ similarSlot,
2512
+ submittedText,
2032
2513
  onDegrade,
2033
2514
  onClose,
2034
2515
  onFinish,
2035
- onReportGlitchgrabProblem
2516
+ onReportGlitchgrabProblem,
2517
+ historyKey = null
2036
2518
  }) {
2519
+ var _a;
2037
2520
  const narrow = useIsNarrow();
2038
2521
  const severityAsked = showSeverity && currentTile === "BUG";
2039
2522
  const [messages, setMessages] = (0, import_react3.useState)([]);
2040
2523
  const [input, setInput] = (0, import_react3.useState)("");
2041
2524
  const [busy, setBusy] = (0, import_react3.useState)(false);
2042
2525
  const [conversationId, setConversationId] = (0, import_react3.useState)(null);
2526
+ const chatIdRef = (0, import_react3.useRef)(newChatId());
2527
+ const [savedChats, setSavedChats] = (0, import_react3.useState)(
2528
+ () => historyKey ? loadChats(historyKey) : []
2529
+ );
2530
+ const [showChats, setShowChats] = (0, import_react3.useState)(false);
2531
+ const [viewingChat, setViewingChat] = (0, import_react3.useState)(null);
2043
2532
  const [hoveredStar, setHoveredStar] = (0, import_react3.useState)(0);
2044
2533
  const [options, setOptions] = (0, import_react3.useState)([]);
2045
2534
  const [duplicate, setDuplicate] = (0, import_react3.useState)(null);
@@ -2052,6 +2541,7 @@ function AssistSheet({
2052
2541
  const [autoFileLeft, setAutoFileLeft] = (0, import_react3.useState)(null);
2053
2542
  const scrollRef = (0, import_react3.useRef)(null);
2054
2543
  const draftRef = (0, import_react3.useRef)(null);
2544
+ const composerRef = (0, import_react3.useRef)(null);
2055
2545
  const seededRef = (0, import_react3.useRef)(false);
2056
2546
  const seedPendingRef = (0, import_react3.useRef)(false);
2057
2547
  const eventsRef = (0, import_react3.useRef)([]);
@@ -2090,9 +2580,15 @@ function AssistSheet({
2090
2580
  if (el) el.scrollTop = el.scrollHeight;
2091
2581
  }, [messages, busy, phase]);
2092
2582
  (0, import_react3.useEffect)(() => {
2093
- var _a;
2094
- if (phase === "draft") (_a = draftRef.current) == null ? void 0 : _a.focus();
2583
+ var _a2;
2584
+ if (phase === "draft") (_a2 = draftRef.current) == null ? void 0 : _a2.focus();
2095
2585
  }, [phase]);
2586
+ (0, import_react3.useEffect)(() => {
2587
+ const el = composerRef.current;
2588
+ if (!el) return;
2589
+ el.style.height = "auto";
2590
+ el.style.height = `${el.scrollHeight + 2}px`;
2591
+ }, [input]);
2096
2592
  (0, import_react3.useEffect)(() => {
2097
2593
  if (autoFileLeft === null) return;
2098
2594
  if (isSubmitting || submitted || phase !== "draft") {
@@ -2117,7 +2613,8 @@ function AssistSheet({
2117
2613
  onDegrade(message, retryable);
2118
2614
  }
2119
2615
  async function runTurn(history2, autoFile = false) {
2120
- var _a, _b, _c;
2616
+ var _a2, _b, _c;
2617
+ const turnChat = chatIdRef.current;
2121
2618
  setBusy(true);
2122
2619
  setAutoFileLeft(null);
2123
2620
  let result;
@@ -2154,6 +2651,7 @@ function AssistSheet({
2154
2651
  } finally {
2155
2652
  setBusy(false);
2156
2653
  }
2654
+ if (chatIdRef.current !== turnChat) return;
2157
2655
  if (result.degraded) {
2158
2656
  degradeKeepingEvents(events, result.degraded, result.retryable === true);
2159
2657
  return;
@@ -2164,7 +2662,7 @@ function AssistSheet({
2164
2662
  }
2165
2663
  const attach = result.duplicate ? { ...result.duplicate, kind: "duplicate" } : result.related ? { ...result.related, kind: "related" } : null;
2166
2664
  setDuplicate(attach);
2167
- onDuplicateChange == null ? void 0 : onDuplicateChange((_a = attach == null ? void 0 : attach.number) != null ? _a : null, attach == null ? void 0 : attach.kind);
2665
+ onDuplicateChange == null ? void 0 : onDuplicateChange((_a2 = attach == null ? void 0 : attach.number) != null ? _a2 : null, attach == null ? void 0 : attach.kind);
2168
2666
  setGlitchgrabOffer(!!result.aboutGlitchgrab && !!onReportGlitchgrabProblem && !!result.question);
2169
2667
  if (result.solved) {
2170
2668
  setSolved(result.solved);
@@ -2218,9 +2716,9 @@ function AssistSheet({
2218
2716
  }
2219
2717
  }
2220
2718
  function sendTyped(text) {
2221
- var _a;
2719
+ var _a2;
2222
2720
  const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
2223
- const checkingBack = options.includes(CONFIRM_OPTION) || (lastAssistant == null ? void 0 : lastAssistant.kind) !== "report" && isCheckBackQuestion((_a = lastAssistant == null ? void 0 : lastAssistant.content) != null ? _a : "");
2721
+ const checkingBack = options.includes(CONFIRM_OPTION) || (lastAssistant == null ? void 0 : lastAssistant.kind) !== "report" && isCheckBackQuestion((_a2 = lastAssistant == null ? void 0 : lastAssistant.content) != null ? _a2 : "");
2224
2722
  send(text, checkingBack && isAffirmativeReply(text));
2225
2723
  }
2226
2724
  function send(text, autoFile = false) {
@@ -2262,7 +2760,215 @@ function AssistSheet({
2262
2760
  flushEvents();
2263
2761
  onSend();
2264
2762
  }
2763
+ (0, import_react3.useEffect)(() => {
2764
+ if (!historyKey || !messages.some((m) => m.role === "user")) return;
2765
+ const done = submitted || phase === "solved";
2766
+ const kept = submitted || phase === "draft" ? withCurrentDraft(messages) : messages;
2767
+ setSavedChats(
2768
+ saveChat(historyKey, {
2769
+ id: chatIdRef.current,
2770
+ conversationId,
2771
+ title: chatTitle(kept),
2772
+ tile: currentTile,
2773
+ messages: kept,
2774
+ ...issueTitle ? { issueTitle } : {},
2775
+ status: done ? "filed" : "open",
2776
+ ...done ? {
2777
+ filedLabel: submitted ? submittedText != null ? submittedText : `${reportTypeLabel} sent` : "Answered \u2014 nothing was filed"
2778
+ } : {},
2779
+ updatedAt: Date.now()
2780
+ })
2781
+ );
2782
+ }, [
2783
+ historyKey,
2784
+ messages,
2785
+ conversationId,
2786
+ submitted,
2787
+ phase === "solved",
2788
+ issueTitle,
2789
+ phase === "draft" ? description : null
2790
+ ]);
2791
+ function resumeChat(chat) {
2792
+ var _a2;
2793
+ flushEvents();
2794
+ chatIdRef.current = chat.id;
2795
+ setMessages(chat.messages);
2796
+ setConversationId(chat.conversationId);
2797
+ onConversationChange == null ? void 0 : onConversationChange(chat.conversationId);
2798
+ setOptions([]);
2799
+ setDuplicate(null);
2800
+ onDuplicateChange == null ? void 0 : onDuplicateChange(null);
2801
+ setGlitchgrabOffer(false);
2802
+ setAutoFileLeft(null);
2803
+ setInput("");
2804
+ if (chat.tile !== currentTile && tiles.includes(chat.tile)) {
2805
+ onPickType(chat.tile);
2806
+ }
2807
+ const last = chat.messages[chat.messages.length - 1];
2808
+ if ((last == null ? void 0 : last.kind) === "report") {
2809
+ onDescriptionChange(last.content);
2810
+ onIssueTitleChange((_a2 = chat.issueTitle) != null ? _a2 : "");
2811
+ aiDraftRef.current = last.content;
2812
+ setPhase("draft");
2813
+ } else {
2814
+ setPhase("chat");
2815
+ }
2816
+ setShowChats(false);
2817
+ setViewingChat(null);
2818
+ }
2819
+ function startNewChat() {
2820
+ flushEvents();
2821
+ chatIdRef.current = newChatId();
2822
+ setMessages([]);
2823
+ setConversationId(null);
2824
+ onConversationChange == null ? void 0 : onConversationChange(null);
2825
+ setOptions([]);
2826
+ setDuplicate(null);
2827
+ onDuplicateChange == null ? void 0 : onDuplicateChange(null);
2828
+ setGlitchgrabOffer(false);
2829
+ setAutoFileLeft(null);
2830
+ setInput("");
2831
+ onDescriptionChange("");
2832
+ onIssueTitleChange("");
2833
+ aiDraftRef.current = null;
2834
+ setPhase("chat");
2835
+ setShowChats(false);
2836
+ setViewingChat(null);
2837
+ }
2838
+ function forgetChat(id) {
2839
+ if (!historyKey) return;
2840
+ setSavedChats(removeChat(historyKey, id));
2841
+ if (viewingChat === id) setViewingChat(null);
2842
+ }
2843
+ const otherChats = savedChats.filter((c) => c.id !== chatIdRef.current);
2844
+ const resumable = (_a = otherChats.find((c) => c.status === "open" && c.messages.length > 0)) != null ? _a : null;
2265
2845
  const empty = phase === "chat" && messages.length === 0 && !busy;
2846
+ function renderChatList() {
2847
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { "data-gg-chat-list": "", style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [
2848
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
2849
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "13px", fontWeight: 600 }, children: "Your chats" }),
2850
+ messages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2851
+ "button",
2852
+ {
2853
+ type: "button",
2854
+ "data-gg-new-chat": "",
2855
+ onClick: startNewChat,
2856
+ style: {
2857
+ border: `1px solid ${t.inputBorder}`,
2858
+ borderRadius: "999px",
2859
+ background: "transparent",
2860
+ color: t.text,
2861
+ padding: "7px 12px",
2862
+ fontSize: "12px",
2863
+ fontFamily: "inherit",
2864
+ cursor: "pointer"
2865
+ },
2866
+ children: "+ New chat"
2867
+ }
2868
+ )
2869
+ ] }),
2870
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "11.5px", color: t.textMuted, lineHeight: 1.5 }, children: "Kept in this browser only, for 30 days." }),
2871
+ otherChats.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "12.5px", color: t.textMuted, padding: "12px 0" }, children: "No other chats yet." }),
2872
+ otherChats.map((chat) => {
2873
+ var _a2;
2874
+ const filed = chat.status === "filed";
2875
+ const open = viewingChat === chat.id;
2876
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2877
+ "div",
2878
+ {
2879
+ "data-gg-saved-chat": chat.status,
2880
+ style: {
2881
+ border: `1px solid ${t.border}`,
2882
+ borderRadius: "10px",
2883
+ padding: "10px 12px",
2884
+ display: "flex",
2885
+ flexDirection: "column",
2886
+ gap: "6px",
2887
+ minWidth: 0
2888
+ },
2889
+ children: [
2890
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", alignItems: "flex-start", minWidth: 0 }, children: [
2891
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2892
+ "button",
2893
+ {
2894
+ type: "button",
2895
+ onClick: () => filed ? setViewingChat(open ? null : chat.id) : resumeChat(chat),
2896
+ style: {
2897
+ flex: 1,
2898
+ minWidth: 0,
2899
+ textAlign: "left",
2900
+ border: "none",
2901
+ background: "transparent",
2902
+ color: t.text,
2903
+ padding: 0,
2904
+ fontFamily: "inherit",
2905
+ cursor: "pointer",
2906
+ display: "flex",
2907
+ flexDirection: "column",
2908
+ gap: "4px"
2909
+ },
2910
+ children: [
2911
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "13px", lineHeight: 1.45, overflowWrap: "anywhere" }, children: chat.title || "Untitled chat" }),
2912
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
2913
+ filed ? (_a2 = chat.filedLabel) != null ? _a2 : "Sent" : "Not sent yet \u2014 tap to continue",
2914
+ " \xB7",
2915
+ " ",
2916
+ chatAge(chat.updatedAt)
2917
+ ] })
2918
+ ]
2919
+ }
2920
+ ),
2921
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2922
+ "button",
2923
+ {
2924
+ type: "button",
2925
+ "aria-label": `Delete chat: ${chat.title}`,
2926
+ onClick: () => forgetChat(chat.id),
2927
+ style: {
2928
+ flexShrink: 0,
2929
+ border: "none",
2930
+ background: "transparent",
2931
+ color: t.textMuted,
2932
+ fontSize: "16px",
2933
+ lineHeight: 1,
2934
+ padding: "4px 6px",
2935
+ cursor: "pointer",
2936
+ fontFamily: "inherit"
2937
+ },
2938
+ children: "\xD7"
2939
+ }
2940
+ )
2941
+ ] }),
2942
+ open && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2943
+ "div",
2944
+ {
2945
+ "data-gg-saved-transcript": "",
2946
+ style: {
2947
+ display: "flex",
2948
+ flexDirection: "column",
2949
+ gap: "6px",
2950
+ borderTop: `1px solid ${t.border}`,
2951
+ paddingTop: "8px",
2952
+ fontSize: "12.5px",
2953
+ lineHeight: 1.5
2954
+ },
2955
+ children: chat.messages.map((m, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { minWidth: 0, overflowWrap: "anywhere" }, children: [
2956
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color: t.textMuted, fontWeight: 600 }, children: [
2957
+ m.role === "user" ? "You" : m.kind === "report" ? "Report" : "Assistant",
2958
+ ":",
2959
+ " "
2960
+ ] }),
2961
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { whiteSpace: "pre-wrap" }, children: m.content })
2962
+ ] }, i))
2963
+ }
2964
+ )
2965
+ ]
2966
+ },
2967
+ chat.id
2968
+ );
2969
+ })
2970
+ ] });
2971
+ }
2266
2972
  function renderKnownIssue({ title, footer }) {
2267
2973
  if (!duplicate) return null;
2268
2974
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
@@ -2296,8 +3002,8 @@ function AssistSheet({
2296
3002
  );
2297
3003
  }
2298
3004
  const initials2 = (reporterName != null ? reporterName : "").split(/\s+/).filter(Boolean).slice(0, 2).map((w) => {
2299
- var _a, _b;
2300
- return (_b = (_a = w[0]) == null ? void 0 : _a.toUpperCase()) != null ? _b : "";
3005
+ var _a2, _b;
3006
+ return (_b = (_a2 = w[0]) == null ? void 0 : _a2.toUpperCase()) != null ? _b : "";
2301
3007
  }).join("");
2302
3008
  const panelStyle = narrow ? {
2303
3009
  width: "100%",
@@ -2319,6 +3025,7 @@ function AssistSheet({
2319
3025
  @keyframes gg-sheet-up{from{transform:translateY(24px);opacity:0}to{transform:translateY(0);opacity:1}}
2320
3026
  @keyframes gg-sheet-fade{from{opacity:0}to{opacity:1}}
2321
3027
  @keyframes gg-autofile{from{transform:scaleX(0)}to{transform:scaleX(1)}}
3028
+ @keyframes gg-spin{to{transform:rotate(360deg)}}
2322
3029
  @keyframes gg-dot{0%,80%,100%{transform:translateY(0);opacity:.4}40%{transform:translateY(-3px);opacity:1}}
2323
3030
  [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}
2324
3031
  ` }),
@@ -2387,7 +3094,9 @@ function AssistSheet({
2387
3094
  alignItems: "center",
2388
3095
  gap: "7px",
2389
3096
  fontSize: "14px",
2390
- fontWeight: 600
3097
+ fontWeight: 600,
3098
+ // One line even with the Chats pill beside it (#567).
3099
+ whiteSpace: "nowrap"
2391
3100
  },
2392
3101
  children: [
2393
3102
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Sparkle, { color: t.accent }),
@@ -2417,34 +3126,64 @@ function AssistSheet({
2417
3126
  projectSlot ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { marginLeft: "6px" }, children: projectSlot }) : null
2418
3127
  ] })
2419
3128
  ] }),
2420
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2421
- "button",
2422
- {
2423
- type: "button",
2424
- "data-gg-write-myself": "",
2425
- onClick: () => leave("write_myself", `header, ${phase}`),
2426
- "aria-label": "Write it myself instead",
2427
- style: {
2428
- flexShrink: 0,
2429
- border: `1px solid ${t.inputBorder}`,
2430
- borderRadius: "999px",
2431
- background: "transparent",
2432
- color: t.textMuted,
2433
- cursor: "pointer",
2434
- fontSize: "11.5px",
2435
- lineHeight: 1,
2436
- fontFamily: "inherit",
2437
- whiteSpace: "nowrap",
2438
- // ~32px tall either way — this is the escape hatch, not a hint.
2439
- padding: "9px 11px"
2440
- },
2441
- children: "Write it myself"
2442
- }
2443
- )
3129
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "6px", flexShrink: 0 }, children: [
3130
+ historyKey && (showChats || otherChats.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3131
+ "button",
3132
+ {
3133
+ type: "button",
3134
+ "data-gg-chats-toggle": "",
3135
+ disabled: busy,
3136
+ "aria-pressed": showChats,
3137
+ onClick: () => {
3138
+ setShowChats((v) => !v);
3139
+ setViewingChat(null);
3140
+ },
3141
+ style: {
3142
+ flexShrink: 0,
3143
+ border: `1px solid ${showChats ? t.accent : t.inputBorder}`,
3144
+ borderRadius: "999px",
3145
+ background: "transparent",
3146
+ color: showChats ? t.accent : t.textMuted,
3147
+ cursor: busy ? "default" : "pointer",
3148
+ opacity: busy ? 0.5 : 1,
3149
+ fontSize: "11.5px",
3150
+ lineHeight: 1,
3151
+ fontFamily: "inherit",
3152
+ whiteSpace: "nowrap",
3153
+ padding: "9px 11px"
3154
+ },
3155
+ children: showChats ? "\u2190 Chat" : `Chats \xB7 ${otherChats.length}`
3156
+ }
3157
+ ),
3158
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3159
+ "button",
3160
+ {
3161
+ type: "button",
3162
+ "data-gg-write-myself": "",
3163
+ onClick: () => leave("write_myself", `header, ${phase}`),
3164
+ "aria-label": "Write it myself instead",
3165
+ style: {
3166
+ flexShrink: 0,
3167
+ border: `1px solid ${t.inputBorder}`,
3168
+ borderRadius: "999px",
3169
+ background: "transparent",
3170
+ color: t.textMuted,
3171
+ cursor: "pointer",
3172
+ fontSize: "11.5px",
3173
+ lineHeight: 1,
3174
+ fontFamily: "inherit",
3175
+ whiteSpace: "nowrap",
3176
+ // ~32px tall either way — this is the escape hatch, not a hint.
3177
+ padding: "9px 11px"
3178
+ },
3179
+ children: "Write it myself"
3180
+ }
3181
+ )
3182
+ ] })
2444
3183
  ]
2445
3184
  }
2446
3185
  ),
2447
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3186
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2448
3187
  "div",
2449
3188
  {
2450
3189
  ref: scrollRef,
@@ -2456,765 +3195,845 @@ function AssistSheet({
2456
3195
  display: "flex",
2457
3196
  flexDirection: "column"
2458
3197
  },
2459
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2460
- "div",
2461
- {
2462
- style: {
2463
- marginTop: "auto",
2464
- display: "flex",
2465
- flexDirection: "column",
2466
- gap: "12px",
2467
- minWidth: 0
2468
- },
2469
- children: [
2470
- phase === "type" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2471
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
2472
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2473
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3198
+ children: [
3199
+ showChats && renderChatList(),
3200
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3201
+ "div",
3202
+ {
3203
+ style: {
3204
+ marginTop: "auto",
3205
+ display: "flex",
3206
+ flexDirection: "column",
3207
+ gap: "12px",
3208
+ minWidth: 0
3209
+ },
3210
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: showChats ? "none" : "contents" }, children: [
3211
+ phase === "type" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3212
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3213
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3214
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3215
+ "div",
3216
+ {
3217
+ style: {
3218
+ padding: "9px 12px",
3219
+ borderRadius: "12px 12px 12px 4px",
3220
+ backgroundColor: t.bgSecondary,
3221
+ fontSize: "13px",
3222
+ lineHeight: 1.55,
3223
+ maxWidth: "88%"
3224
+ },
3225
+ children: [
3226
+ reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 w` : "W",
3227
+ "hat do you want to do? Pick one and I'll take it from there."
3228
+ ]
3229
+ }
3230
+ )
3231
+ ] }),
3232
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2474
3233
  "div",
2475
3234
  {
2476
3235
  style: {
2477
- padding: "9px 12px",
2478
- borderRadius: "12px 12px 12px 4px",
2479
- backgroundColor: t.bgSecondary,
2480
- fontSize: "13px",
2481
- lineHeight: 1.55,
2482
- maxWidth: "88%"
3236
+ display: "flex",
3237
+ flexWrap: "wrap",
3238
+ gap: "6px",
3239
+ paddingLeft: "32px"
2483
3240
  },
2484
- children: [
2485
- reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 w` : "W",
2486
- "hat do you want to do? Pick one and I'll take it from there."
2487
- ]
3241
+ children: tiles.map((tile) => {
3242
+ const rate = tile === "RATING";
3243
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3244
+ "button",
3245
+ {
3246
+ type: "button",
3247
+ "data-gg-chip": "",
3248
+ "data-gg-tile": tile,
3249
+ onClick: () => pickType(tile),
3250
+ style: {
3251
+ display: "inline-flex",
3252
+ alignItems: "center",
3253
+ gap: "5px",
3254
+ // The rating is the only tile that isn't a report, so
3255
+ // it wears the star colour the dialog's hero row uses
3256
+ // rather than hiding among the report types.
3257
+ border: `1px solid ${rate ? "rgba(245,158,11,0.5)" : t.inputBorder}`,
3258
+ background: rate ? "rgba(245,158,11,0.08)" : "transparent",
3259
+ color: rate ? "#f59e0b" : t.textMuted,
3260
+ borderRadius: "999px",
3261
+ // 8px vertical keeps the tap target at ~32px.
3262
+ padding: "8px 12px",
3263
+ fontSize: "12px",
3264
+ fontWeight: rate ? 600 : 400,
3265
+ fontFamily: "inherit",
3266
+ cursor: "pointer"
3267
+ },
3268
+ children: rate ? "\u2605 Rate us" : getTypeLabel(tile)
3269
+ },
3270
+ tile
3271
+ );
3272
+ })
2488
3273
  }
2489
3274
  )
2490
3275
  ] }),
2491
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2492
- "div",
2493
- {
2494
- style: {
2495
- display: "flex",
2496
- flexWrap: "wrap",
2497
- gap: "6px",
2498
- paddingLeft: "32px"
2499
- },
2500
- children: tiles.map((tile) => {
2501
- const rate = tile === "RATING";
2502
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3276
+ phase === "severity" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3277
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3278
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3279
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3280
+ "div",
3281
+ {
3282
+ style: {
3283
+ padding: "9px 12px",
3284
+ borderRadius: "12px 12px 12px 4px",
3285
+ backgroundColor: t.bgSecondary,
3286
+ fontSize: "13px",
3287
+ lineHeight: 1.55,
3288
+ maxWidth: "88%"
3289
+ },
3290
+ children: "How bad is it?"
3291
+ }
3292
+ )
3293
+ ] }),
3294
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3295
+ "div",
3296
+ {
3297
+ role: "radiogroup",
3298
+ "aria-label": "Severity",
3299
+ style: {
3300
+ display: "flex",
3301
+ flexWrap: "wrap",
3302
+ gap: "6px",
3303
+ paddingLeft: "32px"
3304
+ },
3305
+ children: SEVERITY_LEVELS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2503
3306
  "button",
2504
3307
  {
2505
3308
  type: "button",
3309
+ role: "radio",
3310
+ "aria-checked": severity === s,
2506
3311
  "data-gg-chip": "",
2507
- "data-gg-tile": tile,
2508
- onClick: () => pickType(tile),
3312
+ "data-gg-severity": s,
3313
+ onClick: () => answerSeverity(s),
2509
3314
  style: {
2510
- display: "inline-flex",
2511
- alignItems: "center",
2512
- gap: "5px",
2513
- // The rating is the only tile that isn't a report, so
2514
- // it wears the star colour the dialog's hero row uses
2515
- // rather than hiding among the report types.
2516
- border: `1px solid ${rate ? "rgba(245,158,11,0.5)" : t.inputBorder}`,
2517
- background: rate ? "rgba(245,158,11,0.08)" : "transparent",
2518
- color: rate ? "#f59e0b" : t.textMuted,
3315
+ border: `1px solid ${t.inputBorder}`,
3316
+ background: "transparent",
3317
+ color: t.textMuted,
2519
3318
  borderRadius: "999px",
2520
- // 8px vertical keeps the tap target at ~32px.
3319
+ // Same as the picker and option chips — one chip size.
2521
3320
  padding: "8px 12px",
2522
3321
  fontSize: "12px",
2523
- fontWeight: rate ? 600 : 400,
2524
3322
  fontFamily: "inherit",
2525
3323
  cursor: "pointer"
2526
3324
  },
2527
- children: rate ? "\u2605 Rate us" : getTypeLabel(tile)
3325
+ children: SEVERITY_LABELS[s]
2528
3326
  },
2529
- tile
2530
- );
2531
- })
2532
- }
2533
- )
2534
- ] }),
2535
- phase === "severity" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2536
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
2537
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2538
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2539
- "div",
2540
- {
2541
- style: {
2542
- padding: "9px 12px",
2543
- borderRadius: "12px 12px 12px 4px",
2544
- backgroundColor: t.bgSecondary,
2545
- fontSize: "13px",
2546
- lineHeight: 1.55,
2547
- maxWidth: "88%"
2548
- },
2549
- children: "How bad is it?"
3327
+ s
3328
+ ))
2550
3329
  }
2551
3330
  )
2552
3331
  ] }),
2553
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2554
- "div",
2555
- {
2556
- role: "radiogroup",
2557
- "aria-label": "Severity",
2558
- style: {
2559
- display: "flex",
2560
- flexWrap: "wrap",
2561
- gap: "6px",
2562
- paddingLeft: "32px"
2563
- },
2564
- children: SEVERITY_LEVELS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2565
- "button",
3332
+ phase === "rating" && !submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3333
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3334
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3335
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3336
+ "div",
2566
3337
  {
2567
- type: "button",
2568
- role: "radio",
2569
- "aria-checked": severity === s,
2570
- "data-gg-chip": "",
2571
- "data-gg-severity": s,
2572
- onClick: () => answerSeverity(s),
2573
3338
  style: {
2574
- border: `1px solid ${t.inputBorder}`,
2575
- background: "transparent",
2576
- color: t.textMuted,
2577
- borderRadius: "999px",
2578
- // Same as the picker and option chips — one chip size.
2579
- padding: "8px 12px",
2580
- fontSize: "12px",
2581
- fontFamily: "inherit",
2582
- cursor: "pointer"
3339
+ padding: "9px 12px",
3340
+ borderRadius: "12px 12px 12px 4px",
3341
+ backgroundColor: t.bgSecondary,
3342
+ fontSize: "13px",
3343
+ lineHeight: 1.55,
3344
+ maxWidth: "88%"
2583
3345
  },
2584
- children: SEVERITY_LABELS[s]
2585
- },
2586
- s
2587
- ))
2588
- }
2589
- )
2590
- ] }),
2591
- phase === "rating" && !submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2592
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
2593
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2594
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3346
+ children: "How are we doing? Tap a star \u2014 the words are optional."
3347
+ }
3348
+ )
3349
+ ] }),
3350
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2595
3351
  "div",
2596
3352
  {
2597
3353
  style: {
2598
- padding: "9px 12px",
2599
- borderRadius: "12px 12px 12px 4px",
2600
- backgroundColor: t.bgSecondary,
2601
- fontSize: "13px",
2602
- lineHeight: 1.55,
2603
- maxWidth: "88%"
3354
+ display: "flex",
3355
+ flexDirection: "column",
3356
+ gap: "10px",
3357
+ marginLeft: "32px",
3358
+ padding: "12px",
3359
+ borderRadius: "12px",
3360
+ border: "1px solid rgba(245,158,11,0.4)",
3361
+ backgroundColor: "rgba(245,158,11,0.06)"
2604
3362
  },
2605
- children: "How are we doing? Tap a star \u2014 the words are optional."
2606
- }
2607
- )
2608
- ] }),
2609
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2610
- "div",
2611
- {
2612
- style: {
2613
- display: "flex",
2614
- flexDirection: "column",
2615
- gap: "10px",
2616
- marginLeft: "32px",
2617
- padding: "12px",
2618
- borderRadius: "12px",
2619
- border: "1px solid rgba(245,158,11,0.4)",
2620
- backgroundColor: "rgba(245,158,11,0.06)"
2621
- },
2622
- children: [
2623
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "flex", gap: "4px" }, role: "group", "aria-label": "Rating", children: [1, 2, 3, 4, 5].map((star) => {
2624
- const filled = star <= (hoveredStar || rating);
2625
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3363
+ children: [
3364
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "flex", gap: "4px" }, role: "group", "aria-label": "Rating", children: [1, 2, 3, 4, 5].map((star) => {
3365
+ const filled = star <= (hoveredStar || rating);
3366
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3367
+ "button",
3368
+ {
3369
+ type: "button",
3370
+ "aria-label": `${star} star${star > 1 ? "s" : ""}`,
3371
+ "aria-pressed": star <= rating,
3372
+ onClick: () => onRatingChange(star),
3373
+ onMouseEnter: () => setHoveredStar(star),
3374
+ onMouseLeave: () => setHoveredStar(0),
3375
+ style: {
3376
+ border: "none",
3377
+ background: "transparent",
3378
+ padding: 0,
3379
+ cursor: "pointer",
3380
+ lineHeight: 0
3381
+ },
3382
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "30", height: "30", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3383
+ "path",
3384
+ {
3385
+ 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",
3386
+ fill: filled ? "#f59e0b" : "transparent",
3387
+ stroke: filled ? "#f59e0b" : t.inputBorder,
3388
+ strokeWidth: "1.5",
3389
+ strokeLinejoin: "round"
3390
+ }
3391
+ ) })
3392
+ },
3393
+ star
3394
+ );
3395
+ }) }),
3396
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3397
+ "textarea",
3398
+ {
3399
+ value: description,
3400
+ onChange: (e) => onDescriptionChange(e.target.value),
3401
+ rows: 3,
3402
+ placeholder: "What made it good \u2014 or what let you down? (optional)",
3403
+ style: {
3404
+ width: "100%",
3405
+ padding: "10px",
3406
+ borderRadius: "8px",
3407
+ border: `1px solid ${t.inputBorder}`,
3408
+ backgroundColor: t.inputBg,
3409
+ color: t.text,
3410
+ fontSize: "13px",
3411
+ lineHeight: 1.5,
3412
+ fontFamily: "inherit",
3413
+ resize: "vertical",
3414
+ outline: "none",
3415
+ boxSizing: "border-box"
3416
+ }
3417
+ }
3418
+ ),
3419
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2626
3420
  "button",
2627
3421
  {
2628
3422
  type: "button",
2629
- "aria-label": `${star} star${star > 1 ? "s" : ""}`,
2630
- "aria-pressed": star <= rating,
2631
- onClick: () => onRatingChange(star),
2632
- onMouseEnter: () => setHoveredStar(star),
2633
- onMouseLeave: () => setHoveredStar(0),
3423
+ onClick: () => setPhase("type"),
2634
3424
  style: {
3425
+ alignSelf: "flex-start",
2635
3426
  border: "none",
2636
3427
  background: "transparent",
2637
- padding: 0,
3428
+ color: t.textMuted,
3429
+ fontSize: "12px",
3430
+ fontFamily: "inherit",
3431
+ // A bare text link is ~14px tall — half a thumb.
3432
+ padding: "9px 0",
2638
3433
  cursor: "pointer",
2639
- lineHeight: 0
3434
+ textDecoration: "underline"
2640
3435
  },
2641
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "30", height: "30", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2642
- "path",
2643
- {
2644
- 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",
2645
- fill: filled ? "#f59e0b" : "transparent",
2646
- stroke: filled ? "#f59e0b" : t.inputBorder,
2647
- strokeWidth: "1.5",
2648
- strokeLinejoin: "round"
2649
- }
2650
- ) })
2651
- },
2652
- star
2653
- );
2654
- }) }),
2655
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2656
- "textarea",
2657
- {
2658
- value: description,
2659
- onChange: (e) => onDescriptionChange(e.target.value),
2660
- rows: 3,
2661
- placeholder: "What made it good \u2014 or what let you down? (optional)",
2662
- style: {
2663
- width: "100%",
2664
- padding: "10px",
2665
- borderRadius: "8px",
2666
- border: `1px solid ${t.inputBorder}`,
2667
- backgroundColor: t.inputBg,
2668
- color: t.text,
2669
- fontSize: "13px",
2670
- lineHeight: 1.5,
2671
- fontFamily: "inherit",
2672
- resize: "vertical",
2673
- outline: "none",
2674
- boxSizing: "border-box"
3436
+ children: "Report something instead"
2675
3437
  }
2676
- }
2677
- ),
2678
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2679
- "button",
2680
- {
2681
- type: "button",
2682
- onClick: () => setPhase("type"),
2683
- style: {
2684
- alignSelf: "flex-start",
2685
- border: "none",
2686
- background: "transparent",
2687
- color: t.textMuted,
2688
- fontSize: "12px",
2689
- fontFamily: "inherit",
2690
- // A bare text link is ~14px tall — half a thumb.
2691
- padding: "9px 0",
2692
- cursor: "pointer",
2693
- textDecoration: "underline"
2694
- },
2695
- children: "Report something instead"
2696
- }
2697
- )
2698
- ]
2699
- }
2700
- )
2701
- ] }),
2702
- empty && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
2703
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
2704
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2705
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3438
+ )
3439
+ ]
3440
+ }
3441
+ )
3442
+ ] }),
3443
+ empty && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3444
+ resumable && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2706
3445
  "div",
2707
3446
  {
3447
+ "data-gg-resume-chat": "",
2708
3448
  style: {
2709
- padding: "9px 12px",
2710
- borderRadius: "12px 12px 12px 4px",
2711
- backgroundColor: t.bgSecondary,
2712
- fontSize: "13px",
2713
- lineHeight: 1.55,
2714
- maxWidth: "88%"
3449
+ display: "flex",
3450
+ flexDirection: "column",
3451
+ gap: "8px",
3452
+ padding: "10px 12px",
3453
+ borderRadius: "10px",
3454
+ border: `1px solid ${t.accent}55`,
3455
+ backgroundColor: t.bgSecondary,
3456
+ minWidth: 0
2715
3457
  },
2716
3458
  children: [
2717
- reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 t` : "T",
2718
- "ell me what went wrong in your own words. I can already see your screenshot and what you clicked, so skip the boring parts."
2719
- ]
2720
- }
2721
- )
2722
- ] }),
2723
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2724
- "div",
2725
- {
2726
- style: {
2727
- display: "flex",
2728
- flexWrap: "wrap",
2729
- gap: "6px",
2730
- paddingLeft: "32px"
2731
- },
2732
- children: STARTERS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2733
- "button",
2734
- {
2735
- type: "button",
2736
- "data-gg-chip": "",
2737
- onClick: () => {
2738
- logEvent("starter_tap", s);
2739
- send(s);
2740
- },
2741
- style: {
2742
- border: `1px solid ${t.inputBorder}`,
2743
- background: "transparent",
2744
- color: t.textMuted,
2745
- borderRadius: "999px",
2746
- padding: "6px 11px",
2747
- fontSize: "12px",
2748
- fontFamily: "inherit",
2749
- cursor: "pointer"
2750
- },
2751
- children: s
2752
- },
2753
- s
2754
- ))
2755
- }
2756
- )
2757
- ] }),
2758
- messages.map((m, i) => {
2759
- const mine = m.role === "user";
2760
- if (m.kind === "report") {
2761
- const live = phase === "draft" && i === messages.length - 1;
2762
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, children: [
2763
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2764
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2765
- "div",
2766
- {
2767
- "data-gg-draft-bubble": "",
2768
- style: {
2769
- maxWidth: "82%",
2770
- minWidth: 0,
2771
- padding: "9px 12px",
2772
- borderRadius: "12px 12px 12px 4px",
2773
- fontSize: "13px",
2774
- lineHeight: 1.55,
2775
- backgroundColor: t.bgSecondary,
2776
- color: t.text,
2777
- border: live ? "none" : `1px solid ${t.accent}66`
2778
- },
2779
- children: live ? (
2780
- // #402: "have a read" beside a countdown reads as a
2781
- // contradiction. Say what is about to happen instead.
2782
- 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."
2783
- ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3459
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
3460
+ "Pick up where you left off \xB7 ",
3461
+ chatAge(resumable.updatedAt)
3462
+ ] }),
3463
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3464
+ "span",
3465
+ {
3466
+ style: {
3467
+ fontSize: "13px",
3468
+ lineHeight: 1.45,
3469
+ overflowWrap: "anywhere"
3470
+ },
3471
+ children: resumable.title
3472
+ }
3473
+ ),
3474
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap" }, children: [
2784
3475
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2785
- "span",
3476
+ "button",
2786
3477
  {
3478
+ type: "button",
3479
+ "data-gg-resume-continue": "",
3480
+ onClick: () => resumeChat(resumable),
2787
3481
  style: {
2788
- display: "block",
2789
- fontSize: "10px",
2790
- fontWeight: 700,
2791
- letterSpacing: ".06em",
2792
- textTransform: "uppercase",
2793
- color: t.accent,
2794
- marginBottom: "4px"
3482
+ border: "none",
3483
+ borderRadius: "999px",
3484
+ backgroundColor: t.accent,
3485
+ color: t.accentText,
3486
+ padding: "7px 13px",
3487
+ fontSize: "12px",
3488
+ fontWeight: 600,
3489
+ fontFamily: "inherit",
3490
+ cursor: "pointer"
2795
3491
  },
2796
- children: "Draft so far"
3492
+ children: "Continue this chat"
2797
3493
  }
2798
3494
  ),
2799
3495
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2800
- "span",
3496
+ "button",
2801
3497
  {
3498
+ type: "button",
3499
+ onClick: () => setShowChats(true),
2802
3500
  style: {
2803
- display: "block",
2804
- whiteSpace: "pre-wrap",
2805
- wordBreak: "break-word",
2806
- maxHeight: "160px",
2807
- overflowY: "auto",
2808
- color: t.textMuted
3501
+ border: `1px solid ${t.inputBorder}`,
3502
+ borderRadius: "999px",
3503
+ background: "transparent",
3504
+ color: t.textMuted,
3505
+ padding: "7px 13px",
3506
+ fontSize: "12px",
3507
+ fontFamily: "inherit",
3508
+ cursor: "pointer"
2809
3509
  },
2810
- children: m.content
3510
+ children: "All chats"
2811
3511
  }
2812
3512
  )
2813
3513
  ] })
3514
+ ]
3515
+ }
3516
+ ),
3517
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3518
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3519
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3520
+ "div",
3521
+ {
3522
+ style: {
3523
+ padding: "9px 12px",
3524
+ borderRadius: "12px 12px 12px 4px",
3525
+ backgroundColor: t.bgSecondary,
3526
+ fontSize: "13px",
3527
+ lineHeight: 1.55,
3528
+ maxWidth: "88%"
3529
+ },
3530
+ children: [
3531
+ reporterName ? `Hi ${reporterName.split(" ")[0]} \u2014 t` : "T",
3532
+ "ell me what went wrong in your own words. I can already see your screenshot and what you clicked, so skip the boring parts."
3533
+ ]
2814
3534
  }
2815
3535
  )
2816
- ] }, i);
2817
- }
2818
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2819
- "div",
2820
- {
2821
- style: {
2822
- display: "flex",
2823
- gap: "8px",
2824
- minWidth: 0,
2825
- flexDirection: mine ? "row-reverse" : "row"
2826
- },
2827
- children: [
2828
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: m.role, theme: t, initials: initials2 }),
2829
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2830
- "div",
2831
- {
2832
- style: {
2833
- maxWidth: "82%",
2834
- minWidth: 0,
2835
- padding: "9px 12px",
2836
- borderRadius: mine ? "12px 12px 4px 12px" : "12px 12px 12px 4px",
2837
- fontSize: "13px",
2838
- lineHeight: 1.55,
2839
- // Markdown owns the assistant's line breaks and lists.
2840
- whiteSpace: mine ? "pre-wrap" : "normal",
2841
- wordBreak: "break-word",
2842
- backgroundColor: mine ? t.accent : t.bgSecondary,
2843
- color: mine ? t.accentText : t.text
2844
- },
2845
- children: mine ? m.content : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatMarkdown, { text: m.content, theme: t })
2846
- }
2847
- )
2848
- ]
2849
- },
2850
- i
2851
- );
2852
- }),
2853
- duplicate && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: renderKnownIssue({ title: false }) }),
2854
- glitchgrabOffer && onReportGlitchgrabProblem && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2855
- "div",
2856
- {
2857
- "data-gg-glitchgrab-offer": "",
2858
- style: {
2859
- display: "flex",
2860
- flexDirection: "column",
2861
- gap: "8px",
2862
- padding: "10px",
2863
- borderRadius: "8px",
2864
- border: `1px solid ${t.accent}55`,
2865
- backgroundColor: `${t.accent}14`,
2866
- fontSize: "12px",
2867
- lineHeight: 1.5
2868
- },
2869
- children: [
2870
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: t.text, fontWeight: 600 }, children: "This is about Glitchgrab, not this app" }),
2871
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: t.textMuted }, children: "It goes to the team that builds this reporter, with your chat attached." }),
2872
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: [
2873
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3536
+ ] }),
3537
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3538
+ "div",
3539
+ {
3540
+ style: {
3541
+ display: "flex",
3542
+ flexWrap: "wrap",
3543
+ gap: "6px",
3544
+ paddingLeft: "32px"
3545
+ },
3546
+ children: STARTERS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2874
3547
  "button",
2875
3548
  {
2876
3549
  type: "button",
2877
- "data-gg-glitchgrab-offer-send": "",
3550
+ "data-gg-chip": "",
2878
3551
  onClick: () => {
2879
- logEvent("glitchgrab_offer_tap");
2880
- setGlitchgrabOffer(false);
2881
- onReportGlitchgrabProblem(
2882
- messages.filter((m) => m.role === "user").map((m) => m.content.trim()).filter(Boolean).join("\n\n")
2883
- );
3552
+ logEvent("starter_tap", s);
3553
+ send(s);
2884
3554
  },
2885
3555
  style: {
2886
- padding: "8px 12px",
3556
+ border: `1px solid ${t.inputBorder}`,
3557
+ background: "transparent",
3558
+ color: t.textMuted,
2887
3559
  borderRadius: "999px",
2888
- border: "none",
2889
- backgroundColor: t.accent,
2890
- color: t.accentText,
3560
+ padding: "6px 11px",
2891
3561
  fontSize: "12px",
2892
- fontWeight: 700,
2893
3562
  fontFamily: "inherit",
2894
- cursor: "pointer",
2895
- // Both stretch, so on a phone each wrapped line reads as
2896
- // a choice rather than one button left stranded.
2897
- flex: "1 1 auto"
3563
+ cursor: "pointer"
2898
3564
  },
2899
- children: "Send to the Glitchgrab team"
2900
- }
2901
- ),
3565
+ children: s
3566
+ },
3567
+ s
3568
+ ))
3569
+ }
3570
+ )
3571
+ ] }),
3572
+ messages.map((m, i) => {
3573
+ const mine = m.role === "user";
3574
+ if (m.kind === "report") {
3575
+ const live = phase === "draft" && i === messages.length - 1;
3576
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, children: [
3577
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2902
3578
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2903
- "button",
3579
+ "div",
2904
3580
  {
2905
- type: "button",
2906
- "data-gg-glitchgrab-offer-no": "",
2907
- onClick: () => {
2908
- logEvent("glitchgrab_offer_declined");
2909
- send("No \u2014 it's about this app, not the reporting tool");
2910
- },
3581
+ "data-gg-draft-bubble": "",
2911
3582
  style: {
2912
- padding: "8px 12px",
2913
- borderRadius: "999px",
2914
- border: `1px solid ${t.inputBorder}`,
2915
- background: "transparent",
2916
- color: t.textMuted,
2917
- fontSize: "12px",
2918
- fontFamily: "inherit",
2919
- cursor: "pointer",
2920
- flex: "1 1 auto"
3583
+ maxWidth: "82%",
3584
+ minWidth: 0,
3585
+ padding: "9px 12px",
3586
+ borderRadius: "12px 12px 12px 4px",
3587
+ fontSize: "13px",
3588
+ lineHeight: 1.55,
3589
+ backgroundColor: t.bgSecondary,
3590
+ color: t.text,
3591
+ border: live ? "none" : `1px solid ${t.accent}66`
2921
3592
  },
2922
- children: "No, it's about this app"
3593
+ children: live ? (
3594
+ // #402: "have a read" beside a countdown reads as a
3595
+ // contradiction. Say what is about to happen instead.
3596
+ 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."
3597
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3598
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3599
+ "span",
3600
+ {
3601
+ style: {
3602
+ display: "block",
3603
+ fontSize: "10px",
3604
+ fontWeight: 700,
3605
+ letterSpacing: ".06em",
3606
+ textTransform: "uppercase",
3607
+ color: t.accent,
3608
+ marginBottom: "4px"
3609
+ },
3610
+ children: "Draft so far"
3611
+ }
3612
+ ),
3613
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3614
+ "span",
3615
+ {
3616
+ style: {
3617
+ display: "block",
3618
+ whiteSpace: "pre-wrap",
3619
+ wordBreak: "break-word",
3620
+ maxHeight: "160px",
3621
+ overflowY: "auto",
3622
+ color: t.textMuted
3623
+ },
3624
+ children: m.content
3625
+ }
3626
+ )
3627
+ ] })
2923
3628
  }
2924
3629
  )
2925
- ] })
2926
- ]
2927
- }
2928
- ) }),
2929
- options.length > 0 && !busy && phase === "chat" && !glitchgrabOffer && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2930
- "div",
2931
- {
2932
- style: {
2933
- display: "flex",
2934
- // Long labels wrapped into a ragged row that always stranded
2935
- // the last chip on a line of its own. Past ~24 characters
2936
- // these stop being chips and become a choice list, so they
2937
- // stack full width and read like one.
2938
- flexDirection: options.some((o) => o.length > 24) ? "column" : "row",
2939
- alignItems: options.some((o) => o.length > 24) ? "stretch" : "flex-start",
2940
- flexWrap: "wrap",
2941
- gap: "6px",
2942
- paddingLeft: "32px"
2943
- },
2944
- children: options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2945
- "button",
3630
+ ] }, i);
3631
+ }
3632
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3633
+ "div",
2946
3634
  {
2947
- type: "button",
2948
- "data-gg-chip": "",
2949
- "data-gg-option": "",
2950
- onClick: () => {
2951
- logEvent("option_tap", option);
2952
- send(option, option === CONFIRM_OPTION);
2953
- },
2954
3635
  style: {
2955
- border: `1px solid ${t.inputBorder}`,
2956
- background: "transparent",
2957
- color: t.textMuted,
2958
- borderRadius: "999px",
2959
- // Same 8px as the picker chips — one tap target size in
2960
- // this sheet, not two.
2961
- padding: "8px 12px",
2962
- fontSize: "12px",
2963
- fontFamily: "inherit",
2964
- cursor: "pointer",
2965
- textAlign: "left"
3636
+ display: "flex",
3637
+ gap: "8px",
3638
+ minWidth: 0,
3639
+ flexDirection: mine ? "row-reverse" : "row"
2966
3640
  },
2967
- children: option
3641
+ children: [
3642
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: m.role, theme: t, initials: initials2 }),
3643
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3644
+ "div",
3645
+ {
3646
+ style: {
3647
+ maxWidth: "82%",
3648
+ minWidth: 0,
3649
+ padding: "9px 12px",
3650
+ borderRadius: mine ? "12px 12px 4px 12px" : "12px 12px 12px 4px",
3651
+ fontSize: "13px",
3652
+ lineHeight: 1.55,
3653
+ // Markdown owns the assistant's line breaks and lists.
3654
+ whiteSpace: mine ? "pre-wrap" : "normal",
3655
+ wordBreak: "break-word",
3656
+ backgroundColor: mine ? t.accent : t.bgSecondary,
3657
+ color: mine ? t.accentText : t.text
3658
+ },
3659
+ children: mine ? m.content : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatMarkdown, { text: m.content, theme: t })
3660
+ }
3661
+ )
3662
+ ]
2968
3663
  },
2969
- option
2970
- ))
2971
- }
2972
- ),
2973
- busy && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, role: "status", "aria-label": "Thinking", children: [
2974
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
2975
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2976
- "span",
3664
+ i
3665
+ );
3666
+ }),
3667
+ duplicate && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: renderKnownIssue({ title: false }) }),
3668
+ glitchgrabOffer && onReportGlitchgrabProblem && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3669
+ "div",
2977
3670
  {
3671
+ "data-gg-glitchgrab-offer": "",
2978
3672
  style: {
2979
- display: "inline-flex",
2980
- alignItems: "center",
2981
- gap: "4px",
2982
- padding: "12px",
2983
- borderRadius: "12px 12px 12px 4px",
2984
- backgroundColor: t.bgSecondary
3673
+ display: "flex",
3674
+ flexDirection: "column",
3675
+ gap: "8px",
3676
+ padding: "10px",
3677
+ borderRadius: "8px",
3678
+ border: `1px solid ${t.accent}55`,
3679
+ backgroundColor: `${t.accent}14`,
3680
+ fontSize: "12px",
3681
+ lineHeight: 1.5
2985
3682
  },
2986
- children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2987
- "span",
2988
- {
2989
- style: {
2990
- width: "5px",
2991
- height: "5px",
2992
- borderRadius: "50%",
2993
- backgroundColor: t.textMuted,
2994
- animation: `gg-dot 1.1s ease-in-out ${i * 0.15}s infinite`
2995
- }
2996
- },
2997
- i
2998
- ))
3683
+ children: [
3684
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: t.text, fontWeight: 600 }, children: "This is about Glitchgrab, not this app" }),
3685
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: t.textMuted }, children: "It goes to the team that builds this reporter, with your chat attached." }),
3686
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: [
3687
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3688
+ "button",
3689
+ {
3690
+ type: "button",
3691
+ "data-gg-glitchgrab-offer-send": "",
3692
+ onClick: () => {
3693
+ logEvent("glitchgrab_offer_tap");
3694
+ setGlitchgrabOffer(false);
3695
+ onReportGlitchgrabProblem(
3696
+ messages.filter((m) => m.role === "user").map((m) => m.content.trim()).filter(Boolean).join("\n\n")
3697
+ );
3698
+ },
3699
+ style: {
3700
+ padding: "8px 12px",
3701
+ borderRadius: "999px",
3702
+ border: "none",
3703
+ backgroundColor: t.accent,
3704
+ color: t.accentText,
3705
+ fontSize: "12px",
3706
+ fontWeight: 700,
3707
+ fontFamily: "inherit",
3708
+ cursor: "pointer",
3709
+ // Both stretch, so on a phone each wrapped line reads as
3710
+ // a choice rather than one button left stranded.
3711
+ flex: "1 1 auto"
3712
+ },
3713
+ children: "Send to the Glitchgrab team"
3714
+ }
3715
+ ),
3716
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3717
+ "button",
3718
+ {
3719
+ type: "button",
3720
+ "data-gg-glitchgrab-offer-no": "",
3721
+ onClick: () => {
3722
+ logEvent("glitchgrab_offer_declined");
3723
+ send("No \u2014 it's about this app, not the reporting tool");
3724
+ },
3725
+ style: {
3726
+ padding: "8px 12px",
3727
+ borderRadius: "999px",
3728
+ border: `1px solid ${t.inputBorder}`,
3729
+ background: "transparent",
3730
+ color: t.textMuted,
3731
+ fontSize: "12px",
3732
+ fontFamily: "inherit",
3733
+ cursor: "pointer",
3734
+ flex: "1 1 auto"
3735
+ },
3736
+ children: "No, it's about this app"
3737
+ }
3738
+ )
3739
+ ] })
3740
+ ]
2999
3741
  }
3000
- )
3001
- ] }),
3002
- phase === "draft" && !submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3003
- "div",
3004
- {
3005
- style: {
3006
- border: `1px solid ${t.accent}`,
3007
- borderRadius: "12px",
3008
- backgroundColor: t.bgSecondary,
3009
- padding: "12px",
3010
- display: "flex",
3011
- flexDirection: "column",
3012
- gap: "10px",
3013
- minWidth: 0,
3014
- boxSizing: "border-box"
3015
- },
3016
- children: [
3017
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3018
- "span",
3019
- {
3020
- style: {
3021
- fontSize: "10px",
3022
- fontWeight: 700,
3023
- letterSpacing: ".06em",
3024
- textTransform: "uppercase",
3025
- color: t.accent
3026
- },
3027
- children: duplicate ? `What we'll add to #${duplicate.number}${duplicate.kind === "related" ? " as a related request" : ""} \xB7 edit anything` : "Your report \xB7 edit anything"
3028
- }
3029
- ),
3030
- renderKnownIssue({
3031
- title: true,
3032
- footer: "Your details will be added to it instead of opening a new issue."
3033
- }),
3034
- !duplicate && issueTitle && // Wraps rather than scrolls sideways: a title cut off at the
3035
- // box edge is the very thing #543 is about. One line of text,
3036
- // so Enter is swallowed.
3037
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3038
- "textarea",
3742
+ ) }),
3743
+ options.length > 0 && !busy && phase === "chat" && !glitchgrabOffer && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3744
+ "div",
3745
+ {
3746
+ style: {
3747
+ display: "flex",
3748
+ // Long labels wrapped into a ragged row that always stranded
3749
+ // the last chip on a line of its own. Past ~24 characters
3750
+ // these stop being chips and become a choice list, so they
3751
+ // stack full width and read like one.
3752
+ flexDirection: options.some((o) => o.length > 24) ? "column" : "row",
3753
+ alignItems: options.some((o) => o.length > 24) ? "stretch" : "flex-start",
3754
+ flexWrap: "wrap",
3755
+ gap: "6px",
3756
+ paddingLeft: "32px"
3757
+ },
3758
+ children: options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3759
+ "button",
3039
3760
  {
3040
- "aria-label": "Title",
3041
- value: issueTitle,
3042
- maxLength: 120,
3043
- rows: 2,
3044
- onKeyDown: (e) => {
3045
- if (e.key === "Enter") e.preventDefault();
3046
- },
3047
- onChange: (e) => {
3048
- stopAutoFile("edited the title");
3049
- onIssueTitleChange(e.target.value.replace(/[\r\n]+/g, " "));
3761
+ type: "button",
3762
+ "data-gg-chip": "",
3763
+ "data-gg-option": "",
3764
+ onClick: () => {
3765
+ logEvent("option_tap", option);
3766
+ send(option, option === CONFIRM_OPTION);
3050
3767
  },
3051
3768
  style: {
3052
- width: "100%",
3053
- padding: "8px 10px",
3054
- lineHeight: 1.45,
3055
- resize: "none",
3056
- borderRadius: "8px",
3057
3769
  border: `1px solid ${t.inputBorder}`,
3058
- backgroundColor: t.inputBg,
3059
- color: t.text,
3060
- fontSize: "13px",
3061
- fontWeight: 600,
3770
+ background: "transparent",
3771
+ color: t.textMuted,
3772
+ borderRadius: "999px",
3773
+ // Same 8px as the picker chips — one tap target size in
3774
+ // this sheet, not two.
3775
+ padding: "8px 12px",
3776
+ fontSize: "12px",
3062
3777
  fontFamily: "inherit",
3063
- outline: "none",
3064
- boxSizing: "border-box"
3065
- }
3066
- }
3067
- ),
3068
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3069
- "textarea",
3070
- {
3071
- ref: draftRef,
3072
- value: description,
3073
- onChange: (e) => {
3074
- stopAutoFile("edited the draft");
3075
- onDescriptionChange(e.target.value);
3778
+ cursor: "pointer",
3779
+ textAlign: "left"
3076
3780
  },
3077
- rows: 6,
3078
- style: {
3079
- width: "100%",
3080
- minHeight: "120px",
3081
- padding: "10px",
3082
- borderRadius: "8px",
3083
- border: `1px solid ${t.inputBorder}`,
3084
- backgroundColor: t.inputBg,
3085
- color: t.text,
3086
- fontSize: "13px",
3087
- lineHeight: 1.55,
3088
- fontFamily: "inherit",
3089
- resize: "vertical",
3090
- outline: "none",
3091
- boxSizing: "border-box"
3092
- }
3093
- }
3094
- ),
3095
- severityAsked && autoFileLeft === null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
3096
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3781
+ children: option
3782
+ },
3783
+ option
3784
+ ))
3785
+ }
3786
+ ),
3787
+ busy && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, role: "status", "aria-label": "Thinking", children: [
3788
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3789
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3790
+ "span",
3791
+ {
3792
+ style: {
3793
+ display: "inline-flex",
3794
+ alignItems: "center",
3795
+ // The "still working" line must wrap on a phone, not push
3796
+ // the bubble past the sheet's edge.
3797
+ flexWrap: "wrap",
3798
+ minWidth: 0,
3799
+ gap: "4px",
3800
+ padding: "12px",
3801
+ borderRadius: "12px 12px 12px 4px",
3802
+ backgroundColor: t.bgSecondary
3803
+ },
3804
+ children: [
3805
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3806
+ "span",
3807
+ {
3808
+ style: {
3809
+ width: "5px",
3810
+ height: "5px",
3811
+ borderRadius: "50%",
3812
+ backgroundColor: t.textMuted,
3813
+ animation: `gg-dot 1.1s ease-in-out ${i * 0.15}s infinite`
3814
+ }
3815
+ },
3816
+ i
3817
+ )),
3818
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ThinkingClock, { color: t.textMuted })
3819
+ ]
3820
+ }
3821
+ )
3822
+ ] }),
3823
+ phase === "draft" && !submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3824
+ "div",
3825
+ {
3826
+ style: {
3827
+ border: `1px solid ${t.accent}`,
3828
+ borderRadius: "12px",
3829
+ backgroundColor: t.bgSecondary,
3830
+ padding: "12px",
3831
+ display: "flex",
3832
+ flexDirection: "column",
3833
+ gap: "10px",
3834
+ minWidth: 0,
3835
+ boxSizing: "border-box"
3836
+ },
3837
+ children: [
3838
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3097
3839
  "span",
3098
3840
  {
3099
- style: {
3100
- fontSize: "11.5px",
3101
- color: t.textMuted,
3102
- display: "block",
3103
- marginBottom: "6px"
3841
+ style: {
3842
+ fontSize: "10px",
3843
+ fontWeight: 700,
3844
+ letterSpacing: ".06em",
3845
+ textTransform: "uppercase",
3846
+ color: t.accent
3847
+ },
3848
+ children: duplicate ? `What we'll add to #${duplicate.number}${duplicate.kind === "related" ? " as a related request" : ""} \xB7 edit anything` : "Your report \xB7 edit anything"
3849
+ }
3850
+ ),
3851
+ renderKnownIssue({
3852
+ title: true,
3853
+ footer: "Your details will be added to it instead of opening a new issue."
3854
+ }),
3855
+ !duplicate && issueTitle && // Wraps rather than scrolls sideways: a title cut off at the
3856
+ // box edge is the very thing #543 is about. One line of text,
3857
+ // so Enter is swallowed.
3858
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3859
+ "textarea",
3860
+ {
3861
+ "aria-label": "Title",
3862
+ value: issueTitle,
3863
+ maxLength: 120,
3864
+ rows: 2,
3865
+ onKeyDown: (e) => {
3866
+ if (e.key === "Enter") e.preventDefault();
3104
3867
  },
3105
- children: [
3106
- "How bad is it?",
3107
- !severity && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3108
- "span",
3109
- {
3110
- style: {
3111
- marginLeft: "6px",
3112
- fontSize: "11px",
3113
- color: severityRefused ? "#ef4444" : t.textMuted
3114
- },
3115
- children: "\xB7 required"
3116
- }
3117
- )
3118
- ]
3868
+ onChange: (e) => {
3869
+ stopAutoFile("edited the title");
3870
+ onIssueTitleChange(e.target.value.replace(/[\r\n]+/g, " "));
3871
+ },
3872
+ style: {
3873
+ width: "100%",
3874
+ padding: "8px 10px",
3875
+ lineHeight: 1.45,
3876
+ resize: "none",
3877
+ borderRadius: "8px",
3878
+ border: `1px solid ${t.inputBorder}`,
3879
+ backgroundColor: t.inputBg,
3880
+ color: t.text,
3881
+ fontSize: "13px",
3882
+ fontWeight: 600,
3883
+ fontFamily: "inherit",
3884
+ outline: "none",
3885
+ boxSizing: "border-box"
3886
+ }
3119
3887
  }
3120
3888
  ),
3121
3889
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3122
- "div",
3890
+ "textarea",
3123
3891
  {
3124
- role: "radiogroup",
3125
- "aria-label": "Severity",
3126
- style: { display: "flex", gap: "6px" },
3127
- children: SEVERITY_LEVELS.map((s) => {
3128
- const on = severity === s;
3129
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3130
- "button",
3131
- {
3132
- type: "button",
3133
- onClick: () => onSeverityChange(s),
3134
- role: "radio",
3135
- "aria-checked": on,
3136
- style: {
3137
- flex: 1,
3138
- minWidth: 0,
3139
- padding: "8px 2px",
3140
- borderRadius: "8px",
3141
- border: `1px solid ${on ? t.accent : severityRefused ? "#ef4444" : t.inputBorder}`,
3142
- backgroundColor: on ? t.accent : "transparent",
3143
- color: on ? t.accentText : t.textMuted,
3144
- fontSize: "12px",
3145
- fontWeight: 600,
3146
- fontFamily: "inherit",
3147
- whiteSpace: "nowrap",
3148
- overflow: "hidden",
3149
- textOverflow: "ellipsis",
3150
- cursor: "pointer"
3151
- },
3152
- children: SEVERITY_LABELS[s]
3153
- },
3154
- s
3155
- );
3156
- })
3892
+ ref: draftRef,
3893
+ value: description,
3894
+ onChange: (e) => {
3895
+ stopAutoFile("edited the draft");
3896
+ onDescriptionChange(e.target.value);
3897
+ },
3898
+ rows: 6,
3899
+ style: {
3900
+ width: "100%",
3901
+ minHeight: "120px",
3902
+ padding: "10px",
3903
+ borderRadius: "8px",
3904
+ border: `1px solid ${t.inputBorder}`,
3905
+ backgroundColor: t.inputBg,
3906
+ color: t.text,
3907
+ fontSize: "13px",
3908
+ lineHeight: 1.55,
3909
+ fontFamily: "inherit",
3910
+ resize: "vertical",
3911
+ outline: "none",
3912
+ boxSizing: "border-box"
3913
+ }
3157
3914
  }
3158
- )
3159
- ] }),
3160
- attachmentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
3161
- attachmentCount,
3162
- " ",
3163
- attachmentCount === 1 ? "attachment" : "attachments",
3164
- " will be sent with this."
3165
- ] })
3166
- ]
3167
- }
3168
- ),
3169
- phase === "solved" && solved && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3170
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3171
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3915
+ ),
3916
+ severityAsked && autoFileLeft === null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
3917
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3918
+ "span",
3919
+ {
3920
+ style: {
3921
+ fontSize: "11.5px",
3922
+ color: t.textMuted,
3923
+ display: "block",
3924
+ marginBottom: "6px"
3925
+ },
3926
+ children: [
3927
+ "How bad is it?",
3928
+ !severity && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3929
+ "span",
3930
+ {
3931
+ style: {
3932
+ marginLeft: "6px",
3933
+ fontSize: "11px",
3934
+ color: severityRefused ? "#ef4444" : t.textMuted
3935
+ },
3936
+ children: "\xB7 required"
3937
+ }
3938
+ )
3939
+ ]
3940
+ }
3941
+ ),
3942
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3943
+ "div",
3944
+ {
3945
+ role: "radiogroup",
3946
+ "aria-label": "Severity",
3947
+ style: { display: "flex", gap: "6px" },
3948
+ children: SEVERITY_LEVELS.map((s) => {
3949
+ const on = severity === s;
3950
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3951
+ "button",
3952
+ {
3953
+ type: "button",
3954
+ onClick: () => onSeverityChange(s),
3955
+ role: "radio",
3956
+ "aria-checked": on,
3957
+ style: {
3958
+ flex: 1,
3959
+ minWidth: 0,
3960
+ padding: "8px 2px",
3961
+ borderRadius: "8px",
3962
+ border: `1px solid ${on ? t.accent : severityRefused ? "#ef4444" : t.inputBorder}`,
3963
+ backgroundColor: on ? t.accent : "transparent",
3964
+ color: on ? t.accentText : t.textMuted,
3965
+ fontSize: "12px",
3966
+ fontWeight: 600,
3967
+ fontFamily: "inherit",
3968
+ whiteSpace: "nowrap",
3969
+ overflow: "hidden",
3970
+ textOverflow: "ellipsis",
3971
+ cursor: "pointer"
3972
+ },
3973
+ children: SEVERITY_LABELS[s]
3974
+ },
3975
+ s
3976
+ );
3977
+ })
3978
+ }
3979
+ )
3980
+ ] }),
3981
+ attachmentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: [
3982
+ attachmentCount,
3983
+ " ",
3984
+ attachmentCount === 1 ? "attachment" : "attachments",
3985
+ " will be sent with this."
3986
+ ] })
3987
+ ]
3988
+ }
3989
+ ),
3990
+ phase === "solved" && solved && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [
3991
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
3992
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Avatar, { who: "assistant", theme: t }),
3993
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3994
+ "div",
3995
+ {
3996
+ style: {
3997
+ padding: "9px 12px",
3998
+ borderRadius: "12px 12px 12px 4px",
3999
+ backgroundColor: t.bgSecondary,
4000
+ fontSize: "13px",
4001
+ lineHeight: 1.55,
4002
+ maxWidth: "88%",
4003
+ whiteSpace: "pre-wrap"
4004
+ },
4005
+ children: solved
4006
+ }
4007
+ )
4008
+ ] }),
3172
4009
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3173
- "div",
4010
+ "span",
3174
4011
  {
3175
- style: {
3176
- padding: "9px 12px",
3177
- borderRadius: "12px 12px 12px 4px",
3178
- backgroundColor: t.bgSecondary,
3179
- fontSize: "13px",
3180
- lineHeight: 1.55,
3181
- maxWidth: "88%",
3182
- whiteSpace: "pre-wrap"
3183
- },
3184
- children: solved
4012
+ style: { fontSize: "11.5px", color: t.textMuted, paddingLeft: "32px" },
4013
+ children: "Nothing was filed \u2014 you are all set."
3185
4014
  }
3186
4015
  )
3187
4016
  ] }),
3188
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3189
- "span",
4017
+ submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4018
+ "div",
3190
4019
  {
3191
- style: { fontSize: "11.5px", color: t.textMuted, paddingLeft: "32px" },
3192
- children: "Nothing was filed \u2014 you are all set."
4020
+ style: {
4021
+ textAlign: "center",
4022
+ padding: "24px 0",
4023
+ color: t.accent,
4024
+ fontSize: "14px",
4025
+ fontWeight: 600
4026
+ },
4027
+ children: submittedText != null ? submittedText : `${reportTypeLabel} sent. Thank you!`
3193
4028
  }
3194
4029
  )
3195
- ] }),
3196
- submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3197
- "div",
3198
- {
3199
- style: {
3200
- textAlign: "center",
3201
- padding: "24px 0",
3202
- color: t.accent,
3203
- fontSize: "14px",
3204
- fontWeight: 600
3205
- },
3206
- children: [
3207
- reportTypeLabel,
3208
- " sent. Thank you!"
3209
- ]
3210
- }
3211
- )
3212
- ]
3213
- }
3214
- )
4030
+ ] })
4031
+ }
4032
+ )
4033
+ ]
3215
4034
  }
3216
4035
  ),
3217
- !submitted && phase !== "type" && phase !== "severity" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
4036
+ !submitted && !showChats && phase !== "type" && phase !== "severity" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3218
4037
  "div",
3219
4038
  {
3220
4039
  style: {
@@ -3272,342 +4091,363 @@ function AssistSheet({
3272
4091
  // instead of remounting, and a draft is something you can
3273
4092
  // still reply to (#360), not a form that replaced the chat.
3274
4093
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3275
- (screenshots.length > 0 || files.length > 0 || onAddFiles) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3276
- "div",
3277
- {
3278
- style: {
3279
- display: "flex",
3280
- gap: "8px",
3281
- alignItems: "center",
3282
- flexWrap: "wrap",
3283
- marginBottom: "8px"
3284
- },
3285
- children: [
3286
- screenshots.map((shot, i) => {
3287
- const read = readShots.has(shot);
3288
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3289
- "div",
3290
- {
3291
- style: {
3292
- position: "relative",
3293
- width: "40px",
3294
- height: "40px",
3295
- flexShrink: 0
3296
- },
3297
- children: [
3298
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3299
- "img",
3300
- {
3301
- src: shot,
3302
- alt: read ? "Attached image the assistant reads" : "Attached image",
3303
- "data-gg-assist-thumb": "",
3304
- style: {
3305
- display: "block",
3306
- width: "100%",
3307
- height: "100%",
3308
- objectFit: "cover",
3309
- borderRadius: "8px",
3310
- border: `1px solid ${read ? t.accent : t.inputBorder}`,
3311
- opacity: read ? 1 : 0.45,
3312
- boxSizing: "border-box"
4094
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: similarSlot ? "none" : "contents" }, children: [
4095
+ (screenshots.length > 0 || files.length > 0 || onAddFiles) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
4096
+ "div",
4097
+ {
4098
+ style: {
4099
+ display: "flex",
4100
+ gap: "8px",
4101
+ alignItems: "center",
4102
+ flexWrap: "wrap",
4103
+ marginBottom: "8px"
4104
+ },
4105
+ children: [
4106
+ screenshots.map((shot, i) => {
4107
+ const read = readShots.has(shot);
4108
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
4109
+ "div",
4110
+ {
4111
+ style: {
4112
+ position: "relative",
4113
+ width: "40px",
4114
+ height: "40px",
4115
+ flexShrink: 0
4116
+ },
4117
+ children: [
4118
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4119
+ "img",
4120
+ {
4121
+ src: shot,
4122
+ alt: read ? "Attached image the assistant reads" : "Attached image",
4123
+ "data-gg-assist-thumb": "",
4124
+ style: {
4125
+ display: "block",
4126
+ width: "100%",
4127
+ height: "100%",
4128
+ objectFit: "cover",
4129
+ borderRadius: "8px",
4130
+ border: `1px solid ${read ? t.accent : t.inputBorder}`,
4131
+ opacity: read ? 1 : 0.45,
4132
+ boxSizing: "border-box"
4133
+ }
3313
4134
  }
3314
- }
3315
- ),
3316
- onRemoveImage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3317
- "button",
3318
- {
3319
- type: "button",
3320
- "data-gg-assist-remove": "",
3321
- "aria-label": `Remove image ${i + 1}`,
3322
- title: "Remove this image",
3323
- onClick: () => {
3324
- stopAutoFile("changed attachments");
3325
- logEvent(
3326
- "image_removed",
3327
- pageShot && shot === pageShot ? "page capture" : "attached"
3328
- );
3329
- onRemoveImage(i);
3330
- },
3331
- style: {
3332
- position: "absolute",
3333
- top: "-13px",
3334
- right: "-13px",
3335
- width: "32px",
3336
- height: "32px",
3337
- border: "none",
3338
- background: "transparent",
3339
- padding: 0,
3340
- cursor: "pointer",
3341
- display: "flex",
3342
- alignItems: "center",
3343
- justifyContent: "center",
3344
- borderRadius: "50%"
3345
- },
3346
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3347
- "span",
3348
- {
3349
- "aria-hidden": "true",
3350
- style: {
3351
- width: "20px",
3352
- height: "20px",
3353
- boxSizing: "border-box",
3354
- borderRadius: "50%",
3355
- border: `2px solid ${t.bg}`,
3356
- background: "#ef4444",
3357
- color: "#fff",
3358
- display: "flex",
3359
- alignItems: "center",
3360
- justifyContent: "center"
3361
- },
3362
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3363
- "path",
3364
- {
3365
- d: "M1 1L9 9M9 1L1 9",
3366
- stroke: "currentColor",
3367
- strokeWidth: "1.6",
3368
- strokeLinecap: "round"
3369
- }
3370
- ) })
4135
+ ),
4136
+ onRemoveImage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4137
+ "button",
4138
+ {
4139
+ type: "button",
4140
+ "data-gg-assist-remove": "",
4141
+ "aria-label": `Remove image ${i + 1}`,
4142
+ title: "Remove this image",
4143
+ onClick: () => {
4144
+ stopAutoFile("changed attachments");
4145
+ logEvent(
4146
+ "image_removed",
4147
+ pageShot && shot === pageShot ? "page capture" : "attached"
4148
+ );
4149
+ onRemoveImage(i);
4150
+ },
4151
+ style: {
4152
+ position: "absolute",
4153
+ top: "-13px",
4154
+ right: "-13px",
4155
+ width: "32px",
4156
+ height: "32px",
4157
+ border: "none",
4158
+ background: "transparent",
4159
+ padding: 0,
4160
+ cursor: "pointer",
4161
+ display: "flex",
4162
+ alignItems: "center",
4163
+ justifyContent: "center",
4164
+ borderRadius: "50%"
4165
+ },
4166
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4167
+ "span",
4168
+ {
4169
+ "aria-hidden": "true",
4170
+ style: {
4171
+ width: "20px",
4172
+ height: "20px",
4173
+ boxSizing: "border-box",
4174
+ borderRadius: "50%",
4175
+ border: `2px solid ${t.bg}`,
4176
+ background: "#ef4444",
4177
+ color: "#fff",
4178
+ display: "flex",
4179
+ alignItems: "center",
4180
+ justifyContent: "center"
4181
+ },
4182
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4183
+ "path",
4184
+ {
4185
+ d: "M1 1L9 9M9 1L1 9",
4186
+ stroke: "currentColor",
4187
+ strokeWidth: "1.6",
4188
+ strokeLinecap: "round"
4189
+ }
4190
+ ) })
4191
+ }
4192
+ )
4193
+ }
4194
+ )
4195
+ ]
4196
+ },
4197
+ `${i}-${shot.slice(-16)}`
4198
+ );
4199
+ }),
4200
+ files.map((file, i) => {
4201
+ const read = readFiles.has(i);
4202
+ const pdf = isPdfFile(file.name);
4203
+ 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";
4204
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
4205
+ "div",
4206
+ {
4207
+ "data-gg-assist-file": "",
4208
+ "data-gg-assist-file-read": read ? "" : void 0,
4209
+ "data-gg-assist-file-reading": pdf && file.reading ? "" : void 0,
4210
+ title: `${file.name} \u2014 ${why}`,
4211
+ style: {
4212
+ position: "relative",
4213
+ height: "40px",
4214
+ maxWidth: "170px",
4215
+ display: "flex",
4216
+ alignItems: "center",
4217
+ gap: "6px",
4218
+ padding: "0 10px",
4219
+ borderRadius: "8px",
4220
+ border: `1px solid ${read ? t.accent : t.inputBorder}`,
4221
+ opacity: read || pdf && file.reading ? 1 : 0.55,
4222
+ boxSizing: "border-box",
4223
+ flexShrink: 0
4224
+ },
4225
+ children: [
4226
+ pdf && file.reading ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4227
+ "span",
4228
+ {
4229
+ role: "status",
4230
+ "aria-label": "Reading this PDF",
4231
+ style: {
4232
+ width: "12px",
4233
+ height: "12px",
4234
+ flexShrink: 0,
4235
+ borderRadius: "50%",
4236
+ border: `1.5px solid ${t.inputBorder}`,
4237
+ borderTopColor: t.accent,
4238
+ boxSizing: "border-box",
4239
+ animation: "gg-spin .8s linear infinite"
3371
4240
  }
3372
- )
3373
- }
3374
- )
3375
- ]
3376
- },
3377
- `${i}-${shot.slice(-16)}`
3378
- );
3379
- }),
3380
- files.map((file, i) => {
3381
- const read = readFiles.has(i);
3382
- 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";
3383
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3384
- "div",
4241
+ }
4242
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4243
+ "path",
4244
+ {
4245
+ d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z M14 2v6h6",
4246
+ stroke: t.textMuted,
4247
+ strokeWidth: "1.5",
4248
+ strokeLinecap: "round",
4249
+ strokeLinejoin: "round"
4250
+ }
4251
+ ) }),
4252
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4253
+ "span",
4254
+ {
4255
+ style: {
4256
+ fontSize: "11.5px",
4257
+ color: t.text,
4258
+ overflow: "hidden",
4259
+ textOverflow: "ellipsis",
4260
+ whiteSpace: "nowrap"
4261
+ },
4262
+ children: file.name
4263
+ }
4264
+ ),
4265
+ onRemoveFile && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4266
+ "button",
4267
+ {
4268
+ type: "button",
4269
+ "data-gg-assist-remove-file": "",
4270
+ "aria-label": `Remove file ${file.name}`,
4271
+ title: "Remove this file",
4272
+ onClick: () => {
4273
+ stopAutoFile("changed attachments");
4274
+ logEvent("file_removed", read ? "read" : "not read");
4275
+ onRemoveFile(i);
4276
+ },
4277
+ style: {
4278
+ position: "absolute",
4279
+ top: "-13px",
4280
+ right: "-13px",
4281
+ width: "32px",
4282
+ height: "32px",
4283
+ border: "none",
4284
+ background: "transparent",
4285
+ padding: 0,
4286
+ cursor: "pointer",
4287
+ display: "flex",
4288
+ alignItems: "center",
4289
+ justifyContent: "center",
4290
+ borderRadius: "50%"
4291
+ },
4292
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4293
+ "span",
4294
+ {
4295
+ "aria-hidden": "true",
4296
+ style: {
4297
+ width: "20px",
4298
+ height: "20px",
4299
+ boxSizing: "border-box",
4300
+ borderRadius: "50%",
4301
+ border: `2px solid ${t.bg}`,
4302
+ background: "#ef4444",
4303
+ color: "#fff",
4304
+ display: "flex",
4305
+ alignItems: "center",
4306
+ justifyContent: "center"
4307
+ },
4308
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4309
+ "path",
4310
+ {
4311
+ d: "M1 1L9 9M9 1L1 9",
4312
+ stroke: "currentColor",
4313
+ strokeWidth: "1.6",
4314
+ strokeLinecap: "round"
4315
+ }
4316
+ ) })
4317
+ }
4318
+ )
4319
+ }
4320
+ )
4321
+ ]
4322
+ },
4323
+ `file-${i}-${file.name}`
4324
+ );
4325
+ }),
4326
+ onAddFiles && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
4327
+ "label",
3385
4328
  {
3386
- "data-gg-assist-file": "",
3387
- "data-gg-assist-file-read": read ? "" : void 0,
3388
- title: `${file.name} \u2014 ${why}`,
4329
+ "data-gg-assist-attach": "",
4330
+ title: "Attach an image or a file (html, json, log\u2026) \u2014 or paste one",
3389
4331
  style: {
3390
- position: "relative",
4332
+ width: "40px",
3391
4333
  height: "40px",
3392
- maxWidth: "170px",
3393
- display: "flex",
3394
- alignItems: "center",
3395
- gap: "6px",
3396
- padding: "0 10px",
3397
4334
  borderRadius: "8px",
3398
- border: `1px solid ${read ? t.accent : t.inputBorder}`,
3399
- opacity: read ? 1 : 0.55,
3400
- boxSizing: "border-box",
3401
- flexShrink: 0
3402
- },
3403
- children: [
3404
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3405
- "path",
3406
- {
3407
- d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z M14 2v6h6",
3408
- stroke: t.textMuted,
3409
- strokeWidth: "1.5",
3410
- strokeLinecap: "round",
3411
- strokeLinejoin: "round"
3412
- }
3413
- ) }),
3414
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3415
- "span",
3416
- {
3417
- style: {
3418
- fontSize: "11.5px",
3419
- color: t.text,
3420
- overflow: "hidden",
3421
- textOverflow: "ellipsis",
3422
- whiteSpace: "nowrap"
3423
- },
3424
- children: file.name
3425
- }
3426
- ),
3427
- onRemoveFile && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3428
- "button",
3429
- {
3430
- type: "button",
3431
- "data-gg-assist-remove-file": "",
3432
- "aria-label": `Remove file ${file.name}`,
3433
- title: "Remove this file",
3434
- onClick: () => {
3435
- stopAutoFile("changed attachments");
3436
- logEvent("file_removed", read ? "read" : "not read");
3437
- onRemoveFile(i);
3438
- },
3439
- style: {
3440
- position: "absolute",
3441
- top: "-13px",
3442
- right: "-13px",
3443
- width: "32px",
3444
- height: "32px",
3445
- border: "none",
3446
- background: "transparent",
3447
- padding: 0,
3448
- cursor: "pointer",
3449
- display: "flex",
3450
- alignItems: "center",
3451
- justifyContent: "center",
3452
- borderRadius: "50%"
3453
- },
3454
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3455
- "span",
3456
- {
3457
- "aria-hidden": "true",
3458
- style: {
3459
- width: "20px",
3460
- height: "20px",
3461
- boxSizing: "border-box",
3462
- borderRadius: "50%",
3463
- border: `2px solid ${t.bg}`,
3464
- background: "#ef4444",
3465
- color: "#fff",
3466
- display: "flex",
3467
- alignItems: "center",
3468
- justifyContent: "center"
3469
- },
3470
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3471
- "path",
3472
- {
3473
- d: "M1 1L9 9M9 1L1 9",
3474
- stroke: "currentColor",
3475
- strokeWidth: "1.6",
3476
- strokeLinecap: "round"
3477
- }
3478
- ) })
3479
- }
3480
- )
3481
- }
3482
- )
3483
- ]
3484
- },
3485
- `file-${i}-${file.name}`
3486
- );
3487
- }),
3488
- onAddFiles && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3489
- "label",
3490
- {
3491
- "data-gg-assist-attach": "",
3492
- title: "Attach an image or a file (html, json, log\u2026) \u2014 or paste one",
3493
- style: {
3494
- width: "40px",
3495
- height: "40px",
3496
- borderRadius: "8px",
3497
- border: `1px dashed ${t.inputBorder}`,
3498
- display: "inline-flex",
3499
- alignItems: "center",
3500
- justifyContent: "center",
3501
- color: t.textMuted,
3502
- fontSize: "18px",
3503
- cursor: "pointer",
3504
- flexShrink: 0
3505
- },
3506
- children: [
3507
- "+",
3508
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3509
- "input",
3510
- {
3511
- type: "file",
3512
- accept: ASSIST_ATTACHMENT_ACCEPT,
3513
- multiple: true,
3514
- onChange: (e) => {
3515
- var _a;
3516
- const picked = Array.from((_a = e.target.files) != null ? _a : []);
3517
- if (picked.length) {
3518
- stopAutoFile("changed attachments");
3519
- const images = picked.filter((f) => f.type.startsWith("image/")).length;
3520
- if (images) logEvent("image_added", String(images));
3521
- if (picked.length > images) {
3522
- logEvent("file_added", String(picked.length - images));
4335
+ border: `1px dashed ${t.inputBorder}`,
4336
+ display: "inline-flex",
4337
+ alignItems: "center",
4338
+ justifyContent: "center",
4339
+ color: t.textMuted,
4340
+ fontSize: "18px",
4341
+ cursor: "pointer",
4342
+ flexShrink: 0
4343
+ },
4344
+ children: [
4345
+ "+",
4346
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4347
+ "input",
4348
+ {
4349
+ type: "file",
4350
+ accept: canReadPdf ? ASSIST_ATTACHMENT_ACCEPT_WITH_PDF : ASSIST_ATTACHMENT_ACCEPT,
4351
+ multiple: true,
4352
+ onChange: (e) => {
4353
+ var _a2;
4354
+ const picked = Array.from((_a2 = e.target.files) != null ? _a2 : []);
4355
+ if (picked.length) {
4356
+ stopAutoFile("changed attachments");
4357
+ const images = picked.filter((f) => f.type.startsWith("image/")).length;
4358
+ if (images) logEvent("image_added", String(images));
4359
+ if (picked.length > images) {
4360
+ logEvent("file_added", String(picked.length - images));
4361
+ }
4362
+ onAddFiles(picked);
3523
4363
  }
3524
- onAddFiles(picked);
3525
- }
3526
- e.target.value = "";
3527
- },
3528
- style: { display: "none" }
3529
- }
3530
- )
3531
- ]
3532
- }
3533
- ),
3534
- readShots.size < screenshots.length && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color: t.textMuted, fontSize: "11px" }, children: [
3535
- readShots.size,
3536
- " of ",
3537
- screenshots.length,
3538
- " read"
3539
- ] })
3540
- ]
3541
- }
3542
- ),
3543
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", alignItems: "flex-end" }, children: [
3544
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3545
- "textarea",
3546
- {
3547
- value: input,
3548
- onChange: (e) => {
3549
- stopAutoFile("typing a change");
3550
- setInput(e.target.value);
3551
- },
3552
- onKeyDown: (e) => {
3553
- if (e.key === "Enter" && !e.shiftKey) {
3554
- e.preventDefault();
3555
- sendTyped(input);
3556
- }
3557
- },
3558
- placeholder: phase === "draft" ? "Anything to change? Tell me here\u2026" : "Type your answer\u2026",
3559
- rows: 1,
3560
- disabled: busy,
3561
- autoFocus: true,
3562
- style: {
3563
- flex: 1,
3564
- minHeight: "42px",
3565
- maxHeight: "120px",
3566
- padding: "11px 12px",
3567
- borderRadius: "10px",
3568
- border: `1px solid ${t.inputBorder}`,
3569
- backgroundColor: t.inputBg,
3570
- color: t.text,
3571
- fontSize: "13px",
3572
- lineHeight: 1.4,
3573
- fontFamily: "inherit",
3574
- resize: "none",
3575
- outline: "none",
3576
- boxSizing: "border-box"
3577
- }
4364
+ e.target.value = "";
4365
+ },
4366
+ style: { display: "none" }
4367
+ }
4368
+ )
4369
+ ]
4370
+ }
4371
+ ),
4372
+ readShots.size < screenshots.length && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color: t.textMuted, fontSize: "11px" }, children: [
4373
+ readShots.size,
4374
+ " of ",
4375
+ screenshots.length,
4376
+ " read"
4377
+ ] })
4378
+ ]
3578
4379
  }
3579
4380
  ),
3580
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3581
- "button",
3582
- {
3583
- type: "button",
3584
- "data-gg-sheet-send": "",
3585
- onClick: () => sendTyped(input),
3586
- disabled: busy || !input.trim(),
3587
- "aria-label": "Send message",
3588
- "data-gg-send-message": "",
3589
- style: {
3590
- flexShrink: 0,
3591
- width: "42px",
3592
- height: "42px",
3593
- borderRadius: "10px",
3594
- border: "none",
3595
- display: "inline-flex",
3596
- alignItems: "center",
3597
- justifyContent: "center",
3598
- backgroundColor: busy || !input.trim() ? t.bgSecondary : t.accent,
3599
- color: busy || !input.trim() ? t.textMuted : t.accentText,
3600
- cursor: busy || !input.trim() ? "default" : "pointer"
3601
- },
3602
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3603
- "path",
3604
- {
3605
- d: "M3 20.5L21.5 12 3 3.5V10l12 2-12 2v6.5z",
3606
- fill: "currentColor"
4381
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", alignItems: "flex-end" }, children: [
4382
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4383
+ "textarea",
4384
+ {
4385
+ ref: composerRef,
4386
+ value: input,
4387
+ onChange: (e) => {
4388
+ stopAutoFile("typing a change");
4389
+ setInput(e.target.value);
4390
+ },
4391
+ onKeyDown: (e) => {
4392
+ if (e.key === "Enter" && !e.shiftKey) {
4393
+ e.preventDefault();
4394
+ sendTyped(input);
4395
+ }
4396
+ },
4397
+ placeholder: phase === "draft" ? "Anything to change? Tell me here\u2026" : "Type your answer\u2026",
4398
+ rows: 3,
4399
+ disabled: busy,
4400
+ autoFocus: true,
4401
+ style: {
4402
+ flex: 1,
4403
+ minHeight: "72px",
4404
+ maxHeight: "220px",
4405
+ padding: "11px 12px",
4406
+ borderRadius: "10px",
4407
+ border: `1px solid ${t.inputBorder}`,
4408
+ backgroundColor: t.inputBg,
4409
+ color: t.text,
4410
+ fontSize: "13px",
4411
+ lineHeight: 1.4,
4412
+ fontFamily: "inherit",
4413
+ resize: "none",
4414
+ outline: "none",
4415
+ boxSizing: "border-box"
3607
4416
  }
3608
- ) })
3609
- }
3610
- )
4417
+ }
4418
+ ),
4419
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4420
+ "button",
4421
+ {
4422
+ type: "button",
4423
+ "data-gg-sheet-send": "",
4424
+ onClick: () => sendTyped(input),
4425
+ disabled: busy || !input.trim(),
4426
+ "aria-label": "Send message",
4427
+ "data-gg-send-message": "",
4428
+ style: {
4429
+ flexShrink: 0,
4430
+ width: "42px",
4431
+ height: "42px",
4432
+ borderRadius: "10px",
4433
+ border: "none",
4434
+ display: "inline-flex",
4435
+ alignItems: "center",
4436
+ justifyContent: "center",
4437
+ backgroundColor: busy || !input.trim() ? t.bgSecondary : t.accent,
4438
+ color: busy || !input.trim() ? t.textMuted : t.accentText,
4439
+ cursor: busy || !input.trim() ? "default" : "pointer"
4440
+ },
4441
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4442
+ "path",
4443
+ {
4444
+ d: "M3 20.5L21.5 12 3 3.5V10l12 2-12 2v6.5z",
4445
+ fill: "currentColor"
4446
+ }
4447
+ ) })
4448
+ }
4449
+ )
4450
+ ] })
3611
4451
  ] }),
3612
4452
  phase === "draft" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3613
4453
  validationError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -3621,7 +4461,7 @@ function AssistSheet({
3621
4461
  children: validationError
3622
4462
  }
3623
4463
  ),
3624
- autoFileLeft !== null ? (
4464
+ similarSlot ? similarSlot : autoFileLeft !== null ? (
3625
4465
  // #402: "Yes, that's it" was the confirmation. The draft
3626
4466
  // files itself unless they stop it — the bar shows how
3627
4467
  // long is left, the button sends now, the × on its right
@@ -4489,20 +5329,20 @@ function initialsOf(name) {
4489
5329
  }).join("");
4490
5330
  }
4491
5331
  function ReporterChip({
4492
- reporter,
5332
+ reporter: reporter2,
4493
5333
  t
4494
5334
  }) {
4495
5335
  var _a;
4496
5336
  const [avatarFailed, setAvatarFailed] = (0, import_react.useState)(false);
4497
- const avatarUrl = (reporter == null ? void 0 : reporter.avatarUrl) || "";
5337
+ const avatarUrl = (reporter2 == null ? void 0 : reporter2.avatarUrl) || "";
4498
5338
  (0, import_react.useEffect)(() => {
4499
5339
  setAvatarFailed(false);
4500
5340
  }, [avatarUrl]);
4501
- const name = ((_a = reporter == null ? void 0 : reporter.name) == null ? void 0 : _a.trim()) || "";
5341
+ const name = ((_a = reporter2 == null ? void 0 : reporter2.name) == null ? void 0 : _a.trim()) || "";
4502
5342
  const isAnonymous = !name;
4503
5343
  const initials2 = isAnonymous ? "" : initialsOf(name);
4504
5344
  const label = isAnonymous ? "Anonymous" : name;
4505
- 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 ");
5345
+ 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 ");
4506
5346
  const showAvatarImage = !!avatarUrl && !avatarFailed;
4507
5347
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
4508
5348
  "span",
@@ -4566,7 +5406,7 @@ function ReporterChip({
4566
5406
  },
4567
5407
  children: [
4568
5408
  label,
4569
- (reporter == null ? void 0 : reporter.role) ? ` \xB7 ${reporter.role}` : ""
5409
+ (reporter2 == null ? void 0 : reporter2.role) ? ` \xB7 ${reporter2.role}` : ""
4570
5410
  ]
4571
5411
  }
4572
5412
  )
@@ -4677,16 +5517,18 @@ function ReportDialog({
4677
5517
  enhanceText: enhanceText2,
4678
5518
  assist,
4679
5519
  findSimilarIssues: findSimilarIssues2,
5520
+ readFile,
4680
5521
  assistContext = null,
4681
5522
  transcribeAudio: transcribeAudio2,
4682
5523
  types,
4683
5524
  showSeverity = true,
4684
5525
  captureScreenshot = captureDefaultScreenshot,
4685
- reporter,
5526
+ reporter: reporter2,
4686
5527
  onClose,
4687
5528
  headerSlot,
4688
5529
  reportGlitchgrabProblem,
4689
- layout = "modal"
5530
+ layout = "modal",
5531
+ chatHistoryKey: chatHistoryKey2 = null
4690
5532
  }) {
4691
5533
  var _a;
4692
5534
  const sheet = layout === "sheet";
@@ -4730,6 +5572,15 @@ function ReportDialog({
4730
5572
  const pendingShotRef = (0, import_react.useRef)(null);
4731
5573
  const openGenRef = (0, import_react.useRef)(0);
4732
5574
  const [attachments, setAttachments] = (0, import_react.useState)([]);
5575
+ const [pdfTexts, setPdfTexts] = (0, import_react.useState)({});
5576
+ const heldFiles = (0, import_react.useMemo)(
5577
+ () => attachments.map((a) => {
5578
+ const read = pdfTexts[heldFileKey(a)];
5579
+ if (!read) return a;
5580
+ return "reading" in read ? { ...a, reading: true } : { ...a, text: read.text, ...read.tooLarge ? { tooLarge: true } : {} };
5581
+ }),
5582
+ [attachments, pdfTexts]
5583
+ );
4733
5584
  const [previewIndex, setPreviewIndex] = (0, import_react.useState)(null);
4734
5585
  const [annotatingIndex, setAnnotatingIndex] = (0, import_react.useState)(null);
4735
5586
  const [previewZoomed, setPreviewZoomed] = (0, import_react.useState)(false);
@@ -4782,15 +5633,15 @@ function ReportDialog({
4782
5633
  });
4783
5634
  };
4784
5635
  applyInert();
4785
- const observer = new MutationObserver((mutations) => {
5636
+ const observer2 = new MutationObserver((mutations) => {
4786
5637
  const structural = mutations.some(
4787
5638
  (m) => m.addedNodes.length > 0 || m.removedNodes.length > 0
4788
5639
  );
4789
5640
  if (structural) applyInert();
4790
5641
  });
4791
- observer.observe(document.body, { childList: true, subtree: true });
5642
+ observer2.observe(document.body, { childList: true, subtree: true });
4792
5643
  return () => {
4793
- observer.disconnect();
5644
+ observer2.disconnect();
4794
5645
  inerted.forEach((el) => el.removeAttribute("inert"));
4795
5646
  };
4796
5647
  }, [isOpen]);
@@ -4890,10 +5741,16 @@ function ReportDialog({
4890
5741
  files.filter((file) => !file.type.startsWith("image/")).forEach((file) => {
4891
5742
  const reader = new FileReader();
4892
5743
  reader.onload = () => {
4893
- setAttachments((prev) => [
4894
- ...prev,
4895
- { name: file.name, size: file.size, dataUrl: reader.result }
4896
- ]);
5744
+ const dataUrl = reader.result;
5745
+ setAttachments((prev) => [...prev, { name: file.name, size: file.size, dataUrl }]);
5746
+ const key = heldFileKey(file);
5747
+ if (!readFile || !isPdfFile(file.name) || pdfTexts[key]) return;
5748
+ if (file.size > MAX_READABLE_PDF_BYTES) {
5749
+ setPdfTexts((prev) => ({ ...prev, [key]: { text: null, tooLarge: true } }));
5750
+ return;
5751
+ }
5752
+ setPdfTexts((prev) => ({ ...prev, [key]: { reading: true } }));
5753
+ void Promise.resolve().then(() => readFile({ name: file.name, dataUrl })).catch(() => null).then((text) => setPdfTexts((prev) => ({ ...prev, [key]: { text } })));
4897
5754
  };
4898
5755
  reader.readAsDataURL(file);
4899
5756
  });
@@ -5329,6 +6186,10 @@ function ReportDialog({
5329
6186
  stopListeningAndTranscribe();
5330
6187
  }
5331
6188
  };
6189
+ (0, import_react.useEffect)(() => {
6190
+ setSimilarIssues(null);
6191
+ setPickedSimilar(null);
6192
+ }, [description]);
5332
6193
  const handleSubmit = async (opts) => {
5333
6194
  var _a2, _b, _c;
5334
6195
  try {
@@ -5369,11 +6230,17 @@ function ReportDialog({
5369
6230
  }
5370
6231
  const duplicateOf = (_a2 = opts == null ? void 0 : opts.duplicateOf) != null ? _a2 : duplicateIssueNumber;
5371
6232
  const attachKind = (opts == null ? void 0 : opts.duplicateOf) ? (_b = opts.kind) != null ? _b : "duplicate" : duplicateKind;
5372
- if (findSimilarIssues2 && !(opts == null ? void 0 : opts.skipSimilar) && !duplicateOf && !aiConversationId) {
6233
+ if (findSimilarIssues2 && !(opts == null ? void 0 : opts.skipSimilar) && !duplicateOf) {
6234
+ const similarFiles = filesForSimilar(
6235
+ heldFiles,
6236
+ MAX_SIMILAR_FILE_CHARS,
6237
+ MAX_SIMILAR_FILES_TOTAL_CHARS
6238
+ );
5373
6239
  setIsSubmitting(true);
5374
6240
  setCheckingSimilar(true);
5375
6241
  const matches = await Promise.race([
5376
- findSimilarIssues2(description.trim()).catch(() => null),
6242
+ // A PDF still being read is left out rather than waited for.
6243
+ (similarFiles.length ? findSimilarIssues2(description.trim(), similarFiles) : findSimilarIssues2(description.trim())).catch(() => null),
5377
6244
  new Promise((resolve) => setTimeout(() => resolve(null), SIMILAR_WAIT_MS))
5378
6245
  ]);
5379
6246
  setCheckingSimilar(false);
@@ -5405,6 +6272,8 @@ function ReportDialog({
5405
6272
  if (duplicateOf) {
5406
6273
  metadata.duplicateIssueNumber = String(duplicateOf);
5407
6274
  if (attachKind === "related") metadata.duplicateKind = "related";
6275
+ } else if (opts == null ? void 0 : opts.possiblyRelated) {
6276
+ metadata.possiblyRelatedIssue = String(opts.possiblyRelated);
5408
6277
  }
5409
6278
  if (aiConversationId) {
5410
6279
  metadata.aiConversationId = aiConversationId;
@@ -5418,14 +6287,20 @@ function ReportDialog({
5418
6287
  Object.keys(metadata).length > 0 ? metadata : void 0
5419
6288
  );
5420
6289
  if (result) {
5421
- setAddedToIssue(duplicateOf && result.issueNumber === duplicateOf ? duplicateOf : null);
6290
+ setAddedToIssue(
6291
+ duplicateOf && result.issueNumber === duplicateOf ? duplicateOf : result.status === "DUPLICATE" && result.issueNumber ? result.issueNumber : null
6292
+ );
5422
6293
  setAddedAsRelated(!!duplicateOf && result.issueNumber === duplicateOf && attachKind === "related");
5423
6294
  const queued = result.status === "QUEUED" && !!result.message;
5424
6295
  setQueuedMessage(queued ? (_c = result.message) != null ? _c : null : null);
6296
+ if (chatHistoryKey2 && aiConversationId) {
6297
+ markChatFiled(chatHistoryKey2, aiConversationId, `${getTypeLabel(reportType)} sent`);
6298
+ }
5425
6299
  setSubmitted(true);
5426
6300
  setDescription("");
5427
6301
  setScreenshots([]);
5428
6302
  setAttachments([]);
6303
+ setPdfTexts({});
5429
6304
  setAiConversationId(null);
5430
6305
  setIssueTitle("");
5431
6306
  setDuplicateIssueNumber(null);
@@ -5446,6 +6321,220 @@ function ReportDialog({
5446
6321
  setAttachingTo(null);
5447
6322
  }
5448
6323
  };
6324
+ const similarCard = similarIssues ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
6325
+ "div",
6326
+ {
6327
+ "data-gg-similar": "",
6328
+ style: {
6329
+ marginTop: "12px",
6330
+ display: "flex",
6331
+ flexDirection: "column",
6332
+ gap: "10px",
6333
+ padding: "12px",
6334
+ borderRadius: "10px",
6335
+ border: "1px solid rgba(245,158,11,0.4)",
6336
+ backgroundColor: "rgba(245,158,11,0.08)"
6337
+ },
6338
+ children: [
6339
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [
6340
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: t.text, fontWeight: 600, fontSize: "13px" }, children: "Our team may already have this, or something close" }),
6341
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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." })
6342
+ ] }),
6343
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
6344
+ "div",
6345
+ {
6346
+ role: "radiogroup",
6347
+ "aria-label": "Existing issues",
6348
+ style: { display: "flex", flexDirection: "column", gap: "6px" },
6349
+ children: similarIssues.map((issue) => {
6350
+ const picked = pickedSimilar === issue.number;
6351
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
6352
+ "button",
6353
+ {
6354
+ type: "button",
6355
+ role: "radio",
6356
+ "aria-checked": picked,
6357
+ disabled: isSubmitting,
6358
+ onClick: () => setPickedSimilar(issue.number),
6359
+ style: {
6360
+ display: "flex",
6361
+ alignItems: "flex-start",
6362
+ gap: "10px",
6363
+ width: "100%",
6364
+ minHeight: "44px",
6365
+ padding: "9px 10px",
6366
+ borderRadius: "8px",
6367
+ border: `1px solid ${picked ? t.accent : t.border}`,
6368
+ boxShadow: picked ? `0 0 0 1px ${t.accent}` : "none",
6369
+ backgroundColor: t.bg,
6370
+ color: t.text,
6371
+ textAlign: "left",
6372
+ cursor: isSubmitting ? "not-allowed" : "pointer",
6373
+ fontFamily: "inherit"
6374
+ },
6375
+ children: [
6376
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
6377
+ "span",
6378
+ {
6379
+ "aria-hidden": "true",
6380
+ style: {
6381
+ flexShrink: 0,
6382
+ marginTop: "2px",
6383
+ width: "14px",
6384
+ height: "14px",
6385
+ boxSizing: "border-box",
6386
+ borderRadius: "50%",
6387
+ border: `2px solid ${picked ? t.accent : t.textMuted}`,
6388
+ backgroundColor: picked ? t.accent : "transparent",
6389
+ boxShadow: picked ? `inset 0 0 0 2px ${t.bg}` : "none"
6390
+ }
6391
+ }
6392
+ ),
6393
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
6394
+ "span",
6395
+ {
6396
+ style: {
6397
+ display: "flex",
6398
+ flexDirection: "column",
6399
+ gap: "3px",
6400
+ minWidth: 0
6401
+ },
6402
+ children: [
6403
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
6404
+ "span",
6405
+ {
6406
+ title: issue.title,
6407
+ style: {
6408
+ fontSize: "12.5px",
6409
+ lineHeight: 1.45,
6410
+ wordBreak: "break-word",
6411
+ overflow: "hidden",
6412
+ display: "-webkit-box",
6413
+ WebkitLineClamp: 3,
6414
+ WebkitBoxOrient: "vertical"
6415
+ },
6416
+ children: [
6417
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color: t.textMuted, fontWeight: 600 }, children: [
6418
+ "#",
6419
+ issue.number
6420
+ ] }),
6421
+ " ",
6422
+ issue.title
6423
+ ]
6424
+ }
6425
+ ),
6426
+ issue.status && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: issue.status })
6427
+ ]
6428
+ }
6429
+ )
6430
+ ]
6431
+ },
6432
+ issue.number
6433
+ );
6434
+ })
6435
+ }
6436
+ ),
6437
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
6438
+ "button",
6439
+ {
6440
+ type: "button",
6441
+ disabled: isSubmitting || !pickedSimilar,
6442
+ onClick: () => {
6443
+ if (!pickedSimilar) return;
6444
+ setAttachingTo(pickedSimilar);
6445
+ setDuplicateIssueNumber(pickedSimilar);
6446
+ setDuplicateKind("duplicate");
6447
+ void handleSubmit({ duplicateOf: pickedSimilar, kind: "duplicate" });
6448
+ },
6449
+ style: {
6450
+ width: "100%",
6451
+ minHeight: "40px",
6452
+ padding: "10px",
6453
+ borderRadius: "8px",
6454
+ border: "none",
6455
+ backgroundColor: isSubmitting || !pickedSimilar ? t.bgSecondary : t.accent,
6456
+ color: isSubmitting || !pickedSimilar ? t.textMuted : t.accentText,
6457
+ fontSize: "14px",
6458
+ fontWeight: 600,
6459
+ cursor: isSubmitting || !pickedSimilar ? "not-allowed" : "pointer",
6460
+ fontFamily: "inherit"
6461
+ },
6462
+ children: typeof attachingTo === "number" && duplicateKind !== "related" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
6463
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ButtonSpinner, {}),
6464
+ "Adding to #",
6465
+ attachingTo,
6466
+ "\u2026"
6467
+ ] }) : pickedSimilar ? `Add to #${pickedSimilar}` : "Pick the matching issue"
6468
+ }
6469
+ ),
6470
+ pickedSimilar && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
6471
+ "button",
6472
+ {
6473
+ type: "button",
6474
+ disabled: isSubmitting,
6475
+ onClick: () => {
6476
+ setAttachingTo(pickedSimilar);
6477
+ setDuplicateIssueNumber(pickedSimilar);
6478
+ setDuplicateKind("related");
6479
+ void handleSubmit({ duplicateOf: pickedSimilar, kind: "related" });
6480
+ },
6481
+ style: {
6482
+ width: "100%",
6483
+ minHeight: "40px",
6484
+ marginTop: "-4px",
6485
+ padding: "9px 10px",
6486
+ borderRadius: "8px",
6487
+ border: "1px solid rgba(245,158,11,0.4)",
6488
+ backgroundColor: "transparent",
6489
+ color: isSubmitting ? t.textMuted : t.text,
6490
+ fontSize: "13px",
6491
+ fontWeight: 600,
6492
+ cursor: isSubmitting ? "not-allowed" : "pointer",
6493
+ fontFamily: "inherit"
6494
+ },
6495
+ children: typeof attachingTo === "number" && duplicateKind === "related" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
6496
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ButtonSpinner, {}),
6497
+ "Adding to #",
6498
+ attachingTo,
6499
+ "\u2026"
6500
+ ] }) : `Related request \u2014 add to #${pickedSimilar}`
6501
+ }
6502
+ ),
6503
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
6504
+ "button",
6505
+ {
6506
+ type: "button",
6507
+ disabled: isSubmitting,
6508
+ onClick: () => {
6509
+ var _a2;
6510
+ setAttachingTo("new");
6511
+ void handleSubmit({ skipSimilar: true, possiblyRelated: (_a2 = similarIssues[0]) == null ? void 0 : _a2.number });
6512
+ },
6513
+ style: {
6514
+ width: "100%",
6515
+ minHeight: "40px",
6516
+ marginTop: "-4px",
6517
+ padding: "8px",
6518
+ borderRadius: "8px",
6519
+ border: "none",
6520
+ backgroundColor: "transparent",
6521
+ color: isSubmitting ? t.textMuted : t.text,
6522
+ fontSize: "13px",
6523
+ fontWeight: 500,
6524
+ textDecoration: "underline",
6525
+ textUnderlineOffset: "3px",
6526
+ cursor: isSubmitting ? "not-allowed" : "pointer",
6527
+ fontFamily: "inherit"
6528
+ },
6529
+ children: attachingTo === "new" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
6530
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ButtonSpinner, {}),
6531
+ "Sending as new\u2026"
6532
+ ] }) : "No, mine is different \u2014 send as new"
6533
+ }
6534
+ )
6535
+ ]
6536
+ }
6537
+ ) : null;
5449
6538
  if (!mounted) return null;
5450
6539
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
5451
6540
  isOpen && (0, import_react_dom.createPortal)(
@@ -6984,224 +8073,7 @@ function ReportDialog({
6984
8073
  children: validationError
6985
8074
  }
6986
8075
  ),
6987
- similarIssues ? (
6988
- // Same visual language as the assistant's "Our team is
6989
- // already on this" card, so both routes to a duplicate
6990
- // read as one feature.
6991
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
6992
- "div",
6993
- {
6994
- "data-gg-similar": "",
6995
- style: {
6996
- marginTop: "12px",
6997
- display: "flex",
6998
- flexDirection: "column",
6999
- gap: "10px",
7000
- padding: "12px",
7001
- borderRadius: "10px",
7002
- border: "1px solid rgba(245,158,11,0.4)",
7003
- backgroundColor: "rgba(245,158,11,0.08)"
7004
- },
7005
- children: [
7006
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [
7007
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: t.text, fontWeight: 600, fontSize: "13px" }, children: "Our team may already have this, or something close" }),
7008
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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." })
7009
- ] }),
7010
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7011
- "div",
7012
- {
7013
- role: "radiogroup",
7014
- "aria-label": "Existing issues",
7015
- style: { display: "flex", flexDirection: "column", gap: "6px" },
7016
- children: similarIssues.map((issue) => {
7017
- const picked = pickedSimilar === issue.number;
7018
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
7019
- "button",
7020
- {
7021
- type: "button",
7022
- role: "radio",
7023
- "aria-checked": picked,
7024
- disabled: isSubmitting,
7025
- onClick: () => setPickedSimilar(issue.number),
7026
- style: {
7027
- display: "flex",
7028
- alignItems: "flex-start",
7029
- gap: "10px",
7030
- width: "100%",
7031
- minHeight: "44px",
7032
- padding: "9px 10px",
7033
- borderRadius: "8px",
7034
- border: `1px solid ${picked ? t.accent : t.border}`,
7035
- boxShadow: picked ? `0 0 0 1px ${t.accent}` : "none",
7036
- backgroundColor: t.bg,
7037
- color: t.text,
7038
- textAlign: "left",
7039
- cursor: isSubmitting ? "not-allowed" : "pointer",
7040
- fontFamily: "inherit"
7041
- },
7042
- children: [
7043
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7044
- "span",
7045
- {
7046
- "aria-hidden": "true",
7047
- style: {
7048
- flexShrink: 0,
7049
- marginTop: "2px",
7050
- width: "14px",
7051
- height: "14px",
7052
- boxSizing: "border-box",
7053
- borderRadius: "50%",
7054
- border: `2px solid ${picked ? t.accent : t.textMuted}`,
7055
- backgroundColor: picked ? t.accent : "transparent",
7056
- boxShadow: picked ? `inset 0 0 0 2px ${t.bg}` : "none"
7057
- }
7058
- }
7059
- ),
7060
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
7061
- "span",
7062
- {
7063
- style: {
7064
- display: "flex",
7065
- flexDirection: "column",
7066
- gap: "3px",
7067
- minWidth: 0
7068
- },
7069
- children: [
7070
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
7071
- "span",
7072
- {
7073
- title: issue.title,
7074
- style: {
7075
- fontSize: "12.5px",
7076
- lineHeight: 1.45,
7077
- wordBreak: "break-word",
7078
- overflow: "hidden",
7079
- display: "-webkit-box",
7080
- WebkitLineClamp: 3,
7081
- WebkitBoxOrient: "vertical"
7082
- },
7083
- children: [
7084
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color: t.textMuted, fontWeight: 600 }, children: [
7085
- "#",
7086
- issue.number
7087
- ] }),
7088
- " ",
7089
- issue.title
7090
- ]
7091
- }
7092
- ),
7093
- issue.status && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: issue.status })
7094
- ]
7095
- }
7096
- )
7097
- ]
7098
- },
7099
- issue.number
7100
- );
7101
- })
7102
- }
7103
- ),
7104
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7105
- "button",
7106
- {
7107
- type: "button",
7108
- disabled: isSubmitting || !pickedSimilar,
7109
- onClick: () => {
7110
- if (!pickedSimilar) return;
7111
- setAttachingTo(pickedSimilar);
7112
- setDuplicateIssueNumber(pickedSimilar);
7113
- setDuplicateKind("duplicate");
7114
- void handleSubmit({ duplicateOf: pickedSimilar, kind: "duplicate" });
7115
- },
7116
- style: {
7117
- width: "100%",
7118
- minHeight: "40px",
7119
- padding: "10px",
7120
- borderRadius: "8px",
7121
- border: "none",
7122
- backgroundColor: isSubmitting || !pickedSimilar ? t.bgSecondary : t.accent,
7123
- color: isSubmitting || !pickedSimilar ? t.textMuted : t.accentText,
7124
- fontSize: "14px",
7125
- fontWeight: 600,
7126
- cursor: isSubmitting || !pickedSimilar ? "not-allowed" : "pointer",
7127
- fontFamily: "inherit"
7128
- },
7129
- children: typeof attachingTo === "number" && duplicateKind !== "related" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
7130
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ButtonSpinner, {}),
7131
- "Adding to #",
7132
- attachingTo,
7133
- "\u2026"
7134
- ] }) : pickedSimilar ? `Add to #${pickedSimilar}` : "Pick the matching issue"
7135
- }
7136
- ),
7137
- pickedSimilar && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7138
- "button",
7139
- {
7140
- type: "button",
7141
- disabled: isSubmitting,
7142
- onClick: () => {
7143
- setAttachingTo(pickedSimilar);
7144
- setDuplicateIssueNumber(pickedSimilar);
7145
- setDuplicateKind("related");
7146
- void handleSubmit({ duplicateOf: pickedSimilar, kind: "related" });
7147
- },
7148
- style: {
7149
- width: "100%",
7150
- minHeight: "40px",
7151
- marginTop: "-4px",
7152
- padding: "9px 10px",
7153
- borderRadius: "8px",
7154
- border: "1px solid rgba(245,158,11,0.4)",
7155
- backgroundColor: "transparent",
7156
- color: isSubmitting ? t.textMuted : t.text,
7157
- fontSize: "13px",
7158
- fontWeight: 600,
7159
- cursor: isSubmitting ? "not-allowed" : "pointer",
7160
- fontFamily: "inherit"
7161
- },
7162
- children: typeof attachingTo === "number" && duplicateKind === "related" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
7163
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ButtonSpinner, {}),
7164
- "Adding to #",
7165
- attachingTo,
7166
- "\u2026"
7167
- ] }) : `Related request \u2014 add to #${pickedSimilar}`
7168
- }
7169
- ),
7170
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7171
- "button",
7172
- {
7173
- type: "button",
7174
- disabled: isSubmitting,
7175
- onClick: () => {
7176
- setAttachingTo("new");
7177
- void handleSubmit({ skipSimilar: true });
7178
- },
7179
- style: {
7180
- width: "100%",
7181
- minHeight: "40px",
7182
- marginTop: "-4px",
7183
- padding: "8px",
7184
- borderRadius: "8px",
7185
- border: "none",
7186
- backgroundColor: "transparent",
7187
- color: isSubmitting ? t.textMuted : t.text,
7188
- fontSize: "13px",
7189
- fontWeight: 500,
7190
- textDecoration: "underline",
7191
- textUnderlineOffset: "3px",
7192
- cursor: isSubmitting ? "not-allowed" : "pointer",
7193
- fontFamily: "inherit"
7194
- },
7195
- children: attachingTo === "new" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
7196
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ButtonSpinner, {}),
7197
- "Sending as new\u2026"
7198
- ] }) : "No, mine is different \u2014 send as new"
7199
- }
7200
- )
7201
- ]
7202
- }
7203
- )
7204
- ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
8076
+ similarCard != null ? similarCard : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7205
8077
  "button",
7206
8078
  {
7207
8079
  type: "button",
@@ -7243,7 +8115,7 @@ function ReportDialog({
7243
8115
  gap: "12px"
7244
8116
  },
7245
8117
  children: [
7246
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ReporterChip, { reporter, t }),
8118
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ReporterChip, { reporter: reporter2, t }),
7247
8119
  reportGlitchgrabProblem ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7248
8120
  "button",
7249
8121
  {
@@ -7500,7 +8372,8 @@ function ReportDialog({
7500
8372
  pageShot,
7501
8373
  onAddFiles: addFiles,
7502
8374
  onRemoveImage: (index) => setScreenshots((prev) => prev.filter((_, i) => i !== index)),
7503
- files: attachments,
8375
+ files: heldFiles,
8376
+ canReadPdf: !!readFile,
7504
8377
  onRemoveFile: (index) => setAttachments((prev) => prev.filter((_, i) => i !== index)),
7505
8378
  attachmentCount: screenshots.length + attachments.length,
7506
8379
  context: { ...assistContext != null ? assistContext : {}, reportType },
@@ -7514,10 +8387,15 @@ function ReportDialog({
7514
8387
  setStep(2);
7515
8388
  },
7516
8389
  onDuplicateChange: (issueNumber, kind) => {
8390
+ if (issueNumber) {
8391
+ setSimilarIssues(null);
8392
+ setPickedSimilar(null);
8393
+ }
7517
8394
  setDuplicateIssueNumber(issueNumber);
7518
8395
  setDuplicateKind(issueNumber && kind === "related" ? "related" : "duplicate");
7519
8396
  },
7520
8397
  onConversationChange: setAiConversationId,
8398
+ historyKey: chatHistoryKey2,
7521
8399
  onFinish: handleClose,
7522
8400
  rating,
7523
8401
  onRatingChange: (value) => {
@@ -7525,7 +8403,7 @@ function ReportDialog({
7525
8403
  if (validationError) setValidationError(null);
7526
8404
  },
7527
8405
  projectSlot: headerSlot,
7528
- reporterName: (_a = reporter == null ? void 0 : reporter.name) != null ? _a : null,
8406
+ reporterName: (_a = reporter2 == null ? void 0 : reporter2.name) != null ? _a : null,
7529
8407
  description,
7530
8408
  onDescriptionChange: (value) => {
7531
8409
  setDescription(value);
@@ -7543,7 +8421,9 @@ function ReportDialog({
7543
8421
  severityRefused: needsSeverity,
7544
8422
  isSubmitting,
7545
8423
  submitted,
7546
- onSend: () => void handleSubmit({ skipSimilar: true }),
8424
+ onSend: () => void handleSubmit(),
8425
+ similarSlot: similarCard,
8426
+ submittedText: addedToIssue ? `Added to #${addedToIssue}${addedAsRelated ? " as a related request" : ""}. Thank you!` : null,
7547
8427
  onReportGlitchgrabProblem: reportGlitchgrabProblem ? (prefill) => {
7548
8428
  setSelfReportPrefill(prefill != null ? prefill : "");
7549
8429
  setSelfReportFrom("assist");
@@ -7837,7 +8717,9 @@ function BookingDialog({
7837
8717
  token,
7838
8718
  baseUrl,
7839
8719
  open,
7840
- onClose
8720
+ onClose,
8721
+ preview = false,
8722
+ availabilityUrl
7841
8723
  }) {
7842
8724
  var _a, _b, _c, _d, _e;
7843
8725
  const wide = useMediaQuery(WIDE_QUERY, true);
@@ -7871,10 +8753,12 @@ function BookingDialog({
7871
8753
  const [meetUrl, setMeetUrl] = (0, import_react5.useState)(null);
7872
8754
  const [busy, setBusy] = (0, import_react5.useState)(false);
7873
8755
  const [error, setError] = (0, import_react5.useState)(null);
7874
- const headers = (0, import_react5.useMemo)(
7875
- () => ({ Authorization: `Bearer ${token}`, "Content-Type": "application/json" }),
7876
- [token]
7877
- );
8756
+ const [previewEnded, setPreviewEnded] = (0, import_react5.useState)(false);
8757
+ const headers = (0, import_react5.useMemo)(() => {
8758
+ const out = { "Content-Type": "application/json" };
8759
+ if (token) out.Authorization = `Bearer ${token}`;
8760
+ return out;
8761
+ }, [token]);
7878
8762
  const loadMonth = (0, import_react5.useCallback)(
7879
8763
  async (target) => {
7880
8764
  var _a2, _b2, _c2;
@@ -7886,8 +8770,9 @@ function BookingDialog({
7886
8770
  return;
7887
8771
  }
7888
8772
  try {
8773
+ const source = availabilityUrl != null ? availabilityUrl : `${baseUrl}/api/v1/sdk/booking/availability`;
7889
8774
  const res = await fetch(
7890
- `${baseUrl}/api/v1/sdk/booking/availability?from=${encodeURIComponent(new Date(from).toISOString())}&to=${encodeURIComponent(new Date(to).toISOString())}`,
8775
+ `${source}${source.includes("?") ? "&" : "?"}from=${encodeURIComponent(new Date(from).toISOString())}&to=${encodeURIComponent(new Date(to).toISOString())}${preview ? "&preview=1" : ""}`,
7891
8776
  { headers }
7892
8777
  );
7893
8778
  const json = await res.json();
@@ -7916,7 +8801,7 @@ function BookingDialog({
7916
8801
  setPageLoaded(true);
7917
8802
  }
7918
8803
  },
7919
- [baseUrl, headers]
8804
+ [baseUrl, headers, availabilityUrl, preview]
7920
8805
  );
7921
8806
  (0, import_react5.useEffect)(() => {
7922
8807
  if (!open) return;
@@ -7930,6 +8815,7 @@ function BookingDialog({
7930
8815
  setCode("");
7931
8816
  setBookingId(null);
7932
8817
  setError(null);
8818
+ setPreviewEnded(false);
7933
8819
  setAvatarFailed(false);
7934
8820
  setPageLoaded(false);
7935
8821
  setAsked(false);
@@ -7968,6 +8854,10 @@ function BookingDialog({
7968
8854
  async function startBooking() {
7969
8855
  var _a2, _b2;
7970
8856
  if (!selected) return;
8857
+ if (preview) {
8858
+ setPreviewEnded(true);
8859
+ return;
8860
+ }
7971
8861
  setBusy(true);
7972
8862
  setError(null);
7973
8863
  try {
@@ -8161,6 +9051,22 @@ function BookingDialog({
8161
9051
  ) : null,
8162
9052
  page.hostName && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 14, fontWeight: 500, ...muted }, children: page.hostName })
8163
9053
  ] }),
9054
+ preview && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
9055
+ "div",
9056
+ {
9057
+ style: {
9058
+ alignSelf: "flex-start",
9059
+ padding: "3px 9px",
9060
+ borderRadius: 999,
9061
+ border: `1px dashed ${t.border}`,
9062
+ fontSize: 11.5,
9063
+ fontWeight: 600,
9064
+ letterSpacing: "0.02em",
9065
+ color: t.muted
9066
+ },
9067
+ children: "Preview \u2014 nothing is booked"
9068
+ }
9069
+ ),
8164
9070
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 20, fontWeight: 600, lineHeight: 1.25, paddingRight: 32, overflowWrap: "anywhere" }, children: page.title }),
8165
9071
  page.description && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 13, lineHeight: 1.5, overflowWrap: "anywhere", ...muted }, children: page.description }),
8166
9072
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 10, marginTop: 4, fontSize: 14 }, children: [
@@ -8598,6 +9504,7 @@ function BookingDialog({
8598
9504
  },
8599
9505
  children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { maxWidth: 440, display: "flex", flexDirection: "column", gap: 14 }, children: [
8600
9506
  error && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { color: t.danger, fontSize: 13, lineHeight: 1.5 }, children: error }),
9507
+ previewEnded && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("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." }),
8601
9508
  step === "details" && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
8602
9509
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Field, { t, label: "Your name", value: name, onChange: setName, autoComplete: "name" }),
8603
9510
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Field, { t, label: "Email", value: email, onChange: setEmail, type: "email", autoComplete: "email" }),
@@ -9083,6 +9990,9 @@ function GlitchgrabProviderInner({
9083
9990
  onError,
9084
9991
  onReportSent
9085
9992
  });
9993
+ if (process.env.NODE_ENV !== "development") {
9994
+ installHydrationCapture(reportHydrationError);
9995
+ }
9086
9996
  (0, import_react6.useEffect)(() => {
9087
9997
  if (enableBreadcrumbs) {
9088
9998
  initBreadcrumbs(maxBreadcrumbs2, { responseBodyOrigins: responseBodyOrigins2 });
@@ -9138,7 +10048,7 @@ function GlitchgrabProviderInner({
9138
10048
  return matchesIgnorePatterns(message, ignoreErrors);
9139
10049
  };
9140
10050
  const handleError = (event) => {
9141
- var _a, _b, _c, _d, _e, _f, _g;
10051
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
9142
10052
  try {
9143
10053
  const isOpaqueCrossOrigin = event.message === "Script error." || event.message === "Script error" || !event.error && !event.filename;
9144
10054
  if (isOpaqueCrossOrigin) {
@@ -9153,6 +10063,7 @@ function GlitchgrabProviderInner({
9153
10063
  if (event.filename && EXTENSION_ORIGIN_RE.test(event.filename) || ((_a = event.error) == null ? void 0 : _a.stack) && EXTENSION_ORIGIN_RE.test(event.error.stack)) {
9154
10064
  return;
9155
10065
  }
10066
+ if (isHandledHydrationError(event.error)) return;
9156
10067
  incrementErrorCount();
9157
10068
  const context = captureContext(visitedPagesRef.current);
9158
10069
  const sig = computeSignature({
@@ -9178,6 +10089,9 @@ function GlitchgrabProviderInner({
9178
10089
  filename: (_e = event.filename) != null ? _e : "",
9179
10090
  lineno: String((_f = event.lineno) != null ? _f : ""),
9180
10091
  colno: String((_g = event.colno) != null ? _g : ""),
10092
+ // The root patch could not attach (React internals moved, or a
10093
+ // root it does not know): still label it and send the DOM context.
10094
+ ...isHydrationError((_h = event.error) != null ? _h : event.message) ? hydrationMetadata((_i = event.error) != null ? _i : event.message) : {},
9181
10095
  ...(session == null ? void 0 : session.userId) ? { sessionUserId: session.userId } : {},
9182
10096
  ...(session == null ? void 0 : session.name) ? { sessionUserName: String(session.name) } : {},
9183
10097
  ...(session == null ? void 0 : session.email) ? { sessionUserEmail: String(session.email) } : {},
@@ -9438,7 +10352,11 @@ function GlitchgrabProviderInner({
9438
10352
  [token, baseUrl, session]
9439
10353
  );
9440
10354
  const findSimilar = (0, import_react6.useCallback)(
9441
- (description) => findSimilarIssues(description, token, baseUrl, identity),
10355
+ (description, files) => findSimilarIssues(description, token, baseUrl, identity, files),
10356
+ [token, baseUrl, identity]
10357
+ );
10358
+ const readFile = (0, import_react6.useCallback)(
10359
+ (file) => readAttachment(file, token, baseUrl, identity),
9442
10360
  [token, baseUrl, identity]
9443
10361
  );
9444
10362
  const transcribe = (0, import_react6.useCallback)(
@@ -9469,8 +10387,9 @@ function GlitchgrabProviderInner({
9469
10387
  const [glitchgrabReport, setGlitchgrabReport] = (0, import_react6.useState)(false);
9470
10388
  const [signedUsers, setSignedUsers] = (0, import_react6.useState)("OFF");
9471
10389
  const aiBlockedUnsigned = signedUsers === "ENFORCE" && !signature;
9472
- const [bookingOpen, setBookingOpen] = (0, import_react6.useState)(false);
9473
- const openBookingDialog = (0, import_react6.useCallback)(() => setBookingOpen(true), []);
10390
+ const [booking, setBooking] = (0, import_react6.useState)("closed");
10391
+ const openBookingDialog = (0, import_react6.useCallback)(() => setBooking("live"), []);
10392
+ const openBookingPreview = (0, import_react6.useCallback)(() => setBooking("preview"), []);
9474
10393
  (0, import_react6.useEffect)(() => {
9475
10394
  let cancelled = false;
9476
10395
  fetch(`${baseUrl}/api/v1/sdk/project`, {
@@ -9514,7 +10433,7 @@ function GlitchgrabProviderInner({
9514
10433
  } catch (e) {
9515
10434
  }
9516
10435
  }, [openReportDialog]);
9517
- const reporter = (0, import_react6.useMemo)(() => {
10436
+ const reporter2 = (0, import_react6.useMemo)(() => {
9518
10437
  var _a;
9519
10438
  if (!session) return null;
9520
10439
  const avatar = (_a = session.avatarUrl) != null ? _a : session.image;
@@ -9545,6 +10464,7 @@ function GlitchgrabProviderInner({
9545
10464
  addBreadcrumb: addBreadcrumb2,
9546
10465
  openReportDialog,
9547
10466
  openBookingDialog,
10467
+ openBookingPreview,
9548
10468
  enhanceText: enhance,
9549
10469
  shortcutLabel
9550
10470
  },
@@ -9566,8 +10486,9 @@ function GlitchgrabProviderInner({
9566
10486
  {
9567
10487
  token,
9568
10488
  baseUrl: baseUrl != null ? baseUrl : DEFAULT_BASE_URL2,
9569
- open: bookingOpen,
9570
- onClose: () => setBookingOpen(false)
10489
+ open: booking !== "closed",
10490
+ preview: booking === "preview",
10491
+ onClose: () => setBooking("closed")
9571
10492
  }
9572
10493
  ),
9573
10494
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
@@ -9578,11 +10499,13 @@ function GlitchgrabProviderInner({
9578
10499
  enhanceText: aiAssist && !aiBlockedUnsigned ? enhance : void 0,
9579
10500
  assist: aiAssist && !aiBlockedUnsigned ? assist : void 0,
9580
10501
  findSimilarIssues: aiAssist && !aiBlockedUnsigned ? findSimilar : void 0,
10502
+ readFile: aiAssist && !aiBlockedUnsigned ? readFile : void 0,
9581
10503
  reportGlitchgrabProblem: glitchgrabReport ? reportGlitchgrabProblem : void 0,
9582
10504
  transcribeAudio: aiBlockedUnsigned ? void 0 : transcribe,
9583
10505
  types,
9584
10506
  showSeverity,
9585
- reporter,
10507
+ reporter: reporter2,
10508
+ chatHistoryKey: chatHistoryKey("sdk", token, session == null ? void 0 : session.userId),
9586
10509
  headerSlot: projectName ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { fontSize: 11, color: "#9aa0a6" }, children: [
9587
10510
  "Reporting to ",
9588
10511
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("strong", { style: { color: "#e5e5e5" }, children: projectName })
@@ -9982,6 +10905,7 @@ function useGlitchgrabFeedback(query) {
9982
10905
  // Annotate the CommonJS export names for ESM import in node:
9983
10906
  0 && (module.exports = {
9984
10907
  BookDemoButton,
10908
+ BookingDialog,
9985
10909
  FeedbackButton,
9986
10910
  GLITCHGRAB_SHORTCUT,
9987
10911
  GLITCHGRAB_SHORTCUT_MAC,