@ponderai/web
Version:
Ponder Web SDK – easiest way to build voice AI agents for the web
1 lines • 18.4 kB
Source Map (JSON)
{"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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"}