@yak-io/javascript 0.11.1 → 0.11.2

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts", "../src/logger.ts", "../src/page-context.ts", "../src/version.ts", "../src/client.ts", "../src/voice-machine.ts", "../src/tool-name.ts", "../src/voice-session.ts", "../src/embed.ts", "../src/toolset.ts"],
4
- "sourcesContent": ["// Public types\n\nexport type { YakClientConfig } from \"./client.js\";\n// Public client API\nexport { YakClient } from \"./client.js\";\nexport type {\n TriggerButtonConfig,\n WidgetMode,\n YakEmbedConfig,\n YakEmbedState,\n YakEmbedStateListener,\n} from \"./embed.js\";\n// Embed (DOM rendering layer \u2014 chat + voice)\nexport { YakEmbed } from \"./embed.js\";\n// Logging utilities\nexport { disableYakLogging, enableYakLogging, isYakLoggingEnabled, logger } from \"./logger.js\";\n// Client-side tool composition (one merged manifest + one routed onToolCall)\nexport type { YakServerAdapterConfig } from \"./toolset.js\";\nexport { createYakServerAdapter, createYakToolset } from \"./toolset.js\";\nexport * from \"./types/config.js\";\nexport * from \"./types/messaging.js\";\nexport * from \"./types/routes.js\";\nexport * from \"./types/tools.js\";\nexport type { EmbedProtocolVersion } from \"./version.js\";\n// Version\nexport { EMBED_PROTOCOL_VERSION } from \"./version.js\";\nexport type { VoiceEvent, VoiceMachine, VoiceState } from \"./voice-machine.js\";\nexport {\n handleRealtimeMessage,\n INITIAL_VOICE_MACHINE,\n voiceReducer,\n} from \"./voice-machine.js\";\n// Voice session (composed by YakEmbed; exposed for advanced consumers)\nexport type { VoiceStateListener, YakVoiceSessionConfig } from \"./voice-session.js\";\nexport { YakVoiceSession } from \"./voice-session.js\";\n", "/**\n * Simple logger utility for the yak client SDK.\n * Debug/info logs are controlled by __YAK_LOGGING_ENABLED__.\n * Warnings and errors are always logged.\n *\n * Logging is controlled via:\n * 1. localStorage key \"yakLogging\" (persists across reloads)\n * 2. window.__YAK_LOGGING_ENABLED__ (runtime override)\n *\n * Use the helper functions to enable/disable:\n * enableYakLogging() - turns on logging, persists in localStorage\n * disableYakLogging() - turns off logging, clears localStorage\n */\n\nconst STORAGE_KEY = \"yakLogging\";\n\nfunction isLoggingEnabled(): boolean {\n if (typeof window === \"undefined\") {\n return false;\n }\n\n // Window flag takes precedence (runtime override)\n if (typeof window.__YAK_LOGGING_ENABLED__ === \"boolean\") {\n return window.__YAK_LOGGING_ENABLED__;\n }\n\n // Fall back to localStorage for persistence across reloads\n try {\n return localStorage.getItem(STORAGE_KEY) === \"true\";\n } catch {\n // localStorage may be unavailable (e.g., private browsing)\n return false;\n }\n}\n\n/**\n * Check if Yak logging is currently enabled.\n * Useful for checking state before creating iframes.\n */\nexport function isYakLoggingEnabled(): boolean {\n return isLoggingEnabled();\n}\n\n/**\n * Enable Yak debug logging.\n * Persists in localStorage so it survives page reloads.\n * Call this from browser console: `enableYakLogging()`\n */\nexport function enableYakLogging(): void {\n if (typeof window === \"undefined\") {\n return;\n }\n window.__YAK_LOGGING_ENABLED__ = true;\n try {\n localStorage.setItem(STORAGE_KEY, \"true\");\n } catch {\n // localStorage may be unavailable\n }\n console.info(\"[yak] Logging enabled\");\n}\n\n/**\n * Disable Yak debug logging.\n * Clears localStorage and window flag.\n * Call this from browser console: `disableYakLogging()`\n */\nexport function disableYakLogging(): void {\n if (typeof window === \"undefined\") {\n return;\n }\n window.__YAK_LOGGING_ENABLED__ = false;\n try {\n localStorage.removeItem(STORAGE_KEY);\n } catch {\n // localStorage may be unavailable\n }\n console.info(\"[yak] Logging disabled\");\n}\n\n// Expose helpers globally so they can be called from browser console\nif (typeof window !== \"undefined\") {\n (\n window as Window & {\n enableYakLogging?: typeof enableYakLogging;\n disableYakLogging?: typeof disableYakLogging;\n }\n ).enableYakLogging = enableYakLogging;\n (\n window as Window & {\n enableYakLogging?: typeof enableYakLogging;\n disableYakLogging?: typeof disableYakLogging;\n }\n ).disableYakLogging = disableYakLogging;\n}\n\nexport const logger = {\n debug: (message: string, data?: unknown): void => {\n if (isLoggingEnabled()) {\n if (data !== undefined) {\n console.log(`[yak-host] ${message}`, data);\n } else {\n console.log(`[yak-host] ${message}`);\n }\n }\n },\n\n info: (message: string, data?: unknown): void => {\n if (isLoggingEnabled()) {\n if (data !== undefined) {\n console.info(`[yak-host] ${message}`, data);\n } else {\n console.info(`[yak-host] ${message}`);\n }\n }\n },\n\n warn: (message: string, data?: unknown): void => {\n if (data !== undefined) {\n console.warn(`[yak-host] ${message}`, data);\n } else {\n console.warn(`[yak-host] ${message}`);\n }\n },\n\n error: (message: string, data?: unknown): void => {\n if (data !== undefined) {\n console.error(`[yak-host] ${message}`, data);\n } else {\n console.error(`[yak-host] ${message}`);\n }\n },\n};\n", "import type { PageContext } from \"./types/messaging.js\";\n\n/**\n * Extract visible text content from the DOM, filtering out non-content elements\n */\nfunction extractPageText(): string {\n if (typeof document === \"undefined\") return \"\";\n\n // Clone the body to avoid modifying the actual DOM\n const bodyClone = document.body.cloneNode(true) as HTMLElement;\n\n // Remove script, style, noscript tags and hidden elements\n const unwantedSelectors = [\n \"script\",\n \"style\",\n \"noscript\",\n \"iframe\",\n \"[style*='display: none']\",\n \"[style*='display:none']\",\n \"[hidden]\",\n \".yak-chat-widget\",\n ];\n\n for (const selector of unwantedSelectors) {\n const elements = bodyClone.querySelectorAll(selector);\n for (const el of elements) {\n el.remove();\n }\n }\n\n // Get text content and clean it up\n let text = bodyClone.textContent || bodyClone.innerText || \"\";\n\n // Normalize whitespace: replace multiple spaces/newlines with single space\n text = text.replace(/\\s+/g, \" \").trim();\n\n // Limit to reasonable size (100KB max)\n const maxLength = 100000;\n if (text.length > maxLength) {\n text = `${text.substring(0, maxLength)}... [truncated]`;\n }\n\n return text;\n}\n\n/**\n * Extract page context including URL, title, and visible text content\n */\nexport function extractPageContext(): PageContext {\n if (typeof window === \"undefined\") {\n return {\n url: \"\",\n title: \"\",\n text: \"\",\n timestamp: Date.now(),\n };\n }\n return {\n url: window.location.href,\n title: document.title,\n text: extractPageText(),\n timestamp: Date.now(),\n };\n}\n\n/**\n * Debounce function to limit how often a function is called\n */\nexport function debounce<T extends (...args: unknown[]) => void>(\n func: T,\n wait: number\n): (...args: Parameters<T>) => void {\n let timeout: NodeJS.Timeout | null = null;\n\n return function executedFunction(...args: Parameters<T>) {\n const later = () => {\n timeout = null;\n func(...args);\n };\n\n if (timeout) {\n clearTimeout(timeout);\n }\n timeout = setTimeout(later, wait);\n };\n}\n", "/**\n * Embed protocol version.\n *\n * This version is used in the embed URL path (e.g., /embed/v1/[appId])\n * and in the message protocol to ensure compatibility between the\n * host packages (@yak-io/javascript, @yak-io/react, @yak-io/nextjs)\n * and the embedded chat UI.\n *\n * Increment this version when making breaking changes to:\n * - The postMessage protocol (IframeMessageFromHost, IframeMessageToHost)\n * - The tool manifest format\n * - The route manifest format\n * - The config payload structure\n *\n * Version history:\n * - v1: Initial versioned protocol\n */\nexport const EMBED_PROTOCOL_VERSION = \"1\" as const;\n\n/**\n * Type for the embed protocol version\n */\nexport type EmbedProtocolVersion = typeof EMBED_PROTOCOL_VERSION;\n", "import { isYakLoggingEnabled, logger } from \"./logger.js\";\nimport { debounce, extractPageContext } from \"./page-context.js\";\nimport type { ChatConfig } from \"./types/config.js\";\nimport type {\n IframeMessageFromHost,\n IframeMessageToHost,\n Theme,\n UserIdentity,\n} from \"./types/messaging.js\";\nimport type { ToolCallEventListener, ToolCallHandler } from \"./types/tools.js\";\nimport { EMBED_PROTOCOL_VERSION } from \"./version.js\";\n\n/** localStorage key for the per-app signed session token. */\nconst SESSION_STORAGE_KEY = (appId: string) => `yak:session:${appId}`;\n/** localStorage key for the per-app active-conversation pointer. */\nconst CONVERSATION_POINTER_KEY = (appId: string) => `yak:conversation:${appId}`;\n\n/** Read a stored session token for this app, if any. SSR-safe. */\nfunction readStoredSessionToken(appId: string): string | undefined {\n if (typeof window === \"undefined\" || typeof window.localStorage === \"undefined\") return undefined;\n try {\n return window.localStorage.getItem(SESSION_STORAGE_KEY(appId)) ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction writeStoredSessionToken(appId: string, token: string): void {\n if (typeof window === \"undefined\" || typeof window.localStorage === \"undefined\") return;\n try {\n window.localStorage.setItem(SESSION_STORAGE_KEY(appId), token);\n } catch {\n // localStorage can throw in private-browsing modes; silently ignore.\n }\n}\n\nfunction readStoredConversationPointer(appId: string): string | undefined {\n if (typeof window === \"undefined\" || typeof window.localStorage === \"undefined\") return undefined;\n try {\n return window.localStorage.getItem(CONVERSATION_POINTER_KEY(appId)) ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction writeStoredConversationPointer(appId: string, pointer: string): void {\n if (typeof window === \"undefined\" || typeof window.localStorage === \"undefined\") return;\n try {\n window.localStorage.setItem(CONVERSATION_POINTER_KEY(appId), pointer);\n } catch {\n // localStorage can throw in private-browsing modes; silently ignore.\n }\n}\n\nfunction clearStoredConversationPointer(appId: string): void {\n if (typeof window === \"undefined\" || typeof window.localStorage === \"undefined\") return;\n try {\n window.localStorage.removeItem(CONVERSATION_POINTER_KEY(appId));\n } catch {\n // localStorage can throw in private-browsing modes; silently ignore.\n }\n}\n\n/** Production chat origin \u2014 where the widget iframe loads from by default. */\nconst DEFAULT_CHAT_ORIGIN = \"https://chat.yak.io\";\n\n// Local development flag, set by yak's own apps to point the widget at a chat\n// UI running on localhost. `__YAK_LOGGING_ENABLED__` toggles verbose SDK logs.\ndeclare global {\n interface Window {\n __YAK_INTERNAL_DEV__?: boolean;\n __YAK_LOGGING_ENABLED__?: boolean;\n }\n}\n\n/**\n * Resolves the iframe origin when no explicit `origin` is configured. Points at\n * a local chat UI during yak's own local development; production otherwise.\n * Integrators override the result with the `origin` config option.\n */\nfunction getDefaultIframeOrigin(): string {\n if (\n typeof window !== \"undefined\" &&\n (window.location.hostname === \"localhost\" || window.location.hostname === \"127.0.0.1\") &&\n typeof window.__YAK_INTERNAL_DEV__ !== \"undefined\"\n ) {\n return \"http://localhost:3001\";\n }\n return DEFAULT_CHAT_ORIGIN;\n}\n\nexport interface YakClientConfig {\n appId: string;\n /**\n * Override the origin the chat widget iframe loads from. Defaults to\n * `https://chat.yak.io`. Most integrators never set this \u2014 it exists for\n * non-production environments (e.g. a chat UI running on localhost).\n */\n origin?: string;\n /**\n * Handler for tool calls from the chat widget.\n * The consuming platform decides how to execute (browser, server fetch, etc.)\n *\n * @example Browser-only execution\n * ```ts\n * onToolCall: async (name, args) => {\n * if (name === \"ui.scrollTo\") {\n * document.getElementById(args.id)?.scrollIntoView();\n * return { success: true };\n * }\n * throw new Error(`Unknown tool: ${name}`);\n * }\n * ```\n *\n * @example Server delegation\n * ```ts\n * onToolCall: async (name, args) => {\n * const res = await fetch(\"/api/yak/tools\", {\n * method: \"POST\",\n * body: JSON.stringify({ name, args }),\n * });\n * const data = await res.json();\n * if (!data.ok) throw new Error(data.error);\n * return data.result;\n * }\n * ```\n */\n onToolCall?: ToolCallHandler;\n theme?: Theme;\n chatConfig?: ChatConfig;\n onRedirect?: (path: string) => void;\n onClose?: () => void;\n onReady?: () => void;\n /**\n * Called after every tool call completes (success or failure).\n * Useful for page-level cache invalidation based on which tools were called.\n *\n * @example\n * ```ts\n * onToolCallComplete: (event) => {\n * if (event.ok && event.name.startsWith(\"order.\")) {\n * queryClient.invalidateQueries({ queryKey: [\"orders\"] });\n * }\n * }\n * ```\n */\n onToolCallComplete?: ToolCallEventListener;\n /** Chat configuration options */\n options?: {\n /** Disable the restart session button in the header */\n disableRestartButton?: boolean;\n };\n /**\n * Signed end-user identity. When supplied, the widget persists conversations\n * server-side keyed to this user and surfaces a history pane. The `hash`\n * must be HMAC-SHA256(apiSecret, id) computed on the integrator's backend \u2014\n * never expose `apiSecret` to the browser.\n *\n * @example\n * ```ts\n * // Integrator backend (Node.js)\n * const hash = crypto\n * .createHmac(\"sha256\", process.env.YAK_API_SECRET)\n * .update(currentUser.id)\n * .digest(\"hex\");\n *\n * // Browser\n * new YakClient({\n * appId: \"app_abc\",\n * user: { id: currentUser.id, hash },\n * });\n * ```\n */\n user?: UserIdentity;\n}\n\nexport class YakClient {\n private config: YakClientConfig;\n private iframeWindow: Window | null = null;\n private isWidgetOpen = false;\n private readyTarget: { window: Window; origin: string } | null = null;\n private unexpectedOriginLogged = false;\n private lastUrl = \" \";\n private debouncedSendContext: () => void;\n private observer: MutationObserver | null = null;\n\n constructor(config: YakClientConfig) {\n this.config = config;\n this.debouncedSendContext = debounce(() => {\n logger.debug(\"DOM mutation detected, sending page context\");\n this.sendPageContext();\n }, 2000);\n }\n\n public updateConfig(newConfig: Partial<YakClientConfig>) {\n this.config = { ...this.config, ...newConfig };\n // Resend config when the iframe is ready and we have anything to deliver \u2014\n // tool/route manifests, or a user identity that needs to land in the\n // widget so persistence/history light up.\n if (this.readyTarget && (this.config.chatConfig || this.config.user)) {\n this.sendConfigToIframe(this.readyTarget.window, this.readyTarget.origin);\n }\n }\n\n /**\n * Get the iframe origin URL (base URL for the chat widget). Recomputed on\n * each call so environment-dependent defaults resolve correctly.\n */\n public getIframeOrigin(): string {\n return this.config.origin ?? getDefaultIframeOrigin();\n }\n\n /**\n * Get the full iframe embed URL for the chatbot\n *\n * @example\n * ```ts\n * const client = new YakClient({ appId: \"my-app\" });\n * const iframeSrc = client.getEmbedUrl();\n * // Returns: \"https://chat.yak.io/embed/v1/my-app\"\n * ```\n */\n public getEmbedUrl(): string {\n const origin = this.getIframeOrigin();\n const baseUrl = `${origin}/embed/v${EMBED_PROTOCOL_VERSION}/${encodeURIComponent(this.config.appId)}`;\n\n const params = new URLSearchParams();\n const theme = this.config.theme;\n\n if (theme?.colorMode && theme.colorMode !== \"system\") {\n params.set(\"colorMode\", theme.colorMode);\n }\n\n this.appendThemeColors(params, theme?.light, \"light\");\n this.appendThemeColors(params, theme?.dark, \"dark\");\n\n // Pass debug flag to iframe so it can enable logging immediately\n if (isYakLoggingEnabled()) {\n params.set(\"yakDebug\", \"1\");\n }\n\n const queryString = params.toString();\n return queryString ? `${baseUrl}?${queryString}` : baseUrl;\n }\n\n /**\n * Append theme color parameters to a URLSearchParams object\n */\n private appendThemeColors(\n params: URLSearchParams,\n colors: Theme[\"light\"] | Theme[\"dark\"] | undefined,\n prefix: \"light\" | \"dark\"\n ): void {\n if (!colors) return;\n const map: [string, string | undefined][] = [\n [`${prefix}Bg`, colors.background],\n [`${prefix}Border`, colors.border],\n [`${prefix}MessageBg`, colors.messageBackground],\n [`${prefix}Placeholder`, colors.placeholderColor],\n [`${prefix}SubmitBtn`, colors.submitButtonColor],\n [`${prefix}SubmitBtnText`, colors.submitButtonTextColor],\n [`${prefix}HeaderIcon`, colors.headerIconColor],\n ];\n for (const [key, value] of map) {\n if (value) params.set(key, value);\n }\n }\n\n /**\n * Get the app ID\n */\n public getAppId(): string {\n return this.config.appId;\n }\n\n /**\n * Get the current theme configuration\n */\n public getTheme(): Theme | undefined {\n return this.config.theme;\n }\n\n /**\n * Send a prompt message to the chatbot iframe\n * Note: The iframe must be ready to receive messages (onReady callback must have fired)\n *\n * @example\n * ```ts\n * const client = new YakClient({ appId: \"my-app\", onReady: () => {\n * client.sendPrompt(\"Help me with my order\");\n * }});\n * ```\n */\n public sendPrompt(prompt: string): void {\n const target = this.getActiveTarget();\n if (!target) {\n logger.warn(\"Cannot send prompt: iframe not ready\");\n return;\n }\n\n const message: IframeMessageFromHost = {\n type: \"yak:prompt\",\n payload: { prompt },\n };\n target.window.postMessage(message, target.origin);\n }\n\n /**\n * Send a focus request to the chatbot iframe\n * This will focus the chat input field\n */\n public sendFocus(): void {\n const target = this.getActiveTarget();\n if (!target) {\n logger.warn(\"Cannot send focus: iframe not ready\");\n return;\n }\n\n const message: IframeMessageFromHost = {\n type: \"yak:focus\",\n };\n target.window.postMessage(message, target.origin);\n }\n\n /**\n * The live postMessage target. Prefers `readyTarget` \u2014 the window that\n * completed the `yak:ready` handshake \u2014 and falls back to the iframe's\n * `contentWindow`. The two are normally identical, but they're populated by\n * different events (the ready postMessage vs. the DOM `load` event) and can\n * briefly diverge (e.g. a remount under React StrictMode, where `isReady`\n * goes true before \u2014 or without \u2014 the load event repopulating\n * `iframeWindow`). Trusting `readyTarget` keeps `sendFocus`/`sendPrompt`\n * consistent with `isReady()` and avoids spurious \"iframe not ready\" warns.\n */\n private getActiveTarget(): { window: Window; origin: string } | null {\n if (this.readyTarget) return this.readyTarget;\n if (this.iframeWindow) return { window: this.iframeWindow, origin: this.getIframeOrigin() };\n return null;\n }\n\n /**\n * Check if the iframe is ready to receive messages\n */\n public isReady(): boolean {\n return this.readyTarget !== null;\n }\n\n public setIframeWindow(window: Window | null) {\n this.iframeWindow = window;\n if (!window) {\n this.readyTarget = null;\n }\n }\n\n public setWidgetOpen(isOpen: boolean) {\n this.isWidgetOpen = isOpen;\n if (isOpen && this.readyTarget && this.config.chatConfig) {\n this.sendConfigToIframe(this.readyTarget.window, this.readyTarget.origin);\n }\n\n if (isOpen) {\n this.startObserving();\n } else {\n this.stopObserving();\n }\n }\n\n public mount() {\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"message\", this.handleMessage);\n window.addEventListener(\"popstate\", this.handlePopState);\n }\n }\n\n public unmount() {\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"message\", this.handleMessage);\n window.removeEventListener(\"popstate\", this.handlePopState);\n }\n this.stopObserving();\n }\n\n private startObserving() {\n if (typeof window === \"undefined\" || !this.iframeWindow) return;\n\n // Initial check\n const currentUrl = window.location.href;\n if (currentUrl !== this.lastUrl) {\n this.lastUrl = currentUrl;\n logger.debug(\"URL changed, sending page context\");\n this.sendPageContext();\n }\n\n if (this.observer) return;\n\n this.observer = new MutationObserver((mutations) => {\n const hasSubstantialChanges = mutations.some(\n (mutation) =>\n mutation.type === \"childList\" &&\n (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)\n );\n\n if (hasSubstantialChanges) {\n this.debouncedSendContext();\n }\n });\n\n this.observer.observe(document.body, {\n childList: true,\n subtree: true,\n });\n }\n\n private stopObserving() {\n if (this.observer) {\n this.observer.disconnect();\n this.observer = null;\n }\n }\n\n private handlePopState = () => {\n logger.debug(\"Navigation detected, sending page context\");\n this.sendPageContext();\n };\n\n private handleMessage = (event: MessageEvent) => {\n if (typeof window === \"undefined\") return;\n if (!this.isWidgetOpen && event.data?.type !== \"yak:ready\") {\n return;\n }\n\n const hostOrigin = window.location.origin;\n const allowedOrigins = new Set<string>();\n allowedOrigins.add(this.getIframeOrigin());\n if (hostOrigin) {\n allowedOrigins.add(hostOrigin);\n }\n\n if (!allowedOrigins.has(event.origin)) {\n if (!this.unexpectedOriginLogged) {\n logger.warn(\n `Ignoring message from unexpected origin: ${event.origin}, allowed: ${Array.from(allowedOrigins).join(\", \")}`\n );\n this.unexpectedOriginLogged = true;\n }\n return;\n }\n\n // Validate message structure\n if (!event.data || typeof event.data !== \"object\" || !(\"type\" in event.data)) {\n // Filter out known browser extension messages\n const data = event.data as Record<string, unknown>;\n const isReactDevTools =\n data?.source === \"react-devtools-content-script\" ||\n data?.source === \"react-devtools-bridge\" ||\n data?.source === \"react-devtools-inject-backend\";\n const isReduxDevTools = data?.source === \"@devtools-page\";\n\n if (isReactDevTools || isReduxDevTools) {\n return;\n }\n return;\n }\n\n const message = event.data as IframeMessageToHost;\n const targetWindow =\n (event.source && \"postMessage\" in event.source ? (event.source as Window) : null) ??\n this.iframeWindow;\n const targetOrigin = this.getIframeOrigin();\n\n logger.debug(\"Message received from iframe:\", message.type);\n\n switch (message.type) {\n case \"yak:ready\": {\n logger.debug(\"Iframe ready, sending config\");\n\n if (targetWindow) {\n this.readyTarget = { window: targetWindow, origin: targetOrigin };\n this.sendConfigToIframe(targetWindow, targetOrigin);\n\n // Send initial page context after config\n setTimeout(() => this.sendPageContext(), 100);\n\n // Mark iframe as ready after config is sent\n setTimeout(() => this.config.onReady?.(), 200);\n } else {\n logger.warn(\"Unable to send config: iframe window not registered yet\");\n }\n break;\n }\n\n case \"yak:tool_call\": {\n const { id, name, args } = message.payload;\n void this.handleToolCall(id, name, args);\n break;\n }\n\n case \"yak:redirect\": {\n const { path } = message.payload;\n logger.debug(\"Redirect request received:\", path);\n\n if (!this.isAllowedRedirect(path)) {\n logger.warn(\"Blocked potentially unsafe redirect:\", path);\n break;\n }\n\n if (this.config.onRedirect) {\n this.config.onRedirect(path);\n } else if (typeof window !== \"undefined\") {\n window.location.assign(path);\n }\n break;\n }\n\n case \"yak:session\": {\n const { sessionToken } = message.payload;\n logger.debug(\"Session token received from iframe; persisting\");\n writeStoredSessionToken(this.config.appId, sessionToken);\n break;\n }\n\n case \"yak:conversation\": {\n const { pointer } = message.payload;\n if (pointer === null) {\n logger.debug(\"Conversation pointer cleared by iframe\");\n clearStoredConversationPointer(this.config.appId);\n } else {\n logger.debug(\"Conversation pointer received from iframe; persisting\");\n writeStoredConversationPointer(this.config.appId, pointer);\n }\n break;\n }\n\n case \"yak:close\": {\n logger.debug(\"Close message received from iframe\");\n this.config.onClose?.();\n break;\n }\n\n default:\n logger.debug(\"Unknown message type:\", (message as { type: string }).type);\n break;\n }\n };\n\n private sendConfigToIframe(targetWindow: Window, targetOrigin: string) {\n // Get logging enabled state from window flag\n const loggingEnabled =\n typeof window !== \"undefined\" && typeof window.__YAK_LOGGING_ENABLED__ === \"boolean\"\n ? window.__YAK_LOGGING_ENABLED__\n : undefined;\n\n const storedSessionToken = readStoredSessionToken(this.config.appId);\n const storedConversationPointer = readStoredConversationPointer(this.config.appId);\n\n const configMessage: IframeMessageFromHost = {\n type: \"yak:config\",\n payload: {\n version: EMBED_PROTOCOL_VERSION,\n appId: this.config.appId,\n theme: this.config.theme,\n toolManifest: this.config.chatConfig?.tools ?? undefined,\n routeManifest: this.config.chatConfig?.routes ?? undefined,\n options: this.config.options,\n loggingEnabled,\n user: this.config.user,\n sessionToken: storedSessionToken,\n conversationPointer: storedConversationPointer,\n },\n };\n\n logger.debug(\"Posting config to iframe origin:\", {\n origin: targetOrigin,\n version: EMBED_PROTOCOL_VERSION,\n hasToolManifest: Boolean(this.config.chatConfig?.tools),\n toolCount: this.config.chatConfig?.tools?.tools.length ?? 0,\n hasRouteManifest: Boolean(this.config.chatConfig?.routes),\n });\n\n targetWindow.postMessage(configMessage, targetOrigin);\n }\n\n private sendPageContext() {\n if (!this.iframeWindow) return;\n\n try {\n const pageContext = extractPageContext();\n const message: IframeMessageFromHost = {\n type: \"yak:page_context\",\n payload: pageContext,\n };\n logger.debug(\"Sending page context to iframe:\", {\n url: pageContext.url,\n title: pageContext.title,\n textLength: pageContext.text.length,\n });\n this.iframeWindow.postMessage(message, this.getIframeOrigin());\n } catch (error) {\n logger.error(\"Error extracting page context:\", error);\n }\n }\n\n private async handleToolCall(id: string, name: string, args: unknown): Promise<void> {\n logger.debug(`Tool call received: ${name}`, { id, args });\n\n if (!this.config.onToolCall) {\n logger.error(\"Tool call received but no onToolCall handler configured\");\n this.sendToolResultToIframe(id, false, undefined, \"No tool call handler configured\");\n this.config.onToolCallComplete?.({\n name,\n args,\n ok: false,\n error: \"No tool call handler configured\",\n });\n return;\n }\n\n try {\n const result = await this.config.onToolCall(name, args);\n logger.debug(`Tool call succeeded: ${name}`, { id });\n this.sendToolResultToIframe(id, true, result);\n this.config.onToolCallComplete?.({ name, args, ok: true, result });\n } catch (error) {\n const errorMessage = this.extractErrorMessage(error);\n // A thrown tool error is the documented way to surface a failure to the\n // model (see the adapter `execute` contract) \u2014 it's captured, returned to\n // the widget, and reported to the integrator via `onToolCallComplete`.\n // That makes it expected data flow, not a console-worthy fault, so log it\n // at debug to avoid noise in the host page's console.\n logger.debug(`Tool call failed: ${name}`, { id, error });\n this.sendToolResultToIframe(id, false, undefined, errorMessage);\n this.config.onToolCallComplete?.({ name, args, ok: false, error: errorMessage });\n }\n }\n\n private sendToolResultToIframe(id: string, ok: true, result: unknown): void;\n private sendToolResultToIframe(id: string, ok: false, result: undefined, error: string): void;\n private sendToolResultToIframe(id: string, ok: boolean, result: unknown, error?: string): void {\n if (!this.iframeWindow) return;\n\n if (ok) {\n // Serialize result to remove non-cloneable values (functions, etc.)\n // postMessage uses structured clone which cannot handle functions\n const safeResult = this.toSerializable(result);\n const message: IframeMessageFromHost = {\n type: \"yak:tool_result\",\n payload: { id, ok: true, result: safeResult },\n };\n this.iframeWindow.postMessage(message, this.getIframeOrigin());\n } else {\n const message: IframeMessageFromHost = {\n type: \"yak:tool_result\",\n payload: { id, ok: false, error: error ?? \"Unknown error\" },\n };\n this.iframeWindow.postMessage(message, this.getIframeOrigin());\n }\n }\n\n /**\n * Convert a value to a serializable form by stripping functions and other non-cloneable values.\n * Uses JSON.parse(JSON.stringify()) which handles most cases.\n */\n private toSerializable(value: unknown): unknown {\n if (value === undefined || value === null) {\n return value;\n }\n try {\n return JSON.parse(JSON.stringify(value));\n } catch {\n // If JSON serialization fails, return a string representation\n logger.warn(\"Failed to serialize tool result, returning string representation\");\n return String(value);\n }\n }\n\n private extractErrorMessage(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n if (typeof error === \"string\") {\n return error;\n }\n return \"Unknown error\";\n }\n\n /**\n * Validates that a redirect path is safe (relative path or same-origin).\n * Blocks absolute URLs to external domains to prevent open redirect attacks.\n */\n private isAllowedRedirect(path: string): boolean {\n // Allow relative paths that don't start with // (protocol-relative URLs)\n if (path.startsWith(\"/\") && !path.startsWith(\"//\")) {\n return true;\n }\n // Allow hash-only and query-only paths\n if (path.startsWith(\"#\") || path.startsWith(\"?\")) {\n return true;\n }\n // For absolute URLs, verify same origin\n if (typeof window !== \"undefined\") {\n try {\n const url = new URL(path, window.location.origin);\n return url.origin === window.location.origin;\n } catch {\n // Invalid URL - block it\n return false;\n }\n }\n // In non-browser environments, only allow relative paths\n return false;\n }\n}\n", "/**\n * Pure state machine for a single voice session.\n *\n * The reducer has no DOM or WebRTC dependencies \u2014 it can be unit-tested by\n * driving events through `voiceReducer` and checking the resulting state.\n *\n * The companion `handleRealtimeMessage` parses an OpenAI Realtime data-channel\n * message and dispatches reducer events plus side effects (tool dispatch,\n * sending follow-up events back over the data channel). Side effects are\n * delegated to the injected `RealtimeMessageContext` so the function is\n * testable with a plain in-memory mock.\n */\n\n/**\n * Lifecycle of a voice session, in order:\n * - `idle` \u2014 no session (the starting and stopped state).\n * - `connecting` \u2014 establishing the WebRTC connection.\n * - `listening` \u2014 connected and capturing the user's speech.\n * - `thinking` \u2014 the model is processing / generating a response.\n * - `speaking` \u2014 the assistant is playing audio back.\n * - `error` \u2014 the session failed; see {@link VoiceMachine.errorMessage}.\n */\nexport type VoiceState = \"idle\" | \"connecting\" | \"listening\" | \"thinking\" | \"speaking\" | \"error\";\n\nexport type VoiceEvent =\n | { type: \"start\" }\n | { type: \"connected\" }\n /** An assistant-initiated turn was requested (e.g. the opening greeting). */\n | { type: \"response_requested\" }\n | { type: \"speech_started\" }\n | { type: \"speech_stopped\" }\n | { type: \"audio_delta\" }\n | { type: \"audio_stopped\" }\n | { type: \"stop\" }\n | { type: \"error\"; message: string };\n\n/** Snapshot of a voice session's state machine. */\nexport interface VoiceMachine {\n /** Current lifecycle state of the session. */\n state: VoiceState;\n /** Human-readable failure reason \u2014 set only when `state === \"error\"`. */\n errorMessage?: string;\n}\n\nexport const INITIAL_VOICE_MACHINE: VoiceMachine = { state: \"idle\" };\n\nexport function voiceReducer(machine: VoiceMachine, event: VoiceEvent): VoiceMachine {\n switch (event.type) {\n case \"start\":\n return machine.state === \"idle\" ? { state: \"connecting\" } : machine;\n case \"connected\":\n return machine.state === \"connecting\" ? { state: \"listening\" } : machine;\n case \"response_requested\":\n // The assistant is generating an unprompted turn (the opening greeting).\n // Move to `thinking` so the subsequent `audio_delta` lands on `speaking`,\n // matching a normal turn; no-op if we're not idling in `listening`.\n return machine.state === \"listening\" ? { state: \"thinking\" } : machine;\n case \"speech_started\":\n if (machine.state === \"idle\" || machine.state === \"error\") return machine;\n return { state: \"listening\" };\n case \"speech_stopped\":\n return machine.state === \"listening\" ? { state: \"thinking\" } : machine;\n case \"audio_delta\":\n if (machine.state === \"thinking\" || machine.state === \"speaking\") {\n return { state: \"speaking\" };\n }\n return machine;\n case \"audio_stopped\":\n return machine.state === \"speaking\" ? { state: \"listening\" } : machine;\n case \"stop\":\n return { state: \"idle\" };\n case \"error\":\n return { state: \"error\", errorMessage: event.message };\n default: {\n const _exhaustive: never = event;\n void _exhaustive;\n return machine;\n }\n }\n}\n\n/**\n * Per-`response.done` token usage emitted by OpenAI's Realtime API.\n * The SDK accumulates these across the session and ships the totals to the\n * `session-event` stop endpoint so billing has the dimensions it needs.\n */\nexport interface RealtimeResponseUsage {\n /** Total input tokens for this response (text + audio combined). */\n inputTokens?: number;\n /** Cached input tokens \u2014 subset of `inputTokens`. */\n cachedInputTokens?: number;\n /** Total output tokens for this response (text + audio combined). */\n outputTokens?: number;\n /** Audio-input tokens (subset of `inputTokens`). */\n audioInputTokens?: number;\n /** Audio-output tokens (subset of `outputTokens`). */\n audioOutputTokens?: number;\n /** Text-input tokens (subset of `inputTokens`). */\n textInputTokens?: number;\n /** Text-output tokens (subset of `outputTokens`). */\n textOutputTokens?: number;\n}\n\nexport interface RealtimeMessageContext {\n send: (event: VoiceEvent) => void;\n sendData: (payload: unknown) => void;\n dispatchToolCall: (name: string, args: unknown) => Promise<unknown>;\n isDispatched: (callId: string) => boolean;\n markDispatched: (callId: string) => void;\n /** Forward a per-response usage payload to the session for accumulation. */\n recordUsage?: (usage: RealtimeResponseUsage) => void;\n}\n\ninterface RealtimeFunctionCallItem {\n type: \"function_call\";\n call_id?: string;\n name?: string;\n arguments?: string;\n}\n\ninterface RealtimeResponseDoneItem {\n type?: string;\n call_id?: string;\n name?: string;\n arguments?: string;\n}\n\ninterface RealtimeUsageTokenDetails {\n cached_tokens?: number;\n audio_tokens?: number;\n text_tokens?: number;\n}\n\ninterface RealtimeUsage {\n input_tokens?: number;\n output_tokens?: number;\n total_tokens?: number;\n input_token_details?: RealtimeUsageTokenDetails;\n output_token_details?: RealtimeUsageTokenDetails;\n}\n\ninterface RealtimeResponseDone {\n output?: RealtimeResponseDoneItem[];\n usage?: RealtimeUsage;\n}\n\ninterface RealtimeMessage {\n type?: string;\n response?: RealtimeResponseDone;\n error?: { message?: string };\n}\n\nfunction isFunctionCall(item: RealtimeResponseDoneItem): item is RealtimeFunctionCallItem {\n return item.type === \"function_call\";\n}\n\nfunction parseToolArgs(raw: string | undefined): unknown {\n if (!raw) return {};\n try {\n return JSON.parse(raw);\n } catch {\n return {};\n }\n}\n\nasync function dispatchFunctionCall(\n call: RealtimeFunctionCallItem,\n ctx: RealtimeMessageContext\n): Promise<void> {\n const callId = call.call_id;\n const name = call.name;\n if (!callId || !name) return;\n if (ctx.isDispatched(callId)) return;\n ctx.markDispatched(callId);\n\n const args = parseToolArgs(call.arguments);\n\n let output: string;\n try {\n const result = await ctx.dispatchToolCall(name, args);\n output = JSON.stringify(result ?? null);\n } catch (error) {\n output = JSON.stringify({\n error: error instanceof Error ? error.message : \"Tool execution failed\",\n });\n }\n\n ctx.sendData({\n type: \"conversation.item.create\",\n item: { type: \"function_call_output\", call_id: callId, output },\n });\n ctx.sendData({ type: \"response.create\" });\n}\n\nfunction extractUsage(raw: RealtimeUsage | undefined): RealtimeResponseUsage | null {\n if (!raw) return null;\n const usage: RealtimeResponseUsage = {};\n if (typeof raw.input_tokens === \"number\") usage.inputTokens = raw.input_tokens;\n if (typeof raw.output_tokens === \"number\") usage.outputTokens = raw.output_tokens;\n const inDetails = raw.input_token_details;\n if (inDetails) {\n if (typeof inDetails.cached_tokens === \"number\") {\n usage.cachedInputTokens = inDetails.cached_tokens;\n }\n if (typeof inDetails.audio_tokens === \"number\") {\n usage.audioInputTokens = inDetails.audio_tokens;\n }\n if (typeof inDetails.text_tokens === \"number\") {\n usage.textInputTokens = inDetails.text_tokens;\n }\n }\n const outDetails = raw.output_token_details;\n if (outDetails) {\n if (typeof outDetails.audio_tokens === \"number\") {\n usage.audioOutputTokens = outDetails.audio_tokens;\n }\n if (typeof outDetails.text_tokens === \"number\") {\n usage.textOutputTokens = outDetails.text_tokens;\n }\n }\n return Object.keys(usage).length > 0 ? usage : null;\n}\n\nasync function handleResponseDone(\n response: RealtimeResponseDone | undefined,\n ctx: RealtimeMessageContext\n): Promise<void> {\n const usage = extractUsage(response?.usage);\n if (usage && ctx.recordUsage) {\n try {\n ctx.recordUsage(usage);\n } catch {\n // recordUsage is best-effort; never let it break the session loop.\n }\n }\n\n const calls = (response?.output ?? []).filter(isFunctionCall);\n for (const call of calls) {\n await dispatchFunctionCall(call, ctx);\n }\n}\n\nexport async function handleRealtimeMessage(\n raw: string,\n ctx: RealtimeMessageContext\n): Promise<void> {\n let message: RealtimeMessage;\n try {\n message = JSON.parse(raw) as RealtimeMessage;\n } catch {\n return;\n }\n\n switch (message.type) {\n case \"input_audio_buffer.speech_started\":\n ctx.send({ type: \"speech_started\" });\n return;\n case \"input_audio_buffer.speech_stopped\":\n ctx.send({ type: \"speech_stopped\" });\n return;\n case \"response.output_audio_transcript.delta\":\n case \"response.audio_transcript.delta\":\n ctx.send({ type: \"audio_delta\" });\n return;\n case \"output_audio_buffer.stopped\":\n case \"response.output_audio_buffer.stopped\":\n ctx.send({ type: \"audio_stopped\" });\n return;\n case \"response.done\":\n await handleResponseDone(message.response, ctx);\n return;\n case \"error\":\n ctx.send({\n type: \"error\",\n message: message.error?.message ?? \"Voice session error\",\n });\n return;\n default:\n return;\n }\n}\n", "/** OpenAI/Realtime function names must match `^[a-zA-Z0-9_-]{1,64}$`. */\nconst MAX_TOOL_NAME_LENGTH = 64;\n\n/**\n * Convert a host tool name into a model-facing function name.\n *\n * We sanitise to the allowed function-name charset (rather than hashing to an opaque\n * `yt_<hex>`), so the model keeps the semantic signal of a readable name \u2014 e.g.\n * `orders.list` \u2192 `orders_list`.\n *\n * This is a pure per-name transform; uniqueness across a manifest (and avoiding the\n * reserved `redirect` name / `mcp__` namespace) is handled by the caller via\n * {@link uniqueToolId}. Each runtime (the chat-ui iframe and this SDK's voice session)\n * decorates and reverse-maps with its own manifest, so the ids never need to match\n * across paths \u2014 only to be self-consistent within one.\n */\nexport function generateToolId(originalName: string): string {\n const sanitized = originalName.replace(/[^A-Za-z0-9_-]/g, \"_\").slice(0, MAX_TOOL_NAME_LENGTH);\n return sanitized.length > 0 ? sanitized : \"tool\";\n}\n\n/** The MCP namespace prefix the voice/chat dispatchers route on. */\nconst MCP_NAMESPACE_PREFIX = \"mcp__\";\n\n/**\n * Resolve a collision-free, model-safe id for a host tool name, given the ids already\n * taken in this manifest (seed `used` with reserved names like `redirect`). Host ids must\n * never start with the `mcp__` namespace, which the dispatcher routes on.\n */\nexport function uniqueToolId(originalName: string, used: Set<string>): string {\n let base = generateToolId(originalName);\n if (base.startsWith(MCP_NAMESPACE_PREFIX)) {\n base = `t_${base}`.slice(0, MAX_TOOL_NAME_LENGTH);\n }\n if (!used.has(base)) return base;\n\n for (let i = 2; i < 1000; i++) {\n const suffix = `_${i}`;\n const candidate = `${base.slice(0, MAX_TOOL_NAME_LENGTH - suffix.length)}${suffix}`;\n if (!used.has(candidate)) return candidate;\n }\n // Pathological fallback \u2014 vanishingly unlikely in practice.\n return `${base.slice(0, 56)}_${used.size}`;\n}\n", "import { logger } from \"./logger.js\";\nimport { extractPageContext } from \"./page-context.js\";\nimport { uniqueToolId } from \"./tool-name.js\";\nimport type { ChatConfig, ChatConfigProvider } from \"./types/config.js\";\nimport type { PageContext } from \"./types/messaging.js\";\nimport type { ToolCallHandler, ToolDefinition } from \"./types/tools.js\";\nimport {\n handleRealtimeMessage,\n INITIAL_VOICE_MACHINE,\n type RealtimeMessageContext,\n type RealtimeResponseUsage,\n type VoiceMachine,\n voiceReducer,\n} from \"./voice-machine.js\";\n\nconst DEFAULT_REALTIME_MODEL = \"gpt-realtime\";\nconst REALTIME_CALLS_URL = \"https://api.openai.com/v1/realtime/calls\";\nconst DEFAULT_API_ORIGIN = \"https://chat.yak.io\";\n\n// Local development flag, set by yak's own apps to point voice sessions at a\n// chat UI running on localhost.\ndeclare global {\n interface Window {\n __YAK_INTERNAL_DEV__?: boolean;\n }\n}\n\n/**\n * Resolves the API origin when no explicit `apiOrigin` is configured. Points at\n * a local chat UI during yak's own local development; production otherwise.\n */\nfunction getDefaultApiOrigin(): string {\n if (\n typeof window !== \"undefined\" &&\n (window.location.hostname === \"localhost\" || window.location.hostname === \"127.0.0.1\") &&\n typeof window.__YAK_INTERNAL_DEV__ !== \"undefined\"\n ) {\n return \"http://localhost:3001\";\n }\n return DEFAULT_API_ORIGIN;\n}\n\nexport type VoiceStateListener = (machine: VoiceMachine) => void;\n\nexport interface YakVoiceSessionConfig {\n appId: string;\n /** Tool call handler. Same shape as `YakClientConfig.onToolCall`. */\n onToolCall?: ToolCallHandler;\n onRedirect?: (path: string) => void;\n /**\n * Static chat config (routes + tools). Sent to the mint endpoint so the LLM\n * knows what tools are available. Use this OR `getConfig`.\n */\n chatConfig?: ChatConfig;\n /**\n * Async provider for chat config. Called on every session start \u2014 useful when\n * tools/routes depend on the current page or user. Takes precedence over\n * `chatConfig` if both are provided.\n */\n getConfig?: ChatConfigProvider;\n /**\n * Override the API origin (where voice sessions are minted). Defaults to\n * `https://chat.yak.io`. Most integrators never set this.\n */\n apiOrigin?: string;\n}\n\ninterface MintResponse {\n clientSecret: string;\n expiresAt: number;\n voiceSessionId: string;\n /**\n * Whether to auto-greet on connect. Driven by the app's voice intro setting:\n * `false` only when the operator chose \"no intro\". A missing flag (older\n * server) is treated as `true` so existing apps keep greeting.\n */\n autoGreet?: boolean;\n}\n\ninterface SessionResources {\n pc: RTCPeerConnection | null;\n dataChannel: RTCDataChannel | null;\n micStream: MediaStream | null;\n audioElement: HTMLAudioElement | null;\n voiceSessionId: string | null;\n}\n\nconst EMPTY_RESOURCES: SessionResources = {\n pc: null,\n dataChannel: null,\n micStream: null,\n audioElement: null,\n voiceSessionId: null,\n};\n\n/**\n * Manages a single voice session against the OpenAI Realtime API.\n *\n * Runs entirely on the host page \u2014 no iframe. The mint endpoint\n * (`POST /api/voice/realtime-token`) is called cross-origin to chat.yak.io;\n * origin validation happens server-side via the app's `allowedOrigins`.\n *\n * Tool calls are dispatched to the configured handlers \u2014 the same ones\n * customers wire up for `YakEmbed`. The class manages WebRTC lifecycle,\n * full teardown on stop, and `pagehide` cleanup via `navigator.sendBeacon`.\n */\ninterface AccumulatedUsage {\n inputTokens: number;\n cachedInputTokens: number;\n outputTokens: number;\n audioInputTokens: number;\n audioOutputTokens: number;\n textInputTokens: number;\n textOutputTokens: number;\n responseCount: number;\n}\n\nfunction emptyUsage(): AccumulatedUsage {\n return {\n inputTokens: 0,\n cachedInputTokens: 0,\n outputTokens: 0,\n audioInputTokens: 0,\n audioOutputTokens: 0,\n textInputTokens: 0,\n textOutputTokens: 0,\n responseCount: 0,\n };\n}\n\nexport class YakVoiceSession {\n private config: YakVoiceSessionConfig;\n private machine: VoiceMachine = INITIAL_VOICE_MACHINE;\n private resources: SessionResources = EMPTY_RESOURCES;\n private dispatchedCallIds = new Set<string>();\n private listeners = new Set<VoiceStateListener>();\n private pageHideHandler: (() => void) | null = null;\n /** Per-session token totals, accumulated from each `response.done` event. */\n private usage: AccumulatedUsage = emptyUsage();\n /**\n * Reverse map: hashed tool id (what OpenAI calls back with) \u2192 original host\n * tool name (what `onToolCall` expects). Populated on every `start()` from\n * the resolved chat config.\n */\n private toolNameById = new Map<string, string>();\n\n constructor(config: YakVoiceSessionConfig) {\n this.config = config;\n this.attachPageHide();\n }\n\n /**\n * Resolve the API origin lazily on each call. Environment-dependent defaults\n * (e.g. a local chat UI) may not be ready at construction time, so resolving\n * eagerly would risk baking in the production URL.\n */\n private get apiOrigin(): string {\n return this.config.apiOrigin ?? getDefaultApiOrigin();\n }\n\n /** Update mutable config fields (handlers, getConfig). */\n public updateConfig(patch: Partial<YakVoiceSessionConfig>): void {\n this.config = { ...this.config, ...patch };\n }\n\n public getState(): VoiceMachine {\n return this.machine;\n }\n\n /**\n * The current API origin (defaults to `https://chat.yak.io`). Useful for\n * building URLs to static assets like the brand logo.\n */\n public getApiOrigin(): string {\n return this.apiOrigin;\n }\n\n public onStateChange(listener: VoiceStateListener): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Begin a voice session. Should be invoked from a user gesture (button\n * click) so `getUserMedia` and audio playback both have transient activation.\n */\n public async start(): Promise<void> {\n if (this.machine.state !== \"idle\") return;\n logger.debug(\"Voice: start() called\");\n this.usage = emptyUsage();\n this.dispatch({ type: \"start\" });\n\n let chatConfig: ChatConfig | undefined = this.config.chatConfig;\n if (this.config.getConfig) {\n try {\n chatConfig = await this.config.getConfig();\n logger.debug(\"Voice: getConfig() resolved\", {\n toolCount: chatConfig?.tools?.tools.length ?? 0,\n routeCount: chatConfig?.routes?.routes.length ?? 0,\n });\n } catch (err) {\n logger.warn(\"Voice: getConfig() failed\", err);\n }\n } else if (chatConfig) {\n logger.debug(\"Voice: using static chatConfig\", {\n toolCount: chatConfig.tools?.tools.length ?? 0,\n routeCount: chatConfig.routes?.routes.length ?? 0,\n });\n } else {\n logger.debug(\"Voice: no chatConfig or getConfig \u2014 only built-in tools will be available\");\n }\n\n // Decorate host tools with hash ids and build the reverse lookup so we\n // can map id-named tool calls back to the original host name when the\n // model invokes them. Mirrors the chat-ui iframe's decoration step.\n const decoratedManifest = this.buildDecoratedManifest(chatConfig);\n logger.debug(\"Voice: decorated tools\", {\n ids: decoratedManifest.tools.map((t) => `${t.id}=${t.name}`),\n });\n\n const pageContext = this.safeExtractPageContext();\n logger.debug(\"Voice: page context extracted\", {\n url: pageContext?.url,\n title: pageContext?.title,\n textLength: pageContext?.text?.length ?? 0,\n });\n\n let mint: MintResponse;\n try {\n logger.debug(\"Voice: requesting ephemeral token from mint endpoint\");\n mint = await this.mintToken(chatConfig, decoratedManifest, pageContext);\n logger.debug(\"Voice: mint succeeded\", {\n voiceSessionId: mint.voiceSessionId,\n expiresAt: mint.expiresAt,\n });\n } catch (err) {\n await this.failWith(err instanceof Error ? err.message : \"Failed to start voice session\");\n return;\n }\n\n let micStream: MediaStream;\n try {\n logger.debug(\"Voice: requesting microphone access\");\n micStream = await navigator.mediaDevices.getUserMedia({ audio: true });\n logger.debug(\"Voice: microphone access granted\");\n } catch (err) {\n const name = err instanceof Error ? err.name : \"\";\n const message =\n name === \"NotAllowedError\" || name === \"PermissionDeniedError\"\n ? \"Microphone permission was denied. Enable microphone access in your browser settings to use voice mode.\"\n : \"Could not access microphone.\";\n await this.failWith(message);\n return;\n }\n\n const pc = new RTCPeerConnection();\n const audioElement = document.createElement(\"audio\");\n audioElement.autoplay = true;\n audioElement.style.display = \"none\";\n document.body.appendChild(audioElement);\n\n pc.ontrack = (event) => {\n logger.debug(\"Voice: pc.ontrack received remote audio stream\");\n if (event.streams[0]) {\n audioElement.srcObject = event.streams[0];\n }\n };\n\n pc.oniceconnectionstatechange = () => {\n const s = pc.iceConnectionState;\n logger.debug(\"Voice: ICE connection state \u2192\", s);\n if (s === \"failed\" || s === \"disconnected\") {\n void this.failWith(`WebRTC connection ${s}`);\n }\n };\n pc.onconnectionstatechange = () => {\n logger.debug(\"Voice: peer connection state \u2192\", pc.connectionState);\n if (pc.connectionState === \"failed\") {\n void this.failWith(\"WebRTC connection failed\");\n }\n };\n\n for (const track of micStream.getAudioTracks()) {\n pc.addTrack(track, micStream);\n }\n\n const dataChannel = pc.createDataChannel(\"oai-events\");\n dataChannel.onmessage = (event) => {\n const raw = typeof event.data === \"string\" ? event.data : \"\";\n if (!raw) return;\n logger.debug(\"Voice: \u2190 data channel message\", raw.slice(0, 200));\n void handleRealtimeMessage(raw, this.buildMessageContext());\n };\n dataChannel.onopen = () => {\n logger.debug(\"Voice: data channel opened\");\n this.dispatch({ type: \"connected\" });\n // Kick off an opening greeting so the assistant speaks first instead of\n // waiting silently for the user. The minted session already carries the\n // full instructions (persona, governance, language, \"first turn\" rule),\n // so a bare `response.create` is enough \u2014 do NOT attach response-level\n // instructions here, which would replace the session instructions.\n // Skip it when the app's voice intro is \"none\" (`autoGreet === false`);\n // a missing flag defaults to greeting for back-compat.\n if (mint.autoGreet !== false) {\n this.dispatch({ type: \"response_requested\" });\n this.sendOverDataChannel({ type: \"response.create\" });\n }\n };\n dataChannel.onclose = () => {\n logger.debug(\"Voice: data channel closed\");\n };\n\n this.resources = {\n pc,\n dataChannel,\n micStream,\n audioElement,\n voiceSessionId: mint.voiceSessionId,\n };\n\n try {\n logger.debug(\"Voice: creating WebRTC offer\");\n const offer = await pc.createOffer();\n await pc.setLocalDescription(offer);\n logger.debug(\"Voice: exchanging SDP with OpenAI Realtime\");\n const answerSdp = await this.exchangeSdp(offer, mint.clientSecret);\n await pc.setRemoteDescription({ type: \"answer\", sdp: answerSdp });\n logger.debug(\"Voice: WebRTC negotiation complete\");\n } catch (err) {\n await this.failWith(\n err instanceof Error ? err.message : \"Failed to negotiate voice connection\"\n );\n return;\n }\n\n void this.postSessionEvent(\"start\", mint.voiceSessionId, pageContext);\n }\n\n /** Stop the session and tear down all resources. */\n public async stop(): Promise<void> {\n logger.debug(\"Voice: stop() called\");\n this.dispatch({ type: \"stop\" });\n await this.teardown();\n }\n\n /** Tear down everything and remove listeners. Call once before discarding the instance. */\n public destroy(): void {\n void this.teardown();\n if (this.pageHideHandler) {\n window.removeEventListener(\"pagehide\", this.pageHideHandler);\n this.pageHideHandler = null;\n }\n this.listeners.clear();\n }\n\n // \u2500\u2500 Internals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private buildMessageContext(): RealtimeMessageContext {\n return {\n send: (event) => this.dispatch(event),\n sendData: (payload) => this.sendOverDataChannel(payload),\n dispatchToolCall: (name, args) => this.routeToolCall(name, args),\n isDispatched: (id) => this.dispatchedCallIds.has(id),\n markDispatched: (id) => {\n this.dispatchedCallIds.add(id);\n },\n recordUsage: (usage) => this.accumulateUsage(usage),\n };\n }\n\n private accumulateUsage(usage: RealtimeResponseUsage): void {\n this.usage.responseCount += 1;\n if (typeof usage.inputTokens === \"number\") this.usage.inputTokens += usage.inputTokens;\n if (typeof usage.cachedInputTokens === \"number\") {\n this.usage.cachedInputTokens += usage.cachedInputTokens;\n }\n if (typeof usage.outputTokens === \"number\") this.usage.outputTokens += usage.outputTokens;\n if (typeof usage.audioInputTokens === \"number\") {\n this.usage.audioInputTokens += usage.audioInputTokens;\n }\n if (typeof usage.audioOutputTokens === \"number\") {\n this.usage.audioOutputTokens += usage.audioOutputTokens;\n }\n if (typeof usage.textInputTokens === \"number\") {\n this.usage.textInputTokens += usage.textInputTokens;\n }\n if (typeof usage.textOutputTokens === \"number\") {\n this.usage.textOutputTokens += usage.textOutputTokens;\n }\n }\n\n private sendOverDataChannel(payload: unknown): void {\n const channel = this.resources.dataChannel;\n if (!channel || channel.readyState !== \"open\") {\n logger.warn(\"Voice data channel not ready; dropping payload\");\n return;\n }\n try {\n const serialized = JSON.stringify(payload);\n logger.debug(\"Voice: \u2192 data channel send\", serialized.slice(0, 200));\n channel.send(serialized);\n } catch (err) {\n logger.warn(\"Failed to send on voice data channel\", err);\n }\n }\n\n private async routeToolCall(idOrName: string, args: unknown): Promise<unknown> {\n // The model calls us back using the decorated id (e.g. orders_list).\n // Resolve it to the original host tool name; fall back to the raw value\n // (it might be `redirect` or some non-decorated name).\n const name = this.toolNameById.get(idOrName) ?? idOrName;\n logger.debug(\"Voice: tool call dispatched\", { id: idOrName, name, args });\n // MCP tools execute server-side (the org token never reaches the browser).\n // The model calls back with the `mcp__\u2026` name minted by the server; relay\n // it to the exec endpoint and feed the result back over the data channel.\n if (name.startsWith(\"mcp__\")) {\n return await this.execMcpTool(name, args);\n }\n if (name === \"redirect\") {\n const path = (args as { path?: unknown })?.path;\n if (typeof path !== \"string\") {\n throw new Error(\"redirect tool requires a string `path` argument\");\n }\n if (this.config.onRedirect) {\n this.config.onRedirect(path);\n } else if (typeof window !== \"undefined\") {\n window.location.assign(path);\n }\n return { success: true, redirected: true, path };\n }\n if (this.config.onToolCall) {\n return await this.config.onToolCall(name, args);\n }\n throw new Error(`No handler configured for tool: ${name}`);\n }\n\n /**\n * Relay an MCP tool call to the server, which holds the org's credentials\n * and executes against the remote MCP server. The browser only ever passes\n * through the tool name, args, and the opaque result.\n */\n private async execMcpTool(toolName: string, args: unknown): Promise<unknown> {\n try {\n const res = await fetch(`${this.apiOrigin}/api/voice/mcp-exec`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n appId: this.config.appId,\n toolName,\n args: args ?? {},\n pageContext: this.safeExtractPageContext(),\n }),\n });\n if (!res.ok) {\n const body = (await res.json().catch(() => ({}))) as { error?: string };\n return { error: body.error ?? `MCP tool failed (${res.status})` };\n }\n const body = (await res.json()) as { result?: unknown };\n return body.result ?? {};\n } catch (err) {\n // The relay failed, but we hand a structured `{ error }` straight back to\n // the model to recover from \u2014 expected data flow, not a console-worthy\n // fault. Log at debug to match the text-chat tool-call path (client.ts).\n logger.debug(\"Voice: MCP tool relay failed\", err);\n return { error: \"The integration could not complete this request.\" };\n }\n }\n\n private async mintToken(\n chatConfig: ChatConfig | undefined,\n decoratedManifest: { tools: Array<ToolDefinition & { id: string }> },\n pageContext: PageContext | undefined\n ): Promise<MintResponse> {\n const res = await fetch(`${this.apiOrigin}/api/voice/realtime-token`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n appId: this.config.appId,\n pageContext,\n toolManifest: decoratedManifest,\n routeManifest: chatConfig?.routes,\n }),\n });\n if (!res.ok) {\n const body = (await res.json().catch(() => ({}))) as { error?: string };\n throw new Error(body.error ?? `Mint failed (${res.status})`);\n }\n return (await res.json()) as MintResponse;\n }\n\n /**\n * Decorate the host's tool manifest with readable, collision-free model-facing ids\n * and populate `this.toolNameById` for reverse lookup. Mirrors the decoration the\n * chat-ui iframe applies before sending tools to `/api/chat`. GraphQL/REST tools are\n * ordinary manifest entries here (contributed by their adapters), so no special-casing\n * is needed.\n */\n private buildDecoratedManifest(chatConfig: ChatConfig | undefined): {\n tools: Array<ToolDefinition & { id: string }>;\n } {\n this.toolNameById.clear();\n const used = new Set<string>([\"redirect\"]);\n\n const decoratedHostTools = (chatConfig?.tools?.tools ?? []).map((t: ToolDefinition) => {\n const id = uniqueToolId(t.name, used);\n used.add(id);\n this.toolNameById.set(id, t.name);\n return { ...t, id };\n });\n\n return { tools: decoratedHostTools };\n }\n\n private async exchangeSdp(\n offer: RTCSessionDescriptionInit,\n clientSecret: string\n ): Promise<string> {\n const sdpResponse = await fetch(`${REALTIME_CALLS_URL}?model=${DEFAULT_REALTIME_MODEL}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${clientSecret}`,\n \"Content-Type\": \"application/sdp\",\n },\n body: offer.sdp,\n });\n if (!sdpResponse.ok) {\n const body = await sdpResponse.text().catch(() => \"\");\n throw new Error(`SDP exchange failed (${sdpResponse.status}): ${body}`);\n }\n return await sdpResponse.text();\n }\n\n private buildStopEventBody(voiceSessionId: string, pageContext: PageContext | undefined) {\n return {\n appId: this.config.appId,\n voiceSessionId,\n event: \"stop\" as const,\n clientTimestamp: Date.now(),\n pageContext,\n usage: { ...this.usage },\n };\n }\n\n private async postSessionEvent(\n event: \"start\" | \"stop\",\n voiceSessionId: string,\n pageContext: PageContext | undefined\n ): Promise<void> {\n try {\n const body =\n event === \"stop\"\n ? this.buildStopEventBody(voiceSessionId, pageContext)\n : {\n appId: this.config.appId,\n voiceSessionId,\n event,\n clientTimestamp: Date.now(),\n pageContext,\n };\n await fetch(`${this.apiOrigin}/api/voice/session-event`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n keepalive: event === \"stop\",\n });\n } catch (err) {\n logger.warn(`Failed to post voice.session.${event}`, err);\n }\n }\n\n private async teardown(): Promise<void> {\n const r = this.resources;\n this.resources = EMPTY_RESOURCES;\n this.dispatchedCallIds = new Set();\n\n try {\n r.dataChannel?.close();\n } catch (err) {\n logger.warn(\"Error closing data channel\", err);\n }\n try {\n for (const track of r.micStream?.getTracks() ?? []) {\n track.stop();\n }\n } catch (err) {\n logger.warn(\"Error stopping mic tracks\", err);\n }\n try {\n for (const sender of r.pc?.getSenders() ?? []) {\n sender.track?.stop();\n }\n r.pc?.close();\n } catch (err) {\n logger.warn(\"Error closing peer connection\", err);\n }\n if (r.audioElement) {\n try {\n r.audioElement.srcObject = null;\n r.audioElement.remove();\n } catch (err) {\n logger.warn(\"Error removing audio element\", err);\n }\n }\n\n if (r.voiceSessionId) {\n await this.postSessionEvent(\"stop\", r.voiceSessionId, this.safeExtractPageContext());\n }\n }\n\n private async failWith(message: string): Promise<void> {\n logger.warn(\"Voice session error:\", message);\n this.dispatch({ type: \"error\", message });\n await this.teardown();\n }\n\n private dispatch(event: Parameters<typeof voiceReducer>[1]): void {\n const next = voiceReducer(this.machine, event);\n if (next === this.machine) return;\n this.machine = next;\n for (const listener of this.listeners) {\n try {\n listener(next);\n } catch (err) {\n logger.warn(\"Voice state listener threw\", err);\n }\n }\n }\n\n private safeExtractPageContext(): PageContext | undefined {\n try {\n return extractPageContext();\n } catch {\n return undefined;\n }\n }\n\n private attachPageHide(): void {\n if (typeof window === \"undefined\") return;\n this.pageHideHandler = () => {\n const r = this.resources;\n if (!r.voiceSessionId) return;\n const body = JSON.stringify(this.buildStopEventBody(r.voiceSessionId, undefined));\n if (navigator.sendBeacon) {\n navigator.sendBeacon(\n `${this.apiOrigin}/api/voice/session-event`,\n new Blob([body], { type: \"application/json\" })\n );\n }\n };\n window.addEventListener(\"pagehide\", this.pageHideHandler);\n }\n}\n", "import { YakClient, type YakClientConfig } from \"./client.js\";\nimport { logger } from \"./logger.js\";\nimport type { ChatConfigProvider } from \"./types/config.js\";\nimport type { IframeMessageFromHost, WidgetPosition } from \"./types/messaging.js\";\nimport { INITIAL_VOICE_MACHINE, type VoiceMachine, type VoiceState } from \"./voice-machine.js\";\nimport {\n type VoiceStateListener,\n YakVoiceSession,\n type YakVoiceSessionConfig,\n} from \"./voice-session.js\";\n\n// \u2500\u2500 Widget mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Which experiences the widget exposes:\n * - `chat` \u2014 chat icon only (opens the chat iframe panel). The default.\n * - `voice` \u2014 voice icon only (starts a WebRTC voice session).\n * - `both` \u2014 both icons, sharing one trigger pill.\n */\nexport type WidgetMode = \"chat\" | \"voice\" | \"both\";\n\n// Single source of truth for the default trigger + panel corner.\nconst DEFAULT_POSITION: WidgetPosition = \"bottom-left\";\n\n// \u2500\u2500 Trigger button configuration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type TriggerButtonConfig = {\n /** Custom color overrides for light mode */\n lightButton?: { background?: string; color?: string; border?: string };\n /** Custom color overrides for dark mode */\n darkButton?: { background?: string; color?: string; border?: string };\n};\n\n// \u2500\u2500 YakEmbed configuration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type YakEmbedConfig = YakClientConfig & {\n /** DOM element to append the widget into. Defaults to document.body. */\n target?: HTMLElement;\n /** Show the floating trigger button. Default: true */\n trigger?: boolean | TriggerButtonConfig;\n /**\n * Which experiences this widget exposes.\n * \"chat\" \u2014 chat icon only (opens the chat iframe).\n * \"voice\" \u2014 voice icon only (starts a WebRTC voice session).\n * \"both\" \u2014 both icons in one pill.\n * Default: \"chat\".\n */\n mode?: WidgetMode;\n /**\n * Async provider for chat config (routes + tools). Used by the voice\n * session on every `start()` and by the iframe via postMessage. Takes\n * precedence over the static `chatConfig` on `YakClientConfig`.\n */\n getConfig?: ChatConfigProvider;\n};\n\n// \u2500\u2500 State listener \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type YakEmbedState = {\n /** Whether the chat panel is open. */\n isOpen: boolean;\n /** Whether the chat iframe has handshaked and can receive messages. */\n isReady: boolean;\n /**\n * Whether the chat is opening but not yet interactive \u2014 `isOpen && !isReady`.\n * Stays true from the moment the panel opens until the iframe reports ready,\n * so custom triggers can show a spinner without re-deriving it.\n */\n isLoading: boolean;\n /** Whether the panel is expanded to (near) full-screen. */\n isExpanded: boolean;\n};\n\nexport type YakEmbedStateListener = (state: YakEmbedState) => void;\n\n// \u2500\u2500 Inline SVG icons (lucide) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst MESSAGE_CIRCLE_SVG = `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M7.9 20A9 9 0 1 0 4 16.1L2 22Z\"/></svg>`;\n\nconst AUDIO_LINES_SVG = `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M2 10v3\"/><path d=\"M6 6v11\"/><path d=\"M10 3v18\"/><path d=\"M14 8v7\"/><path d=\"M18 5v13\"/><path d=\"M22 10v3\"/></svg>`;\n\nconst STOP_SVG = `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"2\"/></svg>`;\n\nconst VOICE_STATE_ARIA: Record<VoiceState, string> = {\n idle: \"Start voice mode\",\n connecting: \"Connecting voice session\",\n listening: \"Voice listening \u2014 tap to stop\",\n thinking: \"Voice thinking \u2014 tap to stop\",\n speaking: \"Voice speaking \u2014 tap to stop\",\n error: \"Voice error \u2014 tap to retry\",\n};\n\n// \u2500\u2500 CSS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction getPanelStyles(): string {\n return `\n .yak-panel-root {\n position: fixed;\n top: 0; left: 0; right: 0; bottom: 0;\n width: 100vw; height: 100vh;\n pointer-events: none;\n z-index: 9998;\n }\n\n .yak-panel-container {\n position: absolute;\n width: 500px; height: 600px;\n max-width: calc(100vw - 40px);\n max-height: calc(100vh - 120px);\n border-radius: 15px;\n overflow: hidden;\n background-color: transparent;\n pointer-events: auto;\n }\n\n .yak-panel-container[data-position=\"top-left\"]:not(.yak-panel-drawer) { top: 16px; left: 16px; }\n .yak-panel-container[data-position=\"top-center\"]:not(.yak-panel-drawer) { top: 16px; left: 50%; transform: translateX(-50%); }\n .yak-panel-container[data-position=\"top-right\"]:not(.yak-panel-drawer) { top: 16px; right: 16px; }\n .yak-panel-container[data-position=\"left-center\"]:not(.yak-panel-drawer) { top: 50%; left: 16px; transform: translateY(-50%); }\n .yak-panel-container[data-position=\"right-center\"]:not(.yak-panel-drawer) { top: 50%; right: 16px; transform: translateY(-50%); }\n .yak-panel-container[data-position=\"bottom-left\"]:not(.yak-panel-drawer) { bottom: 16px; left: 16px; }\n .yak-panel-container[data-position=\"bottom-center\"]:not(.yak-panel-drawer) { bottom: 16px; left: 50%; transform: translateX(-50%); }\n .yak-panel-container[data-position=\"bottom-right\"]:not(.yak-panel-drawer) { bottom: 16px; right: 16px; }\n\n .yak-panel-container:not(.yak-panel-drawer) { display: none; }\n .yak-panel-container:not(.yak-panel-drawer)[data-open=\"true\"] { display: block; }\n\n .yak-panel-container[data-expanded=\"true\"] {\n width: calc(100vw - 32px) !important;\n height: calc(100vh - 32px) !important;\n max-width: none !important; max-height: none !important;\n top: 16px !important; left: 16px !important; right: 16px !important; bottom: 16px !important;\n border-radius: 15px !important;\n border: 1px solid rgba(0, 0, 0, 0.1) !important;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15) !important;\n }\n @media (prefers-color-scheme: dark) {\n .yak-panel-container[data-expanded=\"true\"]:not(.yak-panel-light) { border-color: rgba(255,255,255,0.1) !important; }\n }\n .yak-panel-container.yak-panel-dark[data-expanded=\"true\"] { border-color: rgba(255,255,255,0.1) !important; }\n .yak-panel-container.yak-panel-light[data-expanded=\"true\"] { border-color: rgba(0,0,0,0.1) !important; }\n\n .yak-panel-container.yak-panel-drawer {\n height: calc(100% - 32px); max-width: 100vw; max-height: none;\n border-radius: 15px;\n transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);\n }\n .yak-panel-container.yak-panel-drawer[data-position=\"left-center\"],\n .yak-panel-container.yak-panel-drawer[data-position=\"top-left\"],\n .yak-panel-container.yak-panel-drawer[data-position=\"bottom-left\"] {\n top: 16px; left: 16px; bottom: 16px;\n transform: translateX(calc(-100% - 16px));\n }\n .yak-panel-container.yak-panel-drawer[data-position=\"right-center\"],\n .yak-panel-container.yak-panel-drawer[data-position=\"top-right\"],\n .yak-panel-container.yak-panel-drawer[data-position=\"bottom-right\"] {\n top: 16px; right: 16px; bottom: 16px;\n transform: translateX(calc(100% + 16px));\n }\n .yak-panel-container.yak-panel-drawer[data-position=\"top-center\"],\n .yak-panel-container.yak-panel-drawer[data-position=\"bottom-center\"] {\n top: 16px; right: 16px; bottom: 16px;\n transform: translateX(calc(100% + 16px));\n }\n .yak-panel-container.yak-panel-drawer[data-open=\"true\"] { transform: translateX(0); }\n\n .yak-panel-iframe {\n position: absolute; inset: 0;\n width: 100%; height: 100%; border: none;\n }\n\n @media (max-width: 640px) {\n .yak-panel-container:not(.yak-panel-drawer) {\n width: 100% !important; height: 100% !important; height: 100dvh !important;\n max-width: none !important; max-height: none !important;\n top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important;\n border-radius: 0 !important;\n }\n .yak-panel-container.yak-panel-drawer { width: 100% !important; max-width: none !important; }\n }\n `;\n}\n\nfunction getTriggerStyles(): string {\n return `\n .yak-widget-trigger {\n position: fixed; z-index: 9997;\n display: inline-flex; align-items: center; gap: 8px;\n border: none; border-radius: 30px;\n padding: 5px; height: 45px;\n transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);\n background-color: #000; color: #fff;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n font-family: system-ui, -apple-system, sans-serif;\n }\n\n .yak-widget-trigger[data-position=\"top-left\"] { top: 28px; left: 28px; }\n .yak-widget-trigger[data-position=\"top-center\"] { top: 28px; left: 50%; transform: translateX(-50%); }\n .yak-widget-trigger[data-position=\"top-right\"] { top: 28px; right: 28px; }\n .yak-widget-trigger[data-position=\"left-center\"] { top: 50%; left: 28px; transform: translateY(-50%); }\n .yak-widget-trigger[data-position=\"right-center\"] { top: 50%; right: 28px; transform: translateY(-50%); }\n .yak-widget-trigger[data-position=\"bottom-left\"] { bottom: 28px; left: 28px; }\n .yak-widget-trigger[data-position=\"bottom-center\"] { bottom: 28px; left: 50%; transform: translateX(-50%); }\n .yak-widget-trigger[data-position=\"bottom-right\"] { bottom: 28px; right: 28px; }\n\n .yak-widget-icon-bg {\n display: flex; align-items: center; justify-content: center;\n width: 36px; height: 36px; border-radius: 50%;\n background-color: rgba(255, 255, 255, 0.1);\n flex-shrink: 0;\n }\n\n .yak-widget-icon { width: 20px; height: 20px; color: currentColor; }\n\n .yak-widget-trigger-icon-btn {\n display: inline-flex; align-items: center; justify-content: center;\n width: 36px; height: 36px; border-radius: 50%;\n border: none; padding: 0;\n background-color: transparent;\n color: inherit;\n cursor: pointer;\n position: relative;\n transition: background-color 0.15s ease;\n flex-shrink: 0;\n }\n .yak-widget-trigger-icon-btn:hover { background-color: rgba(255, 255, 255, 0.12); }\n .yak-widget-trigger-icon-btn:disabled { cursor: wait; opacity: 0.7; }\n .yak-widget-trigger-icon-btn svg { width: 20px; height: 20px; display: block; }\n\n .yak-widget-trigger-icon-btn[data-action=\"voice\"][data-state=\"error\"] {\n background-color: rgba(185, 28, 28, 0.18);\n }\n .yak-widget-trigger-icon-btn[data-action=\"voice\"][data-state=\"listening\"]::after,\n .yak-widget-trigger-icon-btn[data-action=\"voice\"][data-state=\"speaking\"]::after {\n content: \"\";\n position: absolute; inset: 2px;\n border-radius: 50%;\n border: 2px solid currentColor;\n pointer-events: none;\n }\n .yak-widget-trigger-icon-btn[data-action=\"voice\"][data-state=\"listening\"]::after {\n opacity: 0.4; animation: yak-widget-pulse 1.2s ease-out infinite;\n }\n .yak-widget-trigger-icon-btn[data-action=\"voice\"][data-state=\"speaking\"]::after {\n opacity: 0.5; animation: yak-widget-wave 0.8s ease-in-out infinite;\n }\n\n @media (prefers-color-scheme: dark) {\n .yak-widget-trigger:not(.yak-widget-light) .yak-widget-icon { filter: invert(1); }\n .yak-widget-trigger:not(.yak-widget-light) .yak-widget-trigger-icon-btn:hover {\n background-color: rgba(255, 255, 255, 0.12);\n }\n }\n .yak-widget-trigger.yak-widget-dark .yak-widget-icon { filter: invert(1); }\n .yak-widget-trigger.yak-widget-light .yak-widget-icon { filter: none; }\n .yak-widget-trigger.yak-widget-light .yak-widget-trigger-icon-btn:hover {\n background-color: rgba(0, 0, 0, 0.06);\n }\n\n .yak-widget-spinner {\n width: 20px; height: 20px;\n border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%;\n animation: yak-widget-spin 0.8s linear infinite;\n }\n @keyframes yak-widget-spin { to { transform: rotate(360deg); } }\n @keyframes yak-widget-pulse {\n 0% { transform: scale(1); opacity: 0.5; }\n 100% { transform: scale(1.45); opacity: 0; }\n }\n @keyframes yak-widget-wave {\n 0%, 100% { transform: scale(1); opacity: 0.5; }\n 50% { transform: scale(1.25); opacity: 0.9; }\n }\n\n .yak-widget-trigger.yak-widget-custom-light {\n background-color: var(--yak-btn-light-bg, #fff); color: var(--yak-btn-light-color, #000);\n border: 1px solid var(--yak-btn-light-border, transparent);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n }\n .yak-widget-trigger.yak-widget-custom-dark {\n background-color: var(--yak-btn-dark-bg, #000); color: var(--yak-btn-dark-color, #fff);\n border: 1px solid var(--yak-btn-dark-border, transparent);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n }\n\n @media (prefers-color-scheme: light) {\n .yak-widget-trigger[data-has-light-custom]:not(.yak-widget-dark) {\n background-color: var(--yak-btn-light-bg, #fff); color: var(--yak-btn-light-color, #000);\n border: 1px solid var(--yak-btn-light-border, transparent);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n }\n }\n @media (prefers-color-scheme: dark) {\n .yak-widget-trigger[data-has-dark-custom]:not(.yak-widget-light) {\n background-color: var(--yak-btn-dark-bg, #000); color: var(--yak-btn-dark-color, #fff);\n border: 1px solid var(--yak-btn-dark-border, transparent);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n }\n }\n\n @media (prefers-color-scheme: light) {\n .yak-widget-trigger:not(.yak-widget-dark):not([data-has-light-custom]) {\n background-color: #fff; color: #000;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); border: 1px solid #e5e5e5;\n }\n .yak-widget-trigger:not(.yak-widget-dark):not([data-has-light-custom]) .yak-widget-icon-bg {\n background-color: rgba(0, 0, 0, 0.05);\n }\n }\n\n @media (prefers-color-scheme: dark) {\n .yak-widget-trigger:not(.yak-widget-light):not([data-has-dark-custom]) {\n background-color: #000; color: #fff;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); border: none;\n }\n .yak-widget-trigger:not(.yak-widget-light):not([data-has-dark-custom]) .yak-widget-icon-bg {\n background-color: rgba(255, 255, 255, 0.1);\n }\n }\n\n .yak-widget-trigger.yak-widget-light:not(.yak-widget-custom-light) {\n background-color: #fff; color: #000;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); border: 1px solid #e5e5e5;\n }\n .yak-widget-trigger.yak-widget-light:not(.yak-widget-custom-light) .yak-widget-icon-bg {\n background-color: rgba(0, 0, 0, 0.05);\n }\n\n .yak-widget-trigger.yak-widget-dark:not(.yak-widget-custom-dark) {\n background-color: #000; color: #fff;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); border: none;\n }\n .yak-widget-trigger.yak-widget-dark:not(.yak-widget-custom-dark) .yak-widget-icon-bg {\n background-color: rgba(255, 255, 255, 0.1);\n }\n `;\n}\n\n// \u2500\u2500 YakEmbed class \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Drop-in widget that renders the yak trigger pill plus, depending on mode,\n * the chat iframe panel and/or a WebRTC voice session. Composes both\n * `YakClient` (chat) and `YakVoiceSession` (voice) under one trigger.\n *\n * @example\n * ```ts\n * const embed = new YakEmbed({\n * appId: \"my-app\",\n * mode: \"both\",\n * theme: { position: \"bottom-left\" },\n * onToolCall: async (name, args) => { ... },\n * });\n * embed.mount();\n * ```\n */\nexport class YakEmbed {\n private readonly client: YakClient;\n private readonly voice: YakVoiceSession | null;\n private readonly config: YakEmbedConfig;\n private readonly mode: WidgetMode;\n\n // DOM elements\n private styleEl: HTMLStyleElement | null = null;\n private panelRoot: HTMLDivElement | null = null;\n private container: HTMLDivElement | null = null;\n private iframe: HTMLIFrameElement | null = null;\n private triggerEl: HTMLDivElement | null = null;\n private chatButton: HTMLButtonElement | null = null;\n private voiceButton: HTMLButtonElement | null = null;\n\n // State\n private isOpen = false;\n private isReady = false;\n private isExpanded = false;\n private hasBeenOpened = false;\n private pendingPrompt: string | null = null;\n private mounted = false;\n private voiceMachine: VoiceMachine = INITIAL_VOICE_MACHINE;\n\n // Listeners\n private stateListeners = new Set<YakEmbedStateListener>();\n private voiceListeners = new Set<VoiceStateListener>();\n private unsubscribeVoice: (() => void) | null = null;\n private mobileQuery: MediaQueryList | null = null;\n private mobileHandler: ((e: MediaQueryListEvent) => void) | null = null;\n private expandHandler: ((e: MessageEvent) => void) | null = null;\n\n constructor(config: YakEmbedConfig) {\n this.config = config;\n this.mode = config.mode ?? \"chat\";\n\n // Wrap callbacks to integrate with our state\n this.client = new YakClient({\n ...config,\n onReady: () => {\n this.isReady = true;\n this.updatePanelState();\n this.updateChatButtonState();\n this.sendPendingPrompt();\n this.sendFocusIfOpen();\n this.notifyMobileState();\n // Surface the ready transition to framework subscribers (React/Vue/\n // Svelte/Angular all derive their loading state from this). Without\n // it `isReady` stays false forever and consumers spin indefinitely.\n this.notifyListeners();\n config.onReady?.();\n },\n onClose: () => {\n this.close();\n config.onClose?.();\n },\n });\n\n if (this.mode !== \"chat\") {\n const voiceConfig: YakVoiceSessionConfig = {\n appId: config.appId,\n // A single `origin` on the embed drives both surfaces: chat (iframe)\n // and voice (mint endpoint).\n apiOrigin: config.origin,\n getConfig: config.getConfig,\n chatConfig: config.chatConfig,\n onToolCall: config.onToolCall,\n onRedirect: config.onRedirect,\n };\n this.voice = new YakVoiceSession(voiceConfig);\n } else {\n this.voice = null;\n }\n }\n\n /** The underlying headless YakClient for advanced usage */\n public getClient(): YakClient {\n return this.client;\n }\n\n /** The underlying voice session \u2014 null when mode === \"chat\". */\n public getVoiceSession(): YakVoiceSession | null {\n return this.voice;\n }\n\n /** Current widget mode (immutable for the lifetime of the embed). */\n public getMode(): WidgetMode {\n return this.mode;\n }\n\n // \u2500\u2500 Lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Mount the widget into the DOM. Call once after construction.\n * Inserts styles and trigger button (if enabled). The chat iframe is\n * lazily created on the first call to open().\n */\n public mount(target?: HTMLElement): void {\n if (this.mounted) return;\n this.mounted = true;\n\n const parent = target ?? this.config.target ?? document.body;\n\n // Inject styles\n this.styleEl = document.createElement(\"style\");\n this.styleEl.textContent = getPanelStyles() + getTriggerStyles();\n parent.appendChild(this.styleEl);\n\n // Create trigger\n if (this.config.trigger !== false) {\n this.createTrigger(parent);\n }\n\n // Listen for expansion messages from iframe\n this.expandHandler = (event: MessageEvent) => {\n if (event.data?.type === \"YAK_SET_EXPANDED\") {\n this.isExpanded = Boolean(event.data.expanded);\n this.updatePanelState();\n this.notifyListeners();\n }\n };\n window.addEventListener(\"message\", this.expandHandler);\n\n // Start the client's message listeners\n this.client.mount();\n\n // Subscribe to voice state for trigger updates + fan-out to listeners\n if (this.voice) {\n this.voiceMachine = this.voice.getState();\n this.unsubscribeVoice = this.voice.onStateChange((machine) => {\n this.voiceMachine = machine;\n this.updateVoiceButtonState();\n for (const listener of this.voiceListeners) {\n try {\n listener(machine);\n } catch (err) {\n logger.warn(\"Error in voice listener:\", err);\n }\n }\n });\n this.updateVoiceButtonState();\n }\n }\n\n /** Remove all DOM elements and event listeners. */\n public destroy(): void {\n if (!this.mounted) return;\n this.mounted = false;\n\n this.client.unmount();\n // Drop references to the now-detached iframe window so a remount (e.g.\n // React StrictMode's mount\u2192unmount\u2192mount) doesn't leave the client\n // pointing at a dead window. Clears both `iframeWindow` and `readyTarget`.\n this.client.setIframeWindow(null);\n\n if (this.unsubscribeVoice) {\n this.unsubscribeVoice();\n this.unsubscribeVoice = null;\n }\n this.voice?.destroy();\n\n if (this.expandHandler) {\n window.removeEventListener(\"message\", this.expandHandler);\n this.expandHandler = null;\n }\n\n if (this.mobileQuery && this.mobileHandler) {\n this.mobileQuery.removeEventListener(\"change\", this.mobileHandler);\n this.mobileQuery = null;\n this.mobileHandler = null;\n }\n\n this.panelRoot?.remove();\n this.triggerEl?.remove();\n this.styleEl?.remove();\n\n this.panelRoot = null;\n this.container = null;\n this.iframe = null;\n this.triggerEl = null;\n this.chatButton = null;\n this.voiceButton = null;\n this.styleEl = null;\n this.isOpen = false;\n this.isReady = false;\n this.isExpanded = false;\n this.hasBeenOpened = false;\n\n this.stateListeners.clear();\n this.voiceListeners.clear();\n }\n\n // \u2500\u2500 Public chat API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Open the chat widget. Creates the iframe on first call (lazy mount). */\n public open(): void {\n if (!this.mounted) return;\n\n if (!this.hasBeenOpened) {\n this.hasBeenOpened = true;\n\n const parent = this.config.target ?? document.body;\n this.createPanel(parent);\n }\n\n this.isOpen = true;\n this.client.setWidgetOpen(true);\n this.updatePanelState();\n this.updateChatButtonState();\n this.sendFocusIfOpen();\n this.notifyListeners();\n }\n\n /** Close the chat widget. The iframe remains in the DOM for instant re-open. */\n public close(): void {\n this.isOpen = false;\n this.client.setWidgetOpen(false);\n this.updatePanelState();\n this.updateChatButtonState();\n this.notifyListeners();\n }\n\n /** Toggle the chat widget open/closed. */\n public toggle(): void {\n if (this.isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /** Open the chat and immediately send a prompt. */\n public openWithPrompt(prompt: string): void {\n this.pendingPrompt = prompt;\n this.open();\n this.sendPendingPrompt();\n }\n\n /** Get the current widget state. */\n public getState(): YakEmbedState {\n return {\n isOpen: this.isOpen,\n isReady: this.isReady,\n isLoading: this.isOpen && !this.isReady,\n isExpanded: this.isExpanded,\n };\n }\n\n /** Subscribe to state changes. Returns an unsubscribe function. */\n public onStateChange(listener: YakEmbedStateListener): () => void {\n this.stateListeners.add(listener);\n return () => {\n this.stateListeners.delete(listener);\n };\n }\n\n // \u2500\u2500 Public voice API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Start a voice session. Must be invoked from a user gesture. */\n public voiceStart(): Promise<void> {\n return this.voice ? this.voice.start() : Promise.resolve();\n }\n\n /** Stop the current voice session. */\n public voiceStop(): Promise<void> {\n return this.voice ? this.voice.stop() : Promise.resolve();\n }\n\n /** Toggle: start if idle/error, stop if active. */\n public async voiceToggle(): Promise<void> {\n if (!this.voice) return;\n const state = this.voice.getState().state;\n if (state === \"idle\" || state === \"error\") {\n await this.voice.start();\n } else if (state === \"listening\" || state === \"speaking\" || state === \"thinking\") {\n await this.voice.stop();\n }\n }\n\n /** Current voice machine snapshot. */\n public getVoiceState(): VoiceMachine {\n return this.voice ? this.voice.getState() : INITIAL_VOICE_MACHINE;\n }\n\n /** Subscribe to voice state changes. */\n public onVoiceStateChange(listener: VoiceStateListener): () => void {\n this.voiceListeners.add(listener);\n return () => {\n this.voiceListeners.delete(listener);\n };\n }\n\n // \u2500\u2500 DOM creation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private createPanel(parent: HTMLElement): void {\n const theme = this.config.theme;\n const position = theme?.position ?? DEFAULT_POSITION;\n const colorMode = theme?.colorMode;\n const displayMode = theme?.displayMode ?? \"chatbox\";\n const isDrawer = displayMode === \"drawer\";\n\n // Root overlay (pointer-events: none)\n this.panelRoot = document.createElement(\"div\");\n this.panelRoot.className = \"yak-panel-root\";\n\n // Container\n this.container = document.createElement(\"div\");\n const classes = [\"yak-panel-container\"];\n if (isDrawer) classes.push(\"yak-panel-drawer\");\n if (colorMode === \"light\") classes.push(\"yak-panel-light\");\n else if (colorMode === \"dark\") classes.push(\"yak-panel-dark\");\n this.container.className = classes.join(\" \");\n this.container.dataset.position = position;\n\n // Iframe \u2014 set `allow` and `title` BEFORE `src` (some browsers\n // persist the pre-load Permissions-Policy otherwise).\n this.iframe = document.createElement(\"iframe\");\n this.iframe.allow = \"clipboard-write\";\n this.iframe.title = \"yak-chat-host\";\n this.iframe.className = \"yak-panel-iframe\";\n this.iframe.src = this.client.getEmbedUrl();\n\n this.iframe.addEventListener(\"load\", () => {\n this.client.setIframeWindow(this.iframe?.contentWindow ?? null);\n });\n\n this.container.appendChild(this.iframe);\n this.panelRoot.appendChild(this.container);\n parent.appendChild(this.panelRoot);\n\n // Set up mobile detection\n this.mobileQuery = window.matchMedia(\"(max-width: 640px)\");\n this.mobileHandler = (e: MediaQueryListEvent) => {\n this.notifyIframeFullscreen(e.matches);\n };\n this.mobileQuery.addEventListener(\"change\", this.mobileHandler);\n }\n\n private createTrigger(parent: HTMLElement): void {\n const theme = this.config.theme;\n const position: WidgetPosition = theme?.position ?? DEFAULT_POSITION;\n const colorMode = theme?.colorMode;\n const triggerConfig = typeof this.config.trigger === \"object\" ? this.config.trigger : {};\n\n this.triggerEl = document.createElement(\"div\");\n this.triggerEl.dataset.position = position;\n this.triggerEl.dataset.mode = this.mode;\n this.triggerEl.className = this.buildTriggerClasses(colorMode, triggerConfig);\n this.applyTriggerCustomColors(triggerConfig);\n\n // Logo circle on the left\n const iconBg = document.createElement(\"div\");\n iconBg.className = \"yak-widget-icon-bg\";\n const logoImg = document.createElement(\"img\");\n logoImg.src = `${this.client.getIframeOrigin()}/logo.svg`;\n logoImg.alt = \"\";\n logoImg.width = 20;\n logoImg.height = 20;\n logoImg.className = \"yak-widget-icon\";\n iconBg.appendChild(logoImg);\n this.triggerEl.appendChild(iconBg);\n\n // Chat icon button\n if (this.mode === \"chat\" || this.mode === \"both\") {\n this.chatButton = document.createElement(\"button\");\n this.chatButton.type = \"button\";\n this.chatButton.className = \"yak-widget-trigger-icon-btn\";\n this.chatButton.dataset.action = \"chat\";\n this.chatButton.setAttribute(\"aria-label\", \"Open chat\");\n this.chatButton.innerHTML = MESSAGE_CIRCLE_SVG;\n this.chatButton.addEventListener(\"click\", () => this.open());\n this.triggerEl.appendChild(this.chatButton);\n }\n\n // Voice icon button\n if (this.mode === \"voice\" || this.mode === \"both\") {\n this.voiceButton = document.createElement(\"button\");\n this.voiceButton.type = \"button\";\n this.voiceButton.className = \"yak-widget-trigger-icon-btn\";\n this.voiceButton.dataset.action = \"voice\";\n this.voiceButton.dataset.state = \"idle\";\n this.voiceButton.setAttribute(\"aria-label\", VOICE_STATE_ARIA.idle);\n this.voiceButton.innerHTML = AUDIO_LINES_SVG;\n this.voiceButton.addEventListener(\"click\", () => {\n void this.voiceToggle();\n });\n this.triggerEl.appendChild(this.voiceButton);\n }\n\n parent.appendChild(this.triggerEl);\n }\n\n private buildTriggerClasses(\n colorMode: string | undefined,\n triggerConfig: TriggerButtonConfig\n ): string {\n const classes = [\"yak-widget-trigger\"];\n if (colorMode === \"light\") classes.push(\"yak-widget-light\");\n else if (colorMode === \"dark\") classes.push(\"yak-widget-dark\");\n\n const hasLightCustom =\n triggerConfig.lightButton?.background ||\n triggerConfig.lightButton?.color ||\n triggerConfig.lightButton?.border;\n const hasDarkCustom =\n triggerConfig.darkButton?.background ||\n triggerConfig.darkButton?.color ||\n triggerConfig.darkButton?.border;\n\n if (colorMode === \"light\" && hasLightCustom) classes.push(\"yak-widget-custom-light\");\n else if (colorMode === \"dark\" && hasDarkCustom) classes.push(\"yak-widget-custom-dark\");\n\n return classes.join(\" \");\n }\n\n private applyTriggerCustomColors(triggerConfig: TriggerButtonConfig): void {\n if (!this.triggerEl) return;\n const { lightButton, darkButton } = triggerConfig;\n\n const hasLightCustom = lightButton?.background || lightButton?.color || lightButton?.border;\n const hasDarkCustom = darkButton?.background || darkButton?.color || darkButton?.border;\n\n if (hasLightCustom || hasDarkCustom) {\n const vars: [string, string | undefined][] = [\n [\"--yak-btn-light-bg\", lightButton?.background],\n [\"--yak-btn-light-color\", lightButton?.color],\n [\"--yak-btn-light-border\", lightButton?.border],\n [\"--yak-btn-dark-bg\", darkButton?.background],\n [\"--yak-btn-dark-color\", darkButton?.color],\n [\"--yak-btn-dark-border\", darkButton?.border],\n ];\n for (const [prop, value] of vars) {\n if (value) this.triggerEl.style.setProperty(prop, value);\n }\n }\n\n if (hasLightCustom) this.triggerEl.dataset.hasLightCustom = \"true\";\n if (hasDarkCustom) this.triggerEl.dataset.hasDarkCustom = \"true\";\n }\n\n // \u2500\u2500 Internal state management \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private updatePanelState(): void {\n if (!this.container) return;\n this.container.dataset.open = String(this.isOpen && this.isReady);\n this.container.dataset.expanded = String(this.isExpanded);\n if (this.panelRoot) {\n this.panelRoot.dataset.expanded = String(this.isExpanded);\n }\n }\n\n private updateChatButtonState(): void {\n if (!this.chatButton) return;\n const isLoading = this.isOpen && !this.isReady;\n this.chatButton.disabled = isLoading;\n this.chatButton.setAttribute(\"aria-label\", isLoading ? \"Loading chat\" : \"Open chat\");\n\n if (isLoading) {\n this.chatButton.innerHTML = `<span class=\"yak-widget-spinner\" aria-hidden=\"true\"></span>`;\n } else {\n this.chatButton.innerHTML = MESSAGE_CIRCLE_SVG;\n }\n }\n\n private updateVoiceButtonState(): void {\n if (!this.voiceButton) return;\n const state = this.voiceMachine.state;\n this.voiceButton.dataset.state = state;\n this.voiceButton.setAttribute(\"aria-label\", VOICE_STATE_ARIA[state]);\n this.voiceButton.disabled = state === \"connecting\";\n this.voiceButton.innerHTML = this.iconForVoiceState(state);\n }\n\n private iconForVoiceState(state: VoiceState): string {\n if (state === \"connecting\") {\n return `<span class=\"yak-widget-spinner\" aria-hidden=\"true\"></span>`;\n }\n if (state === \"listening\" || state === \"speaking\" || state === \"thinking\") {\n return STOP_SVG;\n }\n return AUDIO_LINES_SVG;\n }\n\n private sendPendingPrompt(): void {\n if (!this.pendingPrompt || !this.isReady) return;\n logger.debug(\"Sending pending prompt:\", this.pendingPrompt);\n this.client.sendPrompt(this.pendingPrompt);\n this.pendingPrompt = null;\n }\n\n private sendFocusIfOpen(): void {\n if (this.isOpen && this.isReady) {\n this.client.sendFocus();\n }\n }\n\n private notifyMobileState(): void {\n if (this.mobileQuery) {\n this.notifyIframeFullscreen(this.mobileQuery.matches);\n }\n }\n\n private notifyIframeFullscreen(isFullscreen: boolean): void {\n if (!this.iframe?.contentWindow) return;\n const msg: IframeMessageFromHost = {\n type: \"yak:viewport\",\n payload: { fullscreen: isFullscreen },\n };\n this.iframe.contentWindow.postMessage(msg, this.client.getIframeOrigin());\n }\n\n private notifyListeners(): void {\n const state = this.getState();\n for (const listener of this.stateListeners) {\n try {\n listener(state);\n } catch (err) {\n logger.warn(\"Error in state listener:\", err);\n }\n }\n }\n}\n", "import { logger } from \"./logger.js\";\nimport type { ChatConfig } from \"./types/config.js\";\nimport type {\n ToolAdapter,\n ToolCallHandler,\n ToolDefinition,\n ToolManifest,\n YakToolset,\n} from \"./types/tools.js\";\n\ntype ResolvedAdapter = {\n adapter: ToolAdapter;\n tools: ToolDefinition[];\n};\n\n/**\n * Compose client-side {@link ToolAdapter}s into ONE merged tool manifest and ONE\n * routed `onToolCall`. This is the single injection point for \"available tools\"\n * into the iframe: every adapter \u2014 client-side (GraphQL/REST/custom, run by your own\n * `execute` callback) or server-relayed ({@link createYakServerAdapter}) \u2014 contributes\n * its tools to the merged manifest and its executor to the routed handler. Because every\n * tool now flows through `onToolCall`, they all surface through `onToolCallComplete` /\n * `useYakToolEvent`.\n *\n * @example\n * ```tsx\n * const toolset = createYakToolset([\n * createYakServerAdapter({ endpoint: \"/api/yak\" }), // tRPC + custom server tools\n * createGraphQLToolAdapter({ name: \"shop\", schema, execute: runShopQuery }),\n * createRESTToolAdapter({ name: \"billing\", spec, execute: callBillingApi }),\n * ]);\n *\n * <YakProvider\n * getConfig={async () => ({ routes, ...(await toolset.getConfig()) })}\n * onToolCall={toolset.onToolCall}\n * />;\n * ```\n */\nexport function createYakToolset(adapters: ToolAdapter[]): YakToolset {\n let cache: ResolvedAdapter[] | null = null;\n\n async function resolve(force = false): Promise<ResolvedAdapter[]> {\n if (cache && !force) return cache;\n cache = await Promise.all(\n adapters.map(async (adapter) => ({ adapter, tools: await adapter.getTools() }))\n );\n return cache;\n }\n\n async function getConfig(): Promise<{ tools: ToolManifest }> {\n // Refresh on every config load \u2014 tools may depend on page/user.\n const resolved = await resolve(true);\n const tools: ToolDefinition[] = [];\n const seen = new Set<string>();\n\n for (const { adapter, tools: adapterTools } of resolved) {\n for (const tool of adapterTools) {\n if (seen.has(tool.name)) {\n logger.warn(\n `Duplicate tool name \"${tool.name}\"; keeping the first and ignoring the one from adapter \"${adapter.id ?? \"unknown\"}\".`\n );\n continue;\n }\n seen.add(tool.name);\n tools.push(tool);\n }\n }\n\n return {\n tools: {\n tools,\n sources: resolved.map(({ adapter, tools: adapterTools }, index) => ({\n id: adapter.id ?? `adapter-${index}`,\n count: adapterTools.length,\n })),\n },\n };\n }\n\n const onToolCall: ToolCallHandler = async (name, args) => {\n // Fast path: explicit ownership predicates need no resolution.\n for (const adapter of adapters) {\n if (adapter.ownsTool?.(name)) {\n return adapter.execute(name, args);\n }\n }\n // Otherwise route by the adapter's resolved tool names.\n const resolved = await resolve();\n for (const { adapter, tools } of resolved) {\n if (adapter.ownsTool) continue; // already consulted above\n if (tools.some((tool) => tool.name === name)) {\n return adapter.execute(name, args);\n }\n }\n throw new Error(`Unknown tool: ${name}`);\n };\n\n return { getConfig, onToolCall };\n}\n\n/** Config for {@link createYakServerAdapter}. */\nexport type YakServerAdapterConfig = {\n /**\n * Endpoint mounting `createYakHandler` / `createNextYakHandler`. GET returns the\n * tool manifest; POST `{ name, args }` executes a tool. Default: `\"/api/yak\"`.\n */\n endpoint?: string;\n /** Stable id for diagnostics. Default: `\"server\"`. */\n id?: string;\n /** Extra headers (e.g. auth) applied to both the GET manifest load and POST execution. */\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n};\n\nasync function buildHeaders(\n source: YakServerAdapterConfig[\"headers\"],\n base?: Record<string, string>\n): Promise<Headers> {\n const headers = new Headers(base);\n const resolved = typeof source === \"function\" ? await source() : source;\n if (resolved) {\n for (const [key, value] of new Headers(resolved)) {\n headers.set(key, value);\n }\n }\n return headers;\n}\n\n/**\n * Bridge a server-side yak handler ({@link createYakHandler} /\n * `createNextYakHandler`, e.g. fronting the `@yak-io/trpc` adapter) into a\n * client-side {@link ToolAdapter}, so server-relayed tools merge into the same\n * manifest + `onToolCall` as browser-executed adapters.\n */\nexport function createYakServerAdapter(config: YakServerAdapterConfig = {}): ToolAdapter {\n const endpoint = config.endpoint ?? \"/api/yak\";\n\n return {\n id: config.id ?? \"server\",\n getTools: async () => {\n const res = await fetch(endpoint, { headers: await buildHeaders(config.headers) });\n if (!res.ok) {\n throw new Error(`Failed to load tools from ${endpoint} (${res.status})`);\n }\n const chatConfig = (await res.json()) as ChatConfig;\n return chatConfig.tools?.tools ?? [];\n },\n execute: async (name, args) => {\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: await buildHeaders(config.headers, { \"Content-Type\": \"application/json\" }),\n body: JSON.stringify({ name, args }),\n });\n const data = (await res.json().catch(() => ({}))) as {\n ok?: boolean;\n result?: unknown;\n error?: string;\n };\n if (!res.ok || !data.ok) {\n throw new Error(data.error ?? `Tool \"${name}\" failed (${res.status})`);\n }\n return data.result;\n },\n };\n}\n"],
4
+ "sourcesContent": ["// Public types\n\nexport type { YakClientConfig } from \"./client.js\";\n// Public client API\nexport { YakClient } from \"./client.js\";\nexport type {\n TriggerButtonConfig,\n WidgetMode,\n YakEmbedConfig,\n YakEmbedState,\n YakEmbedStateListener,\n} from \"./embed.js\";\n// Embed (DOM rendering layer \u2014 chat + voice)\nexport { YakEmbed } from \"./embed.js\";\n// Logging utilities\nexport { disableYakLogging, enableYakLogging, isYakLoggingEnabled, logger } from \"./logger.js\";\n// Client-side tool composition (one merged manifest + one routed onToolCall)\nexport type { YakServerAdapterConfig } from \"./toolset.js\";\nexport { createYakServerAdapter, createYakToolset } from \"./toolset.js\";\nexport * from \"./types/config.js\";\nexport * from \"./types/messaging.js\";\nexport * from \"./types/routes.js\";\nexport * from \"./types/tools.js\";\nexport type { EmbedProtocolVersion } from \"./version.js\";\n// Version\nexport { EMBED_PROTOCOL_VERSION } from \"./version.js\";\nexport type { VoiceEvent, VoiceMachine, VoiceState } from \"./voice-machine.js\";\nexport {\n handleRealtimeMessage,\n INITIAL_VOICE_MACHINE,\n voiceReducer,\n} from \"./voice-machine.js\";\n// Voice session (composed by YakEmbed; exposed for advanced consumers)\nexport type { VoiceStateListener, YakVoiceSessionConfig } from \"./voice-session.js\";\nexport { YakVoiceSession } from \"./voice-session.js\";\n", "/**\n * Simple logger utility for the yak client SDK.\n * Debug/info logs are controlled by __YAK_LOGGING_ENABLED__.\n * Warnings and errors are always logged.\n *\n * Logging is controlled via:\n * 1. localStorage key \"yakLogging\" (persists across reloads)\n * 2. window.__YAK_LOGGING_ENABLED__ (runtime override)\n *\n * Use the helper functions to enable/disable:\n * enableYakLogging() - turns on logging, persists in localStorage\n * disableYakLogging() - turns off logging, clears localStorage\n */\n\nconst STORAGE_KEY = \"yakLogging\";\n\nfunction isLoggingEnabled(): boolean {\n if (typeof window === \"undefined\") {\n return false;\n }\n\n // Window flag takes precedence (runtime override)\n if (typeof window.__YAK_LOGGING_ENABLED__ === \"boolean\") {\n return window.__YAK_LOGGING_ENABLED__;\n }\n\n // Fall back to localStorage for persistence across reloads\n try {\n return localStorage.getItem(STORAGE_KEY) === \"true\";\n } catch {\n // localStorage may be unavailable (e.g., private browsing)\n return false;\n }\n}\n\n/**\n * Check if Yak logging is currently enabled.\n * Useful for checking state before creating iframes.\n */\nexport function isYakLoggingEnabled(): boolean {\n return isLoggingEnabled();\n}\n\n/**\n * Enable Yak debug logging.\n * Persists in localStorage so it survives page reloads.\n * Call this from browser console: `enableYakLogging()`\n */\nexport function enableYakLogging(): void {\n if (typeof window === \"undefined\") {\n return;\n }\n window.__YAK_LOGGING_ENABLED__ = true;\n try {\n localStorage.setItem(STORAGE_KEY, \"true\");\n } catch {\n // localStorage may be unavailable\n }\n console.info(\"[yak] Logging enabled\");\n}\n\n/**\n * Disable Yak debug logging.\n * Clears localStorage and window flag.\n * Call this from browser console: `disableYakLogging()`\n */\nexport function disableYakLogging(): void {\n if (typeof window === \"undefined\") {\n return;\n }\n window.__YAK_LOGGING_ENABLED__ = false;\n try {\n localStorage.removeItem(STORAGE_KEY);\n } catch {\n // localStorage may be unavailable\n }\n console.info(\"[yak] Logging disabled\");\n}\n\n// Expose helpers globally so they can be called from browser console\nif (typeof window !== \"undefined\") {\n (\n window as Window & {\n enableYakLogging?: typeof enableYakLogging;\n disableYakLogging?: typeof disableYakLogging;\n }\n ).enableYakLogging = enableYakLogging;\n (\n window as Window & {\n enableYakLogging?: typeof enableYakLogging;\n disableYakLogging?: typeof disableYakLogging;\n }\n ).disableYakLogging = disableYakLogging;\n}\n\nexport const logger = {\n debug: (message: string, data?: unknown): void => {\n if (isLoggingEnabled()) {\n if (data !== undefined) {\n console.log(`[yak-host] ${message}`, data);\n } else {\n console.log(`[yak-host] ${message}`);\n }\n }\n },\n\n info: (message: string, data?: unknown): void => {\n if (isLoggingEnabled()) {\n if (data !== undefined) {\n console.info(`[yak-host] ${message}`, data);\n } else {\n console.info(`[yak-host] ${message}`);\n }\n }\n },\n\n warn: (message: string, data?: unknown): void => {\n if (data !== undefined) {\n console.warn(`[yak-host] ${message}`, data);\n } else {\n console.warn(`[yak-host] ${message}`);\n }\n },\n\n error: (message: string, data?: unknown): void => {\n if (data !== undefined) {\n console.error(`[yak-host] ${message}`, data);\n } else {\n console.error(`[yak-host] ${message}`);\n }\n },\n};\n", "import type { PageContext } from \"./types/messaging.js\";\n\n/**\n * Extract visible text content from the DOM, filtering out non-content elements\n */\nfunction extractPageText(): string {\n if (typeof document === \"undefined\") return \"\";\n\n // Clone the body to avoid modifying the actual DOM\n const bodyClone = document.body.cloneNode(true) as HTMLElement;\n\n // Remove script, style, noscript tags and hidden elements\n const unwantedSelectors = [\n \"script\",\n \"style\",\n \"noscript\",\n \"iframe\",\n \"[style*='display: none']\",\n \"[style*='display:none']\",\n \"[hidden]\",\n \".yak-chat-widget\",\n ];\n\n for (const selector of unwantedSelectors) {\n const elements = bodyClone.querySelectorAll(selector);\n for (const el of elements) {\n el.remove();\n }\n }\n\n // Get text content and clean it up\n let text = bodyClone.textContent || bodyClone.innerText || \"\";\n\n // Normalize whitespace: replace multiple spaces/newlines with single space\n text = text.replace(/\\s+/g, \" \").trim();\n\n // Limit to reasonable size (100KB max)\n const maxLength = 100000;\n if (text.length > maxLength) {\n text = `${text.substring(0, maxLength)}... [truncated]`;\n }\n\n return text;\n}\n\n/**\n * Extract page context including URL, title, and visible text content\n */\nexport function extractPageContext(): PageContext {\n if (typeof window === \"undefined\") {\n return {\n url: \"\",\n title: \"\",\n text: \"\",\n timestamp: Date.now(),\n };\n }\n return {\n url: window.location.href,\n title: document.title,\n text: extractPageText(),\n timestamp: Date.now(),\n };\n}\n\n/**\n * Debounce function to limit how often a function is called\n */\nexport function debounce<T extends (...args: unknown[]) => void>(\n func: T,\n wait: number\n): (...args: Parameters<T>) => void {\n let timeout: NodeJS.Timeout | null = null;\n\n return function executedFunction(...args: Parameters<T>) {\n const later = () => {\n timeout = null;\n func(...args);\n };\n\n if (timeout) {\n clearTimeout(timeout);\n }\n timeout = setTimeout(later, wait);\n };\n}\n", "/**\n * Embed protocol version.\n *\n * This version is used in the embed URL path (e.g., /embed/v1/[appId])\n * and in the message protocol to ensure compatibility between the\n * host packages (@yak-io/javascript, @yak-io/react, @yak-io/nextjs)\n * and the embedded chat UI.\n *\n * Increment this version when making breaking changes to:\n * - The postMessage protocol (IframeMessageFromHost, IframeMessageToHost)\n * - The tool manifest format\n * - The route manifest format\n * - The config payload structure\n *\n * Version history:\n * - v1: Initial versioned protocol\n */\nexport const EMBED_PROTOCOL_VERSION = \"1\" as const;\n\n/**\n * Type for the embed protocol version\n */\nexport type EmbedProtocolVersion = typeof EMBED_PROTOCOL_VERSION;\n", "import { isYakLoggingEnabled, logger } from \"./logger.js\";\nimport { debounce, extractPageContext } from \"./page-context.js\";\nimport type { ChatConfig } from \"./types/config.js\";\nimport type {\n IframeMessageFromHost,\n IframeMessageToHost,\n Theme,\n UserIdentity,\n} from \"./types/messaging.js\";\nimport type { ToolCallEventListener, ToolCallHandler } from \"./types/tools.js\";\nimport { EMBED_PROTOCOL_VERSION } from \"./version.js\";\n\n/** localStorage key for the per-app signed session token. */\nconst SESSION_STORAGE_KEY = (appId: string) => `yak:session:${appId}`;\n/** localStorage key for the per-app active-conversation pointer. */\nconst CONVERSATION_POINTER_KEY = (appId: string) => `yak:conversation:${appId}`;\n\n/** Read a stored session token for this app, if any. SSR-safe. */\nfunction readStoredSessionToken(appId: string): string | undefined {\n if (typeof window === \"undefined\" || typeof window.localStorage === \"undefined\") return undefined;\n try {\n return window.localStorage.getItem(SESSION_STORAGE_KEY(appId)) ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction writeStoredSessionToken(appId: string, token: string): void {\n if (typeof window === \"undefined\" || typeof window.localStorage === \"undefined\") return;\n try {\n window.localStorage.setItem(SESSION_STORAGE_KEY(appId), token);\n } catch {\n // localStorage can throw in private-browsing modes; silently ignore.\n }\n}\n\nfunction readStoredConversationPointer(appId: string): string | undefined {\n if (typeof window === \"undefined\" || typeof window.localStorage === \"undefined\") return undefined;\n try {\n return window.localStorage.getItem(CONVERSATION_POINTER_KEY(appId)) ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction writeStoredConversationPointer(appId: string, pointer: string): void {\n if (typeof window === \"undefined\" || typeof window.localStorage === \"undefined\") return;\n try {\n window.localStorage.setItem(CONVERSATION_POINTER_KEY(appId), pointer);\n } catch {\n // localStorage can throw in private-browsing modes; silently ignore.\n }\n}\n\nfunction clearStoredConversationPointer(appId: string): void {\n if (typeof window === \"undefined\" || typeof window.localStorage === \"undefined\") return;\n try {\n window.localStorage.removeItem(CONVERSATION_POINTER_KEY(appId));\n } catch {\n // localStorage can throw in private-browsing modes; silently ignore.\n }\n}\n\n/** Production chat origin \u2014 where the widget iframe loads from by default. */\nconst DEFAULT_CHAT_ORIGIN = \"https://chat.yak.io\";\n\n// Local development flag, set by yak's own apps to point the widget at a chat\n// UI running on localhost. `__YAK_LOGGING_ENABLED__` toggles verbose SDK logs.\ndeclare global {\n interface Window {\n __YAK_INTERNAL_DEV__?: boolean;\n __YAK_LOGGING_ENABLED__?: boolean;\n }\n}\n\n/**\n * Resolves the iframe origin when no explicit `origin` is configured. Points at\n * a local chat UI during yak's own local development; production otherwise.\n * Integrators override the result with the `origin` config option.\n */\nfunction getDefaultIframeOrigin(): string {\n if (\n typeof window !== \"undefined\" &&\n (window.location.hostname === \"localhost\" || window.location.hostname === \"127.0.0.1\") &&\n typeof window.__YAK_INTERNAL_DEV__ !== \"undefined\"\n ) {\n return \"http://localhost:3001\";\n }\n return DEFAULT_CHAT_ORIGIN;\n}\n\nexport interface YakClientConfig {\n appId: string;\n /**\n * Override the origin the chat widget iframe loads from. Defaults to\n * `https://chat.yak.io`. Most integrators never set this \u2014 it exists for\n * non-production environments (e.g. a chat UI running on localhost).\n */\n origin?: string;\n /**\n * Handler for tool calls from the chat widget.\n * The consuming platform decides how to execute (browser, server fetch, etc.)\n *\n * @example Browser-only execution\n * ```ts\n * onToolCall: async (name, args) => {\n * if (name === \"ui.scrollTo\") {\n * document.getElementById(args.id)?.scrollIntoView();\n * return { success: true };\n * }\n * throw new Error(`Unknown tool: ${name}`);\n * }\n * ```\n *\n * @example Server delegation\n * ```ts\n * onToolCall: async (name, args) => {\n * const res = await fetch(\"/api/yak/tools\", {\n * method: \"POST\",\n * body: JSON.stringify({ name, args }),\n * });\n * const data = await res.json();\n * if (!data.ok) throw new Error(data.error);\n * return data.result;\n * }\n * ```\n */\n onToolCall?: ToolCallHandler;\n theme?: Theme;\n chatConfig?: ChatConfig;\n onRedirect?: (path: string) => void;\n onClose?: () => void;\n onReady?: () => void;\n /**\n * Called after every tool call completes (success or failure).\n * Useful for page-level cache invalidation based on which tools were called.\n *\n * @example\n * ```ts\n * onToolCallComplete: (event) => {\n * if (event.ok && event.name.startsWith(\"order.\")) {\n * queryClient.invalidateQueries({ queryKey: [\"orders\"] });\n * }\n * }\n * ```\n */\n onToolCallComplete?: ToolCallEventListener;\n /** Chat configuration options */\n options?: {\n /** Disable the restart session button in the header */\n disableRestartButton?: boolean;\n };\n /**\n * Signed end-user identity. When supplied, the widget persists conversations\n * server-side keyed to this user and surfaces a history pane. The `hash`\n * must be HMAC-SHA256(apiSecret, id) computed on the integrator's backend \u2014\n * never expose `apiSecret` to the browser.\n *\n * @example\n * ```ts\n * // Integrator backend (Node.js)\n * const hash = crypto\n * .createHmac(\"sha256\", process.env.YAK_API_SECRET)\n * .update(currentUser.id)\n * .digest(\"hex\");\n *\n * // Browser\n * new YakClient({\n * appId: \"app_abc\",\n * user: { id: currentUser.id, hash },\n * });\n * ```\n */\n user?: UserIdentity;\n}\n\nexport class YakClient {\n private config: YakClientConfig;\n private iframeWindow: Window | null = null;\n private isWidgetOpen = false;\n private readyTarget: { window: Window; origin: string } | null = null;\n private unexpectedOriginLogged = false;\n private lastUrl = \" \";\n private debouncedSendContext: () => void;\n private observer: MutationObserver | null = null;\n\n constructor(config: YakClientConfig) {\n this.config = config;\n this.debouncedSendContext = debounce(() => {\n logger.debug(\"DOM mutation detected, sending page context\");\n this.sendPageContext();\n }, 2000);\n }\n\n public updateConfig(newConfig: Partial<YakClientConfig>) {\n this.config = { ...this.config, ...newConfig };\n // Resend config when the iframe is ready and we have anything to deliver \u2014\n // tool/route manifests, or a user identity that needs to land in the\n // widget so persistence/history light up.\n if (this.readyTarget && (this.config.chatConfig || this.config.user)) {\n this.sendConfigToIframe(this.readyTarget.window, this.readyTarget.origin);\n }\n }\n\n /**\n * Get the iframe origin URL (base URL for the chat widget). Recomputed on\n * each call so environment-dependent defaults resolve correctly.\n */\n public getIframeOrigin(): string {\n return this.config.origin ?? getDefaultIframeOrigin();\n }\n\n /**\n * Get the full iframe embed URL for the chatbot\n *\n * @example\n * ```ts\n * const client = new YakClient({ appId: \"my-app\" });\n * const iframeSrc = client.getEmbedUrl();\n * // Returns: \"https://chat.yak.io/embed/v1/my-app\"\n * ```\n */\n public getEmbedUrl(): string {\n const origin = this.getIframeOrigin();\n const baseUrl = `${origin}/embed/v${EMBED_PROTOCOL_VERSION}/${encodeURIComponent(this.config.appId)}`;\n\n const params = new URLSearchParams();\n const theme = this.config.theme;\n\n if (theme?.colorMode && theme.colorMode !== \"system\") {\n params.set(\"colorMode\", theme.colorMode);\n }\n\n this.appendThemeColors(params, theme?.light, \"light\");\n this.appendThemeColors(params, theme?.dark, \"dark\");\n\n // Pass debug flag to iframe so it can enable logging immediately\n if (isYakLoggingEnabled()) {\n params.set(\"yakDebug\", \"1\");\n }\n\n const queryString = params.toString();\n return queryString ? `${baseUrl}?${queryString}` : baseUrl;\n }\n\n /**\n * Append theme color parameters to a URLSearchParams object\n */\n private appendThemeColors(\n params: URLSearchParams,\n colors: Theme[\"light\"] | Theme[\"dark\"] | undefined,\n prefix: \"light\" | \"dark\"\n ): void {\n if (!colors) return;\n const map: [string, string | undefined][] = [\n [`${prefix}Bg`, colors.background],\n [`${prefix}Border`, colors.border],\n [`${prefix}MessageBg`, colors.messageBackground],\n [`${prefix}Placeholder`, colors.placeholderColor],\n [`${prefix}SubmitBtn`, colors.submitButtonColor],\n [`${prefix}SubmitBtnText`, colors.submitButtonTextColor],\n [`${prefix}HeaderIcon`, colors.headerIconColor],\n ];\n for (const [key, value] of map) {\n if (value) params.set(key, value);\n }\n }\n\n /**\n * Get the app ID\n */\n public getAppId(): string {\n return this.config.appId;\n }\n\n /**\n * Get the current theme configuration\n */\n public getTheme(): Theme | undefined {\n return this.config.theme;\n }\n\n /**\n * Send a prompt message to the chatbot iframe\n * Note: The iframe must be ready to receive messages (onReady callback must have fired)\n *\n * @example\n * ```ts\n * const client = new YakClient({ appId: \"my-app\", onReady: () => {\n * client.sendPrompt(\"Help me with my order\");\n * }});\n * ```\n */\n public sendPrompt(prompt: string): void {\n const target = this.getActiveTarget();\n if (!target) {\n logger.warn(\"Cannot send prompt: iframe not ready\");\n return;\n }\n\n const message: IframeMessageFromHost = {\n type: \"yak:prompt\",\n payload: { prompt },\n };\n target.window.postMessage(message, target.origin);\n }\n\n /**\n * Send a focus request to the chatbot iframe\n * This will focus the chat input field\n */\n public sendFocus(): void {\n const target = this.getActiveTarget();\n if (!target) {\n logger.warn(\"Cannot send focus: iframe not ready\");\n return;\n }\n\n const message: IframeMessageFromHost = {\n type: \"yak:focus\",\n };\n target.window.postMessage(message, target.origin);\n }\n\n /**\n * The live postMessage target. Prefers `readyTarget` \u2014 the window that\n * completed the `yak:ready` handshake \u2014 and falls back to the iframe's\n * `contentWindow`. The two are normally identical, but they're populated by\n * different events (the ready postMessage vs. the DOM `load` event) and can\n * briefly diverge (e.g. a remount under React StrictMode, where `isReady`\n * goes true before \u2014 or without \u2014 the load event repopulating\n * `iframeWindow`). Trusting `readyTarget` keeps `sendFocus`/`sendPrompt`\n * consistent with `isReady()` and avoids spurious \"iframe not ready\" warns.\n */\n private getActiveTarget(): { window: Window; origin: string } | null {\n if (this.readyTarget) return this.readyTarget;\n if (this.iframeWindow) return { window: this.iframeWindow, origin: this.getIframeOrigin() };\n return null;\n }\n\n /**\n * Check if the iframe is ready to receive messages\n */\n public isReady(): boolean {\n return this.readyTarget !== null;\n }\n\n public setIframeWindow(window: Window | null) {\n this.iframeWindow = window;\n if (!window) {\n this.readyTarget = null;\n }\n }\n\n public setWidgetOpen(isOpen: boolean) {\n this.isWidgetOpen = isOpen;\n if (isOpen && this.readyTarget && this.config.chatConfig) {\n this.sendConfigToIframe(this.readyTarget.window, this.readyTarget.origin);\n }\n\n if (isOpen) {\n this.startObserving();\n } else {\n this.stopObserving();\n }\n }\n\n public mount() {\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"message\", this.handleMessage);\n window.addEventListener(\"popstate\", this.handlePopState);\n }\n }\n\n public unmount() {\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"message\", this.handleMessage);\n window.removeEventListener(\"popstate\", this.handlePopState);\n }\n this.stopObserving();\n }\n\n private startObserving() {\n if (typeof window === \"undefined\" || !this.iframeWindow) return;\n\n // Initial check\n const currentUrl = window.location.href;\n if (currentUrl !== this.lastUrl) {\n this.lastUrl = currentUrl;\n logger.debug(\"URL changed, sending page context\");\n this.sendPageContext();\n }\n\n if (this.observer) return;\n\n this.observer = new MutationObserver((mutations) => {\n const hasSubstantialChanges = mutations.some(\n (mutation) =>\n mutation.type === \"childList\" &&\n (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)\n );\n\n if (hasSubstantialChanges) {\n this.debouncedSendContext();\n }\n });\n\n this.observer.observe(document.body, {\n childList: true,\n subtree: true,\n });\n }\n\n private stopObserving() {\n if (this.observer) {\n this.observer.disconnect();\n this.observer = null;\n }\n }\n\n private handlePopState = () => {\n logger.debug(\"Navigation detected, sending page context\");\n this.sendPageContext();\n };\n\n private handleMessage = (event: MessageEvent) => {\n if (typeof window === \"undefined\") return;\n if (!this.isWidgetOpen && event.data?.type !== \"yak:ready\") {\n return;\n }\n\n const hostOrigin = window.location.origin;\n const allowedOrigins = new Set<string>();\n allowedOrigins.add(this.getIframeOrigin());\n if (hostOrigin) {\n allowedOrigins.add(hostOrigin);\n }\n\n if (!allowedOrigins.has(event.origin)) {\n if (!this.unexpectedOriginLogged) {\n logger.warn(\n `Ignoring message from unexpected origin: ${event.origin}, allowed: ${Array.from(allowedOrigins).join(\", \")}`\n );\n this.unexpectedOriginLogged = true;\n }\n return;\n }\n\n // Validate message structure\n if (!event.data || typeof event.data !== \"object\" || !(\"type\" in event.data)) {\n // Filter out known browser extension messages\n const data = event.data as Record<string, unknown>;\n const isReactDevTools =\n data?.source === \"react-devtools-content-script\" ||\n data?.source === \"react-devtools-bridge\" ||\n data?.source === \"react-devtools-inject-backend\";\n const isReduxDevTools = data?.source === \"@devtools-page\";\n\n if (isReactDevTools || isReduxDevTools) {\n return;\n }\n return;\n }\n\n const message = event.data as IframeMessageToHost;\n const targetWindow =\n (event.source && \"postMessage\" in event.source ? (event.source as Window) : null) ??\n this.iframeWindow;\n const targetOrigin = this.getIframeOrigin();\n\n logger.debug(\"Message received from iframe:\", message.type);\n\n switch (message.type) {\n case \"yak:ready\": {\n logger.debug(\"Iframe ready, sending config\");\n\n if (targetWindow) {\n this.readyTarget = { window: targetWindow, origin: targetOrigin };\n this.sendConfigToIframe(targetWindow, targetOrigin);\n\n // Send initial page context after config\n setTimeout(() => this.sendPageContext(), 100);\n\n // Mark iframe as ready after config is sent\n setTimeout(() => this.config.onReady?.(), 200);\n } else {\n logger.warn(\"Unable to send config: iframe window not registered yet\");\n }\n break;\n }\n\n case \"yak:tool_call\": {\n const { id, name, args } = message.payload;\n void this.handleToolCall(id, name, args);\n break;\n }\n\n case \"yak:redirect\": {\n const { path } = message.payload;\n logger.debug(\"Redirect request received:\", path);\n\n if (!this.isAllowedRedirect(path)) {\n logger.warn(\"Blocked potentially unsafe redirect:\", path);\n break;\n }\n\n if (this.config.onRedirect) {\n this.config.onRedirect(path);\n } else if (typeof window !== \"undefined\") {\n window.location.assign(path);\n }\n break;\n }\n\n case \"yak:session\": {\n const { sessionToken } = message.payload;\n logger.debug(\"Session token received from iframe; persisting\");\n writeStoredSessionToken(this.config.appId, sessionToken);\n break;\n }\n\n case \"yak:conversation\": {\n const { pointer } = message.payload;\n if (pointer === null) {\n logger.debug(\"Conversation pointer cleared by iframe\");\n clearStoredConversationPointer(this.config.appId);\n } else {\n logger.debug(\"Conversation pointer received from iframe; persisting\");\n writeStoredConversationPointer(this.config.appId, pointer);\n }\n break;\n }\n\n case \"yak:close\": {\n logger.debug(\"Close message received from iframe\");\n this.config.onClose?.();\n break;\n }\n\n default:\n logger.debug(\"Unknown message type:\", (message as { type: string }).type);\n break;\n }\n };\n\n private sendConfigToIframe(targetWindow: Window, targetOrigin: string) {\n // Get logging enabled state from window flag\n const loggingEnabled =\n typeof window !== \"undefined\" && typeof window.__YAK_LOGGING_ENABLED__ === \"boolean\"\n ? window.__YAK_LOGGING_ENABLED__\n : undefined;\n\n const storedSessionToken = readStoredSessionToken(this.config.appId);\n const storedConversationPointer = readStoredConversationPointer(this.config.appId);\n\n const configMessage: IframeMessageFromHost = {\n type: \"yak:config\",\n payload: {\n version: EMBED_PROTOCOL_VERSION,\n appId: this.config.appId,\n theme: this.config.theme,\n toolManifest: this.config.chatConfig?.tools ?? undefined,\n routeManifest: this.config.chatConfig?.routes ?? undefined,\n options: this.config.options,\n loggingEnabled,\n user: this.config.user,\n sessionToken: storedSessionToken,\n conversationPointer: storedConversationPointer,\n },\n };\n\n logger.debug(\"Posting config to iframe origin:\", {\n origin: targetOrigin,\n version: EMBED_PROTOCOL_VERSION,\n hasToolManifest: Boolean(this.config.chatConfig?.tools),\n toolCount: this.config.chatConfig?.tools?.tools.length ?? 0,\n hasRouteManifest: Boolean(this.config.chatConfig?.routes),\n });\n\n targetWindow.postMessage(configMessage, targetOrigin);\n }\n\n private sendPageContext() {\n if (!this.iframeWindow) return;\n\n try {\n const pageContext = extractPageContext();\n const message: IframeMessageFromHost = {\n type: \"yak:page_context\",\n payload: pageContext,\n };\n logger.debug(\"Sending page context to iframe:\", {\n url: pageContext.url,\n title: pageContext.title,\n textLength: pageContext.text.length,\n });\n this.iframeWindow.postMessage(message, this.getIframeOrigin());\n } catch (error) {\n logger.error(\"Error extracting page context:\", error);\n }\n }\n\n private async handleToolCall(id: string, name: string, args: unknown): Promise<void> {\n logger.debug(`Tool call received: ${name}`, { id, args });\n\n if (!this.config.onToolCall) {\n logger.error(\"Tool call received but no onToolCall handler configured\");\n this.sendToolResultToIframe(id, false, undefined, \"No tool call handler configured\");\n this.config.onToolCallComplete?.({\n name,\n args,\n ok: false,\n error: \"No tool call handler configured\",\n });\n return;\n }\n\n try {\n const result = await this.config.onToolCall(name, args);\n logger.debug(`Tool call succeeded: ${name}`, { id });\n this.sendToolResultToIframe(id, true, result);\n this.config.onToolCallComplete?.({ name, args, ok: true, result });\n } catch (error) {\n const errorMessage = this.extractErrorMessage(error);\n // A thrown tool error is the documented way to surface a failure to the\n // model (see the adapter `execute` contract) \u2014 it's captured, returned to\n // the widget, and reported to the integrator via `onToolCallComplete`.\n // That makes it expected data flow, not a console-worthy fault, so log it\n // at debug to avoid noise in the host page's console.\n logger.debug(`Tool call failed: ${name}`, { id, error });\n this.sendToolResultToIframe(id, false, undefined, errorMessage);\n this.config.onToolCallComplete?.({ name, args, ok: false, error: errorMessage });\n }\n }\n\n private sendToolResultToIframe(id: string, ok: true, result: unknown): void;\n private sendToolResultToIframe(id: string, ok: false, result: undefined, error: string): void;\n private sendToolResultToIframe(id: string, ok: boolean, result: unknown, error?: string): void {\n if (!this.iframeWindow) return;\n\n if (ok) {\n // Serialize result to remove non-cloneable values (functions, etc.)\n // postMessage uses structured clone which cannot handle functions\n const safeResult = this.toSerializable(result);\n const message: IframeMessageFromHost = {\n type: \"yak:tool_result\",\n payload: { id, ok: true, result: safeResult },\n };\n this.iframeWindow.postMessage(message, this.getIframeOrigin());\n } else {\n const message: IframeMessageFromHost = {\n type: \"yak:tool_result\",\n payload: { id, ok: false, error: error ?? \"Unknown error\" },\n };\n this.iframeWindow.postMessage(message, this.getIframeOrigin());\n }\n }\n\n /**\n * Convert a value to a serializable form by stripping functions and other non-cloneable values.\n * Uses JSON.parse(JSON.stringify()) which handles most cases.\n */\n private toSerializable(value: unknown): unknown {\n if (value === undefined || value === null) {\n return value;\n }\n try {\n return JSON.parse(JSON.stringify(value));\n } catch {\n // If JSON serialization fails, return a string representation\n logger.warn(\"Failed to serialize tool result, returning string representation\");\n return String(value);\n }\n }\n\n private extractErrorMessage(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n if (typeof error === \"string\") {\n return error;\n }\n return \"Unknown error\";\n }\n\n /**\n * Validates that a redirect path is safe (relative path or same-origin).\n * Blocks absolute URLs to external domains to prevent open redirect attacks.\n */\n private isAllowedRedirect(path: string): boolean {\n // Allow relative paths that don't start with // (protocol-relative URLs)\n if (path.startsWith(\"/\") && !path.startsWith(\"//\")) {\n return true;\n }\n // Allow hash-only and query-only paths\n if (path.startsWith(\"#\") || path.startsWith(\"?\")) {\n return true;\n }\n // For absolute URLs, verify same origin\n if (typeof window !== \"undefined\") {\n try {\n const url = new URL(path, window.location.origin);\n return url.origin === window.location.origin;\n } catch {\n // Invalid URL - block it\n return false;\n }\n }\n // In non-browser environments, only allow relative paths\n return false;\n }\n}\n", "/**\n * Pure state machine for a single voice session.\n *\n * The reducer has no DOM or WebRTC dependencies \u2014 it can be unit-tested by\n * driving events through `voiceReducer` and checking the resulting state.\n *\n * The companion `handleRealtimeMessage` parses an OpenAI Realtime data-channel\n * message and dispatches reducer events plus side effects (tool dispatch,\n * sending follow-up events back over the data channel). Side effects are\n * delegated to the injected `RealtimeMessageContext` so the function is\n * testable with a plain in-memory mock.\n */\n\n/**\n * Lifecycle of a voice session, in order:\n * - `idle` \u2014 no session (the starting and stopped state).\n * - `connecting` \u2014 establishing the WebRTC connection.\n * - `listening` \u2014 connected and capturing the user's speech.\n * - `thinking` \u2014 the model is processing / generating a response.\n * - `speaking` \u2014 the assistant is playing audio back.\n * - `error` \u2014 the session failed; see {@link VoiceMachine.errorMessage}.\n */\nexport type VoiceState = \"idle\" | \"connecting\" | \"listening\" | \"thinking\" | \"speaking\" | \"error\";\n\nexport type VoiceEvent =\n | { type: \"start\" }\n | { type: \"connected\" }\n /** An assistant-initiated turn was requested (e.g. the opening greeting). */\n | { type: \"response_requested\" }\n | { type: \"speech_started\" }\n | { type: \"speech_stopped\" }\n | { type: \"audio_delta\" }\n | { type: \"audio_stopped\" }\n | { type: \"stop\" }\n | { type: \"error\"; message: string };\n\n/** Snapshot of a voice session's state machine. */\nexport interface VoiceMachine {\n /** Current lifecycle state of the session. */\n state: VoiceState;\n /** Human-readable failure reason \u2014 set only when `state === \"error\"`. */\n errorMessage?: string;\n}\n\nexport const INITIAL_VOICE_MACHINE: VoiceMachine = { state: \"idle\" };\n\nexport function voiceReducer(machine: VoiceMachine, event: VoiceEvent): VoiceMachine {\n switch (event.type) {\n case \"start\":\n return machine.state === \"idle\" ? { state: \"connecting\" } : machine;\n case \"connected\":\n return machine.state === \"connecting\" ? { state: \"listening\" } : machine;\n case \"response_requested\":\n // The assistant is generating an unprompted turn (the opening greeting).\n // Move to `thinking` so the subsequent `audio_delta` lands on `speaking`,\n // matching a normal turn; no-op if we're not idling in `listening`.\n return machine.state === \"listening\" ? { state: \"thinking\" } : machine;\n case \"speech_started\":\n if (machine.state === \"idle\" || machine.state === \"error\") return machine;\n return { state: \"listening\" };\n case \"speech_stopped\":\n return machine.state === \"listening\" ? { state: \"thinking\" } : machine;\n case \"audio_delta\":\n if (machine.state === \"thinking\" || machine.state === \"speaking\") {\n return { state: \"speaking\" };\n }\n return machine;\n case \"audio_stopped\":\n return machine.state === \"speaking\" ? { state: \"listening\" } : machine;\n case \"stop\":\n return { state: \"idle\" };\n case \"error\":\n return { state: \"error\", errorMessage: event.message };\n default: {\n const _exhaustive: never = event;\n void _exhaustive;\n return machine;\n }\n }\n}\n\n/**\n * Per-`response.done` token usage emitted by OpenAI's Realtime API.\n * The SDK accumulates these across the session and ships the totals to the\n * `session-event` stop endpoint so billing has the dimensions it needs.\n */\nexport interface RealtimeResponseUsage {\n /** Total input tokens for this response (text + audio combined). */\n inputTokens?: number;\n /** Cached input tokens \u2014 subset of `inputTokens`. */\n cachedInputTokens?: number;\n /** Total output tokens for this response (text + audio combined). */\n outputTokens?: number;\n /** Audio-input tokens (subset of `inputTokens`). */\n audioInputTokens?: number;\n /** Audio-output tokens (subset of `outputTokens`). */\n audioOutputTokens?: number;\n /** Text-input tokens (subset of `inputTokens`). */\n textInputTokens?: number;\n /** Text-output tokens (subset of `outputTokens`). */\n textOutputTokens?: number;\n}\n\nexport interface RealtimeMessageContext {\n send: (event: VoiceEvent) => void;\n sendData: (payload: unknown) => void;\n dispatchToolCall: (name: string, args: unknown) => Promise<unknown>;\n isDispatched: (callId: string) => boolean;\n markDispatched: (callId: string) => void;\n /** Forward a per-response usage payload to the session for accumulation. */\n recordUsage?: (usage: RealtimeResponseUsage) => void;\n}\n\ninterface RealtimeFunctionCallItem {\n type: \"function_call\";\n call_id?: string;\n name?: string;\n arguments?: string;\n}\n\ninterface RealtimeResponseDoneItem {\n type?: string;\n call_id?: string;\n name?: string;\n arguments?: string;\n}\n\ninterface RealtimeUsageTokenDetails {\n cached_tokens?: number;\n audio_tokens?: number;\n text_tokens?: number;\n}\n\ninterface RealtimeUsage {\n input_tokens?: number;\n output_tokens?: number;\n total_tokens?: number;\n input_token_details?: RealtimeUsageTokenDetails;\n output_token_details?: RealtimeUsageTokenDetails;\n}\n\ninterface RealtimeResponseDone {\n output?: RealtimeResponseDoneItem[];\n usage?: RealtimeUsage;\n}\n\ninterface RealtimeMessage {\n type?: string;\n response?: RealtimeResponseDone;\n error?: { message?: string };\n}\n\nfunction isFunctionCall(item: RealtimeResponseDoneItem): item is RealtimeFunctionCallItem {\n return item.type === \"function_call\";\n}\n\nfunction parseToolArgs(raw: string | undefined): unknown {\n if (!raw) return {};\n try {\n return JSON.parse(raw);\n } catch {\n return {};\n }\n}\n\nasync function dispatchFunctionCall(\n call: RealtimeFunctionCallItem,\n ctx: RealtimeMessageContext\n): Promise<void> {\n const callId = call.call_id;\n const name = call.name;\n if (!callId || !name) return;\n if (ctx.isDispatched(callId)) return;\n ctx.markDispatched(callId);\n\n const args = parseToolArgs(call.arguments);\n\n let output: string;\n try {\n const result = await ctx.dispatchToolCall(name, args);\n output = JSON.stringify(result ?? null);\n } catch (error) {\n output = JSON.stringify({\n error: error instanceof Error ? error.message : \"Tool execution failed\",\n });\n }\n\n ctx.sendData({\n type: \"conversation.item.create\",\n item: { type: \"function_call_output\", call_id: callId, output },\n });\n ctx.sendData({ type: \"response.create\" });\n}\n\nfunction extractUsage(raw: RealtimeUsage | undefined): RealtimeResponseUsage | null {\n if (!raw) return null;\n const usage: RealtimeResponseUsage = {};\n if (typeof raw.input_tokens === \"number\") usage.inputTokens = raw.input_tokens;\n if (typeof raw.output_tokens === \"number\") usage.outputTokens = raw.output_tokens;\n const inDetails = raw.input_token_details;\n if (inDetails) {\n if (typeof inDetails.cached_tokens === \"number\") {\n usage.cachedInputTokens = inDetails.cached_tokens;\n }\n if (typeof inDetails.audio_tokens === \"number\") {\n usage.audioInputTokens = inDetails.audio_tokens;\n }\n if (typeof inDetails.text_tokens === \"number\") {\n usage.textInputTokens = inDetails.text_tokens;\n }\n }\n const outDetails = raw.output_token_details;\n if (outDetails) {\n if (typeof outDetails.audio_tokens === \"number\") {\n usage.audioOutputTokens = outDetails.audio_tokens;\n }\n if (typeof outDetails.text_tokens === \"number\") {\n usage.textOutputTokens = outDetails.text_tokens;\n }\n }\n return Object.keys(usage).length > 0 ? usage : null;\n}\n\nasync function handleResponseDone(\n response: RealtimeResponseDone | undefined,\n ctx: RealtimeMessageContext\n): Promise<void> {\n const usage = extractUsage(response?.usage);\n if (usage && ctx.recordUsage) {\n try {\n ctx.recordUsage(usage);\n } catch {\n // recordUsage is best-effort; never let it break the session loop.\n }\n }\n\n const calls = (response?.output ?? []).filter(isFunctionCall);\n for (const call of calls) {\n await dispatchFunctionCall(call, ctx);\n }\n}\n\nexport async function handleRealtimeMessage(\n raw: string,\n ctx: RealtimeMessageContext\n): Promise<void> {\n let message: RealtimeMessage;\n try {\n message = JSON.parse(raw) as RealtimeMessage;\n } catch {\n return;\n }\n\n switch (message.type) {\n case \"input_audio_buffer.speech_started\":\n ctx.send({ type: \"speech_started\" });\n return;\n case \"input_audio_buffer.speech_stopped\":\n ctx.send({ type: \"speech_stopped\" });\n return;\n case \"response.output_audio_transcript.delta\":\n case \"response.audio_transcript.delta\":\n ctx.send({ type: \"audio_delta\" });\n return;\n case \"output_audio_buffer.stopped\":\n case \"response.output_audio_buffer.stopped\":\n ctx.send({ type: \"audio_stopped\" });\n return;\n case \"response.done\":\n await handleResponseDone(message.response, ctx);\n return;\n case \"error\":\n ctx.send({\n type: \"error\",\n message: message.error?.message ?? \"Voice session error\",\n });\n return;\n default:\n return;\n }\n}\n", "/** OpenAI/Realtime function names must match `^[a-zA-Z0-9_-]{1,64}$`. */\nconst MAX_TOOL_NAME_LENGTH = 64;\n\n/**\n * Convert a host tool name into a model-facing function name.\n *\n * We sanitise to the allowed function-name charset (rather than hashing to an opaque\n * `yt_<hex>`), so the model keeps the semantic signal of a readable name \u2014 e.g.\n * `orders.list` \u2192 `orders_list`.\n *\n * This is a pure per-name transform; uniqueness across a manifest (and avoiding the\n * reserved `redirect` name / `mcp__` namespace) is handled by the caller via\n * {@link uniqueToolId}. Each runtime (the chat-ui iframe and this SDK's voice session)\n * decorates and reverse-maps with its own manifest, so the ids never need to match\n * across paths \u2014 only to be self-consistent within one.\n */\nexport function generateToolId(originalName: string): string {\n const sanitized = originalName.replace(/[^A-Za-z0-9_-]/g, \"_\").slice(0, MAX_TOOL_NAME_LENGTH);\n return sanitized.length > 0 ? sanitized : \"tool\";\n}\n\n/** The MCP namespace prefix the voice/chat dispatchers route on. */\nconst MCP_NAMESPACE_PREFIX = \"mcp__\";\n\n/**\n * Resolve a collision-free, model-safe id for a host tool name, given the ids already\n * taken in this manifest (seed `used` with reserved names like `redirect`). Host ids must\n * never start with the `mcp__` namespace, which the dispatcher routes on.\n */\nexport function uniqueToolId(originalName: string, used: Set<string>): string {\n let base = generateToolId(originalName);\n if (base.startsWith(MCP_NAMESPACE_PREFIX)) {\n base = `t_${base}`.slice(0, MAX_TOOL_NAME_LENGTH);\n }\n if (!used.has(base)) return base;\n\n for (let i = 2; i < 1000; i++) {\n const suffix = `_${i}`;\n const candidate = `${base.slice(0, MAX_TOOL_NAME_LENGTH - suffix.length)}${suffix}`;\n if (!used.has(candidate)) return candidate;\n }\n // Pathological fallback \u2014 vanishingly unlikely in practice.\n return `${base.slice(0, 56)}_${used.size}`;\n}\n", "import { logger } from \"./logger.js\";\nimport { extractPageContext } from \"./page-context.js\";\nimport { uniqueToolId } from \"./tool-name.js\";\nimport type { ChatConfig, ChatConfigProvider } from \"./types/config.js\";\nimport type { PageContext } from \"./types/messaging.js\";\nimport type { ToolCallHandler, ToolDefinition } from \"./types/tools.js\";\nimport {\n handleRealtimeMessage,\n INITIAL_VOICE_MACHINE,\n type RealtimeMessageContext,\n type RealtimeResponseUsage,\n type VoiceMachine,\n voiceReducer,\n} from \"./voice-machine.js\";\n\nconst DEFAULT_REALTIME_MODEL = \"gpt-realtime\";\nconst REALTIME_CALLS_URL = \"https://api.openai.com/v1/realtime/calls\";\nconst DEFAULT_API_ORIGIN = \"https://chat.yak.io\";\n\n// Local development flag, set by yak's own apps to point voice sessions at a\n// chat UI running on localhost.\ndeclare global {\n interface Window {\n __YAK_INTERNAL_DEV__?: boolean;\n }\n}\n\n/**\n * Resolves the API origin when no explicit `apiOrigin` is configured. Points at\n * a local chat UI during yak's own local development; production otherwise.\n */\nfunction getDefaultApiOrigin(): string {\n if (\n typeof window !== \"undefined\" &&\n (window.location.hostname === \"localhost\" || window.location.hostname === \"127.0.0.1\") &&\n typeof window.__YAK_INTERNAL_DEV__ !== \"undefined\"\n ) {\n return \"http://localhost:3001\";\n }\n return DEFAULT_API_ORIGIN;\n}\n\nexport type VoiceStateListener = (machine: VoiceMachine) => void;\n\nexport interface YakVoiceSessionConfig {\n appId: string;\n /** Tool call handler. Same shape as `YakClientConfig.onToolCall`. */\n onToolCall?: ToolCallHandler;\n onRedirect?: (path: string) => void;\n /**\n * Static chat config (routes + tools). Sent to the mint endpoint so the LLM\n * knows what tools are available. Use this OR `getConfig`.\n */\n chatConfig?: ChatConfig;\n /**\n * Async provider for chat config. Called on every session start \u2014 useful when\n * tools/routes depend on the current page or user. Takes precedence over\n * `chatConfig` if both are provided.\n */\n getConfig?: ChatConfigProvider;\n /**\n * Override the API origin (where voice sessions are minted). Defaults to\n * `https://chat.yak.io`. Most integrators never set this.\n */\n apiOrigin?: string;\n}\n\ninterface MintResponse {\n clientSecret: string;\n expiresAt: number;\n voiceSessionId: string;\n /**\n * Whether to auto-greet on connect. Driven by the app's voice intro setting:\n * `false` only when the operator chose \"no intro\". A missing flag (older\n * server) is treated as `true` so existing apps keep greeting.\n */\n autoGreet?: boolean;\n}\n\ninterface SessionResources {\n pc: RTCPeerConnection | null;\n dataChannel: RTCDataChannel | null;\n micStream: MediaStream | null;\n audioElement: HTMLAudioElement | null;\n voiceSessionId: string | null;\n}\n\nconst EMPTY_RESOURCES: SessionResources = {\n pc: null,\n dataChannel: null,\n micStream: null,\n audioElement: null,\n voiceSessionId: null,\n};\n\n/**\n * Manages a single voice session against the OpenAI Realtime API.\n *\n * Runs entirely on the host page \u2014 no iframe. The mint endpoint\n * (`POST /api/voice/realtime-token`) is called cross-origin to chat.yak.io;\n * origin validation happens server-side via the app's `allowedOrigins`.\n *\n * Tool calls are dispatched to the configured handlers \u2014 the same ones\n * customers wire up for `YakEmbed`. The class manages WebRTC lifecycle,\n * full teardown on stop, and `pagehide` cleanup via `navigator.sendBeacon`.\n */\ninterface AccumulatedUsage {\n inputTokens: number;\n cachedInputTokens: number;\n outputTokens: number;\n audioInputTokens: number;\n audioOutputTokens: number;\n textInputTokens: number;\n textOutputTokens: number;\n responseCount: number;\n}\n\nfunction emptyUsage(): AccumulatedUsage {\n return {\n inputTokens: 0,\n cachedInputTokens: 0,\n outputTokens: 0,\n audioInputTokens: 0,\n audioOutputTokens: 0,\n textInputTokens: 0,\n textOutputTokens: 0,\n responseCount: 0,\n };\n}\n\nexport class YakVoiceSession {\n private config: YakVoiceSessionConfig;\n private machine: VoiceMachine = INITIAL_VOICE_MACHINE;\n private resources: SessionResources = EMPTY_RESOURCES;\n private dispatchedCallIds = new Set<string>();\n private listeners = new Set<VoiceStateListener>();\n private pageHideHandler: (() => void) | null = null;\n /** Per-session token totals, accumulated from each `response.done` event. */\n private usage: AccumulatedUsage = emptyUsage();\n /**\n * Reverse map: hashed tool id (what OpenAI calls back with) \u2192 original host\n * tool name (what `onToolCall` expects). Populated on every `start()` from\n * the resolved chat config.\n */\n private toolNameById = new Map<string, string>();\n\n constructor(config: YakVoiceSessionConfig) {\n this.config = config;\n this.attachPageHide();\n }\n\n /**\n * Resolve the API origin lazily on each call. Environment-dependent defaults\n * (e.g. a local chat UI) may not be ready at construction time, so resolving\n * eagerly would risk baking in the production URL.\n */\n private get apiOrigin(): string {\n return this.config.apiOrigin ?? getDefaultApiOrigin();\n }\n\n /** Update mutable config fields (handlers, getConfig). */\n public updateConfig(patch: Partial<YakVoiceSessionConfig>): void {\n this.config = { ...this.config, ...patch };\n }\n\n public getState(): VoiceMachine {\n return this.machine;\n }\n\n /**\n * The current API origin (defaults to `https://chat.yak.io`). Useful for\n * building URLs to static assets like the brand logo.\n */\n public getApiOrigin(): string {\n return this.apiOrigin;\n }\n\n public onStateChange(listener: VoiceStateListener): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Begin a voice session. Should be invoked from a user gesture (button\n * click) so `getUserMedia` and audio playback both have transient activation.\n */\n public async start(): Promise<void> {\n if (this.machine.state !== \"idle\") return;\n logger.debug(\"Voice: start() called\");\n this.usage = emptyUsage();\n this.dispatch({ type: \"start\" });\n\n let chatConfig: ChatConfig | undefined = this.config.chatConfig;\n if (this.config.getConfig) {\n try {\n chatConfig = await this.config.getConfig();\n logger.debug(\"Voice: getConfig() resolved\", {\n toolCount: chatConfig?.tools?.tools.length ?? 0,\n routeCount: chatConfig?.routes?.routes.length ?? 0,\n });\n } catch (err) {\n logger.warn(\"Voice: getConfig() failed\", err);\n }\n } else if (chatConfig) {\n logger.debug(\"Voice: using static chatConfig\", {\n toolCount: chatConfig.tools?.tools.length ?? 0,\n routeCount: chatConfig.routes?.routes.length ?? 0,\n });\n } else {\n logger.debug(\"Voice: no chatConfig or getConfig \u2014 only built-in tools will be available\");\n }\n\n // Decorate host tools with hash ids and build the reverse lookup so we\n // can map id-named tool calls back to the original host name when the\n // model invokes them. Mirrors the chat-ui iframe's decoration step.\n const decoratedManifest = this.buildDecoratedManifest(chatConfig);\n logger.debug(\"Voice: decorated tools\", {\n ids: decoratedManifest.tools.map((t) => `${t.id}=${t.name}`),\n });\n\n const pageContext = this.safeExtractPageContext();\n logger.debug(\"Voice: page context extracted\", {\n url: pageContext?.url,\n title: pageContext?.title,\n textLength: pageContext?.text?.length ?? 0,\n });\n\n let mint: MintResponse;\n try {\n logger.debug(\"Voice: requesting ephemeral token from mint endpoint\");\n mint = await this.mintToken(chatConfig, decoratedManifest, pageContext);\n logger.debug(\"Voice: mint succeeded\", {\n voiceSessionId: mint.voiceSessionId,\n expiresAt: mint.expiresAt,\n });\n } catch (err) {\n await this.failWith(err instanceof Error ? err.message : \"Failed to start voice session\");\n return;\n }\n\n let micStream: MediaStream;\n try {\n logger.debug(\"Voice: requesting microphone access\");\n micStream = await navigator.mediaDevices.getUserMedia({ audio: true });\n logger.debug(\"Voice: microphone access granted\");\n } catch (err) {\n const name = err instanceof Error ? err.name : \"\";\n const message =\n name === \"NotAllowedError\" || name === \"PermissionDeniedError\"\n ? \"Microphone permission was denied. Enable microphone access in your browser settings to use voice mode.\"\n : \"Could not access microphone.\";\n await this.failWith(message);\n return;\n }\n\n const pc = new RTCPeerConnection();\n const audioElement = document.createElement(\"audio\");\n audioElement.autoplay = true;\n audioElement.style.display = \"none\";\n document.body.appendChild(audioElement);\n\n pc.ontrack = (event) => {\n logger.debug(\"Voice: pc.ontrack received remote audio stream\");\n if (event.streams[0]) {\n audioElement.srcObject = event.streams[0];\n }\n };\n\n pc.oniceconnectionstatechange = () => {\n const s = pc.iceConnectionState;\n logger.debug(\"Voice: ICE connection state \u2192\", s);\n if (s === \"failed\" || s === \"disconnected\") {\n void this.failWith(`WebRTC connection ${s}`);\n }\n };\n pc.onconnectionstatechange = () => {\n logger.debug(\"Voice: peer connection state \u2192\", pc.connectionState);\n if (pc.connectionState === \"failed\") {\n void this.failWith(\"WebRTC connection failed\");\n }\n };\n\n for (const track of micStream.getAudioTracks()) {\n pc.addTrack(track, micStream);\n }\n\n const dataChannel = pc.createDataChannel(\"oai-events\");\n dataChannel.onmessage = (event) => {\n const raw = typeof event.data === \"string\" ? event.data : \"\";\n if (!raw) return;\n logger.debug(\"Voice: \u2190 data channel message\", raw.slice(0, 200));\n void handleRealtimeMessage(raw, this.buildMessageContext());\n };\n dataChannel.onopen = () => {\n logger.debug(\"Voice: data channel opened\");\n this.dispatch({ type: \"connected\" });\n // Kick off an opening greeting so the assistant speaks first instead of\n // waiting silently for the user. The minted session already carries the\n // full instructions (persona, governance, language, \"first turn\" rule),\n // so a bare `response.create` is enough \u2014 do NOT attach response-level\n // instructions here, which would replace the session instructions.\n // Skip it when the app's voice intro is \"none\" (`autoGreet === false`);\n // a missing flag defaults to greeting for back-compat.\n if (mint.autoGreet !== false) {\n this.dispatch({ type: \"response_requested\" });\n this.sendOverDataChannel({ type: \"response.create\" });\n }\n };\n dataChannel.onclose = () => {\n logger.debug(\"Voice: data channel closed\");\n };\n\n this.resources = {\n pc,\n dataChannel,\n micStream,\n audioElement,\n voiceSessionId: mint.voiceSessionId,\n };\n\n try {\n logger.debug(\"Voice: creating WebRTC offer\");\n const offer = await pc.createOffer();\n await pc.setLocalDescription(offer);\n logger.debug(\"Voice: exchanging SDP with OpenAI Realtime\");\n const answerSdp = await this.exchangeSdp(offer, mint.clientSecret);\n await pc.setRemoteDescription({ type: \"answer\", sdp: answerSdp });\n logger.debug(\"Voice: WebRTC negotiation complete\");\n } catch (err) {\n await this.failWith(\n err instanceof Error ? err.message : \"Failed to negotiate voice connection\"\n );\n return;\n }\n\n void this.postSessionEvent(\"start\", mint.voiceSessionId, pageContext);\n }\n\n /** Stop the session and tear down all resources. */\n public async stop(): Promise<void> {\n logger.debug(\"Voice: stop() called\");\n this.dispatch({ type: \"stop\" });\n await this.teardown();\n }\n\n /** Tear down everything and remove listeners. Call once before discarding the instance. */\n public destroy(): void {\n void this.teardown();\n if (this.pageHideHandler) {\n window.removeEventListener(\"pagehide\", this.pageHideHandler);\n this.pageHideHandler = null;\n }\n this.listeners.clear();\n }\n\n // \u2500\u2500 Internals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private buildMessageContext(): RealtimeMessageContext {\n return {\n send: (event) => this.dispatch(event),\n sendData: (payload) => this.sendOverDataChannel(payload),\n dispatchToolCall: (name, args) => this.routeToolCall(name, args),\n isDispatched: (id) => this.dispatchedCallIds.has(id),\n markDispatched: (id) => {\n this.dispatchedCallIds.add(id);\n },\n recordUsage: (usage) => this.accumulateUsage(usage),\n };\n }\n\n private accumulateUsage(usage: RealtimeResponseUsage): void {\n this.usage.responseCount += 1;\n if (typeof usage.inputTokens === \"number\") this.usage.inputTokens += usage.inputTokens;\n if (typeof usage.cachedInputTokens === \"number\") {\n this.usage.cachedInputTokens += usage.cachedInputTokens;\n }\n if (typeof usage.outputTokens === \"number\") this.usage.outputTokens += usage.outputTokens;\n if (typeof usage.audioInputTokens === \"number\") {\n this.usage.audioInputTokens += usage.audioInputTokens;\n }\n if (typeof usage.audioOutputTokens === \"number\") {\n this.usage.audioOutputTokens += usage.audioOutputTokens;\n }\n if (typeof usage.textInputTokens === \"number\") {\n this.usage.textInputTokens += usage.textInputTokens;\n }\n if (typeof usage.textOutputTokens === \"number\") {\n this.usage.textOutputTokens += usage.textOutputTokens;\n }\n }\n\n private sendOverDataChannel(payload: unknown): void {\n const channel = this.resources.dataChannel;\n if (!channel || channel.readyState !== \"open\") {\n logger.warn(\"Voice data channel not ready; dropping payload\");\n return;\n }\n try {\n const serialized = JSON.stringify(payload);\n logger.debug(\"Voice: \u2192 data channel send\", serialized.slice(0, 200));\n channel.send(serialized);\n } catch (err) {\n logger.warn(\"Failed to send on voice data channel\", err);\n }\n }\n\n private async routeToolCall(idOrName: string, args: unknown): Promise<unknown> {\n // The model calls us back using the decorated id (e.g. orders_list).\n // Resolve it to the original host tool name; fall back to the raw value\n // (it might be `redirect` or some non-decorated name).\n const name = this.toolNameById.get(idOrName) ?? idOrName;\n logger.debug(\"Voice: tool call dispatched\", { id: idOrName, name, args });\n // MCP tools execute server-side (the org token never reaches the browser).\n // The model calls back with the `mcp__\u2026` name minted by the server; relay\n // it to the exec endpoint and feed the result back over the data channel.\n if (name.startsWith(\"mcp__\")) {\n return await this.execMcpTool(name, args);\n }\n if (name === \"redirect\") {\n const path = (args as { path?: unknown })?.path;\n if (typeof path !== \"string\") {\n throw new Error(\"redirect tool requires a string `path` argument\");\n }\n if (this.config.onRedirect) {\n this.config.onRedirect(path);\n } else if (typeof window !== \"undefined\") {\n window.location.assign(path);\n }\n return { success: true, redirected: true, path };\n }\n if (this.config.onToolCall) {\n return await this.config.onToolCall(name, args);\n }\n throw new Error(`No handler configured for tool: ${name}`);\n }\n\n /**\n * Relay an MCP tool call to the server, which holds the org's credentials\n * and executes against the remote MCP server. The browser only ever passes\n * through the tool name, args, and the opaque result.\n */\n private async execMcpTool(toolName: string, args: unknown): Promise<unknown> {\n try {\n const res = await fetch(`${this.apiOrigin}/api/voice/mcp-exec`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n appId: this.config.appId,\n toolName,\n args: args ?? {},\n pageContext: this.safeExtractPageContext(),\n }),\n });\n if (!res.ok) {\n const body = (await res.json().catch(() => ({}))) as { error?: string };\n return { error: body.error ?? `MCP tool failed (${res.status})` };\n }\n const body = (await res.json()) as { result?: unknown };\n return body.result ?? {};\n } catch (err) {\n // The relay failed, but we hand a structured `{ error }` straight back to\n // the model to recover from \u2014 expected data flow, not a console-worthy\n // fault. Log at debug to match the text-chat tool-call path (client.ts).\n logger.debug(\"Voice: MCP tool relay failed\", err);\n return { error: \"The integration could not complete this request.\" };\n }\n }\n\n private async mintToken(\n chatConfig: ChatConfig | undefined,\n decoratedManifest: { tools: Array<ToolDefinition & { id: string }> },\n pageContext: PageContext | undefined\n ): Promise<MintResponse> {\n const res = await fetch(`${this.apiOrigin}/api/voice/realtime-token`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n appId: this.config.appId,\n pageContext,\n toolManifest: decoratedManifest,\n routeManifest: chatConfig?.routes,\n }),\n });\n if (!res.ok) {\n const body = (await res.json().catch(() => ({}))) as { error?: string };\n throw new Error(body.error ?? `Mint failed (${res.status})`);\n }\n return (await res.json()) as MintResponse;\n }\n\n /**\n * Decorate the host's tool manifest with readable, collision-free model-facing ids\n * and populate `this.toolNameById` for reverse lookup. Mirrors the decoration the\n * chat-ui iframe applies before sending tools to `/api/chat`. GraphQL/REST tools are\n * ordinary manifest entries here (contributed by their adapters), so no special-casing\n * is needed.\n */\n private buildDecoratedManifest(chatConfig: ChatConfig | undefined): {\n tools: Array<ToolDefinition & { id: string }>;\n } {\n this.toolNameById.clear();\n const used = new Set<string>([\"redirect\"]);\n\n const decoratedHostTools = (chatConfig?.tools?.tools ?? []).map((t: ToolDefinition) => {\n const id = uniqueToolId(t.name, used);\n used.add(id);\n this.toolNameById.set(id, t.name);\n return { ...t, id };\n });\n\n return { tools: decoratedHostTools };\n }\n\n private async exchangeSdp(\n offer: RTCSessionDescriptionInit,\n clientSecret: string\n ): Promise<string> {\n const sdpResponse = await fetch(`${REALTIME_CALLS_URL}?model=${DEFAULT_REALTIME_MODEL}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${clientSecret}`,\n \"Content-Type\": \"application/sdp\",\n },\n body: offer.sdp,\n });\n if (!sdpResponse.ok) {\n const body = await sdpResponse.text().catch(() => \"\");\n throw new Error(`SDP exchange failed (${sdpResponse.status}): ${body}`);\n }\n return await sdpResponse.text();\n }\n\n private buildStopEventBody(voiceSessionId: string, pageContext: PageContext | undefined) {\n return {\n appId: this.config.appId,\n voiceSessionId,\n event: \"stop\" as const,\n clientTimestamp: Date.now(),\n pageContext,\n usage: { ...this.usage },\n };\n }\n\n private async postSessionEvent(\n event: \"start\" | \"stop\",\n voiceSessionId: string,\n pageContext: PageContext | undefined\n ): Promise<void> {\n try {\n const body =\n event === \"stop\"\n ? this.buildStopEventBody(voiceSessionId, pageContext)\n : {\n appId: this.config.appId,\n voiceSessionId,\n event,\n clientTimestamp: Date.now(),\n pageContext,\n };\n await fetch(`${this.apiOrigin}/api/voice/session-event`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n keepalive: event === \"stop\",\n });\n } catch (err) {\n logger.warn(`Failed to post voice.session.${event}`, err);\n }\n }\n\n private async teardown(): Promise<void> {\n const r = this.resources;\n this.resources = EMPTY_RESOURCES;\n this.dispatchedCallIds = new Set();\n\n try {\n r.dataChannel?.close();\n } catch (err) {\n logger.warn(\"Error closing data channel\", err);\n }\n try {\n for (const track of r.micStream?.getTracks() ?? []) {\n track.stop();\n }\n } catch (err) {\n logger.warn(\"Error stopping mic tracks\", err);\n }\n try {\n for (const sender of r.pc?.getSenders() ?? []) {\n sender.track?.stop();\n }\n r.pc?.close();\n } catch (err) {\n logger.warn(\"Error closing peer connection\", err);\n }\n if (r.audioElement) {\n try {\n r.audioElement.srcObject = null;\n r.audioElement.remove();\n } catch (err) {\n logger.warn(\"Error removing audio element\", err);\n }\n }\n\n if (r.voiceSessionId) {\n await this.postSessionEvent(\"stop\", r.voiceSessionId, this.safeExtractPageContext());\n }\n }\n\n private async failWith(message: string): Promise<void> {\n logger.warn(\"Voice session error:\", message);\n this.dispatch({ type: \"error\", message });\n await this.teardown();\n }\n\n private dispatch(event: Parameters<typeof voiceReducer>[1]): void {\n const next = voiceReducer(this.machine, event);\n if (next === this.machine) return;\n this.machine = next;\n for (const listener of this.listeners) {\n try {\n listener(next);\n } catch (err) {\n logger.warn(\"Voice state listener threw\", err);\n }\n }\n }\n\n private safeExtractPageContext(): PageContext | undefined {\n try {\n return extractPageContext();\n } catch {\n return undefined;\n }\n }\n\n private attachPageHide(): void {\n if (typeof window === \"undefined\") return;\n this.pageHideHandler = () => {\n const r = this.resources;\n if (!r.voiceSessionId) return;\n const body = JSON.stringify(this.buildStopEventBody(r.voiceSessionId, undefined));\n if (navigator.sendBeacon) {\n navigator.sendBeacon(\n `${this.apiOrigin}/api/voice/session-event`,\n new Blob([body], { type: \"application/json\" })\n );\n }\n };\n window.addEventListener(\"pagehide\", this.pageHideHandler);\n }\n}\n", "import { YakClient, type YakClientConfig } from \"./client.js\";\nimport { logger } from \"./logger.js\";\nimport type { ChatConfigProvider } from \"./types/config.js\";\nimport type { IframeMessageFromHost, WidgetPosition } from \"./types/messaging.js\";\nimport { INITIAL_VOICE_MACHINE, type VoiceMachine, type VoiceState } from \"./voice-machine.js\";\nimport {\n type VoiceStateListener,\n YakVoiceSession,\n type YakVoiceSessionConfig,\n} from \"./voice-session.js\";\n\n// \u2500\u2500 Widget mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Which experiences the widget exposes:\n * - `chat` \u2014 chat icon only (opens the chat iframe panel). The default.\n * - `voice` \u2014 voice icon only (starts a WebRTC voice session).\n * - `both` \u2014 both icons, sharing one trigger pill.\n */\nexport type WidgetMode = \"chat\" | \"voice\" | \"both\";\n\n// Single source of truth for the default trigger + panel corner.\nconst DEFAULT_POSITION: WidgetPosition = \"bottom-left\";\n\n// \u2500\u2500 Trigger button configuration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type TriggerButtonConfig = {\n /** Custom color overrides for light mode */\n lightButton?: { background?: string; color?: string; border?: string };\n /** Custom color overrides for dark mode */\n darkButton?: { background?: string; color?: string; border?: string };\n};\n\n// \u2500\u2500 YakEmbed configuration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type YakEmbedConfig = YakClientConfig & {\n /** DOM element to append the widget into. Defaults to document.body. */\n target?: HTMLElement;\n /** Show the floating trigger button. Default: true */\n trigger?: boolean | TriggerButtonConfig;\n /**\n * Which experiences this widget exposes.\n * \"chat\" \u2014 chat icon only (opens the chat iframe).\n * \"voice\" \u2014 voice icon only (starts a WebRTC voice session).\n * \"both\" \u2014 both icons in one pill.\n * Default: \"chat\".\n */\n mode?: WidgetMode;\n /**\n * Async provider for chat config (routes + tools). Used by the voice\n * session on every `start()` and by the iframe via postMessage. Takes\n * precedence over the static `chatConfig` on `YakClientConfig`.\n */\n getConfig?: ChatConfigProvider;\n};\n\n// \u2500\u2500 State listener \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type YakEmbedState = {\n /** Whether the chat panel is open. */\n isOpen: boolean;\n /** Whether the chat iframe has handshaked and can receive messages. */\n isReady: boolean;\n /**\n * Whether the chat is opening but not yet interactive \u2014 `isOpen && !isReady`.\n * Stays true from the moment the panel opens until the iframe reports ready,\n * so custom triggers can show a spinner without re-deriving it.\n */\n isLoading: boolean;\n /** Whether the panel is expanded to (near) full-screen. */\n isExpanded: boolean;\n};\n\nexport type YakEmbedStateListener = (state: YakEmbedState) => void;\n\n// \u2500\u2500 Inline SVG icons (lucide) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst MESSAGE_CIRCLE_SVG = `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M7.9 20A9 9 0 1 0 4 16.1L2 22Z\"/></svg>`;\n\nconst AUDIO_LINES_SVG = `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M2 10v3\"/><path d=\"M6 6v11\"/><path d=\"M10 3v18\"/><path d=\"M14 8v7\"/><path d=\"M18 5v13\"/><path d=\"M22 10v3\"/></svg>`;\n\nconst STOP_SVG = `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"2\"/></svg>`;\n\nconst VOICE_STATE_ARIA: Record<VoiceState, string> = {\n idle: \"Start voice mode\",\n connecting: \"Connecting voice session\",\n listening: \"Voice listening \u2014 tap to stop\",\n thinking: \"Voice thinking \u2014 tap to stop\",\n speaking: \"Voice speaking \u2014 tap to stop\",\n error: \"Voice error \u2014 tap to retry\",\n};\n\n// \u2500\u2500 CSS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction getPanelStyles(): string {\n return `\n .yak-panel-root {\n position: fixed;\n top: 0; left: 0; right: 0; bottom: 0;\n width: 100vw; height: 100vh;\n pointer-events: none;\n z-index: 9998;\n }\n\n .yak-panel-container {\n position: absolute;\n width: 500px; height: 600px;\n max-width: calc(100vw - 40px);\n max-height: calc(100vh - 120px);\n border-radius: 15px;\n overflow: hidden;\n background-color: transparent;\n pointer-events: auto;\n }\n\n .yak-panel-container[data-position=\"top-left\"]:not(.yak-panel-drawer) { top: 16px; left: 16px; }\n .yak-panel-container[data-position=\"top-center\"]:not(.yak-panel-drawer) { top: 16px; left: 50%; transform: translateX(-50%); }\n .yak-panel-container[data-position=\"top-right\"]:not(.yak-panel-drawer) { top: 16px; right: 16px; }\n .yak-panel-container[data-position=\"left-center\"]:not(.yak-panel-drawer) { top: 50%; left: 16px; transform: translateY(-50%); }\n .yak-panel-container[data-position=\"right-center\"]:not(.yak-panel-drawer) { top: 50%; right: 16px; transform: translateY(-50%); }\n .yak-panel-container[data-position=\"bottom-left\"]:not(.yak-panel-drawer) { bottom: 16px; left: 16px; }\n .yak-panel-container[data-position=\"bottom-center\"]:not(.yak-panel-drawer) { bottom: 16px; left: 50%; transform: translateX(-50%); }\n .yak-panel-container[data-position=\"bottom-right\"]:not(.yak-panel-drawer) { bottom: 16px; right: 16px; }\n\n .yak-panel-container:not(.yak-panel-drawer) { display: none; }\n .yak-panel-container:not(.yak-panel-drawer)[data-open=\"true\"] { display: block; }\n\n .yak-panel-container[data-expanded=\"true\"] {\n width: calc(100vw - 32px) !important;\n height: calc(100vh - 32px) !important;\n max-width: none !important; max-height: none !important;\n top: 16px !important; left: 16px !important; right: 16px !important; bottom: 16px !important;\n border-radius: 15px !important;\n border: 1px solid rgba(0, 0, 0, 0.1) !important;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15) !important;\n }\n @media (prefers-color-scheme: dark) {\n .yak-panel-container[data-expanded=\"true\"]:not(.yak-panel-light) { border-color: rgba(255,255,255,0.1) !important; }\n }\n .yak-panel-container.yak-panel-dark[data-expanded=\"true\"] { border-color: rgba(255,255,255,0.1) !important; }\n .yak-panel-container.yak-panel-light[data-expanded=\"true\"] { border-color: rgba(0,0,0,0.1) !important; }\n\n .yak-panel-container.yak-panel-drawer {\n height: calc(100% - 32px); max-width: 100vw; max-height: none;\n border-radius: 15px;\n transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);\n }\n .yak-panel-container.yak-panel-drawer[data-position=\"left-center\"],\n .yak-panel-container.yak-panel-drawer[data-position=\"top-left\"],\n .yak-panel-container.yak-panel-drawer[data-position=\"bottom-left\"] {\n top: 16px; left: 16px; bottom: 16px;\n transform: translateX(calc(-100% - 16px));\n }\n .yak-panel-container.yak-panel-drawer[data-position=\"right-center\"],\n .yak-panel-container.yak-panel-drawer[data-position=\"top-right\"],\n .yak-panel-container.yak-panel-drawer[data-position=\"bottom-right\"] {\n top: 16px; right: 16px; bottom: 16px;\n transform: translateX(calc(100% + 16px));\n }\n .yak-panel-container.yak-panel-drawer[data-position=\"top-center\"],\n .yak-panel-container.yak-panel-drawer[data-position=\"bottom-center\"] {\n top: 16px; right: 16px; bottom: 16px;\n transform: translateX(calc(100% + 16px));\n }\n .yak-panel-container.yak-panel-drawer[data-open=\"true\"] { transform: translateX(0); }\n\n .yak-panel-iframe {\n position: absolute; inset: 0;\n width: 100%; height: 100%; border: none;\n }\n\n @media (max-width: 640px) {\n .yak-panel-container:not(.yak-panel-drawer) {\n width: 100% !important; height: 100% !important; height: 100dvh !important;\n max-width: none !important; max-height: none !important;\n top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important;\n border-radius: 0 !important;\n }\n .yak-panel-container.yak-panel-drawer { width: 100% !important; max-width: none !important; }\n }\n `;\n}\n\nfunction getTriggerStyles(): string {\n return `\n .yak-widget-trigger {\n position: fixed; z-index: 9997;\n display: inline-flex; align-items: center; gap: 8px;\n border: none; border-radius: 30px;\n padding: 5px; height: 45px;\n transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);\n background-color: #000; color: #fff;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n font-family: system-ui, -apple-system, sans-serif;\n }\n\n .yak-widget-trigger[data-position=\"top-left\"] { top: 28px; left: 28px; }\n .yak-widget-trigger[data-position=\"top-center\"] { top: 28px; left: 50%; transform: translateX(-50%); }\n .yak-widget-trigger[data-position=\"top-right\"] { top: 28px; right: 28px; }\n .yak-widget-trigger[data-position=\"left-center\"] { top: 50%; left: 28px; transform: translateY(-50%); }\n .yak-widget-trigger[data-position=\"right-center\"] { top: 50%; right: 28px; transform: translateY(-50%); }\n .yak-widget-trigger[data-position=\"bottom-left\"] { bottom: 28px; left: 28px; }\n .yak-widget-trigger[data-position=\"bottom-center\"] { bottom: 28px; left: 50%; transform: translateX(-50%); }\n .yak-widget-trigger[data-position=\"bottom-right\"] { bottom: 28px; right: 28px; }\n\n .yak-widget-icon-bg {\n display: flex; align-items: center; justify-content: center;\n width: 36px; height: 36px; border-radius: 50%;\n background-color: rgba(255, 255, 255, 0.1);\n flex-shrink: 0;\n }\n\n .yak-widget-icon { width: 24px; height: 24px; color: currentColor; }\n\n .yak-widget-trigger-icon-btn {\n display: inline-flex; align-items: center; justify-content: center;\n width: 36px; height: 36px; border-radius: 50%;\n border: none; padding: 0;\n background-color: transparent;\n color: inherit;\n cursor: pointer;\n position: relative;\n transition: background-color 0.15s ease;\n flex-shrink: 0;\n }\n .yak-widget-trigger-icon-btn:hover { background-color: rgba(255, 255, 255, 0.12); }\n .yak-widget-trigger-icon-btn:disabled { cursor: wait; opacity: 0.7; }\n .yak-widget-trigger-icon-btn svg { width: 20px; height: 20px; display: block; }\n\n .yak-widget-trigger-icon-btn[data-action=\"voice\"][data-state=\"error\"] {\n background-color: rgba(185, 28, 28, 0.18);\n }\n .yak-widget-trigger-icon-btn[data-action=\"voice\"][data-state=\"listening\"]::after,\n .yak-widget-trigger-icon-btn[data-action=\"voice\"][data-state=\"speaking\"]::after {\n content: \"\";\n position: absolute; inset: 2px;\n border-radius: 50%;\n border: 2px solid currentColor;\n pointer-events: none;\n }\n .yak-widget-trigger-icon-btn[data-action=\"voice\"][data-state=\"listening\"]::after {\n opacity: 0.4; animation: yak-widget-pulse 1.2s ease-out infinite;\n }\n .yak-widget-trigger-icon-btn[data-action=\"voice\"][data-state=\"speaking\"]::after {\n opacity: 0.5; animation: yak-widget-wave 0.8s ease-in-out infinite;\n }\n\n @media (prefers-color-scheme: dark) {\n .yak-widget-trigger:not(.yak-widget-light) .yak-widget-icon { filter: invert(1); }\n .yak-widget-trigger:not(.yak-widget-light) .yak-widget-trigger-icon-btn:hover {\n background-color: rgba(255, 255, 255, 0.12);\n }\n }\n .yak-widget-trigger.yak-widget-dark .yak-widget-icon { filter: invert(1); }\n .yak-widget-trigger.yak-widget-light .yak-widget-icon { filter: none; }\n .yak-widget-trigger.yak-widget-light .yak-widget-trigger-icon-btn:hover {\n background-color: rgba(0, 0, 0, 0.06);\n }\n\n .yak-widget-spinner {\n width: 20px; height: 20px;\n border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%;\n animation: yak-widget-spin 0.8s linear infinite;\n }\n @keyframes yak-widget-spin { to { transform: rotate(360deg); } }\n @keyframes yak-widget-pulse {\n 0% { transform: scale(1); opacity: 0.5; }\n 100% { transform: scale(1.45); opacity: 0; }\n }\n @keyframes yak-widget-wave {\n 0%, 100% { transform: scale(1); opacity: 0.5; }\n 50% { transform: scale(1.25); opacity: 0.9; }\n }\n\n .yak-widget-trigger.yak-widget-custom-light {\n background-color: var(--yak-btn-light-bg, #fff); color: var(--yak-btn-light-color, #000);\n border: 1px solid var(--yak-btn-light-border, transparent);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n }\n .yak-widget-trigger.yak-widget-custom-dark {\n background-color: var(--yak-btn-dark-bg, #000); color: var(--yak-btn-dark-color, #fff);\n border: 1px solid var(--yak-btn-dark-border, transparent);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n }\n\n @media (prefers-color-scheme: light) {\n .yak-widget-trigger[data-has-light-custom]:not(.yak-widget-dark) {\n background-color: var(--yak-btn-light-bg, #fff); color: var(--yak-btn-light-color, #000);\n border: 1px solid var(--yak-btn-light-border, transparent);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n }\n }\n @media (prefers-color-scheme: dark) {\n .yak-widget-trigger[data-has-dark-custom]:not(.yak-widget-light) {\n background-color: var(--yak-btn-dark-bg, #000); color: var(--yak-btn-dark-color, #fff);\n border: 1px solid var(--yak-btn-dark-border, transparent);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n }\n }\n\n @media (prefers-color-scheme: light) {\n .yak-widget-trigger:not(.yak-widget-dark):not([data-has-light-custom]) {\n background-color: #fff; color: #000;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); border: 1px solid #e5e5e5;\n }\n .yak-widget-trigger:not(.yak-widget-dark):not([data-has-light-custom]) .yak-widget-icon-bg {\n background-color: rgba(0, 0, 0, 0.05);\n }\n }\n\n @media (prefers-color-scheme: dark) {\n .yak-widget-trigger:not(.yak-widget-light):not([data-has-dark-custom]) {\n background-color: #000; color: #fff;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); border: none;\n }\n .yak-widget-trigger:not(.yak-widget-light):not([data-has-dark-custom]) .yak-widget-icon-bg {\n background-color: rgba(255, 255, 255, 0.1);\n }\n }\n\n .yak-widget-trigger.yak-widget-light:not(.yak-widget-custom-light) {\n background-color: #fff; color: #000;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); border: 1px solid #e5e5e5;\n }\n .yak-widget-trigger.yak-widget-light:not(.yak-widget-custom-light) .yak-widget-icon-bg {\n background-color: rgba(0, 0, 0, 0.05);\n }\n\n .yak-widget-trigger.yak-widget-dark:not(.yak-widget-custom-dark) {\n background-color: #000; color: #fff;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); border: none;\n }\n .yak-widget-trigger.yak-widget-dark:not(.yak-widget-custom-dark) .yak-widget-icon-bg {\n background-color: rgba(255, 255, 255, 0.1);\n }\n `;\n}\n\n// \u2500\u2500 YakEmbed class \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Drop-in widget that renders the yak trigger pill plus, depending on mode,\n * the chat iframe panel and/or a WebRTC voice session. Composes both\n * `YakClient` (chat) and `YakVoiceSession` (voice) under one trigger.\n *\n * @example\n * ```ts\n * const embed = new YakEmbed({\n * appId: \"my-app\",\n * mode: \"both\",\n * theme: { position: \"bottom-left\" },\n * onToolCall: async (name, args) => { ... },\n * });\n * embed.mount();\n * ```\n */\nexport class YakEmbed {\n private readonly client: YakClient;\n private readonly voice: YakVoiceSession | null;\n private readonly config: YakEmbedConfig;\n private readonly mode: WidgetMode;\n\n // DOM elements\n private styleEl: HTMLStyleElement | null = null;\n private panelRoot: HTMLDivElement | null = null;\n private container: HTMLDivElement | null = null;\n private iframe: HTMLIFrameElement | null = null;\n private triggerEl: HTMLDivElement | null = null;\n private chatButton: HTMLButtonElement | null = null;\n private voiceButton: HTMLButtonElement | null = null;\n\n // State\n private isOpen = false;\n private isReady = false;\n private isExpanded = false;\n private hasBeenOpened = false;\n private pendingPrompt: string | null = null;\n private mounted = false;\n private voiceMachine: VoiceMachine = INITIAL_VOICE_MACHINE;\n\n // Listeners\n private stateListeners = new Set<YakEmbedStateListener>();\n private voiceListeners = new Set<VoiceStateListener>();\n private unsubscribeVoice: (() => void) | null = null;\n private mobileQuery: MediaQueryList | null = null;\n private mobileHandler: ((e: MediaQueryListEvent) => void) | null = null;\n private expandHandler: ((e: MessageEvent) => void) | null = null;\n\n constructor(config: YakEmbedConfig) {\n this.config = config;\n this.mode = config.mode ?? \"chat\";\n\n // Wrap callbacks to integrate with our state\n this.client = new YakClient({\n ...config,\n onReady: () => {\n this.isReady = true;\n this.updatePanelState();\n this.updateChatButtonState();\n this.sendPendingPrompt();\n this.sendFocusIfOpen();\n this.notifyMobileState();\n // Surface the ready transition to framework subscribers (React/Vue/\n // Svelte/Angular all derive their loading state from this). Without\n // it `isReady` stays false forever and consumers spin indefinitely.\n this.notifyListeners();\n config.onReady?.();\n },\n onClose: () => {\n this.close();\n config.onClose?.();\n },\n });\n\n if (this.mode !== \"chat\") {\n const voiceConfig: YakVoiceSessionConfig = {\n appId: config.appId,\n // A single `origin` on the embed drives both surfaces: chat (iframe)\n // and voice (mint endpoint).\n apiOrigin: config.origin,\n getConfig: config.getConfig,\n chatConfig: config.chatConfig,\n onToolCall: config.onToolCall,\n onRedirect: config.onRedirect,\n };\n this.voice = new YakVoiceSession(voiceConfig);\n } else {\n this.voice = null;\n }\n }\n\n /** The underlying headless YakClient for advanced usage */\n public getClient(): YakClient {\n return this.client;\n }\n\n /** The underlying voice session \u2014 null when mode === \"chat\". */\n public getVoiceSession(): YakVoiceSession | null {\n return this.voice;\n }\n\n /** Current widget mode (immutable for the lifetime of the embed). */\n public getMode(): WidgetMode {\n return this.mode;\n }\n\n // \u2500\u2500 Lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Mount the widget into the DOM. Call once after construction.\n * Inserts styles and trigger button (if enabled). The chat iframe is\n * lazily created on the first call to open().\n */\n public mount(target?: HTMLElement): void {\n if (this.mounted) return;\n this.mounted = true;\n\n const parent = target ?? this.config.target ?? document.body;\n\n // Inject styles\n this.styleEl = document.createElement(\"style\");\n this.styleEl.textContent = getPanelStyles() + getTriggerStyles();\n parent.appendChild(this.styleEl);\n\n // Create trigger\n if (this.config.trigger !== false) {\n this.createTrigger(parent);\n }\n\n // Listen for expansion messages from iframe\n this.expandHandler = (event: MessageEvent) => {\n if (event.data?.type === \"YAK_SET_EXPANDED\") {\n this.isExpanded = Boolean(event.data.expanded);\n this.updatePanelState();\n this.notifyListeners();\n }\n };\n window.addEventListener(\"message\", this.expandHandler);\n\n // Start the client's message listeners\n this.client.mount();\n\n // Subscribe to voice state for trigger updates + fan-out to listeners\n if (this.voice) {\n this.voiceMachine = this.voice.getState();\n this.unsubscribeVoice = this.voice.onStateChange((machine) => {\n this.voiceMachine = machine;\n this.updateVoiceButtonState();\n for (const listener of this.voiceListeners) {\n try {\n listener(machine);\n } catch (err) {\n logger.warn(\"Error in voice listener:\", err);\n }\n }\n });\n this.updateVoiceButtonState();\n }\n }\n\n /** Remove all DOM elements and event listeners. */\n public destroy(): void {\n if (!this.mounted) return;\n this.mounted = false;\n\n this.client.unmount();\n // Drop references to the now-detached iframe window so a remount (e.g.\n // React StrictMode's mount\u2192unmount\u2192mount) doesn't leave the client\n // pointing at a dead window. Clears both `iframeWindow` and `readyTarget`.\n this.client.setIframeWindow(null);\n\n if (this.unsubscribeVoice) {\n this.unsubscribeVoice();\n this.unsubscribeVoice = null;\n }\n this.voice?.destroy();\n\n if (this.expandHandler) {\n window.removeEventListener(\"message\", this.expandHandler);\n this.expandHandler = null;\n }\n\n if (this.mobileQuery && this.mobileHandler) {\n this.mobileQuery.removeEventListener(\"change\", this.mobileHandler);\n this.mobileQuery = null;\n this.mobileHandler = null;\n }\n\n this.panelRoot?.remove();\n this.triggerEl?.remove();\n this.styleEl?.remove();\n\n this.panelRoot = null;\n this.container = null;\n this.iframe = null;\n this.triggerEl = null;\n this.chatButton = null;\n this.voiceButton = null;\n this.styleEl = null;\n this.isOpen = false;\n this.isReady = false;\n this.isExpanded = false;\n this.hasBeenOpened = false;\n\n this.stateListeners.clear();\n this.voiceListeners.clear();\n }\n\n // \u2500\u2500 Public chat API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Open the chat widget. Creates the iframe on first call (lazy mount). */\n public open(): void {\n if (!this.mounted) return;\n\n if (!this.hasBeenOpened) {\n this.hasBeenOpened = true;\n\n const parent = this.config.target ?? document.body;\n this.createPanel(parent);\n }\n\n this.isOpen = true;\n this.client.setWidgetOpen(true);\n this.updatePanelState();\n this.updateChatButtonState();\n this.sendFocusIfOpen();\n this.notifyListeners();\n }\n\n /** Close the chat widget. The iframe remains in the DOM for instant re-open. */\n public close(): void {\n this.isOpen = false;\n this.client.setWidgetOpen(false);\n this.updatePanelState();\n this.updateChatButtonState();\n this.notifyListeners();\n }\n\n /** Toggle the chat widget open/closed. */\n public toggle(): void {\n if (this.isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /** Open the chat and immediately send a prompt. */\n public openWithPrompt(prompt: string): void {\n this.pendingPrompt = prompt;\n this.open();\n this.sendPendingPrompt();\n }\n\n /** Get the current widget state. */\n public getState(): YakEmbedState {\n return {\n isOpen: this.isOpen,\n isReady: this.isReady,\n isLoading: this.isOpen && !this.isReady,\n isExpanded: this.isExpanded,\n };\n }\n\n /** Subscribe to state changes. Returns an unsubscribe function. */\n public onStateChange(listener: YakEmbedStateListener): () => void {\n this.stateListeners.add(listener);\n return () => {\n this.stateListeners.delete(listener);\n };\n }\n\n // \u2500\u2500 Public voice API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Start a voice session. Must be invoked from a user gesture. */\n public voiceStart(): Promise<void> {\n return this.voice ? this.voice.start() : Promise.resolve();\n }\n\n /** Stop the current voice session. */\n public voiceStop(): Promise<void> {\n return this.voice ? this.voice.stop() : Promise.resolve();\n }\n\n /** Toggle: start if idle/error, stop if active. */\n public async voiceToggle(): Promise<void> {\n if (!this.voice) return;\n const state = this.voice.getState().state;\n if (state === \"idle\" || state === \"error\") {\n await this.voice.start();\n } else if (state === \"listening\" || state === \"speaking\" || state === \"thinking\") {\n await this.voice.stop();\n }\n }\n\n /** Current voice machine snapshot. */\n public getVoiceState(): VoiceMachine {\n return this.voice ? this.voice.getState() : INITIAL_VOICE_MACHINE;\n }\n\n /** Subscribe to voice state changes. */\n public onVoiceStateChange(listener: VoiceStateListener): () => void {\n this.voiceListeners.add(listener);\n return () => {\n this.voiceListeners.delete(listener);\n };\n }\n\n // \u2500\u2500 DOM creation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private createPanel(parent: HTMLElement): void {\n const theme = this.config.theme;\n const position = theme?.position ?? DEFAULT_POSITION;\n const colorMode = theme?.colorMode;\n const displayMode = theme?.displayMode ?? \"chatbox\";\n const isDrawer = displayMode === \"drawer\";\n\n // Root overlay (pointer-events: none)\n this.panelRoot = document.createElement(\"div\");\n this.panelRoot.className = \"yak-panel-root\";\n\n // Container\n this.container = document.createElement(\"div\");\n const classes = [\"yak-panel-container\"];\n if (isDrawer) classes.push(\"yak-panel-drawer\");\n if (colorMode === \"light\") classes.push(\"yak-panel-light\");\n else if (colorMode === \"dark\") classes.push(\"yak-panel-dark\");\n this.container.className = classes.join(\" \");\n this.container.dataset.position = position;\n\n // Iframe \u2014 set `allow` and `title` BEFORE `src` (some browsers\n // persist the pre-load Permissions-Policy otherwise).\n this.iframe = document.createElement(\"iframe\");\n this.iframe.allow = \"clipboard-write\";\n this.iframe.title = \"yak-chat-host\";\n this.iframe.className = \"yak-panel-iframe\";\n this.iframe.src = this.client.getEmbedUrl();\n\n this.iframe.addEventListener(\"load\", () => {\n this.client.setIframeWindow(this.iframe?.contentWindow ?? null);\n });\n\n this.container.appendChild(this.iframe);\n this.panelRoot.appendChild(this.container);\n parent.appendChild(this.panelRoot);\n\n // Set up mobile detection\n this.mobileQuery = window.matchMedia(\"(max-width: 640px)\");\n this.mobileHandler = (e: MediaQueryListEvent) => {\n this.notifyIframeFullscreen(e.matches);\n };\n this.mobileQuery.addEventListener(\"change\", this.mobileHandler);\n }\n\n private createTrigger(parent: HTMLElement): void {\n const theme = this.config.theme;\n const position: WidgetPosition = theme?.position ?? DEFAULT_POSITION;\n const colorMode = theme?.colorMode;\n const triggerConfig = typeof this.config.trigger === \"object\" ? this.config.trigger : {};\n\n this.triggerEl = document.createElement(\"div\");\n this.triggerEl.dataset.position = position;\n this.triggerEl.dataset.mode = this.mode;\n this.triggerEl.className = this.buildTriggerClasses(colorMode, triggerConfig);\n this.applyTriggerCustomColors(triggerConfig);\n\n // Logo circle on the left\n const iconBg = document.createElement(\"div\");\n iconBg.className = \"yak-widget-icon-bg\";\n const logoImg = document.createElement(\"img\");\n logoImg.src = `${this.client.getIframeOrigin()}/logo.svg`;\n logoImg.alt = \"\";\n logoImg.width = 24;\n logoImg.height = 24;\n logoImg.className = \"yak-widget-icon\";\n iconBg.appendChild(logoImg);\n this.triggerEl.appendChild(iconBg);\n\n // Chat icon button\n if (this.mode === \"chat\" || this.mode === \"both\") {\n this.chatButton = document.createElement(\"button\");\n this.chatButton.type = \"button\";\n this.chatButton.className = \"yak-widget-trigger-icon-btn\";\n this.chatButton.dataset.action = \"chat\";\n this.chatButton.setAttribute(\"aria-label\", \"Open chat\");\n this.chatButton.innerHTML = MESSAGE_CIRCLE_SVG;\n this.chatButton.addEventListener(\"click\", () => this.open());\n this.triggerEl.appendChild(this.chatButton);\n }\n\n // Voice icon button\n if (this.mode === \"voice\" || this.mode === \"both\") {\n this.voiceButton = document.createElement(\"button\");\n this.voiceButton.type = \"button\";\n this.voiceButton.className = \"yak-widget-trigger-icon-btn\";\n this.voiceButton.dataset.action = \"voice\";\n this.voiceButton.dataset.state = \"idle\";\n this.voiceButton.setAttribute(\"aria-label\", VOICE_STATE_ARIA.idle);\n this.voiceButton.innerHTML = AUDIO_LINES_SVG;\n this.voiceButton.addEventListener(\"click\", () => {\n void this.voiceToggle();\n });\n this.triggerEl.appendChild(this.voiceButton);\n }\n\n parent.appendChild(this.triggerEl);\n }\n\n private buildTriggerClasses(\n colorMode: string | undefined,\n triggerConfig: TriggerButtonConfig\n ): string {\n const classes = [\"yak-widget-trigger\"];\n if (colorMode === \"light\") classes.push(\"yak-widget-light\");\n else if (colorMode === \"dark\") classes.push(\"yak-widget-dark\");\n\n const hasLightCustom =\n triggerConfig.lightButton?.background ||\n triggerConfig.lightButton?.color ||\n triggerConfig.lightButton?.border;\n const hasDarkCustom =\n triggerConfig.darkButton?.background ||\n triggerConfig.darkButton?.color ||\n triggerConfig.darkButton?.border;\n\n if (colorMode === \"light\" && hasLightCustom) classes.push(\"yak-widget-custom-light\");\n else if (colorMode === \"dark\" && hasDarkCustom) classes.push(\"yak-widget-custom-dark\");\n\n return classes.join(\" \");\n }\n\n private applyTriggerCustomColors(triggerConfig: TriggerButtonConfig): void {\n if (!this.triggerEl) return;\n const { lightButton, darkButton } = triggerConfig;\n\n const hasLightCustom = lightButton?.background || lightButton?.color || lightButton?.border;\n const hasDarkCustom = darkButton?.background || darkButton?.color || darkButton?.border;\n\n if (hasLightCustom || hasDarkCustom) {\n const vars: [string, string | undefined][] = [\n [\"--yak-btn-light-bg\", lightButton?.background],\n [\"--yak-btn-light-color\", lightButton?.color],\n [\"--yak-btn-light-border\", lightButton?.border],\n [\"--yak-btn-dark-bg\", darkButton?.background],\n [\"--yak-btn-dark-color\", darkButton?.color],\n [\"--yak-btn-dark-border\", darkButton?.border],\n ];\n for (const [prop, value] of vars) {\n if (value) this.triggerEl.style.setProperty(prop, value);\n }\n }\n\n if (hasLightCustom) this.triggerEl.dataset.hasLightCustom = \"true\";\n if (hasDarkCustom) this.triggerEl.dataset.hasDarkCustom = \"true\";\n }\n\n // \u2500\u2500 Internal state management \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private updatePanelState(): void {\n if (!this.container) return;\n this.container.dataset.open = String(this.isOpen && this.isReady);\n this.container.dataset.expanded = String(this.isExpanded);\n if (this.panelRoot) {\n this.panelRoot.dataset.expanded = String(this.isExpanded);\n }\n }\n\n private updateChatButtonState(): void {\n if (!this.chatButton) return;\n const isLoading = this.isOpen && !this.isReady;\n this.chatButton.disabled = isLoading;\n this.chatButton.setAttribute(\"aria-label\", isLoading ? \"Loading chat\" : \"Open chat\");\n\n if (isLoading) {\n this.chatButton.innerHTML = `<span class=\"yak-widget-spinner\" aria-hidden=\"true\"></span>`;\n } else {\n this.chatButton.innerHTML = MESSAGE_CIRCLE_SVG;\n }\n }\n\n private updateVoiceButtonState(): void {\n if (!this.voiceButton) return;\n const state = this.voiceMachine.state;\n this.voiceButton.dataset.state = state;\n this.voiceButton.setAttribute(\"aria-label\", VOICE_STATE_ARIA[state]);\n this.voiceButton.disabled = state === \"connecting\";\n this.voiceButton.innerHTML = this.iconForVoiceState(state);\n }\n\n private iconForVoiceState(state: VoiceState): string {\n if (state === \"connecting\") {\n return `<span class=\"yak-widget-spinner\" aria-hidden=\"true\"></span>`;\n }\n if (state === \"listening\" || state === \"speaking\" || state === \"thinking\") {\n return STOP_SVG;\n }\n return AUDIO_LINES_SVG;\n }\n\n private sendPendingPrompt(): void {\n if (!this.pendingPrompt || !this.isReady) return;\n logger.debug(\"Sending pending prompt:\", this.pendingPrompt);\n this.client.sendPrompt(this.pendingPrompt);\n this.pendingPrompt = null;\n }\n\n private sendFocusIfOpen(): void {\n if (this.isOpen && this.isReady) {\n this.client.sendFocus();\n }\n }\n\n private notifyMobileState(): void {\n if (this.mobileQuery) {\n this.notifyIframeFullscreen(this.mobileQuery.matches);\n }\n }\n\n private notifyIframeFullscreen(isFullscreen: boolean): void {\n if (!this.iframe?.contentWindow) return;\n const msg: IframeMessageFromHost = {\n type: \"yak:viewport\",\n payload: { fullscreen: isFullscreen },\n };\n this.iframe.contentWindow.postMessage(msg, this.client.getIframeOrigin());\n }\n\n private notifyListeners(): void {\n const state = this.getState();\n for (const listener of this.stateListeners) {\n try {\n listener(state);\n } catch (err) {\n logger.warn(\"Error in state listener:\", err);\n }\n }\n }\n}\n", "import { logger } from \"./logger.js\";\nimport type { ChatConfig } from \"./types/config.js\";\nimport type {\n ToolAdapter,\n ToolCallHandler,\n ToolDefinition,\n ToolManifest,\n YakToolset,\n} from \"./types/tools.js\";\n\ntype ResolvedAdapter = {\n adapter: ToolAdapter;\n tools: ToolDefinition[];\n};\n\n/**\n * Compose client-side {@link ToolAdapter}s into ONE merged tool manifest and ONE\n * routed `onToolCall`. This is the single injection point for \"available tools\"\n * into the iframe: every adapter \u2014 client-side (GraphQL/REST/custom, run by your own\n * `execute` callback) or server-relayed ({@link createYakServerAdapter}) \u2014 contributes\n * its tools to the merged manifest and its executor to the routed handler. Because every\n * tool now flows through `onToolCall`, they all surface through `onToolCallComplete` /\n * `useYakToolEvent`.\n *\n * @example\n * ```tsx\n * const toolset = createYakToolset([\n * createYakServerAdapter({ endpoint: \"/api/yak\" }), // tRPC + custom server tools\n * createGraphQLToolAdapter({ name: \"shop\", schema, execute: runShopQuery }),\n * createRESTToolAdapter({ name: \"billing\", spec, execute: callBillingApi }),\n * ]);\n *\n * <YakProvider\n * getConfig={async () => ({ routes, ...(await toolset.getConfig()) })}\n * onToolCall={toolset.onToolCall}\n * />;\n * ```\n */\nexport function createYakToolset(adapters: ToolAdapter[]): YakToolset {\n let cache: ResolvedAdapter[] | null = null;\n\n async function resolve(force = false): Promise<ResolvedAdapter[]> {\n if (cache && !force) return cache;\n cache = await Promise.all(\n adapters.map(async (adapter) => ({ adapter, tools: await adapter.getTools() }))\n );\n return cache;\n }\n\n async function getConfig(): Promise<{ tools: ToolManifest }> {\n // Refresh on every config load \u2014 tools may depend on page/user.\n const resolved = await resolve(true);\n const tools: ToolDefinition[] = [];\n const seen = new Set<string>();\n\n for (const { adapter, tools: adapterTools } of resolved) {\n for (const tool of adapterTools) {\n if (seen.has(tool.name)) {\n logger.warn(\n `Duplicate tool name \"${tool.name}\"; keeping the first and ignoring the one from adapter \"${adapter.id ?? \"unknown\"}\".`\n );\n continue;\n }\n seen.add(tool.name);\n tools.push(tool);\n }\n }\n\n return {\n tools: {\n tools,\n sources: resolved.map(({ adapter, tools: adapterTools }, index) => ({\n id: adapter.id ?? `adapter-${index}`,\n count: adapterTools.length,\n })),\n },\n };\n }\n\n const onToolCall: ToolCallHandler = async (name, args) => {\n // Fast path: explicit ownership predicates need no resolution.\n for (const adapter of adapters) {\n if (adapter.ownsTool?.(name)) {\n return adapter.execute(name, args);\n }\n }\n // Otherwise route by the adapter's resolved tool names.\n const resolved = await resolve();\n for (const { adapter, tools } of resolved) {\n if (adapter.ownsTool) continue; // already consulted above\n if (tools.some((tool) => tool.name === name)) {\n return adapter.execute(name, args);\n }\n }\n throw new Error(`Unknown tool: ${name}`);\n };\n\n return { getConfig, onToolCall };\n}\n\n/** Config for {@link createYakServerAdapter}. */\nexport type YakServerAdapterConfig = {\n /**\n * Endpoint mounting `createYakHandler` / `createNextYakHandler`. GET returns the\n * tool manifest; POST `{ name, args }` executes a tool. Default: `\"/api/yak\"`.\n */\n endpoint?: string;\n /** Stable id for diagnostics. Default: `\"server\"`. */\n id?: string;\n /** Extra headers (e.g. auth) applied to both the GET manifest load and POST execution. */\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n};\n\nasync function buildHeaders(\n source: YakServerAdapterConfig[\"headers\"],\n base?: Record<string, string>\n): Promise<Headers> {\n const headers = new Headers(base);\n const resolved = typeof source === \"function\" ? await source() : source;\n if (resolved) {\n for (const [key, value] of new Headers(resolved)) {\n headers.set(key, value);\n }\n }\n return headers;\n}\n\n/**\n * Bridge a server-side yak handler ({@link createYakHandler} /\n * `createNextYakHandler`, e.g. fronting the `@yak-io/trpc` adapter) into a\n * client-side {@link ToolAdapter}, so server-relayed tools merge into the same\n * manifest + `onToolCall` as browser-executed adapters.\n */\nexport function createYakServerAdapter(config: YakServerAdapterConfig = {}): ToolAdapter {\n const endpoint = config.endpoint ?? \"/api/yak\";\n\n return {\n id: config.id ?? \"server\",\n getTools: async () => {\n const res = await fetch(endpoint, { headers: await buildHeaders(config.headers) });\n if (!res.ok) {\n throw new Error(`Failed to load tools from ${endpoint} (${res.status})`);\n }\n const chatConfig = (await res.json()) as ChatConfig;\n return chatConfig.tools?.tools ?? [];\n },\n execute: async (name, args) => {\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: await buildHeaders(config.headers, { \"Content-Type\": \"application/json\" }),\n body: JSON.stringify({ name, args }),\n });\n const data = (await res.json().catch(() => ({}))) as {\n ok?: boolean;\n result?: unknown;\n error?: string;\n };\n if (!res.ok || !data.ok) {\n throw new Error(data.error ?? `Tool \"${name}\" failed (${res.status})`);\n }\n return data.result;\n },\n };\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACcA,IAAM,cAAc;AAEpB,SAAS,mBAA4B;AACnC,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO;AAAA,EACT;AAGA,MAAI,OAAO,OAAO,4BAA4B,WAAW;AACvD,WAAO,OAAO;AAAA,EAChB;AAGA,MAAI;AACF,WAAO,aAAa,QAAQ,WAAW,MAAM;AAAA,EAC/C,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,sBAA+B;AAC7C,SAAO,iBAAiB;AAC1B;AAOO,SAAS,mBAAyB;AACvC,MAAI,OAAO,WAAW,aAAa;AACjC;AAAA,EACF;AACA,SAAO,0BAA0B;AACjC,MAAI;AACF,iBAAa,QAAQ,aAAa,MAAM;AAAA,EAC1C,QAAQ;AAAA,EAER;AACA,UAAQ,KAAK,uBAAuB;AACtC;AAOO,SAAS,oBAA0B;AACxC,MAAI,OAAO,WAAW,aAAa;AACjC;AAAA,EACF;AACA,SAAO,0BAA0B;AACjC,MAAI;AACF,iBAAa,WAAW,WAAW;AAAA,EACrC,QAAQ;AAAA,EAER;AACA,UAAQ,KAAK,wBAAwB;AACvC;AAGA,IAAI,OAAO,WAAW,aAAa;AACjC,EACE,OAIA,mBAAmB;AACrB,EACE,OAIA,oBAAoB;AACxB;AAEO,IAAM,SAAS;AAAA,EACpB,OAAO,CAAC,SAAiB,SAAyB;AAChD,QAAI,iBAAiB,GAAG;AACtB,UAAI,SAAS,QAAW;AACtB,gBAAQ,IAAI,cAAc,OAAO,IAAI,IAAI;AAAA,MAC3C,OAAO;AACL,gBAAQ,IAAI,cAAc,OAAO,EAAE;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,CAAC,SAAiB,SAAyB;AAC/C,QAAI,iBAAiB,GAAG;AACtB,UAAI,SAAS,QAAW;AACtB,gBAAQ,KAAK,cAAc,OAAO,IAAI,IAAI;AAAA,MAC5C,OAAO;AACL,gBAAQ,KAAK,cAAc,OAAO,EAAE;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,CAAC,SAAiB,SAAyB;AAC/C,QAAI,SAAS,QAAW;AACtB,cAAQ,KAAK,cAAc,OAAO,IAAI,IAAI;AAAA,IAC5C,OAAO;AACL,cAAQ,KAAK,cAAc,OAAO,EAAE;AAAA,IACtC;AAAA,EACF;AAAA,EAEA,OAAO,CAAC,SAAiB,SAAyB;AAChD,QAAI,SAAS,QAAW;AACtB,cAAQ,MAAM,cAAc,OAAO,IAAI,IAAI;AAAA,IAC7C,OAAO;AACL,cAAQ,MAAM,cAAc,OAAO,EAAE;AAAA,IACvC;AAAA,EACF;AACF;;;AC9HA,SAAS,kBAA0B;AACjC,MAAI,OAAO,aAAa,YAAa,QAAO;AAG5C,QAAM,YAAY,SAAS,KAAK,UAAU,IAAI;AAG9C,QAAM,oBAAoB;AAAA,IACxB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,aAAW,YAAY,mBAAmB;AACxC,UAAM,WAAW,UAAU,iBAAiB,QAAQ;AACpD,eAAW,MAAM,UAAU;AACzB,SAAG,OAAO;AAAA,IACZ;AAAA,EACF;AAGA,MAAI,OAAO,UAAU,eAAe,UAAU,aAAa;AAG3D,SAAO,KAAK,QAAQ,QAAQ,GAAG,EAAE,KAAK;AAGtC,QAAM,YAAY;AAClB,MAAI,KAAK,SAAS,WAAW;AAC3B,WAAO,GAAG,KAAK,UAAU,GAAG,SAAS,CAAC;AAAA,EACxC;AAEA,SAAO;AACT;AAKO,SAAS,qBAAkC;AAChD,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO;AAAA,MACL,KAAK;AAAA,MACL,OAAO;AAAA,MACP,MAAM;AAAA,MACN,WAAW,KAAK,IAAI;AAAA,IACtB;AAAA,EACF;AACA,SAAO;AAAA,IACL,KAAK,OAAO,SAAS;AAAA,IACrB,OAAO,SAAS;AAAA,IAChB,MAAM,gBAAgB;AAAA,IACtB,WAAW,KAAK,IAAI;AAAA,EACtB;AACF;AAKO,SAAS,SACd,MACA,MACkC;AAClC,MAAI,UAAiC;AAErC,SAAO,SAAS,oBAAoB,MAAqB;AACvD,UAAM,QAAQ,MAAM;AAClB,gBAAU;AACV,WAAK,GAAG,IAAI;AAAA,IACd;AAEA,QAAI,SAAS;AACX,mBAAa,OAAO;AAAA,IACtB;AACA,cAAU,WAAW,OAAO,IAAI;AAAA,EAClC;AACF;;;ACpEO,IAAM,yBAAyB;;;ACJtC,IAAM,sBAAsB,CAAC,UAAkB,eAAe,KAAK;AAEnE,IAAM,2BAA2B,CAAC,UAAkB,oBAAoB,KAAK;AAG7E,SAAS,uBAAuB,OAAmC;AACjE,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,iBAAiB,YAAa,QAAO;AACxF,MAAI;AACF,WAAO,OAAO,aAAa,QAAQ,oBAAoB,KAAK,CAAC,KAAK;AAAA,EACpE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,wBAAwB,OAAe,OAAqB;AACnE,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,iBAAiB,YAAa;AACjF,MAAI;AACF,WAAO,aAAa,QAAQ,oBAAoB,KAAK,GAAG,KAAK;AAAA,EAC/D,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,8BAA8B,OAAmC;AACxE,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,iBAAiB,YAAa,QAAO;AACxF,MAAI;AACF,WAAO,OAAO,aAAa,QAAQ,yBAAyB,KAAK,CAAC,KAAK;AAAA,EACzE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,+BAA+B,OAAe,SAAuB;AAC5E,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,iBAAiB,YAAa;AACjF,MAAI;AACF,WAAO,aAAa,QAAQ,yBAAyB,KAAK,GAAG,OAAO;AAAA,EACtE,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,+BAA+B,OAAqB;AAC3D,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,iBAAiB,YAAa;AACjF,MAAI;AACF,WAAO,aAAa,WAAW,yBAAyB,KAAK,CAAC;AAAA,EAChE,QAAQ;AAAA,EAER;AACF;AAGA,IAAM,sBAAsB;AAgB5B,SAAS,yBAAiC;AACxC,MACE,OAAO,WAAW,gBACjB,OAAO,SAAS,aAAa,eAAe,OAAO,SAAS,aAAa,gBAC1E,OAAO,OAAO,yBAAyB,aACvC;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAuFO,IAAM,YAAN,MAAgB;AAAA,EACb;AAAA,EACA,eAA8B;AAAA,EAC9B,eAAe;AAAA,EACf,cAAyD;AAAA,EACzD,yBAAyB;AAAA,EACzB,UAAU;AAAA,EACV;AAAA,EACA,WAAoC;AAAA,EAE5C,YAAY,QAAyB;AACnC,SAAK,SAAS;AACd,SAAK,uBAAuB,SAAS,MAAM;AACzC,aAAO,MAAM,6CAA6C;AAC1D,WAAK,gBAAgB;AAAA,IACvB,GAAG,GAAI;AAAA,EACT;AAAA,EAEO,aAAa,WAAqC;AACvD,SAAK,SAAS,EAAE,GAAG,KAAK,QAAQ,GAAG,UAAU;AAI7C,QAAI,KAAK,gBAAgB,KAAK,OAAO,cAAc,KAAK,OAAO,OAAO;AACpE,WAAK,mBAAmB,KAAK,YAAY,QAAQ,KAAK,YAAY,MAAM;AAAA,IAC1E;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,kBAA0B;AAC/B,WAAO,KAAK,OAAO,UAAU,uBAAuB;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYO,cAAsB;AAC3B,UAAM,SAAS,KAAK,gBAAgB;AACpC,UAAM,UAAU,GAAG,MAAM,WAAW,sBAAsB,IAAI,mBAAmB,KAAK,OAAO,KAAK,CAAC;AAEnG,UAAM,SAAS,IAAI,gBAAgB;AACnC,UAAM,QAAQ,KAAK,OAAO;AAE1B,QAAI,OAAO,aAAa,MAAM,cAAc,UAAU;AACpD,aAAO,IAAI,aAAa,MAAM,SAAS;AAAA,IACzC;AAEA,SAAK,kBAAkB,QAAQ,OAAO,OAAO,OAAO;AACpD,SAAK,kBAAkB,QAAQ,OAAO,MAAM,MAAM;AAGlD,QAAI,oBAAoB,GAAG;AACzB,aAAO,IAAI,YAAY,GAAG;AAAA,IAC5B;AAEA,UAAM,cAAc,OAAO,SAAS;AACpC,WAAO,cAAc,GAAG,OAAO,IAAI,WAAW,KAAK;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA,EAKQ,kBACN,QACA,QACA,QACM;AACN,QAAI,CAAC,OAAQ;AACb,UAAM,MAAsC;AAAA,MAC1C,CAAC,GAAG,MAAM,MAAM,OAAO,UAAU;AAAA,MACjC,CAAC,GAAG,MAAM,UAAU,OAAO,MAAM;AAAA,MACjC,CAAC,GAAG,MAAM,aAAa,OAAO,iBAAiB;AAAA,MAC/C,CAAC,GAAG,MAAM,eAAe,OAAO,gBAAgB;AAAA,MAChD,CAAC,GAAG,MAAM,aAAa,OAAO,iBAAiB;AAAA,MAC/C,CAAC,GAAG,MAAM,iBAAiB,OAAO,qBAAqB;AAAA,MACvD,CAAC,GAAG,MAAM,cAAc,OAAO,eAAe;AAAA,IAChD;AACA,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK;AAC9B,UAAI,MAAO,QAAO,IAAI,KAAK,KAAK;AAAA,IAClC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,WAAmB;AACxB,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA,EAKO,WAA8B;AACnC,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaO,WAAW,QAAsB;AACtC,UAAM,SAAS,KAAK,gBAAgB;AACpC,QAAI,CAAC,QAAQ;AACX,aAAO,KAAK,sCAAsC;AAClD;AAAA,IACF;AAEA,UAAM,UAAiC;AAAA,MACrC,MAAM;AAAA,MACN,SAAS,EAAE,OAAO;AAAA,IACpB;AACA,WAAO,OAAO,YAAY,SAAS,OAAO,MAAM;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,YAAkB;AACvB,UAAM,SAAS,KAAK,gBAAgB;AACpC,QAAI,CAAC,QAAQ;AACX,aAAO,KAAK,qCAAqC;AACjD;AAAA,IACF;AAEA,UAAM,UAAiC;AAAA,MACrC,MAAM;AAAA,IACR;AACA,WAAO,OAAO,YAAY,SAAS,OAAO,MAAM;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYQ,kBAA6D;AACnE,QAAI,KAAK,YAAa,QAAO,KAAK;AAClC,QAAI,KAAK,aAAc,QAAO,EAAE,QAAQ,KAAK,cAAc,QAAQ,KAAK,gBAAgB,EAAE;AAC1F,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKO,UAAmB;AACxB,WAAO,KAAK,gBAAgB;AAAA,EAC9B;AAAA,EAEO,gBAAgBA,SAAuB;AAC5C,SAAK,eAAeA;AACpB,QAAI,CAACA,SAAQ;AACX,WAAK,cAAc;AAAA,IACrB;AAAA,EACF;AAAA,EAEO,cAAc,QAAiB;AACpC,SAAK,eAAe;AACpB,QAAI,UAAU,KAAK,eAAe,KAAK,OAAO,YAAY;AACxD,WAAK,mBAAmB,KAAK,YAAY,QAAQ,KAAK,YAAY,MAAM;AAAA,IAC1E;AAEA,QAAI,QAAQ;AACV,WAAK,eAAe;AAAA,IACtB,OAAO;AACL,WAAK,cAAc;AAAA,IACrB;AAAA,EACF;AAAA,EAEO,QAAQ;AACb,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,iBAAiB,WAAW,KAAK,aAAa;AACrD,aAAO,iBAAiB,YAAY,KAAK,cAAc;AAAA,IACzD;AAAA,EACF;AAAA,EAEO,UAAU;AACf,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,oBAAoB,WAAW,KAAK,aAAa;AACxD,aAAO,oBAAoB,YAAY,KAAK,cAAc;AAAA,IAC5D;AACA,SAAK,cAAc;AAAA,EACrB;AAAA,EAEQ,iBAAiB;AACvB,QAAI,OAAO,WAAW,eAAe,CAAC,KAAK,aAAc;AAGzD,UAAM,aAAa,OAAO,SAAS;AACnC,QAAI,eAAe,KAAK,SAAS;AAC/B,WAAK,UAAU;AACf,aAAO,MAAM,mCAAmC;AAChD,WAAK,gBAAgB;AAAA,IACvB;AAEA,QAAI,KAAK,SAAU;AAEnB,SAAK,WAAW,IAAI,iBAAiB,CAAC,cAAc;AAClD,YAAM,wBAAwB,UAAU;AAAA,QACtC,CAAC,aACC,SAAS,SAAS,gBACjB,SAAS,WAAW,SAAS,KAAK,SAAS,aAAa,SAAS;AAAA,MACtE;AAEA,UAAI,uBAAuB;AACzB,aAAK,qBAAqB;AAAA,MAC5B;AAAA,IACF,CAAC;AAED,SAAK,SAAS,QAAQ,SAAS,MAAM;AAAA,MACnC,WAAW;AAAA,MACX,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AAAA,EAEQ,gBAAgB;AACtB,QAAI,KAAK,UAAU;AACjB,WAAK,SAAS,WAAW;AACzB,WAAK,WAAW;AAAA,IAClB;AAAA,EACF;AAAA,EAEQ,iBAAiB,MAAM;AAC7B,WAAO,MAAM,2CAA2C;AACxD,SAAK,gBAAgB;AAAA,EACvB;AAAA,EAEQ,gBAAgB,CAAC,UAAwB;AAC/C,QAAI,OAAO,WAAW,YAAa;AACnC,QAAI,CAAC,KAAK,gBAAgB,MAAM,MAAM,SAAS,aAAa;AAC1D;AAAA,IACF;AAEA,UAAM,aAAa,OAAO,SAAS;AACnC,UAAM,iBAAiB,oBAAI,IAAY;AACvC,mBAAe,IAAI,KAAK,gBAAgB,CAAC;AACzC,QAAI,YAAY;AACd,qBAAe,IAAI,UAAU;AAAA,IAC/B;AAEA,QAAI,CAAC,eAAe,IAAI,MAAM,MAAM,GAAG;AACrC,UAAI,CAAC,KAAK,wBAAwB;AAChC,eAAO;AAAA,UACL,4CAA4C,MAAM,MAAM,cAAc,MAAM,KAAK,cAAc,EAAE,KAAK,IAAI,CAAC;AAAA,QAC7G;AACA,aAAK,yBAAyB;AAAA,MAChC;AACA;AAAA,IACF;AAGA,QAAI,CAAC,MAAM,QAAQ,OAAO,MAAM,SAAS,YAAY,EAAE,UAAU,MAAM,OAAO;AAE5E,YAAM,OAAO,MAAM;AACnB,YAAM,kBACJ,MAAM,WAAW,mCACjB,MAAM,WAAW,2BACjB,MAAM,WAAW;AACnB,YAAM,kBAAkB,MAAM,WAAW;AAEzC,UAAI,mBAAmB,iBAAiB;AACtC;AAAA,MACF;AACA;AAAA,IACF;AAEA,UAAM,UAAU,MAAM;AACtB,UAAM,gBACH,MAAM,UAAU,iBAAiB,MAAM,SAAU,MAAM,SAAoB,SAC5E,KAAK;AACP,UAAM,eAAe,KAAK,gBAAgB;AAE1C,WAAO,MAAM,iCAAiC,QAAQ,IAAI;AAE1D,YAAQ,QAAQ,MAAM;AAAA,MACpB,KAAK,aAAa;AAChB,eAAO,MAAM,8BAA8B;AAE3C,YAAI,cAAc;AAChB,eAAK,cAAc,EAAE,QAAQ,cAAc,QAAQ,aAAa;AAChE,eAAK,mBAAmB,cAAc,YAAY;AAGlD,qBAAW,MAAM,KAAK,gBAAgB,GAAG,GAAG;AAG5C,qBAAW,MAAM,KAAK,OAAO,UAAU,GAAG,GAAG;AAAA,QAC/C,OAAO;AACL,iBAAO,KAAK,yDAAyD;AAAA,QACvE;AACA;AAAA,MACF;AAAA,MAEA,KAAK,iBAAiB;AACpB,cAAM,EAAE,IAAI,MAAM,KAAK,IAAI,QAAQ;AACnC,aAAK,KAAK,eAAe,IAAI,MAAM,IAAI;AACvC;AAAA,MACF;AAAA,MAEA,KAAK,gBAAgB;AACnB,cAAM,EAAE,KAAK,IAAI,QAAQ;AACzB,eAAO,MAAM,8BAA8B,IAAI;AAE/C,YAAI,CAAC,KAAK,kBAAkB,IAAI,GAAG;AACjC,iBAAO,KAAK,wCAAwC,IAAI;AACxD;AAAA,QACF;AAEA,YAAI,KAAK,OAAO,YAAY;AAC1B,eAAK,OAAO,WAAW,IAAI;AAAA,QAC7B,WAAW,OAAO,WAAW,aAAa;AACxC,iBAAO,SAAS,OAAO,IAAI;AAAA,QAC7B;AACA;AAAA,MACF;AAAA,MAEA,KAAK,eAAe;AAClB,cAAM,EAAE,aAAa,IAAI,QAAQ;AACjC,eAAO,MAAM,gDAAgD;AAC7D,gCAAwB,KAAK,OAAO,OAAO,YAAY;AACvD;AAAA,MACF;AAAA,MAEA,KAAK,oBAAoB;AACvB,cAAM,EAAE,QAAQ,IAAI,QAAQ;AAC5B,YAAI,YAAY,MAAM;AACpB,iBAAO,MAAM,wCAAwC;AACrD,yCAA+B,KAAK,OAAO,KAAK;AAAA,QAClD,OAAO;AACL,iBAAO,MAAM,uDAAuD;AACpE,yCAA+B,KAAK,OAAO,OAAO,OAAO;AAAA,QAC3D;AACA;AAAA,MACF;AAAA,MAEA,KAAK,aAAa;AAChB,eAAO,MAAM,oCAAoC;AACjD,aAAK,OAAO,UAAU;AACtB;AAAA,MACF;AAAA,MAEA;AACE,eAAO,MAAM,yBAA0B,QAA6B,IAAI;AACxE;AAAA,IACJ;AAAA,EACF;AAAA,EAEQ,mBAAmB,cAAsB,cAAsB;AAErE,UAAM,iBACJ,OAAO,WAAW,eAAe,OAAO,OAAO,4BAA4B,YACvE,OAAO,0BACP;AAEN,UAAM,qBAAqB,uBAAuB,KAAK,OAAO,KAAK;AACnE,UAAM,4BAA4B,8BAA8B,KAAK,OAAO,KAAK;AAEjF,UAAM,gBAAuC;AAAA,MAC3C,MAAM;AAAA,MACN,SAAS;AAAA,QACP,SAAS;AAAA,QACT,OAAO,KAAK,OAAO;AAAA,QACnB,OAAO,KAAK,OAAO;AAAA,QACnB,cAAc,KAAK,OAAO,YAAY,SAAS;AAAA,QAC/C,eAAe,KAAK,OAAO,YAAY,UAAU;AAAA,QACjD,SAAS,KAAK,OAAO;AAAA,QACrB;AAAA,QACA,MAAM,KAAK,OAAO;AAAA,QAClB,cAAc;AAAA,QACd,qBAAqB;AAAA,MACvB;AAAA,IACF;AAEA,WAAO,MAAM,oCAAoC;AAAA,MAC/C,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,iBAAiB,QAAQ,KAAK,OAAO,YAAY,KAAK;AAAA,MACtD,WAAW,KAAK,OAAO,YAAY,OAAO,MAAM,UAAU;AAAA,MAC1D,kBAAkB,QAAQ,KAAK,OAAO,YAAY,MAAM;AAAA,IAC1D,CAAC;AAED,iBAAa,YAAY,eAAe,YAAY;AAAA,EACtD;AAAA,EAEQ,kBAAkB;AACxB,QAAI,CAAC,KAAK,aAAc;AAExB,QAAI;AACF,YAAM,cAAc,mBAAmB;AACvC,YAAM,UAAiC;AAAA,QACrC,MAAM;AAAA,QACN,SAAS;AAAA,MACX;AACA,aAAO,MAAM,mCAAmC;AAAA,QAC9C,KAAK,YAAY;AAAA,QACjB,OAAO,YAAY;AAAA,QACnB,YAAY,YAAY,KAAK;AAAA,MAC/B,CAAC;AACD,WAAK,aAAa,YAAY,SAAS,KAAK,gBAAgB,CAAC;AAAA,IAC/D,SAAS,OAAO;AACd,aAAO,MAAM,kCAAkC,KAAK;AAAA,IACtD;AAAA,EACF;AAAA,EAEA,MAAc,eAAe,IAAY,MAAc,MAA8B;AACnF,WAAO,MAAM,uBAAuB,IAAI,IAAI,EAAE,IAAI,KAAK,CAAC;AAExD,QAAI,CAAC,KAAK,OAAO,YAAY;AAC3B,aAAO,MAAM,yDAAyD;AACtE,WAAK,uBAAuB,IAAI,OAAO,QAAW,iCAAiC;AACnF,WAAK,OAAO,qBAAqB;AAAA,QAC/B;AAAA,QACA;AAAA,QACA,IAAI;AAAA,QACJ,OAAO;AAAA,MACT,CAAC;AACD;AAAA,IACF;AAEA,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,OAAO,WAAW,MAAM,IAAI;AACtD,aAAO,MAAM,wBAAwB,IAAI,IAAI,EAAE,GAAG,CAAC;AACnD,WAAK,uBAAuB,IAAI,MAAM,MAAM;AAC5C,WAAK,OAAO,qBAAqB,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,CAAC;AAAA,IACnE,SAAS,OAAO;AACd,YAAM,eAAe,KAAK,oBAAoB,KAAK;AAMnD,aAAO,MAAM,qBAAqB,IAAI,IAAI,EAAE,IAAI,MAAM,CAAC;AACvD,WAAK,uBAAuB,IAAI,OAAO,QAAW,YAAY;AAC9D,WAAK,OAAO,qBAAqB,EAAE,MAAM,MAAM,IAAI,OAAO,OAAO,aAAa,CAAC;AAAA,IACjF;AAAA,EACF;AAAA,EAIQ,uBAAuB,IAAY,IAAa,QAAiB,OAAsB;AAC7F,QAAI,CAAC,KAAK,aAAc;AAExB,QAAI,IAAI;AAGN,YAAM,aAAa,KAAK,eAAe,MAAM;AAC7C,YAAM,UAAiC;AAAA,QACrC,MAAM;AAAA,QACN,SAAS,EAAE,IAAI,IAAI,MAAM,QAAQ,WAAW;AAAA,MAC9C;AACA,WAAK,aAAa,YAAY,SAAS,KAAK,gBAAgB,CAAC;AAAA,IAC/D,OAAO;AACL,YAAM,UAAiC;AAAA,QACrC,MAAM;AAAA,QACN,SAAS,EAAE,IAAI,IAAI,OAAO,OAAO,SAAS,gBAAgB;AAAA,MAC5D;AACA,WAAK,aAAa,YAAY,SAAS,KAAK,gBAAgB,CAAC;AAAA,IAC/D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,eAAe,OAAyB;AAC9C,QAAI,UAAU,UAAa,UAAU,MAAM;AACzC,aAAO;AAAA,IACT;AACA,QAAI;AACF,aAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AAAA,IACzC,QAAQ;AAEN,aAAO,KAAK,kEAAkE;AAC9E,aAAO,OAAO,KAAK;AAAA,IACrB;AAAA,EACF;AAAA,EAEQ,oBAAoB,OAAwB;AAClD,QAAI,iBAAiB,OAAO;AAC1B,aAAO,MAAM;AAAA,IACf;AACA,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,kBAAkB,MAAuB;AAE/C,QAAI,KAAK,WAAW,GAAG,KAAK,CAAC,KAAK,WAAW,IAAI,GAAG;AAClD,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,WAAW,GAAG,KAAK,KAAK,WAAW,GAAG,GAAG;AAChD,aAAO;AAAA,IACT;AAEA,QAAI,OAAO,WAAW,aAAa;AACjC,UAAI;AACF,cAAM,MAAM,IAAI,IAAI,MAAM,OAAO,SAAS,MAAM;AAChD,eAAO,IAAI,WAAW,OAAO,SAAS;AAAA,MACxC,QAAQ;AAEN,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;;;AC3pBO,IAAM,wBAAsC,EAAE,OAAO,OAAO;AAE5D,SAAS,aAAa,SAAuB,OAAiC;AACnF,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AACH,aAAO,QAAQ,UAAU,SAAS,EAAE,OAAO,aAAa,IAAI;AAAA,IAC9D,KAAK;AACH,aAAO,QAAQ,UAAU,eAAe,EAAE,OAAO,YAAY,IAAI;AAAA,IACnE,KAAK;AAIH,aAAO,QAAQ,UAAU,cAAc,EAAE,OAAO,WAAW,IAAI;AAAA,IACjE,KAAK;AACH,UAAI,QAAQ,UAAU,UAAU,QAAQ,UAAU,QAAS,QAAO;AAClE,aAAO,EAAE,OAAO,YAAY;AAAA,IAC9B,KAAK;AACH,aAAO,QAAQ,UAAU,cAAc,EAAE,OAAO,WAAW,IAAI;AAAA,IACjE,KAAK;AACH,UAAI,QAAQ,UAAU,cAAc,QAAQ,UAAU,YAAY;AAChE,eAAO,EAAE,OAAO,WAAW;AAAA,MAC7B;AACA,aAAO;AAAA,IACT,KAAK;AACH,aAAO,QAAQ,UAAU,aAAa,EAAE,OAAO,YAAY,IAAI;AAAA,IACjE,KAAK;AACH,aAAO,EAAE,OAAO,OAAO;AAAA,IACzB,KAAK;AACH,aAAO,EAAE,OAAO,SAAS,cAAc,MAAM,QAAQ;AAAA,IACvD,SAAS;AACP,YAAM,cAAqB;AAC3B,WAAK;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAyEA,SAAS,eAAe,MAAkE;AACxF,SAAO,KAAK,SAAS;AACvB;AAEA,SAAS,cAAc,KAAkC;AACvD,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,MAAI;AACF,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,eAAe,qBACb,MACA,KACe;AACf,QAAM,SAAS,KAAK;AACpB,QAAM,OAAO,KAAK;AAClB,MAAI,CAAC,UAAU,CAAC,KAAM;AACtB,MAAI,IAAI,aAAa,MAAM,EAAG;AAC9B,MAAI,eAAe,MAAM;AAEzB,QAAM,OAAO,cAAc,KAAK,SAAS;AAEzC,MAAI;AACJ,MAAI;AACF,UAAM,SAAS,MAAM,IAAI,iBAAiB,MAAM,IAAI;AACpD,aAAS,KAAK,UAAU,UAAU,IAAI;AAAA,EACxC,SAAS,OAAO;AACd,aAAS,KAAK,UAAU;AAAA,MACtB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAClD,CAAC;AAAA,EACH;AAEA,MAAI,SAAS;AAAA,IACX,MAAM;AAAA,IACN,MAAM,EAAE,MAAM,wBAAwB,SAAS,QAAQ,OAAO;AAAA,EAChE,CAAC;AACD,MAAI,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC1C;AAEA,SAAS,aAAa,KAA8D;AAClF,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,QAA+B,CAAC;AACtC,MAAI,OAAO,IAAI,iBAAiB,SAAU,OAAM,cAAc,IAAI;AAClE,MAAI,OAAO,IAAI,kBAAkB,SAAU,OAAM,eAAe,IAAI;AACpE,QAAM,YAAY,IAAI;AACtB,MAAI,WAAW;AACb,QAAI,OAAO,UAAU,kBAAkB,UAAU;AAC/C,YAAM,oBAAoB,UAAU;AAAA,IACtC;AACA,QAAI,OAAO,UAAU,iBAAiB,UAAU;AAC9C,YAAM,mBAAmB,UAAU;AAAA,IACrC;AACA,QAAI,OAAO,UAAU,gBAAgB,UAAU;AAC7C,YAAM,kBAAkB,UAAU;AAAA,IACpC;AAAA,EACF;AACA,QAAM,aAAa,IAAI;AACvB,MAAI,YAAY;AACd,QAAI,OAAO,WAAW,iBAAiB,UAAU;AAC/C,YAAM,oBAAoB,WAAW;AAAA,IACvC;AACA,QAAI,OAAO,WAAW,gBAAgB,UAAU;AAC9C,YAAM,mBAAmB,WAAW;AAAA,IACtC;AAAA,EACF;AACA,SAAO,OAAO,KAAK,KAAK,EAAE,SAAS,IAAI,QAAQ;AACjD;AAEA,eAAe,mBACb,UACA,KACe;AACf,QAAM,QAAQ,aAAa,UAAU,KAAK;AAC1C,MAAI,SAAS,IAAI,aAAa;AAC5B,QAAI;AACF,UAAI,YAAY,KAAK;AAAA,IACvB,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,SAAS,UAAU,UAAU,CAAC,GAAG,OAAO,cAAc;AAC5D,aAAW,QAAQ,OAAO;AACxB,UAAM,qBAAqB,MAAM,GAAG;AAAA,EACtC;AACF;AAEA,eAAsB,sBACpB,KACA,KACe;AACf,MAAI;AACJ,MAAI;AACF,cAAU,KAAK,MAAM,GAAG;AAAA,EAC1B,QAAQ;AACN;AAAA,EACF;AAEA,UAAQ,QAAQ,MAAM;AAAA,IACpB,KAAK;AACH,UAAI,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACnC;AAAA,IACF,KAAK;AACH,UAAI,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACnC;AAAA,IACF,KAAK;AAAA,IACL,KAAK;AACH,UAAI,KAAK,EAAE,MAAM,cAAc,CAAC;AAChC;AAAA,IACF,KAAK;AAAA,IACL,KAAK;AACH,UAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClC;AAAA,IACF,KAAK;AACH,YAAM,mBAAmB,QAAQ,UAAU,GAAG;AAC9C;AAAA,IACF,KAAK;AACH,UAAI,KAAK;AAAA,QACP,MAAM;AAAA,QACN,SAAS,QAAQ,OAAO,WAAW;AAAA,MACrC,CAAC;AACD;AAAA,IACF;AACE;AAAA,EACJ;AACF;;;ACvRA,IAAM,uBAAuB;AAetB,SAAS,eAAe,cAA8B;AAC3D,QAAM,YAAY,aAAa,QAAQ,mBAAmB,GAAG,EAAE,MAAM,GAAG,oBAAoB;AAC5F,SAAO,UAAU,SAAS,IAAI,YAAY;AAC5C;AAGA,IAAM,uBAAuB;AAOtB,SAAS,aAAa,cAAsB,MAA2B;AAC5E,MAAI,OAAO,eAAe,YAAY;AACtC,MAAI,KAAK,WAAW,oBAAoB,GAAG;AACzC,WAAO,KAAK,IAAI,GAAG,MAAM,GAAG,oBAAoB;AAAA,EAClD;AACA,MAAI,CAAC,KAAK,IAAI,IAAI,EAAG,QAAO;AAE5B,WAAS,IAAI,GAAG,IAAI,KAAM,KAAK;AAC7B,UAAM,SAAS,IAAI,CAAC;AACpB,UAAM,YAAY,GAAG,KAAK,MAAM,GAAG,uBAAuB,OAAO,MAAM,CAAC,GAAG,MAAM;AACjF,QAAI,CAAC,KAAK,IAAI,SAAS,EAAG,QAAO;AAAA,EACnC;AAEA,SAAO,GAAG,KAAK,MAAM,GAAG,EAAE,CAAC,IAAI,KAAK,IAAI;AAC1C;;;AC5BA,IAAM,yBAAyB;AAC/B,IAAM,qBAAqB;AAC3B,IAAM,qBAAqB;AAc3B,SAAS,sBAA8B;AACrC,MACE,OAAO,WAAW,gBACjB,OAAO,SAAS,aAAa,eAAe,OAAO,SAAS,aAAa,gBAC1E,OAAO,OAAO,yBAAyB,aACvC;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AA+CA,IAAM,kBAAoC;AAAA,EACxC,IAAI;AAAA,EACJ,aAAa;AAAA,EACb,WAAW;AAAA,EACX,cAAc;AAAA,EACd,gBAAgB;AAClB;AAwBA,SAAS,aAA+B;AACtC,SAAO;AAAA,IACL,aAAa;AAAA,IACb,mBAAmB;AAAA,IACnB,cAAc;AAAA,IACd,kBAAkB;AAAA,IAClB,mBAAmB;AAAA,IACnB,iBAAiB;AAAA,IACjB,kBAAkB;AAAA,IAClB,eAAe;AAAA,EACjB;AACF;AAEO,IAAM,kBAAN,MAAsB;AAAA,EACnB;AAAA,EACA,UAAwB;AAAA,EACxB,YAA8B;AAAA,EAC9B,oBAAoB,oBAAI,IAAY;AAAA,EACpC,YAAY,oBAAI,IAAwB;AAAA,EACxC,kBAAuC;AAAA;AAAA,EAEvC,QAA0B,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMrC,eAAe,oBAAI,IAAoB;AAAA,EAE/C,YAAY,QAA+B;AACzC,SAAK,SAAS;AACd,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAY,YAAoB;AAC9B,WAAO,KAAK,OAAO,aAAa,oBAAoB;AAAA,EACtD;AAAA;AAAA,EAGO,aAAa,OAA6C;AAC/D,SAAK,SAAS,EAAE,GAAG,KAAK,QAAQ,GAAG,MAAM;AAAA,EAC3C;AAAA,EAEO,WAAyB;AAC9B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,eAAuB;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA,EAEO,cAAc,UAA0C;AAC7D,SAAK,UAAU,IAAI,QAAQ;AAC3B,WAAO,MAAM;AACX,WAAK,UAAU,OAAO,QAAQ;AAAA,IAChC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAa,QAAuB;AAClC,QAAI,KAAK,QAAQ,UAAU,OAAQ;AACnC,WAAO,MAAM,uBAAuB;AACpC,SAAK,QAAQ,WAAW;AACxB,SAAK,SAAS,EAAE,MAAM,QAAQ,CAAC;AAE/B,QAAI,aAAqC,KAAK,OAAO;AACrD,QAAI,KAAK,OAAO,WAAW;AACzB,UAAI;AACF,qBAAa,MAAM,KAAK,OAAO,UAAU;AACzC,eAAO,MAAM,+BAA+B;AAAA,UAC1C,WAAW,YAAY,OAAO,MAAM,UAAU;AAAA,UAC9C,YAAY,YAAY,QAAQ,OAAO,UAAU;AAAA,QACnD,CAAC;AAAA,MACH,SAAS,KAAK;AACZ,eAAO,KAAK,6BAA6B,GAAG;AAAA,MAC9C;AAAA,IACF,WAAW,YAAY;AACrB,aAAO,MAAM,kCAAkC;AAAA,QAC7C,WAAW,WAAW,OAAO,MAAM,UAAU;AAAA,QAC7C,YAAY,WAAW,QAAQ,OAAO,UAAU;AAAA,MAClD,CAAC;AAAA,IACH,OAAO;AACL,aAAO,MAAM,gFAA2E;AAAA,IAC1F;AAKA,UAAM,oBAAoB,KAAK,uBAAuB,UAAU;AAChE,WAAO,MAAM,0BAA0B;AAAA,MACrC,KAAK,kBAAkB,MAAM,IAAI,CAAC,MAAM,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;AAAA,IAC7D,CAAC;AAED,UAAM,cAAc,KAAK,uBAAuB;AAChD,WAAO,MAAM,iCAAiC;AAAA,MAC5C,KAAK,aAAa;AAAA,MAClB,OAAO,aAAa;AAAA,MACpB,YAAY,aAAa,MAAM,UAAU;AAAA,IAC3C,CAAC;AAED,QAAI;AACJ,QAAI;AACF,aAAO,MAAM,sDAAsD;AACnE,aAAO,MAAM,KAAK,UAAU,YAAY,mBAAmB,WAAW;AACtE,aAAO,MAAM,yBAAyB;AAAA,QACpC,gBAAgB,KAAK;AAAA,QACrB,WAAW,KAAK;AAAA,MAClB,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,YAAM,KAAK,SAAS,eAAe,QAAQ,IAAI,UAAU,+BAA+B;AACxF;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,aAAO,MAAM,qCAAqC;AAClD,kBAAY,MAAM,UAAU,aAAa,aAAa,EAAE,OAAO,KAAK,CAAC;AACrE,aAAO,MAAM,kCAAkC;AAAA,IACjD,SAAS,KAAK;AACZ,YAAM,OAAO,eAAe,QAAQ,IAAI,OAAO;AAC/C,YAAM,UACJ,SAAS,qBAAqB,SAAS,0BACnC,2GACA;AACN,YAAM,KAAK,SAAS,OAAO;AAC3B;AAAA,IACF;AAEA,UAAM,KAAK,IAAI,kBAAkB;AACjC,UAAM,eAAe,SAAS,cAAc,OAAO;AACnD,iBAAa,WAAW;AACxB,iBAAa,MAAM,UAAU;AAC7B,aAAS,KAAK,YAAY,YAAY;AAEtC,OAAG,UAAU,CAAC,UAAU;AACtB,aAAO,MAAM,gDAAgD;AAC7D,UAAI,MAAM,QAAQ,CAAC,GAAG;AACpB,qBAAa,YAAY,MAAM,QAAQ,CAAC;AAAA,MAC1C;AAAA,IACF;AAEA,OAAG,6BAA6B,MAAM;AACpC,YAAM,IAAI,GAAG;AACb,aAAO,MAAM,sCAAiC,CAAC;AAC/C,UAAI,MAAM,YAAY,MAAM,gBAAgB;AAC1C,aAAK,KAAK,SAAS,qBAAqB,CAAC,EAAE;AAAA,MAC7C;AAAA,IACF;AACA,OAAG,0BAA0B,MAAM;AACjC,aAAO,MAAM,uCAAkC,GAAG,eAAe;AACjE,UAAI,GAAG,oBAAoB,UAAU;AACnC,aAAK,KAAK,SAAS,0BAA0B;AAAA,MAC/C;AAAA,IACF;AAEA,eAAW,SAAS,UAAU,eAAe,GAAG;AAC9C,SAAG,SAAS,OAAO,SAAS;AAAA,IAC9B;AAEA,UAAM,cAAc,GAAG,kBAAkB,YAAY;AACrD,gBAAY,YAAY,CAAC,UAAU;AACjC,YAAM,MAAM,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO;AAC1D,UAAI,CAAC,IAAK;AACV,aAAO,MAAM,sCAAiC,IAAI,MAAM,GAAG,GAAG,CAAC;AAC/D,WAAK,sBAAsB,KAAK,KAAK,oBAAoB,CAAC;AAAA,IAC5D;AACA,gBAAY,SAAS,MAAM;AACzB,aAAO,MAAM,4BAA4B;AACzC,WAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAQnC,UAAI,KAAK,cAAc,OAAO;AAC5B,aAAK,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAC5C,aAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAAA,MACtD;AAAA,IACF;AACA,gBAAY,UAAU,MAAM;AAC1B,aAAO,MAAM,4BAA4B;AAAA,IAC3C;AAEA,SAAK,YAAY;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,gBAAgB,KAAK;AAAA,IACvB;AAEA,QAAI;AACF,aAAO,MAAM,8BAA8B;AAC3C,YAAM,QAAQ,MAAM,GAAG,YAAY;AACnC,YAAM,GAAG,oBAAoB,KAAK;AAClC,aAAO,MAAM,4CAA4C;AACzD,YAAM,YAAY,MAAM,KAAK,YAAY,OAAO,KAAK,YAAY;AACjE,YAAM,GAAG,qBAAqB,EAAE,MAAM,UAAU,KAAK,UAAU,CAAC;AAChE,aAAO,MAAM,oCAAoC;AAAA,IACnD,SAAS,KAAK;AACZ,YAAM,KAAK;AAAA,QACT,eAAe,QAAQ,IAAI,UAAU;AAAA,MACvC;AACA;AAAA,IACF;AAEA,SAAK,KAAK,iBAAiB,SAAS,KAAK,gBAAgB,WAAW;AAAA,EACtE;AAAA;AAAA,EAGA,MAAa,OAAsB;AACjC,WAAO,MAAM,sBAAsB;AACnC,SAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC9B,UAAM,KAAK,SAAS;AAAA,EACtB;AAAA;AAAA,EAGO,UAAgB;AACrB,SAAK,KAAK,SAAS;AACnB,QAAI,KAAK,iBAAiB;AACxB,aAAO,oBAAoB,YAAY,KAAK,eAAe;AAC3D,WAAK,kBAAkB;AAAA,IACzB;AACA,SAAK,UAAU,MAAM;AAAA,EACvB;AAAA;AAAA,EAIQ,sBAA8C;AACpD,WAAO;AAAA,MACL,MAAM,CAAC,UAAU,KAAK,SAAS,KAAK;AAAA,MACpC,UAAU,CAAC,YAAY,KAAK,oBAAoB,OAAO;AAAA,MACvD,kBAAkB,CAAC,MAAM,SAAS,KAAK,cAAc,MAAM,IAAI;AAAA,MAC/D,cAAc,CAAC,OAAO,KAAK,kBAAkB,IAAI,EAAE;AAAA,MACnD,gBAAgB,CAAC,OAAO;AACtB,aAAK,kBAAkB,IAAI,EAAE;AAAA,MAC/B;AAAA,MACA,aAAa,CAAC,UAAU,KAAK,gBAAgB,KAAK;AAAA,IACpD;AAAA,EACF;AAAA,EAEQ,gBAAgB,OAAoC;AAC1D,SAAK,MAAM,iBAAiB;AAC5B,QAAI,OAAO,MAAM,gBAAgB,SAAU,MAAK,MAAM,eAAe,MAAM;AAC3E,QAAI,OAAO,MAAM,sBAAsB,UAAU;AAC/C,WAAK,MAAM,qBAAqB,MAAM;AAAA,IACxC;AACA,QAAI,OAAO,MAAM,iBAAiB,SAAU,MAAK,MAAM,gBAAgB,MAAM;AAC7E,QAAI,OAAO,MAAM,qBAAqB,UAAU;AAC9C,WAAK,MAAM,oBAAoB,MAAM;AAAA,IACvC;AACA,QAAI,OAAO,MAAM,sBAAsB,UAAU;AAC/C,WAAK,MAAM,qBAAqB,MAAM;AAAA,IACxC;AACA,QAAI,OAAO,MAAM,oBAAoB,UAAU;AAC7C,WAAK,MAAM,mBAAmB,MAAM;AAAA,IACtC;AACA,QAAI,OAAO,MAAM,qBAAqB,UAAU;AAC9C,WAAK,MAAM,oBAAoB,MAAM;AAAA,IACvC;AAAA,EACF;AAAA,EAEQ,oBAAoB,SAAwB;AAClD,UAAM,UAAU,KAAK,UAAU;AAC/B,QAAI,CAAC,WAAW,QAAQ,eAAe,QAAQ;AAC7C,aAAO,KAAK,gDAAgD;AAC5D;AAAA,IACF;AACA,QAAI;AACF,YAAM,aAAa,KAAK,UAAU,OAAO;AACzC,aAAO,MAAM,mCAA8B,WAAW,MAAM,GAAG,GAAG,CAAC;AACnE,cAAQ,KAAK,UAAU;AAAA,IACzB,SAAS,KAAK;AACZ,aAAO,KAAK,wCAAwC,GAAG;AAAA,IACzD;AAAA,EACF;AAAA,EAEA,MAAc,cAAc,UAAkB,MAAiC;AAI7E,UAAM,OAAO,KAAK,aAAa,IAAI,QAAQ,KAAK;AAChD,WAAO,MAAM,+BAA+B,EAAE,IAAI,UAAU,MAAM,KAAK,CAAC;AAIxE,QAAI,KAAK,WAAW,OAAO,GAAG;AAC5B,aAAO,MAAM,KAAK,YAAY,MAAM,IAAI;AAAA,IAC1C;AACA,QAAI,SAAS,YAAY;AACvB,YAAM,OAAQ,MAA6B;AAC3C,UAAI,OAAO,SAAS,UAAU;AAC5B,cAAM,IAAI,MAAM,iDAAiD;AAAA,MACnE;AACA,UAAI,KAAK,OAAO,YAAY;AAC1B,aAAK,OAAO,WAAW,IAAI;AAAA,MAC7B,WAAW,OAAO,WAAW,aAAa;AACxC,eAAO,SAAS,OAAO,IAAI;AAAA,MAC7B;AACA,aAAO,EAAE,SAAS,MAAM,YAAY,MAAM,KAAK;AAAA,IACjD;AACA,QAAI,KAAK,OAAO,YAAY;AAC1B,aAAO,MAAM,KAAK,OAAO,WAAW,MAAM,IAAI;AAAA,IAChD;AACA,UAAM,IAAI,MAAM,mCAAmC,IAAI,EAAE;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,YAAY,UAAkB,MAAiC;AAC3E,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,GAAG,KAAK,SAAS,uBAAuB;AAAA,QAC9D,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU;AAAA,UACnB,OAAO,KAAK,OAAO;AAAA,UACnB;AAAA,UACA,MAAM,QAAQ,CAAC;AAAA,UACf,aAAa,KAAK,uBAAuB;AAAA,QAC3C,CAAC;AAAA,MACH,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAMC,QAAQ,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC/C,eAAO,EAAE,OAAOA,MAAK,SAAS,oBAAoB,IAAI,MAAM,IAAI;AAAA,MAClE;AACA,YAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,aAAO,KAAK,UAAU,CAAC;AAAA,IACzB,SAAS,KAAK;AAIZ,aAAO,MAAM,gCAAgC,GAAG;AAChD,aAAO,EAAE,OAAO,mDAAmD;AAAA,IACrE;AAAA,EACF;AAAA,EAEA,MAAc,UACZ,YACA,mBACA,aACuB;AACvB,UAAM,MAAM,MAAM,MAAM,GAAG,KAAK,SAAS,6BAA6B;AAAA,MACpE,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU;AAAA,QACnB,OAAO,KAAK,OAAO;AAAA,QACnB;AAAA,QACA,cAAc;AAAA,QACd,eAAe,YAAY;AAAA,MAC7B,CAAC;AAAA,IACH,CAAC;AACD,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,OAAQ,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC/C,YAAM,IAAI,MAAM,KAAK,SAAS,gBAAgB,IAAI,MAAM,GAAG;AAAA,IAC7D;AACA,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,uBAAuB,YAE7B;AACA,SAAK,aAAa,MAAM;AACxB,UAAM,OAAO,oBAAI,IAAY,CAAC,UAAU,CAAC;AAEzC,UAAM,sBAAsB,YAAY,OAAO,SAAS,CAAC,GAAG,IAAI,CAAC,MAAsB;AACrF,YAAM,KAAK,aAAa,EAAE,MAAM,IAAI;AACpC,WAAK,IAAI,EAAE;AACX,WAAK,aAAa,IAAI,IAAI,EAAE,IAAI;AAChC,aAAO,EAAE,GAAG,GAAG,GAAG;AAAA,IACpB,CAAC;AAED,WAAO,EAAE,OAAO,mBAAmB;AAAA,EACrC;AAAA,EAEA,MAAc,YACZ,OACA,cACiB;AACjB,UAAM,cAAc,MAAM,MAAM,GAAG,kBAAkB,UAAU,sBAAsB,IAAI;AAAA,MACvF,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,YAAY;AAAA,QACrC,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,MAAM;AAAA,IACd,CAAC;AACD,QAAI,CAAC,YAAY,IAAI;AACnB,YAAM,OAAO,MAAM,YAAY,KAAK,EAAE,MAAM,MAAM,EAAE;AACpD,YAAM,IAAI,MAAM,wBAAwB,YAAY,MAAM,MAAM,IAAI,EAAE;AAAA,IACxE;AACA,WAAO,MAAM,YAAY,KAAK;AAAA,EAChC;AAAA,EAEQ,mBAAmB,gBAAwB,aAAsC;AACvF,WAAO;AAAA,MACL,OAAO,KAAK,OAAO;AAAA,MACnB;AAAA,MACA,OAAO;AAAA,MACP,iBAAiB,KAAK,IAAI;AAAA,MAC1B;AAAA,MACA,OAAO,EAAE,GAAG,KAAK,MAAM;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,MAAc,iBACZ,OACA,gBACA,aACe;AACf,QAAI;AACF,YAAM,OACJ,UAAU,SACN,KAAK,mBAAmB,gBAAgB,WAAW,IACnD;AAAA,QACE,OAAO,KAAK,OAAO;AAAA,QACnB;AAAA,QACA;AAAA,QACA,iBAAiB,KAAK,IAAI;AAAA,QAC1B;AAAA,MACF;AACN,YAAM,MAAM,GAAG,KAAK,SAAS,4BAA4B;AAAA,QACvD,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,IAAI;AAAA,QACzB,WAAW,UAAU;AAAA,MACvB,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,aAAO,KAAK,gCAAgC,KAAK,IAAI,GAAG;AAAA,IAC1D;AAAA,EACF;AAAA,EAEA,MAAc,WAA0B;AACtC,UAAM,IAAI,KAAK;AACf,SAAK,YAAY;AACjB,SAAK,oBAAoB,oBAAI,IAAI;AAEjC,QAAI;AACF,QAAE,aAAa,MAAM;AAAA,IACvB,SAAS,KAAK;AACZ,aAAO,KAAK,8BAA8B,GAAG;AAAA,IAC/C;AACA,QAAI;AACF,iBAAW,SAAS,EAAE,WAAW,UAAU,KAAK,CAAC,GAAG;AAClD,cAAM,KAAK;AAAA,MACb;AAAA,IACF,SAAS,KAAK;AACZ,aAAO,KAAK,6BAA6B,GAAG;AAAA,IAC9C;AACA,QAAI;AACF,iBAAW,UAAU,EAAE,IAAI,WAAW,KAAK,CAAC,GAAG;AAC7C,eAAO,OAAO,KAAK;AAAA,MACrB;AACA,QAAE,IAAI,MAAM;AAAA,IACd,SAAS,KAAK;AACZ,aAAO,KAAK,iCAAiC,GAAG;AAAA,IAClD;AACA,QAAI,EAAE,cAAc;AAClB,UAAI;AACF,UAAE,aAAa,YAAY;AAC3B,UAAE,aAAa,OAAO;AAAA,MACxB,SAAS,KAAK;AACZ,eAAO,KAAK,gCAAgC,GAAG;AAAA,MACjD;AAAA,IACF;AAEA,QAAI,EAAE,gBAAgB;AACpB,YAAM,KAAK,iBAAiB,QAAQ,EAAE,gBAAgB,KAAK,uBAAuB,CAAC;AAAA,IACrF;AAAA,EACF;AAAA,EAEA,MAAc,SAAS,SAAgC;AACrD,WAAO,KAAK,wBAAwB,OAAO;AAC3C,SAAK,SAAS,EAAE,MAAM,SAAS,QAAQ,CAAC;AACxC,UAAM,KAAK,SAAS;AAAA,EACtB;AAAA,EAEQ,SAAS,OAAiD;AAChE,UAAM,OAAO,aAAa,KAAK,SAAS,KAAK;AAC7C,QAAI,SAAS,KAAK,QAAS;AAC3B,SAAK,UAAU;AACf,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI;AACF,iBAAS,IAAI;AAAA,MACf,SAAS,KAAK;AACZ,eAAO,KAAK,8BAA8B,GAAG;AAAA,MAC/C;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,yBAAkD;AACxD,QAAI;AACF,aAAO,mBAAmB;AAAA,IAC5B,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,OAAO,WAAW,YAAa;AACnC,SAAK,kBAAkB,MAAM;AAC3B,YAAM,IAAI,KAAK;AACf,UAAI,CAAC,EAAE,eAAgB;AACvB,YAAM,OAAO,KAAK,UAAU,KAAK,mBAAmB,EAAE,gBAAgB,MAAS,CAAC;AAChF,UAAI,UAAU,YAAY;AACxB,kBAAU;AAAA,UACR,GAAG,KAAK,SAAS;AAAA,UACjB,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAAA,QAC/C;AAAA,MACF;AAAA,IACF;AACA,WAAO,iBAAiB,YAAY,KAAK,eAAe;AAAA,EAC1D;AACF;;;ACvnBA,IAAM,mBAAmC;AAuDzC,IAAM,qBAAqB;AAE3B,IAAM,kBAAkB;AAExB,IAAM,WAAW;AAEjB,IAAM,mBAA+C;AAAA,EACnD,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AACT;AAIA,SAAS,iBAAyB;AAChC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsFT;AAEA,SAAS,mBAA2B;AAClC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwJT;AAoBO,IAAM,WAAN,MAAe;AAAA,EACH;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAGT,UAAmC;AAAA,EACnC,YAAmC;AAAA,EACnC,YAAmC;AAAA,EACnC,SAAmC;AAAA,EACnC,YAAmC;AAAA,EACnC,aAAuC;AAAA,EACvC,cAAwC;AAAA;AAAA,EAGxC,SAAS;AAAA,EACT,UAAU;AAAA,EACV,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB,gBAA+B;AAAA,EAC/B,UAAU;AAAA,EACV,eAA6B;AAAA;AAAA,EAG7B,iBAAiB,oBAAI,IAA2B;AAAA,EAChD,iBAAiB,oBAAI,IAAwB;AAAA,EAC7C,mBAAwC;AAAA,EACxC,cAAqC;AAAA,EACrC,gBAA2D;AAAA,EAC3D,gBAAoD;AAAA,EAE5D,YAAY,QAAwB;AAClC,SAAK,SAAS;AACd,SAAK,OAAO,OAAO,QAAQ;AAG3B,SAAK,SAAS,IAAI,UAAU;AAAA,MAC1B,GAAG;AAAA,MACH,SAAS,MAAM;AACb,aAAK,UAAU;AACf,aAAK,iBAAiB;AACtB,aAAK,sBAAsB;AAC3B,aAAK,kBAAkB;AACvB,aAAK,gBAAgB;AACrB,aAAK,kBAAkB;AAIvB,aAAK,gBAAgB;AACrB,eAAO,UAAU;AAAA,MACnB;AAAA,MACA,SAAS,MAAM;AACb,aAAK,MAAM;AACX,eAAO,UAAU;AAAA,MACnB;AAAA,IACF,CAAC;AAED,QAAI,KAAK,SAAS,QAAQ;AACxB,YAAM,cAAqC;AAAA,QACzC,OAAO,OAAO;AAAA;AAAA;AAAA,QAGd,WAAW,OAAO;AAAA,QAClB,WAAW,OAAO;AAAA,QAClB,YAAY,OAAO;AAAA,QACnB,YAAY,OAAO;AAAA,QACnB,YAAY,OAAO;AAAA,MACrB;AACA,WAAK,QAAQ,IAAI,gBAAgB,WAAW;AAAA,IAC9C,OAAO;AACL,WAAK,QAAQ;AAAA,IACf;AAAA,EACF;AAAA;AAAA,EAGO,YAAuB;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGO,kBAA0C;AAC/C,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGO,UAAsB;AAC3B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASO,MAAM,QAA4B;AACvC,QAAI,KAAK,QAAS;AAClB,SAAK,UAAU;AAEf,UAAM,SAAS,UAAU,KAAK,OAAO,UAAU,SAAS;AAGxD,SAAK,UAAU,SAAS,cAAc,OAAO;AAC7C,SAAK,QAAQ,cAAc,eAAe,IAAI,iBAAiB;AAC/D,WAAO,YAAY,KAAK,OAAO;AAG/B,QAAI,KAAK,OAAO,YAAY,OAAO;AACjC,WAAK,cAAc,MAAM;AAAA,IAC3B;AAGA,SAAK,gBAAgB,CAAC,UAAwB;AAC5C,UAAI,MAAM,MAAM,SAAS,oBAAoB;AAC3C,aAAK,aAAa,QAAQ,MAAM,KAAK,QAAQ;AAC7C,aAAK,iBAAiB;AACtB,aAAK,gBAAgB;AAAA,MACvB;AAAA,IACF;AACA,WAAO,iBAAiB,WAAW,KAAK,aAAa;AAGrD,SAAK,OAAO,MAAM;AAGlB,QAAI,KAAK,OAAO;AACd,WAAK,eAAe,KAAK,MAAM,SAAS;AACxC,WAAK,mBAAmB,KAAK,MAAM,cAAc,CAAC,YAAY;AAC5D,aAAK,eAAe;AACpB,aAAK,uBAAuB;AAC5B,mBAAW,YAAY,KAAK,gBAAgB;AAC1C,cAAI;AACF,qBAAS,OAAO;AAAA,UAClB,SAAS,KAAK;AACZ,mBAAO,KAAK,4BAA4B,GAAG;AAAA,UAC7C;AAAA,QACF;AAAA,MACF,CAAC;AACD,WAAK,uBAAuB;AAAA,IAC9B;AAAA,EACF;AAAA;AAAA,EAGO,UAAgB;AACrB,QAAI,CAAC,KAAK,QAAS;AACnB,SAAK,UAAU;AAEf,SAAK,OAAO,QAAQ;AAIpB,SAAK,OAAO,gBAAgB,IAAI;AAEhC,QAAI,KAAK,kBAAkB;AACzB,WAAK,iBAAiB;AACtB,WAAK,mBAAmB;AAAA,IAC1B;AACA,SAAK,OAAO,QAAQ;AAEpB,QAAI,KAAK,eAAe;AACtB,aAAO,oBAAoB,WAAW,KAAK,aAAa;AACxD,WAAK,gBAAgB;AAAA,IACvB;AAEA,QAAI,KAAK,eAAe,KAAK,eAAe;AAC1C,WAAK,YAAY,oBAAoB,UAAU,KAAK,aAAa;AACjE,WAAK,cAAc;AACnB,WAAK,gBAAgB;AAAA,IACvB;AAEA,SAAK,WAAW,OAAO;AACvB,SAAK,WAAW,OAAO;AACvB,SAAK,SAAS,OAAO;AAErB,SAAK,YAAY;AACjB,SAAK,YAAY;AACjB,SAAK,SAAS;AACd,SAAK,YAAY;AACjB,SAAK,aAAa;AAClB,SAAK,cAAc;AACnB,SAAK,UAAU;AACf,SAAK,SAAS;AACd,SAAK,UAAU;AACf,SAAK,aAAa;AAClB,SAAK,gBAAgB;AAErB,SAAK,eAAe,MAAM;AAC1B,SAAK,eAAe,MAAM;AAAA,EAC5B;AAAA;AAAA;AAAA,EAKO,OAAa;AAClB,QAAI,CAAC,KAAK,QAAS;AAEnB,QAAI,CAAC,KAAK,eAAe;AACvB,WAAK,gBAAgB;AAErB,YAAM,SAAS,KAAK,OAAO,UAAU,SAAS;AAC9C,WAAK,YAAY,MAAM;AAAA,IACzB;AAEA,SAAK,SAAS;AACd,SAAK,OAAO,cAAc,IAAI;AAC9B,SAAK,iBAAiB;AACtB,SAAK,sBAAsB;AAC3B,SAAK,gBAAgB;AACrB,SAAK,gBAAgB;AAAA,EACvB;AAAA;AAAA,EAGO,QAAc;AACnB,SAAK,SAAS;AACd,SAAK,OAAO,cAAc,KAAK;AAC/B,SAAK,iBAAiB;AACtB,SAAK,sBAAsB;AAC3B,SAAK,gBAAgB;AAAA,EACvB;AAAA;AAAA,EAGO,SAAe;AACpB,QAAI,KAAK,QAAQ;AACf,WAAK,MAAM;AAAA,IACb,OAAO;AACL,WAAK,KAAK;AAAA,IACZ;AAAA,EACF;AAAA;AAAA,EAGO,eAAe,QAAsB;AAC1C,SAAK,gBAAgB;AACrB,SAAK,KAAK;AACV,SAAK,kBAAkB;AAAA,EACzB;AAAA;AAAA,EAGO,WAA0B;AAC/B,WAAO;AAAA,MACL,QAAQ,KAAK;AAAA,MACb,SAAS,KAAK;AAAA,MACd,WAAW,KAAK,UAAU,CAAC,KAAK;AAAA,MAChC,YAAY,KAAK;AAAA,IACnB;AAAA,EACF;AAAA;AAAA,EAGO,cAAc,UAA6C;AAChE,SAAK,eAAe,IAAI,QAAQ;AAChC,WAAO,MAAM;AACX,WAAK,eAAe,OAAO,QAAQ;AAAA,IACrC;AAAA,EACF;AAAA;AAAA;AAAA,EAKO,aAA4B;AACjC,WAAO,KAAK,QAAQ,KAAK,MAAM,MAAM,IAAI,QAAQ,QAAQ;AAAA,EAC3D;AAAA;AAAA,EAGO,YAA2B;AAChC,WAAO,KAAK,QAAQ,KAAK,MAAM,KAAK,IAAI,QAAQ,QAAQ;AAAA,EAC1D;AAAA;AAAA,EAGA,MAAa,cAA6B;AACxC,QAAI,CAAC,KAAK,MAAO;AACjB,UAAM,QAAQ,KAAK,MAAM,SAAS,EAAE;AACpC,QAAI,UAAU,UAAU,UAAU,SAAS;AACzC,YAAM,KAAK,MAAM,MAAM;AAAA,IACzB,WAAW,UAAU,eAAe,UAAU,cAAc,UAAU,YAAY;AAChF,YAAM,KAAK,MAAM,KAAK;AAAA,IACxB;AAAA,EACF;AAAA;AAAA,EAGO,gBAA8B;AACnC,WAAO,KAAK,QAAQ,KAAK,MAAM,SAAS,IAAI;AAAA,EAC9C;AAAA;AAAA,EAGO,mBAAmB,UAA0C;AAClE,SAAK,eAAe,IAAI,QAAQ;AAChC,WAAO,MAAM;AACX,WAAK,eAAe,OAAO,QAAQ;AAAA,IACrC;AAAA,EACF;AAAA;AAAA,EAIQ,YAAY,QAA2B;AAC7C,UAAM,QAAQ,KAAK,OAAO;AAC1B,UAAM,WAAW,OAAO,YAAY;AACpC,UAAM,YAAY,OAAO;AACzB,UAAM,cAAc,OAAO,eAAe;AAC1C,UAAM,WAAW,gBAAgB;AAGjC,SAAK,YAAY,SAAS,cAAc,KAAK;AAC7C,SAAK,UAAU,YAAY;AAG3B,SAAK,YAAY,SAAS,cAAc,KAAK;AAC7C,UAAM,UAAU,CAAC,qBAAqB;AACtC,QAAI,SAAU,SAAQ,KAAK,kBAAkB;AAC7C,QAAI,cAAc,QAAS,SAAQ,KAAK,iBAAiB;AAAA,aAChD,cAAc,OAAQ,SAAQ,KAAK,gBAAgB;AAC5D,SAAK,UAAU,YAAY,QAAQ,KAAK,GAAG;AAC3C,SAAK,UAAU,QAAQ,WAAW;AAIlC,SAAK,SAAS,SAAS,cAAc,QAAQ;AAC7C,SAAK,OAAO,QAAQ;AACpB,SAAK,OAAO,QAAQ;AACpB,SAAK,OAAO,YAAY;AACxB,SAAK,OAAO,MAAM,KAAK,OAAO,YAAY;AAE1C,SAAK,OAAO,iBAAiB,QAAQ,MAAM;AACzC,WAAK,OAAO,gBAAgB,KAAK,QAAQ,iBAAiB,IAAI;AAAA,IAChE,CAAC;AAED,SAAK,UAAU,YAAY,KAAK,MAAM;AACtC,SAAK,UAAU,YAAY,KAAK,SAAS;AACzC,WAAO,YAAY,KAAK,SAAS;AAGjC,SAAK,cAAc,OAAO,WAAW,oBAAoB;AACzD,SAAK,gBAAgB,CAAC,MAA2B;AAC/C,WAAK,uBAAuB,EAAE,OAAO;AAAA,IACvC;AACA,SAAK,YAAY,iBAAiB,UAAU,KAAK,aAAa;AAAA,EAChE;AAAA,EAEQ,cAAc,QAA2B;AAC/C,UAAM,QAAQ,KAAK,OAAO;AAC1B,UAAM,WAA2B,OAAO,YAAY;AACpD,UAAM,YAAY,OAAO;AACzB,UAAM,gBAAgB,OAAO,KAAK,OAAO,YAAY,WAAW,KAAK,OAAO,UAAU,CAAC;AAEvF,SAAK,YAAY,SAAS,cAAc,KAAK;AAC7C,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,UAAU,QAAQ,OAAO,KAAK;AACnC,SAAK,UAAU,YAAY,KAAK,oBAAoB,WAAW,aAAa;AAC5E,SAAK,yBAAyB,aAAa;AAG3C,UAAM,SAAS,SAAS,cAAc,KAAK;AAC3C,WAAO,YAAY;AACnB,UAAM,UAAU,SAAS,cAAc,KAAK;AAC5C,YAAQ,MAAM,GAAG,KAAK,OAAO,gBAAgB,CAAC;AAC9C,YAAQ,MAAM;AACd,YAAQ,QAAQ;AAChB,YAAQ,SAAS;AACjB,YAAQ,YAAY;AACpB,WAAO,YAAY,OAAO;AAC1B,SAAK,UAAU,YAAY,MAAM;AAGjC,QAAI,KAAK,SAAS,UAAU,KAAK,SAAS,QAAQ;AAChD,WAAK,aAAa,SAAS,cAAc,QAAQ;AACjD,WAAK,WAAW,OAAO;AACvB,WAAK,WAAW,YAAY;AAC5B,WAAK,WAAW,QAAQ,SAAS;AACjC,WAAK,WAAW,aAAa,cAAc,WAAW;AACtD,WAAK,WAAW,YAAY;AAC5B,WAAK,WAAW,iBAAiB,SAAS,MAAM,KAAK,KAAK,CAAC;AAC3D,WAAK,UAAU,YAAY,KAAK,UAAU;AAAA,IAC5C;AAGA,QAAI,KAAK,SAAS,WAAW,KAAK,SAAS,QAAQ;AACjD,WAAK,cAAc,SAAS,cAAc,QAAQ;AAClD,WAAK,YAAY,OAAO;AACxB,WAAK,YAAY,YAAY;AAC7B,WAAK,YAAY,QAAQ,SAAS;AAClC,WAAK,YAAY,QAAQ,QAAQ;AACjC,WAAK,YAAY,aAAa,cAAc,iBAAiB,IAAI;AACjE,WAAK,YAAY,YAAY;AAC7B,WAAK,YAAY,iBAAiB,SAAS,MAAM;AAC/C,aAAK,KAAK,YAAY;AAAA,MACxB,CAAC;AACD,WAAK,UAAU,YAAY,KAAK,WAAW;AAAA,IAC7C;AAEA,WAAO,YAAY,KAAK,SAAS;AAAA,EACnC;AAAA,EAEQ,oBACN,WACA,eACQ;AACR,UAAM,UAAU,CAAC,oBAAoB;AACrC,QAAI,cAAc,QAAS,SAAQ,KAAK,kBAAkB;AAAA,aACjD,cAAc,OAAQ,SAAQ,KAAK,iBAAiB;AAE7D,UAAM,iBACJ,cAAc,aAAa,cAC3B,cAAc,aAAa,SAC3B,cAAc,aAAa;AAC7B,UAAM,gBACJ,cAAc,YAAY,cAC1B,cAAc,YAAY,SAC1B,cAAc,YAAY;AAE5B,QAAI,cAAc,WAAW,eAAgB,SAAQ,KAAK,yBAAyB;AAAA,aAC1E,cAAc,UAAU,cAAe,SAAQ,KAAK,wBAAwB;AAErF,WAAO,QAAQ,KAAK,GAAG;AAAA,EACzB;AAAA,EAEQ,yBAAyB,eAA0C;AACzE,QAAI,CAAC,KAAK,UAAW;AACrB,UAAM,EAAE,aAAa,WAAW,IAAI;AAEpC,UAAM,iBAAiB,aAAa,cAAc,aAAa,SAAS,aAAa;AACrF,UAAM,gBAAgB,YAAY,cAAc,YAAY,SAAS,YAAY;AAEjF,QAAI,kBAAkB,eAAe;AACnC,YAAM,OAAuC;AAAA,QAC3C,CAAC,sBAAsB,aAAa,UAAU;AAAA,QAC9C,CAAC,yBAAyB,aAAa,KAAK;AAAA,QAC5C,CAAC,0BAA0B,aAAa,MAAM;AAAA,QAC9C,CAAC,qBAAqB,YAAY,UAAU;AAAA,QAC5C,CAAC,wBAAwB,YAAY,KAAK;AAAA,QAC1C,CAAC,yBAAyB,YAAY,MAAM;AAAA,MAC9C;AACA,iBAAW,CAAC,MAAM,KAAK,KAAK,MAAM;AAChC,YAAI,MAAO,MAAK,UAAU,MAAM,YAAY,MAAM,KAAK;AAAA,MACzD;AAAA,IACF;AAEA,QAAI,eAAgB,MAAK,UAAU,QAAQ,iBAAiB;AAC5D,QAAI,cAAe,MAAK,UAAU,QAAQ,gBAAgB;AAAA,EAC5D;AAAA;AAAA,EAIQ,mBAAyB;AAC/B,QAAI,CAAC,KAAK,UAAW;AACrB,SAAK,UAAU,QAAQ,OAAO,OAAO,KAAK,UAAU,KAAK,OAAO;AAChE,SAAK,UAAU,QAAQ,WAAW,OAAO,KAAK,UAAU;AACxD,QAAI,KAAK,WAAW;AAClB,WAAK,UAAU,QAAQ,WAAW,OAAO,KAAK,UAAU;AAAA,IAC1D;AAAA,EACF;AAAA,EAEQ,wBAA8B;AACpC,QAAI,CAAC,KAAK,WAAY;AACtB,UAAM,YAAY,KAAK,UAAU,CAAC,KAAK;AACvC,SAAK,WAAW,WAAW;AAC3B,SAAK,WAAW,aAAa,cAAc,YAAY,iBAAiB,WAAW;AAEnF,QAAI,WAAW;AACb,WAAK,WAAW,YAAY;AAAA,IAC9B,OAAO;AACL,WAAK,WAAW,YAAY;AAAA,IAC9B;AAAA,EACF;AAAA,EAEQ,yBAA+B;AACrC,QAAI,CAAC,KAAK,YAAa;AACvB,UAAM,QAAQ,KAAK,aAAa;AAChC,SAAK,YAAY,QAAQ,QAAQ;AACjC,SAAK,YAAY,aAAa,cAAc,iBAAiB,KAAK,CAAC;AACnE,SAAK,YAAY,WAAW,UAAU;AACtC,SAAK,YAAY,YAAY,KAAK,kBAAkB,KAAK;AAAA,EAC3D;AAAA,EAEQ,kBAAkB,OAA2B;AACnD,QAAI,UAAU,cAAc;AAC1B,aAAO;AAAA,IACT;AACA,QAAI,UAAU,eAAe,UAAU,cAAc,UAAU,YAAY;AACzE,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,oBAA0B;AAChC,QAAI,CAAC,KAAK,iBAAiB,CAAC,KAAK,QAAS;AAC1C,WAAO,MAAM,2BAA2B,KAAK,aAAa;AAC1D,SAAK,OAAO,WAAW,KAAK,aAAa;AACzC,SAAK,gBAAgB;AAAA,EACvB;AAAA,EAEQ,kBAAwB;AAC9B,QAAI,KAAK,UAAU,KAAK,SAAS;AAC/B,WAAK,OAAO,UAAU;AAAA,IACxB;AAAA,EACF;AAAA,EAEQ,oBAA0B;AAChC,QAAI,KAAK,aAAa;AACpB,WAAK,uBAAuB,KAAK,YAAY,OAAO;AAAA,IACtD;AAAA,EACF;AAAA,EAEQ,uBAAuB,cAA6B;AAC1D,QAAI,CAAC,KAAK,QAAQ,cAAe;AACjC,UAAM,MAA6B;AAAA,MACjC,MAAM;AAAA,MACN,SAAS,EAAE,YAAY,aAAa;AAAA,IACtC;AACA,SAAK,OAAO,cAAc,YAAY,KAAK,KAAK,OAAO,gBAAgB,CAAC;AAAA,EAC1E;AAAA,EAEQ,kBAAwB;AAC9B,UAAM,QAAQ,KAAK,SAAS;AAC5B,eAAW,YAAY,KAAK,gBAAgB;AAC1C,UAAI;AACF,iBAAS,KAAK;AAAA,MAChB,SAAS,KAAK;AACZ,eAAO,KAAK,4BAA4B,GAAG;AAAA,MAC7C;AAAA,IACF;AAAA,EACF;AACF;;;ACv0BO,SAAS,iBAAiB,UAAqC;AACpE,MAAI,QAAkC;AAEtC,iBAAe,QAAQ,QAAQ,OAAmC;AAChE,QAAI,SAAS,CAAC,MAAO,QAAO;AAC5B,YAAQ,MAAM,QAAQ;AAAA,MACpB,SAAS,IAAI,OAAO,aAAa,EAAE,SAAS,OAAO,MAAM,QAAQ,SAAS,EAAE,EAAE;AAAA,IAChF;AACA,WAAO;AAAA,EACT;AAEA,iBAAe,YAA8C;AAE3D,UAAM,WAAW,MAAM,QAAQ,IAAI;AACnC,UAAM,QAA0B,CAAC;AACjC,UAAM,OAAO,oBAAI,IAAY;AAE7B,eAAW,EAAE,SAAS,OAAO,aAAa,KAAK,UAAU;AACvD,iBAAW,QAAQ,cAAc;AAC/B,YAAI,KAAK,IAAI,KAAK,IAAI,GAAG;AACvB,iBAAO;AAAA,YACL,wBAAwB,KAAK,IAAI,2DAA2D,QAAQ,MAAM,SAAS;AAAA,UACrH;AACA;AAAA,QACF;AACA,aAAK,IAAI,KAAK,IAAI;AAClB,cAAM,KAAK,IAAI;AAAA,MACjB;AAAA,IACF;AAEA,WAAO;AAAA,MACL,OAAO;AAAA,QACL;AAAA,QACA,SAAS,SAAS,IAAI,CAAC,EAAE,SAAS,OAAO,aAAa,GAAG,WAAW;AAAA,UAClE,IAAI,QAAQ,MAAM,WAAW,KAAK;AAAA,UAClC,OAAO,aAAa;AAAA,QACtB,EAAE;AAAA,MACJ;AAAA,IACF;AAAA,EACF;AAEA,QAAM,aAA8B,OAAO,MAAM,SAAS;AAExD,eAAW,WAAW,UAAU;AAC9B,UAAI,QAAQ,WAAW,IAAI,GAAG;AAC5B,eAAO,QAAQ,QAAQ,MAAM,IAAI;AAAA,MACnC;AAAA,IACF;AAEA,UAAM,WAAW,MAAM,QAAQ;AAC/B,eAAW,EAAE,SAAS,MAAM,KAAK,UAAU;AACzC,UAAI,QAAQ,SAAU;AACtB,UAAI,MAAM,KAAK,CAAC,SAAS,KAAK,SAAS,IAAI,GAAG;AAC5C,eAAO,QAAQ,QAAQ,MAAM,IAAI;AAAA,MACnC;AAAA,IACF;AACA,UAAM,IAAI,MAAM,iBAAiB,IAAI,EAAE;AAAA,EACzC;AAEA,SAAO,EAAE,WAAW,WAAW;AACjC;AAeA,eAAe,aACb,QACA,MACkB;AAClB,QAAM,UAAU,IAAI,QAAQ,IAAI;AAChC,QAAM,WAAW,OAAO,WAAW,aAAa,MAAM,OAAO,IAAI;AACjE,MAAI,UAAU;AACZ,eAAW,CAAC,KAAK,KAAK,KAAK,IAAI,QAAQ,QAAQ,GAAG;AAChD,cAAQ,IAAI,KAAK,KAAK;AAAA,IACxB;AAAA,EACF;AACA,SAAO;AACT;AAQO,SAAS,uBAAuB,SAAiC,CAAC,GAAgB;AACvF,QAAM,WAAW,OAAO,YAAY;AAEpC,SAAO;AAAA,IACL,IAAI,OAAO,MAAM;AAAA,IACjB,UAAU,YAAY;AACpB,YAAM,MAAM,MAAM,MAAM,UAAU,EAAE,SAAS,MAAM,aAAa,OAAO,OAAO,EAAE,CAAC;AACjF,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI,MAAM,6BAA6B,QAAQ,KAAK,IAAI,MAAM,GAAG;AAAA,MACzE;AACA,YAAM,aAAc,MAAM,IAAI,KAAK;AACnC,aAAO,WAAW,OAAO,SAAS,CAAC;AAAA,IACrC;AAAA,IACA,SAAS,OAAO,MAAM,SAAS;AAC7B,YAAM,MAAM,MAAM,MAAM,UAAU;AAAA,QAChC,QAAQ;AAAA,QACR,SAAS,MAAM,aAAa,OAAO,SAAS,EAAE,gBAAgB,mBAAmB,CAAC;AAAA,QAClF,MAAM,KAAK,UAAU,EAAE,MAAM,KAAK,CAAC;AAAA,MACrC,CAAC;AACD,YAAM,OAAQ,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAK/C,UAAI,CAAC,IAAI,MAAM,CAAC,KAAK,IAAI;AACvB,cAAM,IAAI,MAAM,KAAK,SAAS,SAAS,IAAI,aAAa,IAAI,MAAM,GAAG;AAAA,MACvE;AACA,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACF;",
6
6
  "names": ["window", "body"]
7
7
  }