adam-sdk
Version:
A JavaScript SDK for integrating A.D.A.M. 3D avatars into web applications.
167 lines (166 loc) • 6.39 kB
JavaScript
class h {
iframe;
targetOrigin;
eventListeners = /* @__PURE__ */ new Map();
pendingCommands = /* @__PURE__ */ new Map();
isReady = !1;
handshakeInterval;
fastPollInterval;
logLevel;
/**
* Creates an instance of the AvatarSDK.
* @param iframeElement The HTMLIFrameElement that contains the avatar.
* @param options Configuration options for the SDK.
*/
constructor(e, t = {}) {
this.iframe = e, this.targetOrigin = t.targetOrigin || new URL(e.src).origin, this.logLevel = t.logLevel || "info", window.addEventListener("message", this._handleMessage.bind(this)), this._log("info", "AvatarSDK: Message listener attached");
}
/**
* Establishes a connection with the avatar iframe.
* This method must be called before any other commands can be sent to the avatar.
* It returns a promise that resolves when the connection is established.
* @returns A promise that resolves when the connection is established.
* @throws An error if the connection times out or the iframe fails to load.
*/
connect() {
return new Promise((e, t) => {
if (this.isReady) return e();
const s = setTimeout(() => {
clearInterval(this.handshakeInterval), clearInterval(this.fastPollInterval), t(new Error("Connection timed out. The avatar did not respond."));
}, 1e4);
this.iframe.onerror = () => {
clearTimeout(s), clearInterval(this.handshakeInterval), clearInterval(this.fastPollInterval), t(new Error("The avatar iframe failed to load."));
}, this.on("ready", () => {
clearTimeout(s), clearInterval(this.handshakeInterval), clearInterval(this.fastPollInterval), e();
}), this._postMessage({ type: "CHECK_READY_STATUS" });
let n = 0;
this.fastPollInterval = setInterval(() => {
n < 8 ? (this._postMessage({ type: "CHECK_READY_STATUS" }), n++) : (clearInterval(this.fastPollInterval), this.handshakeInterval = setInterval(() => {
this.isReady || this._postMessage({ type: "CHECK_READY_STATUS" });
}, 500));
}, 250);
});
}
/**
* Makes the avatar speak the provided text.
* @param text The text for the avatar to speak.
* @param options Additional options for speech, such as voice, rate, and pitch.
* @returns A promise that resolves when the command is acknowledged by the iframe.
* @throws An error if the text is not a non-empty string.
*/
speak(e, t = {}) {
return console.log("[AvatarSDK] speak", e, t), typeof e != "string" || !e.trim() ? Promise.reject(new Error("Text must be a non-empty string")) : this._sendCommand("speak", { text: e, ...t });
}
/**
* Triggers a specific animation by name.
* @param name The name of the animation to play.
* @param loop Whether the animation should loop.
* @returns A promise that resolves with the command result.
*/
playAnimation(e, t = !1) {
return this._sendCommand("playAnimation", { name: e, loop: t });
}
/**
* Sets the avatar's facial expression.
* @param name The name of the expression to set.
* @returns A promise that resolves with the command result.
*/
setExpression(e) {
return this._sendCommand("setExpression", { name: e });
}
/**
* Interrupts the current speech or action.
* @returns A promise that resolves when the interrupt is complete.
*/
interrupt() {
return this._sendCommand("interrupt");
}
/**
* Registers an event listener for a specific event.
* @param eventName The name of the event to listen for.
* @param callback The callback function to execute when the event is triggered.
* @example
* ```javascript
* sdk.on('speech:start', (payload) => {
* console.log('Avatar started speaking:', payload.text);
* });
* ```
*/
on(e, t) {
this.eventListeners.has(e) || this.eventListeners.set(e, []), this.eventListeners.get(e)?.push(t);
}
/**
* Removes an event listener for a specific event.
* @param eventName The name of the event to remove the listener from.
* @param callback The callback function to remove.
*/
off(e, t) {
const s = this.eventListeners.get(e);
if (s) {
const n = s.indexOf(t);
n > -1 && s.splice(n, 1);
}
}
/**
* Cleans up resources and removes event listeners.
* This should be called when the SDK is no longer needed to prevent memory leaks.
*/
destroy() {
window.removeEventListener("message", this._handleMessage.bind(this)), clearInterval(this.handshakeInterval), clearInterval(this.fastPollInterval), this.eventListeners.clear(), this.pendingCommands.clear(), this.isReady = !1;
}
_sendCommand(e, t = {}) {
if (!this.isReady)
return Promise.reject(new Error("Not connected to avatar."));
const s = `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, n = {
type: "AVATAR_COMMAND",
command: e,
payload: t,
commandId: s
};
return new Promise((r, i) => {
const a = setTimeout(() => {
this.pendingCommands.has(s) && (this.pendingCommands.delete(s), i(new Error(`Command '${e}' timed out after 30s`)));
}, 3e4);
this.pendingCommands.set(s, { resolve: r, reject: i, timeout: a }), this._postMessage(n);
});
}
_handleMessage(e) {
if (e.origin !== this.targetOrigin)
return;
const { type: t, commandId: s, event: n, payload: r } = e.data;
if (t === "AVATAR_READY") {
this.isReady = !0, this._emitEvent("ready", void 0);
return;
}
if (t === "AVATAR_EVENT" && (this._emitEvent(n, r), s && this.pendingCommands.has(s))) {
const { resolve: i, reject: a, timeout: o } = this.pendingCommands.get(s);
clearTimeout(o), n === "command:success" ? i(r) : n === "command:error" && a(new Error(r?.error || "Command failed")), this.pendingCommands.delete(s);
}
}
_postMessage(e) {
this.iframe.contentWindow && this.iframe.contentWindow.postMessage(e, this.targetOrigin);
}
_log(e, t) {
if (this.logLevel === "none" || e === "none")
return;
const s = {
error: 1,
warn: 2,
info: 3
};
s[e] <= s[this.logLevel] && console[e](t);
}
_emitEvent(e, t) {
const s = this.eventListeners.get(e);
s && s.forEach((n) => {
try {
n(t);
} catch (r) {
console.error(`Error in event listener for ${e}:`, r);
}
});
}
}
export {
h as AvatarSDK
};