andy-widget-react 2.2.1 → 2.3.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.esm.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
- import { useState, useRef, useEffect, useCallback } from 'react';
4
- import ReactDOM from 'react-dom';
3
+ import { useRef, useState, useSyncExternalStore, useEffect, useCallback } from 'react';
4
+ import ReactDOM, { createPortal } from 'react-dom';
5
5
 
6
6
  /**
7
7
  * Design tokens for AndyChat
@@ -771,6 +771,11 @@ const sendMessageForWidget = async (embedId, sessionId, message, options = {}) =
771
771
  messages: [{ role: 'user', parts: [{ type: 'text', text: message }] }]
772
772
  })
773
773
  });
774
+ return consumeWidgetReply(response, options)
775
+ };
776
+
777
+ /** Shared text-only UI stream decoder; no retry can occur after reading starts. */
778
+ const consumeWidgetReply = async (response, options = {}) => {
774
779
  if (!response.ok || !response.headers.get('content-type')?.includes('text/event-stream')) {
775
780
  const data = await response.json().catch(() => null);
776
781
  throw new Error(data?.error || data?.message || 'Chat is temporarily unavailable.')
@@ -811,6 +816,7 @@ const sendMessageForWidget = async (embedId, sessionId, message, options = {}) =
811
816
  };
812
817
  try {
813
818
  while (!complete) {
819
+ options.signal?.throwIfAborted();
814
820
  const { value, done } = await reader.read();
815
821
  buffer += decoder.decode(value, { stream: !done });
816
822
  let newline;
@@ -3675,7 +3681,425 @@ const createBrowserHandoff = (chatbotId, bootstrap, onProjection, fresh = false)
3675
3681
  }
3676
3682
  };
3677
3683
 
3678
- const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessage, bubbleLabel, lang, predefinedQuestions, theme, orientation, brandColor, bubbleStyle, useBrandColor, logoUrl, previewMode = false, initialOpen = false }) => {
3684
+ /** In-page Requester credentials. A failed response is renewed once, before its body is consumed. */
3685
+ const createRequesterFetch = getRequesterPass => {
3686
+ let pass = null;
3687
+ let loading = null;
3688
+ let revision = 0;
3689
+ const obtain = async () => {
3690
+ const current = revision;
3691
+ if (!loading) {
3692
+ const pending = Promise.resolve().then(getRequesterPass).then(value => {
3693
+ if (current !== revision || typeof value !== 'string' || !value.trim()) throw new Error('Requester authentication is unavailable.')
3694
+ pass = value;
3695
+ return value
3696
+ }).catch(() => { throw new Error('Requester authentication is unavailable.') }).finally(() => { if (loading === pending) loading = null; });
3697
+ loading = pending;
3698
+ }
3699
+ return loading
3700
+ };
3701
+ const requesterFetch = async (url, options = {}) => {
3702
+ const current = revision;
3703
+ const headers = new Headers(options.headers);
3704
+ if (headers.has('authorization')) throw new Error('Requester requests cannot use operator authorization.')
3705
+ const attempt = async credential => {
3706
+ options.signal?.throwIfAborted();
3707
+ if (current !== revision) throw new DOMException('Requester context changed.', 'AbortError')
3708
+ const selected = new Headers(headers);
3709
+ selected.set('X-Andy-Requester', credential);
3710
+ const response = await fetch(url, { ...options, headers: selected, credentials: 'omit', cache: 'no-store' });
3711
+ if (current !== revision) throw new DOMException('Requester context changed.', 'AbortError')
3712
+ return response
3713
+ };
3714
+ let response = await attempt(pass || await obtain());
3715
+ if (!response.ok) {
3716
+ const problem = await response.clone().json().catch(() => null);
3717
+ // Private help and canonical Ticket HTTP use their existing problem envelopes.
3718
+ if (['expired', 'unknown_key'].includes(problem?.problem?.code ?? problem?.code)) {
3719
+ pass = null;
3720
+ response = await attempt(await obtain());
3721
+ }
3722
+ }
3723
+ return response
3724
+ };
3725
+ requesterFetch.clear = () => { revision++; pass = null; loading = null; };
3726
+ return requesterFetch
3727
+ };
3728
+
3729
+ const createSupportClient = ({ installationId, getRequesterPass, apiBaseUrl }) => {
3730
+ if (!/^[a-zA-Z0-9_-]{3,100}$/.test(installationId)) throw new Error('A Support installation is required.')
3731
+ const base = (apiBaseUrl || getApiBaseUrl()).replace(/\/+$/, '');
3732
+ const requesterFetch = createRequesterFetch(getRequesterPass);
3733
+ const widgetUrl = `${base}/support/widget/${encodeURIComponent(installationId)}`;
3734
+ const json = async response => {
3735
+ const result = await response.json().catch(() => null);
3736
+ if (!response.ok || result?.ok === false) {
3737
+ const error = new Error('Support is temporarily unavailable. Please retry.');
3738
+ error.code = result?.problem?.code || result?.code || 'unavailable';
3739
+ throw error
3740
+ }
3741
+ return result
3742
+ };
3743
+ return {
3744
+ bootstrap: signal => requesterFetch(widgetUrl, { signal }).then(json),
3745
+ answer: async (messages, options = {}) => {
3746
+ const response = await requesterFetch(widgetUrl, {
3747
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: options.signal,
3748
+ body: JSON.stringify({ messages })
3749
+ });
3750
+ if (!response.ok) await json(response);
3751
+ return consumeWidgetReply(response, options)
3752
+ },
3753
+ operation: async (action, input, signal) => json(await requesterFetch(`${base}/public/operations/support/request/${action}`, {
3754
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, signal, body: JSON.stringify(input)
3755
+ })),
3756
+ dispose: () => requesterFetch.clear()
3757
+ }
3758
+ };
3759
+
3760
+ /** One in-memory conversation/form receipt lifecycle shared by React renderers and hooks. */
3761
+ const createSupportController = options => {
3762
+ const client = createSupportClient(options);
3763
+ const listeners = new Set();
3764
+ let state = { config: null, view: 'conversation', messages: [], transcript: [], tickets: [], ticket: null, entries: [], loading: true, busy: false, error: null, submission: null, followup: null, unreadCount: 0 };
3765
+ let active = null;
3766
+ let generation = 0;
3767
+ let disposed = false;
3768
+ let submitting = null;
3769
+ let following = null;
3770
+ let visible = true;
3771
+ let timer = null;
3772
+ const schedule = () => {
3773
+ clearTimeout(timer);
3774
+ if (!disposed && state.config && listeners.size) {
3775
+ timer = setTimeout(() => void refresh(), 3000);
3776
+ timer.unref?.();
3777
+ }
3778
+ };
3779
+ const publish = changes => {
3780
+ if (disposed) return
3781
+ state = { ...state, ...changes };
3782
+ listeners.forEach(listener => listener());
3783
+ };
3784
+ const abort = () => { generation++; active?.abort(); active = null; clearTimeout(timer); };
3785
+ const fail = error => {
3786
+ if (error?.code === 'resource_not_found' && state.view === 'request') publish({ ticket: null, entries: [], followup: null });
3787
+ if (error?.code === 'ticket_resolved' && state.ticket) {
3788
+ updateTicket({ ...state.ticket, status: 'resolved' });
3789
+ publish({ followup: null });
3790
+ }
3791
+ publish({ error: error?.code || 'unavailable', busy: false, loading: false });
3792
+ };
3793
+ const request = async (work, background = false) => {
3794
+ abort();
3795
+ const epoch = generation;
3796
+ const controller = new AbortController();
3797
+ active = controller;
3798
+ if (!background) publish({ busy: true, error: null });
3799
+ try {
3800
+ const result = await work(controller.signal, epoch);
3801
+ if (disposed || generation !== epoch || controller.signal.aborted) return
3802
+ return result
3803
+ } catch (error) {
3804
+ if (!controller.signal.aborted && generation === epoch) fail(error);
3805
+ } finally {
3806
+ if (active === controller) { active = null; if (!background) publish({ busy: false }); schedule(); }
3807
+ }
3808
+ };
3809
+ const current = (signal, epoch) => !disposed && generation === epoch && !signal.aborted;
3810
+ const page = async (action, input, signal, field) => {
3811
+ let result, cursor;
3812
+ const values = [];
3813
+ do {
3814
+ result = await client.operation(action, { ...input, pageSize: 100, ...(cursor ? { cursor } : {}) }, signal);
3815
+ values.push(...result.data[field]);
3816
+ cursor = result.meta?.nextCursor;
3817
+ } while (cursor && !signal.aborted)
3818
+ return { ...result.data, [field]: values }
3819
+ };
3820
+ const updateTicket = ticket => {
3821
+ const tickets = state.tickets.map(previous => previous.id === ticket.id ? ticket : previous);
3822
+ publish({ ticket, tickets, unreadCount: tickets.filter(ticket => ticket.unread).length });
3823
+ };
3824
+ const readTicket = async (ticketId, signal, epoch) => {
3825
+ const result = await page('inspect', { ticketId }, signal, 'entries');
3826
+ if (!current(signal, epoch)) return
3827
+ // Only entries returned for this reader can advance the server's seen mark.
3828
+ const entries = result.entries.sort((a, b) => a.sequence - b.sequence);
3829
+ updateTicket(result.ticket);
3830
+ publish({ entries });
3831
+ const throughSequence = Math.max(0, ...entries.map(entry => entry.sequence || 0));
3832
+ if (visible && globalThis.document?.visibilityState !== 'hidden' && throughSequence > result.ticket.requesterLastSeenSequence) {
3833
+ const seen = await client.operation('mark_seen', { ticketId, throughSequence }, signal);
3834
+ if (current(signal, epoch)) updateTicket(seen.data.ticket);
3835
+ }
3836
+ };
3837
+ const readRequests = async (signal, epoch) => {
3838
+ const result = await page('list', {}, signal, 'tickets');
3839
+ if (current(signal, epoch)) publish({ tickets: result.tickets, unreadCount: result.tickets.filter(ticket => ticket.unread).length });
3840
+ };
3841
+ const refresh = () => {
3842
+ if (disposed || !state.config) return Promise.resolve()
3843
+ if (state.busy || globalThis.document?.visibilityState === 'hidden') { schedule(); return Promise.resolve() }
3844
+ return request(async (signal, epoch) => {
3845
+ await readRequests(signal, epoch);
3846
+ if (current(signal, epoch) && visible && state.view === 'request' && state.ticket) await readTicket(state.ticket.id, signal, epoch);
3847
+ }, true)
3848
+ };
3849
+ const transcript = () => state.messages.map(({ role, content }) => ({ role, content }));
3850
+ const bootstrap = () => request(async (signal, epoch) => {
3851
+ const config = await client.bootstrap(signal);
3852
+ if (generation !== epoch || signal.aborted) return
3853
+ publish({ config, loading: false, messages: [{ role: 'assistant', content: config.greeting }] });
3854
+ });
3855
+ const send = async content => {
3856
+ if (state.busy || state.loading || state.view !== 'conversation' || !content.trim() || content.length > 10_000 || state.messages.length >= 39) return
3857
+ publish({ messages: [...transcript(), { role: 'user', content: content.trim() }] });
3858
+ const history = transcript();
3859
+ return request(async (signal, epoch) => {
3860
+ const show = text => {
3861
+ if (generation === epoch && !signal.aborted && state.view === 'conversation')
3862
+ publish({ messages: [...history, { role: 'assistant', content: text }] });
3863
+ };
3864
+ const result = await client.answer(history, { signal, onText: show });
3865
+ if (generation === epoch && !signal.aborted && result.status === 'agent') show(result.text);
3866
+ })
3867
+ };
3868
+ const escalate = () => {
3869
+ if (!state.config) return
3870
+ abort();
3871
+ publish({ view: 'form', transcript: transcript(), busy: false, error: null, submission: null, ticket: null });
3872
+ schedule();
3873
+ };
3874
+ const submit = (subject, body, replyLanguage = 'es') => {
3875
+ if (submitting) return submitting
3876
+ if (state.view !== 'form' || !subject.trim() || !body.trim()) return Promise.resolve()
3877
+ // A retry uses the captured consent/input, even if the caller passes different arguments.
3878
+ const submission = state.submission || { subject: subject.trim(), body: body.trim(), replyLanguage, transcript: state.transcript, idempotencyKey: crypto.randomUUID() };
3879
+ publish({ submission });
3880
+ submitting = request(async (signal, epoch) => {
3881
+ const result = await client.operation('open', submission, signal);
3882
+ if (generation === epoch && !signal.aborted) publish({ view: 'receipt', ticket: result.data.ticket, error: null });
3883
+ return result
3884
+ }).finally(() => { submitting = null; });
3885
+ return submitting
3886
+ };
3887
+ const showRequests = () => {
3888
+ abort();
3889
+ publish({ view: 'requests', busy: false, error: null });
3890
+ return request(readRequests)
3891
+ };
3892
+ const inspect = ticketId => {
3893
+ abort();
3894
+ publish({ view: 'request', ticket: null, entries: [], followup: null, busy: false, error: null });
3895
+ return request((signal, epoch) => readTicket(ticketId, signal, epoch))
3896
+ };
3897
+ const reply = (body, reopen = false) => {
3898
+ if (following) return following
3899
+ if (state.view !== 'request' || !state.ticket || !body.trim() || body.length > 20_000) return Promise.resolve()
3900
+ if (state.ticket.status === 'resolved' && !reopen) { publish({ error: 'ticket_resolved' }); return Promise.resolve() }
3901
+ const action = reopen ? 'reopen' : 'reply';
3902
+ const followup = state.followup?.action === action ? state.followup : { action, ticketId: state.ticket.id, body: body.trim(), idempotencyKey: crypto.randomUUID() };
3903
+ publish({ followup });
3904
+ following = request(async (signal, epoch) => {
3905
+ const result = await client.operation(followup.action, { ticketId: followup.ticketId, body: followup.body, idempotencyKey: followup.idempotencyKey }, signal);
3906
+ if (!current(signal, epoch)) return
3907
+ await readTicket(followup.ticketId, signal, epoch);
3908
+ if (current(signal, epoch)) publish({ followup: null });
3909
+ return result
3910
+ }).finally(() => { following = null; });
3911
+ return following
3912
+ };
3913
+ const newProblem = () => {
3914
+ abort();
3915
+ publish({ view: 'form', transcript: [], submission: null, followup: null, ticket: null, entries: [], busy: false, error: null });
3916
+ schedule();
3917
+ };
3918
+ const newConversation = () => {
3919
+ abort();
3920
+ publish({ view: 'conversation', messages: state.config ? [{ role: 'assistant', content: state.config.greeting }] : [], transcript: [], ticket: null, entries: [], error: null, busy: false, submission: null, followup: null });
3921
+ schedule();
3922
+ };
3923
+ return {
3924
+ getSnapshot: () => state,
3925
+ subscribe: listener => { listeners.add(listener); schedule(); return () => { listeners.delete(listener); if (!listeners.size) clearTimeout(timer); } },
3926
+ bootstrap, send, escalate, submit, showRequests, inspect, newConversation, refresh, reply, newProblem,
3927
+ setVisible: value => { visible = value; if (value) void refresh(); },
3928
+ back: () => { abort(); publish({ view: 'conversation', busy: false, error: null, followup: null }); schedule(); },
3929
+ dispose: () => { abort(); publish({ config: null, view: 'conversation', messages: [], transcript: [], ticket: null, entries: [], tickets: [], submission: null, followup: null, unreadCount: 0, busy: false, loading: true, error: null }); disposed = true; client.dispose(); listeners.clear(); }
3930
+ }
3931
+ };
3932
+
3933
+ /** React Support panel uses the shared design tokens and its own BEM namespace. */
3934
+ const getSupportStylesheet = () => `
3935
+ :host { font-family: var(--andy-font-family-primary); color: #171717; line-height: 1.5; --andy-support-color: #171717; --andy-support-contrast: #fff; }
3936
+ *, *::before, *::after { box-sizing: border-box; }
3937
+ .andy-support button, .andy-support input, .andy-support textarea { font: inherit; }
3938
+ .andy-support button { cursor: pointer; }
3939
+ .andy-support button:disabled { cursor: default; opacity: .45; }
3940
+ .andy-support button:focus-visible, .andy-support a:focus-visible, .andy-support input:focus-visible, .andy-support textarea:focus-visible, .andy-support summary:focus-visible { outline: 2px solid var(--andy-support-color); outline-offset: 3px; }
3941
+ .andy-support__launcher { position: fixed; bottom: 24px; right: 24px; z-index: var(--andy-z-fixed); border: 0; border-radius: var(--andy-radius-full); padding: 14px 20px; background: var(--andy-support-color); color: var(--andy-support-contrast); box-shadow: var(--andy-shadow-lg); display: flex; gap: 24px; align-items: center; font-size: 14px; }
3942
+ .andy-support__panel { position: fixed; right: 24px; bottom: 24px; z-index: var(--andy-z-fixed); width: 400px; max-width: calc(100vw - 48px); height: min(680px, calc(100dvh - 48px)); background: #fff; border: 1px solid #e5e7eb; border-radius: 20px; box-shadow: var(--andy-shadow-xl); display: flex; flex-direction: column; overflow: hidden; font-size: 14px; }
3943
+ :host(.andy-support-host--left) .andy-support__launcher, :host(.andy-support-host--left) .andy-support__panel { left: 24px; right: auto; }
3944
+ .andy-support__header { flex-shrink: 0; position: relative; display: flex; align-items: center; gap: 10px; padding: 18px; border-bottom: 1px solid #f0f0f0; }
3945
+ .andy-support__header > button { border: 0; background: transparent; color: #737373; padding: 5px; font-size: 22px; line-height: 1; min-width: 28px; }
3946
+ .andy-support__mark { width: 34px; height: 34px; border-radius: 10px; background: var(--andy-support-color); color: var(--andy-support-contrast); display: grid; place-items: center; font-weight: 600; }
3947
+ .andy-support__identity { flex: 1; min-width: 0; display: flex; flex-direction: column; }
3948
+ .andy-support__identity strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; }
3949
+ .andy-support__identity small { color: #737373; font-size: 12px; }
3950
+ .andy-support__menu { position: absolute; top: 65px; right: 16px; background: #fff; box-shadow: var(--andy-shadow-lg); border: 1px solid #e5e7eb; border-radius: 12px; padding: 6px; z-index: 1; min-width: 210px; }
3951
+ .andy-support__menu button { display: block; border: 0; border-radius: 6px; background: none; padding: 10px; text-align: left; width: 100%; }
3952
+ .andy-support__menu button:hover { background: #f5f5f5; }
3953
+ .andy-support__content { flex: 1; min-height: 0; overflow-y: auto; padding: 24px 20px; overscroll-behavior: contain; }
3954
+ .andy-support h2 { font-size: 17px; font-weight: 600; margin: 0 0 20px; }
3955
+ .andy-support__message { white-space: pre-wrap; overflow-wrap: anywhere; margin-bottom: 16px; line-height: 1.65; }
3956
+ .andy-support__message--user { margin-left: auto; width: fit-content; max-width: 90%; padding: 10px 14px; border-radius: 16px 16px 4px 16px; background: #f5f5f5; }
3957
+ .andy-support__questions { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; margin-top: 22px; }
3958
+ .andy-support__questions button, .andy-support__secondary { border: 1px solid #e5e7eb; border-radius: 18px; background: #fff; padding: 10px 14px; text-align: left; color: #525252; }
3959
+ .andy-support__questions button:hover, .andy-support__secondary:hover { background: #fafafa; }
3960
+ .andy-support__secondary { margin-top: 8px; }
3961
+ .andy-support__composer { flex-shrink: 0; display: flex; gap: 8px; margin: 0 18px 12px; padding: 8px; border: 1px solid #e5e7eb; border-radius: var(--andy-radius-full); background: #fafafa; }
3962
+ .andy-support__composer input { flex: 1; min-width: 0; border: 0; background: none; padding: 4px 8px; outline-offset: 0; color: inherit; }
3963
+ .andy-support__composer button { border: 0; width: 32px; height: 32px; border-radius: 50%; background: var(--andy-support-color); color: var(--andy-support-contrast); font-size: 20px; }
3964
+ .andy-support__footer { flex-shrink: 0; display: flex; justify-content: center; flex-wrap: wrap; gap: 10px; padding: 0 12px 14px; font-size: 10px; color: #a3a3a3; }
3965
+ .andy-support__footer a { color: #737373; text-decoration: none; }
3966
+ .andy-support__form { display: flex; flex-direction: column; gap: 16px; }
3967
+ .andy-support__form h2 { margin-bottom: 0; }
3968
+ .andy-support__form label { display: flex; flex-direction: column; gap: 6px; font-size: 12px; font-weight: 500; }
3969
+ .andy-support__form input, .andy-support__form textarea { width: 100%; border: 1px solid #d4d4d4; border-radius: 10px; background: #fff; padding: 10px 12px; font-size: 14px; font-weight: 400; color: inherit; }
3970
+ .andy-support__form textarea { resize: vertical; }
3971
+ .andy-support__transcript { border: 1px solid #e5e7eb; border-radius: 10px; padding: 12px; font-size: 12px; }
3972
+ .andy-support__transcript summary { cursor: pointer; }
3973
+ .andy-support__transcript p, .andy-support__entry p { white-space: pre-wrap; overflow-wrap: anywhere; }
3974
+ .andy-support__muted { color: #737373; font-size: 12px; }
3975
+ .andy-support__primary { border: 0; border-radius: 10px; background: var(--andy-support-color); color: var(--andy-support-contrast); padding: 12px 18px; }
3976
+ .andy-support__receipt { text-align: center; padding: 30px 8px; }
3977
+ .andy-support__check { display: inline-grid; place-items: center; width: 48px; height: 48px; border-radius: 50%; background: #f5f5f5; font-size: 24px; margin-bottom: 20px; }
3978
+ .andy-support__receipt h2 { margin-bottom: 10px; }
3979
+ .andy-support__receipt > strong { font-size: 22px; }
3980
+ .andy-support__receipt .andy-support__primary { margin-top: 16px; }
3981
+ .andy-support__ticket { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; width: 100%; text-align: left; padding: 16px 0; border: 0; border-bottom: 1px solid #eee; background: none; }
3982
+ .andy-support__ticket small { grid-column: 1; color: #737373; }
3983
+ .andy-support__ticket > span:last-child { grid-column: 2; grid-row: 1 / 3; align-self: center; font-size: 20px; }
3984
+ .andy-support__entry { border-bottom: 1px solid #eee; padding: 16px 0; }
3985
+ .andy-support__unread { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: currentColor; margin-left: 6px; vertical-align: middle; }
3986
+ .andy-support__ticket .andy-support__unread, .andy-support__menu .andy-support__unread { color: var(--andy-support-color); }
3987
+ .andy-support__request-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }
3988
+ .andy-support__request-actions .andy-support__secondary { margin-top: 0; }
3989
+ .andy-support__followup { flex-shrink: 0; display: flex; flex-direction: column; gap: 8px; margin: 0 18px 12px; }
3990
+ .andy-support__followup label { display: flex; flex-direction: column; gap: 6px; font-size: 12px; }
3991
+ .andy-support__followup textarea { width: 100%; max-height: 120px; resize: vertical; border: 1px solid #d4d4d4; border-radius: 10px; padding: 10px 12px; color: inherit; background: #fff; }
3992
+ .andy-support__followup button { align-self: flex-end; }
3993
+ .andy-support__error { margin-top: 16px; padding: 12px; border-radius: 10px; background: #fef2f2; color: #991b1b; font-size: 12px; }
3994
+ .andy-support__error button { border: 0; background: none; color: inherit; text-decoration: underline; padding: 0; }
3995
+ @media (max-width: 600px) { .andy-support__panel, :host(.andy-support-host--left) .andy-support__panel { inset: 0; width: 100%; max-width: none; height: 100dvh; border-radius: 0; border: 0; padding-bottom: env(safe-area-inset-bottom); } .andy-support__launcher { bottom: calc(18px + env(safe-area-inset-bottom)); right: 18px; } }
3996
+ @media (prefers-reduced-motion: reduce) { .andy-support__content { scroll-behavior: auto; } }
3997
+ `;
3998
+
3999
+ const empty = { config: null, view: 'conversation', messages: [], transcript: [], tickets: [], ticket: null, entries: [], loading: true, busy: false, error: null, submission: null, followup: null, unreadCount: 0 };
4000
+ const noSubscribe = () => () => { };
4001
+ const emptySnapshot = () => empty;
4002
+ /** The host context key changes before a new user/company is allowed to see any previous state. */
4003
+ const useSupportChat = (options) => {
4004
+ const key = options ? JSON.stringify([options.installationId, options.contextKey, options.apiBaseUrl]) : null;
4005
+ const callback = useRef(options?.getRequesterPass);
4006
+ callback.current = options?.getRequesterPass;
4007
+ const [owned, setOwned] = useState(null);
4008
+ const controller = owned?.key === key ? owned?.controller : null;
4009
+ const state = useSyncExternalStore(controller?.subscribe || noSubscribe, controller?.getSnapshot || emptySnapshot, emptySnapshot);
4010
+ useEffect(() => {
4011
+ if (!options || !key)
4012
+ return;
4013
+ const controller = createSupportController({ ...options, getRequesterPass: () => callback.current() });
4014
+ setOwned({ key, controller });
4015
+ void controller.bootstrap();
4016
+ return () => controller.dispose();
4017
+ }, [key]);
4018
+ useEffect(() => {
4019
+ if (state.config && options?.ticketId)
4020
+ void controller?.inspect(options.ticketId);
4021
+ }, [controller, state.config, options?.ticketId]);
4022
+ return { mode: 'support', state, controller };
4023
+ };
4024
+
4025
+ const copy = {
4026
+ es: { support: 'Soporte', close: 'Cerrar', back: 'Volver', menu: 'Menú', new: 'Nueva conversación', team: 'Hablar con el equipo', requests: 'Mis solicitudes', placeholder: '¿En qué te podemos ayudar?', send: 'Enviar', subject: 'Asunto', detail: 'Cuéntanos qué ocurrió', notice: 'Se adjuntará esta conversación', consent: 'Al enviar, aceptas adjuntar esta conversación para que el equipo pueda ayudarte.', submit: 'Enviar solicitud', receipt: 'Solicitud enviada', promise: 'El equipo responderá en', businessDay: 'un día hábil', hours: 'horas hábiles', minutes: 'minutos hábiles', empty: 'Todavía no tienes solicitudes.', powered: 'Con tecnología de Andy', loading: 'Cargando…', retry: 'Reintentar', error: 'No pudimos completar la solicitud. Puedes reintentar.', auth: 'Tu sesión de soporte no está disponible. Vuelve a iniciar sesión en esta aplicación.', denied: 'El soporte no está disponible para esta cuenta.', transcript: 'mensajes', reply: 'Escribe tu respuesta', reopen: 'Reabrir', reopening: 'Cuéntanos qué volvió a ocurrir', otherProblem: 'Es otro problema', reopened: 'Reabierto', unread: 'Respuestas sin leer', resolvedReply: 'Esta solicitud está resuelta. Reábrela para continuar con el mismo problema.', open: 'Recibido', in_progress: 'En curso', waiting_customer: 'Espera tu respuesta', resolved: 'Resuelto' },
4027
+ en: { support: 'Support', close: 'Close', back: 'Back', menu: 'Menu', new: 'New conversation', team: 'Talk to the team', requests: 'My requests', placeholder: 'How can we help?', send: 'Send', subject: 'Subject', detail: 'Tell us what happened', notice: 'This conversation will be attached', consent: 'By sending, you agree to attach this conversation so the team can help you.', submit: 'Send request', receipt: 'Request sent', promise: 'The team will reply within', businessDay: 'one business day', hours: 'business hours', minutes: 'business minutes', empty: 'You have no requests yet.', powered: 'Powered by Andy', loading: 'Loading…', retry: 'Retry', error: 'We could not complete the request. You can retry.', auth: 'Your support session is unavailable. Sign in again in this application.', denied: 'Support is unavailable for this account.', transcript: 'messages', reply: 'Write your reply', reopen: 'Reopen', reopening: 'Tell us what happened again', otherProblem: 'This is another problem', reopened: 'Reopened', unread: 'Unread replies', resolvedReply: 'This request is resolved. Reopen it to continue with the same problem.', open: 'Received', in_progress: 'In progress', waiting_customer: 'Waiting for your reply', resolved: 'Resolved' }
4028
+ };
4029
+ /** Private Requester UI. Sales bootstrap, visitor identity and storage never enter this renderer. */
4030
+ const SupportPanel = ({ support, lang, orientation = 'right', initialOpen = false, tooltip }) => {
4031
+ const { state, controller } = useSupportChat(support);
4032
+ const replyLanguage = lang || state.config?.defaultLang || 'es';
4033
+ const strings = copy[replyLanguage];
4034
+ const [open, setOpen] = useState(initialOpen);
4035
+ const [menu, setMenu] = useState(false);
4036
+ const [draft, setDraft] = useState('');
4037
+ const [subject, setSubject] = useState('');
4038
+ const [body, setBody] = useState('');
4039
+ const [replyDraft, setReplyDraft] = useState('');
4040
+ const [reopening, setReopening] = useState(false);
4041
+ const [selectedTicketId, setSelectedTicketId] = useState(null);
4042
+ const host = useRef(null);
4043
+ const messages = useRef(null);
4044
+ const [portal, setPortal] = useState(null);
4045
+ useEffect(() => {
4046
+ if (!host.current)
4047
+ return;
4048
+ const shadow = host.current.shadowRoot || host.current.attachShadow({ mode: 'open' });
4049
+ const style = document.createElement('style');
4050
+ style.textContent = generateDesignTokenVariablesString() + getSupportStylesheet();
4051
+ const target = document.createElement('div');
4052
+ shadow.append(style, target);
4053
+ setPortal(target);
4054
+ return () => { style.remove(); target.remove(); };
4055
+ }, []);
4056
+ useEffect(() => {
4057
+ const color = state.config?.branding.color || '#171717';
4058
+ host.current?.style.setProperty('--andy-support-color', color);
4059
+ host.current?.style.setProperty('--andy-support-contrast', getContrastTextColor(color));
4060
+ }, [state.config?.branding.color]);
4061
+ useEffect(() => { messages.current?.scrollTo?.({ top: messages.current.scrollHeight, behavior: 'smooth' }); }, [state.messages, state.view]);
4062
+ useEffect(() => { controller?.setVisible(open); }, [controller, open]);
4063
+ useEffect(() => { if (support.ticketId)
4064
+ setOpen(true); }, [support.ticketId]);
4065
+ useEffect(() => { if (state.ticket?.status !== 'resolved' && state.followup?.action !== 'reopen')
4066
+ setReopening(false); }, [state.ticket?.status, state.followup?.action]);
4067
+ const choose = (action) => { setMenu(false); action(); };
4068
+ const escalate = () => { setSubject(''); setBody(''); controller?.escalate(); };
4069
+ const send = () => { if (!draft.trim() || state.busy)
4070
+ return; void controller?.send(draft); setDraft(''); };
4071
+ const newProblem = () => { setSubject(''); setBody(''); setReplyDraft(''); setReopening(false); controller?.newProblem(); };
4072
+ const followup = async () => {
4073
+ const result = await controller?.reply(replyDraft, reopening);
4074
+ if (result) {
4075
+ setReplyDraft('');
4076
+ setReopening(false);
4077
+ }
4078
+ };
4079
+ const unread = jsx("span", { className: 'andy-support__unread', role: 'img', "aria-label": strings.unread });
4080
+ const title = state.config?.branding.name || strings.support;
4081
+ const ticketRow = (ticket) => jsxs("button", { type: 'button', className: 'andy-support__ticket', onClick: () => { setSelectedTicketId(ticket.id); setReplyDraft(''); setReopening(false); void controller?.inspect(ticket.id); }, children: [jsxs("span", { children: ["#", ticket.number, " \u00B7 ", ticket.subject, " ", ticket.unread && unread] }), jsx("small", { children: strings[ticket.status] }), jsx("span", { "aria-hidden": 'true', children: "\u203A" })] }, ticket.id);
4082
+ const retry = () => {
4083
+ if (!state.config)
4084
+ void controller?.bootstrap();
4085
+ else if (state.view === 'form')
4086
+ void controller?.submit(subject, body, replyLanguage);
4087
+ else if (state.view === 'requests')
4088
+ void controller?.showRequests();
4089
+ else if (state.view === 'request' && state.followup)
4090
+ void followup();
4091
+ else if (state.view === 'request' && (selectedTicketId || support.ticketId))
4092
+ void controller?.inspect((selectedTicketId || support.ticketId));
4093
+ };
4094
+ const errorText = state.error === 'ticket_resolved' ? strings.resolvedReply : ['expired', 'unknown_key', 'malformed', 'authentication_required', 'invalid_signature'].includes(state.error || '') ? strings.auth : state.error === 'permission_denied' ? strings.denied : strings.error;
4095
+ const promise = state.ticket?.replyPromise || state.config?.replyPromise;
4096
+ const promiseText = !promise ? null : promise.minutes === promise.businessDayMinutes ? strings.businessDay : promise.minutes % 60 === 0 ? `${promise.minutes / 60} ${strings.hours}` : `${promise.minutes} ${strings.minutes}`;
4097
+ return jsx("div", { ref: host, className: `andy-support-host andy-support-host--${orientation}`, children: portal && createPortal(jsxs("div", { className: 'andy-support', "data-private": true, "data-ph-no-capture": true, "data-sentry-block": true, lang: replyLanguage, children: [!open && jsxs("button", { type: 'button', className: 'andy-support__launcher', title: tooltip, onClick: () => setOpen(true), children: [jsxs("span", { children: [strings.support, " \u00B7 ", title, " ", state.unreadCount > 0 && unread] }), jsx("span", { "aria-hidden": 'true', children: "\u2197" })] }), open && jsxs("section", { className: 'andy-support__panel', role: 'dialog', "aria-label": `${strings.support} · ${title}`, children: [jsxs("header", { className: 'andy-support__header', children: [state.view !== 'conversation' && jsx("button", { type: 'button', "aria-label": strings.back, onClick: () => state.view === 'request' ? void controller?.showRequests() : controller?.back(), children: "\u2039" }), jsx("span", { className: 'andy-support__mark', "aria-hidden": 'true', children: title.slice(0, 1) }), jsxs("div", { className: 'andy-support__identity', children: [jsx("strong", { children: title }), jsx("small", { children: strings.support })] }), jsx("button", { type: 'button', "aria-label": strings.menu, "aria-expanded": menu, disabled: !state.config, onClick: () => setMenu(!menu), children: "\u00B7\u00B7\u00B7" }), jsx("button", { type: 'button', "aria-label": strings.close, onClick: () => setOpen(false), children: "\u00D7" }), menu && jsxs("nav", { className: 'andy-support__menu', "aria-label": strings.menu, children: [jsx("button", { type: 'button', onClick: () => choose(() => controller?.newConversation()), children: strings.new }), jsx("button", { type: 'button', onClick: () => choose(escalate), children: strings.team }), jsxs("button", { type: 'button', onClick: () => choose(() => void controller?.showRequests()), children: [strings.requests, " ", state.unreadCount > 0 && unread] })] })] }), jsxs("div", { className: 'andy-support__content', ref: messages, children: [state.loading && jsx("p", { role: 'status', children: strings.loading }), state.view === 'conversation' && jsxs(Fragment, { children: [state.messages.map((message, index) => jsx("div", { className: `andy-support__message andy-support__message--${message.role}`, children: message.content }, index)), state.config && state.messages.length === 1 && jsx("div", { className: 'andy-support__questions', children: state.config.questions.map(question => jsx("button", { type: 'button', disabled: state.busy, onClick: () => void controller?.send(question), children: question }, question)) }), state.busy && jsx("p", { className: 'andy-support__muted', role: 'status', children: "\u00B7\u00B7\u00B7" }), state.messages.length > 1 && !state.busy && jsxs("button", { type: 'button', className: 'andy-support__secondary', onClick: escalate, children: [strings.team, " \u2197"] })] }), state.view === 'form' && jsxs("form", { className: 'andy-support__form', onSubmit: event => { event.preventDefault(); void controller?.submit(subject, body, replyLanguage); }, children: [jsx("h2", { children: strings.team }), jsxs("label", { children: [strings.subject, jsx("input", { required: true, maxLength: 200, value: state.submission?.subject ?? subject, readOnly: !!state.submission, disabled: state.busy, onChange: event => setSubject(event.target.value) })] }), jsxs("label", { children: [strings.detail, jsx("textarea", { required: true, maxLength: 20000, rows: 5, value: state.submission?.body ?? body, readOnly: !!state.submission, disabled: state.busy, onChange: event => setBody(event.target.value) })] }), jsxs("details", { className: 'andy-support__transcript', children: [jsxs("summary", { children: [strings.notice, " \u00B7 ", state.transcript.length, " ", strings.transcript] }), state.transcript.map((message, index) => jsx("p", { children: message.content }, index))] }), jsx("p", { className: 'andy-support__muted', children: strings.consent }), jsx("button", { type: 'submit', className: 'andy-support__primary', disabled: state.busy || !subject.trim() || !body.trim(), children: state.busy ? strings.loading : strings.submit })] }), state.view === 'receipt' && state.ticket && jsxs("div", { className: 'andy-support__receipt', children: [jsx("span", { className: 'andy-support__check', "aria-hidden": 'true', children: "\u2713" }), jsx("h2", { children: strings.receipt }), jsxs("strong", { children: ["#", state.ticket.number] }), jsx("p", { children: state.ticket.subject }), promiseText && jsxs("p", { children: [strings.promise, " ", jsx("strong", { children: promiseText }), "."] }), jsx("button", { type: 'button', className: 'andy-support__primary', onClick: () => void controller?.showRequests(), children: strings.requests })] }), state.view === 'requests' && jsxs(Fragment, { children: [jsx("h2", { children: strings.requests }), state.tickets.map(ticketRow), !state.busy && !state.error && !state.tickets.length && jsx("p", { className: 'andy-support__muted', children: strings.empty })] }), state.view === 'request' && state.ticket && jsxs(Fragment, { children: [jsxs("h2", { children: ["#", state.ticket.number, " \u00B7 ", state.ticket.subject] }), jsxs("p", { className: 'andy-support__muted', children: [strings[state.ticket.status], state.ticket.reopened && ` · ${strings.reopened}`] }), state.entries.filter(entry => ['requester_message', 'public_reply'].includes(entry.kind)).map(entry => jsxs("article", { className: 'andy-support__entry', children: [jsx("strong", { children: entry.authorName }), jsx("p", { children: entry.body })] }, entry.id)), state.ticket.status === 'resolved' && !reopening && jsxs("div", { className: 'andy-support__request-actions', children: [jsx("button", { type: 'button', className: 'andy-support__primary', onClick: () => setReopening(true), children: strings.reopen }), jsx("button", { type: 'button', className: 'andy-support__secondary', onClick: newProblem, children: strings.otherProblem })] })] }), state.error && jsxs("div", { className: 'andy-support__error', role: 'alert', children: [jsx("p", { children: errorText }), (!state.config || state.view === 'form' || state.view === 'requests' || (state.view === 'request' && state.error !== 'ticket_resolved' && (selectedTicketId || support.ticketId))) && jsx("button", { type: 'button', onClick: retry, disabled: state.busy, children: strings.retry })] })] }), state.view === 'conversation' && jsxs("form", { className: 'andy-support__composer', onSubmit: event => { event.preventDefault(); send(); }, children: [jsx("input", { "aria-label": strings.placeholder, placeholder: strings.placeholder, value: draft, maxLength: 10000, disabled: !state.config, onChange: event => setDraft(event.target.value) }), jsx("button", { type: 'submit', "aria-label": strings.send, disabled: state.busy || !state.config || !draft.trim(), children: "\u2191" })] }), state.view === 'request' && state.ticket && (state.ticket.status !== 'resolved' || reopening) && jsxs("form", { className: 'andy-support__followup', onSubmit: event => { event.preventDefault(); void followup(); }, children: [jsxs("label", { children: [reopening ? strings.reopening : strings.reply, jsx("textarea", { rows: 3, maxLength: 20000, required: true, value: state.followup?.body ?? replyDraft, readOnly: !!state.followup, disabled: state.busy, onChange: event => setReplyDraft(event.target.value) })] }), jsx("button", { type: 'submit', className: 'andy-support__primary', disabled: state.busy || !replyDraft.trim(), children: state.busy ? strings.loading : reopening ? strings.reopen : strings.send })] }), jsxs("footer", { className: 'andy-support__footer', children: [jsx("span", { children: strings.powered }), state.config?.branding.footer.map(link => jsx("a", { href: link.url, target: '_blank', rel: 'noopener noreferrer', children: link.label }, link.url))] })] })] }), portal) });
4098
+ };
4099
+ /** A host identity change also destroys local drafts and menu state before the next render. */
4100
+ const SupportChat = props => jsx(SupportPanel, { ...props }, JSON.stringify([props.support.installationId, props.support.contextKey, props.support.apiBaseUrl]));
4101
+
4102
+ const SalesAndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessage, bubbleLabel, lang, predefinedQuestions, theme, orientation, brandColor, bubbleStyle, useBrandColor, logoUrl, previewMode = false, initialOpen = false }) => {
3679
4103
  const [config, setConfig] = useState(null);
3680
4104
  // The visitor's language wins over the Agent's own default: a Spanish Agent
3681
4105
  // embedded on an English page must still render English chrome.
@@ -4300,10 +4724,14 @@ const AndyChat = ({ embedId, tooltip, name, headline, description, welcomeMessag
4300
4724
  config?.welcomeMessage ||
4301
4725
  'Welcome! How can I help you today?' }), jsx("button", { type: 'button', className: 'andy-welcome-popup__close', "aria-label": strings.closeWelcomeMessage, onClick: () => setShowWelcomePopup(false), children: jsx("svg", { className: 'andy-welcome-popup__close-icon', width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: jsx("path", { d: 'M18 6L6 18M6 6l12 12', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }) }) })] }), jsx("div", { className: 'andy-welcome-popup__arrow' })] }), portalRef.current)] })) }));
4302
4726
  };
4727
+ const AndyChat = props => props.mode === 'support'
4728
+ ? jsx(SupportChat, { ...props }, `${props.support.installationId}:${props.support.contextKey}`)
4729
+ : jsx(SalesAndyChat, { ...props });
4303
4730
 
4304
4731
  const { SESSION_STORAGE_KEY, FORM_DISPLAY_DELAY_MS, PERSIST_SESSION } = CONFIG;
4305
- const useAndyChat = (embedId) => {
4306
- initializeConfig('andy-widget-react');
4732
+ const useSalesAndyChat = (embedId, enabled) => {
4733
+ if (enabled)
4734
+ initializeConfig('andy-widget-react');
4307
4735
  const [config, setConfig] = useState({});
4308
4736
  const [isLoading, setIsLoading] = useState(true);
4309
4737
  const [isOpen, setIsOpen] = useState(false);
@@ -4323,6 +4751,8 @@ const useAndyChat = (embedId) => {
4323
4751
  const welcomeTimeoutRef = useRef(null);
4324
4752
  const userInfoTimeoutRef = useRef(null);
4325
4753
  const initializeChatbot = useCallback(async () => {
4754
+ if (!enabled)
4755
+ return;
4326
4756
  if (!embedId) {
4327
4757
  throw new Error('Embed ID is required');
4328
4758
  }
@@ -4357,7 +4787,7 @@ const useAndyChat = (embedId) => {
4357
4787
  setIsLoading(false);
4358
4788
  throw err;
4359
4789
  }
4360
- }, [embedId]);
4790
+ }, [embedId, enabled]);
4361
4791
  const toggleChat = useCallback(() => {
4362
4792
  setIsOpen(prev => {
4363
4793
  const newIsOpen = !prev;
@@ -4391,7 +4821,7 @@ const useAndyChat = (embedId) => {
4391
4821
  });
4392
4822
  }, [config, welcomePopupShown, userInfoCollected]);
4393
4823
  const sendMessage = useCallback(async (messageText) => {
4394
- if (!messageText.trim() || !config || !sessionId || activeReplyRef.current) {
4824
+ if (!enabled || !messageText.trim() || !config || !sessionId || activeReplyRef.current) {
4395
4825
  return;
4396
4826
  }
4397
4827
  const controller = new AbortController();
@@ -4441,7 +4871,7 @@ const useAndyChat = (embedId) => {
4441
4871
  if (activeReplyRef.current === controller)
4442
4872
  activeReplyRef.current = null;
4443
4873
  }
4444
- }, [embedId, sessionId, config]);
4874
+ }, [embedId, sessionId, config, enabled]);
4445
4875
  const showFaq = useCallback(() => {
4446
4876
  setShowingFaq(true);
4447
4877
  }, []);
@@ -4461,6 +4891,8 @@ const useAndyChat = (embedId) => {
4461
4891
  setShowingUserInfoForm(false);
4462
4892
  }, []);
4463
4893
  const submitUserInfo$1 = useCallback(async (userInfo) => {
4894
+ if (!enabled)
4895
+ return;
4464
4896
  try {
4465
4897
  await submitUserInfo(embedId, sessionId, userInfo);
4466
4898
  setUserInfoCollected(true);
@@ -4477,8 +4909,10 @@ const useAndyChat = (embedId) => {
4477
4909
  console.error('Error submitting user info:', err);
4478
4910
  throw err;
4479
4911
  }
4480
- }, [embedId, sessionId]);
4912
+ }, [embedId, sessionId, enabled]);
4481
4913
  const startNewSession = useCallback(() => {
4914
+ if (!enabled)
4915
+ return;
4482
4916
  activeReplyRef.current?.abort();
4483
4917
  activeReplyRef.current = null;
4484
4918
  const newSessionId = generateSessionId();
@@ -4504,7 +4938,7 @@ const useAndyChat = (embedId) => {
4504
4938
  setMessages([greetingMessage]);
4505
4939
  }
4506
4940
  }
4507
- }, [config]);
4941
+ }, [config, enabled]);
4508
4942
  const addMessage = useCallback((message) => {
4509
4943
  setMessages(prev => [...prev, message]);
4510
4944
  }, []);
@@ -4533,6 +4967,11 @@ const useAndyChat = (embedId) => {
4533
4967
  addMessage
4534
4968
  };
4535
4969
  };
4970
+ function useAndyChat(input) {
4971
+ const support = useSupportChat(typeof input === 'string' ? null : input.support);
4972
+ const sales = useSalesAndyChat(typeof input === 'string' ? input : '', typeof input === 'string');
4973
+ return typeof input === 'string' ? sales : support;
4974
+ }
4536
4975
 
4537
4976
  const ChatBubble = ({ isOpen, config, onClick }) => {
4538
4977
  const bubbleClasses = ['andy-bubble', isOpen && 'andy-bubble--open'].filter(Boolean).join(' ');
@@ -4721,4 +5160,3 @@ const UserInfoForm = ({ headerText, onSubmit, onClose }) => {
4721
5160
  };
4722
5161
 
4723
5162
  export { AndyChat as Andy, AndyChat, ChatBubble, ChatWindow, FAQ, MessageInput, MessageList, UserInfoForm, WelcomePopup, animation, borderRadius, chat, colors, AndyChat as default, generateSessionId, getApiBaseUrl, getEmbedId, isValidEmbedId, parseMarkdown, sanitizeUrl, setApiBaseUrl, shadows, spacing, theme, typography, useAndyChat, zIndex };
4724
- //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import type { SupportAndyChatProps } from './types';
3
+ /** A host identity change also destroys local drafts and menu state before the next render. */
4
+ export declare const SupportChat: React.FC<SupportAndyChatProps>;
@@ -1,2 +1,8 @@
1
1
  import type { UseAndyChatReturn } from '../types';
2
- export declare const useAndyChat: (embedId: string) => UseAndyChatReturn;
2
+ import type { SupportOptions } from 'andy-widget-core';
3
+ import { useSupportChat } from './useSupportChat';
4
+ export declare function useAndyChat(embedId: string): UseAndyChatReturn;
5
+ export declare function useAndyChat(options: {
6
+ mode: 'support';
7
+ support: SupportOptions;
8
+ }): ReturnType<typeof useSupportChat>;
@@ -0,0 +1,23 @@
1
+ import { type SupportOptions, type SupportState } from 'andy-widget-core';
2
+ /** The host context key changes before a new user/company is allowed to see any previous state. */
3
+ export declare const useSupportChat: (options: SupportOptions | null) => {
4
+ mode: "support";
5
+ state: SupportState;
6
+ controller: {
7
+ getSnapshot: () => SupportState;
8
+ subscribe: (listener: () => void) => () => void;
9
+ bootstrap: () => Promise<unknown>;
10
+ send: (content: string) => Promise<unknown>;
11
+ escalate: () => void;
12
+ submit: (subject: string, body: string, replyLanguage?: "es" | "en") => Promise<unknown>;
13
+ showRequests: () => Promise<unknown>;
14
+ inspect: (ticketId: string) => Promise<unknown>;
15
+ reply: (body: string, reopen?: boolean) => Promise<unknown>;
16
+ newProblem: () => void;
17
+ refresh: () => Promise<unknown>;
18
+ setVisible: (visible: boolean) => void;
19
+ newConversation: () => void;
20
+ back: () => void;
21
+ dispose: () => void;
22
+ } | null;
23
+ };
@@ -8,6 +8,6 @@ export { default as MessageInput } from './components/MessageInput';
8
8
  export { default as FAQ } from './components/FAQ';
9
9
  export { default as WelcomePopup } from './components/WelcomePopup';
10
10
  export { default as UserInfoForm } from './components/UserInfoForm';
11
- export type { AndyChatProps, ChatbotConfig, Message, FAQItem, PredefinedQuestion, WelcomePopupConfig, CollectUserInfoConfig, UserInfo, UseAndyChatReturn, ChatBubbleProps, ChatWindowProps, MessageListProps, MessageInputProps, FAQProps, WelcomePopupProps, UserInfoFormProps, FaqItemProps } from './types';
11
+ export type { AndyChatProps, SupportAndyChatProps, SalesAndyChatProps, ChatbotConfig, Message, FAQItem, PredefinedQuestion, WelcomePopupConfig, CollectUserInfoConfig, UserInfo, UseAndyChatReturn, ChatBubbleProps, ChatWindowProps, MessageListProps, MessageInputProps, FAQProps, WelcomePopupProps, UserInfoFormProps, FaqItemProps } from './types';
12
12
  export { theme, colors, typography, spacing, borderRadius, shadows, zIndex, animation, chat, generateSessionId, parseMarkdown, sanitizeUrl, isValidEmbedId, getEmbedId, getApiBaseUrl, setApiBaseUrl } from 'andy-widget-core';
13
13
  export { default } from './AndyChat';