glitchgrab 1.56.0 → 1.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -37,6 +37,7 @@ function toAssistTurnResult(ok, body) {
37
37
  question: (_d = d.question) != null ? _d : null,
38
38
  options: Array.isArray(d.options) ? d.options.filter((o) => typeof o === "string") : [],
39
39
  report: (_e = d.report) != null ? _e : null,
40
+ title: typeof d.title === "string" && d.title.trim() ? d.title.trim() : null,
40
41
  duplicate: (_f = d.duplicate) != null ? _f : null,
41
42
  related: (_g = d.related) != null ? _g : null,
42
43
  solved: (_h = d.solved) != null ? _h : null,
@@ -665,6 +666,36 @@ async function sendFeedback(payload, baseUrl) {
665
666
  return null;
666
667
  }
667
668
  }
669
+ async function sendLead(token, lead, baseUrl) {
670
+ var _a, _b;
671
+ try {
672
+ const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/sdk/leads`;
673
+ const body = JSON.stringify(lead);
674
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
675
+ try {
676
+ const response = await fetch(url, {
677
+ method: "POST",
678
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
679
+ body
680
+ });
681
+ if (response.ok) {
682
+ const envelope = await response.json();
683
+ return { success: envelope.success, id: (_a = envelope.data) == null ? void 0 : _a.leadId, created: (_b = envelope.data) == null ? void 0 : _b.created };
684
+ }
685
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
686
+ return null;
687
+ }
688
+ } catch (e) {
689
+ }
690
+ if (attempt < MAX_RETRIES - 1) {
691
+ await new Promise((resolve) => setTimeout(resolve, RETRY_BASE_MS * Math.pow(2, attempt)));
692
+ }
693
+ }
694
+ return null;
695
+ } catch (e) {
696
+ return null;
697
+ }
698
+ }
668
699
  function signedFields(identity) {
669
700
  if (!(identity == null ? void 0 : identity.signature) || !identity.userId) return {};
670
701
  return { userId: identity.userId, signature: identity.signature };
@@ -1931,6 +1962,8 @@ function AssistSheet({
1931
1962
  reporterName,
1932
1963
  description,
1933
1964
  onDescriptionChange,
1965
+ issueTitle,
1966
+ onIssueTitleChange,
1934
1967
  severity,
1935
1968
  onSeverityChange,
1936
1969
  showSeverity,
@@ -2029,7 +2062,7 @@ function AssistSheet({
2029
2062
  onDegrade(message, retryable);
2030
2063
  }
2031
2064
  async function runTurn(history2, autoFile = false) {
2032
- var _a, _b;
2065
+ var _a, _b, _c;
2033
2066
  setBusy(true);
2034
2067
  setAutoFileLeft(null);
2035
2068
  let result;
@@ -2086,6 +2119,7 @@ function AssistSheet({
2086
2119
  }
2087
2120
  if (result.report) {
2088
2121
  onDescriptionChange(result.report);
2122
+ onIssueTitleChange((_b = result.title) != null ? _b : "");
2089
2123
  aiDraftRef.current = result.report;
2090
2124
  logEvent("draft_shown");
2091
2125
  setMessages([...history2, { role: "assistant", content: result.report, kind: "report" }]);
@@ -2098,7 +2132,7 @@ function AssistSheet({
2098
2132
  }
2099
2133
  if (result.question) {
2100
2134
  setMessages([...history2, { role: "assistant", content: result.question }]);
2101
- setOptions((_b = result.options) != null ? _b : []);
2135
+ setOptions((_c = result.options) != null ? _c : []);
2102
2136
  return;
2103
2137
  }
2104
2138
  onDegrade("The assistant had nothing to add \u2014 write your report below and send it as normal.");
@@ -2942,6 +2976,40 @@ function AssistSheet({
2942
2976
  title: true,
2943
2977
  footer: "Your details will be added to it instead of opening a new issue."
2944
2978
  }),
2979
+ !duplicate && issueTitle && // Wraps rather than scrolls sideways: a title cut off at the
2980
+ // box edge is the very thing #543 is about. One line of text,
2981
+ // so Enter is swallowed.
2982
+ /* @__PURE__ */ jsx3(
2983
+ "textarea",
2984
+ {
2985
+ "aria-label": "Title",
2986
+ value: issueTitle,
2987
+ maxLength: 120,
2988
+ rows: 2,
2989
+ onKeyDown: (e) => {
2990
+ if (e.key === "Enter") e.preventDefault();
2991
+ },
2992
+ onChange: (e) => {
2993
+ stopAutoFile("edited the title");
2994
+ onIssueTitleChange(e.target.value.replace(/[\r\n]+/g, " "));
2995
+ },
2996
+ style: {
2997
+ width: "100%",
2998
+ padding: "8px 10px",
2999
+ lineHeight: 1.45,
3000
+ resize: "none",
3001
+ borderRadius: "8px",
3002
+ border: `1px solid ${t.inputBorder}`,
3003
+ backgroundColor: t.inputBg,
3004
+ color: t.text,
3005
+ fontSize: "13px",
3006
+ fontWeight: 600,
3007
+ fontFamily: "inherit",
3008
+ outline: "none",
3009
+ boxSizing: "border-box"
3010
+ }
3011
+ }
3012
+ ),
2945
3013
  /* @__PURE__ */ jsx3(
2946
3014
  "textarea",
2947
3015
  {
@@ -4583,6 +4651,7 @@ function ReportDialog({
4583
4651
  const [pickedSimilar, setPickedSimilar] = useState4(null);
4584
4652
  const [addedToIssue, setAddedToIssue] = useState4(null);
4585
4653
  const [aiConversationId, setAiConversationId] = useState4(null);
4654
+ const [issueTitle, setIssueTitle] = useState4("");
4586
4655
  const [isEnhanced, setIsEnhanced] = useState4(false);
4587
4656
  const [originalDescription, setOriginalDescription] = useState4(
4588
4657
  null
@@ -5284,6 +5353,7 @@ function ReportDialog({
5284
5353
  }
5285
5354
  if (aiConversationId) {
5286
5355
  metadata.aiConversationId = aiConversationId;
5356
+ if (sheetUp && !duplicateOf && issueTitle.trim()) metadata.issueTitle = issueTitle.trim();
5287
5357
  }
5288
5358
  const result = await report(
5289
5359
  // The RATING branch returned above, so anything reaching here is a real
@@ -5302,6 +5372,7 @@ function ReportDialog({
5302
5372
  setScreenshots([]);
5303
5373
  setAttachments([]);
5304
5374
  setAiConversationId(null);
5375
+ setIssueTitle("");
5305
5376
  setDuplicateIssueNumber(null);
5306
5377
  setDuplicateKind("duplicate");
5307
5378
  setSimilarIssues(null);
@@ -7405,6 +7476,8 @@ function ReportDialog({
7405
7476
  setDescription(value);
7406
7477
  if (validationError) setValidationError(null);
7407
7478
  },
7479
+ issueTitle,
7480
+ onIssueTitleChange: setIssueTitle,
7408
7481
  severity,
7409
7482
  onSeverityChange: (value) => {
7410
7483
  setSeverity(value);
@@ -7570,6 +7643,16 @@ function getTypeIcon(type, color, size = 24) {
7570
7643
  // src/booking-dialog.tsx
7571
7644
  import { useCallback as useCallback2, useEffect as useEffect5, useMemo as useMemo3, useState as useState5 } from "react";
7572
7645
  import { Fragment as Fragment5, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
7646
+ function readQuestions(value) {
7647
+ if (!Array.isArray(value)) return [];
7648
+ return value.flatMap((row) => {
7649
+ if (!row || typeof row !== "object") return [];
7650
+ const { id, label, options, required } = row;
7651
+ if (typeof id !== "string" || typeof label !== "string" || !Array.isArray(options)) return [];
7652
+ const choices = options.filter((o) => typeof o === "string" && o !== "");
7653
+ return choices.length > 0 ? [{ id, label, options: choices, required: required !== false }] : [];
7654
+ });
7655
+ }
7573
7656
  var LIGHT = {
7574
7657
  bg: "#ffffff",
7575
7658
  pane: "#ffffff",
@@ -7723,6 +7806,10 @@ function BookingDialog({
7723
7806
  const [email, setEmail] = useState5("");
7724
7807
  const [phone, setPhone] = useState5("");
7725
7808
  const [note, setNote] = useState5("");
7809
+ const [questions, setQuestions] = useState5([]);
7810
+ const [answers, setAnswers] = useState5({});
7811
+ const [pageLoaded, setPageLoaded] = useState5(false);
7812
+ const [asked, setAsked] = useState5(false);
7726
7813
  const [code, setCode] = useState5("");
7727
7814
  const [bookingId, setBookingId] = useState5(null);
7728
7815
  const [meetUrl, setMeetUrl] = useState5(null);
@@ -7751,6 +7838,7 @@ function BookingDialog({
7751
7838
  if (!json.success) {
7752
7839
  setError((_a2 = json.error) != null ? _a2 : "Couldn't load available times");
7753
7840
  setSlotsByMonth((prev) => ({ ...prev, [target]: [] }));
7841
+ setPageLoaded(true);
7754
7842
  return;
7755
7843
  }
7756
7844
  const data = (_b2 = json.data) != null ? _b2 : {};
@@ -7763,9 +7851,12 @@ function BookingDialog({
7763
7851
  slotMinutes: typeof data.slotMinutes === "number" ? data.slotMinutes : 30
7764
7852
  });
7765
7853
  setWhatsappUrl((_c2 = data.whatsappUrl) != null ? _c2 : null);
7854
+ setQuestions(readQuestions(data.questions));
7855
+ setPageLoaded(true);
7766
7856
  } catch (e) {
7767
7857
  setError("Couldn't reach the booking service");
7768
7858
  setSlotsByMonth((prev) => ({ ...prev, [target]: [] }));
7859
+ setPageLoaded(true);
7769
7860
  }
7770
7861
  },
7771
7862
  [baseUrl, headers]
@@ -7783,6 +7874,8 @@ function BookingDialog({
7783
7874
  setBookingId(null);
7784
7875
  setError(null);
7785
7876
  setAvatarFailed(false);
7877
+ setPageLoaded(false);
7878
+ setAsked(false);
7786
7879
  }, [open]);
7787
7880
  const monthLoaded = month in slotsByMonth;
7788
7881
  useEffect5(() => {
@@ -7811,6 +7904,10 @@ function BookingDialog({
7811
7904
  const daySlots = day ? (_d = byDay.get(day)) != null ? _d : [] : [];
7812
7905
  const atFirstMonth = month <= today.slice(0, 7);
7813
7906
  const pastHorizon = monthLoaded && !error && byDay.size === 0 && !atFirstMonth;
7907
+ const given = questions.flatMap((q) => q.options.includes(answers[q.id]) ? [{ ...q, answer: answers[q.id] }] : []);
7908
+ const canContinue = questions.every((q) => !q.required || q.options.includes(answers[q.id]));
7909
+ const view = step !== "slots" ? step : !pageLoaded ? "loading" : questions.length > 0 && !asked ? "questions" : "slots";
7910
+ const roomy = step === "slots";
7814
7911
  async function startBooking() {
7815
7912
  var _a2;
7816
7913
  if (!selected) return;
@@ -7825,6 +7922,7 @@ function BookingDialog({
7825
7922
  email,
7826
7923
  phone,
7827
7924
  note,
7925
+ answers: Object.fromEntries(given.map((q) => [q.id, q.answer])),
7828
7926
  startsAt: selected.startsAt,
7829
7927
  // The zone the times were shown in, which is the one the visitor chose.
7830
7928
  timezone: zone
@@ -7875,7 +7973,7 @@ function BookingDialog({
7875
7973
  "div",
7876
7974
  {
7877
7975
  style: {
7878
- marginTop: wide ? whatsappUrl && step === "slots" ? 14 : "auto" : 0,
7976
+ marginTop: wide ? whatsappUrl && roomy ? 14 : "auto" : 0,
7879
7977
  padding: wide ? 0 : "4px 20px 18px",
7880
7978
  display: "flex",
7881
7979
  justifyContent: wide ? "flex-start" : "center"
@@ -8016,9 +8114,36 @@ function BookingDialog({
8016
8114
  selectedEnd && formatTime(selectedEnd, zone, twelve)
8017
8115
  ] })
8018
8116
  ] }) }),
8117
+ asked && given.length > 0 && /* @__PURE__ */ jsx6(Row, { icon: "check", t, children: /* @__PURE__ */ jsxs6("span", { children: [
8118
+ given.map((q) => q.answer).join(" \xB7 "),
8119
+ view === "slots" && /* @__PURE__ */ jsxs6(Fragment5, { children: [
8120
+ " ",
8121
+ /* @__PURE__ */ jsx6(
8122
+ "button",
8123
+ {
8124
+ type: "button",
8125
+ onClick: () => setAsked(false),
8126
+ className: "gg-bk-link",
8127
+ style: {
8128
+ border: 0,
8129
+ // Room for a thumb without pushing the rows apart.
8130
+ padding: "8px 6px",
8131
+ margin: "-8px 0",
8132
+ background: "transparent",
8133
+ color: t.muted,
8134
+ fontFamily: "inherit",
8135
+ fontSize: 13,
8136
+ textDecoration: "underline",
8137
+ cursor: "pointer"
8138
+ },
8139
+ children: "Change"
8140
+ }
8141
+ )
8142
+ ] })
8143
+ ] }) }),
8019
8144
  /* @__PURE__ */ jsx6(Row, { icon: "clock", t, children: formatDuration(page.slotMinutes) }),
8020
8145
  /* @__PURE__ */ jsx6(Row, { icon: "video", t, children: "Google Meet" }),
8021
- /* @__PURE__ */ jsx6(Row, { icon: "globe", t, children: step === "slots" ? (
8146
+ /* @__PURE__ */ jsx6(Row, { icon: "globe", t, children: roomy ? (
8022
8147
  // The visible label is plain text; the native select sits on top of
8023
8148
  // it, invisible. A styled select sizes itself to the longest zone
8024
8149
  // name in the list, which pushed the chevron to the pane's edge.
@@ -8038,7 +8163,7 @@ function BookingDialog({
8038
8163
  ] })
8039
8164
  ) : zone })
8040
8165
  ] }),
8041
- whatsappUrl && step === "slots" && /* @__PURE__ */ jsxs6(
8166
+ whatsappUrl && roomy && /* @__PURE__ */ jsxs6(
8042
8167
  "a",
8043
8168
  {
8044
8169
  href: whatsappUrl,
@@ -8309,6 +8434,93 @@ function BookingDialog({
8309
8434
  ]
8310
8435
  }
8311
8436
  );
8437
+ const loading = /* @__PURE__ */ jsx6(
8438
+ "div",
8439
+ {
8440
+ style: {
8441
+ flex: "1 1 auto",
8442
+ minHeight: wide ? void 0 : 200,
8443
+ display: "flex",
8444
+ alignItems: "center",
8445
+ justifyContent: "center",
8446
+ fontSize: 13,
8447
+ ...muted
8448
+ },
8449
+ children: "Loading\u2026"
8450
+ }
8451
+ );
8452
+ const ask = /* @__PURE__ */ jsx6(
8453
+ "div",
8454
+ {
8455
+ style: {
8456
+ flex: "1 1 auto",
8457
+ minWidth: 0,
8458
+ padding: wide ? "28px 32px" : "20px",
8459
+ boxSizing: "border-box",
8460
+ overflowY: wide ? "auto" : void 0
8461
+ },
8462
+ children: /* @__PURE__ */ jsxs6("div", { style: { maxWidth: 520, display: "flex", flexDirection: "column", gap: 22 }, children: [
8463
+ /* @__PURE__ */ jsxs6("div", { children: [
8464
+ /* @__PURE__ */ jsx6("div", { style: { fontSize: 16, fontWeight: 600 }, children: questions.length === 1 ? "One question first" : "A few questions first" }),
8465
+ /* @__PURE__ */ jsx6("div", { style: { fontSize: 13, lineHeight: 1.5, marginTop: 4, ...muted }, children: "So the right person takes your demo." })
8466
+ ] }),
8467
+ questions.map((q) => /* @__PURE__ */ jsxs6("div", { role: "group", "aria-label": q.label, children: [
8468
+ /* @__PURE__ */ jsxs6("div", { style: { fontSize: 14, fontWeight: 500, overflowWrap: "anywhere" }, children: [
8469
+ q.label,
8470
+ !q.required && /* @__PURE__ */ jsx6("span", { style: { fontWeight: 400, ...muted }, children: " (optional)" })
8471
+ ] }),
8472
+ /* @__PURE__ */ jsx6("div", { style: { display: "flex", flexWrap: "wrap", gap: 8, marginTop: 10 }, children: q.options.map((option) => {
8473
+ const on = answers[q.id] === option;
8474
+ return /* @__PURE__ */ jsx6(
8475
+ "button",
8476
+ {
8477
+ type: "button",
8478
+ "aria-pressed": on,
8479
+ className: on ? void 0 : "gg-bk-slot",
8480
+ onClick: () => setAnswers((prev) => {
8481
+ if (on && !q.required) {
8482
+ const rest = { ...prev };
8483
+ delete rest[q.id];
8484
+ return rest;
8485
+ }
8486
+ return { ...prev, [q.id]: option };
8487
+ }),
8488
+ style: {
8489
+ padding: "9px 14px",
8490
+ minHeight: 40,
8491
+ borderRadius: 8,
8492
+ border: `1px solid ${on ? t.ink : t.border}`,
8493
+ background: on ? t.ink : t.pane,
8494
+ color: on ? t.onInk : t.text,
8495
+ fontFamily: "inherit",
8496
+ fontSize: 14,
8497
+ fontWeight: 500,
8498
+ cursor: "pointer",
8499
+ overflowWrap: "anywhere",
8500
+ textAlign: "left"
8501
+ },
8502
+ children: option
8503
+ },
8504
+ option
8505
+ );
8506
+ }) })
8507
+ ] }, q.id)),
8508
+ /* @__PURE__ */ jsxs6("div", { style: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: "8px 12px", marginTop: 2 }, children: [
8509
+ /* @__PURE__ */ jsx6(
8510
+ "button",
8511
+ {
8512
+ type: "button",
8513
+ onClick: () => setAsked(true),
8514
+ disabled: !canContinue,
8515
+ style: { ...primaryStyle(t, !canContinue), whiteSpace: "nowrap" },
8516
+ children: "See available times"
8517
+ }
8518
+ ),
8519
+ !canContinue && /* @__PURE__ */ jsx6("span", { style: { fontSize: 12, ...muted }, children: "Pick one for each to continue" })
8520
+ ] })
8521
+ ] })
8522
+ }
8523
+ );
8312
8524
  const form = /* @__PURE__ */ jsx6(
8313
8525
  "div",
8314
8526
  {
@@ -8422,6 +8634,7 @@ function BookingDialog({
8422
8634
  .gg-bk-day:hover { box-shadow: inset 0 0 0 1.5px ${t.ink}; }
8423
8635
  .gg-bk-slot:hover { border-color: ${t.ink} !important; }
8424
8636
  .gg-bk-ghost:hover { background: ${t.open} !important; }
8637
+ .gg-bk-link:hover { color: ${t.text} !important; }
8425
8638
  .gg-bk-root button:focus-visible, .gg-bk-root a:focus-visible,
8426
8639
  .gg-bk-root span:has(> select:focus-visible), .gg-bk-root input:focus-visible, .gg-bk-root textarea:focus-visible {
8427
8640
  outline: 2px solid ${t.ink}; outline-offset: 2px;
@@ -8435,11 +8648,11 @@ function BookingDialog({
8435
8648
  "aria-label": page.title,
8436
8649
  className: "gg-bk-root",
8437
8650
  style: {
8438
- width: step === "slots" ? "min(1060px, 100%)" : "min(820px, 100%)",
8651
+ width: roomy ? "min(1060px, 100%)" : "min(820px, 100%)",
8439
8652
  // A month grid should not jump in height between months; the form
8440
8653
  // steps are short and would sit in a mostly empty box.
8441
- height: wide && step === "slots" ? "min(640px, calc(100vh - 48px))" : void 0,
8442
- minHeight: wide && step !== "slots" ? 420 : void 0,
8654
+ height: wide && roomy ? "min(640px, calc(100vh - 48px))" : void 0,
8655
+ minHeight: wide && !roomy ? 420 : void 0,
8443
8656
  margin: wide ? void 0 : "auto 0",
8444
8657
  display: "flex",
8445
8658
  flexDirection: wide ? "row" : "column",
@@ -8455,7 +8668,7 @@ function BookingDialog({
8455
8668
  },
8456
8669
  children: [
8457
8670
  summary,
8458
- step === "slots" ? /* @__PURE__ */ jsxs6(Fragment5, { children: [
8671
+ view === "loading" ? loading : view === "questions" ? ask : view === "slots" ? /* @__PURE__ */ jsxs6(Fragment5, { children: [
8459
8672
  calendar,
8460
8673
  times
8461
8674
  ] }) : form,
@@ -8542,6 +8755,7 @@ function Icon({ name }) {
8542
8755
  left: /* @__PURE__ */ jsx6("path", { d: "m15 18-6-6 6-6" }),
8543
8756
  right: /* @__PURE__ */ jsx6("path", { d: "m9 18 6-6-6-6" }),
8544
8757
  down: /* @__PURE__ */ jsx6("path", { d: "m6 9 6 6 6-6" }),
8758
+ check: /* @__PURE__ */ jsx6("path", { d: "M20 6 9 17l-5-5" }),
8545
8759
  clock: /* @__PURE__ */ jsxs6(Fragment5, { children: [
8546
8760
  /* @__PURE__ */ jsx6("circle", { cx: "12", cy: "12", r: "9" }),
8547
8761
  /* @__PURE__ */ jsx6("path", { d: "M12 7v5l3 2" })
@@ -9073,6 +9287,17 @@ function GlitchgrabProviderInner({
9073
9287
  },
9074
9288
  [token, baseUrl, session]
9075
9289
  );
9290
+ const submitLead = useCallback3(
9291
+ async (lead) => {
9292
+ try {
9293
+ const context = captureContext(visitedPagesRef.current);
9294
+ return await sendLead(token, { ...lead, pageUrl: context.url }, baseUrl);
9295
+ } catch (e) {
9296
+ return null;
9297
+ }
9298
+ },
9299
+ [token, baseUrl]
9300
+ );
9076
9301
  const openFeedbackDialog = useCallback3(() => {
9077
9302
  if (typeof window !== "undefined") {
9078
9303
  window.dispatchEvent(
@@ -9250,6 +9475,7 @@ function GlitchgrabProviderInner({
9250
9475
  setContext: setContext2,
9251
9476
  setContexts: setContexts2,
9252
9477
  sendFeedback: feedback,
9478
+ submitLead,
9253
9479
  openFeedbackDialog,
9254
9480
  addBreadcrumb: addBreadcrumb2,
9255
9481
  openReportDialog,
@@ -9714,6 +9940,7 @@ export {
9714
9940
  matchesShortcut,
9715
9941
  sanitizeUrl,
9716
9942
  sendFeedback,
9943
+ sendLead,
9717
9944
  sendReport,
9718
9945
  setContext,
9719
9946
  setContexts,