@askly/widget 2.8.0 → 2.10.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
@@ -163,8 +163,21 @@ 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
+
169
+ ### Live replies
170
+
171
+ When one of your agents replies, the widget receives it over a live connection the moment it is
172
+ sent, rather than checking for it every few seconds. This works with the chat panel closed too:
173
+ the launcher shows a badge with the number of new replies, and opening it goes straight to that
174
+ conversation.
175
+
176
+ There is nothing to configure. The connection is opened once a visitor has started a conversation
177
+ and only while the tab is visible. If it cannot be established (a network that blocks streaming,
178
+ or a self-hosted server older than this feature) the widget falls back to checking every few
179
+ seconds while the chat is open, as before. If your site sets a Content Security Policy, the Askly
180
+ API origin must be in `connect-src`.
168
181
 
169
182
  ### Asking visitors for their email
170
183
 
@@ -214,7 +227,63 @@ Notes:
214
227
  - **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.
215
228
  - 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).
216
229
 
217
- ## Lifecycle Callbacks
230
+ ## JavaScript API
231
+
232
+ Everything below is on the global `Askly` (script tag) or the default export (npm). Calls made
233
+ straight after `Askly.init()` are held and carried out once the widget has mounted.
234
+
235
+ ### Controlling the widget
236
+
237
+ | Method | What it does |
238
+ | :--- | :--- |
239
+ | `Askly.open()` / `Askly.show()` | Open the chat panel. |
240
+ | `Askly.close()` / `Askly.hide()` | Close it. |
241
+ | `Askly.toggle()` | Open if closed, close if open. |
242
+ | `Askly.showSpace(space)` | Open on `"home"`, `"messages"` or `"help"`. |
243
+ | `Askly.showNewMessage(text?)` | Open the chat with the message box focused, optionally pre-filled. Nothing is sent. |
244
+ | `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. |
245
+ | `Askly.showArticle(id)` | Open a help article by id. |
246
+ | `Askly.update(config)` | Change display settings without re-initialising, e.g. `{ themeColor, theme, organizationName, welcomeMessage, buttonShape }`. |
247
+ | `Askly.isOpen()` | `true` while the panel is open. |
248
+ | `Askly.getUnreadCount()` | Agent replies the visitor has not seen (the launcher badge). |
249
+ | `Askly.getVisitorId()` | This browser's anonymous visitor id (`""` before mount). |
250
+ | `Askly.ready()` | A promise that resolves once the widget has mounted and applied its configuration. |
251
+
252
+ ```javascript
253
+ document.querySelector("#contact-sales").addEventListener("click", () => {
254
+ Askly.showNewMessage("Hi, I'd like to talk about the Business plan.");
255
+ });
256
+ ```
257
+
258
+ ### Events
259
+
260
+ `Askly.on(name, handler)` returns a function that removes the listener; `Askly.off(name, handler)`
261
+ and `Askly.once(name, handler)` are also available. A handler that throws is logged and does not
262
+ affect the widget.
263
+
264
+ | Event | Payload | When |
265
+ | :--- | :--- | :--- |
266
+ | `ready` | `{ visitorId }` | The widget has mounted and applied its configuration. |
267
+ | `open` / `close` | none | The panel was opened or closed. |
268
+ | `message:sent` | `{ text, conversationId }` | The visitor sent a message. |
269
+ | `message:received` | `{ text, conversationId, from, messageId }` | A reply arrived; `from` is `"ai"` or `"agent"`. |
270
+ | `unread` | `{ count }` | The number of unseen agent replies changed. |
271
+ | `escalated` | `{ conversationId }` | The conversation was handed to a human. |
272
+ | `lead:captured` | none | The visitor left an email address. |
273
+ | `identify:success` / `identify:error` | `{ userId, status? }` | The server accepted or refused `identify()`. |
274
+ | `greeting:shown` / `greeting:clicked` / `greeting:dismissed` | none | Greeting bubble activity. |
275
+ | `error` | `{ message, status? }` | A message could not be sent. |
276
+
277
+ ```javascript
278
+ Askly.on("message:received", ({ from }) => analytics.track("support_reply", { from }));
279
+ Askly.on("unread", ({ count }) => { document.title = count ? `(${count}) Acme` : "Acme"; });
280
+ ```
281
+
282
+ TypeScript users can import the payload types: `import type { AsklyEventMap } from "@askly/widget"`.
283
+
284
+ ### Callbacks at init (older style)
285
+
286
+ These still work and are called alongside the events above.
218
287
 
219
288
  Callbacks are functions, so they can only be attached in code (not from the portal):
220
289
 
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The widget's public event bus and its command channel.
3
+ *
4
+ * Events flow out: the mounted widget emits them and host code listens with `Askly.on()`.
5
+ * Commands flow in: `Askly.open()`, `Askly.sendMessage()` and friends post a command that the
6
+ * mounted widget carries out. Commands posted before the widget has mounted are held and
7
+ * delivered when it does, so a host can call them straight after `Askly.init()`.
8
+ */
9
+ export interface AsklyEventMap {
10
+ /** The widget has mounted and loaded its configuration. */
11
+ ready: {
12
+ visitorId: string;
13
+ };
14
+ open: undefined;
15
+ close: undefined;
16
+ /** The visitor sent a message. */
17
+ "message:sent": {
18
+ text: string;
19
+ conversationId: string;
20
+ };
21
+ /** A reply arrived, from the AI or from one of your agents. */
22
+ "message:received": {
23
+ text: string;
24
+ conversationId: string;
25
+ from: "ai" | "agent";
26
+ messageId?: string;
27
+ };
28
+ /** The number of unread agent replies changed. */
29
+ unread: {
30
+ count: number;
31
+ };
32
+ /** The conversation was handed to a human. */
33
+ escalated: {
34
+ conversationId: string;
35
+ };
36
+ /** The visitor left an email address in the capture form. */
37
+ "lead:captured": undefined;
38
+ "identify:success": {
39
+ userId: string;
40
+ };
41
+ "identify:error": {
42
+ userId: string;
43
+ status?: number;
44
+ };
45
+ "greeting:shown": undefined;
46
+ "greeting:clicked": undefined;
47
+ "greeting:dismissed": undefined;
48
+ /** A request failed in a way the visitor was told about. */
49
+ error: {
50
+ message: string;
51
+ status?: number;
52
+ };
53
+ }
54
+ export type AsklyEventName = keyof AsklyEventMap;
55
+ export type AsklyEventHandler<K extends AsklyEventName> = (payload: AsklyEventMap[K]) => void;
56
+ export declare function on<K extends AsklyEventName>(name: K, handler: AsklyEventHandler<K>): () => void;
57
+ export declare function off<K extends AsklyEventName>(name: K, handler: AsklyEventHandler<K>): void;
58
+ export declare function once<K extends AsklyEventName>(name: K, handler: AsklyEventHandler<K>): () => void;
59
+ export declare function emit<K extends AsklyEventName>(name: K, ...payload: AsklyEventMap[K] extends undefined ? [] : [AsklyEventMap[K]]): void;
60
+ export type AsklySpace = "home" | "messages" | "help";
61
+ export type AsklyCommand = {
62
+ type: "open";
63
+ } | {
64
+ type: "close";
65
+ } | {
66
+ type: "toggle";
67
+ } | {
68
+ type: "showSpace";
69
+ space: AsklySpace;
70
+ } | {
71
+ type: "showNewMessage";
72
+ text?: string;
73
+ } | {
74
+ type: "sendMessage";
75
+ text: string;
76
+ } | {
77
+ type: "showArticle";
78
+ articleId: string;
79
+ } | {
80
+ type: "update";
81
+ config: Record<string, any>;
82
+ };
83
+ export declare function expectMount(): void;
84
+ export declare function sendCommand(command: AsklyCommand): void;
85
+ /** Called by the mounted widget. Returns the unsubscribe; held commands are delivered now. */
86
+ export declare function handleCommands(handler: (command: AsklyCommand) => void): () => void;
87
+ /** Drop commands held for a widget that will not mount (shutdown before mount). */
88
+ export declare function clearPendingCommands(): void;
89
+ /** What the getters on `Askly` report. Written by the mounted widget. */
90
+ export declare const widgetState: {
91
+ ready: boolean;
92
+ open: boolean;
93
+ unread: number;
94
+ visitorId: string;
95
+ };
96
+ export declare function resetWidgetState(): void;
@@ -0,0 +1,2 @@
1
+ /** Small display helpers shared by the UI components. */
2
+ export declare function formatTime(dateAny: any): string;
@@ -0,0 +1,10 @@
1
+ export declare const GREETING_DEFAULT_DELAY_MS = 3000;
2
+ export declare const GREETING_MAX_CHIPS = 3;
3
+ /**
4
+ * The greeting line. `{ai_name}` and `{organizationName}` are filled in from config; with no
5
+ * AI persona (or no AI answering at all) the default never promises one.
6
+ */
7
+ export declare function buildGreeting(template: unknown, aiName: string | null, organizationName: string): string;
8
+ export declare function greetingDismissed(scope: string): boolean;
9
+ export declare function rememberGreetingDismissed(scope: string): void;
10
+ export declare function emitGreetingEvent(type: "shown" | "dismissed" | "clicked"): void;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The widget's session with the server.
3
+ *
4
+ * A short-lived token the server issues for this browser (see /widget/session). Once the
5
+ * widget has one, requests carry it in the Authorization header instead of putting the
6
+ * visitor id in URLs or replaying the host's signature, and it remembers what was already
7
+ * proven: a verified user (so a five-minute signature only has to be fresh once) and a
8
+ * passed security check (so guests are challenged once, not per message). Held in memory
9
+ * only. Servers older than this feature answer 404 and the widget carries on without one.
10
+ */
11
+ export interface WidgetSessionOptions {
12
+ /** Full URL of a widget endpoint, e.g. api("/session"). */
13
+ api: (path: string) => string;
14
+ getOrganizationId: () => string | undefined;
15
+ getVisitorId: () => string;
16
+ /** Called when what the session vouches for changes (so the UI can re-render). */
17
+ onChange: () => void;
18
+ }
19
+ export type SessionUrl = string | ((hasSession: boolean) => string);
20
+ export declare class WidgetSession {
21
+ private options;
22
+ /** The current token, or null. */
23
+ token: string | null;
24
+ /** False once the server has answered 404 for sessions: an older server. Final. */
25
+ supported: boolean;
26
+ /** The host user id the session vouches for, if any. Compared with the current identity
27
+ * so a session verified for one user is never used to speak for another. */
28
+ userId: string | null;
29
+ /** The session has passed the security check. */
30
+ human: boolean;
31
+ private pending;
32
+ /** Why the last attempt to start or renew failed: the server refused the token
33
+ * ("rejected"), or something temporary got in the way ("transient"). */
34
+ private lastFailure;
35
+ constructor(options: WidgetSessionOptions);
36
+ /** Take the session fields from a server response, if it carries any. */
37
+ apply(data: any): void;
38
+ clear(): void;
39
+ /**
40
+ * Start a session from the visitor id, or renew one from its (possibly expired) token.
41
+ * Concurrent callers share one request. Resolves to the token, or null.
42
+ */
43
+ start(renewFrom?: string): Promise<string | null>;
44
+ /**
45
+ * fetch() with the session token attached. `url` may depend on whether there is a token
46
+ * (the poll leaves the visitor id out of its URL when there is one). Resolves to the
47
+ * response and the token the (final) request went out with.
48
+ *
49
+ * On 401 the token is renewed once and the request retried. Only when the server refuses
50
+ * the token outright is the session dropped for an anonymous one; a renewal that failed
51
+ * for a passing reason (rate limit, deploy, network) leaves a verified session intact and
52
+ * the 401 is returned for the caller to treat as a failed request.
53
+ */
54
+ request(url: SessionUrl, init?: RequestInit): Promise<{
55
+ res: Response;
56
+ token: string | null;
57
+ }>;
58
+ fetch(url: SessionUrl, init?: RequestInit): Promise<Response>;
59
+ }
@@ -0,0 +1,4 @@
1
+ /** Local persistence for chat history. A cache, never the record: the server has the truth. */
2
+ import type { SavedConversation } from "./types";
3
+ export declare function loadConversations(key: string): SavedConversation[];
4
+ export declare function saveConversations(key: string, conversations: SavedConversation[]): void;
@@ -0,0 +1,65 @@
1
+ /** Shapes shared by the widget's core and its UI components. */
2
+ import type { MessageFormat } from "../markdown";
3
+ export interface ChatWidgetProps {
4
+ orgId?: string;
5
+ organizationName?: string;
6
+ themeColor?: string;
7
+ theme?: "light" | "dark" | "auto";
8
+ position?: "left" | "right";
9
+ orgServerRoute: string;
10
+ enableVoiceChat?: boolean;
11
+ readAloud?: boolean;
12
+ welcomeMessage?: string;
13
+ placeholderText?: string;
14
+ organizationLogo?: string;
15
+ buttonIcon?: string;
16
+ buttonShape?: "round" | "square";
17
+ autoOpen?: boolean;
18
+ greeting?: boolean | {
19
+ message?: string;
20
+ delayMs?: number;
21
+ };
22
+ showTimestamp?: boolean;
23
+ soundEnabled?: boolean;
24
+ soundVolume?: number;
25
+ poweredBy?: string;
26
+ userId?: string;
27
+ timestamp?: number;
28
+ signature?: string;
29
+ turnstileSiteKey?: string;
30
+ excludePaths?: string[];
31
+ includePaths?: string[];
32
+ configOverride?: Record<string, any>;
33
+ resetGeneration?: number;
34
+ onMessageSent?: (message: string) => void;
35
+ onMessageReceived?: (reply: string) => void;
36
+ onChatOpened?: () => void;
37
+ onChatClosed?: () => void;
38
+ onEscalation?: (data: any) => void;
39
+ }
40
+ export interface Message {
41
+ role: "user" | "agent";
42
+ content: string;
43
+ timestamp?: Date;
44
+ format?: MessageFormat;
45
+ kind?: "greeting";
46
+ messageId?: string;
47
+ }
48
+ export interface SavedConversation {
49
+ id: string;
50
+ messages: Message[];
51
+ lastUpdated: string;
52
+ unread?: number;
53
+ }
54
+ export interface Article {
55
+ id: string;
56
+ title: string;
57
+ content: string;
58
+ document_type: string;
59
+ score?: number;
60
+ }
61
+ export type WidgetTab = "explore" | "messages" | "help";
62
+ export interface Faq {
63
+ question: string;
64
+ answer: string;
65
+ }
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;
@@ -75,5 +76,42 @@ declare const Askly: {
75
76
  close: () => void;
76
77
  /** Toggle the chat panel. */
77
78
  toggle: () => void;
79
+ /** Aliases of open() / close(). */
80
+ show: () => void;
81
+ hide: () => void;
82
+ /** Whether the chat panel is open right now. */
83
+ isOpen: () => boolean;
84
+ /** Agent replies the visitor has not seen yet (the number on the launcher badge). */
85
+ getUnreadCount: () => number;
86
+ /** This browser's anonymous visitor id, or "" before the widget has mounted. */
87
+ getVisitorId: () => string;
88
+ /**
89
+ * Listen for a widget event. Returns a function that removes the listener.
90
+ *
91
+ * Askly.on("message:received", ({ text, from }) => analytics.track("support_reply", { from }));
92
+ * Askly.on("unread", ({ count }) => setBadge(count));
93
+ */
94
+ on: <K extends keyof AsklyEventMap>(name: K, handler: AsklyEventHandler<K>) => (() => void);
95
+ off: <K_1 extends keyof AsklyEventMap>(name: K_1, handler: AsklyEventHandler<K_1>) => void;
96
+ once: <K_2 extends keyof AsklyEventMap>(name: K_2, handler: AsklyEventHandler<K_2>) => (() => void);
97
+ /** Resolves when the widget has mounted and applied its configuration. */
98
+ ready: () => Promise<void>;
99
+ /** Open the widget on a section: "home", "messages" or "help". */
100
+ showSpace: (space: AsklySpace) => void;
101
+ /** Open the chat with the composer ready, optionally pre-filled. Nothing is sent. */
102
+ showNewMessage: (text?: string) => void;
103
+ /**
104
+ * Open the chat and send a message as the visitor. It goes through the same checks as a
105
+ * typed message; if one is pending (email required, security check) it waits in the composer.
106
+ */
107
+ sendMessage: (text: string) => void;
108
+ /** Open a help article by id, as listed in the Help tab. */
109
+ showArticle: (articleId: string) => void;
110
+ /**
111
+ * Change display settings on the mounted widget without re-initialising: the fields the
112
+ * portal controls, e.g. `{ themeColor, theme, organizationName, welcomeMessage, buttonShape }`.
113
+ */
114
+ update: (config: Record<string, any>) => void;
78
115
  };
79
116
  export default Askly;
117
+ export type { AsklyIdentity, AsklyEventMap, AsklyEventName, AsklyEventHandler, AsklySpace };