@askly/widget 2.9.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@ Askly is an **embeddable AI support chat widget** for React and plain HTML. Drop
13
13
 
14
14
  - 🤖 **AI-powered answers** — resolves customer questions automatically from your knowledge base.
15
15
  - 📚 **RAG (docs-grounded)** — replies grounded in your own documentation, not hallucinated.
16
- - 🎙️ **Voice chat** — customers can talk to the assistant, not just type.
16
+ - 🎙️ **Voice input** — customers can speak their message; it is transcribed and sent as text.
17
17
  - 🙋 **Human handoff** — one-click escalation to a live agent when needed.
18
18
  - ⚛️ **React + CDN** — use as an npm module, or a single `<script>` tag on any site (no React required).
19
19
  - 🎨 **Fully themeable** — colors, logo, position, and copy configured in the portal.
@@ -163,8 +163,8 @@ Askly.init({ appId: "YOUR_APP_ID", greeting: { message: "Need a hand?", delayMs:
163
163
  <script src="…/widget.js" data-app-id="YOUR_APP_ID" data-greeting="false" async></script>
164
164
  ```
165
165
 
166
- The widget dispatches `askly:greeting:shown`, `askly:greeting:clicked` and
167
- `askly:greeting:dismissed` on `window` for analytics.
166
+ For analytics, listen for `greeting:shown`, `greeting:clicked` and `greeting:dismissed` with
167
+ `Askly.on()` (see the JavaScript API below).
168
168
 
169
169
  ### Live replies
170
170
 
@@ -179,6 +179,48 @@ or a self-hosted server older than this feature) the widget falls back to checki
179
179
  seconds while the chat is open, as before. If your site sets a Content Security Policy, the Askly
180
180
  API origin must be in `connect-src`.
181
181
 
182
+ ### Voice input
183
+
184
+ Visitors can press the microphone button, speak, and have their words turned into a message. The
185
+ recording stops when they press done, after about two seconds of silence, or at 60 seconds;
186
+ Escape cancels it. It works in current Chrome, Edge, Safari and Firefox.
187
+
188
+ The clip is sent to the Askly server, transcribed there in the language spoken, and not stored.
189
+ The browser's own speech recognition is not used, so the audio goes to Askly and nowhere else.
190
+
191
+ | Mode | What happens to the transcript |
192
+ | :--- | :--- |
193
+ | `"review"` | It is placed in the message box for the visitor to check, edit and send. |
194
+ | `"auto-send"` | It is sent straight away. |
195
+ | off | No microphone button. |
196
+
197
+ Set the mode in the portal under **Widget**. An embed can narrow it, never widen it:
198
+
199
+ ```javascript
200
+ Askly.init({ appId: "…", voiceInput: "auto-send" }); // or "review", or false to turn it off here
201
+ ```
202
+
203
+ ```html
204
+ <script src="…/widget.js" data-app-id="…" data-voice-input="review" async></script>
205
+ ```
206
+
207
+ Each organization has a monthly allowance of transcription minutes, shown in the portal. When it
208
+ is used up, or a visitor records too many clips in a short time, the visitor is asked to type
209
+ instead.
210
+
211
+ Requirements on the host page:
212
+
213
+ - It must be served over **https** (browsers refuse the microphone otherwise).
214
+ - If you send a `Permissions-Policy` header, it must allow `microphone` for your own origin, and
215
+ an iframe that contains the widget needs `allow="microphone"`.
216
+ - With a Content Security Policy, the Askly API origin must be in `connect-src`.
217
+
218
+ `enableVoiceChat` is the older on/off switch: `false` still turns voice input off for the embed,
219
+ `true` leaves the choice to the portal. Voice input needs a server that offers transcription; with
220
+ an older self-hosted server the microphone button is not shown. The
221
+ `readAloud` option, which spoke replies with the browser's speech synthesis, was removed in
222
+ 2.11.0 and is ignored.
223
+
182
224
  ### Asking visitors for their email
183
225
 
184
226
  Anonymous visitors can't be replied to once they close the tab — the answer just waits in a widget
@@ -227,7 +269,68 @@ Notes:
227
269
  - **Optimized:** on non-matching pages the path check runs *before* anything mounts, so there's no DOM node, no React root, and no config network request — the SDK does essentially nothing.
228
270
  - The decision is evaluated once at init; it's a UX targeting tool, not a security boundary (the script can still be loaded on any page).
229
271
 
230
- ## Lifecycle Callbacks
272
+ ## JavaScript API
273
+
274
+ Everything below is on the global `Askly` (script tag) or the default export (npm). Calls made
275
+ straight after `Askly.init()` are held and carried out once the widget has mounted.
276
+
277
+ ### Controlling the widget
278
+
279
+ | Method | What it does |
280
+ | :--- | :--- |
281
+ | `Askly.open()` / `Askly.show()` | Open the chat panel. |
282
+ | `Askly.close()` / `Askly.hide()` | Close it. |
283
+ | `Askly.toggle()` | Open if closed, close if open. |
284
+ | `Askly.showSpace(space)` | Open on `"home"`, `"messages"` or `"help"`. |
285
+ | `Askly.showNewMessage(text?)` | Open the chat with the message box focused, optionally pre-filled. Nothing is sent. |
286
+ | `Askly.sendMessage(text)` | Open the chat and send `text` as the visitor. It passes the same checks as a typed message; if one is pending (email required, security check) it waits in the message box. |
287
+ | `Askly.showArticle(id)` | Open a help article by id. |
288
+ | `Askly.startVoiceInput()` / `Askly.stopVoiceInput()` | Open the chat and start recording a voice message; finish the recording and transcribe it. The browser may ask the visitor for the microphone. |
289
+ | `Askly.update(config)` | Change display settings without re-initialising, e.g. `{ themeColor, theme, organizationName, welcomeMessage, buttonShape }`. `voiceInput` can be narrowed here too. |
290
+ | `Askly.isOpen()` | `true` while the panel is open. |
291
+ | `Askly.getUnreadCount()` | Agent replies the visitor has not seen (the launcher badge). |
292
+ | `Askly.getVisitorId()` | This browser's anonymous visitor id (`""` before mount). |
293
+ | `Askly.ready()` | A promise that resolves once the widget has mounted and applied its configuration. |
294
+
295
+ ```javascript
296
+ document.querySelector("#contact-sales").addEventListener("click", () => {
297
+ Askly.showNewMessage("Hi, I'd like to talk about the Business plan.");
298
+ });
299
+ ```
300
+
301
+ ### Events
302
+
303
+ `Askly.on(name, handler)` returns a function that removes the listener; `Askly.off(name, handler)`
304
+ and `Askly.once(name, handler)` are also available. A handler that throws is logged and does not
305
+ affect the widget.
306
+
307
+ | Event | Payload | When |
308
+ | :--- | :--- | :--- |
309
+ | `ready` | `{ visitorId }` | The widget has mounted and applied its configuration. |
310
+ | `open` / `close` | none | The panel was opened or closed. |
311
+ | `message:sent` | `{ text, conversationId }` | The visitor sent a message. |
312
+ | `message:received` | `{ text, conversationId, from, messageId }` | A reply arrived; `from` is `"ai"` or `"agent"`. |
313
+ | `unread` | `{ count }` | The number of unseen agent replies changed. |
314
+ | `escalated` | `{ conversationId }` | The conversation was handed to a human. |
315
+ | `lead:captured` | none | The visitor left an email address. |
316
+ | `identify:success` / `identify:error` | `{ userId, status? }` | The server accepted or refused `identify()`. |
317
+ | `greeting:shown` / `greeting:clicked` / `greeting:dismissed` | none | Greeting bubble activity. |
318
+ | `voice:start` | none | Voice input started recording. |
319
+ | `voice:stop` | `{ durationMs }` | Recording ended and the clip is being transcribed. |
320
+ | `voice:transcript` | `{ text, language? }` | The transcript arrived, before it is placed in the message box or sent. |
321
+ | `voice:error` | `{ code }` | Voice input failed: `insecure`, `unsupported`, `denied`, `no-microphone`, `no-speech`, `no-audio`, `limit`, `disabled`, `network` or `failed`. |
322
+ | `error` | `{ message, status? }` | A message could not be sent. |
323
+
324
+ ```javascript
325
+ Askly.on("message:received", ({ from }) => analytics.track("support_reply", { from }));
326
+ Askly.on("unread", ({ count }) => { document.title = count ? `(${count}) Acme` : "Acme"; });
327
+ ```
328
+
329
+ TypeScript users can import the payload types: `import type { AsklyEventMap } from "@askly/widget"`.
330
+
331
+ ### Callbacks at init (older style)
332
+
333
+ These still work and are called alongside the events above.
231
334
 
232
335
  Callbacks are functions, so they can only be attached in code (not from the portal):
233
336
 
@@ -45,6 +45,21 @@ export interface AsklyEventMap {
45
45
  "greeting:shown": undefined;
46
46
  "greeting:clicked": undefined;
47
47
  "greeting:dismissed": undefined;
48
+ /** Voice input started recording. */
49
+ "voice:start": undefined;
50
+ /** Recording ended; the clip is about to be transcribed. */
51
+ "voice:stop": {
52
+ durationMs: number;
53
+ };
54
+ /** The transcript, before it is placed in the message box or sent. */
55
+ "voice:transcript": {
56
+ text: string;
57
+ language?: string;
58
+ };
59
+ /** Voice input failed: insecure, unsupported, denied, no-microphone, no-speech, no-audio, limit, disabled, network, failed. */
60
+ "voice:error": {
61
+ code: string;
62
+ };
48
63
  /** A request failed in a way the visitor was told about. */
49
64
  error: {
50
65
  message: string;
@@ -76,6 +91,10 @@ export type AsklyCommand = {
76
91
  } | {
77
92
  type: "showArticle";
78
93
  articleId: string;
94
+ } | {
95
+ type: "startVoiceInput";
96
+ } | {
97
+ type: "stopVoiceInput";
79
98
  } | {
80
99
  type: "update";
81
100
  config: Record<string, any>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Microphone capture for voice input.
3
+ *
4
+ * Records one short clip with MediaRecorder; the widget posts it to the Askly server, which
5
+ * returns the transcript. Nothing here touches the browser's own speech recognition, so the
6
+ * audio goes to Askly and nowhere else.
7
+ */
8
+ export type VoiceInputMode = "off" | "review" | "auto-send";
9
+ /** Why voice input could not start or finish. */
10
+ export type VoiceErrorCode = "insecure" | "unsupported" | "denied" | "no-microphone" | "no-speech" | "no-audio" | "limit" | "disabled" | "network" | "failed";
11
+ export interface VoiceClip {
12
+ blob: Blob;
13
+ durationMs: number;
14
+ /**
15
+ * "no-speech": recording gave up because nobody spoke. "no-audio": the microphone delivered
16
+ * silence throughout. Otherwise "ok", which includes every clip the visitor ended themselves:
17
+ * whether it contains words is then for the transcription to say, not for a level meter.
18
+ */
19
+ outcome: "ok" | "no-speech" | "no-audio";
20
+ }
21
+ export interface Recording {
22
+ /** Finish and hand the clip to `onDone`. */
23
+ stop(): void;
24
+ /** Throw the clip away. `onDone` is not called. */
25
+ cancel(): void;
26
+ }
27
+ export interface RecordingOptions {
28
+ maxMs: number;
29
+ /** Stop this long after the speaker goes quiet. */
30
+ silenceMs: number;
31
+ /** Stop when nothing has been said for this long from the start. */
32
+ noSpeechMs: number;
33
+ /** About ten times a second while recording. `level` is 0 to 1. */
34
+ onMeter: (level: number, elapsedMs: number) => void;
35
+ onDone: (clip: VoiceClip) => void;
36
+ }
37
+ export declare function normalizeVoiceInputMode(value: any): VoiceInputMode | undefined;
38
+ /** Why recording cannot work on this page, or null when it can. */
39
+ export declare function voiceInputBlocker(): VoiceErrorCode | null;
40
+ /** Ask for the microphone and start recording. Rejects with `{ code: VoiceErrorCode }`. */
41
+ export declare function startRecording(options: RecordingOptions): Promise<Recording>;
42
+ /** What to tell the visitor. */
43
+ export declare function voiceErrorMessage(code: VoiceErrorCode): string;
@@ -7,7 +7,10 @@ export interface ChatWidgetProps {
7
7
  theme?: "light" | "dark" | "auto";
8
8
  position?: "left" | "right";
9
9
  orgServerRoute: string;
10
+ voiceInput?: false | "review" | "auto-send";
11
+ /** @deprecated use `voiceInput`. `false` turns voice input off for this embed. */
10
12
  enableVoiceChat?: boolean;
13
+ /** @deprecated removed in 2.11.0; ignored. */
11
14
  readAloud?: boolean;
12
15
  welcomeMessage?: string;
13
16
  placeholderText?: string;
package/dist/icons.d.ts CHANGED
@@ -8,9 +8,6 @@ export declare const X: ({ size, color, strokeWidth, ...rest }: IconProps) => Re
8
8
  export declare const Minus: ({ size, color, strokeWidth, ...rest }: IconProps) => React.ReactSVGElement;
9
9
  export declare const ArrowUp: ({ size, color, strokeWidth, ...rest }: IconProps) => React.ReactSVGElement;
10
10
  export declare const Mic: ({ size, color, strokeWidth, ...rest }: IconProps) => React.ReactSVGElement;
11
- export declare const MicOff: ({ size, color, strokeWidth, ...rest }: IconProps) => React.ReactSVGElement;
12
- export declare const Volume2: ({ size, color, strokeWidth, ...rest }: IconProps) => React.ReactSVGElement;
13
- export declare const VolumeX: ({ size, color, strokeWidth, ...rest }: IconProps) => React.ReactSVGElement;
14
11
  export declare const Home: ({ size, color, strokeWidth, ...rest }: IconProps) => React.ReactSVGElement;
15
12
  export declare const Compass: ({ size, color, strokeWidth, ...rest }: IconProps) => React.ReactSVGElement;
16
13
  export declare const MessageSquare: ({ size, color, strokeWidth, ...rest }: IconProps) => React.ReactSVGElement;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type AsklyEventMap, type AsklyEventName, type AsklyEventHandler, type AsklySpace } from "./core/events";
1
2
  import { type AsklyIdentity } from "./identity";
2
3
  export interface AsklyConfig {
3
4
  appId?: string;
@@ -8,7 +9,10 @@ export interface AsklyConfig {
8
9
  position?: "left" | "right";
9
10
  themeColor?: string;
10
11
  theme?: "light" | "dark" | "auto";
12
+ voiceInput?: false | "review" | "auto-send";
13
+ /** @deprecated use `voiceInput`. */
11
14
  enableVoiceChat?: boolean;
15
+ /** @deprecated removed in 2.11.0; ignored. */
12
16
  readAloud?: boolean;
13
17
  welcomeMessage?: string;
14
18
  placeholderText?: string;
@@ -75,5 +79,46 @@ declare const Askly: {
75
79
  close: () => void;
76
80
  /** Toggle the chat panel. */
77
81
  toggle: () => void;
82
+ /** Aliases of open() / close(). */
83
+ show: () => void;
84
+ hide: () => void;
85
+ /** Whether the chat panel is open right now. */
86
+ isOpen: () => boolean;
87
+ /** Agent replies the visitor has not seen yet (the number on the launcher badge). */
88
+ getUnreadCount: () => number;
89
+ /** This browser's anonymous visitor id, or "" before the widget has mounted. */
90
+ getVisitorId: () => string;
91
+ /**
92
+ * Listen for a widget event. Returns a function that removes the listener.
93
+ *
94
+ * Askly.on("message:received", ({ text, from }) => analytics.track("support_reply", { from }));
95
+ * Askly.on("unread", ({ count }) => setBadge(count));
96
+ */
97
+ on: <K extends keyof AsklyEventMap>(name: K, handler: AsklyEventHandler<K>) => (() => void);
98
+ off: <K_1 extends keyof AsklyEventMap>(name: K_1, handler: AsklyEventHandler<K_1>) => void;
99
+ once: <K_2 extends keyof AsklyEventMap>(name: K_2, handler: AsklyEventHandler<K_2>) => (() => void);
100
+ /** Resolves when the widget has mounted and applied its configuration. */
101
+ ready: () => Promise<void>;
102
+ /** Open the widget on a section: "home", "messages" or "help". */
103
+ showSpace: (space: AsklySpace) => void;
104
+ /** Open the chat with the composer ready, optionally pre-filled. Nothing is sent. */
105
+ showNewMessage: (text?: string) => void;
106
+ /**
107
+ * Open the chat and send a message as the visitor. It goes through the same checks as a
108
+ * typed message; if one is pending (email required, security check) it waits in the composer.
109
+ */
110
+ sendMessage: (text: string) => void;
111
+ /** Open a help article by id, as listed in the Help tab. */
112
+ showArticle: (articleId: string) => void;
113
+ /** Open the chat and start recording a voice message. The browser may ask for the microphone. */
114
+ startVoiceInput: () => void;
115
+ /** Finish the recording in progress and transcribe it. */
116
+ stopVoiceInput: () => void;
117
+ /**
118
+ * Change display settings on the mounted widget without re-initialising: the fields the
119
+ * portal controls, e.g. `{ themeColor, theme, organizationName, welcomeMessage, buttonShape }`.
120
+ */
121
+ update: (config: Record<string, any>) => void;
78
122
  };
79
123
  export default Askly;
124
+ export type { AsklyIdentity, AsklyEventMap, AsklyEventName, AsklyEventHandler, AsklySpace };