@vinaverse/react-native 0.1.0-alpha.2 → 0.1.0-alpha.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/README.md CHANGED
@@ -76,8 +76,33 @@ kit share one copy of `@vinaverse/client`.
76
76
  | `onOpenLink(link, card)` | a card's link (it already passed the deep-link rule): route by the page it names |
77
77
  | `onCardAction(card, action)` | a card action without a link (also sent to the session) |
78
78
  | `onPickPhoto(question, card)` | a photo question: pick, upload to your backend, return `{ upload_ref }` |
79
+ | `failureAction` | `{ label?, onPress(state) }`: a way out on a failed turn and on "couldn't connect" ([When the agent fails](#when-the-agent-fails)) |
79
80
  | `onWarning(message)` | development warnings: a hard-to-read theme, copy that says "assistant" |
80
81
 
82
+ ## When the agent fails
83
+
84
+ Two failures need a way out for the person: a turn that failed before any reply ("Something went wrong.") and no
85
+ session to talk to ("Couldn't connect.", with "Try again"). Give one action for both, drawn as a button by the kit's
86
+ own parts, so a person is always the way out:
87
+
88
+ ```tsx
89
+ <VinaverseChat
90
+ conversation={conversation}
91
+ failureAction={{ onPress: (state) => openSupport(state.conversationId) }} // "Talk to a person", in the person's language
92
+ />
93
+ ```
94
+
95
+ `label` replaces "Talk to a person" (the copy's `talkToPerson`, which `copy` can also change). Or replace either part:
96
+ `components.Error` (the failed turn) and `components.Unavailable` (the banner) each get the `state` and the kit's own
97
+ part as `Default`. With neither, the kit draws both as before.
98
+
99
+ | `state` | |
100
+ |---|---|
101
+ | `kind` | `turn_failed` or `unavailable` |
102
+ | `reason` | the turn's error code (`internal`, `timeout`, …), or `unavailable` / `unauthorized` |
103
+ | `retry` | try again, when it can be: the person's text once more, for a turn the server allows to retry (once); a new session for `unavailable`. Undefined otherwise |
104
+ | `conversationId` | the conversation, once a session named it: for your support records |
105
+
81
106
  ## The keyboard
82
107
 
83
108
  `keyboardBehavior` keeps the composer above the keyboard: `padding` on iOS by default, nothing on Android (the window
package/dist/index.cjs CHANGED
@@ -1473,6 +1473,23 @@ var DisclosureBar = react.memo(function DisclosureBar2() {
1473
1473
  }
1474
1474
  );
1475
1475
  });
1476
+ function FailureActionButton(props) {
1477
+ const { handlers, failureAction } = useChat();
1478
+ const { t } = react$1.useCopy();
1479
+ if (!failureAction) return null;
1480
+ return /* @__PURE__ */ jsxRuntime.jsx(
1481
+ Button,
1482
+ {
1483
+ label: failureAction.label ?? t("talkToPerson"),
1484
+ variant: "text",
1485
+ onPress: () => handlers.current.failureAction?.onPress(props.state)
1486
+ }
1487
+ );
1488
+ }
1489
+ function FailureSlot(props) {
1490
+ const { slot: Slot, state, Default } = props;
1491
+ return Slot ? /* @__PURE__ */ jsxRuntime.jsx(Slot, { state, Default }) : /* @__PURE__ */ jsxRuntime.jsx(Default, { state });
1492
+ }
1476
1493
  function DefaultBubble(props) {
1477
1494
  const { colors: colors2, radius: radius2, space: space2, font: font2 } = useTheme();
1478
1495
  const role = props.item.role;
@@ -1628,9 +1645,29 @@ function announce(text) {
1628
1645
  }
1629
1646
  var SystemRow = react.memo(function SystemRow2(props) {
1630
1647
  const { t } = react$1.useCopy();
1631
- const { store } = useChat();
1632
- const { colors: colors2, space: space2, font: font2 } = useTheme();
1648
+ const { store, slots } = useChat();
1649
+ const conversationId = react$1.useConversationSelector(
1650
+ store,
1651
+ (v) => v.conversationId
1652
+ );
1633
1653
  const { event, data } = props.item;
1654
+ if (event === "turn_failed") {
1655
+ const key = props.item.key;
1656
+ const state = {
1657
+ kind: "turn_failed",
1658
+ reason: typeof data.code === "string" ? data.code : null,
1659
+ retry: data.retry === true ? () => void store.retryTurn(key) : void 0,
1660
+ conversationId
1661
+ };
1662
+ return /* @__PURE__ */ jsxRuntime.jsx(
1663
+ FailureSlot,
1664
+ {
1665
+ slot: slots.Error,
1666
+ state,
1667
+ Default: DefaultTurnFailed
1668
+ }
1669
+ );
1670
+ }
1634
1671
  const agent = store.getSnapshot().agent.name ?? t("agentFallbackName");
1635
1672
  const text = (() => {
1636
1673
  switch (event) {
@@ -1644,18 +1681,30 @@ var SystemRow = react.memo(function SystemRow2(props) {
1644
1681
  return data.expected_response ? t("handoffCreated", { expected: data.expected_response }) : t("handoffCreatedNoTime");
1645
1682
  case "flow_completed":
1646
1683
  return data.result === "ok" ? t("flowSent") : data.result === "failed" ? t("flowNotSent") : null;
1647
- case "turn_failed":
1648
- return t("turnFailed");
1649
1684
  default:
1650
1685
  return null;
1651
1686
  }
1652
1687
  })();
1653
1688
  if (!text) return null;
1689
+ return /* @__PURE__ */ jsxRuntime.jsx(Line, { text });
1690
+ });
1691
+ function DefaultTurnFailed(props) {
1692
+ const { t } = react$1.useCopy();
1693
+ const { failureAction } = useChat();
1694
+ const text = t("turnFailed");
1695
+ if (!failureAction) return /* @__PURE__ */ jsxRuntime.jsx(Line, { text, danger: true });
1696
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: { alignItems: "center" }, children: [
1697
+ /* @__PURE__ */ jsxRuntime.jsx(Line, { text, danger: true }),
1698
+ /* @__PURE__ */ jsxRuntime.jsx(FailureActionButton, { state: props.state })
1699
+ ] });
1700
+ }
1701
+ function Line(props) {
1702
+ const { colors: colors2, space: space2, font: font2 } = useTheme();
1654
1703
  return /* @__PURE__ */ jsxRuntime.jsx(
1655
1704
  reactNative.View,
1656
1705
  {
1657
1706
  accessible: true,
1658
- accessibilityLabel: text,
1707
+ accessibilityLabel: props.text,
1659
1708
  style: {
1660
1709
  alignItems: "center",
1661
1710
  paddingHorizontal: space2.xl,
@@ -1665,16 +1714,16 @@ var SystemRow = react.memo(function SystemRow2(props) {
1665
1714
  reactNative.Text,
1666
1715
  {
1667
1716
  style: {
1668
- color: event === "turn_failed" ? colors2.danger : colors2.textMuted,
1717
+ color: props.danger ? colors2.danger : colors2.textMuted,
1669
1718
  fontSize: font2.size.sm,
1670
1719
  textAlign: "center"
1671
1720
  },
1672
- children: text
1721
+ children: props.text
1673
1722
  }
1674
1723
  )
1675
1724
  }
1676
1725
  );
1677
- });
1726
+ }
1678
1727
  var keyOf = (item) => item.key;
1679
1728
  function itemType(item) {
1680
1729
  if (item.type === "card") return `card:${item.card.kind}`;
@@ -1759,10 +1808,23 @@ function useAnnounceReplies(items) {
1759
1808
  }
1760
1809
  var CONNECTING_DELAY = tokens.motion.bannerDelay;
1761
1810
  var StatusBanner = react.memo(function StatusBanner2() {
1762
- const { store } = useChat();
1811
+ const { store, slots } = useChat();
1763
1812
  const { t } = react$1.useCopy();
1764
- const { colors: colors2, space: space2, font: font2 } = useTheme();
1765
1813
  const banner = react$1.useConversationSelector(store, (v) => v.banner);
1814
+ const status = react$1.useConversationSelector(store, (v) => v.status);
1815
+ const conversationId = react$1.useConversationSelector(
1816
+ store,
1817
+ (v) => v.conversationId
1818
+ );
1819
+ const unavailable = react.useMemo(
1820
+ () => ({
1821
+ kind: "unavailable",
1822
+ reason: status,
1823
+ retry: () => store.start(),
1824
+ conversationId
1825
+ }),
1826
+ [store, status, conversationId]
1827
+ );
1766
1828
  const [late, setLate] = react.useState(false);
1767
1829
  react.useEffect(() => {
1768
1830
  setLate(false);
@@ -1771,12 +1833,20 @@ var StatusBanner = react.memo(function StatusBanner2() {
1771
1833
  return () => clearTimeout(timer);
1772
1834
  }, [banner]);
1773
1835
  if (banner === null || banner === "connecting" && !late) return null;
1836
+ if (banner === "unavailable")
1837
+ return /* @__PURE__ */ jsxRuntime.jsx(
1838
+ FailureSlot,
1839
+ {
1840
+ slot: slots.Unavailable,
1841
+ state: unavailable,
1842
+ Default: DefaultUnavailable
1843
+ }
1844
+ );
1774
1845
  const text = {
1775
1846
  connecting: t("bannerConnecting"),
1776
1847
  reconnecting: t("bannerReconnecting"),
1777
1848
  offline: t("bannerOffline"),
1778
- continued_elsewhere: t("bannerContinuedElsewhere"),
1779
- unavailable: t("bannerUnavailable")
1849
+ continued_elsewhere: t("bannerContinuedElsewhere")
1780
1850
  }[banner];
1781
1851
  const action = banner === "continued_elsewhere" ? /* @__PURE__ */ jsxRuntime.jsx(
1782
1852
  Button,
@@ -1785,7 +1855,7 @@ var StatusBanner = react.memo(function StatusBanner2() {
1785
1855
  variant: "text",
1786
1856
  onPress: () => void store.continueHere()
1787
1857
  }
1788
- ) : banner === "offline" || banner === "unavailable" ? /* @__PURE__ */ jsxRuntime.jsx(
1858
+ ) : banner === "offline" ? /* @__PURE__ */ jsxRuntime.jsx(
1789
1859
  Button,
1790
1860
  {
1791
1861
  label: t("tryAgain"),
@@ -1793,6 +1863,18 @@ var StatusBanner = react.memo(function StatusBanner2() {
1793
1863
  onPress: () => store.start()
1794
1864
  }
1795
1865
  ) : null;
1866
+ return /* @__PURE__ */ jsxRuntime.jsx(Banner, { text, children: action });
1867
+ });
1868
+ function DefaultUnavailable(props) {
1869
+ const { t } = react$1.useCopy();
1870
+ const { retry } = props.state;
1871
+ return /* @__PURE__ */ jsxRuntime.jsxs(Banner, { text: t("bannerUnavailable"), children: [
1872
+ retry ? /* @__PURE__ */ jsxRuntime.jsx(Button, { label: t("tryAgain"), variant: "text", onPress: retry }) : null,
1873
+ /* @__PURE__ */ jsxRuntime.jsx(FailureActionButton, { state: props.state })
1874
+ ] });
1875
+ }
1876
+ function Banner(props) {
1877
+ const { colors: colors2, space: space2, font: font2 } = useTheme();
1796
1878
  return /* @__PURE__ */ jsxRuntime.jsxs(
1797
1879
  reactNative.View,
1798
1880
  {
@@ -1814,22 +1896,35 @@ var StatusBanner = react.memo(function StatusBanner2() {
1814
1896
  reactNative.Text,
1815
1897
  {
1816
1898
  style: { color: colors2.text, fontSize: font2.size.sm, flexShrink: 1 },
1817
- children: text
1899
+ children: props.text
1818
1900
  }
1819
1901
  ),
1820
- action
1902
+ props.children
1821
1903
  ]
1822
1904
  }
1823
1905
  );
1824
- });
1906
+ }
1825
1907
  function VinaverseChatProvider(props) {
1826
1908
  const { conversation, components, agentAvatar, timeZone } = props;
1827
1909
  const handlers = react.useRef(props);
1828
1910
  handlers.current = props;
1829
1911
  const slots = useStableSlots(components);
1912
+ const hasAction = props.failureAction !== void 0;
1913
+ const actionLabel = props.failureAction?.label;
1914
+ const failureAction = react.useMemo(
1915
+ () => hasAction ? { label: actionLabel } : null,
1916
+ [hasAction, actionLabel]
1917
+ );
1830
1918
  const value = react.useMemo(
1831
- () => ({ store: conversation, handlers, slots, agentAvatar, timeZone }),
1832
- [conversation, slots, agentAvatar, timeZone]
1919
+ () => ({
1920
+ store: conversation,
1921
+ handlers,
1922
+ slots,
1923
+ agentAvatar,
1924
+ timeZone,
1925
+ failureAction
1926
+ }),
1927
+ [conversation, slots, agentAvatar, timeZone, failureAction]
1833
1928
  );
1834
1929
  useNativeLifecycle(conversation);
1835
1930
  react.useEffect(() => {
@@ -1916,7 +2011,10 @@ exports.Composer = Composer;
1916
2011
  exports.DefaultBubble = DefaultBubble;
1917
2012
  exports.DefaultComposer = DefaultComposer;
1918
2013
  exports.DefaultHeader = DefaultHeader;
2014
+ exports.DefaultTurnFailed = DefaultTurnFailed;
2015
+ exports.DefaultUnavailable = DefaultUnavailable;
1919
2016
  exports.DisclosureBar = DisclosureBar;
2017
+ exports.FailureActionButton = FailureActionButton;
1920
2018
  exports.InfoCard = InfoCard;
1921
2019
  exports.MessageBubble = MessageBubble;
1922
2020
  exports.MessageList = MessageList;