/** * Public configuration surface for the chat widget. * * A host page configures the widget either declaratively (HTML attributes on the * `` element) or programmatically (passing this object to * {@link mountChatWidget} / `element.configure(...)`). */ export interface ChatWidgetTheme { /** Foreground text color for the widget chrome. */ text?: string; /** Panel background color. */ background?: string; /** Primary accent (launcher button, send button, outbound bubble). */ primary?: string; /** Text color rendered on top of `primary`. */ primaryText?: string; /** A secondary accent (used for subtle highlights). */ secondary?: string; /** Inbound (assistant) chat bubble background. */ assistantBubble?: string; /** Inbound (assistant) chat bubble text color. */ assistantBubbleText?: string; /** Outbound (user) chat bubble background. Defaults to `primary`. */ userBubble?: string; /** Outbound (user) chat bubble text color. Defaults to `primaryText`. */ userBubbleText?: string; /** Border color for the panel and input. */ border?: string; // ── Aliases for the dashboard's 10-color model (SmooAI agent widget config). // When provided, these take precedence over the canonical keys above, so a // config exported from the agent dashboard themes the widget directly. /** Alias for {@link assistantBubble}. */ chatBubbleInbound?: string; /** Alias for {@link assistantBubbleText}. */ chatBubbleInboundText?: string; /** Alias for {@link userBubble}. */ chatBubbleOutbound?: string; /** Alias for {@link userBubbleText}. */ chatBubbleOutboundText?: string; } /** * Layout mode for the widget. * * - `"popover"` (default) — the embeddable launcher bubble + floating panel. * - `"fullpage"` — no launcher; the chat fills its container/viewport with a * branded header, a scrollable message list, and an input bar. Ideal for a * dedicated support page (`/chat`, a docs site sidebar, an iframe, …). */ export type ChatWidgetMode = 'popover' | 'fullpage'; export interface ChatWidgetConfig { /** * smooth-operator WebSocket endpoint, e.g. * `ws://localhost:8787/ws` (local dev) or your deployed `wss://…/ws` URL. */ endpoint: string; /** * Layout mode — `"popover"` (default, launcher + floating panel) or * `"fullpage"` (chat fills its container; no launcher). See {@link ChatWidgetMode}. */ mode?: ChatWidgetMode; /** UUID of the agent to start a conversation session with. */ agentId: string; /** Display name for the agent (header label). Defaults to "Assistant". */ agentName?: string; /** Optional display name for the user participant. */ userName?: string; /** Optional email address for the user participant. */ userEmail?: string; /** Optional phone number for the user participant (passed via session metadata). */ userPhone?: string; /** * Optional pre-auth HMAC context. When the host page has a shared secret with * the agent, it can sign `{ userId, signature, timestamp }` so the chat-ws * wrapper's `/internal/*` identity routes (and the WS create path) verify the * caller without an OTP round-trip (ADR-046/ADR-048). Passed through verbatim. */ authContext?: { userId: string; signature: string; timestamp: number }; /** Placeholder text for the message input. */ placeholder?: string; /** Greeting rendered when the conversation opens (before any messages). */ greeting?: string; /** Message shown when the connection cannot be (re)established. */ connectionErrorMessage?: string; /** Start the panel open instead of collapsed to the launcher. */ startOpen?: boolean; /** * Suggested starter prompts shown as clickable chips before the first message. * Clicking one sends it. Capped at 5 for layout. */ examplePrompts?: string[]; /** Require the visitor's name before chatting. */ requireName?: boolean; /** Require the visitor's email before chatting. */ requireEmail?: boolean; /** Require the visitor's phone before chatting. */ requirePhone?: boolean; /** * Show the phone field on the pre-chat form (optional unless {@link requirePhone}). * Defaults to `true` for this widget — phone rides the session metadata as * `userPhone` so the agent can follow up by SMS. Set `false` to hide it. */ collectPhone?: boolean; /** * Show the email + SMS marketing-consent checkboxes on the pre-chat form. * Explicit opt-in, default UNCHECKED; a `consentAt` timestamp is stamped when * a box is ticked. Defaults to `true`. The consent record is threaded into the * session metadata (ADR-048). */ collectConsent?: boolean; /** * Offer the cross-device "Restore my chats" affordance — an explicit link that * runs the identity-OTP → resolve → replay flow. Defaults to `true`. */ allowChatRestore?: boolean; /** * Let visitors chat without providing any identity. When `true`, the * `require*` flags are ignored and the pre-chat form is skipped. */ allowAnonymous?: boolean; /** Theme overrides. */ theme?: ChatWidgetTheme; } /** The fully-resolved theme (canonical keys only — aliases are folded in). */ export type ResolvedTheme = Required>; export type ResolvedConfig = Required> & { theme: ResolvedTheme; userName?: string; userEmail?: string; userPhone?: string; authContext?: { userId: string; signature: string; timestamp: number }; }; /** Resolve a partial config against the built-in defaults. */ export function resolveConfig(config: ChatWidgetConfig): ResolvedConfig { const theme = config.theme ?? {}; const primary = theme.primary ?? '#00a6a6'; const primaryText = theme.primaryText ?? '#f8fafc'; // Dashboard aliases win over canonical keys when present. const assistantBubble = theme.chatBubbleInbound ?? theme.assistantBubble ?? '#06134b'; const assistantBubbleText = theme.chatBubbleInboundText ?? theme.assistantBubbleText ?? '#f8fafc'; const userBubble = theme.chatBubbleOutbound ?? theme.userBubble ?? primary; const userBubbleText = theme.chatBubbleOutboundText ?? theme.userBubbleText ?? primaryText; return { endpoint: config.endpoint, mode: config.mode ?? 'popover', agentId: config.agentId, agentName: config.agentName ?? 'Assistant', userName: config.userName, userEmail: config.userEmail, userPhone: config.userPhone, authContext: config.authContext, placeholder: config.placeholder ?? 'Type a message…', greeting: config.greeting ?? 'Hi! How can I help you today?', connectionErrorMessage: config.connectionErrorMessage ?? "We couldn't reach the chat. Please try again in a moment.", startOpen: config.startOpen ?? false, examplePrompts: (config.examplePrompts ?? []).filter((p) => p.trim().length > 0).slice(0, 5), requireName: config.requireName ?? false, requireEmail: config.requireEmail ?? false, requirePhone: config.requirePhone ?? false, collectPhone: config.collectPhone ?? true, collectConsent: config.collectConsent ?? true, allowChatRestore: config.allowChatRestore ?? true, allowAnonymous: config.allowAnonymous ?? false, theme: { text: theme.text ?? '#f8fafc', background: theme.background ?? '#040d30', primary, primaryText, secondary: theme.secondary ?? '#ff6b6c', assistantBubble, assistantBubbleText, userBubble, userBubbleText, border: theme.border ?? 'rgba(255, 255, 255, 0.1)', }, }; } /** * Whether the pre-chat identity form should gate the conversation: at least one * field is required and anonymous chat is not allowed. */ export function needsUserInfo(resolved: ResolvedConfig): boolean { return !resolved.allowAnonymous && (resolved.requireName || resolved.requireEmail || resolved.requirePhone); }