@mushi-mushi/react-native 0.19.1 → 0.20.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.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  useEffect as useEffect3,
13
13
  useRef as useRef3,
14
14
  useState as useState2,
15
- useCallback as useCallback3,
15
+ useCallback as useCallback4,
16
16
  useMemo
17
17
  } from "react";
18
18
  import {
@@ -242,9 +242,8 @@ async function encryptQueueBlob(plaintext, secureStorage) {
242
242
  combined.set(new Uint8Array(cipher), iv.length);
243
243
  return ENCRYPTED_PREFIX + bytesToBase64(combined);
244
244
  }
245
- async function decryptQueueBlob(stored, secureStorage) {
245
+ async function decryptQueueBlob(stored, _secureStorage) {
246
246
  if (!stored.startsWith(ENCRYPTED_PREFIX)) return stored;
247
- if (!secureStorage) return stored;
248
247
  const secure = await loadSecureStore();
249
248
  if (!secure || !hasWebCrypto()) return stored;
250
249
  const key = await getOrCreateQueueKey(secure);
@@ -475,33 +474,65 @@ import {
475
474
  ScrollView,
476
475
  useColorScheme
477
476
  } from "react-native";
478
- import { mushiPalette } from "@mushi-mushi/core";
477
+ import { mushiPalette, MUSHI_BANNER_NEON, MUSHI_COPY } from "@mushi-mushi/core";
478
+ import { getLocale } from "@mushi-mushi/web/i18n";
479
+
480
+ // src/reporter-status.ts
481
+ function reporterStatusShort(status) {
482
+ switch (status) {
483
+ case "new":
484
+ case "queued":
485
+ case "pending":
486
+ case "submitted":
487
+ return "Received";
488
+ case "classified":
489
+ case "triaged":
490
+ case "grouped":
491
+ case "dispatched":
492
+ return "In review";
493
+ case "fixing":
494
+ return "Fixing";
495
+ case "fixed":
496
+ case "resolved":
497
+ case "completed":
498
+ return "Fixed";
499
+ case "dismissed":
500
+ return "Closed";
501
+ default:
502
+ return status.replace(/_/g, " ").slice(0, 16);
503
+ }
504
+ }
505
+
506
+ // src/components/MushiBottomSheet.tsx
479
507
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
480
508
  var { height: SCREEN_HEIGHT } = Dimensions2.get("window");
481
509
  var SHEET_HEIGHT = SCREEN_HEIGHT * 0.55;
482
510
  var DISMISS_THRESHOLD = 80;
483
- var NEON = {
484
- accent: "#0FFF50",
485
- accentHover: "#00E646",
486
- ink: "#0a1a0a",
487
- border: "#00C43A"
511
+ var CATEGORY_KEYS = ["bug", "slow", "visual", "confusing", "other"];
512
+ var CATEGORY_EMOJI = {
513
+ bug: "\u{1F41B}",
514
+ slow: "\u{1F422}",
515
+ visual: "\u{1F3A8}",
516
+ confusing: "\u{1F615}",
517
+ other: "\u{1F4AC}"
488
518
  };
489
- var CATEGORIES = [
490
- { key: "bug", emoji: "\u{1F41B}", label: "Bug" },
491
- { key: "slow", emoji: "\u{1F422}", label: "Slow" },
492
- { key: "visual", emoji: "\u{1F3A8}", label: "Visual" },
493
- { key: "confusing", emoji: "\u{1F615}", label: "Confusing" },
494
- { key: "other", emoji: "\u{1F4AC}", label: "Other" }
495
- ];
496
519
  var VERIFIABLE_STATUSES = /* @__PURE__ */ new Set(["fixed", "resolved", "verified"]);
497
520
  var MushiBottomSheet = ({
498
521
  visible,
499
522
  onClose,
523
+ preferredTab = "report",
500
524
  screenshotDataUrl,
501
525
  onClearScreenshot,
502
- screenshotSensitiveHint
526
+ screenshotSensitiveHint,
527
+ assistantEnabled = false,
528
+ assistantLabel = MUSHI_COPY.assistantTab,
529
+ assistantGreeting,
530
+ assistantSuggestions = [],
531
+ inboxPollIntervalMs = 0
503
532
  }) => {
504
533
  const mushi = useMushiContext();
534
+ const t = getLocale();
535
+ const greeting = assistantGreeting ?? t.assistant.defaultGreeting;
505
536
  const scheme = useColorScheme();
506
537
  const dark = scheme === "dark";
507
538
  const translateY = useRef(new Animated.Value(SHEET_HEIGHT)).current;
@@ -515,6 +546,11 @@ var MushiBottomSheet = ({
515
546
  const [selectedReportId, setSelectedReportId] = useState(null);
516
547
  const [threadComments, setThreadComments] = useState([]);
517
548
  const [replyText, setReplyText] = useState("");
549
+ const [assistantInput, setAssistantInput] = useState("");
550
+ const [assistantThreadId, setAssistantThreadId] = useState(null);
551
+ const [assistantSending, setAssistantSending] = useState(false);
552
+ const [assistantError, setAssistantError] = useState(null);
553
+ const [assistantTurns, setAssistantTurns] = useState([]);
518
554
  const [screenshotAttached, setScreenshotAttached] = useState(true);
519
555
  const loadInbox = useCallback(async () => {
520
556
  if (!mushi?.listMyReports) return;
@@ -529,9 +565,17 @@ var MushiBottomSheet = ({
529
565
  useEffect(() => {
530
566
  if (visible) {
531
567
  setScreenshotAttached(true);
532
- if (sheetTab === "inbox") void loadInbox();
568
+ setSheetTab(preferredTab);
569
+ if (preferredTab === "inbox") void loadInbox();
533
570
  }
534
- }, [visible, sheetTab, loadInbox]);
571
+ }, [visible, preferredTab, loadInbox]);
572
+ useEffect(() => {
573
+ if (!visible || sheetTab !== "inbox" || inboxPollIntervalMs <= 0) return;
574
+ const timer = setInterval(() => {
575
+ void loadInbox();
576
+ }, inboxPollIntervalMs);
577
+ return () => clearInterval(timer);
578
+ }, [visible, sheetTab, inboxPollIntervalMs, loadInbox]);
535
579
  const openThread = useCallback(async (reportId) => {
536
580
  setSelectedReportId(reportId);
537
581
  if (!mushi?.listMyComments) return;
@@ -550,6 +594,29 @@ var MushiBottomSheet = ({
550
594
  await loadInbox();
551
595
  await openThread(selectedReportId);
552
596
  }, [mushi, selectedReportId, loadInbox, openThread]);
597
+ const sendAssistant = useCallback(async (message) => {
598
+ if (!mushi?.askAssistant || !message.trim() || assistantSending) return;
599
+ const trimmed = message.trim();
600
+ setAssistantInput("");
601
+ setAssistantError(null);
602
+ setAssistantTurns((prev) => [...prev, { role: "user", text: trimmed }]);
603
+ setAssistantSending(true);
604
+ try {
605
+ const reply = await mushi.askAssistant(trimmed, assistantThreadId);
606
+ if (!reply) {
607
+ setAssistantError("Could not reach the assistant. Try again.");
608
+ return;
609
+ }
610
+ if (reply.threadId) setAssistantThreadId(reply.threadId);
611
+ const botText = reply.kind === "clarify" ? reply.question ?? reply.text ?? "" : reply.text ?? "";
612
+ setAssistantTurns((prev) => [
613
+ ...prev,
614
+ { role: "bot", text: botText, options: reply.options }
615
+ ]);
616
+ } finally {
617
+ setAssistantSending(false);
618
+ }
619
+ }, [mushi, assistantSending, assistantThreadId]);
553
620
  const resetForm = useCallback(() => {
554
621
  setCategory(null);
555
622
  setDescription("");
@@ -634,7 +701,8 @@ var MushiBottomSheet = ({
634
701
  };
635
702
  const activeScreenshot = screenshotDataUrl && screenshotAttached ? screenshotDataUrl : null;
636
703
  const pal = mushiPalette(dark ? "dark" : "light");
637
- const colors = dark ? { bg: pal.paper, text: pal.ink, sub: pal.inkMuted, card: pal.paperRaised, accent: NEON.accent, accentInk: NEON.ink, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.6)", disabled: "#3a3a3c", disabledText: pal.inkFaint } : { bg: pal.paperRaised, text: pal.ink, sub: pal.inkMuted, card: pal.paper, accent: NEON.accent, accentInk: NEON.ink, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.35)", disabled: "#d1d1d6", disabledText: pal.inkFaint };
704
+ const colors = dark ? { bg: pal.paper, text: pal.ink, sub: pal.inkMuted, card: pal.paperRaised, accent: MUSHI_BANNER_NEON.bg, accentInk: MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.6)", disabled: "#3a3a3c", disabledText: pal.inkFaint } : { bg: pal.paperRaised, text: pal.ink, sub: pal.inkMuted, card: pal.paper, accent: MUSHI_BANNER_NEON.bg, accentInk: MUSHI_BANNER_NEON.fg, border: pal.ruleStrong, backdrop: "rgba(0,0,0,0.35)", disabled: "#d1d1d6", disabledText: pal.inkFaint };
705
+ const sheetTabs = ["report", "inbox", ...assistantEnabled ? ["assistant"] : []];
638
706
  const canSubmit = !!category && description.trim().length > 0 && phase === "form";
639
707
  return /* @__PURE__ */ jsx(Modal, { visible, transparent: true, animationType: "none", statusBarTranslucent: true, children: /* @__PURE__ */ jsxs(
640
708
  KeyboardAvoidingView,
@@ -659,11 +727,11 @@ var MushiBottomSheet = ({
659
727
  ],
660
728
  children: [
661
729
  /* @__PURE__ */ jsx(View, { ...panResponder.panHandlers, style: s.handleArea, children: /* @__PURE__ */ jsx(View, { style: [s.handle, { backgroundColor: colors.sub }] }) }),
662
- /* @__PURE__ */ jsxs(View, { style: [s.brandHeader, { backgroundColor: NEON.accent, borderBottomColor: NEON.border }], children: [
663
- /* @__PURE__ */ jsx(Text, { style: [s.brandEyebrow, { color: NEON.ink }], children: "MUSHI \xB7 BETA" }),
664
- /* @__PURE__ */ jsx(Text, { style: [s.brandTitle, { color: NEON.ink }], children: sheetTab === "inbox" ? "Your reports" : sheetTab === "community" ? "Community" : "Report an issue" })
730
+ /* @__PURE__ */ jsxs(View, { style: [s.brandHeader, { backgroundColor: MUSHI_BANNER_NEON.bg, borderBottomColor: MUSHI_BANNER_NEON.border }], children: [
731
+ /* @__PURE__ */ jsx(Text, { style: [s.brandEyebrow, { color: MUSHI_BANNER_NEON.fg }], children: "MUSHI \xB7 BETA" }),
732
+ /* @__PURE__ */ jsx(Text, { style: [s.brandTitle, { color: MUSHI_BANNER_NEON.fg }], children: sheetTab === "assistant" ? assistantLabel : sheetTab === "inbox" ? t.flows.reports.title : t.widget.title })
665
733
  ] }),
666
- /* @__PURE__ */ jsx(View, { style: s.tabRow, children: ["report", "inbox", "community"].map((tab) => /* @__PURE__ */ jsx(
734
+ /* @__PURE__ */ jsx(View, { style: s.tabRow, children: sheetTabs.map((tab) => /* @__PURE__ */ jsx(
667
735
  TouchableOpacity,
668
736
  {
669
737
  onPress: () => {
@@ -672,12 +740,68 @@ var MushiBottomSheet = ({
672
740
  else setSelectedReportId(null);
673
741
  },
674
742
  style: [s.tabBtn, sheetTab === tab && { borderBottomColor: colors.accent }],
675
- children: /* @__PURE__ */ jsx(Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.text : colors.sub }], children: tab === "report" ? "Report" : tab === "inbox" ? "Your reports" : "Community" })
743
+ children: /* @__PURE__ */ jsx(Text, { style: [s.tabLabel, { color: sheetTab === tab ? colors.text : colors.sub }], children: tab === "report" ? t.widget.trigger : tab === "inbox" ? t.flows.reports.title : assistantLabel })
676
744
  },
677
745
  tab
678
746
  )) }),
679
- sheetTab === "inbox" ? /* @__PURE__ */ jsx(ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: inboxLoading ? /* @__PURE__ */ jsx(Text, { style: { color: colors.sub }, children: "Loading\u2026" }) : selectedReportId ? /* @__PURE__ */ jsxs(Fragment, { children: [
680
- /* @__PURE__ */ jsx(TouchableOpacity, { onPress: () => setSelectedReportId(null), children: /* @__PURE__ */ jsx(Text, { style: { color: colors.accent, marginBottom: 8 }, children: "\u2190 Back" }) }),
747
+ sheetTab === "assistant" ? /* @__PURE__ */ jsxs(ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: [
748
+ assistantTurns.length === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
749
+ /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, marginBottom: 12, lineHeight: 20 }, children: greeting }),
750
+ assistantSuggestions.map((chip) => /* @__PURE__ */ jsx(
751
+ TouchableOpacity,
752
+ {
753
+ style: [s.assistantChip, { borderColor: colors.border, backgroundColor: colors.card }],
754
+ onPress: () => void sendAssistant(chip),
755
+ children: /* @__PURE__ */ jsx(Text, { style: { color: colors.text, fontSize: 13 }, children: chip })
756
+ },
757
+ chip
758
+ ))
759
+ ] }) : assistantTurns.map((turn, idx) => /* @__PURE__ */ jsxs(
760
+ View,
761
+ {
762
+ style: [
763
+ s.assistantBubble,
764
+ {
765
+ alignSelf: turn.role === "user" ? "flex-end" : "flex-start",
766
+ backgroundColor: turn.role === "user" ? colors.accent : colors.card
767
+ }
768
+ ],
769
+ children: [
770
+ /* @__PURE__ */ jsx(Text, { style: { color: turn.role === "user" ? colors.accentInk : colors.text }, children: turn.text }),
771
+ turn.options?.map((opt) => /* @__PURE__ */ jsx(TouchableOpacity, { onPress: () => void sendAssistant(opt), style: { marginTop: 8 }, children: /* @__PURE__ */ jsx(Text, { style: { color: colors.accent, fontSize: 12 }, children: opt }) }, opt))
772
+ ]
773
+ },
774
+ `${turn.role}-${idx}`
775
+ )),
776
+ assistantSending ? /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, marginTop: 8 }, accessibilityLiveRegion: "polite", children: t.assistant.thinking }) : null,
777
+ assistantError ? /* @__PURE__ */ jsx(Text, { style: { color: pal.danger, marginTop: 8 }, accessibilityRole: "alert", children: assistantError }) : null,
778
+ /* @__PURE__ */ jsxs(View, { style: s.assistantComposer, children: [
779
+ /* @__PURE__ */ jsx(
780
+ TextInput,
781
+ {
782
+ style: [s.input, { flex: 1, minHeight: 44, backgroundColor: colors.card, color: colors.text, borderColor: colors.border }],
783
+ placeholder: t.assistant.inputPlaceholder,
784
+ placeholderTextColor: colors.sub,
785
+ value: assistantInput,
786
+ onChangeText: setAssistantInput,
787
+ editable: !assistantSending
788
+ }
789
+ ),
790
+ /* @__PURE__ */ jsx(
791
+ TouchableOpacity,
792
+ {
793
+ style: [s.submitBtn, { backgroundColor: colors.accent, paddingHorizontal: 16 }],
794
+ onPress: () => void sendAssistant(assistantInput),
795
+ disabled: !assistantInput.trim() || assistantSending,
796
+ children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.accentInk }], children: "\u2191" })
797
+ }
798
+ )
799
+ ] })
800
+ ] }) : sheetTab === "inbox" ? /* @__PURE__ */ jsx(ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: inboxLoading ? /* @__PURE__ */ jsx(Text, { style: { color: colors.sub }, children: t.flows.reports.loading }) : selectedReportId ? /* @__PURE__ */ jsxs(Fragment, { children: [
801
+ /* @__PURE__ */ jsx(TouchableOpacity, { onPress: () => setSelectedReportId(null), children: /* @__PURE__ */ jsxs(Text, { style: { color: colors.accent, marginBottom: 8 }, children: [
802
+ "\u2190 ",
803
+ t.widget.back
804
+ ] }) }),
681
805
  threadComments.map((c) => /* @__PURE__ */ jsxs(View, { style: [s.threadBubble, { backgroundColor: colors.card }], children: [
682
806
  /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11 }, children: c.author_kind }),
683
807
  /* @__PURE__ */ jsx(Text, { style: { color: colors.text }, children: c.body })
@@ -685,42 +809,26 @@ var MushiBottomSheet = ({
685
809
  VERIFIABLE_STATUSES.has(
686
810
  inboxReports.find((r) => r.id === selectedReportId)?.status ?? ""
687
811
  ) && /* @__PURE__ */ jsxs(View, { style: s.verifyRow, children: [
688
- /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.accent }], onPress: () => submitFeedback("confirms"), children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.accentInk }], children: "Yes, fixed" }) }),
689
- /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.border }], onPress: () => submitFeedback("not_fixed"), children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.text }], children: "Not fixed" }) })
812
+ /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.accent }], onPress: () => submitFeedback("confirms"), children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.accentInk }], children: t.flows.thread.confirmFixed }) }),
813
+ /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.verifyBtn, { backgroundColor: colors.border }], onPress: () => submitFeedback("not_fixed"), children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.text }], children: t.flows.thread.notFixed }) })
690
814
  ] }),
691
815
  /* @__PURE__ */ jsx(
692
816
  TextInput,
693
817
  {
694
818
  style: [s.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border, minHeight: 48 }],
695
- placeholder: "Reply\u2026",
819
+ placeholder: t.flows.thread.replyPlaceholder,
696
820
  placeholderTextColor: colors.sub,
697
821
  value: replyText,
698
822
  onChangeText: setReplyText
699
823
  }
700
824
  ),
701
- /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], onPress: sendReply, children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.accentInk }], children: "Send" }) })
825
+ /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], onPress: sendReply, children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.accentInk }], children: t.flows.thread.send }) })
702
826
  ] }) : inboxReports.map((r) => /* @__PURE__ */ jsxs(TouchableOpacity, { style: [s.inboxRow, { borderColor: colors.border }], onPress: () => openThread(r.id), children: [
703
827
  /* @__PURE__ */ jsx(Text, { style: { color: colors.text, fontWeight: "600" }, numberOfLines: 1, children: (r.summary ?? r.description ?? "Report").slice(0, 60) }),
704
- /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11 }, children: r.status })
705
- ] }, r.id)) }) : sheetTab === "community" ? /* @__PURE__ */ jsxs(ScrollView, { style: s.body, keyboardShouldPersistTaps: "handled", children: [
706
- /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 13, marginBottom: 16, lineHeight: 18 }, children: "Sign in to track your reports across apps and see the global leaderboard. No password needed." }),
707
- /* @__PURE__ */ jsx(
708
- TextInput,
709
- {
710
- style: [s.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border, minHeight: 48 }],
711
- placeholder: "you@example.com",
712
- placeholderTextColor: colors.sub,
713
- keyboardType: "email-address",
714
- autoCapitalize: "none",
715
- autoCorrect: false,
716
- textContentType: "emailAddress"
717
- }
718
- ),
719
- /* @__PURE__ */ jsx(TouchableOpacity, { style: [s.submitBtn, { backgroundColor: colors.accent }], children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: colors.accentInk }], children: "Send sign-in link \u2192" }) }),
720
- /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11, textAlign: "center", marginTop: 12 }, children: "We'll email you a one-tap sign-in link. No password, ever." })
721
- ] }) : phase === "sent" ? /* @__PURE__ */ jsxs(View, { style: s.sentWrap, children: [
828
+ /* @__PURE__ */ jsx(Text, { style: { color: colors.sub, fontSize: 11 }, children: reporterStatusShort(r.status) })
829
+ ] }, r.id)) }) : phase === "sent" ? /* @__PURE__ */ jsxs(View, { style: s.sentWrap, children: [
722
830
  /* @__PURE__ */ jsx(Text, { style: [s.sentEmoji], children: "\u2705" }),
723
- /* @__PURE__ */ jsx(Text, { style: [s.sentText, { color: colors.text }], children: "Report sent!" })
831
+ /* @__PURE__ */ jsx(Text, { style: [s.sentText, { color: colors.text }], children: t.widget.submitted })
724
832
  ] }) : /* @__PURE__ */ jsxs(
725
833
  ScrollView,
726
834
  {
@@ -728,13 +836,13 @@ var MushiBottomSheet = ({
728
836
  keyboardShouldPersistTaps: "handled",
729
837
  contentContainerStyle: { paddingBottom: 24 },
730
838
  children: [
731
- /* @__PURE__ */ jsx(Text, { style: [s.stepLabel, { color: colors.sub }], children: "What kind of issue?" }),
732
- /* @__PURE__ */ jsx(View, { style: s.catRow, children: CATEGORIES.map((c) => {
733
- const active = category === c.key;
839
+ /* @__PURE__ */ jsx(Text, { style: [s.stepLabel, { color: colors.sub }], children: t.step1.heading }),
840
+ /* @__PURE__ */ jsx(View, { style: s.catRow, children: CATEGORY_KEYS.map((key) => {
841
+ const active = category === key;
734
842
  return /* @__PURE__ */ jsxs(
735
843
  TouchableOpacity,
736
844
  {
737
- onPress: () => setCategory(c.key),
845
+ onPress: () => setCategory(key),
738
846
  activeOpacity: 0.7,
739
847
  style: [
740
848
  s.catBtn,
@@ -744,7 +852,7 @@ var MushiBottomSheet = ({
744
852
  }
745
853
  ],
746
854
  children: [
747
- /* @__PURE__ */ jsx(Text, { style: s.catEmoji, children: c.emoji }),
855
+ /* @__PURE__ */ jsx(Text, { style: s.catEmoji, children: CATEGORY_EMOJI[key] }),
748
856
  /* @__PURE__ */ jsx(
749
857
  Text,
750
858
  {
@@ -752,12 +860,12 @@ var MushiBottomSheet = ({
752
860
  s.catLabel,
753
861
  { color: active ? colors.accentInk : colors.text }
754
862
  ],
755
- children: c.label
863
+ children: t.step1.categories[key]
756
864
  }
757
865
  )
758
866
  ]
759
867
  },
760
- c.key
868
+ key
761
869
  );
762
870
  }) }),
763
871
  /* @__PURE__ */ jsx(
@@ -771,7 +879,7 @@ var MushiBottomSheet = ({
771
879
  borderColor: colors.border
772
880
  }
773
881
  ],
774
- placeholder: "What happened?",
882
+ placeholder: t.step3.descriptionPlaceholder,
775
883
  placeholderTextColor: colors.sub,
776
884
  multiline: true,
777
885
  textAlignVertical: "top",
@@ -786,12 +894,12 @@ var MushiBottomSheet = ({
786
894
  {
787
895
  source: { uri: activeScreenshot },
788
896
  style: s.screenshotThumb,
789
- accessibilityLabel: "Attached screenshot"
897
+ accessibilityLabel: t.step3.screenshotPreviewAlt
790
898
  }
791
899
  ),
792
900
  /* @__PURE__ */ jsxs(View, { style: s.screenshotMeta, children: [
793
- /* @__PURE__ */ jsx(Text, { style: [s.screenshotLabel, { color: colors.text }], children: "Screenshot attached" }),
794
- /* @__PURE__ */ jsx(Text, { style: [s.screenshotSub, { color: colors.sub }], children: screenshotSensitiveHint && screenshotSensitiveHint.trim() ? `\u26A0 ${screenshotSensitiveHint}` : "Helps the team see the issue" })
901
+ /* @__PURE__ */ jsx(Text, { style: [s.screenshotLabel, { color: colors.text }], children: t.step3.screenshotAttached.replace(" \u2713", "") }),
902
+ /* @__PURE__ */ jsx(Text, { style: [s.screenshotSub, { color: colors.sub }], children: screenshotSensitiveHint && screenshotSensitiveHint.trim() ? `\u26A0 ${screenshotSensitiveHint}` : t.step3.screenshotSensitiveHint.split("\u2014")[0]?.trim() ?? t.step3.screenshotSensitiveHint })
795
903
  ] }),
796
904
  /* @__PURE__ */ jsx(
797
905
  TouchableOpacity,
@@ -801,7 +909,7 @@ var MushiBottomSheet = ({
801
909
  onClearScreenshot?.();
802
910
  },
803
911
  hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
804
- accessibilityLabel: "Remove screenshot",
912
+ accessibilityLabel: t.widget.close,
805
913
  children: /* @__PURE__ */ jsx(Text, { style: [s.screenshotRemove, { color: colors.sub }], children: "\u2715" })
806
914
  }
807
915
  )
@@ -816,7 +924,7 @@ var MushiBottomSheet = ({
816
924
  s.submitBtn,
817
925
  { backgroundColor: canSubmit ? colors.accent : colors.disabled }
818
926
  ],
819
- children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: canSubmit ? colors.accentInk : colors.disabledText }], children: phase === "sending" ? "Sending\u2026" : "Submit report" })
927
+ children: /* @__PURE__ */ jsx(Text, { style: [s.submitText, { color: canSubmit ? colors.accentInk : colors.disabledText }], children: phase === "sending" ? t.widget.submitting : t.widget.submit })
820
928
  }
821
929
  )
822
930
  ]
@@ -1022,6 +1130,27 @@ var s = StyleSheet.create({
1022
1130
  borderRadius: 10,
1023
1131
  paddingVertical: 10,
1024
1132
  alignItems: "center"
1133
+ },
1134
+ assistantBubble: {
1135
+ maxWidth: "88%",
1136
+ borderRadius: 12,
1137
+ padding: 12,
1138
+ marginBottom: 10
1139
+ },
1140
+ assistantChip: {
1141
+ borderWidth: 1,
1142
+ borderRadius: 999,
1143
+ paddingHorizontal: 12,
1144
+ paddingVertical: 8,
1145
+ marginBottom: 8,
1146
+ alignSelf: "flex-start"
1147
+ },
1148
+ assistantComposer: {
1149
+ flexDirection: "row",
1150
+ alignItems: "flex-end",
1151
+ gap: 8,
1152
+ marginTop: 12,
1153
+ paddingBottom: 8
1025
1154
  }
1026
1155
  });
1027
1156
 
@@ -1035,9 +1164,9 @@ import {
1035
1164
  Dimensions as Dimensions3,
1036
1165
  useColorScheme as useColorScheme2
1037
1166
  } from "react-native";
1038
- import { mushiPalette as mushiPalette2, MUSHI_COPY } from "@mushi-mushi/core";
1167
+ import { mushiPalette as mushiPalette2, MUSHI_COPY as MUSHI_COPY2, MUSHI_GEOMETRY } from "@mushi-mushi/core";
1039
1168
  import { jsx as jsx2 } from "react/jsx-runtime";
1040
- var FAB_SIZE = 56;
1169
+ var FAB_SIZE = MUSHI_GEOMETRY.fabSize;
1041
1170
  var EDGE_PADDING = 12;
1042
1171
  function getScreenBounds() {
1043
1172
  const { width, height } = Dimensions3.get("window");
@@ -1197,7 +1326,7 @@ var MushiFloatingButton = ({
1197
1326
  onStartShouldSetResponder: () => !draggable,
1198
1327
  onResponderGrant: handlePressIn,
1199
1328
  onResponderRelease: handlePressOut,
1200
- children: /* @__PURE__ */ jsx2(Animated2.Text, { style: s2.emoji, children: MUSHI_COPY.triggerText })
1329
+ children: /* @__PURE__ */ jsx2(Animated2.Text, { style: s2.emoji, children: MUSHI_COPY2.triggerText })
1201
1330
  }
1202
1331
  );
1203
1332
  };
@@ -1220,12 +1349,119 @@ var s2 = StyleSheet2.create({
1220
1349
  }
1221
1350
  });
1222
1351
 
1352
+ // src/components/MushiBanner.tsx
1353
+ import { useCallback as useCallback3 } from "react";
1354
+ import {
1355
+ View as View2,
1356
+ Text as Text2,
1357
+ TouchableOpacity as TouchableOpacity2,
1358
+ StyleSheet as StyleSheet3,
1359
+ Platform as Platform4
1360
+ } from "react-native";
1361
+ import { MUSHI_BANNER_NEON as MUSHI_BANNER_NEON2, MUSHI_COPY as MUSHI_COPY3, MUSHI_GEOMETRY as MUSHI_GEOMETRY2 } from "@mushi-mushi/core";
1362
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1363
+ var MUSHI_BANNER_DEFAULT_HEIGHT = MUSHI_GEOMETRY2.bannerHeight;
1364
+ var MushiBanner = ({
1365
+ onPress,
1366
+ onDismiss,
1367
+ message = MUSHI_COPY3.bannerMessage,
1368
+ label = MUSHI_COPY3.bannerLabel,
1369
+ onLayoutHeight
1370
+ }) => {
1371
+ const handleLayout = useCallback3(
1372
+ (e) => {
1373
+ onLayoutHeight?.(e.nativeEvent.layout.height);
1374
+ },
1375
+ [onLayoutHeight]
1376
+ );
1377
+ return /* @__PURE__ */ jsxs2(
1378
+ View2,
1379
+ {
1380
+ style: s3.wrap,
1381
+ onLayout: handleLayout,
1382
+ accessibilityRole: "header",
1383
+ accessibilityLabel: "Mushi feedback banner",
1384
+ children: [
1385
+ /* @__PURE__ */ jsxs2(TouchableOpacity2, { style: s3.main, onPress, activeOpacity: 0.85, children: [
1386
+ /* @__PURE__ */ jsx3(Text2, { style: s3.eyebrow, children: label.toUpperCase() }),
1387
+ /* @__PURE__ */ jsx3(Text2, { style: s3.message, numberOfLines: 2, children: message }),
1388
+ /* @__PURE__ */ jsx3(Text2, { style: s3.cta, children: MUSHI_COPY3.bugCta })
1389
+ ] }),
1390
+ onDismiss ? /* @__PURE__ */ jsx3(
1391
+ TouchableOpacity2,
1392
+ {
1393
+ style: s3.dismiss,
1394
+ onPress: onDismiss,
1395
+ accessibilityRole: "button",
1396
+ accessibilityLabel: "Dismiss banner",
1397
+ hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
1398
+ children: /* @__PURE__ */ jsx3(Text2, { style: s3.dismissText, children: "\xD7" })
1399
+ }
1400
+ ) : null
1401
+ ]
1402
+ }
1403
+ );
1404
+ };
1405
+ var s3 = StyleSheet3.create({
1406
+ wrap: {
1407
+ position: "absolute",
1408
+ left: 0,
1409
+ right: 0,
1410
+ bottom: 0,
1411
+ minHeight: MUSHI_GEOMETRY2.bannerHeight,
1412
+ flexDirection: "row",
1413
+ alignItems: "stretch",
1414
+ backgroundColor: MUSHI_BANNER_NEON2.bg,
1415
+ borderTopWidth: 1,
1416
+ borderTopColor: MUSHI_BANNER_NEON2.border,
1417
+ zIndex: 99998,
1418
+ elevation: 8,
1419
+ paddingBottom: Platform4.OS === "ios" ? 0 : 0
1420
+ },
1421
+ main: {
1422
+ flex: 1,
1423
+ flexDirection: "row",
1424
+ alignItems: "center",
1425
+ gap: 8,
1426
+ paddingHorizontal: 12,
1427
+ paddingVertical: 8
1428
+ },
1429
+ eyebrow: {
1430
+ fontSize: 10,
1431
+ fontWeight: "700",
1432
+ letterSpacing: 1.2,
1433
+ color: MUSHI_BANNER_NEON2.fg
1434
+ },
1435
+ message: {
1436
+ flex: 1,
1437
+ fontSize: 12,
1438
+ fontWeight: "500",
1439
+ color: MUSHI_BANNER_NEON2.fg
1440
+ },
1441
+ cta: {
1442
+ fontSize: 12,
1443
+ fontWeight: "700",
1444
+ color: MUSHI_BANNER_NEON2.fg,
1445
+ textDecorationLine: "underline"
1446
+ },
1447
+ dismiss: {
1448
+ justifyContent: "center",
1449
+ paddingHorizontal: 12
1450
+ },
1451
+ dismissText: {
1452
+ fontSize: 20,
1453
+ lineHeight: 22,
1454
+ color: MUSHI_BANNER_NEON2.fg,
1455
+ opacity: 0.7
1456
+ }
1457
+ });
1458
+
1223
1459
  // src/version.ts
1224
1460
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
1225
- var MUSHI_SDK_VERSION = "0.19.1" ? "0.19.1" : "0.17.0";
1461
+ var MUSHI_SDK_VERSION = "0.20.0" ? "0.20.0" : "0.17.0";
1226
1462
 
1227
1463
  // src/provider.tsx
1228
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1464
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1229
1465
  var MushiContext = createContext(null);
1230
1466
  var DEFAULT_SCREENSHOT_HINT = "Check the preview \u2014 remove it if any private info (balances, personal details) is visible.";
1231
1467
  function resolveScreenshotHint(v) {
@@ -1274,6 +1510,8 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1274
1510
  createBreadcrumbBuffer({ max: config.capture?.maxBreadcrumbs ?? 50 })
1275
1511
  );
1276
1512
  const [sheetVisible, setSheetVisible] = useState2(false);
1513
+ const [sheetPreferredTab, setSheetPreferredTab] = useState2("report");
1514
+ const [bannerDismissed, setBannerDismissed] = useState2(false);
1277
1515
  const [sheetScreenshot, setSheetScreenshot] = useState2(null);
1278
1516
  const reporterTokenRef = useRef3(`rn-${config.projectId}-anon`);
1279
1517
  let resolveReporterTokenReady = () => {
@@ -1371,7 +1609,8 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1371
1609
  }
1372
1610
  return () => unsubscribe?.();
1373
1611
  }, []);
1374
- const open = useCallback3(() => {
1612
+ const open = useCallback4(() => {
1613
+ setSheetPreferredTab("report");
1375
1614
  breadcrumbsRef.current.add({
1376
1615
  category: "lifecycle",
1377
1616
  level: "info",
@@ -1401,8 +1640,14 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1401
1640
  setSheetVisible(true);
1402
1641
  });
1403
1642
  }, [config.capture?.screenshot]);
1404
- const close = useCallback3(() => setSheetVisible(false), []);
1405
- const attachTo = useCallback3(() => ({ onPress: open }), [open]);
1643
+ const openAssistant = useCallback4(() => {
1644
+ if (!config.assistant?.enabled) return;
1645
+ setSheetPreferredTab("assistant");
1646
+ setSheetScreenshot(null);
1647
+ setSheetVisible(true);
1648
+ }, [config.assistant?.enabled]);
1649
+ const close = useCallback4(() => setSheetVisible(false), []);
1650
+ const attachTo = useCallback4(() => ({ onPress: open }), [open]);
1406
1651
  useEffect3(() => {
1407
1652
  const trigger2 = config.widget?.trigger ?? "button";
1408
1653
  if (trigger2 !== "shake" && trigger2 !== "both") return;
@@ -1431,8 +1676,9 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1431
1676
  cleanup?.();
1432
1677
  };
1433
1678
  }, [config.widget?.trigger, config.widget?.shakeThreshold, open]);
1434
- const submitReport = useCallback3(
1679
+ const submitReport = useCallback4(
1435
1680
  async (data) => {
1681
+ await reporterTokenReadyRef.current;
1436
1682
  const deviceInfo = getDeviceInfo();
1437
1683
  breadcrumbsRef.current.add({
1438
1684
  category: "lifecycle",
@@ -1507,8 +1753,9 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1507
1753
  if (!result.ok) {
1508
1754
  const isCredentialError = result.error?.code === "HTTP_401" || result.error?.code === "HTTP_403" || result.error?.code?.includes("UNAUTHORIZED") || result.error?.code?.includes("FORBIDDEN");
1509
1755
  if (isCredentialError) {
1756
+ const where = apiEndpoint.replace(/\/$/, "") === DEFAULT_API_ENDPOINT ? "Get the correct values at: https://kensaur.us/mushi-mushi/admin/projects" : "Get the correct values from your Mushi console's Projects page.";
1510
1757
  console.error(
1511
- '[Mushi] Credentials rejected. Check your projectId and apiKey (must have "report:write" scope). Get the correct values at: https://kensaur.us/mushi-mushi/admin/projects'
1758
+ '[Mushi] Credentials rejected. Check your projectId and apiKey (must have "report:write" scope). ' + where
1512
1759
  );
1513
1760
  return;
1514
1761
  }
@@ -1675,15 +1922,36 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1675
1922
  confirmedBugs: 0,
1676
1923
  totalReports: 0
1677
1924
  };
1925
+ },
1926
+ openAssistant,
1927
+ async askAssistant(message, threadId) {
1928
+ const client = apiClientRef.current;
1929
+ if (!client || !config.assistant?.enabled) return null;
1930
+ const res = await client.askAssistant({
1931
+ message,
1932
+ threadId: threadId ?? null,
1933
+ context: pageContextRef.current
1934
+ });
1935
+ return res.ok ? res.data : null;
1678
1936
  }
1679
1937
  }),
1680
- [open, close, attachTo, submitReport, config.rewards?.enabled]
1938
+ [open, close, attachTo, submitReport, openAssistant, config.rewards?.enabled, config.assistant?.enabled]
1681
1939
  );
1682
1940
  const trigger = config.widget?.trigger ?? "button";
1683
- const showFab = trigger === "button" || trigger === "both" || trigger === "auto" || trigger === "edge-tab";
1684
- return /* @__PURE__ */ jsxs2(MushiContext.Provider, { value: instance, children: [
1941
+ const showBanner = trigger === "banner" && !bannerDismissed;
1942
+ const showFab = !showBanner && (trigger === "button" || trigger === "both" || trigger === "auto" || trigger === "edge-tab");
1943
+ return /* @__PURE__ */ jsxs3(MushiContext.Provider, { value: instance, children: [
1685
1944
  children,
1686
- showFab && /* @__PURE__ */ jsx3(
1945
+ showBanner ? /* @__PURE__ */ jsx4(
1946
+ MushiBanner,
1947
+ {
1948
+ onPress: open,
1949
+ onDismiss: () => setBannerDismissed(true),
1950
+ message: config.widget?.banner?.message,
1951
+ label: config.widget?.banner?.label
1952
+ }
1953
+ ) : null,
1954
+ showFab && /* @__PURE__ */ jsx4(
1687
1955
  MushiFloatingButton,
1688
1956
  {
1689
1957
  onPress: open,
@@ -1691,14 +1959,20 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
1691
1959
  inset: config.widget?.inset
1692
1960
  }
1693
1961
  ),
1694
- /* @__PURE__ */ jsx3(
1962
+ /* @__PURE__ */ jsx4(
1695
1963
  MushiBottomSheet,
1696
1964
  {
1697
1965
  visible: sheetVisible,
1698
1966
  onClose: close,
1967
+ preferredTab: sheetPreferredTab,
1699
1968
  screenshotDataUrl: sheetScreenshot ?? void 0,
1700
1969
  onClearScreenshot: () => setSheetScreenshot(null),
1701
- screenshotSensitiveHint: resolveScreenshotHint(config.widget?.screenshotSensitiveHint)
1970
+ screenshotSensitiveHint: resolveScreenshotHint(config.widget?.screenshotSensitiveHint),
1971
+ assistantEnabled: Boolean(config.assistant?.enabled),
1972
+ assistantLabel: config.assistant?.label,
1973
+ assistantGreeting: config.assistant?.greeting,
1974
+ assistantSuggestions: config.assistant?.suggestions,
1975
+ inboxPollIntervalMs: config.widget?.inboxPollIntervalMs ?? 0
1702
1976
  }
1703
1977
  )
1704
1978
  ] });
@@ -1713,10 +1987,10 @@ function useMushi() {
1713
1987
  }
1714
1988
 
1715
1989
  // src/hooks/useMushiReport.ts
1716
- import { useCallback as useCallback4 } from "react";
1990
+ import { useCallback as useCallback5 } from "react";
1717
1991
  function useMushiReport() {
1718
1992
  const mushi = useMushiContext();
1719
- const submitReport = useCallback4(
1993
+ const submitReport = useCallback5(
1720
1994
  async (data) => {
1721
1995
  if (!mushi) throw new Error("MushiProvider not found");
1722
1996
  await mushi.submitReport(data);
@@ -1727,12 +2001,12 @@ function useMushiReport() {
1727
2001
  }
1728
2002
 
1729
2003
  // src/hooks/useMushiWidget.ts
1730
- import { useCallback as useCallback5 } from "react";
2004
+ import { useCallback as useCallback6 } from "react";
1731
2005
  function useMushiWidget() {
1732
2006
  const mushi = useMushiContext();
1733
2007
  return {
1734
- open: useCallback5(() => mushi?.open(), [mushi]),
1735
- close: useCallback5(() => mushi?.close(), [mushi])
2008
+ open: useCallback6(() => mushi?.open(), [mushi]),
2009
+ close: useCallback6(() => mushi?.close(), [mushi])
1736
2010
  };
1737
2011
  }
1738
2012
 
@@ -1769,6 +2043,8 @@ function useNavigationCapture(navigationRef) {
1769
2043
  }
1770
2044
  export {
1771
2045
  AsyncStorageQueue,
2046
+ MUSHI_BANNER_DEFAULT_HEIGHT,
2047
+ MushiBanner,
1772
2048
  MushiBottomSheet,
1773
2049
  MushiFloatingButton,
1774
2050
  MushiProvider,