import type { WidgetTheme } from "./public-contracts.js"; import type { AccessibilityCapabilities } from "./accessibility/types.js"; import type { WidgetApiClient } from "./api.js"; import { ConversationController } from "./conversation.js"; import { resetConversationSession } from "./session.js"; import { SurfaceView, type SurfaceKind } from "./surface-view.js"; export interface AkintuSurfaceConfiguration { accessibility: AccessibilityCapabilities; apiClient: WidgetApiClient; context?: string | undefined; sessionId: string; sessionVersion?: number | undefined; theme: WidgetTheme; } export interface AkintuSurfaceElement extends HTMLElement { configure(configuration: AkintuSurfaceConfiguration): void; } /** * Creates a browser-only custom element class for one surface variant. Keeping * HTMLElement lookup inside this factory makes importing the package safe in * Node and Next.js server rendering environments. */ export function createSurfaceElementClass( kind: SurfaceKind, ): CustomElementConstructor { return class AkintuSurfaceElementImplementation extends HTMLElement implements AkintuSurfaceElement { private readonly view: SurfaceView; private controller: ConversationController | undefined; private configuration: AkintuSurfaceConfiguration | undefined; constructor() { super(); // Open mode preserves style isolation while keeping the rendered surface // inspectable for integrators, browser tests, and support diagnostics. const shadowRoot = this.attachShadow({ mode: "open" }); this.view = new SurfaceView(shadowRoot, kind); this.view.setHandlers({ onAbort: () => this.controller?.abort(), onConfirmAction: () => { void this.controller?.confirmPendingAction(); }, onCancelAction: () => { void this.controller?.cancelPendingAction(); }, onCompleteExternalAction: (event) => { void this.controller?.completeExternalAction(event); }, onSelectBookingSlot: (slot) => { this.controller?.selectBookingSlot(slot); }, onSubmitPlaybookFields: (step, values) => { void this.controller?.submitPlaybookFields(step, values); }, onSubmitBookingDetails: (details) => { void this.controller?.prepareCalendlyBooking(details); }, onFeedback: (resolved) => { void this.controller?.submitFeedback(resolved); }, onResetConversation: () => { resetConversationSession({ sessionVersion: this.configuration?.sessionVersion, }); window.location.reload(); }, onSend: (message) => { void this.controller?.sendMessage(message); }, }); } configure(configuration: AkintuSurfaceConfiguration): void { // Reconfiguration replaces the request owner instead of allowing two // controllers to update the same Shadow DOM concurrently. this.controller?.destroy(); this.configuration = configuration; this.view.setCapabilities(configuration.accessibility); this.view.setTheme(configuration.theme); const isReducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false; this.controller = new ConversationController({ apiClient: configuration.apiClient, context: configuration.context, sessionId: configuration.sessionId, isReducedMotion, onStateChange: (state) => this.view.update(state), }); void this.controller.loadHistory(); } disconnectedCallback(): void { // Both fetch streaming and Web Speech work can outlive a DOM node unless // they are cancelled explicitly during custom-element teardown. this.controller?.destroy(); this.view.destroy(); } }; } export function isAkintuSurfaceElement( element: Element, ): element is AkintuSurfaceElement { return "configure" in element && typeof element.configure === "function"; }