@appilots/web-sdk 0.2.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.
@@ -0,0 +1,738 @@
1
+ import { AppilotsWebRuntime, APPILOTS_CHAT_ATTR, parseMarkdownBlocks, parseInlineMarkdown } from '../chunk-VHRE4RUO.mjs';
2
+ export { AppilotsWebRuntime } from '../chunk-VHRE4RUO.mjs';
3
+ import { createContext, useMemo, useEffect, useRef, useContext, useSyncExternalStore, useCallback, useState, useLayoutEffect, useId } from 'react';
4
+ import { jsx, jsxs } from 'react/jsx-runtime';
5
+ import { createPortal } from 'react-dom';
6
+
7
+ var RuntimeContext = createContext(null);
8
+ function AppilotsWebProvider(props) {
9
+ const { children, runtime: injected, ...options } = props;
10
+ const runtime = useMemo(
11
+ () => injected ?? new AppilotsWebRuntime(options),
12
+ // Session credentials and host identity define the runtime lifetime.
13
+ // eslint-disable-next-line react-hooks/exhaustive-deps
14
+ [
15
+ injected,
16
+ options.projectId,
17
+ options.apiKey,
18
+ options.apiBaseUrl,
19
+ options.user?.id,
20
+ options.client
21
+ ]
22
+ );
23
+ const { routes, navigate, permissions, root, screens } = options;
24
+ useEffect(() => {
25
+ runtime.configureNavigation({ routes, navigate });
26
+ }, [runtime, routes, navigate]);
27
+ useEffect(() => {
28
+ runtime.setPermissions(permissions);
29
+ }, [runtime, permissions]);
30
+ useEffect(() => {
31
+ runtime.setRoot(root);
32
+ }, [runtime, root]);
33
+ useEffect(() => {
34
+ for (const screen of screens ?? []) runtime.registerScreen(screen);
35
+ }, [runtime, screens]);
36
+ const activeOwnedRuntime = useRef(null);
37
+ useEffect(() => {
38
+ if (injected) return;
39
+ activeOwnedRuntime.current = runtime;
40
+ runtime.resume();
41
+ return () => {
42
+ activeOwnedRuntime.current = null;
43
+ runtime.suspend();
44
+ queueMicrotask(() => {
45
+ if (activeOwnedRuntime.current !== runtime) runtime.dispose();
46
+ });
47
+ };
48
+ }, [runtime, injected]);
49
+ return /* @__PURE__ */ jsx(RuntimeContext.Provider, { value: runtime, children });
50
+ }
51
+ function useAppilotsRuntime() {
52
+ const runtime = useContext(RuntimeContext);
53
+ if (!runtime) {
54
+ throw new Error("useAppilotsRuntime must be used inside an <AppilotsWebProvider>");
55
+ }
56
+ return runtime;
57
+ }
58
+ function useAppilotsChat(options = {}) {
59
+ const runtime = useAppilotsRuntime();
60
+ const { chat } = runtime;
61
+ chat.setOptions(options);
62
+ const state = useSyncExternalStore(chat.subscribeState, chat.getState, chat.getState);
63
+ const sendMessage = useCallback((content) => chat.sendMessage(content), [chat]);
64
+ const requestHuman = useCallback(
65
+ (reason) => chat.requestHuman(reason),
66
+ [chat]
67
+ );
68
+ const cancelMessage = useCallback(() => chat.cancelMessage(), [chat]);
69
+ const clearMessages = useCallback(() => chat.clearMessages(), [chat]);
70
+ const clearError = useCallback(() => chat.clearError(), [chat]);
71
+ return {
72
+ messages: state.messages,
73
+ isLoading: state.isLoading,
74
+ loadingStatusKey: state.loadingStatusKey,
75
+ error: state.error,
76
+ pendingActions: state.pendingActions,
77
+ escalation: state.escalation,
78
+ requestHuman,
79
+ sendMessage,
80
+ cancelMessage,
81
+ clearMessages,
82
+ clearError
83
+ };
84
+ }
85
+ function useAppilotsActions() {
86
+ const runtime = useAppilotsRuntime();
87
+ const { actions } = runtime;
88
+ const state = useSyncExternalStore(actions.subscribeState, actions.getState, actions.getState);
89
+ useEffect(() => {
90
+ actions.scheduleDrain();
91
+ }, [actions, state.actions]);
92
+ const approveAction = useCallback((id) => actions.approveAction(id), [actions]);
93
+ const rejectAction = useCallback((id) => actions.rejectAction(id), [actions]);
94
+ return {
95
+ pendingActions: state.actions.filter((a) => a.status === "pending"),
96
+ executingActions: state.actions.filter((a) => a.status === "executing"),
97
+ completedActions: state.actions.filter(
98
+ (a) => a.status === "completed" || a.status === "failed"
99
+ ),
100
+ approveAction,
101
+ rejectAction
102
+ };
103
+ }
104
+
105
+ // src/react/confirmCard.ts
106
+ var GENERIC_MESSAGE = "Confirm this action?";
107
+ function deriveConfirmCardCopy(payload, defaults = {
108
+ message: GENERIC_MESSAGE,
109
+ confirmLabel: "Confirm",
110
+ cancelLabel: "Cancel",
111
+ describe: (label) => `Confirm "${label}"?`
112
+ }) {
113
+ const str = (key) => {
114
+ const value = payload[key];
115
+ return typeof value === "string" && value ? value : void 0;
116
+ };
117
+ const actionDescription = str("actionDescription");
118
+ const message = str("message") ?? str("title") ?? (actionDescription ? defaults.describe(actionDescription) : void 0) ?? defaults.message;
119
+ return {
120
+ message,
121
+ confirmLabel: str("confirmLabel") ?? defaults.confirmLabel,
122
+ cancelLabel: str("cancelLabel") ?? defaults.cancelLabel
123
+ };
124
+ }
125
+ function inline(text) {
126
+ return parseInlineMarkdown(text).map((segment, index) => {
127
+ switch (segment.kind) {
128
+ case "bold":
129
+ return /* @__PURE__ */ jsx("strong", { children: segment.value }, index);
130
+ case "italic":
131
+ return /* @__PURE__ */ jsx("em", { children: segment.value }, index);
132
+ case "code":
133
+ return /* @__PURE__ */ jsx("code", { children: segment.value }, index);
134
+ case "link":
135
+ return /* @__PURE__ */ jsx("a", { href: segment.href, target: "_blank", rel: "noopener noreferrer", children: segment.value }, index);
136
+ default:
137
+ return segment.value;
138
+ }
139
+ });
140
+ }
141
+ function ChatMarkdown({ content, hiddenValue }) {
142
+ return /* @__PURE__ */ jsx("div", { className: "ap-markdown", children: parseMarkdownBlocks(content.replaceAll("<redacted>", hiddenValue)).map((block, index) => {
143
+ switch (block.kind) {
144
+ case "heading": {
145
+ const Heading = `h${block.level}`;
146
+ return /* @__PURE__ */ jsx(Heading, { children: inline(block.text) }, index);
147
+ }
148
+ case "codeblock":
149
+ return /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: block.code }) }, index);
150
+ case "bullet":
151
+ case "ordered": {
152
+ const List = block.kind === "bullet" ? "ul" : "ol";
153
+ return /* @__PURE__ */ jsx(List, { children: block.items.map((item, i) => /* @__PURE__ */ jsx("li", { children: inline(item) }, i)) }, index);
154
+ }
155
+ default:
156
+ return /* @__PURE__ */ jsx("p", { children: inline(block.text) }, index);
157
+ }
158
+ }) });
159
+ }
160
+
161
+ // src/react/chatStrings.ts
162
+ var chatStrings = {
163
+ en: {
164
+ openChat: "Open chat",
165
+ closeChat: "Close chat",
166
+ hiddenValue: "Protected data",
167
+ title: "Assistant",
168
+ subtitle: "Virtual assistant",
169
+ placeholder: "Write a message\u2026",
170
+ welcome: "How can I help?",
171
+ welcomeBody: "Ask a question or tell me what you would like to do.",
172
+ human: "Talk to a person",
173
+ humanLabel: "Support agent",
174
+ requesting: "Connecting\u2026",
175
+ waiting: "Waiting for a support agent",
176
+ connected: "Human support connected",
177
+ requested: "Your request was sent. You can keep writing here while you wait.",
178
+ resolved: "Human support ended. The assistant is available again.",
179
+ offer: "Would you like help from a person?",
180
+ newChat: "New conversation",
181
+ send: "Send message",
182
+ stop: "Stop response",
183
+ inputHint: "Enter to send \xB7 Shift + Enter for a new line",
184
+ latest: "Latest messages",
185
+ thinking: "Thinking\u2026",
186
+ statusAnalyzing: "Analyzing\u2026",
187
+ statusWaitingApp: "Waiting for the app\u2026",
188
+ statusAdjusting: "Adjusting\u2026",
189
+ errorPrefix: "Something went wrong",
190
+ dismiss: "Dismiss error",
191
+ approval: "Your approval is needed",
192
+ confirm: "Confirm",
193
+ cancel: "Cancel",
194
+ confirmMessage: "Confirm this action?",
195
+ confirmDescription: (label) => `Confirm "${label}"?`
196
+ },
197
+ "pt-BR": {
198
+ openChat: "Abrir chat",
199
+ closeChat: "Fechar chat",
200
+ hiddenValue: "Dado protegido",
201
+ title: "Assistente",
202
+ subtitle: "Assistente virtual",
203
+ placeholder: "Escreva uma mensagem\u2026",
204
+ welcome: "Como posso ajudar?",
205
+ welcomeBody: "Tire uma d\xFAvida ou me conte o que voc\xEA quer fazer.",
206
+ human: "Falar com uma pessoa",
207
+ humanLabel: "Atendente",
208
+ requesting: "Conectando\u2026",
209
+ waiting: "Aguardando um atendente",
210
+ connected: "Atendimento humano conectado",
211
+ requested: "Pedido enviado. Voc\xEA pode continuar escrevendo aqui enquanto aguarda.",
212
+ resolved: "Atendimento humano encerrado. O assistente est\xE1 dispon\xEDvel novamente.",
213
+ offer: "Quer ajuda de uma pessoa?",
214
+ newChat: "Nova conversa",
215
+ send: "Enviar mensagem",
216
+ stop: "Parar resposta",
217
+ inputHint: "Enter para enviar \xB7 Shift + Enter para nova linha",
218
+ latest: "Mensagens recentes",
219
+ thinking: "Pensando\u2026",
220
+ statusAnalyzing: "Analisando\u2026",
221
+ statusWaitingApp: "Aguardando o aplicativo\u2026",
222
+ statusAdjusting: "Ajustando\u2026",
223
+ errorPrefix: "Algo deu errado",
224
+ dismiss: "Fechar erro",
225
+ approval: "Precisamos da sua confirma\xE7\xE3o",
226
+ confirm: "Confirmar",
227
+ cancel: "Cancelar",
228
+ confirmMessage: "Confirma esta a\xE7\xE3o?",
229
+ confirmDescription: (label) => `Confirma "${label}"?`
230
+ }
231
+ };
232
+
233
+ // src/react/chatStyles.ts
234
+ var chatStyles = `
235
+ .appilots-chat { --ap-accent: #2563eb; display: flex; flex-direction: column; height: 100%; min-height: 280px; min-width: 0; color: #172033; background: #f6f8fb; border: 1px solid #e2e7ef; border-radius: 20px; overflow: hidden; font: 14px/1.5 system-ui, -apple-system, sans-serif; box-shadow: 0 8px 32px #17203308; }
236
+ .appilots-chat *, .appilots-chat *::before, .appilots-chat *::after { box-sizing: border-box; }
237
+ .appilots-chat button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; margin: 0; padding: 8px 12px; border: 1px solid #dde3ed; border-radius: 10px; background: #fff; color: inherit; cursor: pointer; font: inherit; line-height: 1.3; }
238
+ .appilots-chat button:hover:not(:disabled) { background: #edf2f9; }
239
+ .appilots-chat button:disabled { opacity: .5; cursor: not-allowed; }
240
+ .appilots-chat button:focus-visible, .appilots-chat a:focus-visible, .appilots-chat textarea:focus-visible { outline: 2px solid var(--ap-accent); outline-offset: 3px; }
241
+ .appilots-chat .ap-header { display: flex; align-items: center; gap: 10px; padding: 18px; background: #fff; border-bottom: 1px solid #e8ecf2; }
242
+ .appilots-chat .ap-avatar { display: grid; place-items: center; flex-shrink: 0; width: 38px; height: 38px; border-radius: 13px; background: #eaf0ff; color: var(--ap-accent); }
243
+ .appilots-chat .ap-identity { flex: 1; min-width: 0; }
244
+ .appilots-chat .ap-title { display: block; font-size: 15px; overflow-wrap: anywhere; }
245
+ .appilots-chat .ap-caption { font-size: 12px; color: #68758a; }
246
+ .appilots-chat .ap-icon { padding: 8px; width: 36px; height: 36px; border: 0; background: transparent; flex-shrink: 0; }
247
+ .appilots-chat .ap-human-button { border: 0; border-radius: 0; border-bottom: 1px solid #e8ecf2; padding: 10px 16px; color: #52647e; font-size: 12px; }
248
+ .appilots-chat .ap-escalation { padding: 12px 18px; color: #16604b; background: #eaf7f1; font-size: 12px; border-bottom: 1px solid #d5ede2; }
249
+ .appilots-chat .ap-log { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 22px 18px; display: flex; flex-direction: column; gap: 20px; scrollbar-width: thin; }
250
+ .appilots-chat .ap-empty { margin: auto 0; padding: 24px 4px; text-align: center; }
251
+ .appilots-chat .ap-empty .ap-avatar { margin: 0 auto 18px; width: 52px; height: 52px; border-radius: 18px; }
252
+ .appilots-chat .ap-empty h2 { margin: 0 0 8px; font-size: 21px; font-weight: 650; letter-spacing: -.4px; }
253
+ .appilots-chat .ap-empty p { margin: 0 auto 24px; max-width: 260px; color: #68758a; }
254
+ .appilots-chat .ap-suggestions { display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
255
+ .appilots-chat .ap-suggestions button { padding: 12px 14px; text-align: left; justify-content: space-between; font-size: 13px; }
256
+ .appilots-chat .ap-message { max-width: 94%; align-self: flex-start; min-width: 0; }
257
+ .appilots-chat .ap-message-user { align-self: flex-end; max-width: 86%; }
258
+ .appilots-chat .ap-sender { display: block; margin: 0 0 5px 3px; color: #68758a; font-size: 11px; font-weight: 600; }
259
+ .appilots-chat .ap-bubble { padding: 12px 15px; background: #fff; border: 1px solid #e7ebf2; border-radius: 4px 17px 17px; overflow-wrap: anywhere; white-space: pre-wrap; }
260
+ .appilots-chat .ap-message-user .ap-bubble { background: var(--ap-accent); color: #fff; border-color: transparent; border-radius: 17px 4px 17px 17px; }
261
+ .appilots-chat .ap-message-human_agent .ap-bubble { border-color: #cde7dc; }
262
+ .appilots-chat .ap-message time { display: block; margin: 5px 3px 0; color: #7a8799; font-size: 10px; }
263
+ .appilots-chat .ap-message-user time { text-align: right; }
264
+ .appilots-chat .ap-system { align-self: center; text-align: center; color: #68758a; font-size: 12px; white-space: pre-wrap; }
265
+ .appilots-chat .ap-system button { display: flex; margin: 8px auto 0; }
266
+ .appilots-chat .ap-markdown { white-space: normal; }
267
+ .appilots-chat .ap-markdown p { margin: 0 0 10px; white-space: pre-wrap; }
268
+ .appilots-chat .ap-markdown h1, .appilots-chat .ap-markdown h2, .appilots-chat .ap-markdown h3 { font-size: 16px; line-height: 1.4; margin: 12px 0 8px; }
269
+ .appilots-chat .ap-markdown ul, .appilots-chat .ap-markdown ol { margin: 8px 0; padding-left: 22px; }
270
+ .appilots-chat .ap-markdown li { margin: 4px 0; white-space: pre-wrap; }
271
+ .appilots-chat .ap-markdown code { font: 12px/1.6 ui-monospace, monospace; background: #edf1f7; border-radius: 4px; padding: 2px 4px; }
272
+ .appilots-chat .ap-markdown pre { margin: 10px 0; padding: 12px; background: #edf1f7; border-radius: 8px; overflow-x: auto; white-space: pre; }
273
+ .appilots-chat .ap-markdown pre code { padding: 0; }
274
+ .appilots-chat .ap-markdown a { color: #1d4ed8; text-decoration: underline; }
275
+ .appilots-chat .ap-markdown > :first-child { margin-top: 0; }
276
+ .appilots-chat .ap-markdown > :last-child { margin-bottom: 0; }
277
+ .appilots-chat .ap-confirm { border: 1px solid #eddbb4; background: #fffdf7; border-radius: 14px; padding: 14px; }
278
+ .appilots-chat .ap-confirm-title { color: #8c641d; font-size: 11px; font-weight: 600; margin-bottom: 6px; }
279
+ .appilots-chat .ap-confirm-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
280
+ .appilots-chat .ap-primary, .appilots-chat .ap-primary:hover:not(:disabled) { background: var(--ap-accent); border-color: transparent; color: #fff; }
281
+ .appilots-chat .ap-loading { display: flex; align-items: center; gap: 8px; color: #68758a; font-size: 12px; }
282
+ .appilots-chat .ap-loading::before { content: ''; width: 6px; height: 6px; background: var(--ap-accent); border-radius: 50%; }
283
+ .appilots-chat .ap-error { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 14px; margin: 0 14px 8px; background: #fef2f2; color: #b42318; border-radius: 10px; font-size: 12px; }
284
+ .appilots-chat .ap-latest { align-self: center; font-size: 12px; margin: 0 0 8px; }
285
+ .appilots-chat .ap-footer { background: #fff; border-top: 1px solid #e8ecf2; padding: 14px; }
286
+ .appilots-chat .ap-composer { display: flex; align-items: flex-end; gap: 8px; padding: 8px; border: 1px solid #dce3ee; border-radius: 16px; background: #fff; }
287
+ .appilots-chat .ap-composer:focus-within { border-color: var(--ap-accent); box-shadow: 0 0 0 2px #2563eb0b; }
288
+ .appilots-chat textarea { display: block; flex: 1; min-width: 0; min-height: 40px; max-height: 120px; margin: 0; padding: 9px 4px; border: 0; border-radius: 4px; background: transparent; color: inherit; font: inherit; line-height: 22px; resize: none; }
289
+ .appilots-chat textarea:focus-visible { outline: none; }
290
+ .appilots-chat .ap-send { width: 38px; height: 38px; padding: 0; border-radius: 12px; flex-shrink: 0; }
291
+ .appilots-chat .ap-hint { display: block; text-align: center; font-size: 10px; color: #7a8799; padding-top: 8px; }
292
+ `;
293
+ function Icon({ name }) {
294
+ const paths = {
295
+ chat: "M21 11.5a8.4 8.4 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7a8.4 8.4 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6A8.4 8.4 0 0 1 12.5 3H13a8.5 8.5 0 0 1 8 8v.5Z",
296
+ human: "M3 14v-3a9 9 0 0 1 18 0v3M3 12h3v8H3zM18 12h3v8h-3zM18 20c0 2-3 2-6 2",
297
+ new: "M12 5v14M5 12h14",
298
+ send: "M12 19V5M5 12l7-7 7 7",
299
+ stop: "M6 6h12v12H6z",
300
+ close: "M6 6l12 12M18 6 6 18"
301
+ };
302
+ return /* @__PURE__ */ jsx(
303
+ "svg",
304
+ {
305
+ "aria-hidden": "true",
306
+ width: "18",
307
+ height: "18",
308
+ viewBox: "0 0 24 24",
309
+ fill: "none",
310
+ stroke: "currentColor",
311
+ strokeWidth: "1.8",
312
+ strokeLinecap: "round",
313
+ strokeLinejoin: "round",
314
+ children: /* @__PURE__ */ jsx("path", { d: paths[name] })
315
+ }
316
+ );
317
+ }
318
+ function AppilotsChat({
319
+ title,
320
+ placeholder,
321
+ locale = "en",
322
+ suggestedPrompts = [],
323
+ style,
324
+ onClose
325
+ }) {
326
+ const strings = chatStrings[locale];
327
+ const chatTitle = title ?? strings.title;
328
+ const {
329
+ messages,
330
+ isLoading,
331
+ loadingStatusKey,
332
+ error,
333
+ escalation,
334
+ sendMessage,
335
+ cancelMessage,
336
+ clearMessages,
337
+ clearError,
338
+ requestHuman
339
+ } = useAppilotsChat({
340
+ errorPrefix: strings.errorPrefix,
341
+ escalationStrings: {
342
+ requested: strings.requested,
343
+ connected: strings.connected,
344
+ resolved: strings.resolved,
345
+ offer: strings.offer
346
+ }
347
+ });
348
+ const { pendingActions, approveAction, rejectAction } = useAppilotsActions();
349
+ const [draft, setDraft] = useState("");
350
+ const [requestingHuman, setRequestingHuman] = useState(false);
351
+ const [showLatest, setShowLatest] = useState(false);
352
+ const inputRef = useRef(null);
353
+ const logRef = useRef(null);
354
+ const followMessages = useRef(true);
355
+ const confirms = pendingActions.filter((action) => action.type === "confirm");
356
+ const scrollToLatest = () => {
357
+ followMessages.current = true;
358
+ if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
359
+ setShowLatest(false);
360
+ };
361
+ useLayoutEffect(() => {
362
+ if (followMessages.current && logRef.current) {
363
+ logRef.current.scrollTop = logRef.current.scrollHeight;
364
+ }
365
+ }, [messages, isLoading, confirms.length]);
366
+ useLayoutEffect(() => {
367
+ const input = inputRef.current;
368
+ if (!input) return;
369
+ input.style.height = "auto";
370
+ input.style.height = `${Math.min(input.scrollHeight, 120)}px`;
371
+ }, [draft]);
372
+ const send = () => {
373
+ const content = draft.trim();
374
+ if (!content || isLoading || requestingHuman) return;
375
+ scrollToLatest();
376
+ clearError();
377
+ setDraft("");
378
+ void sendMessage(content);
379
+ inputRef.current?.focus();
380
+ };
381
+ const askHuman = async (reason) => {
382
+ if (requestingHuman || escalation) return;
383
+ setRequestingHuman(true);
384
+ clearError();
385
+ scrollToLatest();
386
+ try {
387
+ await requestHuman(reason);
388
+ } finally {
389
+ setRequestingHuman(false);
390
+ }
391
+ };
392
+ return (
393
+ // Keep every control and transcript inside the DOM walker's skip marker.
394
+ /* @__PURE__ */ jsxs(
395
+ "section",
396
+ {
397
+ ...{ [APPILOTS_CHAT_ATTR]: "true" },
398
+ className: "appilots-chat",
399
+ style,
400
+ "aria-label": chatTitle,
401
+ lang: locale,
402
+ children: [
403
+ /* @__PURE__ */ jsx("style", { children: chatStyles }),
404
+ /* @__PURE__ */ jsxs("header", { className: "ap-header", children: [
405
+ /* @__PURE__ */ jsx("div", { className: "ap-avatar", children: /* @__PURE__ */ jsx(Icon, { name: escalation ? "human" : "chat" }) }),
406
+ /* @__PURE__ */ jsxs("div", { className: "ap-identity", children: [
407
+ /* @__PURE__ */ jsx("strong", { className: "ap-title", children: chatTitle }),
408
+ /* @__PURE__ */ jsx("span", { className: "ap-caption", children: escalation?.operatorName ?? (escalation ? strings.humanLabel : strings.subtitle) })
409
+ ] }),
410
+ /* @__PURE__ */ jsx(
411
+ "button",
412
+ {
413
+ type: "button",
414
+ className: "ap-icon",
415
+ title: strings.newChat,
416
+ "aria-label": strings.newChat,
417
+ disabled: requestingHuman || !messages.length && !escalation && !error,
418
+ onClick: () => {
419
+ clearMessages();
420
+ setDraft("");
421
+ scrollToLatest();
422
+ inputRef.current?.focus();
423
+ },
424
+ children: /* @__PURE__ */ jsx(Icon, { name: "new" })
425
+ }
426
+ ),
427
+ onClose && /* @__PURE__ */ jsx(
428
+ "button",
429
+ {
430
+ type: "button",
431
+ className: "ap-icon",
432
+ "aria-label": strings.closeChat,
433
+ title: strings.closeChat,
434
+ onClick: onClose,
435
+ children: /* @__PURE__ */ jsx(Icon, { name: "close" })
436
+ }
437
+ )
438
+ ] }),
439
+ escalation ? /* @__PURE__ */ jsxs("div", { className: "ap-escalation", role: "status", "data-testid": "appilots-escalation-status", children: [
440
+ escalation.status === "pending" ? strings.waiting : strings.connected,
441
+ escalation.operatorName && /* @__PURE__ */ jsxs("strong", { children: [
442
+ " \xB7 ",
443
+ escalation.operatorName
444
+ ] })
445
+ ] }) : /* @__PURE__ */ jsxs(
446
+ "button",
447
+ {
448
+ type: "button",
449
+ className: "ap-human-button",
450
+ disabled: requestingHuman,
451
+ onClick: () => void askHuman("user_requested"),
452
+ children: [
453
+ /* @__PURE__ */ jsx(Icon, { name: "human" }),
454
+ requestingHuman ? strings.requesting : strings.human
455
+ ]
456
+ }
457
+ ),
458
+ /* @__PURE__ */ jsxs(
459
+ "div",
460
+ {
461
+ className: "ap-log",
462
+ ref: logRef,
463
+ role: "log",
464
+ "aria-label": chatTitle,
465
+ "aria-live": "polite",
466
+ "data-testid": "appilots-chat-log",
467
+ onScroll: () => {
468
+ const log = logRef.current;
469
+ if (!log) return;
470
+ followMessages.current = log.scrollHeight - log.scrollTop - log.clientHeight < 80;
471
+ setShowLatest(!followMessages.current);
472
+ },
473
+ children: [
474
+ messages.length === 0 && /* @__PURE__ */ jsxs("div", { className: "ap-empty", children: [
475
+ /* @__PURE__ */ jsx("div", { className: "ap-avatar", children: /* @__PURE__ */ jsx(Icon, { name: "chat" }) }),
476
+ /* @__PURE__ */ jsx("h2", { children: strings.welcome }),
477
+ /* @__PURE__ */ jsx("p", { children: strings.welcomeBody }),
478
+ /* @__PURE__ */ jsx("div", { className: "ap-suggestions", children: suggestedPrompts.map((prompt) => /* @__PURE__ */ jsxs(
479
+ "button",
480
+ {
481
+ type: "button",
482
+ onClick: () => {
483
+ setDraft(prompt);
484
+ inputRef.current?.focus();
485
+ },
486
+ children: [
487
+ prompt,
488
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2197" })
489
+ ]
490
+ },
491
+ prompt
492
+ )) })
493
+ ] }),
494
+ messages.map(
495
+ (message) => message.role === "system" ? /* @__PURE__ */ jsxs("div", { className: "ap-system", "data-testid": "appilots-message-system", children: [
496
+ message.content,
497
+ message.metadata?.escalationOffer === true && !escalation && /* @__PURE__ */ jsxs(
498
+ "button",
499
+ {
500
+ type: "button",
501
+ disabled: requestingHuman,
502
+ onClick: () => void askHuman("agent_gave_up"),
503
+ children: [
504
+ /* @__PURE__ */ jsx(Icon, { name: "human" }),
505
+ strings.human
506
+ ]
507
+ }
508
+ )
509
+ ] }, message.id) : /* @__PURE__ */ jsxs(
510
+ "article",
511
+ {
512
+ className: `ap-message ap-message-${message.role}`,
513
+ "data-testid": `appilots-message-${message.role}`,
514
+ children: [
515
+ message.role !== "user" && /* @__PURE__ */ jsx("span", { className: "ap-sender", children: message.role === "human_agent" ? escalation?.operatorName ?? strings.humanLabel : chatTitle }),
516
+ /* @__PURE__ */ jsx("div", { className: "ap-bubble", children: message.role === "user" ? message.content : /* @__PURE__ */ jsx(ChatMarkdown, { content: message.content, hiddenValue: strings.hiddenValue }) }),
517
+ /* @__PURE__ */ jsx("time", { dateTime: new Date(message.timestamp).toISOString(), children: new Date(message.timestamp).toLocaleTimeString(locale, {
518
+ hour: "2-digit",
519
+ minute: "2-digit"
520
+ }) })
521
+ ]
522
+ },
523
+ message.id
524
+ )
525
+ ),
526
+ confirms.map((action) => {
527
+ const { message, confirmLabel, cancelLabel } = deriveConfirmCardCopy(
528
+ action.payload ?? {},
529
+ {
530
+ message: strings.confirmMessage,
531
+ confirmLabel: strings.confirm,
532
+ cancelLabel: strings.cancel,
533
+ describe: strings.confirmDescription
534
+ }
535
+ );
536
+ return /* @__PURE__ */ jsxs("div", { className: "ap-confirm", "data-testid": "appilots-confirm", children: [
537
+ /* @__PURE__ */ jsx("div", { className: "ap-confirm-title", children: strings.approval }),
538
+ /* @__PURE__ */ jsx(ChatMarkdown, { content: message, hiddenValue: strings.hiddenValue }),
539
+ /* @__PURE__ */ jsxs("div", { className: "ap-confirm-actions", children: [
540
+ /* @__PURE__ */ jsx(
541
+ "button",
542
+ {
543
+ type: "button",
544
+ className: "ap-primary",
545
+ "data-testid": "appilots-confirm-accept",
546
+ onClick: () => void approveAction(action.id),
547
+ children: confirmLabel
548
+ }
549
+ ),
550
+ /* @__PURE__ */ jsx(
551
+ "button",
552
+ {
553
+ type: "button",
554
+ "data-testid": "appilots-confirm-reject",
555
+ onClick: () => void rejectAction(action.id),
556
+ children: cancelLabel
557
+ }
558
+ )
559
+ ] })
560
+ ] }, action.id);
561
+ }),
562
+ isLoading && /* @__PURE__ */ jsx("div", { className: "ap-loading", role: "status", "data-testid": "appilots-loading", children: strings[loadingStatusKey] })
563
+ ]
564
+ }
565
+ ),
566
+ showLatest && /* @__PURE__ */ jsxs("button", { type: "button", className: "ap-latest", onClick: scrollToLatest, children: [
567
+ strings.latest,
568
+ " \u2193"
569
+ ] }),
570
+ error && /* @__PURE__ */ jsxs("div", { className: "ap-error", role: "alert", children: [
571
+ /* @__PURE__ */ jsx("span", { children: error }),
572
+ /* @__PURE__ */ jsx(
573
+ "button",
574
+ {
575
+ type: "button",
576
+ className: "ap-icon",
577
+ "aria-label": strings.dismiss,
578
+ onClick: clearError,
579
+ children: /* @__PURE__ */ jsx(Icon, { name: "close" })
580
+ }
581
+ )
582
+ ] }),
583
+ /* @__PURE__ */ jsxs("footer", { className: "ap-footer", children: [
584
+ /* @__PURE__ */ jsxs(
585
+ "form",
586
+ {
587
+ className: "ap-composer",
588
+ onSubmit: (event) => {
589
+ event.preventDefault();
590
+ send();
591
+ },
592
+ children: [
593
+ /* @__PURE__ */ jsx(
594
+ "textarea",
595
+ {
596
+ ref: inputRef,
597
+ rows: 1,
598
+ value: draft,
599
+ placeholder: placeholder ?? strings.placeholder,
600
+ "aria-label": placeholder ?? strings.placeholder,
601
+ "data-testid": "appilots-chat-input",
602
+ onChange: (event) => setDraft(event.target.value),
603
+ onKeyDown: (event) => {
604
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing && event.keyCode !== 229) {
605
+ event.preventDefault();
606
+ send();
607
+ }
608
+ }
609
+ }
610
+ ),
611
+ isLoading ? /* @__PURE__ */ jsx(
612
+ "button",
613
+ {
614
+ type: "button",
615
+ className: "ap-send ap-primary",
616
+ "aria-label": strings.stop,
617
+ title: strings.stop,
618
+ onClick: cancelMessage,
619
+ children: /* @__PURE__ */ jsx(Icon, { name: "stop" })
620
+ }
621
+ ) : /* @__PURE__ */ jsx(
622
+ "button",
623
+ {
624
+ type: "submit",
625
+ className: "ap-send ap-primary",
626
+ "data-testid": "appilots-chat-send",
627
+ "aria-label": strings.send,
628
+ title: strings.send,
629
+ disabled: !draft.trim() || requestingHuman,
630
+ children: /* @__PURE__ */ jsx(Icon, { name: "send" })
631
+ }
632
+ )
633
+ ]
634
+ }
635
+ ),
636
+ /* @__PURE__ */ jsx("span", { className: "ap-hint", children: strings.inputHint })
637
+ ] })
638
+ ]
639
+ }
640
+ )
641
+ );
642
+ }
643
+ var styles = `
644
+ .appilots-chat-fab { position: fixed; bottom: max(16px, env(safe-area-inset-bottom)); right: max(16px, env(safe-area-inset-right)); display: flex; flex-direction: column; align-items: flex-end; gap: 12px; }
645
+ .appilots-chat-fab[data-position="bottom-left"] { right: auto; left: max(16px, env(safe-area-inset-left)); align-items: flex-start; }
646
+ .appilots-chat-fab .ap-fab-panel { width: min(390px, calc(100vw - max(16px, env(safe-area-inset-left)) - max(16px, env(safe-area-inset-right)))); height: min(640px, calc(100dvh - 68px - max(16px, env(safe-area-inset-top)) - max(16px, env(safe-area-inset-bottom)))); }
647
+ .appilots-chat-fab .ap-fab-panel[hidden] { display: none; }
648
+ .appilots-chat-fab .appilots-chat { min-height: 0; box-shadow: 0 16px 64px #17203326; }
649
+ .appilots-chat-fab .ap-fab-button { box-sizing: border-box; display: grid; place-items: center; width: 56px; height: 56px; flex-shrink: 0; padding: 0; margin: 0; border: 0; border-radius: 50%; color: #fff; background: var(--ap-fab-accent); box-shadow: 0 6px 24px #17203333; cursor: pointer; }
650
+ .appilots-chat-fab .ap-fab-button svg { width: 24px; height: 24px; }
651
+ .appilots-chat-fab .ap-fab-button:hover { filter: brightness(.94); }
652
+ .appilots-chat-fab .ap-fab-button:focus-visible { outline: 3px solid var(--ap-fab-accent); outline-offset: 4px; }
653
+ `;
654
+ function AppilotsChatFab({
655
+ position = "bottom-right",
656
+ defaultOpen = false,
657
+ accentColor = "#2563eb",
658
+ zIndex = 1e3,
659
+ locale = "en",
660
+ style,
661
+ ...chatProps
662
+ }) {
663
+ const [open, setOpen] = useState(defaultOpen);
664
+ const [host, setHost] = useState(null);
665
+ const panelId = useId();
666
+ const panelRef = useRef(null);
667
+ const triggerRef = useRef(null);
668
+ const strings = chatStrings[locale];
669
+ useEffect(() => setHost(document.body), []);
670
+ useEffect(() => {
671
+ if (!open) return;
672
+ panelRef.current?.querySelector("textarea")?.focus();
673
+ const log = panelRef.current?.querySelector('[role="log"]');
674
+ if (log) log.scrollTop = log.scrollHeight;
675
+ }, [open, host]);
676
+ const close = () => {
677
+ setOpen(false);
678
+ triggerRef.current?.focus();
679
+ };
680
+ if (!host) return null;
681
+ return createPortal(
682
+ /* @__PURE__ */ jsxs(
683
+ "div",
684
+ {
685
+ ...{ [APPILOTS_CHAT_ATTR]: "true" },
686
+ className: "appilots-chat-fab",
687
+ "data-position": position,
688
+ style: { zIndex, "--ap-fab-accent": accentColor },
689
+ onKeyDown: (event) => {
690
+ if (open && event.key === "Escape" && !event.defaultPrevented && !event.nativeEvent.isComposing) {
691
+ event.stopPropagation();
692
+ close();
693
+ }
694
+ },
695
+ children: [
696
+ /* @__PURE__ */ jsx("style", { children: styles }),
697
+ /* @__PURE__ */ jsx(
698
+ "div",
699
+ {
700
+ id: panelId,
701
+ className: "ap-fab-panel",
702
+ ref: panelRef,
703
+ hidden: !open,
704
+ "data-testid": "appilots-chat-panel",
705
+ children: /* @__PURE__ */ jsx(
706
+ AppilotsChat,
707
+ {
708
+ ...chatProps,
709
+ locale,
710
+ onClose: close,
711
+ style: { "--ap-accent": accentColor, ...style }
712
+ }
713
+ )
714
+ }
715
+ ),
716
+ /* @__PURE__ */ jsx(
717
+ "button",
718
+ {
719
+ type: "button",
720
+ className: "ap-fab-button",
721
+ ref: triggerRef,
722
+ "aria-label": open ? strings.closeChat : strings.openChat,
723
+ title: open ? strings.closeChat : strings.openChat,
724
+ "aria-expanded": open,
725
+ "aria-controls": panelId,
726
+ "data-testid": "appilots-chat-fab",
727
+ onClick: () => open ? close() : setOpen(true),
728
+ children: /* @__PURE__ */ jsx(Icon, { name: open ? "close" : "chat" })
729
+ }
730
+ )
731
+ ]
732
+ }
733
+ ),
734
+ host
735
+ );
736
+ }
737
+
738
+ export { AppilotsChat, AppilotsChatFab, AppilotsWebProvider, useAppilotsActions, useAppilotsChat, useAppilotsRuntime };