@myned-ai/avatar-chat-widget 0.10.0 → 0.12.0

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.
@@ -137,6 +137,11 @@ declare class AvatarChatElement extends HTMLElement {
137
137
  * Send message programmatically
138
138
  */
139
139
  sendMessage(text: string): void;
140
+ /**
141
+ * Expose triggerAction for client-side Actions debugging
142
+ * Dispatches a custom nyxAction event as if the server triggered it
143
+ */
144
+ triggerAction(function_name: string, args?: Record<string, any>): void;
140
145
  /**
141
146
  * Check if mounted
142
147
  */
@@ -546,6 +546,7 @@ class AvatarProtocolClient extends EventEmitter {
546
546
  this.socket.on("transcript_done", (msg) => this.handleTranscriptDone(msg));
547
547
  this.socket.on("interrupt", (msg) => this.handleInterrupt(msg));
548
548
  this.socket.on("avatar_state", (msg) => this.emit("avatar_state", msg));
549
+ this.socket.on("trigger_action", (msg) => this.emit("trigger_action", msg));
549
550
  this.socket.on("pong", (msg) => log$h.debug("Pong received", msg));
550
551
  }
551
552
  // ------------------------------------------------------------------
@@ -1789,7 +1790,15 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1789
1790
  return _AudioContextManagerImpl._instance;
1790
1791
  }
1791
1792
  /**
1792
- * Get or create the shared AudioContext
1793
+ * Set the sample rate for when the AudioContext is eventually created.
1794
+ * Call this early (e.g., from server config) before any audio session starts.
1795
+ */
1796
+ setSampleRate(sampleRate) {
1797
+ this._sampleRate = sampleRate;
1798
+ }
1799
+ /**
1800
+ * Get the shared AudioContext. Creates lazily if needed.
1801
+ * Prefer ensureAudioReady() from user-gesture handlers to guarantee iOS unlock.
1793
1802
  * @param sampleRate Optional sample rate (only used on first creation)
1794
1803
  */
1795
1804
  getContext(sampleRate) {
@@ -1799,6 +1808,27 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1799
1808
  if (sampleRate) {
1800
1809
  this._sampleRate = sampleRate;
1801
1810
  }
1811
+ return this.createContext();
1812
+ }
1813
+ /**
1814
+ * Single idempotent entry point: create (if needed) + resume AudioContext.
1815
+ * Call this synchronously from any user-gesture handler (pointerdown, touchend,
1816
+ * click, keydown) to guarantee iOS audio unlock.
1817
+ */
1818
+ ensureAudioReady() {
1819
+ if (!this._context) {
1820
+ this.createContext();
1821
+ }
1822
+ if (this._context && this._context.state !== "running") {
1823
+ this.resume().catch((error2) => {
1824
+ log$c.warn("ensureAudioReady resume failed (will retry on next gesture):", error2);
1825
+ });
1826
+ }
1827
+ }
1828
+ /**
1829
+ * Internal: create the AudioContext and wire up fallback document listeners.
1830
+ */
1831
+ createContext() {
1802
1832
  try {
1803
1833
  const AudioContextClass = window.AudioContext || window.webkitAudioContext;
1804
1834
  if (!AudioContextClass) {
@@ -1807,12 +1837,14 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1807
1837
  this._context = new AudioContextClass({
1808
1838
  sampleRate: this._sampleRate,
1809
1839
  latencyHint: "interactive"
1810
- // Optimize for real-time
1811
1840
  });
1812
1841
  log$c.info(`AudioContext created: sampleRate=${this._context.sampleRate}, state=${this._context.state}`);
1813
1842
  this.setupResumeListener();
1814
1843
  this._context.onstatechange = () => {
1815
1844
  log$c.debug(`AudioContext state changed: ${this._context?.state}`);
1845
+ if (this._context?.state === "running") {
1846
+ this.removeResumeListeners();
1847
+ }
1816
1848
  };
1817
1849
  } catch (error2) {
1818
1850
  errorBoundary.handleError(error2, "audio-context-manager");
@@ -1821,26 +1853,29 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1821
1853
  return this._context;
1822
1854
  }
1823
1855
  /**
1824
- * Setup a single click listener to resume AudioContext (browser policy)
1825
- * This replaces multiple listeners scattered across services
1856
+ * Fallback document-level listeners for audio unlock.
1857
+ * These are a safety net primary unlock happens via ensureAudioReady()
1858
+ * called from widget gesture handlers.
1826
1859
  */
1827
1860
  setupResumeListener() {
1828
1861
  if (this._isResumeListenerAdded) {
1829
1862
  return;
1830
1863
  }
1831
- this._listenerEvents = ["click", "touchstart", "keydown"];
1832
- this._interactionHandler = async () => {
1833
- this.removeResumeListeners();
1834
- await this.resume();
1864
+ this._listenerEvents = ["pointerdown", "touchend", "keydown"];
1865
+ this._interactionHandler = () => {
1866
+ if (this._context && this._context.state !== "running") {
1867
+ this._context.resume().catch(() => {
1868
+ });
1869
+ }
1835
1870
  };
1836
1871
  this._listenerEvents.forEach((event) => {
1837
- document.addEventListener(event, this._interactionHandler, { once: true, passive: true });
1872
+ document.addEventListener(event, this._interactionHandler, { passive: true });
1838
1873
  });
1839
1874
  this._isResumeListenerAdded = true;
1840
- log$c.debug("Audio resume listeners added");
1875
+ log$c.debug("Audio resume fallback listeners added");
1841
1876
  }
1842
1877
  /**
1843
- * Remove resume listeners (called after first interaction or on cleanup)
1878
+ * Remove resume listeners (only after context is running, or on cleanup)
1844
1879
  */
1845
1880
  removeResumeListeners() {
1846
1881
  if (this._interactionHandler) {
@@ -1849,6 +1884,8 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1849
1884
  });
1850
1885
  this._interactionHandler = null;
1851
1886
  this._listenerEvents = [];
1887
+ this._isResumeListenerAdded = false;
1888
+ log$c.debug("Audio resume fallback listeners removed");
1852
1889
  }
1853
1890
  }
1854
1891
  /**
@@ -1869,6 +1906,7 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1869
1906
  try {
1870
1907
  await this._context.resume();
1871
1908
  log$c.info("AudioContext resumed successfully");
1909
+ this.removeResumeListeners();
1872
1910
  } catch (error2) {
1873
1911
  log$c.error("Failed to resume AudioContext:", error2);
1874
1912
  errorBoundary.handleError(error2, "audio-context-manager");
@@ -1997,7 +2035,7 @@ class AudioOutput {
1997
2035
  this.chunkDurationMs = 100;
1998
2036
  this.audioBuffer = new CircularBuffer(CONFIG.audio.output.maxBufferFrames);
1999
2037
  this.adaptiveBuffer = new AdaptiveBuffer(100, 50, 500);
2000
- AudioContextManager.getContext(this.sampleRate);
2038
+ AudioContextManager.setSampleRate(this.sampleRate);
2001
2039
  }
2002
2040
  /**
2003
2041
  * Get the shared AudioContext
@@ -2010,6 +2048,7 @@ class AudioOutput {
2010
2048
  */
2011
2049
  setDefaultSampleRate(sampleRate) {
2012
2050
  this.sampleRate = sampleRate;
2051
+ AudioContextManager.setSampleRate(sampleRate);
2013
2052
  log$b.info(`Output sample rate set to ${sampleRate}Hz`);
2014
2053
  }
2015
2054
  startSession(sessionId, sampleRate) {
@@ -2023,6 +2062,7 @@ class AudioOutput {
2023
2062
  this.isPlaying = false;
2024
2063
  this.nextPlayTime = 0;
2025
2064
  AudioContextManager.resume().catch(() => {
2065
+ log$b.warn("AudioContext resume failed in startSession — will retry on next user gesture");
2026
2066
  });
2027
2067
  }
2028
2068
  addAudioChunk(data, timestamp) {
@@ -2534,7 +2574,7 @@ class SyncPlayback {
2534
2574
  this.floatBuffer = new Float32Array(this.maxSamplesPerFrame);
2535
2575
  this.visibilityHandler = this.handleVisibilityChange.bind(this);
2536
2576
  document.addEventListener("visibilitychange", this.visibilityHandler);
2537
- AudioContextManager.getContext(this.sampleRate);
2577
+ AudioContextManager.setSampleRate(this.sampleRate);
2538
2578
  }
2539
2579
  /**
2540
2580
  * Get the shared AudioContext
@@ -2571,6 +2611,7 @@ class SyncPlayback {
2571
2611
  */
2572
2612
  setDefaultSampleRate(sampleRate) {
2573
2613
  this.sampleRate = sampleRate;
2614
+ AudioContextManager.setSampleRate(sampleRate);
2574
2615
  log$8.info(`SyncPlayback sample rate set to ${sampleRate}Hz`);
2575
2616
  }
2576
2617
  startSession(sessionId, sampleRate) {
@@ -2584,6 +2625,7 @@ class SyncPlayback {
2584
2625
  this.sampleRate = sampleRate;
2585
2626
  }
2586
2627
  AudioContextManager.resume().catch(() => {
2628
+ log$8.warn("AudioContext resume failed in startSession — will retry on next user gesture");
2587
2629
  });
2588
2630
  try {
2589
2631
  const ctx = this.audioContext;
@@ -3922,11 +3964,19 @@ class ChatManager {
3922
3964
  this.protocolClient.on("interrupt", (event) => {
3923
3965
  this.handleInterrupt(event);
3924
3966
  });
3967
+ this.protocolClient.on("trigger_action", (event) => {
3968
+ this.handleTriggerAction(event);
3969
+ });
3925
3970
  this.protocolClient.on("error", (err) => log$4.error("Protocol Error:", err));
3926
3971
  }
3927
3972
  // ============================================================================
3928
3973
  // Protocol Handlers
3929
3974
  // ============================================================================
3975
+ handleTriggerAction(event) {
3976
+ log$4.info(`🎯 Action Triggered: ${event.function_name}`, event.arguments);
3977
+ const customEvent = new CustomEvent("nyxAction", { detail: event });
3978
+ window.dispatchEvent(customEvent);
3979
+ }
3930
3980
  handleAudioStart(event) {
3931
3981
  this.setTyping(false);
3932
3982
  if (this.syncFramesBeforeStart > 0) {
@@ -5875,6 +5925,7 @@ class AvatarChatElement extends HTMLElement {
5875
5925
  }
5876
5926
  });
5877
5927
  chatInput.addEventListener("focus", () => {
5928
+ AudioContextManager.ensureAudioReady();
5878
5929
  const root = this.shadow.querySelector(".widget-root");
5879
5930
  if (root) root.classList.add("input-focused");
5880
5931
  });
@@ -5905,6 +5956,7 @@ class AvatarChatElement extends HTMLElement {
5905
5956
  const handleChipClick = (e) => {
5906
5957
  const target = e.target;
5907
5958
  if (target.classList.contains("suggestion-chip")) {
5959
+ AudioContextManager.ensureAudioReady();
5908
5960
  this.markHasMessages();
5909
5961
  hideSuggestions();
5910
5962
  const text = target.textContent;
@@ -5918,11 +5970,13 @@ class AvatarChatElement extends HTMLElement {
5918
5970
  quickReplies?.addEventListener("click", handleChipClick);
5919
5971
  avatarSuggestions?.addEventListener("click", handleChipClick);
5920
5972
  micBtn?.addEventListener("click", () => {
5973
+ AudioContextManager.ensureAudioReady();
5921
5974
  this.markHasMessages();
5922
5975
  hideSuggestions();
5923
5976
  });
5924
5977
  chatInput.addEventListener("keydown", (e) => {
5925
5978
  if (e.key === "Enter") {
5979
+ AudioContextManager.ensureAudioReady();
5926
5980
  this.markHasMessages();
5927
5981
  hideSuggestions();
5928
5982
  setTimeout(() => {
@@ -6124,6 +6178,7 @@ class AvatarChatElement extends HTMLElement {
6124
6178
  */
6125
6179
  async expand() {
6126
6180
  if (!this._isCollapsed) return;
6181
+ AudioContextManager.ensureAudioReady();
6127
6182
  this._isCollapsed = false;
6128
6183
  this.classList.remove("collapsed");
6129
6184
  this.style.width = `${this.config.width}px`;
@@ -6160,6 +6215,20 @@ class AvatarChatElement extends HTMLElement {
6160
6215
  this.chatManager.sendText(text);
6161
6216
  }
6162
6217
  }
6218
+ /**
6219
+ * Expose triggerAction for client-side Actions debugging
6220
+ * Dispatches a custom nyxAction event as if the server triggered it
6221
+ */
6222
+ triggerAction(function_name, args = {}) {
6223
+ const eventDetail = {
6224
+ type: "trigger_action",
6225
+ function_name,
6226
+ arguments: args
6227
+ };
6228
+ log$2.info(`🛠️ Debug Action Triggered manually: ${function_name}`, args);
6229
+ const customEvent = new CustomEvent("nyxAction", { detail: eventDetail });
6230
+ window.dispatchEvent(customEvent);
6231
+ }
6163
6232
  /**
6164
6233
  * Check if mounted
6165
6234
  */
@@ -6371,7 +6440,8 @@ const AvatarChat = {
6371
6440
  collapse: () => widget.collapse(),
6372
6441
  isMounted: () => widget.isMounted(),
6373
6442
  isConnected: () => widget.isServerConnected(),
6374
- reconnect: () => widget.reconnect()
6443
+ reconnect: () => widget.reconnect(),
6444
+ triggerAction: (name, args) => widget.triggerAction(name, args)
6375
6445
  };
6376
6446
  },
6377
6447
  /**
@@ -6402,7 +6472,8 @@ const AvatarChat = {
6402
6472
  collapse: () => widget.collapse(),
6403
6473
  isMounted: () => widget.isMounted(),
6404
6474
  isConnected: () => widget.isServerConnected(),
6405
- reconnect: () => widget.reconnect()
6475
+ reconnect: () => widget.reconnect(),
6476
+ triggerAction: (name, args) => widget.triggerAction(name, args)
6406
6477
  };
6407
6478
  }
6408
6479
  };