{"version":3,"file":"index.cjs","sources":["../src/ponderweb.js"],"sourcesContent":["/* ponder-sdk.js  —  Pure-JS voice-agent client (ES module) */\n\nexport default class Ponder {\n    /**\n     * Create a new Ponder client instance.\n     * @param {Object}   cfg\n     * @param {string}   cfg.assistantId\n     * @param {string}   cfg.host                    http(s)://…\n     * @param {Array}    [cfg.actions=[]]            [{ name, description, arguments:[{name,type,description,required}] , function }]\n     * @param {string}   [cfg.instructions=\"\"]\n     */\n    constructor({\n        assistantId,\n        host,\n        actions = [],\n        instructions = \"\"\n    }) {\n        this.assistantId = assistantId;\n        this.rawHost = host;\n        this.actions = actions;\n        this.instructions = instructions;\n\n        this.onSpeechStart = (() => { });\n        this.onSpeechEnd = (() => { });\n        this.onMessage = (() => { });\n        this.onConnected = (() => { });\n        this.onDisconnected = (() => { });\n        this.onFunctionCall = ((name, args) => { });\n\n        // internals\n        this._audioCtx = null;\n        this._mediaStream = null;\n        this._mediaSource = null;\n        this._workletNode = null;\n        this._ws = null;\n\n        this._messages = [];\n        this._isUserSpeaking = false;\n        this._activeAudioSources = [];\n        this._nextTime = null;\n        this._blockAudio = false;\n        this._onConnectedInternal = (() => { });\n        this._onDisconnectedInternal = (() => { });\n        this._onMessageInternal = (() => { });\n    }\n\n\n    /* ───────────────────────── PUBLIC API ────────────────────────── */\n\n    /**\n     * Start the voice session.\n     * If already connected, does nothing.\n     * @returns {Promise<void>}\n     */\n    async connect() {\n        if (this._ws || this._audioCtx) return;                       // already connected\n\n        this._audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16_000 });\n        await this.#_startMicStreaming();\n\n        const wsUrl = this.#_toWsUrl(this.rawHost) + \"/ws/talk\";\n        this._ws = new WebSocket(wsUrl);\n\n        this._ws.onopen = () => this.#_initSession();\n        this._ws.onclose = () => {this.disconnect().then(() => {\n            this._onDisconnectedInternal();\n            this.onDisconnected();\n        })\n        };\n\n        this._ws.onerror = () => console.error(\"Error connecting Ponder\");\n        this._ws.onmessage = (evt) => this.#_handleWsMessage(JSON.parse(evt.data));\n    }\n\n    /**\n     * Stop the voice session and release all resources.\n     * @returns {Promise<void>}\n     */\n    async disconnect() {\n        // stop mic & worklet\n        if (this._workletNode) {\n            this._workletNode.port.onmessage = null;\n            this._workletNode.disconnect();\n            this._workletNode = null;\n        }\n        if (this._mediaSource) { this._mediaSource.disconnect(); this._mediaSource = null; }\n        if (this._mediaStream) { this._mediaStream.getTracks().forEach(t => t.stop()); this._mediaStream = null; }\n\n        // audio nodes playing TTS\n        this._activeAudioSources.forEach(src => src.stop());\n        this._activeAudioSources = [];\n        this._nextTime = null;\n\n        if (this._audioCtx && this._audioCtx.state !== \"closed\") {\n            try { await this._audioCtx.close(); } catch { }\n        }\n        this._audioCtx = null;\n\n        // websocket\n        if (this._ws && (this._ws.readyState === WebSocket.OPEN || this._ws.readyState === WebSocket.CONNECTING)) {\n            try {\n                this._ws.close();\n            } catch { }\n        }\n        this._ws = null;\n    }\n\n    /**\n     * Dynamically update the available actions (tools) after construction.\n     * @param {Array} actions - Array of action definitions. format: [{ name, description, arguments: [{name, type, description, required}] }]\n     */\n    updateActions(actions) {\n        this.actions = actions;\n        if (this._ws && this._ws.readyState === WebSocket.OPEN) {\n            this._ws.send(JSON.stringify({ type: \"update_tools\", tools: this.#_actionsToTools(actions) }));\n        }\n    }\n\n    /**\n     * Dynamically update the instructions for the assistant after construction.\n     * @param {string} instr - New instructions.\n     */\n    updateInstructions(instr) {\n        this.instructions = instr;\n        if (this._ws && this._ws.readyState === WebSocket.OPEN) {\n            this._ws.send(JSON.stringify({ type: \"update_instructions\", instructions: instr }));\n        }\n    }\n\n    /**\n     * Send a text message to the assistant (chat input).\n     * @param {string} text - The text message to send.\n     */\n    sendTextMessage(text) {\n        if (!text || !text.trim()) return;\n        this.#_stopTtsPlayback();\n        const userMessage = {\n            role: \"user\",\n            text: text.trim(),\n            type: \"text\"\n        };\n        this._messages.push(userMessage);\n        if (this._ws && this._ws.readyState === WebSocket.OPEN) {\n            this._ws.send(JSON.stringify({\n                type: \"text_input\",\n                messages: this._messages\n            }));\n        }\n    }\n\n    /* ────────────────────── INTERNAL UTILITIES ───────────────────── */\n\n    #_toWsUrl(url) {\n        // Remove trailing slash if present\n        let cleanUrl = url.endsWith('/') ? url.slice(0, -1) : url;\n        if (cleanUrl.startsWith(\"https://\")) return \"wss://\" + cleanUrl.slice(8);\n        if (cleanUrl.startsWith(\"http://\")) return \"ws://\" + cleanUrl.slice(7);\n        return cleanUrl; // already ws:// or wss://\n    }\n\n    #_actionsToTools(actions) {\n        return actions.map(a => ({\n            type: \"function\",\n            function: {\n                name: a.name,\n                description: a.description,\n                parameters: {\n                    type: \"object\",\n                    required: (a.arguments ?? []).filter(arg => arg.required).map(arg => arg.name),\n                    additionalProperties: false,\n                    properties: Object.fromEntries(\n                        (a.arguments ?? []).map(arg => [arg.name, { type: arg.type, description: arg.description }])\n                    )\n                }\n            }\n        }));\n    }\n\n    async #_startMicStreaming() {\n        /* 1. getUserMedia */\n        this._mediaStream = await navigator.mediaDevices.getUserMedia({\n            audio: { channelCount: 1, echoCancellation: true, autoGainControl: true, noiseSuppression: true }\n        });\n        this._mediaSource = this._audioCtx.createMediaStreamSource(this._mediaStream);\n\n        /* 2. create AudioWorklet that forwards Float32 chunks to JS main thread */\n        const workletURL = URL.createObjectURL(new Blob([`\n        class PonderMicProcessor extends AudioWorkletProcessor {\n          process(inputs) {\n            if (inputs[0] && inputs[0][0]) this.port.postMessage(inputs[0][0]);\n            return true;\n          }\n        }\n        registerProcessor('ponder-mic-processor', PonderMicProcessor);\n      `], { type: \"application/javascript\" }));\n        await this._audioCtx.audioWorklet.addModule(workletURL);\n        URL.revokeObjectURL(workletURL);\n\n        this._workletNode = new AudioWorkletNode(this._audioCtx, 'ponder-mic-processor');\n        this._workletNode.port.onmessage = ({ data }) => {\n            if (this._ws?.readyState === WebSocket.OPEN && !this._blockAudio) {\n                this._ws.send(JSON.stringify({\n                    type: \"audio_input\",\n                    audio: Array.from(data),\n                    messages: this._messages,\n                }));\n            }\n        };\n\n        /* avoid feedback */\n        const silentGain = this._audioCtx.createGain();\n        silentGain.gain.value = 0;\n\n        this._mediaSource.connect(this._workletNode);\n        this._workletNode.connect(silentGain);\n        silentGain.connect(this._audioCtx.destination);\n    }\n\n    #_initSession() {\n        this._ws.send(JSON.stringify({\n            type: \"init_session\",\n            assistant_id: this.assistantId,\n            tools: this.#_actionsToTools(this.actions),\n            instructions: this.instructions\n        }));\n    }\n\n    #_handleWsMessage(evt) {\n        switch (evt.type) {\n            case \"session_initialized\":\n                this._onConnectedInternal();\n                this.onConnected();\n                break;\n\n            case \"text\":\n                this._messages.push(evt);\n                this._onMessageInternal(evt);\n                this.onMessage(evt);\n                break;\n\n            case \"speech_start\":\n                this._isUserSpeaking = true;\n                this.#_stopTtsPlayback();\n                this.onSpeechStart();\n                break;\n\n            case \"speech_end\":\n                this._isUserSpeaking = false;\n                this.onSpeechEnd();\n                break;\n\n            case \"audio\":\n                if (!this._isUserSpeaking && this._audioCtx) {\n                    const pcm = Uint8Array.from(atob(evt.audio), c => c.charCodeAt(0));\n                    this.#_playPcmChunk(pcm);\n                }\n                break;\n\n            case \"function_call\":\n                this._messages.push(evt);\n                this.#_invokeFunction(evt.function_calls[0]);\n                break;\n        }\n    }\n\n    #_playPcmChunk(u8) {\n        if (u8.length % 2) return;\n        const float32 = new Float32Array(new Int16Array(u8.buffer)).map(v => v / 32768);\n        const buf = this._audioCtx.createBuffer(1, float32.length, this._audioCtx.sampleRate);\n        buf.copyToChannel(float32, 0);\n\n        const src = this._audioCtx.createBufferSource();\n        src.buffer = buf;\n        src.connect(this._audioCtx.destination);\n\n        const startAt = (this._nextTime === null || this._nextTime < this._audioCtx.currentTime)\n            ? this._audioCtx.currentTime\n            : this._nextTime;\n\n        src.start(startAt);\n        this._nextTime = startAt + buf.duration;\n        this._activeAudioSources.push(src);\n    }\n\n    #_stopTtsPlayback() {\n        this._activeAudioSources.forEach(s => s.stop());\n        this._activeAudioSources = [];\n        this._nextTime = null;\n    }\n\n    async #_invokeFunction(call) {\n        let response = \"function not found\";\n        this._blockAudio = true;\n        try{\n            response = await this.onFunctionCall(call.name, JSON.parse(call.arguments));\n            if (response === undefined) {\n                response = \"success\";\n            }\n            console.log(\"Function response:\", response);\n        }\n        catch (e) {\n            response = \"error calling function\";\n            console.error(e);\n        }\n\n        this._messages.push({ role: \"tool\", type: \"function_response\", response, call_id: call.call_id });\n        if (this._ws?.readyState === WebSocket.OPEN) {\n            this._ws.send(JSON.stringify({ type: \"function_response\", messages: this._messages }));\n            this._blockAudio = false;\n        }\n    }\n}\n"],"names":["constructor","assistantId","host","actions","instructions","this","rawHost","onSpeechStart","onSpeechEnd","onMessage","onConnected","onDisconnected","onFunctionCall","name","args","_audioCtx","_mediaStream","_mediaSource","_workletNode","_ws","_messages","_isUserSpeaking","_activeAudioSources","_nextTime","_blockAudio","_onConnectedInternal","_onDisconnectedInternal","_onMessageInternal","connect","window","AudioContext","webkitAudioContext","sampleRate","_startMicStreaming","wsUrl","_toWsUrl","WebSocket","onopen","_initSession","onclose","disconnect","then","onerror","console","error","onmessage","evt","_handleWsMessage","JSON","parse","data","port","getTracks","forEach","t","stop","src","state","close","readyState","OPEN","CONNECTING","updateActions","send","stringify","type","tools","_actionsToTools","updateInstructions","instr","sendTextMessage","text","trim","_stopTtsPlayback","userMessage","role","push","messages","url","cleanUrl","endsWith","slice","startsWith","map","a","function","description","parameters","required","arguments","filter","arg","additionalProperties","properties","Object","fromEntries","navigator","mediaDevices","getUserMedia","audio","channelCount","echoCancellation","autoGainControl","noiseSuppression","createMediaStreamSource","workletURL","URL","createObjectURL","Blob","audioWorklet","addModule","revokeObjectURL","AudioWorkletNode","Array","from","silentGain","createGain","gain","value","destination","assistant_id","pcm","Uint8Array","atob","c","charCodeAt","_playPcmChunk","_invokeFunction","function_calls","u8","length","float32","Float32Array","Int16Array","buffer","v","buf","createBuffer","copyToChannel","createBufferSource","startAt","currentTime","start","duration","s","call","response","undefined","log","e","call_id"],"mappings":"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"}