import { ApiCaller } from '../api/api-caller'; import { WidgetConfig, WidgetMention } from '../types/widget-config'; import { WidgetAiMessage, WidgetMessageU, WidgetUserMessage } from '../types/messages'; import { SendMessageDto } from '../types/dtos'; import { PrimitiveState } from '../utils/PrimitiveState'; import { SessionCtx } from './session.ctx'; import { ContactCtx } from './contact.ctx'; /** The shape a caller passes to `sendMessage` (and `stageUserTurn`). */ export type SendMessageInput = { content: SendMessageDto['content']; /** Send agent context without displaying a user bubble. */ background?: boolean; attachments?: SendMessageDto['attachments']; customData?: SendMessageDto['custom_data']; /** * Per-message AI-visible context (e.g. the composer's picked page elements), * merged over the config-level `context` on the wire. */ clientContext?: Record; /** Durable lifecycle marker for a connection card handled by this send. */ connectionRequestId?: string; /** * What the visitor @-mentioned (host items picked from `config.mentions`). * Sent as `clientContext.mentions`. */ mentions?: WidgetMention[]; /** * False when the visitor dismissed the page's entity pill for this message: * the send goes out without `context.entity`, everything else intact. * @default true */ withPageEntity?: boolean; exitModePrompt?: string; /** The send is a tap on an option of an agent message (`messageOptions`). */ optionReply?: { messageId: string; optionId: string; }; /** * Signals that the send has passed validation, has a session, and is owned * by a send engine. This lets composers clear immediately without changing * the existing blocking `sendMessage` promise (which still waits for a bot * reply). It is lifecycle-only and is never sent over the wire. * * @internal */ onAccepted?: () => void; }; /** What `stageUserTurn` hands the engine that finishes the send. */ export type StagedUserTurn = { sessionId: string; userMessage: WidgetUserMessage; /** Persistent greetings riding a fresh conversation's first turn. */ initialMessages: WidgetAiMessage[]; }; /** * The seam the headless agent-chat provider registers so the shared * `sendMessage` entry point routes to its `useChat` streaming engine. Core * stays framework-neutral and owns only the blocking bot-chat path plus the * shared persisted message list. */ export type AgentChatHandlers = { /** Includes preparation, queued sends, live work and post-turn reconciliation. */ hasPendingWork?: () => boolean; send: (input: SendMessageInput) => Promise | void; }; /** * The per-message wire context shared by both send engines: the host's * config-level `context` (resolved fresh when it is a function) with the * widget's own per-send page context merged over it. * * `sendsPageContext` is `WidgetCtx.features.pageContext` — the org's * page-context feature narrowed by the embed. Off → the widget adds nothing * of its own (no page marks, no picked elements) but the host's `context` * still rides along exactly as it did in v4; `custom_data` is unaffected. */ export declare const mergeSendContext: (config: WidgetConfig, input: SendMessageInput, { sendsPageContext }: { sendsPageContext: boolean; }) => { clientContext: Record | undefined; custom_data: Record; }; export declare const resolveConfigContext: (config: WidgetConfig) => Record | undefined; /** Wire shape of the per-embed feature toggles (`config.features`). */ type SendFeaturesBody = NonNullable; /** * `config.features` (camelCase) → the snake_cased `features` field both send * engines carry. Page reading and actions require explicit embed opt-in, * including on the server; omission must not inherit organization defaults. */ export declare const resolveSendFeatures: (config: WidgetConfig) => SendFeaturesBody | undefined; /** * Everything one send carries on the wire — the SAME body for the blocking * send and the streaming turn (both endpoints take `WidgetSendMessageInputDto`). * Config is read at call time so function-form `context` and a refreshed * embedder config are reflected on every request. */ export declare const buildSendMessageBody: ({ config, input, uuid, sessionId, content, initialMessages, sendsPageContext, }: { config: WidgetConfig; input: SendMessageInput; /** The user message's wire id (a retry sends a fresh one). */ uuid: string; sessionId: string; /** The rendered user message text (extra collected data already prepended). */ content: string; /** Persistent greetings riding a fresh conversation's first turn. */ initialMessages: readonly WidgetAiMessage[]; sendsPageContext: boolean; }) => SendMessageDto; type MessageCtxState = { messages: WidgetMessageU[]; /** Regardless of assignee */ isSendingMessage: boolean; isSendingMessageToAI: boolean; /** A completed streamed turn can intentionally leave no assistant row. */ settledAgentUserMessageId: string | null; lastAIResMightSolveUserIssue: boolean; isInitialFetchLoading: boolean; }; export declare class MessageCtx { private config; private readonly getRequestConfig; private readonly isStreaming; private readonly getClientCapabilities; private api; private contactCtx; private sessionCtx; state: PrimitiveState; /** One reactive draft per conversation, including while its composer is unmounted. */ draftState: PrimitiveState<{ text: string; mentions: WidgetMention[]; }>; /** * Current transport choice. The org or this embed may request polling * independently of the server's agent version. */ get streaming(): boolean; /** * `WidgetCtx.features.pageContext`: whether the widget's own page context * (page marks, picked elements) rides along with each message. Off → the * user bubble shows no page-mark chips either. */ private readonly getSendsPageContext; get sendsPageContext(): boolean; /** Registered by the headless agent engine for the WidgetProvider lifetime. */ private agentHandlers; /** Sends issued before the agent-chat surface mounts, oldest first. */ private bufferedAgentSends; private sendMessageAbortController; private conversationGeneration; /** Bind asynchronous preparation to this chat, including resets to a new empty chat. */ captureConversation: () => (() => boolean); private messageIdsDispatchedToOnMessageReceivedHook; /** * Text of messages sent in THIS conversation, oldest first, for the * composer's ↑/↓ recall. Lives here rather than in the composer because the * quick-ask bar and the full chat composer are different mounts of the same * component and recall must survive the morph between them — while staying * scoped to one widget instance and one conversation (`reset` clears it, so * a new chat never recalls the previous visitor's or session's text). * Deliberately NOT part of `state`: nothing renders it, so it must not * re-render the transcript on every send. */ private sentTextHistory; private static readonly SENT_TEXT_HISTORY_MAX; constructor({ config, api, sessionCtx, contactCtx, streaming, sendsPageContext, getClientCapabilities, getRequestConfig, isStreaming, getSendsPageContext, }: { config: WidgetConfig; api: ApiCaller; sessionCtx: SessionCtx; contactCtx: ContactCtx; streaming: boolean; sendsPageContext: boolean; /** Read renderer support at send time; React options may change after initialization. */ getClientCapabilities?: () => WidgetConfig['capabilities']; getRequestConfig?: () => WidgetConfig; isStreaming?: () => boolean; getSendsPageContext?: () => boolean; }); reset: () => void; /** * Record sent text for ↑/↓ recall. Consecutive duplicates collapse — the * common case is re-sending the same line after a failure. */ rememberSentText: (text: string) => void; /** Sent text for this conversation, oldest first. */ getSentTextHistory: () => readonly string[]; /** * Wiring for the headless agent engine: WidgetProvider registers its useChat * `send` here and clears it on unmount, so every streaming headless or * styled consumer shares the same lifecycle. Imperative sends can arrive in * the render gap before registration; drain those in FIFO order once ready. */ registerAgentHandlers: (handlers: AgentChatHandlers) => void; /** Hand a send to the streaming engine; its failure can never propagate. */ private static dispatchToEngine; get hasPendingAgentWork(): boolean; unregisterAgentHandlers: (handlers: AgentChatHandlers) => void; /** * Fires `config.hooks.onMessageReceived` for AI or human-agent messages exactly * once per id. Safe to call from any path that ingests messages from the server * (send-message response, polling, initial history fetch). */ dispatchToOnMessageReceivedHook: (message: WidgetMessageU) => void; /** * Records the given message ids as already-dispatched without firing the hook. * Used to suppress `onMessageReceived` for historical messages that flow in * when an existing session is opened — the user is loading context, not * receiving new messages. Subsequent polls still dedupe against these ids. */ markAsDispatchedToOnMessageReceivedHook: (messageIds: string[]) => void; /** * On the very first turn of a fresh conversation, the configured persistent * initial messages are inserted into the transcript above the user's first * message (and posted to the backend so they persist in history). Any later * turn returns an empty array. */ private buildPersistentInitialMessages; /** * Create the session on the conversation's first send; refreshes the * sessions list so the new session shows up immediately. Returns `null` * when creation fails. */ private ensureSessionId; /** Remove only this failed attempt's optimistic rows, preserving poll merges. */ private rollbackOptimisticMessages; /** * Fire the caller's `onAccepted` once a send is validated, has a session, * and is owned by an engine. A consumer callback must never be able to * break an accepted send. A new message also retires the last reply's * `lastAIResMightSolveUserIssue`, whichever engine sends it. */ notifySendAccepted: (input: SendMessageInput) => void; /** * The front half every send shares, whichever engine finishes it: validate, * build the user message (plus the persistent greetings on a fresh * conversation's first turn), render it optimistically, and make sure a * session exists — rolling the optimistic rows back if it cannot. Returns * `null` when there is nothing to send or no session could be created. */ stageUserTurn: (input: SendMessageInput, { pending }: { pending: boolean; }) => Promise; /** * Build the user message for a QUEUED (multi-send) turn WITHOUT rendering * it into the transcript. A queued message must not appear above the still- * streaming response of the active turn — the surface renders it below the * live overlay (dimmed via `pending`) and only `appendUserMessageIfAbsent`s * it into `messages` when its own turn actually starts (see `useAgentChat`). * A session already exists here (a turn is in flight), so this never creates * one. */ buildQueuedUserMessage: (input: SendMessageInput) => { sessionId: string; userMessage: WidgetUserMessage; } | null; /** Validate the input and build its user message; `null` when empty. */ private buildUserMessage; /** * Defensive read of `clientContext.page_marks` → the user bubble's context * chips: each mark contributes the element it was placed on, the note the * visitor wrote, and a reference back to the mark itself (the UI renders * the same presentation the composer showed, thumbnail included). Entries * without a usable name are dropped. */ private static markedElementNames; /** Append a user message to the transcript once, ignoring a duplicate id. */ appendUserMessageIfAbsent: (userMessage: WidgetUserMessage) => void; /** * Clear the `pending` flag for the user message owned by one streamed turn. * Later queued bubbles must remain pending until their own turn reaches the * server. */ markUserMessageDelivered: (messageId: string) => void; /** * The blocking engine refuses a send while the AI reply is pending unless * the embed opted out. The streaming engine never blocks (it queues). */ get blocksSendWhileAwaitingReply(): boolean; /** Set or clear the picked option of an agent message already in the transcript. */ setMessageOptionPick: (messageId: string, pickedOptionId: string | null) => void; private releaseFailedOptionPick; sendMessage: (input: SendMessageInput) => Promise; private toUserMessage; private toBotMessage; private toBotErrorMessage; } export {}; //# sourceMappingURL=message.ctx.d.ts.map