@askly/widget 2.7.0 → 2.9.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
@@ -108,10 +108,11 @@ const hash = crypto
108
108
 
109
109
  Without a valid `hash` the traits are still used locally, but Askly will not link the accounts.
110
110
 
111
- > **Known limitation:** a signature is accepted for 5 minutes from its `timestamp`. On a page that
112
- > stays open longer, call `Askly.identify()` again with a freshly generated `hash` and `timestamp`
113
- > (for example on a timer, or before the user opens the chat). If `identify()` is rejected, the
114
- > reason is logged to the browser console.
111
+ The `hash` only has to be fresh at the moment `identify()` (or `init()`) runs: it is valid for 5
112
+ minutes from its `timestamp`. Once Askly has verified it, the widget holds a session for that user
113
+ and keeps renewing it, so a page that stays open for hours keeps working without a new hash. If
114
+ `identify()` is rejected, the reason is logged to the browser console and messages are sent as
115
+ the anonymous visitor.
115
116
 
116
117
  #### Logging out
117
118
 
@@ -165,6 +166,19 @@ Askly.init({ appId: "YOUR_APP_ID", greeting: { message: "Need a hand?", delayMs:
165
166
  The widget dispatches `askly:greeting:shown`, `askly:greeting:clicked` and
166
167
  `askly:greeting:dismissed` on `window` for analytics.
167
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`.
181
+
168
182
  ### Asking visitors for their email
169
183
 
170
184
  Anonymous visitors can't be replied to once they close the tab — the answer just waits in a widget
@@ -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
+ }