@v-tilt/browser 1.13.0 → 1.13.1
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/dist/all-external-dependencies.js.map +1 -1
- package/dist/array.chat.js +2 -0
- package/dist/array.chat.js.map +1 -0
- package/dist/array.chat.no-external.js +2 -0
- package/dist/array.chat.no-external.js.map +1 -0
- package/dist/array.full.chat.js +2 -0
- package/dist/array.full.chat.js.map +1 -0
- package/dist/array.full.chat.no-external.js +2 -0
- package/dist/array.full.chat.no-external.js.map +1 -0
- package/dist/array.full.js +1 -1
- package/dist/array.full.js.map +1 -1
- package/dist/array.full.no-external.js +2 -0
- package/dist/array.full.no-external.js.map +1 -0
- package/dist/array.js +1 -1
- package/dist/array.js.map +1 -1
- package/dist/array.no-external.js +1 -1
- package/dist/array.no-external.js.map +1 -1
- package/dist/chat.js +1 -1
- package/dist/chat.js.map +1 -1
- package/dist/entrypoints/all-external-dependencies.d.ts +10 -3
- package/dist/entrypoints/array.chat.d.ts +10 -0
- package/dist/entrypoints/array.chat.no-external.d.ts +6 -0
- package/dist/entrypoints/array.full.chat.d.ts +13 -0
- package/dist/entrypoints/array.full.chat.no-external.d.ts +7 -0
- package/dist/entrypoints/array.full.d.ts +5 -9
- package/dist/entrypoints/array.full.no-external.d.ts +12 -0
- package/dist/entrypoints/module.chat.es.d.ts +7 -0
- package/dist/entrypoints/module.full.chat.es.d.ts +12 -0
- package/dist/entrypoints/module.full.es.d.ts +12 -0
- package/dist/entrypoints/module.no-external.es.d.ts +1 -0
- package/dist/extensions/chat/bubble-drag.d.ts +20 -5
- package/dist/extensions/chat/chat-wrapper.d.ts +8 -2
- package/dist/extensions/chat/chat.d.ts +21 -351
- package/dist/extensions/chat/controller/__tests__/fakes/ably-realtime-fake.d.ts +84 -0
- package/dist/extensions/chat/controller/ably-client.d.ts +160 -0
- package/dist/extensions/chat/controller/ably-handlers.d.ts +95 -0
- package/dist/extensions/chat/controller/ably-token.d.ts +67 -0
- package/dist/extensions/chat/controller/chat-controller.d.ts +194 -0
- package/dist/extensions/chat/controller/message-delivery-status.d.ts +6 -0
- package/dist/extensions/chat/controller/message-order.d.ts +12 -0
- package/dist/extensions/chat/controller/message-stream.d.ts +49 -0
- package/dist/extensions/chat/lib/bubble-offset.d.ts +18 -0
- package/dist/extensions/chat/lib/merge-chat-config.d.ts +3 -0
- package/dist/extensions/chat/normalize-send-content.d.ts +2 -0
- package/dist/extensions/chat/outbox/message-delivery.d.ts +17 -0
- package/dist/extensions/chat/outbox/message-outbox.d.ts +57 -0
- package/dist/extensions/chat/store/chat-store.d.ts +122 -0
- package/dist/extensions/chat/types.d.ts +1 -19
- package/dist/extensions/chat/ui/ChannelItem.d.ts +12 -0
- package/dist/extensions/chat/ui/ChannelListView.d.ts +14 -0
- package/dist/extensions/chat/ui/ChatBubble.d.ts +14 -0
- package/dist/extensions/chat/ui/ChatHeader.d.ts +13 -0
- package/dist/extensions/chat/ui/ChatPanel.d.ts +14 -0
- package/dist/extensions/chat/ui/ChatRoot.d.ts +31 -0
- package/dist/extensions/chat/ui/ClosedBanner.d.ts +7 -0
- package/dist/extensions/chat/ui/ConnectionBanner.d.ts +32 -0
- package/dist/extensions/chat/ui/ConversationView.d.ts +14 -0
- package/dist/extensions/chat/ui/MessageBubble.d.ts +25 -0
- package/dist/extensions/chat/ui/MessageInput.d.ts +14 -0
- package/dist/extensions/chat/ui/MessageList.d.ts +19 -0
- package/dist/extensions/chat/ui/NewMessagesPill.d.ts +13 -0
- package/dist/extensions/chat/ui/Skeletons.d.ts +14 -0
- package/dist/extensions/chat/ui/TypingBubble.d.ts +23 -0
- package/dist/extensions/chat/ui/TypingIndicator.d.ts +12 -0
- package/dist/extensions/chat/ui/WidgetSlot.d.ts +20 -0
- package/dist/extensions/chat/ui/hooks/use-auto-mark-read.d.ts +18 -0
- package/dist/extensions/chat/ui/hooks/use-scroll-preservation.d.ts +33 -0
- package/dist/extensions/chat/ui/icons.d.ts +18 -0
- package/dist/extensions/chat/ui/message-render.d.ts +18 -0
- package/dist/extensions/chat/ui/shadow-styles.d.ts +13 -0
- package/dist/extensions/chat/ui/utils/mobile-body-scroll-lock.d.ts +14 -0
- package/dist/extensions/chat/widget-registry.d.ts +21 -5
- package/dist/extensions/chat/widgets/collect-email.d.ts +4 -2
- package/dist/extensions/chat/widgets/escalate-to-human.d.ts +2 -1
- package/dist/external-scripts-loader.js +1 -1
- package/dist/external-scripts-loader.js.map +1 -1
- package/dist/feature.d.ts +1 -0
- package/dist/lib/merge-vtilt-config.d.ts +9 -0
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/module.chat.d.ts +1788 -0
- package/dist/module.chat.js +2 -0
- package/dist/module.chat.js.map +1 -0
- package/dist/module.d.ts +48 -3
- package/dist/module.full.chat.d.ts +1792 -0
- package/dist/module.full.chat.js +2 -0
- package/dist/module.full.chat.js.map +1 -0
- package/dist/module.full.d.ts +1793 -0
- package/dist/module.full.js +2 -0
- package/dist/module.full.js.map +1 -0
- package/dist/module.js +1 -1
- package/dist/module.js.map +1 -1
- package/dist/module.no-external.d.ts +48 -3
- package/dist/module.no-external.js +1 -1
- package/dist/module.no-external.js.map +1 -1
- package/dist/recorder.js.map +1 -1
- package/dist/snippet-stub-methods.d.ts +14 -0
- package/dist/utils/globals.d.ts +53 -27
- package/dist/utils/index.d.ts +8 -0
- package/dist/vtilt.d.ts +6 -1
- package/dist/web-vitals.js.map +1 -1
- package/package.json +74 -71
- package/dist/extensions/chat/chat-styles.d.ts +0 -27
- package/dist/extensions/chat/message-content-styles.d.ts +0 -1
- package/dist/extensions/chat/message-html.d.ts +0 -6
- package/dist/extensions/chat/message-markdown.d.ts +0 -8
- package/dist/extensions/ga4-proxy.d.ts +0 -59
- package/dist/utils/type-utils.d.ts +0 -4
- package/dist/web-vitals.d.ts +0 -81
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory fake of Ably's `BaseRealtime` surface used by AblyClient tests.
|
|
3
|
+
*
|
|
4
|
+
* Only models the parts the chat layer actually touches:
|
|
5
|
+
* - `connection` with `state` + `.on(event, handler)` events
|
|
6
|
+
* for "connected" | "disconnected" | "suspended" | "failed"
|
|
7
|
+
* - `auth.clientId`, `auth.authorize(token)`
|
|
8
|
+
* - `channels.get(name)` returning a FakeRealtimeChannel with attach/detach,
|
|
9
|
+
* subscribe/unsubscribe/publish, and a `state` machine that can be driven
|
|
10
|
+
* programmatically.
|
|
11
|
+
* - `channels.release(name)`
|
|
12
|
+
* - `connect()` / `close()`
|
|
13
|
+
*
|
|
14
|
+
* The Ably typings are wide and surface lots of unrelated APIs; rather than
|
|
15
|
+
* fight them we expose this fake with its own minimal types and only cast
|
|
16
|
+
* to `BaseRealtime` at the construction site.
|
|
17
|
+
*/
|
|
18
|
+
import type { ChannelState, ChannelStateChange, TokenRequest } from "ably";
|
|
19
|
+
import type { BaseRealtime } from "ably/modular";
|
|
20
|
+
type Handler<T> = (event: T) => void;
|
|
21
|
+
export declare class FakeRealtimeChannel {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
state: ChannelState;
|
|
24
|
+
readonly subscriptions: Map<string, ((msg: {
|
|
25
|
+
data: unknown;
|
|
26
|
+
}) => void)[]>;
|
|
27
|
+
readonly published: Array<{
|
|
28
|
+
event: string;
|
|
29
|
+
data: unknown;
|
|
30
|
+
}>;
|
|
31
|
+
attachCalls: number;
|
|
32
|
+
detachCalls: number;
|
|
33
|
+
private _stateListeners;
|
|
34
|
+
private _attachResolvers;
|
|
35
|
+
constructor(name: string);
|
|
36
|
+
attach(): Promise<void>;
|
|
37
|
+
/** Test helper to resolve a pending attach. */
|
|
38
|
+
resolveAttach(): void;
|
|
39
|
+
/** Test helper to reject a pending attach. */
|
|
40
|
+
rejectAttach(err: unknown): void;
|
|
41
|
+
detach(): Promise<void>;
|
|
42
|
+
setState(next: ChannelState): void;
|
|
43
|
+
on(handler: Handler<ChannelStateChange>): void;
|
|
44
|
+
off(handler: Handler<ChannelStateChange>): void;
|
|
45
|
+
subscribe(event: string, callback: (msg: {
|
|
46
|
+
data: unknown;
|
|
47
|
+
}) => void): void;
|
|
48
|
+
unsubscribe(): void;
|
|
49
|
+
publish(event: string, data: unknown): Promise<void>;
|
|
50
|
+
/** Drive an inbound message for tests. */
|
|
51
|
+
emit(event: string, data: unknown): void;
|
|
52
|
+
}
|
|
53
|
+
export declare class FakeAuth {
|
|
54
|
+
clientId: string | null;
|
|
55
|
+
authorizeCalls: number;
|
|
56
|
+
shouldFailAuthorize: boolean;
|
|
57
|
+
authorize(token: TokenRequest): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
export type ConnectionState = "initialized" | "connecting" | "connected" | "disconnected" | "suspended" | "closing" | "closed" | "failed";
|
|
60
|
+
export declare class FakeConnection {
|
|
61
|
+
state: ConnectionState;
|
|
62
|
+
private _listeners;
|
|
63
|
+
on(event: string, handler: Handler<unknown>): void;
|
|
64
|
+
emit(event: ConnectionState): void;
|
|
65
|
+
}
|
|
66
|
+
export declare class FakeChannels {
|
|
67
|
+
releaseCalls: string[];
|
|
68
|
+
private _registry;
|
|
69
|
+
get(name: string): FakeRealtimeChannel;
|
|
70
|
+
release(name: string): void;
|
|
71
|
+
}
|
|
72
|
+
export declare class FakeBaseRealtime {
|
|
73
|
+
connectCalls: number;
|
|
74
|
+
closeCalls: number;
|
|
75
|
+
readonly connection: FakeConnection;
|
|
76
|
+
readonly auth: FakeAuth;
|
|
77
|
+
readonly channels: FakeChannels;
|
|
78
|
+
constructor();
|
|
79
|
+
connect(): void;
|
|
80
|
+
close(): void;
|
|
81
|
+
/** Cast helper to satisfy AblyClient's constructor signature. */
|
|
82
|
+
asBaseRealtime(): BaseRealtime;
|
|
83
|
+
}
|
|
84
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ably client wrapper for the chat widget.
|
|
3
|
+
*
|
|
4
|
+
* Owns the `BaseRealtime` lifecycle (connect/disconnect), the channel
|
|
5
|
+
* registry (notifications + per-conversation message + per-conversation
|
|
6
|
+
* typing), and the race-safe attach/detach flow.
|
|
7
|
+
*
|
|
8
|
+
* Writes connection state into the store; reads channel routing from the
|
|
9
|
+
* store. Does NOT mutate `messages` / `channels` / `typing` signals
|
|
10
|
+
* directly — those flow through the `applyMessage` / `applyNotification`
|
|
11
|
+
* / `applyTyping` handlers in `ably-handlers.ts`.
|
|
12
|
+
*
|
|
13
|
+
* See: docs/modules/chat/widget-architecture.md (Ably layer)
|
|
14
|
+
*/
|
|
15
|
+
import { BaseRealtime } from "ably/modular";
|
|
16
|
+
import type { ApiRequestInstance } from "../chat-api";
|
|
17
|
+
import type { ChatStore } from "../store/chat-store";
|
|
18
|
+
import { type TypingPayload } from "./ably-handlers";
|
|
19
|
+
/**
|
|
20
|
+
* Hooks the controller plugs into the client so domain decisions stay in
|
|
21
|
+
* the controller (e.g. how to react to an inbound message). The client
|
|
22
|
+
* only owns the Ably mechanics.
|
|
23
|
+
*/
|
|
24
|
+
export interface AblyClientHooks {
|
|
25
|
+
getDistinctId: () => string;
|
|
26
|
+
onIdentityChange: () => void;
|
|
27
|
+
onNewChannelNotification: () => void;
|
|
28
|
+
onAgentMessageWhileOpen: () => void;
|
|
29
|
+
onMessageTrack: (m: import("../../../utils/globals").ChatMessage) => void;
|
|
30
|
+
onMessageDispatched: (m: import("../../../utils/globals").ChatMessage) => void;
|
|
31
|
+
onTypingDispatched: (isTyping: boolean, senderName: string) => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Wire `BaseRealtime` lazily — keeps tests free of the constructor side
|
|
35
|
+
* effect of opening a WebSocket.
|
|
36
|
+
*/
|
|
37
|
+
export type BaseRealtimeFactory = (options: ConstructorParameters<typeof BaseRealtime>[0]) => BaseRealtime;
|
|
38
|
+
export declare class AblyClient {
|
|
39
|
+
private readonly instance;
|
|
40
|
+
private readonly store;
|
|
41
|
+
private readonly hooks;
|
|
42
|
+
private _client;
|
|
43
|
+
private _notificationsChannel;
|
|
44
|
+
private _conversationChannel;
|
|
45
|
+
private _typingChannel;
|
|
46
|
+
/**
|
|
47
|
+
* Latest in-flight pre-attach teardown. `_doAttach` must `await` this so a
|
|
48
|
+
* newly-requested attach never calls `auth.authorize()` while a previous
|
|
49
|
+
* channel is still attached — Ably re-validates ALL attached channels
|
|
50
|
+
* against the new token and the previous one would fail with 40160.
|
|
51
|
+
*/
|
|
52
|
+
private _teardownInFlight;
|
|
53
|
+
private _factory;
|
|
54
|
+
constructor(instance: ApiRequestInstance, store: ChatStore, hooks: AblyClientHooks, factory?: BaseRealtimeFactory);
|
|
55
|
+
/**
|
|
56
|
+
* Expose the live clientId (used by `authCallback` mismatch guard and by
|
|
57
|
+
* the controller's identity-change detector on the `connected` event).
|
|
58
|
+
*/
|
|
59
|
+
get clientId(): string | null;
|
|
60
|
+
/**
|
|
61
|
+
* Whether we already own a live (or live-ish) client. Multiple calls to
|
|
62
|
+
* `ensureConnected()` are idempotent — the first one creates the client,
|
|
63
|
+
* the rest no-op.
|
|
64
|
+
*/
|
|
65
|
+
get isLive(): boolean;
|
|
66
|
+
/** Currently-attached per-conversation channel id (null on list view). */
|
|
67
|
+
get currentChannelId(): string | null;
|
|
68
|
+
/**
|
|
69
|
+
* Idempotently bootstrap the Ably client and attach to the project
|
|
70
|
+
* notifications channel. Safe to call from `open()` and on first load.
|
|
71
|
+
*/
|
|
72
|
+
ensureConnected(): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Refresh the Ably token, optionally scoping it to a specific channel.
|
|
75
|
+
* Refuses to authorize on a `failed` connection (terminal — only fix is
|
|
76
|
+
* to recreate the client, which the identity-change hook does).
|
|
77
|
+
*/
|
|
78
|
+
refreshToken(channelId?: string): Promise<boolean>;
|
|
79
|
+
/**
|
|
80
|
+
* Subscribe to per-conversation messages + typing for `channelId`.
|
|
81
|
+
*
|
|
82
|
+
* Atomic swap semantics: `realtimeChannelId` and `realtimeAttached`
|
|
83
|
+
* still reflect the **last successful attach** for the entire duration
|
|
84
|
+
* of this method — they only flip to `channelId` after both the main
|
|
85
|
+
* and typing channels have attached and subscribed. This means
|
|
86
|
+
* `realtimeReady` (and therefore the "Connecting…" banner) does not
|
|
87
|
+
* blink during normal channel switches; the worst case is a slightly
|
|
88
|
+
* stale `realtimeChannelId` for a few ms while the new channel
|
|
89
|
+
* negotiates, which is harmless because:
|
|
90
|
+
*
|
|
91
|
+
* - Stale inbound frames are filtered by the `_conversationChannel !==
|
|
92
|
+
* mainChannel` ref guard inside the subscribe callbacks.
|
|
93
|
+
* - `realtimeReady` already requires `realtimeChannelId === channel.value.id`,
|
|
94
|
+
* so once the new channel.value is set the computed signal correctly
|
|
95
|
+
* reads false until the swap completes.
|
|
96
|
+
*
|
|
97
|
+
* Concurrent navigations (rapid switch / goToChannelList) are caught
|
|
98
|
+
* by `wasSuperseded()` and the `pendingRealtimeChannelId` guard before
|
|
99
|
+
* the final write.
|
|
100
|
+
*
|
|
101
|
+
* Before refreshing the token this method awaits any previously-attached
|
|
102
|
+
* conversation channels' detach, so the `auth.authorize()` call in
|
|
103
|
+
* `refreshToken()` cannot trigger an Ably 40160 capability check against
|
|
104
|
+
* the old channel.
|
|
105
|
+
*/
|
|
106
|
+
attachConversation(channelId: string): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Detach + release the per-conversation channels. Safe to call from
|
|
109
|
+
* `close()`, `goToChannelList()`, and rapid switching paths.
|
|
110
|
+
*
|
|
111
|
+
* Tracks the in-flight teardown on `_teardownInFlight` so the next
|
|
112
|
+
* `attachConversation()` can await it before refreshing the token.
|
|
113
|
+
*/
|
|
114
|
+
detachConversation(): Promise<void>;
|
|
115
|
+
/** Synchronously clear the channel refs and kick off async detach.
|
|
116
|
+
*
|
|
117
|
+
* Does NOT touch `realtimeChannelId` / `realtimeAttached` — those reflect
|
|
118
|
+
* the last successful attach and are owned by `attachConversation()`
|
|
119
|
+
* (which swaps them atomically when the new channel is fully ready) and
|
|
120
|
+
* by `disconnectAll()` (which clears them on full teardown). Leaving
|
|
121
|
+
* them intact during a channel-switch detach is what kills the
|
|
122
|
+
* "Connecting…" flash; see widget-architecture.md §"Atomic realtime
|
|
123
|
+
* swap". */
|
|
124
|
+
private _teardownActive;
|
|
125
|
+
/** Publish a typing event on the active typing channel. Best-effort. */
|
|
126
|
+
publishTyping(payload: TypingPayload & {
|
|
127
|
+
sender_id: string;
|
|
128
|
+
}): void;
|
|
129
|
+
/**
|
|
130
|
+
* Full teardown — detach all channels, close the Ably connection, reset
|
|
131
|
+
* the store's realtime signals. Used on `destroy()` and identity change.
|
|
132
|
+
*/
|
|
133
|
+
disconnectAll(): Promise<void>;
|
|
134
|
+
/**
|
|
135
|
+
* After a `connected` event, if the live clientId no longer matches the
|
|
136
|
+
* distinct id we authorized for (e.g. the page slept through an identity
|
|
137
|
+
* change), trigger a full client recreate.
|
|
138
|
+
*/
|
|
139
|
+
private _checkClientIdDrift;
|
|
140
|
+
/**
|
|
141
|
+
* Unsubscribe + detach. We intentionally do NOT call
|
|
142
|
+
* `client.channels.release()` here.
|
|
143
|
+
*
|
|
144
|
+
* Ably's `channels.release(name)` removes the channel object from the
|
|
145
|
+
* client's local registry. Any inbound WebSocket frame that arrives for
|
|
146
|
+
* that name afterwards triggers the library's
|
|
147
|
+
* "received event for non-existent channel" warning, because dispatch
|
|
148
|
+
* looks the channel up in the same registry. In-flight typing and
|
|
149
|
+
* message frames are easy to land after a detach on a rapid switch, so
|
|
150
|
+
* release-on-switch deterministically surfaces the warning.
|
|
151
|
+
*
|
|
152
|
+
* Per Ably's guidance, `release()` is for permanent cleanup. For
|
|
153
|
+
* conversation switching we just `unsubscribe()` + `detach()`; the
|
|
154
|
+
* channel object stays in the registry with no listeners and any
|
|
155
|
+
* residual frames are silently ignored. Full cleanup happens in
|
|
156
|
+
* `disconnectAll()` via `client.close()`, which tears down the
|
|
157
|
+
* connection without per-channel release.
|
|
158
|
+
*/
|
|
159
|
+
private _teardownChannel;
|
|
160
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure functions that translate inbound Ably events into store mutations.
|
|
3
|
+
*
|
|
4
|
+
* No DOM, no fetch, no Ably types — only the wire shapes of the messages
|
|
5
|
+
* Ably delivers to us. Easy to unit-test against a fake `BaseRealtime`.
|
|
6
|
+
*
|
|
7
|
+
* See: docs/modules/chat/widget-architecture.md (Message-arrival contract)
|
|
8
|
+
*/
|
|
9
|
+
import type { ChatMessage } from "../../../utils/globals";
|
|
10
|
+
import type { ChatStore } from "../store/chat-store";
|
|
11
|
+
export interface NotificationPayload {
|
|
12
|
+
type: string;
|
|
13
|
+
channel_id: string;
|
|
14
|
+
data?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface TypingPayload {
|
|
17
|
+
sender_type: string;
|
|
18
|
+
sender_name: string | null;
|
|
19
|
+
is_typing: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface ReadCursorPayload {
|
|
22
|
+
reader_type: "user" | "agent";
|
|
23
|
+
reader_id: string | null;
|
|
24
|
+
read_at: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Insert or replace a message in the array. Pure id-based reconciliation:
|
|
28
|
+
* because the SDK mints the user message id client-side and the AI bubble
|
|
29
|
+
* adopts the persisted id from the streaming response header, every
|
|
30
|
+
* inbound payload arrives with the same identity the bubble already
|
|
31
|
+
* displays — no content+sender heuristic, no temp-id matching.
|
|
32
|
+
*
|
|
33
|
+
* Two special cases preserved:
|
|
34
|
+
*
|
|
35
|
+
* 1. Streaming `_widgets` metadata: if the local bubble accumulated
|
|
36
|
+
* `_widgets` (from inline tool-call markers) and the Ably echo doesn't
|
|
37
|
+
* carry an authoritative `widgets`, we merge the local `_widgets`
|
|
38
|
+
* into the persisted row so the form / button doesn't disappear.
|
|
39
|
+
*
|
|
40
|
+
* 2. Legacy `temp-ai-*` fallback: very old servers (or a stream that
|
|
41
|
+
* failed to pre-create its row) leave the SDK with a `temp-ai-*` id.
|
|
42
|
+
* When a persisted AI message arrives we swap the first such row so
|
|
43
|
+
* the user still sees a single bubble.
|
|
44
|
+
*
|
|
45
|
+
* Returns the **same array reference** when nothing actually changed
|
|
46
|
+
* (incoming matches an existing row by deep field equality). This lets
|
|
47
|
+
* memoised components (MessageBubble) skip re-renders on duplicate
|
|
48
|
+
* echoes, which is critical for streaming throughput where the same
|
|
49
|
+
* message id can be re-published multiple times.
|
|
50
|
+
*/
|
|
51
|
+
export declare function upsertMessage(messages: ChatMessage[], incoming: ChatMessage): ChatMessage[];
|
|
52
|
+
/** Returns true when the message id is already in the array. */
|
|
53
|
+
export declare function hasMessage(messages: ChatMessage[], id: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Apply an inbound chat message to the store. Performs the upsert, updates
|
|
56
|
+
* unread counts (skipping when the user is currently viewing this channel),
|
|
57
|
+
* and clears the typing indicator.
|
|
58
|
+
*
|
|
59
|
+
* `currentUserDistinctId` is consulted only to suppress duplicate side
|
|
60
|
+
* effects on the user's own echoes — the upsert itself is pure id-based.
|
|
61
|
+
*
|
|
62
|
+
* `onAgentMessageWhileOpen` is invoked when an agent/AI message arrives
|
|
63
|
+
* while the widget is open and viewing this channel — used by the
|
|
64
|
+
* controller to schedule `markAsRead`.
|
|
65
|
+
*
|
|
66
|
+
* `onTrack` is invoked for agent/AI messages so the controller can forward
|
|
67
|
+
* to the SDK's `capture()` pipeline and `config.onMessageReceived` callback.
|
|
68
|
+
*/
|
|
69
|
+
export declare function applyMessage(store: ChatStore, incoming: ChatMessage, currentUserDistinctId: string | null, callbacks: {
|
|
70
|
+
onAgentMessageWhileOpen: () => void;
|
|
71
|
+
onTrack: (m: ChatMessage) => void;
|
|
72
|
+
onMessageDispatched: (m: ChatMessage) => void;
|
|
73
|
+
}): void;
|
|
74
|
+
/**
|
|
75
|
+
* Handle project-level notifications (new channels, channel updates, closes).
|
|
76
|
+
* Updates the channel list and total badge count in real time without
|
|
77
|
+
* touching `messages` — message-list updates flow through the per-channel
|
|
78
|
+
* Ably channel.
|
|
79
|
+
*/
|
|
80
|
+
export declare function applyNotification(store: ChatStore, notification: NotificationPayload, callbacks: {
|
|
81
|
+
onNewChannel: () => void;
|
|
82
|
+
}): void;
|
|
83
|
+
/**
|
|
84
|
+
* Apply an inbound typing event. The user's own typing events are echoed
|
|
85
|
+
* back from Ably and must be ignored here so the indicator only reflects
|
|
86
|
+
* the other party.
|
|
87
|
+
*/
|
|
88
|
+
export declare function applyTyping(store: ChatStore, event: TypingPayload, callbacks: {
|
|
89
|
+
onTyping: (isTyping: boolean, senderName: string) => void;
|
|
90
|
+
}): void;
|
|
91
|
+
/**
|
|
92
|
+
* Apply an agent read-cursor advance. User cursors are not needed in the
|
|
93
|
+
* widget because the widget *is* the user.
|
|
94
|
+
*/
|
|
95
|
+
export declare function applyReadCursor(store: ChatStore, event: ReadCursorPayload): void;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ably token provisioning for the chat widget.
|
|
3
|
+
*
|
|
4
|
+
* - `requestToken()` performs the initial token request before the Ably
|
|
5
|
+
* client is created.
|
|
6
|
+
* - `createAuthCallback()` builds the `authCallback` Ably calls when it
|
|
7
|
+
* needs to refresh or scope a token (e.g. attaching to a per-channel
|
|
8
|
+
* subscription). The callback guards against clientId mismatches (40102)
|
|
9
|
+
* that would land the connection in the terminal `failed` state.
|
|
10
|
+
*
|
|
11
|
+
* No DOM, no Ably types — just `fetch` + the wire payload + a thin
|
|
12
|
+
* `IdentityChangeHook` callback for the rare clientId drift case.
|
|
13
|
+
*
|
|
14
|
+
* See: docs/modules/chat/widget-architecture.md (Race-free identity changes)
|
|
15
|
+
*/
|
|
16
|
+
import type { TokenRequest } from "ably";
|
|
17
|
+
import { type ApiRequestInstance } from "../chat-api";
|
|
18
|
+
export interface InitialTokenResponse {
|
|
19
|
+
tokenRequest: TokenRequest;
|
|
20
|
+
clientId: string;
|
|
21
|
+
projectId: number;
|
|
22
|
+
}
|
|
23
|
+
/** Wire payload returned by the widget token endpoint. */
|
|
24
|
+
interface RawTokenResponse {
|
|
25
|
+
success: boolean;
|
|
26
|
+
tokenRequest: TokenRequest;
|
|
27
|
+
clientId?: string;
|
|
28
|
+
project_id?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Request an Ably token from the widget endpoint.
|
|
32
|
+
*
|
|
33
|
+
* Returns `null` when the request fails — the caller should bail out and
|
|
34
|
+
* leave the store in `connectionState: "failed"`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function requestToken(instance: ApiRequestInstance, distinctId: string, channelId?: string): Promise<RawTokenResponse | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Request the initial token used to bootstrap the Ably client. Asserts that
|
|
39
|
+
* `clientId` and `project_id` are present, since both are required to
|
|
40
|
+
* authorize the connection.
|
|
41
|
+
*/
|
|
42
|
+
export declare function requestInitialToken(instance: ApiRequestInstance, distinctId: string): Promise<InitialTokenResponse | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Provide the live Ably client's clientId. Returning `null` short-circuits
|
|
45
|
+
* the mismatch guard (used in tests / before the client exists).
|
|
46
|
+
*/
|
|
47
|
+
export type LiveClientIdAccessor = () => string | null;
|
|
48
|
+
/** Notify the controller that a clientId drift was detected. */
|
|
49
|
+
export type IdentityChangeHook = () => void;
|
|
50
|
+
/**
|
|
51
|
+
* Build an Ably `authCallback`. Each refresh:
|
|
52
|
+
*
|
|
53
|
+
* 1. Calls the widget token endpoint with the *current* channel scope
|
|
54
|
+
* (derived from the store's `realtimeChannelId` / `pendingRealtimeChannelId`).
|
|
55
|
+
* 2. Verifies the token's `clientId` matches the live connection's
|
|
56
|
+
* `clientId`. If they differ, refuses the token and fires the identity
|
|
57
|
+
* change hook — refusing here prevents Ably from transitioning to the
|
|
58
|
+
* terminal `failed` state.
|
|
59
|
+
*/
|
|
60
|
+
export declare function createAuthCallback(args: {
|
|
61
|
+
instance: ApiRequestInstance;
|
|
62
|
+
getDistinctId: () => string;
|
|
63
|
+
getChannelScope: () => string | null;
|
|
64
|
+
getLiveClientId: LiveClientIdAccessor;
|
|
65
|
+
onIdentityChange: IdentityChangeHook;
|
|
66
|
+
}): (_: unknown, cb: (err: string | null, token: TokenRequest | null) => void) => Promise<void>;
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatController — orchestrates the chat widget.
|
|
3
|
+
*
|
|
4
|
+
* - Implements `LazyLoadedChatInterface` (the public SDK surface)
|
|
5
|
+
* - Owns the ChatStore (state)
|
|
6
|
+
* - Owns the AblyClient (realtime transport)
|
|
7
|
+
* - Performs HTTP calls (channel CRUD, message send, mark-read)
|
|
8
|
+
* - Mounts the Preact UI inside a Shadow DOM via `mountChatUI()`
|
|
9
|
+
*
|
|
10
|
+
* No DOM logic lives here beyond mount/unmount — components read signals
|
|
11
|
+
* directly and emit user intent back to the controller through props.
|
|
12
|
+
*
|
|
13
|
+
* See: docs/modules/chat/widget-architecture.md
|
|
14
|
+
*/
|
|
15
|
+
import { type ChatChannel, type ChatChannelSummary, type ChatConfig, type ChatWidgetView, type LazyLoadedChatInterface, type SendChatMessageContent, type SendChatMessageOptions } from "../../../utils/globals";
|
|
16
|
+
import type { VTilt } from "../../../vtilt";
|
|
17
|
+
import { type ConnectionCallback, type MessageCallback, type TypingCallback, type Unsubscribe } from "../types";
|
|
18
|
+
import { ChatWidgetRegistry, type ChatWidgetDefinition, type WidgetContext } from "../widget-registry";
|
|
19
|
+
import { ChatStore, DEFAULT_THEME } from "../store/chat-store";
|
|
20
|
+
export declare class ChatController implements LazyLoadedChatInterface {
|
|
21
|
+
private _instance;
|
|
22
|
+
private _config;
|
|
23
|
+
private _store;
|
|
24
|
+
private _ably;
|
|
25
|
+
private _ui;
|
|
26
|
+
private _registry;
|
|
27
|
+
private _messageCallbacks;
|
|
28
|
+
private _typingCallbacks;
|
|
29
|
+
private _connectionCallbacks;
|
|
30
|
+
private _lastConnectedNotified;
|
|
31
|
+
private _unsubscribeIdentity;
|
|
32
|
+
private _unsubscribeReset;
|
|
33
|
+
private _identityChangeInFlight;
|
|
34
|
+
private _typingIdleTimer;
|
|
35
|
+
private _isUserTyping;
|
|
36
|
+
private _lastTypingPublishAt;
|
|
37
|
+
private _lastTypingActivityAt;
|
|
38
|
+
private _pendingTypingIntent;
|
|
39
|
+
private _disposeRealtimeReadyWatcher;
|
|
40
|
+
private _isMarkingRead;
|
|
41
|
+
private _streamAbort;
|
|
42
|
+
private _streamingMessageId;
|
|
43
|
+
/** Local typing dot shown before the HTTP response headers arrive. */
|
|
44
|
+
private _optimisticAiTyping;
|
|
45
|
+
private _openedAt;
|
|
46
|
+
private _disposeConnectionWatcher;
|
|
47
|
+
private _outbox;
|
|
48
|
+
/** Distinct id the active outbox storage key was opened for. */
|
|
49
|
+
private _outboxDistinctId;
|
|
50
|
+
private _drainInFlight;
|
|
51
|
+
private _drainTimer;
|
|
52
|
+
private readonly _onOutboxOnline;
|
|
53
|
+
private readonly _onOutboxVisible;
|
|
54
|
+
constructor(instance: VTilt, config?: ChatConfig);
|
|
55
|
+
get isOpen(): boolean;
|
|
56
|
+
get isConnected(): boolean;
|
|
57
|
+
get isLoading(): boolean;
|
|
58
|
+
get unreadCount(): number;
|
|
59
|
+
get channel(): ChatChannel | null;
|
|
60
|
+
get channels(): ChatChannelSummary[];
|
|
61
|
+
get currentView(): ChatWidgetView;
|
|
62
|
+
/** Exposed so components can read the live store inside JSX. */
|
|
63
|
+
get store(): ChatStore;
|
|
64
|
+
/** Exposed so the widget slot can look up custom definitions. */
|
|
65
|
+
get widgets(): ChatWidgetRegistry;
|
|
66
|
+
get theme(): typeof DEFAULT_THEME;
|
|
67
|
+
get config(): ChatConfig;
|
|
68
|
+
open(): void;
|
|
69
|
+
close(): void;
|
|
70
|
+
/** Drop per-channel subs and close Ably — badge while closed uses Tier A REST. */
|
|
71
|
+
private _teardownRealtimeOnClose;
|
|
72
|
+
/**
|
|
73
|
+
* Tier A closed-badge: apply aggregate unread from `GET /api/chat/widget/unread`
|
|
74
|
+
* (called by `chat-wrapper` poll / visibility refresh).
|
|
75
|
+
*/
|
|
76
|
+
applyPolledUnreadCount(count: number): void;
|
|
77
|
+
toggle(): void;
|
|
78
|
+
private _bubbleExplicitShow;
|
|
79
|
+
show(): void;
|
|
80
|
+
hide(): void;
|
|
81
|
+
getChannels(): Promise<void>;
|
|
82
|
+
selectChannel(channelId: string): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Create a new conversation channel.
|
|
85
|
+
*
|
|
86
|
+
* `options.skipGreeting` tells the server not to seed the AI welcome
|
|
87
|
+
* message (used when the user is initiating the conversation via
|
|
88
|
+
* `sendChatMessage` and their own message will arrive immediately after).
|
|
89
|
+
*
|
|
90
|
+
* `options.preserveMessages` keeps any messages already in the store
|
|
91
|
+
* (e.g. an optimistic user temp message inserted before the create call)
|
|
92
|
+
* and merges them with the server response — server messages first, then
|
|
93
|
+
* any local `temp-` rows we still have. This is what powers the
|
|
94
|
+
* "show the user's message instantly while the channel is being
|
|
95
|
+
* created" UX path.
|
|
96
|
+
*/
|
|
97
|
+
createChannel(options?: {
|
|
98
|
+
skipGreeting?: boolean;
|
|
99
|
+
preserveMessages?: boolean;
|
|
100
|
+
}): Promise<void>;
|
|
101
|
+
goToChannelList(): void;
|
|
102
|
+
/** Abort the in-flight AI stream and remove its partial bubble. */
|
|
103
|
+
private _cancelActiveStream;
|
|
104
|
+
/**
|
|
105
|
+
* Remove AI bubbles trailing the last user message — used when a new send
|
|
106
|
+
* aborts an in-flight stream before `onStreamStart` registered its id.
|
|
107
|
+
*/
|
|
108
|
+
private _stripTrailingAiAfterLastUser;
|
|
109
|
+
/** Show the AI typing bubble while waiting for stream headers / first token. */
|
|
110
|
+
private _showOptimisticAiTyping;
|
|
111
|
+
private _clearOptimisticAiTyping;
|
|
112
|
+
sendMessage(content: SendChatMessageContent, options?: SendChatMessageOptions): Promise<void>;
|
|
113
|
+
/** Retry a failed outbound message (tap-to-retry in the bubble meta). */
|
|
114
|
+
retryFailedMessage(messageId: string): void;
|
|
115
|
+
markAsRead(): void;
|
|
116
|
+
/**
|
|
117
|
+
* Send a silent trigger to the messages API after a widget action so the
|
|
118
|
+
* AI follows up (e.g. acknowledges email collection) without creating a
|
|
119
|
+
* visible user message in the chat.
|
|
120
|
+
*/
|
|
121
|
+
triggerAIAfterWidgetAction(channelId: string): Promise<void>;
|
|
122
|
+
onMessage(callback: MessageCallback): Unsubscribe;
|
|
123
|
+
onTyping(callback: TypingCallback): Unsubscribe;
|
|
124
|
+
onConnectionChange(callback: ConnectionCallback): Unsubscribe;
|
|
125
|
+
updateConfig(config: ChatConfig): void;
|
|
126
|
+
private _applyBubbleLayout;
|
|
127
|
+
registerWidget(definition: ChatWidgetDefinition): void;
|
|
128
|
+
/**
|
|
129
|
+
* Called by `MessageInput` on each keystroke.
|
|
130
|
+
*
|
|
131
|
+
* Strategy (industry-standard for Slack/Intercom/Front):
|
|
132
|
+
* - Publish `true` once on the leading edge of a typing burst.
|
|
133
|
+
* - Refresh `true` every 4s while the user keeps typing so the
|
|
134
|
+
* dashboard's local timeout doesn't drop the indicator mid-burst.
|
|
135
|
+
* - Schedule a 5s idle timer that publishes `false` once the user
|
|
136
|
+
* stops. Each new keystroke resets the timer.
|
|
137
|
+
* - When realtime isn't attached (initial load, reconnect window),
|
|
138
|
+
* queue the intent and flush it when `realtimeReady` flips true,
|
|
139
|
+
* provided the activity is still recent (<3s old).
|
|
140
|
+
*/
|
|
141
|
+
notifyUserTyping(): void;
|
|
142
|
+
/**
|
|
143
|
+
* Called from `MessageInput` on send/blur, from `close()`, and from
|
|
144
|
+
* `destroy()`. Synchronously emits `false` so the dashboard's
|
|
145
|
+
* indicator clears immediately rather than waiting for the 5s idle
|
|
146
|
+
* timeout.
|
|
147
|
+
*/
|
|
148
|
+
stopUserTyping(): void;
|
|
149
|
+
private _scheduleTypingIdle;
|
|
150
|
+
destroy(): void;
|
|
151
|
+
/** Build the WidgetContext passed to custom widget renderers and actions. */
|
|
152
|
+
getWidgetContext(messageId: string, widgetType: string): WidgetContext;
|
|
153
|
+
/**
|
|
154
|
+
* Mark a widget as submitted in local message metadata so the next render
|
|
155
|
+
* shows the confirmation UI instead of the input form, then trigger an AI
|
|
156
|
+
* follow-up unless the widget is `escalate_to_human` (AI mode is off).
|
|
157
|
+
*/
|
|
158
|
+
markWidgetSubmittedAndFollowUp(messageId: string, widgetType: string, formData: Record<string, unknown>): void;
|
|
159
|
+
private _outboxStorageKey;
|
|
160
|
+
private _refreshOutboxKey;
|
|
161
|
+
private _markOutboxSending;
|
|
162
|
+
private _failOutboxMessage;
|
|
163
|
+
private _bindOutboxToChannel;
|
|
164
|
+
private _rememberLastCreatedChannel;
|
|
165
|
+
private _migrateOutboxFromDistinctId;
|
|
166
|
+
private _bindOutboxLifecycle;
|
|
167
|
+
private _unbindOutboxLifecycle;
|
|
168
|
+
private _enqueueOutbox;
|
|
169
|
+
private _markMessageDelivered;
|
|
170
|
+
private _scheduleDrainOutbox;
|
|
171
|
+
private _drainOutbox;
|
|
172
|
+
private _deliverOutboxEntry;
|
|
173
|
+
private _createChannelIdForOutbox;
|
|
174
|
+
private _postUserMessageSilent;
|
|
175
|
+
private _postUserMessage;
|
|
176
|
+
private get _distinctId();
|
|
177
|
+
private _stubChannelFromSummary;
|
|
178
|
+
private _handleIdentityChange;
|
|
179
|
+
/**
|
|
180
|
+
* Resolve once `identityChangeInFlight` becomes `false`. Used by
|
|
181
|
+
* `sendMessage` so a `vt.sendChatMessage()` call that arrives immediately
|
|
182
|
+
* after `vt.identify()` doesn't race the in-flight identity reset.
|
|
183
|
+
*/
|
|
184
|
+
private _waitForIdentityChange;
|
|
185
|
+
private _trackChatMessage;
|
|
186
|
+
private _autoMarkAsRead;
|
|
187
|
+
private _publishTyping;
|
|
188
|
+
/**
|
|
189
|
+
* Subscribe to `connectionState` changes and forward `connected`/
|
|
190
|
+
* `disconnected` transitions through the legacy `onConnectionChange`
|
|
191
|
+
* callback contract.
|
|
192
|
+
*/
|
|
193
|
+
private _watchConnectionState;
|
|
194
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local delivery status on optimistic user messages (not persisted server-side).
|
|
3
|
+
*/
|
|
4
|
+
import type { ChatStore } from "../store/chat-store";
|
|
5
|
+
export type MessageDeliveryStatus = "pending" | "sending" | "failed";
|
|
6
|
+
export declare function setMessageDeliveryStatus(store: ChatStore, messageId: string, status: MessageDeliveryStatus | undefined): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chronological ordering for chat messages.
|
|
3
|
+
*
|
|
4
|
+
* The server orders by `(created_at, id)`. The widget store must match
|
|
5
|
+
* that contract whenever messages are inserted out of band (streaming
|
|
6
|
+
* races, Ably echoes) so bubbles don't land in append order.
|
|
7
|
+
*/
|
|
8
|
+
import type { ChatMessage } from "../../../utils/globals";
|
|
9
|
+
/** Compare two messages the way Postgres orders inbox rows. */
|
|
10
|
+
export declare function compareMessagesChronologically(a: ChatMessage, b: ChatMessage): number;
|
|
11
|
+
export declare function sortMessagesChronologically(messages: ChatMessage[]): ChatMessage[];
|
|
12
|
+
export declare function insertMessageChronologically(messages: ChatMessage[], incoming: ChatMessage): ChatMessage[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI streaming response reader.
|
|
3
|
+
*
|
|
4
|
+
* Reads chunks from a `Response`'s body, batches store updates to one
|
|
5
|
+
* per animation frame, strips trailing incomplete HTML tags so partial
|
|
6
|
+
* flash, and parses widget blocks at completion.
|
|
7
|
+
*
|
|
8
|
+
* Pure with respect to the DOM — only writes into the store. The bubble
|
|
9
|
+
* keeps the same id from frame one: the server pre-creates the row and
|
|
10
|
+
* returns the persisted id via the `X-Vtilt-Message-Id` header, so the
|
|
11
|
+
* later Ably echo / DB-update merges into the same row with no remount.
|
|
12
|
+
* On older servers that don't send the header we fall back to the legacy
|
|
13
|
+
* `temp-ai-*` id and upsertMessage's id-swap behaviour.
|
|
14
|
+
*/
|
|
15
|
+
import type { ChatStore } from "../store/chat-store";
|
|
16
|
+
/** Header the server uses to publish the persisted AI message id up-front. */
|
|
17
|
+
export declare const AI_MESSAGE_ID_HEADER = "X-Vtilt-Message-Id";
|
|
18
|
+
/** Header the server uses to publish the persisted AI message timestamp. */
|
|
19
|
+
export declare const AI_MESSAGE_CREATED_AT_HEADER = "X-Vtilt-Message-Created-At";
|
|
20
|
+
/** Widget block marker regex: `<!--vtilt:widget:{...}-->` */
|
|
21
|
+
export declare const WIDGET_BLOCK_RE: RegExp;
|
|
22
|
+
/** Parse `<!--vtilt:widget:{...}-->` markers out of streamed content. */
|
|
23
|
+
export declare function parseWidgetBlocks(text: string): Array<{
|
|
24
|
+
type: string;
|
|
25
|
+
params: Record<string, unknown>;
|
|
26
|
+
}>;
|
|
27
|
+
interface StreamArgs {
|
|
28
|
+
store: ChatStore;
|
|
29
|
+
response: Response;
|
|
30
|
+
channelId: string;
|
|
31
|
+
tempId?: string;
|
|
32
|
+
/** When aborted, the reader stops and removes its partial bubble. */
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
/** Extra guard for superseded streams (new user send while still reading). */
|
|
35
|
+
isActive?: () => boolean;
|
|
36
|
+
onStreamStart?: (messageId: string) => void;
|
|
37
|
+
onComplete?: (messageId: string) => void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read an AI streaming response into the store. The bubble id is decided
|
|
41
|
+
* once at frame one (persisted id from `X-Vtilt-Message-Id` if the server
|
|
42
|
+
* provided it, otherwise a `temp-ai-*` fallback) and never changes, so
|
|
43
|
+
* the same `MessageBubble` instance receives content updates without
|
|
44
|
+
* re-keying.
|
|
45
|
+
*/
|
|
46
|
+
export declare function handleStreamingResponse({ store, response, channelId, tempId, signal, isActive, onStreamStart, onComplete, }: StreamArgs): Promise<void>;
|
|
47
|
+
/** Remove a single message by id (used to clean up failed temp messages). */
|
|
48
|
+
export declare function removeMessageById(store: ChatStore, id: string): void;
|
|
49
|
+
export {};
|