@appilots/web-sdk 0.2.0 → 0.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.
@@ -1,7 +1,7 @@
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';
1
+ import { AppilotsWebRuntime, createWebSpeechHost, isSpeechHost, APPILOTS_CHAT_ATTR, confirmExpiresAt, describeAction, startStillWorkingTimer, appilotsDebugWarn, parseMarkdownBlocks, parseInlineMarkdown } from '../chunk-RR3T5CPP.mjs';
2
+ export { AppilotsWebRuntime, createWebSpeechHost, isWebSpeechSupported } from '../chunk-RR3T5CPP.mjs';
3
+ import { createContext, useMemo, useEffect, useRef, useContext, useSyncExternalStore, useCallback, useState, useId, useLayoutEffect } from 'react';
4
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
  import { createPortal } from 'react-dom';
6
6
 
7
7
  var RuntimeContext = createContext(null);
@@ -68,6 +68,9 @@ function useAppilotsChat(options = {}) {
68
68
  const cancelMessage = useCallback(() => chat.cancelMessage(), [chat]);
69
69
  const clearMessages = useCallback(() => chat.clearMessages(), [chat]);
70
70
  const clearError = useCallback(() => chat.clearError(), [chat]);
71
+ const pauseMission = useCallback(() => chat.pauseMission(), [chat]);
72
+ const resumeMission = useCallback(() => chat.resumeMission(), [chat]);
73
+ const cancelMission = useCallback(() => chat.cancelMission(), [chat]);
71
74
  return {
72
75
  messages: state.messages,
73
76
  isLoading: state.isLoading,
@@ -75,6 +78,10 @@ function useAppilotsChat(options = {}) {
75
78
  error: state.error,
76
79
  pendingActions: state.pendingActions,
77
80
  escalation: state.escalation,
81
+ mission: state.mission ?? null,
82
+ pauseMission,
83
+ resumeMission,
84
+ cancelMission,
78
85
  requestHuman,
79
86
  sendMessage,
80
87
  cancelMessage,
@@ -101,6 +108,78 @@ function useAppilotsActions() {
101
108
  rejectAction
102
109
  };
103
110
  }
111
+ function useActionStatus(locale) {
112
+ const { actions } = useAppilotsRuntime();
113
+ const state = useSyncExternalStore(actions.subscribeState, actions.getState, actions.getState);
114
+ const current = state.actions.find((a) => a.status === "executing" && a.type !== "confirm");
115
+ return current ? describeAction(current, locale).label : "";
116
+ }
117
+ function useVoiceInput({ host, locale, onText }) {
118
+ const [state, setState] = useState("idle");
119
+ const run = useRef(0);
120
+ const onTextRef = useRef(onText);
121
+ onTextRef.current = onText;
122
+ const stateRef = useRef(state);
123
+ stateRef.current = state;
124
+ const cancel = useCallback(() => {
125
+ if (stateRef.current === "idle") return;
126
+ run.current++;
127
+ setState("idle");
128
+ void host?.cancel().catch((err) => appilotsDebugWarn("useVoiceInput: cancel failed", err));
129
+ }, [host]);
130
+ const toggle = useCallback(() => {
131
+ if (!host) return;
132
+ const id = ++run.current;
133
+ const current = stateRef.current;
134
+ if (current === "idle" || current === "error") {
135
+ setState("starting");
136
+ host.start({ locale }).then(
137
+ () => id === run.current && setState("listening"),
138
+ (err) => {
139
+ appilotsDebugWarn("useVoiceInput: start failed", err);
140
+ if (id === run.current) setState("error");
141
+ }
142
+ );
143
+ } else if (current === "starting" || current === "listening") {
144
+ setState("stopping");
145
+ host.stop().then(
146
+ (text) => {
147
+ if (id !== run.current) return;
148
+ onTextRef.current(text ?? "");
149
+ setState("idle");
150
+ },
151
+ (err) => {
152
+ appilotsDebugWarn("useVoiceInput: stop failed", err);
153
+ if (id === run.current) setState("error");
154
+ }
155
+ );
156
+ }
157
+ }, [host, locale]);
158
+ const active = state === "starting" || state === "listening";
159
+ useEffect(() => {
160
+ if (!host || !active) return;
161
+ const id = run.current;
162
+ const offPartial = host.onPartial?.((text) => id === run.current && onTextRef.current(text));
163
+ const offEnd = host.onEnd?.((error) => {
164
+ if (id !== run.current) return;
165
+ run.current++;
166
+ setState(error ? "error" : "idle");
167
+ });
168
+ return () => {
169
+ offPartial?.();
170
+ offEnd?.();
171
+ };
172
+ }, [host, active]);
173
+ useEffect(
174
+ () => () => {
175
+ const s = stateRef.current;
176
+ if (s === "starting" || s === "listening") void host?.cancel().catch(() => {
177
+ });
178
+ },
179
+ [host]
180
+ );
181
+ return { state, toggle, cancel };
182
+ }
104
183
 
105
184
  // src/react/confirmCard.ts
106
185
  var GENERIC_MESSAGE = "Confirm this action?";
@@ -116,13 +195,18 @@ function deriveConfirmCardCopy(payload, defaults = {
116
195
  };
117
196
  const actionDescription = str("actionDescription");
118
197
  const message = str("message") ?? str("title") ?? (actionDescription ? defaults.describe(actionDescription) : void 0) ?? defaults.message;
198
+ const confirmLabel = str("confirmLabel") ?? defaults.confirmLabel;
199
+ const cancelLabel = str("cancelLabel") ?? defaults.cancelLabel;
200
+ const name = (label) => actionDescription ? `${label}: ${actionDescription}` : label;
119
201
  return {
120
202
  message,
121
- confirmLabel: str("confirmLabel") ?? defaults.confirmLabel,
122
- cancelLabel: str("cancelLabel") ?? defaults.cancelLabel
203
+ confirmLabel,
204
+ cancelLabel,
205
+ confirmName: name(confirmLabel),
206
+ cancelName: name(cancelLabel)
123
207
  };
124
208
  }
125
- function inline(text) {
209
+ function inline(text, newTab) {
126
210
  return parseInlineMarkdown(text).map((segment, index) => {
127
211
  switch (segment.kind) {
128
212
  case "bold":
@@ -132,28 +216,38 @@ function inline(text) {
132
216
  case "code":
133
217
  return /* @__PURE__ */ jsx("code", { children: segment.value }, index);
134
218
  case "link":
135
- return /* @__PURE__ */ jsx("a", { href: segment.href, target: "_blank", rel: "noopener noreferrer", children: segment.value }, index);
219
+ return /* @__PURE__ */ jsxs("a", { href: segment.href, target: "_blank", rel: "noopener noreferrer", children: [
220
+ segment.value,
221
+ /* @__PURE__ */ jsxs("span", { className: "ap-sr-only", children: [
222
+ " ",
223
+ newTab
224
+ ] })
225
+ ] }, index);
136
226
  default:
137
227
  return segment.value;
138
228
  }
139
229
  });
140
230
  }
141
- function ChatMarkdown({ content, hiddenValue }) {
231
+ function ChatMarkdown({
232
+ content,
233
+ hiddenValue,
234
+ newTab
235
+ }) {
142
236
  return /* @__PURE__ */ jsx("div", { className: "ap-markdown", children: parseMarkdownBlocks(content.replaceAll("<redacted>", hiddenValue)).map((block, index) => {
143
237
  switch (block.kind) {
144
238
  case "heading": {
145
- const Heading = `h${block.level}`;
146
- return /* @__PURE__ */ jsx(Heading, { children: inline(block.text) }, index);
239
+ const Heading = `h${block.level + 2}`;
240
+ return /* @__PURE__ */ jsx(Heading, { children: inline(block.text, newTab) }, index);
147
241
  }
148
242
  case "codeblock":
149
243
  return /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: block.code }) }, index);
150
244
  case "bullet":
151
245
  case "ordered": {
152
246
  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);
247
+ return /* @__PURE__ */ jsx(List, { children: block.items.map((item, i) => /* @__PURE__ */ jsx("li", { children: inline(item, newTab) }, i)) }, index);
154
248
  }
155
249
  default:
156
- return /* @__PURE__ */ jsx("p", { children: inline(block.text) }, index);
250
+ return /* @__PURE__ */ jsx("p", { children: inline(block.text, newTab) }, index);
157
251
  }
158
252
  }) });
159
253
  }
@@ -182,17 +276,31 @@ var chatStrings = {
182
276
  stop: "Stop response",
183
277
  inputHint: "Enter to send \xB7 Shift + Enter for a new line",
184
278
  latest: "Latest messages",
279
+ messages: "Messages",
280
+ you: "You",
281
+ newTab: "(opens in a new tab)",
185
282
  thinking: "Thinking\u2026",
186
283
  statusAnalyzing: "Analyzing\u2026",
187
284
  statusWaitingApp: "Waiting for the app\u2026",
188
285
  statusAdjusting: "Adjusting\u2026",
286
+ stillWorking: (seconds) => `Still working\u2026 ${seconds} s`,
189
287
  errorPrefix: "Something went wrong",
190
288
  dismiss: "Dismiss error",
191
289
  approval: "Your approval is needed",
192
290
  confirm: "Confirm",
193
291
  cancel: "Cancel",
194
292
  confirmMessage: "Confirm this action?",
195
- confirmDescription: (label) => `Confirm "${label}"?`
293
+ confirmDescription: (label) => `Confirm "${label}"?`,
294
+ confirmExpired: "This confirmation expired. Ask the assistant again to continue.",
295
+ missionControls: "Task controls",
296
+ missionPause: "Pause",
297
+ missionResume: "Resume task",
298
+ missionCancel: "Cancel task",
299
+ missionPaused: "Paused",
300
+ voice: "Dictate by voice",
301
+ voiceListening: "Listening. Press the microphone again to stop.",
302
+ voiceStopped: "Stopped listening. Review the text before sending.",
303
+ voiceError: "Could not use the microphone."
196
304
  },
197
305
  "pt-BR": {
198
306
  openChat: "Abrir chat",
@@ -216,88 +324,118 @@ var chatStrings = {
216
324
  stop: "Parar resposta",
217
325
  inputHint: "Enter para enviar \xB7 Shift + Enter para nova linha",
218
326
  latest: "Mensagens recentes",
327
+ messages: "Mensagens",
328
+ you: "Voc\xEA",
329
+ newTab: "(abre em nova aba)",
219
330
  thinking: "Pensando\u2026",
220
331
  statusAnalyzing: "Analisando\u2026",
221
332
  statusWaitingApp: "Aguardando o aplicativo\u2026",
222
333
  statusAdjusting: "Ajustando\u2026",
334
+ stillWorking: (seconds) => `Ainda trabalhando\u2026 ${seconds} s`,
223
335
  errorPrefix: "Algo deu errado",
224
336
  dismiss: "Fechar erro",
225
337
  approval: "Precisamos da sua confirma\xE7\xE3o",
226
338
  confirm: "Confirmar",
227
339
  cancel: "Cancelar",
228
340
  confirmMessage: "Confirma esta a\xE7\xE3o?",
229
- confirmDescription: (label) => `Confirma "${label}"?`
341
+ confirmDescription: (label) => `Confirma "${label}"?`,
342
+ confirmExpired: "Esta confirma\xE7\xE3o expirou. Pe\xE7a de novo ao assistente para continuar.",
343
+ missionControls: "Controles da tarefa",
344
+ missionPause: "Pausar",
345
+ missionResume: "Retomar tarefa",
346
+ missionCancel: "Cancelar tarefa",
347
+ missionPaused: "Pausada",
348
+ voice: "Ditar por voz",
349
+ voiceListening: "Ouvindo. Pressione o microfone de novo para parar.",
350
+ voiceStopped: "Parou de ouvir. Revise o texto antes de enviar.",
351
+ voiceError: "N\xE3o foi poss\xEDvel usar o microfone."
230
352
  }
231
353
  };
232
354
 
233
355
  // src/react/chatStyles.ts
234
356
  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; }
357
+ .appilots-chat { --ap-accent: #2563eb; display: flex; flex-direction: column; height: 100%; min-height: 17.5rem; min-width: 0; color: #172033; background: #f6f8fb; border: 1px solid #e2e7ef; border-radius: 20px; overflow: hidden auto; font: 0.875rem/1.5 system-ui, -apple-system, sans-serif; box-shadow: 0 8px 32px #17203308; }
358
+ /* At large text sizes the header and footer outgrow the panel: they keep their size and the panel scrolls, instead of shrinking under their own text. */
359
+ .appilots-chat > * { flex-shrink: 0; }
236
360
  .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; }
361
+ .appilots-chat button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 44px; 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
362
  .appilots-chat button:hover:not(:disabled) { background: #edf2f9; }
239
363
  .appilots-chat button:disabled { opacity: .5; cursor: not-allowed; }
240
364
  .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; }
365
+ .appilots-chat .ap-header { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 18px; background: #fff; border-bottom: 1px solid #e8ecf2; }
242
366
  .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; }
367
+ .appilots-chat .ap-identity { flex: 1 1 5rem; min-width: 0; }
368
+ .appilots-chat .ap-title { display: block; margin: 0; font-size: 0.9375rem; font-weight: 700; line-height: 1.4; overflow-wrap: anywhere; }
369
+ .appilots-chat .ap-caption { font-size: 0.75rem; color: #5f6b7d; }
370
+ .appilots-chat .ap-icon { padding: 8px; min-width: 44px; min-height: 44px; border: 0; background: transparent; flex-shrink: 0; }
371
+ .appilots-chat .ap-human-button { border: 0; border-radius: 0; border-bottom: 1px solid #e8ecf2; padding: 10px 16px; color: #52647e; font-size: 0.75rem; }
372
+ .appilots-chat .ap-escalation { padding: 12px 18px; color: #16604b; background: #eaf7f1; font-size: 0.75rem; border-bottom: 1px solid #d5ede2; }
373
+ .appilots-chat .ap-log { flex: 1; min-height: 6rem; overflow-y: auto; overscroll-behavior: contain; padding: 22px 18px; display: flex; flex-direction: column; gap: 20px; scrollbar-width: thin; }
250
374
  .appilots-chat .ap-empty { margin: auto 0; padding: 24px 4px; text-align: center; }
251
375
  .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; }
376
+ .appilots-chat .ap-empty h3 { margin: 0 0 8px; font-size: 1.3125rem; font-weight: 650; letter-spacing: -.4px; }
377
+ .appilots-chat .ap-empty p { margin: 0 auto 24px; max-width: 16.25rem; color: #5f6b7d; }
254
378
  .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; }
379
+ .appilots-chat .ap-suggestions button { padding: 12px 14px; text-align: left; justify-content: space-between; font-size: 0.8125rem; }
256
380
  .appilots-chat .ap-message { max-width: 94%; align-self: flex-start; min-width: 0; }
257
381
  .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; }
382
+ .appilots-chat .ap-sender { display: block; margin: 0 0 5px 3px; color: #5f6b7d; font-size: 0.6875rem; font-weight: 600; }
259
383
  .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
384
  .appilots-chat .ap-message-user .ap-bubble { background: var(--ap-accent); color: #fff; border-color: transparent; border-radius: 17px 4px 17px 17px; }
261
385
  .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; }
386
+ .appilots-chat .ap-message time { display: block; margin: 5px 3px 0; color: #5f6b7d; font-size: 0.625rem; }
263
387
  .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; }
388
+ .appilots-chat .ap-system { align-self: center; text-align: center; color: #5f6b7d; font-size: 0.75rem; white-space: pre-wrap; }
265
389
  .appilots-chat .ap-system button { display: flex; margin: 8px auto 0; }
266
390
  .appilots-chat .ap-markdown { white-space: normal; }
267
391
  .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; }
392
+ .appilots-chat .ap-markdown h3, .appilots-chat .ap-markdown h4, .appilots-chat .ap-markdown h5 { font-size: 1rem; line-height: 1.4; margin: 12px 0 8px; }
269
393
  .appilots-chat .ap-markdown ul, .appilots-chat .ap-markdown ol { margin: 8px 0; padding-left: 22px; }
270
394
  .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; }
395
+ .appilots-chat .ap-markdown code { font: 0.75rem/1.6 ui-monospace, monospace; background: #edf1f7; border-radius: 4px; padding: 2px 4px; }
272
396
  .appilots-chat .ap-markdown pre { margin: 10px 0; padding: 12px; background: #edf1f7; border-radius: 8px; overflow-x: auto; white-space: pre; }
273
397
  .appilots-chat .ap-markdown pre code { padding: 0; }
274
398
  .appilots-chat .ap-markdown a { color: #1d4ed8; text-decoration: underline; }
275
399
  .appilots-chat .ap-markdown > :first-child { margin-top: 0; }
276
400
  .appilots-chat .ap-markdown > :last-child { margin-bottom: 0; }
277
401
  .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; }
402
+ .appilots-chat .ap-confirm-title { color: #8c641d; font-size: 0.6875rem; font-weight: 600; margin-bottom: 6px; }
403
+ .appilots-chat .ap-confirm-expired { margin: 10px 0 0; color: #9a3412; font-size: 0.8125rem; font-weight: 600; }
404
+ .appilots-chat .ap-confirm-actions button { min-height: 44px; min-width: 44px; }
279
405
  .appilots-chat .ap-confirm-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
280
406
  .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; }
407
+ .appilots-chat .ap-sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
408
+ .appilots-chat .ap-loading { display: flex; align-items: center; gap: 8px; color: #5f6b7d; font-size: 0.75rem; }
282
409
  .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; }
410
+ .appilots-chat .ap-mission { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 14px 8px; }
411
+ .appilots-chat .ap-mission button { font-size: 0.75rem; min-height: 44px; }
412
+ .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: 0.75rem; }
413
+ .appilots-chat .ap-latest { align-self: center; font-size: 0.75rem; margin: 0 0 8px; }
285
414
  .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; }
415
+ .appilots-chat .ap-composer { display: flex; align-items: flex-end; gap: 8px; padding: 8px; border: 1px solid #838ea0; border-radius: 16px; background: #fff; }
287
416
  .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; }
417
+ .appilots-chat textarea { display: block; flex: 1; min-width: 0; min-height: 2.5rem; max-height: 7.5rem; margin: 0; padding: 9px 4px; border: 0; border-radius: 4px; background: transparent; color: inherit; font: inherit; line-height: 1.375rem; resize: none; }
289
418
  .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; }
419
+ .appilots-chat .ap-send { min-width: 44px; min-height: 44px; padding: 0; border-radius: 12px; flex-shrink: 0; }
420
+ .appilots-chat .ap-hint { display: block; text-align: center; font-size: 0.625rem; color: #5f6b7d; padding-top: 8px; }
421
+ @media (prefers-reduced-motion: reduce) {
422
+ .appilots-chat *, .appilots-chat *::before, .appilots-chat *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
423
+ }
424
+ .appilots-chat .ap-log:focus-visible { outline: 2px solid var(--ap-accent); outline-offset: -2px; }
425
+ .appilots-chat .ap-mic[aria-pressed="true"] { color: #fff; background: #b42318; border-radius: 12px; }
426
+ .appilots-chat .ap-mic[aria-pressed="true"]:hover:not(:disabled) { background: #9a1c13; }
292
427
  `;
293
- function Icon({ name }) {
428
+ function Icon({
429
+ name
430
+ }) {
294
431
  const paths = {
295
432
  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
433
  human: "M3 14v-3a9 9 0 0 1 18 0v3M3 12h3v8H3zM18 12h3v8h-3zM18 20c0 2-3 2-6 2",
297
434
  new: "M12 5v14M5 12h14",
298
435
  send: "M12 19V5M5 12l7-7 7 7",
299
436
  stop: "M6 6h12v12H6z",
300
- close: "M6 6l12 12M18 6 6 18"
437
+ close: "M6 6l12 12M18 6 6 18",
438
+ mic: "M12 3a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V6a3 3 0 0 0-3-3ZM5 11a7 7 0 0 0 14 0M12 18v3"
301
439
  };
302
440
  return /* @__PURE__ */ jsx(
303
441
  "svg",
@@ -315,13 +453,24 @@ function Icon({ name }) {
315
453
  }
316
454
  );
317
455
  }
456
+ function useStillWorkingSeconds(active) {
457
+ const [seconds, setSeconds] = useState(0);
458
+ useEffect(() => {
459
+ setSeconds(0);
460
+ if (!active) return;
461
+ return startStillWorkingTimer(setSeconds);
462
+ }, [active]);
463
+ return seconds;
464
+ }
318
465
  function AppilotsChat({
319
466
  title,
320
467
  placeholder,
321
468
  locale = "en",
322
469
  suggestedPrompts = [],
323
470
  style,
324
- onClose
471
+ onClose,
472
+ speech,
473
+ hidden
325
474
  }) {
326
475
  const strings = chatStrings[locale];
327
476
  const chatTitle = title ?? strings.title;
@@ -331,6 +480,10 @@ function AppilotsChat({
331
480
  loadingStatusKey,
332
481
  error,
333
482
  escalation,
483
+ mission,
484
+ pauseMission,
485
+ resumeMission,
486
+ cancelMission,
334
487
  sendMessage,
335
488
  cancelMessage,
336
489
  clearMessages,
@@ -352,7 +505,45 @@ function AppilotsChat({
352
505
  const inputRef = useRef(null);
353
506
  const logRef = useRef(null);
354
507
  const followMessages = useRef(true);
508
+ const hintId = useId();
355
509
  const confirms = pendingActions.filter((action) => action.type === "confirm");
510
+ const actionStatus = useActionStatus(locale);
511
+ const working = isLoading && confirms.length === 0;
512
+ const stillWorkingSeconds = useStillWorkingSeconds(working);
513
+ const statusText = stillWorkingSeconds ? strings.stillWorking(stillWorkingSeconds) : strings[loadingStatusKey];
514
+ const activeMission = mission && !["completed", "cancelled"].includes(mission.status) ? mission : null;
515
+ const streaming = isLoading && !!messages.at(-1)?.id.startsWith("streaming_");
516
+ const focusInput = () => inputRef.current?.focus();
517
+ const [speechHost, setSpeechHost] = useState(null);
518
+ useEffect(() => {
519
+ setSpeechHost(
520
+ speech === "browser" ? createWebSpeechHost() : isSpeechHost(speech) ? speech : null
521
+ );
522
+ }, [speech]);
523
+ const draftBeforeVoice = useRef("");
524
+ const voice = useVoiceInput({
525
+ host: speechHost,
526
+ locale,
527
+ onText: (text) => setDraft([draftBeforeVoice.current.trimEnd(), text.trim()].filter(Boolean).join(" "))
528
+ });
529
+ const capturing = voice.state === "starting" || voice.state === "listening";
530
+ const { cancel: cancelCapture } = voice;
531
+ useEffect(() => {
532
+ if (hidden) cancelCapture();
533
+ }, [hidden, cancelCapture]);
534
+ const heardOnce = useRef(false);
535
+ if (voice.state === "listening") heardOnce.current = true;
536
+ const voiceStatus = voice.state === "listening" ? strings.voiceListening : voice.state === "error" ? strings.voiceError : voice.state === "idle" && heardOnce.current ? strings.voiceStopped : "";
537
+ const toggleVoice = () => {
538
+ if (!capturing) draftBeforeVoice.current = draft;
539
+ voice.toggle();
540
+ };
541
+ const cancelVoice = () => {
542
+ if (!capturing) return false;
543
+ voice.cancel();
544
+ setDraft(draftBeforeVoice.current);
545
+ return true;
546
+ };
356
547
  const scrollToLatest = () => {
357
548
  followMessages.current = true;
358
549
  if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
@@ -367,11 +558,12 @@ function AppilotsChat({
367
558
  const input = inputRef.current;
368
559
  if (!input) return;
369
560
  input.style.height = "auto";
370
- input.style.height = `${Math.min(input.scrollHeight, 120)}px`;
561
+ input.style.height = `${input.scrollHeight}px`;
371
562
  }, [draft]);
372
563
  const send = () => {
373
564
  const content = draft.trim();
374
- if (!content || isLoading || requestingHuman) return;
565
+ if (!content || isLoading || requestingHuman || activeMission) return;
566
+ if (voice.state !== "idle" && voice.state !== "error") voice.cancel();
375
567
  scrollToLatest();
376
568
  clearError();
377
569
  setDraft("");
@@ -387,6 +579,7 @@ function AppilotsChat({
387
579
  await requestHuman(reason);
388
580
  } finally {
389
581
  setRequestingHuman(false);
582
+ focusInput();
390
583
  }
391
584
  };
392
585
  return (
@@ -404,7 +597,7 @@ function AppilotsChat({
404
597
  /* @__PURE__ */ jsxs("header", { className: "ap-header", children: [
405
598
  /* @__PURE__ */ jsx("div", { className: "ap-avatar", children: /* @__PURE__ */ jsx(Icon, { name: escalation ? "human" : "chat" }) }),
406
599
  /* @__PURE__ */ jsxs("div", { className: "ap-identity", children: [
407
- /* @__PURE__ */ jsx("strong", { className: "ap-title", children: chatTitle }),
600
+ /* @__PURE__ */ jsx("h2", { className: "ap-title", children: chatTitle }),
408
601
  /* @__PURE__ */ jsx("span", { className: "ap-caption", children: escalation?.operatorName ?? (escalation ? strings.humanLabel : strings.subtitle) })
409
602
  ] }),
410
603
  /* @__PURE__ */ jsx(
@@ -417,6 +610,7 @@ function AppilotsChat({
417
610
  disabled: requestingHuman || !messages.length && !escalation && !error,
418
611
  onClick: () => {
419
612
  clearMessages();
613
+ voice.cancel();
420
614
  setDraft("");
421
615
  scrollToLatest();
422
616
  inputRef.current?.focus();
@@ -461,8 +655,10 @@ function AppilotsChat({
461
655
  className: "ap-log",
462
656
  ref: logRef,
463
657
  role: "log",
464
- "aria-label": chatTitle,
465
- "aria-live": "polite",
658
+ "aria-label": strings.messages,
659
+ "aria-live": streaming ? "off" : "polite",
660
+ "aria-busy": streaming,
661
+ tabIndex: 0,
466
662
  "data-testid": "appilots-chat-log",
467
663
  onScroll: () => {
468
664
  const log = logRef.current;
@@ -473,7 +669,7 @@ function AppilotsChat({
473
669
  children: [
474
670
  messages.length === 0 && /* @__PURE__ */ jsxs("div", { className: "ap-empty", children: [
475
671
  /* @__PURE__ */ jsx("div", { className: "ap-avatar", children: /* @__PURE__ */ jsx(Icon, { name: "chat" }) }),
476
- /* @__PURE__ */ jsx("h2", { children: strings.welcome }),
672
+ /* @__PURE__ */ jsx("h3", { children: strings.welcome }),
477
673
  /* @__PURE__ */ jsx("p", { children: strings.welcomeBody }),
478
674
  /* @__PURE__ */ jsx("div", { className: "ap-suggestions", children: suggestedPrompts.map((prompt) => /* @__PURE__ */ jsxs(
479
675
  "button",
@@ -512,8 +708,18 @@ function AppilotsChat({
512
708
  className: `ap-message ap-message-${message.role}`,
513
709
  "data-testid": `appilots-message-${message.role}`,
514
710
  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 }) }),
711
+ message.role === "user" ? /* @__PURE__ */ jsxs("span", { className: "ap-sr-only", children: [
712
+ strings.you,
713
+ ": "
714
+ ] }) : /* @__PURE__ */ jsx("span", { className: "ap-sender", children: message.role === "human_agent" ? escalation?.operatorName ?? strings.humanLabel : chatTitle }),
715
+ /* @__PURE__ */ jsx("div", { className: "ap-bubble", children: message.role === "user" ? message.content : /* @__PURE__ */ jsx(
716
+ ChatMarkdown,
717
+ {
718
+ content: message.content,
719
+ hiddenValue: strings.hiddenValue,
720
+ newTab: strings.newTab
721
+ }
722
+ ) }),
517
723
  /* @__PURE__ */ jsx("time", { dateTime: new Date(message.timestamp).toISOString(), children: new Date(message.timestamp).toLocaleTimeString(locale, {
518
724
  hour: "2-digit",
519
725
  minute: "2-digit"
@@ -523,50 +729,88 @@ function AppilotsChat({
523
729
  message.id
524
730
  )
525
731
  ),
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] })
732
+ confirms.map((action) => /* @__PURE__ */ jsx(
733
+ ConfirmCard,
734
+ {
735
+ action,
736
+ strings,
737
+ composerRef: inputRef,
738
+ onApprove: () => void approveAction(action.id),
739
+ onReject: () => void rejectAction(action.id)
740
+ },
741
+ action.id
742
+ )),
743
+ (isLoading || activeMission) && // While loading this is visual only: the log is a live region, and a
744
+ // second one nested in it double-speaks. The status below is what
745
+ // readers hear. An idle open task has nothing else speaking.
746
+ /* @__PURE__ */ jsx(
747
+ "div",
748
+ {
749
+ className: "ap-loading",
750
+ "aria-hidden": isLoading ? "true" : void 0,
751
+ role: isLoading ? void 0 : "status",
752
+ "data-testid": isLoading ? "appilots-loading" : "appilots-mission-progress",
753
+ children: activeMission ? `${activeMission.progressText}${activeMission.status === "paused" ? ` \xB7 ${strings.missionPaused}` : ""}` : statusText
754
+ }
755
+ )
756
+ ]
757
+ }
758
+ ),
759
+ /* @__PURE__ */ jsx("div", { className: "ap-sr-only", role: "status", "data-testid": "appilots-chat-status", children: working ? statusText : "" }),
760
+ /* @__PURE__ */ jsx("div", { className: "ap-sr-only", role: "status", "data-testid": "appilots-action-status", children: actionStatus }),
761
+ showLatest && /* @__PURE__ */ jsxs(
762
+ "button",
763
+ {
764
+ type: "button",
765
+ className: "ap-latest",
766
+ onClick: () => {
767
+ scrollToLatest();
768
+ focusInput();
769
+ },
770
+ children: [
771
+ strings.latest,
772
+ " \u2193"
773
+ ]
774
+ }
775
+ ),
776
+ activeMission && /* @__PURE__ */ jsxs(
777
+ "div",
778
+ {
779
+ className: "ap-mission",
780
+ role: "group",
781
+ "aria-label": strings.missionControls,
782
+ "data-testid": "appilots-mission-controls",
783
+ children: [
784
+ activeMission.status === "running" ? /* @__PURE__ */ jsx(
785
+ "button",
786
+ {
787
+ type: "button",
788
+ "data-testid": "appilots-mission-pause",
789
+ onClick: () => void pauseMission(),
790
+ children: strings.missionPause
791
+ }
792
+ ) : activeMission.status !== "awaiting_approval" ? /* @__PURE__ */ jsx(
793
+ "button",
794
+ {
795
+ type: "button",
796
+ "data-testid": "appilots-mission-resume",
797
+ disabled: isLoading,
798
+ onClick: () => void resumeMission(),
799
+ children: strings.missionResume
800
+ }
801
+ ) : null,
802
+ /* @__PURE__ */ jsx(
803
+ "button",
804
+ {
805
+ type: "button",
806
+ "data-testid": "appilots-mission-cancel",
807
+ onClick: () => void cancelMission(),
808
+ children: strings.missionCancel
809
+ }
810
+ )
563
811
  ]
564
812
  }
565
813
  ),
566
- showLatest && /* @__PURE__ */ jsxs("button", { type: "button", className: "ap-latest", onClick: scrollToLatest, children: [
567
- strings.latest,
568
- " \u2193"
569
- ] }),
570
814
  error && /* @__PURE__ */ jsxs("div", { className: "ap-error", role: "alert", children: [
571
815
  /* @__PURE__ */ jsx("span", { children: error }),
572
816
  /* @__PURE__ */ jsx(
@@ -575,7 +819,10 @@ function AppilotsChat({
575
819
  type: "button",
576
820
  className: "ap-icon",
577
821
  "aria-label": strings.dismiss,
578
- onClick: clearError,
822
+ onClick: () => {
823
+ clearError();
824
+ focusInput();
825
+ },
579
826
  children: /* @__PURE__ */ jsx(Icon, { name: "close" })
580
827
  }
581
828
  )
@@ -598,9 +845,15 @@ function AppilotsChat({
598
845
  value: draft,
599
846
  placeholder: placeholder ?? strings.placeholder,
600
847
  "aria-label": placeholder ?? strings.placeholder,
848
+ "aria-describedby": hintId,
601
849
  "data-testid": "appilots-chat-input",
850
+ readOnly: capturing || voice.state === "stopping",
602
851
  onChange: (event) => setDraft(event.target.value),
603
852
  onKeyDown: (event) => {
853
+ if (event.key === "Escape" && cancelVoice()) {
854
+ event.preventDefault();
855
+ return;
856
+ }
604
857
  if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing && event.keyCode !== 229) {
605
858
  event.preventDefault();
606
859
  send();
@@ -608,6 +861,22 @@ function AppilotsChat({
608
861
  }
609
862
  }
610
863
  ),
864
+ speechHost && /* @__PURE__ */ jsx(
865
+ "button",
866
+ {
867
+ type: "button",
868
+ className: "ap-send ap-mic",
869
+ "data-testid": "appilots-mic-button",
870
+ "aria-label": strings.voice,
871
+ title: strings.voice,
872
+ "aria-pressed": capturing,
873
+ onClick: toggleVoice,
874
+ onKeyDown: (event) => {
875
+ if (event.key === "Escape" && cancelVoice()) event.preventDefault();
876
+ },
877
+ children: /* @__PURE__ */ jsx(Icon, { name: "mic" })
878
+ }
879
+ ),
611
880
  isLoading ? /* @__PURE__ */ jsx(
612
881
  "button",
613
882
  {
@@ -615,7 +884,11 @@ function AppilotsChat({
615
884
  className: "ap-send ap-primary",
616
885
  "aria-label": strings.stop,
617
886
  title: strings.stop,
618
- onClick: cancelMessage,
887
+ "data-testid": "appilots-chat-stop",
888
+ onClick: () => {
889
+ cancelMessage();
890
+ focusInput();
891
+ },
619
892
  children: /* @__PURE__ */ jsx(Icon, { name: "stop" })
620
893
  }
621
894
  ) : /* @__PURE__ */ jsx(
@@ -626,31 +899,134 @@ function AppilotsChat({
626
899
  "data-testid": "appilots-chat-send",
627
900
  "aria-label": strings.send,
628
901
  title: strings.send,
629
- disabled: !draft.trim() || requestingHuman,
902
+ disabled: !draft.trim() || requestingHuman || !!activeMission,
630
903
  children: /* @__PURE__ */ jsx(Icon, { name: "send" })
631
904
  }
632
905
  )
633
906
  ]
634
907
  }
635
908
  ),
636
- /* @__PURE__ */ jsx("span", { className: "ap-hint", children: strings.inputHint })
909
+ /* @__PURE__ */ jsx("span", { className: "ap-hint", id: hintId, children: strings.inputHint }),
910
+ speechHost && /* @__PURE__ */ jsx(
911
+ "span",
912
+ {
913
+ className: voiceStatus ? "ap-hint" : "ap-sr-only",
914
+ role: "status",
915
+ "data-testid": "appilots-voice-status",
916
+ children: voiceStatus
917
+ }
918
+ )
637
919
  ] })
638
920
  ]
639
921
  }
640
922
  )
641
923
  );
642
924
  }
925
+ function ConfirmCard({
926
+ action,
927
+ strings,
928
+ composerRef,
929
+ onApprove,
930
+ onReject
931
+ }) {
932
+ const id = useId();
933
+ const cardRef = useRef(null);
934
+ const cancelRef = useRef(null);
935
+ const expiresAt = confirmExpiresAt(action);
936
+ const [expired, setExpired] = useState(() => expiresAt !== void 0 && expiresAt <= Date.now());
937
+ const { message, confirmLabel, cancelLabel, confirmName, cancelName } = deriveConfirmCardCopy(
938
+ action.payload ?? {},
939
+ {
940
+ message: strings.confirmMessage,
941
+ confirmLabel: strings.confirm,
942
+ cancelLabel: strings.cancel,
943
+ describe: strings.confirmDescription
944
+ }
945
+ );
946
+ useLayoutEffect(() => {
947
+ cancelRef.current?.focus();
948
+ const card = cardRef.current;
949
+ return () => {
950
+ if (card?.contains(document.activeElement)) composerRef.current?.focus();
951
+ };
952
+ }, [composerRef]);
953
+ useEffect(() => {
954
+ if (expiresAt === void 0 || expired) return;
955
+ const timer = setTimeout(() => setExpired(true), expiresAt - Date.now());
956
+ return () => clearTimeout(timer);
957
+ }, [expiresAt, expired]);
958
+ return /* @__PURE__ */ jsxs(
959
+ "div",
960
+ {
961
+ ref: cardRef,
962
+ className: "ap-confirm",
963
+ "data-testid": "appilots-confirm",
964
+ role: "alertdialog",
965
+ "aria-labelledby": `${id}-title`,
966
+ "aria-describedby": `${id}-message`,
967
+ onKeyDown: (event) => {
968
+ if (event.key !== "Escape" || event.nativeEvent.isComposing) return;
969
+ event.preventDefault();
970
+ event.stopPropagation();
971
+ onReject();
972
+ },
973
+ children: [
974
+ /* @__PURE__ */ jsx("div", { className: "ap-confirm-title", id: `${id}-title`, children: strings.approval }),
975
+ /* @__PURE__ */ jsx("div", { id: `${id}-message`, children: /* @__PURE__ */ jsx(ChatMarkdown, { content: message, hiddenValue: strings.hiddenValue, newTab: strings.newTab }) }),
976
+ expired && /* @__PURE__ */ jsx("p", { className: "ap-confirm-expired", role: "alert", "data-testid": "appilots-confirm-expired", children: strings.confirmExpired }),
977
+ /* @__PURE__ */ jsxs("div", { className: "ap-confirm-actions", children: [
978
+ /* @__PURE__ */ jsx(
979
+ "button",
980
+ {
981
+ type: "button",
982
+ className: "ap-primary",
983
+ "data-testid": "appilots-confirm-accept",
984
+ "aria-label": confirmName,
985
+ disabled: expired,
986
+ onClick: onApprove,
987
+ children: confirmLabel
988
+ }
989
+ ),
990
+ /* @__PURE__ */ jsx(
991
+ "button",
992
+ {
993
+ ref: cancelRef,
994
+ type: "button",
995
+ "data-testid": "appilots-confirm-reject",
996
+ "aria-label": cancelName,
997
+ onClick: onReject,
998
+ children: cancelLabel
999
+ }
1000
+ )
1001
+ ] })
1002
+ ]
1003
+ }
1004
+ );
1005
+ }
643
1006
  var styles = `
644
1007
  .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
1008
  .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)))); }
1009
+ .appilots-chat-fab .ap-fab-panel { width: min(24.375rem, calc(100vw - max(16px, env(safe-area-inset-left)) - max(16px, env(safe-area-inset-right)))); height: min(40rem, calc(100dvh - 68px - max(16px, env(safe-area-inset-top)) - max(16px, env(safe-area-inset-bottom)))); }
647
1010
  .appilots-chat-fab .ap-fab-panel[hidden] { display: none; }
648
1011
  .appilots-chat-fab .appilots-chat { min-height: 0; box-shadow: 0 16px 64px #17203326; }
649
1012
  .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
1013
  .appilots-chat-fab .ap-fab-button svg { width: 24px; height: 24px; }
651
1014
  .appilots-chat-fab .ap-fab-button:hover { filter: brightness(.94); }
1015
+ .appilots-chat-fab .ap-fab-stop { display: flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 16px; border: 1px solid #d5dbe5; border-radius: 22px; color: #172033; background: #fff; font: 600 14px/1.2 system-ui, -apple-system, sans-serif; box-shadow: 0 4px 16px #17203322; cursor: pointer; }
1016
+ .appilots-chat-fab .ap-fab-stop:focus-visible { outline: 3px solid var(--ap-fab-accent); outline-offset: 2px; }
652
1017
  .appilots-chat-fab .ap-fab-button:focus-visible { outline: 3px solid var(--ap-fab-accent); outline-offset: 4px; }
1018
+ .appilots-chat-fab .ap-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
1019
+ @media (prefers-reduced-motion: reduce) {
1020
+ .appilots-chat-fab, .appilots-chat-fab * { animation: none !important; transition: none !important; }
1021
+ }
653
1022
  `;
1023
+ function ClosedPanelActionStatus({
1024
+ open,
1025
+ locale
1026
+ }) {
1027
+ const actionStatus = useActionStatus(locale);
1028
+ return /* @__PURE__ */ jsx("div", { className: "ap-sr-only", role: "status", "data-testid": "appilots-fab-action-status", children: open ? "" : actionStatus });
1029
+ }
654
1030
  function AppilotsChatFab({
655
1031
  position = "bottom-right",
656
1032
  defaultOpen = false,
@@ -701,6 +1077,8 @@ function AppilotsChatFab({
701
1077
  className: "ap-fab-panel",
702
1078
  ref: panelRef,
703
1079
  hidden: !open,
1080
+ role: "dialog",
1081
+ "aria-label": chatProps.title ?? strings.title,
704
1082
  "data-testid": "appilots-chat-panel",
705
1083
  children: /* @__PURE__ */ jsx(
706
1084
  AppilotsChat,
@@ -708,11 +1086,14 @@ function AppilotsChatFab({
708
1086
  ...chatProps,
709
1087
  locale,
710
1088
  onClose: close,
1089
+ hidden: !open,
711
1090
  style: { "--ap-accent": accentColor, ...style }
712
1091
  }
713
1092
  )
714
1093
  }
715
1094
  ),
1095
+ /* @__PURE__ */ jsx(ClosedPanelActionStatus, { open, locale }),
1096
+ !open && /* @__PURE__ */ jsx(ClosedPanelTurn, { strings }),
716
1097
  /* @__PURE__ */ jsx(
717
1098
  "button",
718
1099
  {
@@ -721,6 +1102,7 @@ function AppilotsChatFab({
721
1102
  ref: triggerRef,
722
1103
  "aria-label": open ? strings.closeChat : strings.openChat,
723
1104
  title: open ? strings.closeChat : strings.openChat,
1105
+ "aria-haspopup": "dialog",
724
1106
  "aria-expanded": open,
725
1107
  "aria-controls": panelId,
726
1108
  "data-testid": "appilots-chat-fab",
@@ -734,5 +1116,30 @@ function AppilotsChatFab({
734
1116
  host
735
1117
  );
736
1118
  }
1119
+ function ClosedPanelTurn({ strings }) {
1120
+ const { chat } = useAppilotsRuntime();
1121
+ const { isLoading, loadingStatusKey } = useSyncExternalStore(
1122
+ chat.subscribeState,
1123
+ chat.getState,
1124
+ chat.getState
1125
+ );
1126
+ const stillWorkingSeconds = useStillWorkingSeconds(isLoading);
1127
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1128
+ isLoading && /* @__PURE__ */ jsxs(
1129
+ "button",
1130
+ {
1131
+ type: "button",
1132
+ className: "ap-fab-stop",
1133
+ "data-testid": "appilots-fab-stop",
1134
+ onClick: () => chat.cancelMessage(),
1135
+ children: [
1136
+ /* @__PURE__ */ jsx(Icon, { name: "stop" }),
1137
+ strings.stop
1138
+ ]
1139
+ }
1140
+ ),
1141
+ /* @__PURE__ */ jsx("div", { className: "ap-sr-only", role: "status", "data-testid": "appilots-fab-status", children: isLoading ? stillWorkingSeconds ? strings.stillWorking(stillWorkingSeconds) : strings[loadingStatusKey] : "" })
1142
+ ] });
1143
+ }
737
1144
 
738
1145
  export { AppilotsChat, AppilotsChatFab, AppilotsWebProvider, useAppilotsActions, useAppilotsChat, useAppilotsRuntime };