@v-tilt/browser 1.11.0 → 1.13.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.
Files changed (90) hide show
  1. package/dist/all-external-dependencies.js +1 -1
  2. package/dist/all-external-dependencies.js.map +1 -1
  3. package/dist/array.full.js +1 -1
  4. package/dist/array.full.js.map +1 -1
  5. package/dist/array.js +1 -1
  6. package/dist/array.js.map +1 -1
  7. package/dist/array.no-external.js +1 -1
  8. package/dist/array.no-external.js.map +1 -1
  9. package/dist/autocapture-types.d.ts +17 -0
  10. package/dist/autocapture-utils.d.ts +24 -1
  11. package/dist/autocapture.d.ts +94 -5
  12. package/dist/chat.js +1 -1
  13. package/dist/chat.js.map +1 -1
  14. package/dist/config.d.ts +8 -1
  15. package/dist/constants.d.ts +19 -13
  16. package/dist/core/capture.d.ts +15 -5
  17. package/dist/core/config-utils.d.ts +15 -0
  18. package/dist/core/consent.d.ts +62 -0
  19. package/dist/core/event-buffer.d.ts +60 -0
  20. package/dist/core/fb-cookies.d.ts +32 -0
  21. package/dist/core/feature-manager.d.ts +61 -69
  22. package/dist/core/fifo-queue.d.ts +23 -0
  23. package/dist/core/identity.d.ts +23 -33
  24. package/dist/core/index.d.ts +7 -1
  25. package/dist/core/page-lifecycle.d.ts +41 -0
  26. package/dist/core/remote-config.d.ts +14 -17
  27. package/dist/extensions/chat/bubble-drag.d.ts +30 -0
  28. package/dist/extensions/chat/chat-api.d.ts +15 -0
  29. package/dist/extensions/chat/chat-styles.d.ts +27 -0
  30. package/dist/extensions/chat/chat-wrapper.d.ts +20 -145
  31. package/dist/extensions/chat/chat.d.ts +261 -14
  32. package/dist/extensions/chat/message-content-styles.d.ts +1 -0
  33. package/dist/extensions/chat/message-html.d.ts +6 -0
  34. package/dist/extensions/chat/message-markdown.d.ts +8 -0
  35. package/dist/extensions/chat/normalize-send-content.d.ts +24 -0
  36. package/dist/extensions/chat/types.d.ts +19 -57
  37. package/dist/extensions/chat/widget-registry.d.ts +53 -0
  38. package/dist/extensions/chat/widgets/collect-email.d.ts +6 -0
  39. package/dist/extensions/chat/widgets/escalate-to-human.d.ts +6 -0
  40. package/dist/extensions/ga4-proxy.d.ts +59 -0
  41. package/dist/extensions/google-tag-gateway/consent-bridge.d.ts +27 -0
  42. package/dist/extensions/google-tag-gateway/enhanced-conversions.d.ts +35 -0
  43. package/dist/extensions/google-tag-gateway/event-bridge.d.ts +74 -0
  44. package/dist/extensions/google-tag-gateway/google-tag-gateway.d.ts +95 -0
  45. package/dist/extensions/google-tag-gateway/gtag-loader.d.ts +85 -0
  46. package/dist/extensions/google-tag-gateway/index.d.ts +7 -0
  47. package/dist/extensions/google-tag-gateway/normalize.d.ts +28 -0
  48. package/dist/extensions/google-tag-gateway/public-api.d.ts +23 -0
  49. package/dist/extensions/history-autocapture.d.ts +2 -2
  50. package/dist/extensions/replay/index.d.ts +1 -1
  51. package/dist/extensions/replay/session-recording-utils.d.ts +13 -43
  52. package/dist/extensions/replay/session-recording-wrapper.d.ts +10 -66
  53. package/dist/extensions/replay/session-recording.d.ts +53 -1
  54. package/dist/extensions/replay/types.d.ts +6 -1
  55. package/dist/extensions/web-vitals/web-vitals-manager.d.ts +14 -43
  56. package/dist/external-scripts-loader.js +1 -1
  57. package/dist/external-scripts-loader.js.map +1 -1
  58. package/dist/feature.d.ts +54 -172
  59. package/dist/main.js +1 -1
  60. package/dist/main.js.map +1 -1
  61. package/dist/module.d.ts +728 -753
  62. package/dist/module.js +1 -1
  63. package/dist/module.js.map +1 -1
  64. package/dist/module.no-external.d.ts +728 -753
  65. package/dist/module.no-external.js +1 -1
  66. package/dist/module.no-external.js.map +1 -1
  67. package/dist/rate-limiter.d.ts +0 -1
  68. package/dist/recorder.js +1 -1
  69. package/dist/recorder.js.map +1 -1
  70. package/dist/request.d.ts +34 -20
  71. package/dist/scroll-depth-tracker.d.ts +42 -0
  72. package/dist/server.d.ts +114 -0
  73. package/dist/server.js +1 -1
  74. package/dist/server.js.map +1 -1
  75. package/dist/session.d.ts +12 -0
  76. package/dist/types.d.ts +204 -9
  77. package/dist/user-manager.d.ts +26 -52
  78. package/dist/utils/base64.d.ts +30 -0
  79. package/dist/utils/bot-detection.d.ts +28 -0
  80. package/dist/utils/endpoint-url.d.ts +36 -0
  81. package/dist/utils/event-emitter.d.ts +1 -0
  82. package/dist/utils/globals.d.ts +71 -2
  83. package/dist/utils/index.d.ts +20 -5
  84. package/dist/utils/logger.d.ts +66 -0
  85. package/dist/utils/request-utils.d.ts +5 -0
  86. package/dist/utils/safewrap.d.ts +6 -1
  87. package/dist/utils/transport-health.d.ts +55 -0
  88. package/dist/vtilt.d.ts +85 -25
  89. package/dist/web-vitals.js.map +1 -1
  90. package/package.json +71 -66
@@ -2,8 +2,9 @@
2
2
  * Chat Widget - Lazy loaded chat implementation using Ably for real-time messaging.
3
3
  */
4
4
  import type { VTilt } from "../../vtilt";
5
- import { type ChatConfig, type ChatChannel, type ChatChannelSummary, type ChatWidgetView, type LazyLoadedChatInterface } from "../../utils/globals";
5
+ import { type ChatConfig, type ChatChannel, type ChatChannelSummary, type ChatWidgetView, type LazyLoadedChatInterface, type SendChatMessageContent, type SendChatMessageOptions } from "../../utils/globals";
6
6
  import { type MessageCallback, type TypingCallback, type ConnectionCallback, type Unsubscribe } from "./types";
7
+ import { type WidgetContext } from "./widget-registry";
7
8
  export declare class LazyLoadedChat implements LazyLoadedChatInterface {
8
9
  private _instance;
9
10
  private _config;
@@ -11,10 +12,19 @@ export declare class LazyLoadedChat implements LazyLoadedChatInterface {
11
12
  private _container;
12
13
  private _widget;
13
14
  private _bubble;
15
+ private _bubbleDragHandle;
16
+ private _chatTheme;
17
+ private _widgetResizeCleanup;
14
18
  private _ably;
19
+ private _notificationsChannel;
15
20
  private _ablyChannel;
16
21
  private _typingChannel;
17
22
  private _connectionState;
23
+ private _ablyProjectId;
24
+ /** Channel id included in the last per-channel Ably token authorize. */
25
+ private _realtimeChannelId;
26
+ /** Channel id we are connecting to (select/create in flight). */
27
+ private _pendingRealtimeChannelId;
18
28
  private _messageCallbacks;
19
29
  private _typingCallbacks;
20
30
  private _connectionCallbacks;
@@ -22,6 +32,18 @@ export declare class LazyLoadedChat implements LazyLoadedChatInterface {
22
32
  private _isUserTyping;
23
33
  private _initialUserReadAt;
24
34
  private _isMarkingRead;
35
+ private _widgetRegistry;
36
+ /** Tracks whether show() was explicitly called (vs open() temporarily revealing the container). */
37
+ private _bubbleExplicitShow;
38
+ private _lastConnectedDistinctId;
39
+ private _unsubscribeIdentity;
40
+ private _unsubscribeReset;
41
+ private _identityChangeInFlight;
42
+ private _userScrolledUp;
43
+ private _unreadNewMessagesCount;
44
+ private _messagesScrollListenerBound;
45
+ private _conversationListenersBound;
46
+ private _channelListListenersBound;
25
47
  constructor(instance: VTilt, config?: ChatConfig);
26
48
  get isOpen(): boolean;
27
49
  get isConnected(): boolean;
@@ -45,12 +67,38 @@ export declare class LazyLoadedChat implements LazyLoadedChatInterface {
45
67
  toggle(): void;
46
68
  show(): void;
47
69
  hide(): void;
70
+ /**
71
+ * Build a stub ChatChannel from a ChatChannelSummary so that
72
+ * `selectChannel` can populate state synchronously while the real
73
+ * `GET /channels/:id` response is in flight. The header reads `ai_mode`
74
+ * and `status` immediately; remaining fields are filled by the response.
75
+ */
76
+ private _stubChannelFromSummary;
77
+ /**
78
+ * Handle a vTilt identity change (anon → identified, or user → user).
79
+ *
80
+ * Ably enforces that the connection's `clientId` is immutable for the
81
+ * lifetime of a Realtime connection. Authorizing a token with a different
82
+ * `clientId` puts the connection into the terminal `failed` state and
83
+ * produces a 40102 error. The correct response is to fully close the
84
+ * existing client and create a new one with a token bound to the new id.
85
+ *
86
+ * This also clears per-identity state (channels, messages, unread count)
87
+ * since those belong to the previous user.
88
+ */
89
+ private _handleIdentityChange;
48
90
  getChannels(): Promise<void>;
49
91
  selectChannel(channelId: string): Promise<void>;
50
92
  createChannel(): Promise<void>;
51
- goToChannelList(): void;
52
- sendMessage(content: string): Promise<void>;
93
+ goToChannelList(): Promise<void>;
94
+ sendMessage(content: SendChatMessageContent, options?: SendChatMessageOptions): Promise<void>;
53
95
  markAsRead(): void;
96
+ /**
97
+ * Send a silent trigger to the messages API after a widget action completes.
98
+ * This makes the AI respond to the action (e.g., acknowledge email collection)
99
+ * without creating a visible user message in the chat.
100
+ */
101
+ private _triggerAIAfterWidgetAction;
54
102
  private _autoMarkAsRead;
55
103
  private _isMessageReadByUser;
56
104
  /**
@@ -62,32 +110,188 @@ export declare class LazyLoadedChat implements LazyLoadedChatInterface {
62
110
  onTyping(callback: TypingCallback): Unsubscribe;
63
111
  onConnectionChange(callback: ConnectionCallback): Unsubscribe;
64
112
  destroy(): void;
65
- private _connectRealtime;
66
- private _disconnectRealtime;
113
+ /**
114
+ * Ensure Ably client is connected and subscribed to the project notifications channel.
115
+ * Called when the widget opens. Safe to call multiple times.
116
+ */
117
+ private _ensureAblyConnected;
118
+ /**
119
+ * Request an Ably token for the current connection scope.
120
+ * Pass channelId when subscribing to a conversation; omit on channel list.
121
+ */
122
+ private _refreshAblyToken;
123
+ /**
124
+ * Subscribe to per-channel messages and typing for a specific conversation.
125
+ *
126
+ * Attach is async; the user may close the widget or switch to a different
127
+ * channel while we are still awaiting `attach()`. In that case
128
+ * `_disconnectChannel()` runs concurrently — it nulls `this._ablyChannel`
129
+ * and calls `detach()`, which makes Ably reject the pending `attach()` with
130
+ * "Attach request superseded by a subsequent detach request". We treat
131
+ * that race as expected: bail out silently instead of logging an error.
132
+ */
133
+ private _connectChannel;
134
+ /**
135
+ * Unsubscribe, detach, and release per-channel Ably channels.
136
+ * Ably only allows release() when the channel is initialized, detached, or
137
+ * failed — not when attached. So we detach first, then release.
138
+ * Releasing removes the channel from the Ably internal registry so a
139
+ * subsequent auth.authorize() with narrower capabilities won't try to
140
+ * reattach it and fail with "Channel denied access."
141
+ */
142
+ private _disconnectChannel;
143
+ /**
144
+ * Fully disconnect from Ably — unsubscribe everything and close the connection.
145
+ */
146
+ private _disconnectAll;
147
+ /**
148
+ * Handle project-level notifications (new channels, updates, closes).
149
+ * Updates channel list and badge count in real time.
150
+ */
151
+ private _handleNotification;
67
152
  private _handleNewMessage;
68
153
  private _handleTypingEvent;
69
154
  private _handleReadCursorEvent;
70
155
  private _notifyConnectionChange;
71
156
  private _createUI;
72
157
  private _attachEventListeners;
158
+ /**
159
+ * Mark a widget as submitted in local message metadata.
160
+ * Called after a successful widget action — _renderWidgets() uses this
161
+ * to show the confirmation UI instead of the input form.
162
+ */
163
+ private _markWidgetSubmitted;
73
164
  private _handleUserTyping;
74
165
  private _sendTypingIndicator;
75
166
  private _handleSend;
76
167
  private _updateUI;
168
+ private _isMobile;
169
+ private _containerBaseStyle;
170
+ private _savedTransform;
171
+ private _isMobileFullscreen;
172
+ /**
173
+ * Mobile fullscreen: make the *container* fullscreen instead of the widget.
174
+ * Using position:fixed on the widget is unreliable because the container
175
+ * may have a CSS transform (from drag) which creates a new containing block.
176
+ * The container is a direct child of <body> so position:fixed is always safe.
177
+ *
178
+ * Both elements get their full style replaced atomically via setAttribute.
179
+ * Display is set by _updateUI after this call.
180
+ */
181
+ private _applyMobileFullscreen;
182
+ private _removeMobileFullscreen;
183
+ private _scrollY;
184
+ private _lockBodyScroll;
185
+ /**
186
+ * Ensure the opened widget:
187
+ * 1. Covers the bubble (anchored at container bottom-right) for UI consistency.
188
+ * 2. Stays fully inside the viewport by shifting its position when near edges (no size change).
189
+ *
190
+ * On mobile the CSS media query handles fullscreen positioning, so this is a no-op.
191
+ */
192
+ private _constrainWidgetToViewport;
77
193
  private _updateHeader;
78
- private _getChannelListHTML;
194
+ /**
195
+ * Render the channel-list view with mode-aware diffing.
196
+ *
197
+ * Three modes share a single content container so the click delegation
198
+ * stays bound across them:
199
+ * - empty: no channels and not loading → welcome / CTA screen
200
+ * - skeleton: no channels and loading → shimmer placeholders
201
+ * - populated: 1+ channels → scaffold + diff items by id
202
+ *
203
+ * Only the populated mode preserves item DOM identity; the other two are
204
+ * rare transient states where a full innerHTML write is cheap.
205
+ */
206
+ private _renderChannelList;
207
+ /**
208
+ * Wire a single delegated click handler on the list container so we don't
209
+ * re-attach per-item listeners on every render.
210
+ */
211
+ private _setupChannelListDelegation;
212
+ private _createChannelItemNode;
213
+ private _updateChannelItemNode;
214
+ /**
215
+ * Compact fingerprint used to decide whether a channel-item DOM node needs
216
+ * an update. Only includes fields that the rendered HTML depends on.
217
+ */
218
+ private _channelItemFingerprint;
219
+ private _getEmptyListHTML;
220
+ private _getListScaffoldHTML;
221
+ private _getListSkeletonHTML;
222
+ /**
223
+ * Build a skeleton bubble that mirrors the shape of a real chat message
224
+ * from `_getMessageHTML`: same avatar size (32×32), same bubble padding
225
+ * (12px 16px), same asymmetric border-radius (20/20/20/4 incoming,
226
+ * 20/20/4/20 outgoing), and a meta line underneath sized like the
227
+ * "Sender · 12:34" footer. The shimmer fills the bubble interior — not
228
+ * floating lines — so the placeholder reads as a "loading message"
229
+ * rather than a generic content blob. Width varies per call so the
230
+ * stack of bubbles has visual rhythm instead of looking like a parade
231
+ * of identical rectangles.
232
+ */
233
+ private _getSkeletonBubbleHTML;
234
+ /**
235
+ * Skeleton placeholder shown while `selectChannel` is loading the
236
+ * messages of an existing conversation. Three alternating bubbles
237
+ * (incoming → outgoing → incoming) imply history is on its way.
238
+ * Bubble widths vary to avoid the "fake repeated rectangles" look.
239
+ */
240
+ private _getMessageSkeletonHTML;
241
+ /**
242
+ * New-conversation loader shown while `createChannel` (or
243
+ * `sendMessage({channel: 'new'})`) is in flight. A single incoming
244
+ * bubble using *exactly* the same shape, padding, border-radius, and
245
+ * avatar treatment as a real incoming message from `_getMessageHTML`
246
+ * — so when the real greeting arrives it morphs in place rather than
247
+ * snapping to a new layout. The avatar uses the live AI/Support color
248
+ * + icon (identity is known from `_config.aiMode`), and the bubble
249
+ * carries three animated typing dots instead of text, signalling
250
+ * "the assistant is preparing your greeting."
251
+ */
252
+ private _getNewChannelLoaderHTML;
79
253
  private _getChannelItemHTML;
80
254
  private _getConversationHTML;
81
- private _attachChannelListListeners;
82
255
  private _attachConversationListeners;
83
- private _escapeHtml;
84
256
  private _formatRelativeTime;
257
+ /**
258
+ * Render messages with a key-based diff.
259
+ *
260
+ * - Each message node carries `data-msg-key="msg:<id>"` and a fingerprint;
261
+ * if both match what we already have, the node is left untouched.
262
+ * - The "New" divider lives as a sibling node with its own key so it
263
+ * participates in the same diff (no flicker when it moves).
264
+ * - Scroll position is preserved: we only snap to the bottom when the user
265
+ * was already near the bottom or just sent their own message. Otherwise
266
+ * we surface a "↓ N new messages" pill instead of yanking them down.
267
+ * - Newly-inserted message nodes get the `vtilt-msg-enter` class for a
268
+ * subtle fade-in animation. The initial bulk render skips animation.
269
+ */
85
270
  private _renderMessages;
86
- private _getContainerStyles;
87
- private _getBubbleStyles;
88
- private _getBubbleHTML;
89
- private _getWidgetStyles;
90
- private _getWidgetHTML;
271
+ /**
272
+ * Build the "New" divider node (rendered above the first unread agent/AI
273
+ * message when scrolled into view).
274
+ */
275
+ private _createMessageDividerNode;
276
+ /**
277
+ * Build a single message DOM node from the message model.
278
+ */
279
+ private _createMessageNode;
280
+ /**
281
+ * Compact fingerprint used to decide whether a message node needs an update.
282
+ * Streaming AI responses update the content field many times — re-rendering
283
+ * the same DOM lets the cursor and layout stay stable for the user.
284
+ */
285
+ private _messageFingerprint;
286
+ /**
287
+ * Wire (once per container) a scroll listener that tracks whether the user
288
+ * is reading history above the latest message. Used to suppress auto-scroll
289
+ * and to dismiss the "new messages" pill when the user scrolls back down.
290
+ */
291
+ private _setupMessagesScrollListener;
292
+ private _isNearBottom;
293
+ private _showNewMessagesPill;
294
+ private _hideNewMessagesPill;
91
295
  private _getMessageHTML;
92
296
  private _isMessageReadByAgent;
93
297
  /**
@@ -99,8 +303,51 @@ export declare class LazyLoadedChat implements LazyLoadedChatInterface {
99
303
  * Remove temporary message by ID
100
304
  */
101
305
  private _removeTempMessage;
102
- private _apiRequest;
103
306
  private _getTimeOpen;
307
+ /**
308
+ * Bubble body for any message — plain escaped text or sanitized rich HTML,
309
+ * using the same rules for user (html/markdown) and agent/AI messages.
310
+ */
311
+ private _renderMessageContent;
312
+ private _renderMessageHtml;
313
+ /**
314
+ * Render widgets for a message. Widget data sources (checked in order):
315
+ * 1. metadata._widgets -- set during streaming (temp messages)
316
+ * 2. metadata.widgets -- persisted in DB (loaded on refresh, received via Ably)
317
+ * 3. Content markers -- fallback for legacy messages with <!--vtilt:widget:...--> in content
318
+ *
319
+ * If a widget has submitted=true in metadata, render the confirmation instead of the input.
320
+ */
321
+ private _renderWidgets;
322
+ /**
323
+ * Build WidgetContext for widget renderers.
324
+ */
325
+ private _getWidgetContext;
326
+ /**
327
+ * Extract widget blocks from streamed text, strip them, and return parsed widgets.
328
+ */
329
+ private _parseWidgetBlocks;
330
+ /**
331
+ * Public method for SDK consumers to register custom widgets.
332
+ */
333
+ registerWidget(definition: {
334
+ type: string;
335
+ toolDescription: string;
336
+ parameters: Record<string, {
337
+ type: string;
338
+ description: string;
339
+ }>;
340
+ render: (params: Record<string, unknown>, context: WidgetContext) => string;
341
+ onAction: (action: string, data: Record<string, unknown>, context: WidgetContext) => Promise<{
342
+ replaceHTML?: string;
343
+ sendMessage?: string;
344
+ }>;
345
+ }): void;
104
346
  private _escapeHTML;
347
+ /**
348
+ * Get a human-readable preview for a message (for channel list).
349
+ * Falls back to widget labels if content is empty (tool-only messages).
350
+ */
351
+ private _getMessagePreview;
105
352
  private _formatTime;
106
353
  }
@@ -0,0 +1 @@
1
+ export declare const CHAT_MESSAGE_CONTENT_CSS = "\n #vtilt-chat-widget .vtilt-md {\n max-width: 100%;\n overflow-wrap: anywhere;\n word-break: break-word;\n font-size: inherit;\n line-height: 1.45;\n }\n #vtilt-chat-widget .vtilt-md * {\n box-sizing: border-box;\n }\n #vtilt-chat-widget .vtilt-md :is(p, h1, h2, h3, h4, h5, h6, ul, ol, blockquote, pre) {\n margin: 0;\n }\n #vtilt-chat-widget .vtilt-md p { margin: 0 0 0.5em 0; }\n #vtilt-chat-widget .vtilt-md p:last-child { margin-bottom: 0; }\n #vtilt-chat-widget .vtilt-md :is(b, strong) { font-weight: 600; }\n #vtilt-chat-widget .vtilt-md :is(i, em) { font-style: italic; }\n #vtilt-chat-widget .vtilt-md u { text-decoration: underline; }\n #vtilt-chat-widget .vtilt-md :is(h1, h2, h3, h4, h5, h6) {\n font-size: 1em;\n font-weight: 600;\n margin: 0.65em 0 0.35em 0;\n line-height: 1.3;\n }\n #vtilt-chat-widget .vtilt-md :is(h1, h2, h3):first-child { margin-top: 0.15em; }\n\n /* Lists \u2014 flex rows + ::before bullets (beats host list-item / ::marker resets) */\n #vtilt-chat-widget .vtilt-md :is(ul, ol) {\n list-style: none !important;\n margin: 0.5em 0 !important;\n padding: 0 0 0 0.35em !important;\n }\n #vtilt-chat-widget .vtilt-md li {\n display: flex !important;\n flex-direction: row !important;\n flex-wrap: wrap !important;\n align-items: flex-start !important;\n gap: 0.35em !important;\n margin: 0.2em 0 !important;\n padding: 0 !important;\n line-height: 1.45;\n list-style: none !important;\n list-style-type: none !important;\n }\n #vtilt-chat-widget .vtilt-md li::marker {\n content: \"\" !important;\n font-size: 0 !important;\n color: transparent !important;\n }\n #vtilt-chat-widget .vtilt-md li::-webkit-list-marker {\n display: none !important;\n }\n #vtilt-chat-widget .vtilt-md ul > li::before {\n content: \"\u2022\" !important;\n display: inline-block !important;\n position: static !important;\n flex: 0 0 auto;\n line-height: 1.45;\n }\n #vtilt-chat-widget .vtilt-md ul ul > li::before {\n content: \"\u25E6\" !important;\n }\n #vtilt-chat-widget .vtilt-md ol { counter-reset: vtilt-ol; }\n #vtilt-chat-widget .vtilt-md ol > li { counter-increment: vtilt-ol; }\n #vtilt-chat-widget .vtilt-md ol > li::before {\n content: counter(vtilt-ol) \".\" !important;\n display: inline-block !important;\n position: static !important;\n flex: 0 0 auto;\n min-width: 1.1em;\n line-height: 1.45;\n }\n #vtilt-chat-widget .vtilt-md ol ol { counter-reset: vtilt-ol; }\n #vtilt-chat-widget .vtilt-md li > :is(ul, ol) {\n flex: 1 0 100% !important;\n width: 100% !important;\n margin: 0.25em 0 0 0 !important;\n padding-left: 0.75em !important;\n }\n\n #vtilt-chat-widget .vtilt-md code {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n font-size: 0.9em;\n background: rgba(0, 0, 0, 0.06);\n padding: 0.1em 0.35em;\n border-radius: 4px;\n }\n #vtilt-chat-widget .vtilt-md pre {\n margin: 0.5em 0;\n padding: 0.5em 0.65em;\n border-radius: 6px;\n background: rgba(0, 0, 0, 0.06);\n overflow-x: auto;\n white-space: pre-wrap;\n }\n #vtilt-chat-widget .vtilt-md pre code { background: none; padding: 0; }\n #vtilt-chat-widget .vtilt-md blockquote {\n margin: 0.5em 0;\n padding-left: 0.75em;\n border-left: 3px solid rgba(0, 0, 0, 0.15);\n }\n #vtilt-chat-widget .vtilt-md a { color: inherit; text-decoration: underline; }\n #vtilt-chat-widget .vtilt-md br { line-height: inherit; }\n\n /* Widget visitor bubble (brand color) */\n #vtilt-chat-widget .vtilt-user-md code { background: rgba(255, 255, 255, 0.2); }\n #vtilt-chat-widget .vtilt-user-md pre { background: rgba(255, 255, 255, 0.15); }\n #vtilt-chat-widget .vtilt-user-md pre code { background: none; }\n #vtilt-chat-widget .vtilt-user-md blockquote { border-left-color: rgba(255, 255, 255, 0.35); }\n";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Convert stored message content to safe HTML for bubble rendering.
3
+ */
4
+ export declare function formatMessageBodyHtml(text: string, contentType: "text" | "html" | "attachment", doc: Document | null): string;
5
+ /** Plain-text preview for channel list (strips widgets and HTML). */
6
+ export declare function plainTextMessagePreview(content: string, maxLength?: number): string;
@@ -0,0 +1,8 @@
1
+ /** Detect content that already contains HTML block-level tags. */
2
+ export declare const HTML_BLOCK_TAG_RE: RegExp;
3
+ /**
4
+ * Best-effort markdown → HTML. Returns input unchanged when it already looks like HTML.
5
+ */
6
+ export declare function markdownToHtml(src: string): string;
7
+ /** Whether markdown conversion should run before sanitization. */
8
+ export declare function shouldUseMarkdownPipeline(contentType: "text" | "html" | "attachment", cleanText: string): boolean;
@@ -0,0 +1,24 @@
1
+ import type { SendChatMessageContent, SendChatMessageOptions } from "../../utils/globals";
2
+ import type { ContentType } from "./types";
3
+ /** Metadata key for markdown stored as `content_type: text`. Must match dashboard `message-rendering.ts`. */
4
+ export declare const RICH_FORMAT_METADATA_KEY = "rich_format";
5
+ export declare const RICH_FORMAT_MARKDOWN = "markdown";
6
+ export interface NormalizedOutgoingMessage {
7
+ content: string;
8
+ content_type: ContentType;
9
+ metadata: Record<string, unknown>;
10
+ }
11
+ export declare function isRichUserMessage(message: {
12
+ content_type: string;
13
+ metadata?: Record<string, unknown>;
14
+ }): boolean;
15
+ /** Whether a stored message should use the rich HTML renderer in the widget. */
16
+ export declare function messageUsesRichRendering(message: {
17
+ sender_type: string;
18
+ content_type: string;
19
+ metadata?: Record<string, unknown>;
20
+ }): boolean;
21
+ /**
22
+ * Normalize integrator input into stored message fields.
23
+ */
24
+ export declare function normalizeSendChatMessageContent(input: SendChatMessageContent, options?: SendChatMessageOptions): NormalizedOutgoingMessage | null;
@@ -4,7 +4,9 @@
4
4
  * Type definitions for the chat widget extension.
5
5
  * Core types are re-exported from globals.ts for consistency.
6
6
  */
7
- export type { ChatMessage, ChatChannel, ChatChannelSummary, ChatConfig, ChatTheme, ChatWidgetView, LazyLoadedChatInterface, } from "../../utils/globals";
7
+ export type { ChatMessage, ChatChannel, ChatChannelSummary, ChatConfig, ChatTheme, ChatWidgetView, LazyLoadedChatInterface, SendChatMessageContent, SendChatMessageOptions, } from "../../utils/globals";
8
+ export { messageUsesRichRendering, normalizeSendChatMessageContent, } from "./normalize-send-content";
9
+ export type { NormalizedOutgoingMessage } from "./normalize-send-content";
8
10
  /**
9
11
  * Sender types for chat messages
10
12
  */
@@ -45,75 +47,35 @@ export interface PresenceStatus {
45
47
  current_page_url?: string;
46
48
  }
47
49
  /**
48
- * Chat event names for analytics tracking
50
+ * Normalized channel event names for analytics tracking
49
51
  *
50
52
  * Message events are tracked client-side via capture() for full enrichment
51
- * (person properties, geo, device, referrer, etc.).
53
+ * (session, page URL, device, person properties, etc.).
52
54
  * Widget UI events (open/close) are exposed via callbacks in ChatConfig.
53
55
  *
54
- * All chat message events use a single event type ($chat_message) with properties:
55
- * - $sender_type: 'user' | 'ai' | 'agent'
56
- * - $channel_id: Chat channel ID
57
- * - $message_id: Message ID
56
+ * All channel message events use $channel_message with normalized properties:
57
+ * - $channel_type: channel discriminator ('chat', 'email', 'sms')
58
+ * - $conversation_id: conversation identifier
59
+ * - $message_id: message identifier
60
+ * - $direction: 'inbound' | 'outbound'
61
+ * - $sender_type: 'user' | 'ai' | 'agent' | 'system'
62
+ * - $content_preview: first 100 chars of content
58
63
  *
59
64
  * Tracking is gated by remote config:
60
65
  * - chatTracking.trackUserMessages → captures when $sender_type=user
61
66
  * - chatTracking.trackAgentMessages → captures when $sender_type=ai|agent
62
67
  */
63
- export declare const CHAT_EVENTS: {
64
- /** Tracked client-side for all messages (user, AI, agent) */
65
- readonly MESSAGE: "$chat_message";
68
+ export declare const CHANNEL_EVENTS: {
69
+ readonly MESSAGE: "$channel_message";
66
70
  };
67
- /**
68
- * Chat event properties for $chat_message (server-side)
69
- */
70
- export interface ChatMessageEventProperties {
71
- $channel_id: string;
72
- $message_id: string;
73
- $sender_type: SenderType;
74
- $content_preview: string;
75
- $ai_mode: boolean;
76
- $word_count: number;
77
- }
78
- /**
79
- * @deprecated Use ChatMessageEventProperties instead
80
- */
81
- export interface ChatMessageSentEventProperties {
82
- $channel_id: string;
71
+ export interface ChannelMessageEventProperties {
72
+ $channel_type: string;
73
+ $conversation_id: string;
83
74
  $message_id: string;
84
- $content_preview: string;
75
+ $direction: "inbound" | "outbound";
85
76
  $sender_type: SenderType;
86
- $ai_mode: boolean;
87
- $word_count: number;
88
- }
89
- /**
90
- * @deprecated Use ChatMessageEventProperties instead
91
- */
92
- export interface ChatMessageReceivedEventProperties {
93
- $channel_id: string;
94
- $message_id: string;
95
77
  $content_preview: string;
96
- $sender_type: SenderType;
97
- $response_time_ms?: number;
98
- }
99
- /**
100
- * Chat event properties for $chat_started
101
- */
102
- export interface ChatStartedEventProperties {
103
- $channel_id: string;
104
- $initiated_by: "user" | "agent";
105
- $ai_mode: boolean;
106
- }
107
- /**
108
- * Chat event properties for $chat_closed
109
- */
110
- export interface ChatClosedEventProperties {
111
- $channel_id: string;
112
- $resolved_by: "agent" | "user" | "system" | "auto";
113
- $resolution: "resolved" | "unresolved" | "abandoned";
114
- $message_count: number;
115
- $duration_seconds: number;
116
- $ai_only: boolean;
78
+ $ai_mode?: boolean;
117
79
  }
118
80
  /**
119
81
  * Internal widget state
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Chat Widget Registry - Extensible widget system for AI-triggered UI.
3
+ * Tool calls from the AI are mapped to registered widgets; each widget provides render() and onAction().
4
+ */
5
+ export interface WidgetContext {
6
+ channelId: string;
7
+ distinctId: string;
8
+ primaryColor: string;
9
+ /** @deprecated Use buildEndpointUrl() instead */
10
+ apiBase: string;
11
+ token: string;
12
+ /** The ID of the message containing this widget */
13
+ messageId: string;
14
+ /** Build a full URL for a given endpoint path, respecting api_host */
15
+ buildEndpointUrl(path: string): string;
16
+ }
17
+ export interface WidgetActionResult {
18
+ /** Whether the action succeeded — drives metadata-based re-render */
19
+ success?: boolean;
20
+ /** @deprecated Use metadata-driven rendering instead. Kept for custom widget compat. */
21
+ replaceHTML?: string;
22
+ }
23
+ export interface ChatWidgetDefinition {
24
+ /** Unique type identifier, e.g. "collect_email", "schedule_meeting" */
25
+ type: string;
26
+ /** AI tool description (sent to LLM so it knows when to invoke) */
27
+ toolDescription: string;
28
+ /** JSON schema shape for the tool parameters */
29
+ parameters: Record<string, {
30
+ type: string;
31
+ description: string;
32
+ }>;
33
+ /** Returns an HTML string to render inline in the chat bubble */
34
+ render(params: Record<string, unknown>, context: WidgetContext): string;
35
+ /** Called when the widget form is submitted or button clicked */
36
+ onAction(action: string, data: Record<string, unknown>, context: WidgetContext): Promise<WidgetActionResult>;
37
+ }
38
+ export declare class ChatWidgetRegistry {
39
+ private _widgets;
40
+ register(definition: ChatWidgetDefinition): void;
41
+ get(type: string): ChatWidgetDefinition | undefined;
42
+ getAll(): ChatWidgetDefinition[];
43
+ /**
44
+ * Build tool definitions for the AI from all registered widgets.
45
+ */
46
+ getToolDefinitions(): Record<string, {
47
+ description: string;
48
+ parameters: Record<string, {
49
+ type: string;
50
+ description: string;
51
+ }>;
52
+ }>;
53
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Built-in collect_email widget: renders name + email inputs, POSTs to widget actions API.
3
+ * UI state is metadata-driven — submitted state is rendered by _renderWidgets().
4
+ */
5
+ import type { ChatWidgetDefinition } from "../widget-registry";
6
+ export declare const collectEmailWidget: ChatWidgetDefinition;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Built-in escalate_to_human widget: shows a "Connect with a human" button.
3
+ * UI state is metadata-driven — submitted state is rendered by _renderWidgets().
4
+ */
5
+ import type { ChatWidgetDefinition } from "../widget-registry";
6
+ export declare const escalateToHumanWidget: ChatWidgetDefinition;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * GA4 Proxy Feature
3
+ *
4
+ * Automatically configures gtag.js to send through vTilt's domain.
5
+ * When enabled, either reconfigures an existing gtag.js or loads a new one
6
+ * with server_container_url pointing to vTilt's proxy routes.
7
+ *
8
+ * Config sources (init config takes priority over remote config):
9
+ * vt.init({ ga4: { measurementId: 'G-XXX' } })
10
+ * /api/decide → { ga4: { measurementId: 'G-XXX' } }
11
+ */
12
+ import type { VTiltConfig } from "../types";
13
+ import type { Feature, FeatureConfig } from "../feature";
14
+ export interface GA4ProxyConfig extends FeatureConfig {
15
+ measurementId?: string;
16
+ debugMode?: boolean;
17
+ }
18
+ export declare class GA4Proxy implements Feature {
19
+ readonly name = "GA4Proxy";
20
+ private _instance;
21
+ private _config;
22
+ private _isStarted;
23
+ private _pendingGtagCallbacks;
24
+ /** Fires once both client_id and session_id gtag callbacks complete. */
25
+ onIdentityResolved?: () => void;
26
+ constructor(instance: {
27
+ getConfig(): VTiltConfig;
28
+ }, config?: GA4ProxyConfig);
29
+ static extractConfig(config: VTiltConfig): GA4ProxyConfig;
30
+ get isEnabled(): boolean;
31
+ get isStarted(): boolean;
32
+ startIfEnabled(): void;
33
+ stop(): void;
34
+ /**
35
+ * Sync user_id to gtag.js so both proxy and MP event streams share
36
+ * the same identity after identify(). GA4 Realtime uses user_id as
37
+ * the highest-priority identifier; without this, proxy events (no
38
+ * user_id) and MP events (with user_id) count as separate users.
39
+ */
40
+ setUserId(userId: string | null): void;
41
+ onConfigUpdate(config: VTiltConfig): void;
42
+ private _start;
43
+ private _getApiBase;
44
+ private _hasGtag;
45
+ private _reconfigureExistingGtag;
46
+ private _loadAndConfigureGtag;
47
+ /**
48
+ * Use gtag('get', ...) to obtain the authoritative client_id and
49
+ * session_id from gtag.js. Fires onIdentityResolved once BOTH
50
+ * callbacks have completed (counter-based).
51
+ */
52
+ private _resolveGtagIdentity;
53
+ /**
54
+ * Fires after each gtag('get', ...) callback. When all pending callbacks
55
+ * have settled, notifies the host (EventBuffer) so buffered MP events
56
+ * can flush with stable GA4 IDs (same client_id as gtag.js).
57
+ */
58
+ private _onGtagCallbackSettled;
59
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Consent Mode v2 bridge.
3
+ *
4
+ * Maps vTilt's 3-boolean consent (analytics / marketing / advertising) to
5
+ * Google's 4-key Consent Mode v2 payload (ad_storage, ad_user_data,
6
+ * ad_personalization, analytics_storage) and pushes it through gtag.
7
+ *
8
+ * Mapping (default):
9
+ * advertising -> ad_storage + ad_user_data + ad_personalization
10
+ * analytics -> analytics_storage
11
+ * marketing is not sent to Google by default (it covers email/SMS, not ads).
12
+ *
13
+ * Admin UI may override individual keys (`consentOverride`) to hard-wire a
14
+ * specific value regardless of user consent — useful for jurisdictions or
15
+ * projects with fixed policies.
16
+ */
17
+ import type { ConsentState } from "../../core/consent";
18
+ import type { GoogleConsentOverride } from "../../types";
19
+ export type GtagFn = (...args: unknown[]) => void;
20
+ export type GoogleConsentPayload = Record<"ad_storage" | "ad_user_data" | "ad_personalization" | "analytics_storage", "granted" | "denied">;
21
+ export declare function buildConsentPayload(consent: ConsentState, override?: GoogleConsentOverride): GoogleConsentPayload;
22
+ /**
23
+ * Send the default consent state BEFORE gtag.js loads so that every
24
+ * subsequent request carries the right flags. Call once at feature start.
25
+ */
26
+ export declare function pushConsentDefault(gtag: GtagFn, consent: ConsentState, override?: GoogleConsentOverride): void;
27
+ export declare function pushConsentUpdate(gtag: GtagFn, consent: ConsentState, override?: GoogleConsentOverride): void;