@eshal-bot/chat-widget 0.2.35 → 0.2.37

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.
@@ -88734,6 +88734,360 @@
88734
88734
  };
88735
88735
  }
88736
88736
 
88737
+ // Kept in its own tiny module (not A2uiBlock.jsx): MessageList imports it
88738
+ // eagerly, and A2uiBlock/MarkdownRenderer are lazy-loaded to keep charts and
88739
+ // react-markdown out of the widget's initial bundle.
88740
+
88741
+ // 2026-09-26 (P1, double-booking risk): the delivered user turn that
88742
+ // followed the message carrying a card, derived from conversation history by
88743
+ // MessageList. `undefined` = not answered yet. One-shot cards (confirmation,
88744
+ // form) render locked once answered, so a reload cannot re-arm an answered
88745
+ // "Confirm booking". Context rather than a prop so it does not have to be
88746
+ // threaded through MessageBubble -> StreamingMarkdown -> MarkdownRenderer.
88747
+ const A2uiAnsweredContext = /*#__PURE__*/React.createContext(undefined);
88748
+
88749
+ const VerificationContext = /*#__PURE__*/React.createContext(null);
88750
+ const BRAND_BLUE$1 = '#2563eb';
88751
+ function fill(text, values) {
88752
+ return Object.entries(values).reduce((s, [k, v]) => s.replace(`{${k}}`, String(v)), String(text));
88753
+ }
88754
+ function boxStyle(isDark) {
88755
+ return {
88756
+ border: `1px solid ${BRAND_BLUE$1}`,
88757
+ borderRadius: 10,
88758
+ padding: 12,
88759
+ marginTop: 8,
88760
+ background: isDark ? '#111827' : '#ffffff',
88761
+ color: isDark ? '#f3f4f6' : '#111827',
88762
+ display: 'grid',
88763
+ gap: 8,
88764
+ fontSize: 13
88765
+ };
88766
+ }
88767
+ function inputStyle(isDark) {
88768
+ return {
88769
+ width: '100%',
88770
+ boxSizing: 'border-box',
88771
+ padding: '7px 10px',
88772
+ borderRadius: 6,
88773
+ border: `1px solid ${isDark ? '#374151' : '#d1d5db'}`,
88774
+ background: isDark ? '#1f2937' : '#f9fafb',
88775
+ color: 'inherit',
88776
+ fontSize: 14
88777
+ };
88778
+ }
88779
+ function buttonStyle(disabled) {
88780
+ return {
88781
+ padding: '7px 12px',
88782
+ borderRadius: 6,
88783
+ border: 'none',
88784
+ background: BRAND_BLUE$1,
88785
+ color: '#ffffff',
88786
+ fontWeight: 600,
88787
+ fontSize: 13,
88788
+ cursor: disabled ? 'default' : 'pointer',
88789
+ opacity: disabled ? 0.5 : 1,
88790
+ justifySelf: 'start'
88791
+ };
88792
+ }
88793
+ const linkStyle = {
88794
+ background: 'none',
88795
+ border: 'none',
88796
+ padding: 0,
88797
+ color: BRAND_BLUE$1,
88798
+ fontSize: 12,
88799
+ cursor: 'pointer',
88800
+ justifySelf: 'start'
88801
+ };
88802
+
88803
+ /**
88804
+ * The verification code card, rendered from an `eshal-ui` fence of type
88805
+ * "verification" (spec: { mode: 'email'|'sms', tool_call_id?, invocation_id? }).
88806
+ * The address and the code go straight to main-backend through the context;
88807
+ * the code is never sent as a chat message. When the code checks out, the
88808
+ * card sends one resume message carrying `verificationComplete` so the held
88809
+ * tool can run.
88810
+ */
88811
+ function VerifyCard({
88812
+ spec,
88813
+ isDark,
88814
+ onButtonClick
88815
+ }) {
88816
+ const ctx = React.useContext(VerificationContext);
88817
+ const mode = spec?.mode === 'sms' ? 'sms' : 'email';
88818
+ const t = ctx?.t ?? (k => k);
88819
+ const [step, setStep] = React.useState('address'); // address | code | done
88820
+ const [address, setAddress] = React.useState(() => (mode === 'sms' ? ctx?.prefillPhone : ctx?.prefillEmail) || '');
88821
+ const [code, setCode] = React.useState('');
88822
+ const [masked, setMasked] = React.useState('');
88823
+ const [busy, setBusy] = React.useState(false);
88824
+ const [error, setError] = React.useState(null);
88825
+ const [resendIn, setResendIn] = React.useState(0);
88826
+ const [cancelled, setCancelled] = React.useState(false);
88827
+ const resumedRef = React.useRef(false);
88828
+ const pendingKey = React.useRef(Symbol('verify-card'));
88829
+ // A card the conversation already moved past (a later visitor turn exists,
88830
+ // e.g. after a reload) is not waiting for anything.
88831
+ const answered = React.useContext(A2uiAnsweredContext) !== undefined;
88832
+ const waiting = Boolean(ctx) && !answered && !cancelled && step !== 'done';
88833
+
88834
+ // While this card waits for its code, the chat input is blocked so the code
88835
+ // is entered here and never typed into the conversation.
88836
+ React.useEffect(() => {
88837
+ if (!waiting || !ctx?.setPending) return undefined;
88838
+ const key = pendingKey.current;
88839
+ ctx.setPending(key, true);
88840
+ return () => ctx.setPending(key, false);
88841
+ }, [waiting, ctx]);
88842
+
88843
+ // SMS only: ask for the delivery report a few times while waiting for the
88844
+ // code, so a number that cannot receive it is said rather than waited on.
88845
+ React.useEffect(() => {
88846
+ if (mode !== 'sms' || step !== 'code' || !ctx?.status) return undefined;
88847
+ let tries = 0;
88848
+ let stopped = false;
88849
+ const id = setInterval(async () => {
88850
+ tries += 1;
88851
+ if (stopped || tries > 12) {
88852
+ clearInterval(id);
88853
+ return;
88854
+ }
88855
+ try {
88856
+ const res = await ctx.status();
88857
+ const delivery = res?.body?.delivery;
88858
+ if (delivery === 'failed') {
88859
+ setError(t('verifyNotDelivered'));
88860
+ setResendIn(0);
88861
+ clearInterval(id);
88862
+ } else if (delivery === 'delivered' || delivery === null) {
88863
+ clearInterval(id);
88864
+ }
88865
+ } catch {
88866
+ // A missed check only means no early warning.
88867
+ }
88868
+ }, 5000);
88869
+ return () => {
88870
+ stopped = true;
88871
+ clearInterval(id);
88872
+ };
88873
+ }, [mode, step, ctx, t]);
88874
+ React.useEffect(() => {
88875
+ if (resendIn <= 0) return undefined;
88876
+ const id = setTimeout(() => setResendIn(s => s - 1), 1000);
88877
+ return () => clearTimeout(id);
88878
+ }, [resendIn]);
88879
+ if (!ctx) return null;
88880
+ if (cancelled || answered && step !== 'done') {
88881
+ return /*#__PURE__*/jsxRuntimeExports.jsx("div", {
88882
+ style: {
88883
+ ...boxStyle(isDark),
88884
+ borderColor: isDark ? '#374151' : '#d1d5db'
88885
+ },
88886
+ "data-testid": "verify-card",
88887
+ children: /*#__PURE__*/jsxRuntimeExports.jsx("div", {
88888
+ style: {
88889
+ fontSize: 12,
88890
+ opacity: 0.7
88891
+ },
88892
+ children: cancelled ? t('verifyCancelled') : t('verifyTitle')
88893
+ })
88894
+ });
88895
+ }
88896
+ const send = async () => {
88897
+ if (busy || !address.trim()) return;
88898
+ setBusy(true);
88899
+ setError(null);
88900
+ try {
88901
+ const res = await ctx.start({
88902
+ method: mode,
88903
+ ...(mode === 'sms' ? {
88904
+ phone: address.trim()
88905
+ } : {
88906
+ email: address.trim()
88907
+ })
88908
+ });
88909
+ if (res.ok) {
88910
+ setMasked(res.body?.masked || address.trim());
88911
+ setResendIn(Number(res.body?.resend_after_s) || 60);
88912
+ setCode('');
88913
+ setStep('code');
88914
+ } else {
88915
+ setError(res.body?.error || t('verifyGenericError'));
88916
+ if (res.body?.resendAfterS) setResendIn(Number(res.body.resendAfterS));
88917
+ }
88918
+ } catch {
88919
+ setError(t('verifyGenericError'));
88920
+ } finally {
88921
+ setBusy(false);
88922
+ }
88923
+ };
88924
+ const confirm = async () => {
88925
+ const digits = code.replace(/\D/g, '');
88926
+ if (busy || digits.length !== 6) return;
88927
+ setBusy(true);
88928
+ setError(null);
88929
+ try {
88930
+ const res = await ctx.verify({
88931
+ code: digits
88932
+ });
88933
+ if (res.ok) {
88934
+ setMasked(res.body?.masked || masked);
88935
+ setStep('done');
88936
+ if (!resumedRef.current && onButtonClick) {
88937
+ resumedRef.current = true;
88938
+ const text = t('verifyResumeMessage');
88939
+ onButtonClick(text, text, {
88940
+ verificationComplete: {
88941
+ ...(spec?.tool_call_id ? {
88942
+ tool_call_id: String(spec.tool_call_id)
88943
+ } : {}),
88944
+ ...(spec?.invocation_id ? {
88945
+ invocation_id: String(spec.invocation_id)
88946
+ } : {})
88947
+ }
88948
+ });
88949
+ }
88950
+ } else if (res.body?.code === 'wrong_code' && typeof res.body?.triesLeft === 'number') {
88951
+ setError(fill(t('verifyTriesLeft'), {
88952
+ count: res.body.triesLeft
88953
+ }));
88954
+ setCode('');
88955
+ } else {
88956
+ setError(res.body?.error || t('verifyGenericError'));
88957
+ if (res.body?.code === 'expired' || res.body?.code === 'locked') setStep('address');
88958
+ }
88959
+ } catch {
88960
+ setError(t('verifyGenericError'));
88961
+ } finally {
88962
+ setBusy(false);
88963
+ }
88964
+ };
88965
+ if (step === 'done') {
88966
+ return /*#__PURE__*/jsxRuntimeExports.jsx("div", {
88967
+ style: boxStyle(isDark),
88968
+ "data-testid": "verify-card",
88969
+ children: /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
88970
+ style: {
88971
+ color: '#13703a',
88972
+ fontWeight: 600
88973
+ },
88974
+ children: ["\u2713 ", fill(t('verifyDone'), {
88975
+ target: masked
88976
+ })]
88977
+ })
88978
+ });
88979
+ }
88980
+ return /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
88981
+ style: boxStyle(isDark),
88982
+ "data-testid": "verify-card",
88983
+ children: [/*#__PURE__*/jsxRuntimeExports.jsx("div", {
88984
+ style: {
88985
+ fontWeight: 600
88986
+ },
88987
+ children: t('verifyTitle')
88988
+ }), step === 'address' ? /*#__PURE__*/jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, {
88989
+ children: [/*#__PURE__*/jsxRuntimeExports.jsxs("label", {
88990
+ style: {
88991
+ display: 'grid',
88992
+ gap: 4
88993
+ },
88994
+ children: [/*#__PURE__*/jsxRuntimeExports.jsx("span", {
88995
+ style: {
88996
+ fontSize: 12,
88997
+ opacity: 0.8
88998
+ },
88999
+ children: mode === 'sms' ? t('verifyPhoneLabel') : t('verifyEmailLabel')
89000
+ }), /*#__PURE__*/jsxRuntimeExports.jsx("input", {
89001
+ style: inputStyle(isDark),
89002
+ type: mode === 'sms' ? 'tel' : 'email',
89003
+ autoComplete: mode === 'sms' ? 'tel' : 'email',
89004
+ inputMode: mode === 'sms' ? 'tel' : 'email',
89005
+ placeholder: mode === 'sms' ? '+971 50 123 4567' : 'name@example.com',
89006
+ value: address,
89007
+ onChange: e => setAddress(e.target.value),
89008
+ onKeyDown: e => {
89009
+ if (e.key === 'Enter') send();
89010
+ }
89011
+ })]
89012
+ }), /*#__PURE__*/jsxRuntimeExports.jsx("button", {
89013
+ type: "button",
89014
+ style: buttonStyle(busy || !address.trim() || resendIn > 0),
89015
+ disabled: busy || !address.trim() || resendIn > 0,
89016
+ onClick: send,
89017
+ children: busy ? t('verifySending') : resendIn > 0 ? fill(t('verifyResendIn'), {
89018
+ seconds: resendIn
89019
+ }) : t('verifySendCode')
89020
+ })]
89021
+ }) : /*#__PURE__*/jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, {
89022
+ children: [/*#__PURE__*/jsxRuntimeExports.jsx("span", {
89023
+ children: fill(t('verifyEnterCode'), {
89024
+ target: masked
89025
+ })
89026
+ }), /*#__PURE__*/jsxRuntimeExports.jsx("input", {
89027
+ style: {
89028
+ ...inputStyle(isDark),
89029
+ letterSpacing: '0.3em',
89030
+ fontFamily: 'monospace',
89031
+ fontSize: 18
89032
+ },
89033
+ "aria-label": t('verifyCodeLabel'),
89034
+ inputMode: "numeric",
89035
+ autoComplete: "one-time-code",
89036
+ maxLength: 6,
89037
+ value: code,
89038
+ onChange: e => setCode(e.target.value.replace(/\D/g, '').slice(0, 6)),
89039
+ onKeyDown: e => {
89040
+ if (e.key === 'Enter') confirm();
89041
+ }
89042
+ }), /*#__PURE__*/jsxRuntimeExports.jsx("button", {
89043
+ type: "button",
89044
+ style: buttonStyle(busy || code.length !== 6),
89045
+ disabled: busy || code.length !== 6,
89046
+ onClick: confirm,
89047
+ children: t('verifyConfirm')
89048
+ }), resendIn > 0 ? /*#__PURE__*/jsxRuntimeExports.jsx("span", {
89049
+ style: {
89050
+ fontSize: 12,
89051
+ opacity: 0.7
89052
+ },
89053
+ children: fill(t('verifyResendIn'), {
89054
+ seconds: resendIn
89055
+ })
89056
+ }) : /*#__PURE__*/jsxRuntimeExports.jsx("button", {
89057
+ type: "button",
89058
+ style: linkStyle,
89059
+ onClick: send,
89060
+ disabled: busy,
89061
+ children: t('verifyResend')
89062
+ }), /*#__PURE__*/jsxRuntimeExports.jsx("button", {
89063
+ type: "button",
89064
+ style: linkStyle,
89065
+ onClick: () => {
89066
+ setStep('address');
89067
+ setError(null);
89068
+ },
89069
+ children: t('verifyDifferent')
89070
+ })]
89071
+ }), error && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
89072
+ role: "alert",
89073
+ style: {
89074
+ color: '#b42318',
89075
+ fontSize: 12
89076
+ },
89077
+ children: error
89078
+ }), /*#__PURE__*/jsxRuntimeExports.jsx("button", {
89079
+ type: "button",
89080
+ style: {
89081
+ ...linkStyle,
89082
+ color: isDark ? '#9ca3af' : '#6b7280'
89083
+ },
89084
+ onClick: () => setCancelled(true),
89085
+ disabled: busy,
89086
+ children: t('verifyCancel')
89087
+ })]
89088
+ });
89089
+ }
89090
+
88737
89091
  /**
88738
89092
  * Table-cell and label formatting shared by the three A2UI renderers - the
88739
89093
  * agent-sdk, the widget and the frontend Testing Lab (EV-4798 MERN-19). Each
@@ -88875,18 +89229,6 @@
88875
89229
  }, key++) : seg.value);
88876
89230
  }
88877
89231
 
88878
- // Kept in its own tiny module (not A2uiBlock.jsx): MessageList imports it
88879
- // eagerly, and A2uiBlock/MarkdownRenderer are lazy-loaded to keep charts and
88880
- // react-markdown out of the widget's initial bundle.
88881
-
88882
- // 2026-09-26 (P1, double-booking risk): the delivered user turn that
88883
- // followed the message carrying a card, derived from conversation history by
88884
- // MessageList. `undefined` = not answered yet. One-shot cards (confirmation,
88885
- // form) render locked once answered, so a reload cannot re-arm an answered
88886
- // "Confirm booking". Context rather than a prop so it does not have to be
88887
- // threaded through MessageBubble -> StreamingMarkdown -> MarkdownRenderer.
88888
- const A2uiAnsweredContext = /*#__PURE__*/React.createContext(undefined);
88889
-
88890
89232
  /**
88891
89233
  * A2uiBlock - render `eshal-ui` / `eshal-chart` / `chart` fence JSON as a
88892
89234
  * real visual block inside the embedded chat widget.
@@ -89026,6 +89368,13 @@
89026
89368
  isDark: isDark,
89027
89369
  onButtonClick: onButtonClick
89028
89370
  });
89371
+ // Verification code card: the runtime holds a personal tool until the
89372
+ // visitor proves their email or phone. The code never travels as chat.
89373
+ if (t === 'verification') return /*#__PURE__*/jsxRuntimeExports.jsx(VerifyCard, {
89374
+ spec: spec,
89375
+ isDark: isDark,
89376
+ onButtonClick: onButtonClick
89377
+ });
89029
89378
  // Cross-industry primitives (14-08-2026) - mirrors
89030
89379
  // packages/frontend/src/components/charts/A2uiBlock.tsx's menu/profile/
89031
89380
  // timeline/rating/comparison/gallery additions.