/** * ChatController — orchestrates the chat widget. * * - Implements `LazyLoadedChatInterface` (the public SDK surface) * - Owns the ChatStore (state) * - Owns the AblyClient (realtime transport) * - Performs HTTP calls (channel CRUD, message send, mark-read) * - Mounts the Preact UI inside a Shadow DOM via `mountChatUI()` * * No DOM logic lives here beyond mount/unmount — components read signals * directly and emit user intent back to the controller through props. * * See: docs/modules/chat/widget-architecture.md */ import { type ChatChannel, type ChatChannelSummary, type ChatConfig, type ChatWidgetView, type LazyLoadedChatInterface, type SendChatMessageContent, type SendChatMessageOptions } from "../../../utils/globals"; import type { VTilt } from "../../../vtilt"; import { type ConnectionCallback, type MessageCallback, type TypingCallback, type Unsubscribe } from "../types"; import { ChatWidgetRegistry, type ChatWidgetDefinition, type WidgetContext } from "../widget-registry"; import { ChatStore, DEFAULT_THEME } from "../store/chat-store"; export declare class ChatController implements LazyLoadedChatInterface { private _instance; private _config; private _store; private _ably; private _ui; private _registry; private _messageCallbacks; private _typingCallbacks; private _connectionCallbacks; private _lastConnectedNotified; private _unsubscribeIdentity; private _unsubscribeReset; private _identityChangeInFlight; private _typingIdleTimer; private _isUserTyping; private _lastTypingPublishAt; private _lastTypingActivityAt; private _pendingTypingIntent; private _disposeRealtimeReadyWatcher; private _isMarkingRead; private _streamAbort; private _streamingMessageId; /** Local typing dot shown before the HTTP response headers arrive. */ private _optimisticAiTyping; private _openedAt; private _disposeConnectionWatcher; private _outbox; /** Distinct id the active outbox storage key was opened for. */ private _outboxDistinctId; private _drainInFlight; private _drainTimer; private readonly _onOutboxOnline; private readonly _onOutboxVisible; constructor(instance: VTilt, config?: ChatConfig); get isOpen(): boolean; get isConnected(): boolean; get isLoading(): boolean; get unreadCount(): number; get channel(): ChatChannel | null; get channels(): ChatChannelSummary[]; get currentView(): ChatWidgetView; /** Exposed so components can read the live store inside JSX. */ get store(): ChatStore; /** Exposed so the widget slot can look up custom definitions. */ get widgets(): ChatWidgetRegistry; get theme(): typeof DEFAULT_THEME; get config(): ChatConfig; open(): void; close(): void; /** Drop per-channel subs and close Ably — badge while closed uses Tier A REST. */ private _teardownRealtimeOnClose; /** * Tier A closed-badge: apply aggregate unread from `GET /api/chat/widget/unread` * (called by `chat-wrapper` poll / visibility refresh). */ applyPolledUnreadCount(count: number): void; toggle(): void; private _bubbleExplicitShow; show(): void; hide(): void; getChannels(): Promise; selectChannel(channelId: string): Promise; /** * Create a new conversation channel. * * `options.skipGreeting` tells the server not to seed the AI welcome * message (used when the user is initiating the conversation via * `sendChatMessage` and their own message will arrive immediately after). * * `options.preserveMessages` keeps any messages already in the store * (e.g. an optimistic user temp message inserted before the create call) * and merges them with the server response — server messages first, then * any local `temp-` rows we still have. This is what powers the * "show the user's message instantly while the channel is being * created" UX path. */ createChannel(options?: { skipGreeting?: boolean; preserveMessages?: boolean; }): Promise; goToChannelList(): void; /** Abort the in-flight AI stream and remove its partial bubble. */ private _cancelActiveStream; /** * Remove AI bubbles trailing the last user message — used when a new send * aborts an in-flight stream before `onStreamStart` registered its id. */ private _stripTrailingAiAfterLastUser; /** Show the AI typing bubble while waiting for stream headers / first token. */ private _showOptimisticAiTyping; private _clearOptimisticAiTyping; private _makeOptimisticUserMessage; private _insertOptimisticUserMessage; /** Paint the outbound bubble as early as possible — before network awaits. */ private _tryPaintOptimisticSend; sendMessage(content: SendChatMessageContent, options?: SendChatMessageOptions): Promise; /** Retry a failed outbound message (tap-to-retry in the bubble meta). */ retryFailedMessage(messageId: string): void; markAsRead(): void; /** * Send a silent trigger to the messages API after a widget action so the * AI follows up (e.g. acknowledges email collection) without creating a * visible user message in the chat. */ triggerAIAfterWidgetAction(channelId: string): Promise; onMessage(callback: MessageCallback): Unsubscribe; onTyping(callback: TypingCallback): Unsubscribe; onConnectionChange(callback: ConnectionCallback): Unsubscribe; updateConfig(config: ChatConfig): void; private _applyBubbleLayout; registerWidget(definition: ChatWidgetDefinition): void; /** * Called by `MessageInput` on each keystroke. * * Strategy (industry-standard for Slack/Intercom/Front): * - Publish `true` once on the leading edge of a typing burst. * - Refresh `true` every 4s while the user keeps typing so the * dashboard's local timeout doesn't drop the indicator mid-burst. * - Schedule a 5s idle timer that publishes `false` once the user * stops. Each new keystroke resets the timer. * - When realtime isn't attached (initial load, reconnect window), * queue the intent and flush it when `realtimeReady` flips true, * provided the activity is still recent (<3s old). */ notifyUserTyping(): void; /** * Called from `MessageInput` on send/blur, from `close()`, and from * `destroy()`. Synchronously emits `false` so the dashboard's * indicator clears immediately rather than waiting for the 5s idle * timeout. */ stopUserTyping(): void; private _scheduleTypingIdle; destroy(): void; /** Build the WidgetContext passed to custom widget renderers and actions. */ getWidgetContext(messageId: string, widgetType: string): WidgetContext; /** * Mark a widget as submitted in local message metadata so the next render * shows the confirmation UI instead of the input form, then trigger an AI * follow-up unless the widget is `escalate_to_human` (AI mode is off). */ markWidgetSubmittedAndFollowUp(messageId: string, widgetType: string, formData: Record): void; private _outboxStorageKey; private _refreshOutboxKey; private _markOutboxSending; private _failOutboxMessage; private _bindOutboxToChannel; private _rememberLastCreatedChannel; private _migrateOutboxFromDistinctId; private _bindOutboxLifecycle; private _unbindOutboxLifecycle; private _enqueueOutbox; /** * Merge a channel GET payload with on-screen / cached rows and any outbox * entries whose bubbles were dropped during navigation. */ private _mergeChannelHydration; private _outboxMessagesForChannel; private _outboxEntryToMessage; private _markMessageDelivered; private _scheduleDrainOutbox; private _drainOutbox; private _deliverOutboxEntry; private _createChannelIdForOutbox; private _postUserMessageSilent; private _postUserMessage; private get _distinctId(); private _stubChannelFromSummary; private _handleIdentityChange; /** * Resolve once `identityChangeInFlight` becomes `false`. Used by * `sendMessage` so a `vt.sendChatMessage()` call that arrives immediately * after `vt.identify()` doesn't race the in-flight identity reset. */ private _waitForIdentityChange; private _trackChatMessage; private _autoMarkAsRead; private _publishTyping; /** * Subscribe to `connectionState` changes and forward `connected`/ * `disconnected` transitions through the legacy `onConnectionChange` * callback contract. */ private _watchConnectionState; }