andy-widget-react 2.3.0 → 2.3.2
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 +2 -2
- package/dist/index.cjs +52 -16
- package/dist/index.cjs.map +1 -0
- package/dist/index.esm.js +52 -16
- package/dist/index.esm.js.map +1 -0
- package/dist/types/hooks/useSupportChat.d.ts +1 -0
- package/package.json +2 -2
- package/src/AndyChat.tsx +6 -5
- package/src/SupportChat.tsx +7 -7
- package/src/hooks/useSupportChat.ts +4 -2
package/dist/index.esm.js
CHANGED
|
@@ -1098,6 +1098,17 @@ const generateSessionId = () => {
|
|
|
1098
1098
|
}
|
|
1099
1099
|
};
|
|
1100
1100
|
|
|
1101
|
+
const HTML_ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Escape a value for HTML element content and quoted attribute values.
|
|
1105
|
+
* Use it on every value interpolated into an `innerHTML` template; it
|
|
1106
|
+
* stringifies exactly as `${value}` would.
|
|
1107
|
+
* @param {unknown} value - The value to escape
|
|
1108
|
+
* @returns {string} The escaped text
|
|
1109
|
+
*/
|
|
1110
|
+
const escapeHtml = value => String(value).replace(/[&<>"']/g, character => HTML_ESCAPES[character]);
|
|
1111
|
+
|
|
1101
1112
|
/**
|
|
1102
1113
|
* Sanitize URL to prevent XSS attacks
|
|
1103
1114
|
* @param {string} url - The URL to sanitize
|
|
@@ -1120,6 +1131,12 @@ const sanitizeUrl = url => {
|
|
|
1120
1131
|
return trimmedUrl
|
|
1121
1132
|
}
|
|
1122
1133
|
|
|
1134
|
+
// Root-relative paths stay on the host site. `//`, `/\` and the tabs or
|
|
1135
|
+
// newlines browsers strip from URLs could turn one into another host.
|
|
1136
|
+
if (/^\/(?![/\\])[^\t\n\r]*$/.test(trimmedUrl)) {
|
|
1137
|
+
return trimmedUrl
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1123
1140
|
// If no protocol specified, assume https for regular URLs
|
|
1124
1141
|
if (!lowerUrl.includes(':') && !lowerUrl.startsWith('//')) {
|
|
1125
1142
|
return 'https://' + trimmedUrl
|
|
@@ -1130,17 +1147,19 @@ const sanitizeUrl = url => {
|
|
|
1130
1147
|
};
|
|
1131
1148
|
|
|
1132
1149
|
/**
|
|
1133
|
-
* Parse markdown text to HTML
|
|
1150
|
+
* Parse markdown text to HTML. The text is escaped first, so any HTML in it
|
|
1151
|
+
* renders as text and the result is safe for `innerHTML`.
|
|
1134
1152
|
* @param {string} text - The markdown text to parse
|
|
1135
1153
|
* @returns {string} The parsed HTML
|
|
1136
1154
|
*/
|
|
1137
1155
|
const parseMarkdown = text => {
|
|
1138
1156
|
if (!text) return ''
|
|
1139
1157
|
|
|
1140
|
-
let html = text;
|
|
1158
|
+
let html = escapeHtml(text);
|
|
1141
1159
|
|
|
1142
1160
|
// Handle links first (before bold/italic to avoid conflicts)
|
|
1143
|
-
// Match [text](url) format
|
|
1161
|
+
// Match [text](url) format. Both parts are already escaped, so a quote
|
|
1162
|
+
// cannot leave the href attribute.
|
|
1144
1163
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => {
|
|
1145
1164
|
const sanitizedUrl = sanitizeUrl(url);
|
|
1146
1165
|
const isExternal = sanitizedUrl.startsWith('http://') || sanitizedUrl.startsWith('https://');
|
|
@@ -3769,6 +3788,11 @@ const createSupportController = options => {
|
|
|
3769
3788
|
let following = null;
|
|
3770
3789
|
let visible = true;
|
|
3771
3790
|
let timer = null;
|
|
3791
|
+
let lang = options.lang;
|
|
3792
|
+
let bootstrapConfig = null;
|
|
3793
|
+
const localizedConfig = () => lang && bootstrapConfig.defaultLang && lang !== bootstrapConfig.defaultLang
|
|
3794
|
+
? { ...bootstrapConfig, greeting: lang === 'en' ? 'How can we help?' : '¿Cómo podemos ayudarte?', questions: [] }
|
|
3795
|
+
: bootstrapConfig;
|
|
3772
3796
|
const schedule = () => {
|
|
3773
3797
|
clearTimeout(timer);
|
|
3774
3798
|
if (!disposed && state.config && listeners.size) {
|
|
@@ -3848,8 +3872,10 @@ const createSupportController = options => {
|
|
|
3848
3872
|
};
|
|
3849
3873
|
const transcript = () => state.messages.map(({ role, content }) => ({ role, content }));
|
|
3850
3874
|
const bootstrap = () => request(async (signal, epoch) => {
|
|
3851
|
-
const
|
|
3875
|
+
const bootstrap = await client.bootstrap(signal);
|
|
3852
3876
|
if (generation !== epoch || signal.aborted) return
|
|
3877
|
+
bootstrapConfig = bootstrap;
|
|
3878
|
+
const config = localizedConfig();
|
|
3853
3879
|
publish({ config, loading: false, messages: [{ role: 'assistant', content: config.greeting }] });
|
|
3854
3880
|
});
|
|
3855
3881
|
const send = async content => {
|
|
@@ -3924,6 +3950,13 @@ const createSupportController = options => {
|
|
|
3924
3950
|
getSnapshot: () => state,
|
|
3925
3951
|
subscribe: listener => { listeners.add(listener); schedule(); return () => { listeners.delete(listener); if (!listeners.size) clearTimeout(timer); } },
|
|
3926
3952
|
bootstrap, send, escalate, submit, showRequests, inspect, newConversation, refresh, reply, newProblem,
|
|
3953
|
+
setLanguage: value => {
|
|
3954
|
+
lang = value;
|
|
3955
|
+
if (!state.config) return
|
|
3956
|
+
const config = localizedConfig();
|
|
3957
|
+
// A started conversation and consented transcript keep their original text.
|
|
3958
|
+
publish({ config, ...(state.view === 'conversation' && state.messages.length === 1 ? { messages: [{ role: 'assistant', content: config.greeting }] } : {}) });
|
|
3959
|
+
},
|
|
3927
3960
|
setVisible: value => { visible = value; if (value) void refresh(); },
|
|
3928
3961
|
back: () => { abort(); publish({ view: 'conversation', busy: false, error: null, followup: null }); schedule(); },
|
|
3929
3962
|
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(); }
|
|
@@ -4015,21 +4048,23 @@ const useSupportChat = (options) => {
|
|
|
4015
4048
|
void controller.bootstrap();
|
|
4016
4049
|
return () => controller.dispose();
|
|
4017
4050
|
}, [key]);
|
|
4051
|
+
useEffect(() => { controller?.setLanguage(options?.lang); }, [controller, options?.lang]);
|
|
4052
|
+
const ready = !!state.config;
|
|
4018
4053
|
useEffect(() => {
|
|
4019
|
-
if (
|
|
4054
|
+
if (ready && options?.ticketId)
|
|
4020
4055
|
void controller?.inspect(options.ticketId);
|
|
4021
|
-
}, [controller,
|
|
4056
|
+
}, [controller, ready, options?.ticketId]);
|
|
4022
4057
|
return { mode: 'support', state, controller };
|
|
4023
4058
|
};
|
|
4024
4059
|
|
|
4025
4060
|
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' }
|
|
4061
|
+
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', transcriptSingular: 'mensaje', 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' },
|
|
4062
|
+
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', transcriptSingular: 'message', 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
4063
|
};
|
|
4029
4064
|
/** Private Requester UI. Sales bootstrap, visitor identity and storage never enter this renderer. */
|
|
4030
4065
|
const SupportPanel = ({ support, lang, orientation = 'right', initialOpen = false, tooltip }) => {
|
|
4031
|
-
const { state, controller } = useSupportChat(support);
|
|
4032
|
-
const replyLanguage = lang || state.config?.defaultLang || 'es';
|
|
4066
|
+
const { state, controller } = useSupportChat({ ...support, lang: lang || support.lang });
|
|
4067
|
+
const replyLanguage = lang || support.lang || state.config?.defaultLang || 'es';
|
|
4033
4068
|
const strings = copy[replyLanguage];
|
|
4034
4069
|
const [open, setOpen] = useState(initialOpen);
|
|
4035
4070
|
const [menu, setMenu] = useState(false);
|
|
@@ -4094,7 +4129,7 @@ const SupportPanel = ({ support, lang, orientation = 'right', initialOpen = fals
|
|
|
4094
4129
|
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
4130
|
const promise = state.ticket?.replyPromise || state.config?.replyPromise;
|
|
4096
4131
|
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) });
|
|
4132
|
+
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, " ", state.transcript.length === 1 ? strings.transcriptSingular : 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: sanitizeUrl(link.url), target: '_blank', rel: 'noopener noreferrer', children: link.label }, link.url))] })] })] }), portal) });
|
|
4098
4133
|
};
|
|
4099
4134
|
/** A host identity change also destroys local drafts and menu state before the next render. */
|
|
4100
4135
|
const SupportChat = props => jsx(SupportPanel, { ...props }, JSON.stringify([props.support.installationId, props.support.contextKey, props.support.apiBaseUrl]));
|
|
@@ -4637,7 +4672,7 @@ const SalesAndyChat = ({ embedId, tooltip, name, headline, description, welcomeM
|
|
|
4637
4672
|
bubble.className = 'andy-chat-bubble';
|
|
4638
4673
|
bubble.setAttribute('aria-label', strings.openChat);
|
|
4639
4674
|
const tooltipHTML = tooltip
|
|
4640
|
-
? `<span class="andy-chat-bubble__tooltip" role="tooltip">${tooltip}</span>`
|
|
4675
|
+
? `<span class="andy-chat-bubble__tooltip" role="tooltip">${escapeHtml(tooltip)}</span>`
|
|
4641
4676
|
: '';
|
|
4642
4677
|
bubble.innerHTML = `${CHAT_BUBBLE_DEFAULT_ICON}${tooltipHTML}`;
|
|
4643
4678
|
shadowRef.current.appendChild(bubble);
|
|
@@ -4662,7 +4697,7 @@ const SalesAndyChat = ({ embedId, tooltip, name, headline, description, welcomeM
|
|
|
4662
4697
|
bubble.classList.add('andy-chat-bubble--loaded');
|
|
4663
4698
|
}
|
|
4664
4699
|
const tooltipHTML = !isOpen && tooltip && !isPillStyle
|
|
4665
|
-
? `<span class="andy-chat-bubble__tooltip" role="tooltip">${tooltip}</span>`
|
|
4700
|
+
? `<span class="andy-chat-bubble__tooltip" role="tooltip">${escapeHtml(tooltip)}</span>`
|
|
4666
4701
|
: '';
|
|
4667
4702
|
const configUseBrandColor = config?.useBrandColor ?? true;
|
|
4668
4703
|
const isDarkTheme = config?.theme === 'dark';
|
|
@@ -4675,13 +4710,13 @@ const SalesAndyChat = ({ embedId, tooltip, name, headline, description, welcomeM
|
|
|
4675
4710
|
}
|
|
4676
4711
|
else if (isPillStyle && displayLabel) {
|
|
4677
4712
|
bubble.innerHTML = `
|
|
4678
|
-
<span class="andy-chat-bubble__label">${displayLabel}</span>
|
|
4679
|
-
<img src="${bubbleLogoUrl}" alt="Chat" class="andy-chat-bubble__icon--pill" style="${darkModeIconStyle}" />
|
|
4713
|
+
<span class="andy-chat-bubble__label">${escapeHtml(displayLabel)}</span>
|
|
4714
|
+
<img src="${escapeHtml(bubbleLogoUrl)}" alt="Chat" class="andy-chat-bubble__icon--pill" style="${darkModeIconStyle}" />
|
|
4680
4715
|
`;
|
|
4681
4716
|
}
|
|
4682
4717
|
else {
|
|
4683
4718
|
if (config?.logoUrl) {
|
|
4684
|
-
bubble.innerHTML = `<img src="${config.logoUrl}" alt="Chatbot Logo" class="andy-chat-bubble__logo" style="${darkModeIconStyle}" />${tooltipHTML}`;
|
|
4719
|
+
bubble.innerHTML = `<img src="${escapeHtml(config.logoUrl)}" alt="Chatbot Logo" class="andy-chat-bubble__logo" style="${darkModeIconStyle}" />${tooltipHTML}`;
|
|
4685
4720
|
}
|
|
4686
4721
|
else {
|
|
4687
4722
|
const defaultIconWithStyle = CHAT_BUBBLE_DEFAULT_ICON.replace('class="andy-chat-bubble__icon"', `class="andy-chat-bubble__icon" style="${darkModeIconStyle}"`);
|
|
@@ -5160,3 +5195,4 @@ const UserInfoForm = ({ headerText, onSubmit, onClose }) => {
|
|
|
5160
5195
|
};
|
|
5161
5196
|
|
|
5162
5197
|
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 };
|
|
5198
|
+
//# sourceMappingURL=index.esm.js.map
|