{"version":3,"file":"webchat-CUUzLbMs.mjs","names":["Base","options: any","params: Record<string, string | boolean | number | undefined>","config: IWebchatConfiguration","evt: Websocket.MessageEvent","d: any","stop?: boolean","sampleRate?: number","e: MessageEvent","constraints: MediaStreamConstraints","f32: Float32Array","u8: Uint8Array"],"sources":["../src/client/webchat/webchat.ts"],"sourcesContent":["import {\n  IWebchatConfiguration,\n  IWebchatState,\n  IWebchatWSSettings,\n} from \"../../types/webchat\";\nimport Base from \"../base\";\nimport Websocket from \"isomorphic-ws\";\n\ninterface IWebchatPublic {\n  state: IWebchatState;\n  start: (config: IWebchatConfiguration) => Promise<Websocket>;\n  stop: () => void;\n}\n\nclass Webchat extends Base implements IWebchatPublic {\n  state: IWebchatState;\n  private connectionUrl: string | null = null;\n  private websocket: Websocket | null = null;\n  private wsSettings: IWebchatWSSettings;\n  private lastActivityAt: number | null = null;\n  private keepAliveInterval: ReturnType<typeof setInterval> | null = null;\n\n  private audioContext: AudioContext | null = null;\n  private micStream: MediaStream | null = null;\n  private sourceNode: MediaStreamAudioSourceNode | null = null;\n  private micNode: AudioWorkletNode | null = null;\n  private sinkGain: GainNode | null = null;\n\n  private playbackLead = 0.15;\n  private nextPlaybackTime: number | null = null;\n  private pendingSources: AudioBufferSourceNode[] = [];\n  private downstreamSampleRate: number | null = null;\n\n  constructor(options: any) {\n    super(options);\n    this.endpoint =\n      options?.endpoint ||\n      \"wss://stream-v2.aws.dc8.bland.ai/ws/connect/blandshared\";\n    this.connectionUrl = null;\n    this.wsSettings = { interval: 15000, timeout: 10000 };\n    this.state = \"closed\";\n  }\n\n  private params(\n    params: Record<string, string | boolean | number | undefined>\n  ): string {\n    const q = new URLSearchParams();\n    for (const [k, v] of Object.entries(params)) {\n      if (v !== undefined && v !== null) q.append(k, String(v));\n    }\n    return q.toString();\n  }\n\n  public async start(config: IWebchatConfiguration): Promise<Websocket> {\n    if (this.state !== \"closed\") throw new Error(\"Webchat already started\");\n    this.state = \"connecting\";\n\n    if (!config.sessionId) {\n      this.state = \"closed\";\n      throw new Error(\n        \"A session token is required to start the webchat. Please use the admin client server-side to generate a session token.\"\n      );\n    }\n\n    if (!config.agentId) {\n      this.state = \"closed\";\n      throw new Error(\n        \"An agent ID is required to start the webchat. Please create an agent in the dashboard, and use the agent ID here.\"\n      );\n    }\n\n    const wssUrl = `${this.endpoint}?${this.params({\n      agent: config.agentId,\n      token: config.sessionId,\n    })}`;\n\n    this.connectionUrl = wssUrl;\n    this.websocket = this.create();\n    this.websocket.binaryType = \"arraybuffer\";\n\n    this.websocket.onopen = async (): Promise<void> => {\n      try {\n        await this.initAudioWorklet((config as any)?.sampleRate);\n        this.downstreamSampleRate =\n          (config as any)?.playbackSampleRate ||\n          (config as any)?.ttsSampleRate ||\n          (config as any)?.sampleRate ||\n          null;\n        this.state = \"open\";\n        this.keepAlive();\n      } catch (e) {\n        this.state = \"closed\";\n        this.stop();\n        throw e;\n      }\n    };\n\n    this.websocket.onmessage = (evt: Websocket.MessageEvent): void => {\n      this.setActivity();\n      const d: any = (evt as any).data;\n      if (typeof d === \"string\") {\n        try {\n          const m = JSON.parse(d);\n          const r =\n            m.playbackSampleRate ??\n            m.sample_rate ??\n            m.sampleRate ??\n            m.pcm_sample_rate;\n          if (typeof r === \"number\") this.downstreamSampleRate = r;\n        } catch {}\n        return;\n      }\n      if (d instanceof ArrayBuffer) {\n        this.playPcm(new Uint8Array(d));\n        return;\n      }\n      if (d && d.buffer instanceof ArrayBuffer) {\n        this.playPcm(new Uint8Array(d.buffer));\n      }\n    };\n\n    this.websocket.onclose = (): void => {\n      this.keepAlive(true);\n      this.teardownAudio();\n      this.state = \"closed\";\n    };\n\n    this.websocket.onerror = (): void => {\n      this.keepAlive(true);\n      this.teardownAudio();\n      this.state = \"closed\";\n    };\n\n    return this.websocket as Websocket;\n  }\n\n  private create(): Websocket {\n    if (!this.connectionUrl) {\n      this.state = \"closed\";\n      throw new Error(\n        \"Connection URL is not set. Please call start() before creating the websocket connection.\"\n      );\n    }\n\n    this.websocket = new Websocket(this.connectionUrl || \"\");\n    return this.websocket;\n  }\n\n  public stop(): void {\n    if (\n      this.websocket &&\n      (this.websocket.readyState === this.websocket.OPEN ||\n        this.websocket.readyState === this.websocket.CONNECTING)\n    ) {\n      this.state = \"closing\";\n      this.websocket.close();\n    }\n    this.keepAlive(true);\n    this.teardownAudio();\n  }\n\n  private keepAlive(stop?: boolean): void {\n    if (stop) {\n      if (this.keepAliveInterval) clearInterval(this.keepAliveInterval);\n      this.keepAliveInterval = null;\n      this.lastActivityAt = null;\n    } else {\n      this.lastActivityAt = Date.now();\n      if (this.keepAliveInterval) clearInterval(this.keepAliveInterval);\n      this.keepAliveInterval = setInterval((): void => {\n        if (\n          this.lastActivityAt &&\n          Date.now() - this.lastActivityAt > this.wsSettings.timeout\n        ) {\n          this.stop();\n          this.state = \"closed\";\n        }\n      }, this.wsSettings.interval);\n    }\n  }\n\n  private setActivity(): void {\n    this.lastActivityAt = Date.now();\n  }\n\n  private async initAudioWorklet(sampleRate?: number): Promise<void> {\n    this.audioContext = new AudioContext(\n      sampleRate\n        ? { sampleRate, latencyHint: \"interactive\" }\n        : { latencyHint: \"interactive\" }\n    );\n    const code = `\n      class MicCaptureProcessor extends AudioWorkletProcessor {\n        constructor() {\n          super();\n          this._frame = Math.max(128, Math.floor(sampleRate * 0.02));\n          this._buf = new Float32Array(this._frame);\n          this._off = 0;\n        }\n        process(inputs) {\n          const ch = inputs[0] && inputs[0][0];\n          if (ch) {\n            let i = 0;\n            while (i < ch.length) {\n              const space = this._frame - this._off;\n              const toCopy = Math.min(space, ch.length - i);\n              this._buf.set(ch.subarray(i, i + toCopy), this._off);\n              this._off += toCopy;\n              i += toCopy;\n              if (this._off >= this._frame) {\n                const copy = new Float32Array(this._buf);\n                this.port.postMessage(copy.buffer, [copy.buffer]);\n                this._off = 0;\n              }\n            }\n          }\n          return true;\n        }\n      }\n      registerProcessor('mic-capture', MicCaptureProcessor)\n    `;\n    const blob = new Blob([code], { type: \"application/javascript\" });\n    const url = URL.createObjectURL(blob);\n    await this.audioContext.audioWorklet.addModule(url);\n    URL.revokeObjectURL(url);\n\n    this.micNode = new AudioWorkletNode(this.audioContext, \"mic-capture\");\n    this.micNode.port.onmessage = (e: MessageEvent): void => {\n      if (!this.websocket || this.websocket.readyState !== this.websocket.OPEN)\n        return;\n      const f32 = new Float32Array(e.data as ArrayBuffer);\n      const pcm = this.float32ToPCM16(f32);\n      this.websocket.send(pcm);\n    };\n\n    const constraints: MediaStreamConstraints = {\n      audio: {\n        sampleRate: sampleRate,\n        echoCancellation: true,\n        noiseSuppression: true,\n        channelCount: 1,\n      },\n    };\n    this.micStream = await navigator.mediaDevices.getUserMedia(constraints);\n    this.sourceNode = this.audioContext.createMediaStreamSource(this.micStream);\n    this.sinkGain = this.audioContext.createGain();\n    this.sinkGain.gain.value = 0;\n\n    this.sourceNode.connect(this.micNode);\n    this.micNode.connect(this.sinkGain);\n    this.sinkGain.connect(this.audioContext.destination);\n    await this.audioContext.resume();\n  }\n\n  private teardownAudio(): void {\n    if (this.pendingSources.length) {\n      try {\n        this.pendingSources.forEach((s) => {\n          try {\n            s.stop();\n          } catch {}\n          try {\n            s.disconnect();\n          } catch {}\n        });\n      } catch {}\n      this.pendingSources = [];\n    }\n    this.nextPlaybackTime = null;\n    if (this.micNode) {\n      try {\n        this.micNode.disconnect();\n      } catch {}\n      this.micNode.port.onmessage = null as any;\n      this.micNode = null;\n    }\n    if (this.sourceNode) {\n      try {\n        this.sourceNode.disconnect();\n      } catch {}\n      this.sourceNode = null;\n    }\n    if (this.sinkGain) {\n      try {\n        this.sinkGain.disconnect();\n      } catch {}\n      this.sinkGain = null;\n    }\n    if (this.micStream) {\n      this.micStream.getTracks().forEach((t) => t.stop());\n      this.micStream = null;\n    }\n    if (this.audioContext) {\n      this.audioContext.close();\n      this.audioContext = null;\n    }\n  }\n\n  private float32ToPCM16(f32: Float32Array): Uint8Array {\n    const out = new Int16Array(f32.length);\n    for (let i = 0; i < f32.length; i++) {\n      const s = Math.max(-1, Math.min(1, f32[i]));\n      out[i] = s < 0 ? s * 0x8000 : s * 0x7fff;\n    }\n    return new Uint8Array(out.buffer);\n  }\n\n  private pcm16ToFloat32(u8: Uint8Array): Float32Array {\n    const dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);\n    const len = u8.byteLength / 2;\n    const f32 = new Float32Array(len);\n    for (let i = 0; i < len; i++) {\n      const s = dv.getInt16(i * 2, true);\n      f32[i] = s / 0x8000;\n    }\n    return f32;\n  }\n\n  private playPcm(u8: Uint8Array): void {\n    if (!this.audioContext) return;\n    const f32 = this.pcm16ToFloat32(u8);\n    const sr = this.downstreamSampleRate || this.audioContext.sampleRate;\n    const buffer = this.audioContext.createBuffer(1, f32.length, sr);\n    buffer.copyToChannel(f32, 0, 0);\n    const src = this.audioContext.createBufferSource();\n    src.buffer = buffer;\n    src.connect(this.audioContext.destination);\n    const now = this.audioContext.currentTime;\n    if (this.nextPlaybackTime === null || this.nextPlaybackTime < now + 0.005) {\n      this.nextPlaybackTime = now + this.playbackLead;\n    }\n    src.start(this.nextPlaybackTime);\n    this.pendingSources.push(src);\n    this.nextPlaybackTime += buffer.duration;\n    src.onended = () => {\n      const i = this.pendingSources.indexOf(src);\n      if (i >= 0) this.pendingSources.splice(i, 1);\n      try {\n        src.disconnect();\n      } catch {}\n    };\n  }\n}\n\nexport type { IWebchatPublic };\nexport default Webchat;\n"],"mappings":"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"}