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