@charisma-ai/sdk
Version:
Charisma.ai SDK for Javascript (browser)
795 lines (653 loc) • 21.8 kB
JavaScript
import fetch from 'isomorphic-unfetch';
import querystring from 'query-string';
import EventEmitter from 'eventemitter3';
import { Client } from 'colyseus.js';
import jwtDecode from 'jwt-decode';
import PQueue from 'p-queue';
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
const fetchHelper = async (endpoint, options = {}) => {
let headers = _extends({
Accept: "application/json"
}, options.headers);
if (typeof options.method === "string" && options.method.toLowerCase() === "post") {
headers = _extends({
"Content-Type": "application/json"
}, headers);
}
const response = await fetch(endpoint, _extends({
mode: "cors"
}, options, {
headers
}));
let data = {};
try {
data = await response.json();
} catch (err) {}
if (!response.ok) {
throw new Error(data.error || `Something went wrong calling \`${endpoint}\``);
}
return data;
};
let globalBaseUrl = "https://play.charisma.ai";
const getGlobalBaseUrl = () => globalBaseUrl;
const setGlobalBaseUrl = newBaseUrl => {
globalBaseUrl = newBaseUrl;
};
async function createPlaythroughToken(options, apiOptions) {
if (options.version === -1 && options.userToken === undefined && options.apiKey === undefined) {
throw new Error("To play the draft version (-1) of a story, an `apiKey` or `userToken` must also be passed.");
}
let authHeader;
if (options.apiKey) {
authHeader = `API-Key ${options.apiKey}`;
} else if (options.userToken) {
authHeader = `Bearer ${options.userToken}`;
}
try {
const {
token
} = await fetchHelper(`${(apiOptions == null ? void 0 : apiOptions.baseUrl) || globalBaseUrl}/play/token`, {
body: JSON.stringify({
storyId: options.storyId,
version: options.version
}),
headers: authHeader ? {
Authorization: authHeader
} : undefined,
method: "POST"
});
return token;
} catch (err) {
throw new Error(`A playthrough token could not be generated: ${err}`);
}
}
async function createConversation(token, apiOptions) {
const {
conversationId
} = await fetchHelper(`${(apiOptions == null ? void 0 : apiOptions.baseUrl) || globalBaseUrl}/play/conversation`, {
body: JSON.stringify({}),
headers: {
Authorization: `Bearer ${token}`
},
method: "POST"
});
return conversationId;
}
async function createCharacterConversation(token, characterId, apiOptions) {
const {
conversationId
} = await fetchHelper(`${(apiOptions == null ? void 0 : apiOptions.baseUrl) || globalBaseUrl}/play/conversation/character`, {
body: JSON.stringify({
characterId
}),
headers: {
Authorization: `Bearer ${token}`
},
method: "POST"
});
return conversationId;
}
async function getMessageHistory(token, conversationId, minEventId, apiOptions) {
const query = querystring.stringify({
conversationId,
minEventId
});
const result = await fetchHelper(`${(apiOptions == null ? void 0 : apiOptions.baseUrl) || globalBaseUrl}/play/message-history?${query}`, {
headers: {
Authorization: `Bearer ${token}`
},
method: "GET"
});
return result;
}
async function getPlaythroughInfo(token, apiOptions) {
const result = await fetchHelper(`${(apiOptions == null ? void 0 : apiOptions.baseUrl) || globalBaseUrl}/play/playthrough-info`, {
headers: {
Authorization: `Bearer ${token}`
},
method: "GET"
});
return result;
}
async function setMemory(token, memoryIdOrRecallValue, saveValue, apiOptions) {
await fetchHelper(`${(apiOptions == null ? void 0 : apiOptions.baseUrl) || globalBaseUrl}/play/set-memory`, {
body: JSON.stringify(_extends({}, typeof memoryIdOrRecallValue === "number" ? {
memoryId: memoryIdOrRecallValue
} : {
memoryRecallValue: memoryIdOrRecallValue
}, {
saveValue
})),
headers: {
Authorization: `Bearer ${token}`
},
method: "POST"
});
}
async function restartFromEpisodeId(token, episodeId, apiOptions) {
await fetchHelper(`${(apiOptions == null ? void 0 : apiOptions.baseUrl) || globalBaseUrl}/play/restart-from-episode`, {
body: JSON.stringify({
episodeId
}),
headers: {
Authorization: `Bearer ${token}`
},
method: "POST"
});
}
async function restartFromEpisodeIndex(token, episodeIndex, apiOptions) {
await fetchHelper(`${(apiOptions == null ? void 0 : apiOptions.baseUrl) || globalBaseUrl}/play/restart-from-episode`, {
body: JSON.stringify({
episodeIndex
}),
headers: {
Authorization: `Bearer ${token}`
},
method: "POST"
});
}
async function restartFromEventId(token, eventId, apiOptions) {
await fetchHelper(`${(apiOptions == null ? void 0 : apiOptions.baseUrl) || globalBaseUrl}/play/restart-from-event`, {
body: JSON.stringify({
eventId
}),
headers: {
Authorization: `Bearer ${token}`
},
method: "POST"
});
}
var api = {
__proto__: null,
getGlobalBaseUrl: getGlobalBaseUrl,
setGlobalBaseUrl: setGlobalBaseUrl,
createPlaythroughToken: createPlaythroughToken,
createConversation: createConversation,
createCharacterConversation: createCharacterConversation,
getMessageHistory: getMessageHistory,
getPlaythroughInfo: getPlaythroughInfo,
setMemory: setMemory,
restartFromEpisodeId: restartFromEpisodeId,
restartFromEpisodeIndex: restartFromEpisodeIndex,
restartFromEventId: restartFromEventId
};
class Conversation extends EventEmitter {
constructor(conversationId, playthroughInstance, options) {
var _this;
super();
_this = this;
this.eventQueue = new PQueue();
this.options = {};
this.addIncomingEvent = (eventName, ...eventArgs) => {
this.eventQueue.add(() => this.emit(eventName, ...eventArgs));
return true;
};
this.start = (event = {}) => {
return this.playthroughInstance.addOutgoingEvent("start", _extends({}, this.options, event, {
conversationId: this.id
}));
};
this.reply = event => {
return this.playthroughInstance.addOutgoingEvent("reply", _extends({}, this.options, event, {
conversationId: this.id
}));
};
this.tap = () => {
return this.playthroughInstance.addOutgoingEvent("tap", _extends({}, this.options, {
conversationId: this.id
}));
};
this.action = event => {
return this.playthroughInstance.addOutgoingEvent("action", _extends({}, this.options, event, {
conversationId: this.id
}));
};
this.resume = () => {
return this.playthroughInstance.addOutgoingEvent("resume", _extends({}, this.options, {
conversationId: this.id
}));
};
this.setSpeechConfig = speechConfig => {
this.options.speechConfig = speechConfig;
};
this.reconnect = async function () {
if (typeof _this.lastEventId === "string") {
_this.eventQueue.pause();
try {
const {
messages
} = await _this.playthroughInstance.getMessageHistory(_this.id, _this.lastEventId);
if (messages.length > 0) {
_this.emit("playback-start");
messages.forEach(message => {
if (typeof BigInt === "undefined") {
if (message.timestamp > _this.lastTimestamp) {
_this.emit("message", _extends({}, message, {
conversationId: _this.id
}));
}
} else if (BigInt(message.eventId) > BigInt(_this.lastEventId)) {
_this.emit("message", _extends({}, message, {
conversationId: _this.id
}));
}
});
_this.emit("playback-stop");
}
} finally {
_this.eventQueue.start();
}
}
};
this.id = conversationId;
this.playthroughInstance = playthroughInstance;
if (options) {
this.options = options;
}
this.on("message", message => {
this.lastEventId = message.eventId;
this.lastTimestamp = message.timestamp;
});
}
}
class Playthrough extends EventEmitter {
constructor(token, baseUrl) {
var _this;
super();
_this = this;
this.connectionStatus = "disconnected";
this.shouldReconnect = true;
this.activeConversations = new Map();
this.joinConversation = (conversationId, options) => {
const conversation = new Conversation(conversationId, this, options);
if (this.activeConversations.has(conversationId)) {
return this.activeConversations.get(conversationId);
}
this.activeConversations.set(conversationId, conversation);
return conversation;
};
this.leaveConversation = conversationId => {
if (!this.activeConversations.has(conversationId)) {
throw new Error(`The conversation with id \`${conversationId}\` has not been joined, so cannot be left.`);
}
this.activeConversations.delete(conversationId);
};
this.getConversation = conversationId => {
return this.activeConversations.get(conversationId);
};
this.addOutgoingEvent = (eventName, eventData) => {
if (this.room) {
if (this.connectionStatus === "connected") {
this.room.send(eventName, eventData);
} else {
console.warn(`Event \`${eventName}\` was not sent as the socket was not ready. Wait for the \`connection-status\` event to be called with \`connected\` before sending events.`);
}
} else {
console.log(`Event \`${eventName}\` was not sent as the socket was not initialised. Call \`playthrough.connect()\` to connect the socket.`);
}
};
this.connect = async function () {
const baseUrl = _this.baseUrl || getGlobalBaseUrl();
if (!_this.client) {
_this.client = new Client(baseUrl.replace(/^http/, "ws"));
}
_this.room = await _this.client.joinOrCreate("chat", {
playthroughId: _this.playthroughId,
token: _this.token
});
_this.attachRoomHandlers(_this.room);
_this.shouldReconnect = true;
};
this.pause = () => {
this.addOutgoingEvent("pause");
};
this.play = () => {
this.addOutgoingEvent("play");
};
this.attachRoomHandlers = room => {
room.onMessage("status", this.onConnected);
room.onMessage("problem", this.onProblem);
room.onMessage("start-typing", this.onStartTyping);
room.onMessage("stop-typing", this.onStopTyping);
room.onMessage("message", this.onMessage);
room.onMessage("episode-complete", this.onEpisodeComplete);
room.onError(this.onError);
room.onLeave(async function (code) {
room.removeAllListeners();
_this.room = undefined;
if (code === 4000 || !_this.shouldReconnect) {
_this.onDisconnect();
return;
}
let roomExpired = false;
for (let attempts = 0; attempts < 20; attempts += 1) {
if (!roomExpired) {
try {
var _this$client;
_this.onReconnecting();
const newRoom = await ((_this$client = _this.client) == null ? void 0 : _this$client.reconnect(room.id, room.sessionId));
if (newRoom) {
_this.attachRoomHandlers(newRoom);
_this.room = newRoom;
_this.onReconnect();
_this.onConnected();
return;
}
} catch (err) {
if (/room ".*" not found/.test(err.message)) {
roomExpired = true;
}
}
}
if (roomExpired) {
try {
var _this$client2;
const newRoom = await ((_this$client2 = _this.client) == null ? void 0 : _this$client2.joinOrCreate("chat", {
playthroughId: _this.playthroughId,
token: _this.token
}));
if (newRoom) {
_this.attachRoomHandlers(newRoom);
_this.room = newRoom;
_this.onReconnect();
_this.onConnected();
return;
}
} catch (err2) {
console.error("Could not reconnect to a Charisma playthrough.", err2);
}
}
await new Promise(resolve => setTimeout(() => resolve(), 5000 + Math.floor(Math.random() * 1000)));
}
_this.onDisconnect();
});
};
this.disconnect = () => {
this.shouldReconnect = false;
if (this.room) {
this.room.leave();
}
};
this.changeConnectionStatus = newStatus => {
if (newStatus !== this.connectionStatus) {
this.connectionStatus = newStatus;
this.emit("connection-status", newStatus);
}
};
this.onReconnect = () => {
this.activeConversations.forEach(conversation => {
conversation.reconnect().catch(err => {
console.error(`Something went wrong reconnecting to conversation:`, err);
});
});
};
this.onReconnecting = () => {
this.changeConnectionStatus("connecting");
};
this.onDisconnect = () => {
this.changeConnectionStatus("disconnected");
};
this.onConnected = () => {
this.changeConnectionStatus("connected");
};
this.onError = (code, message) => {
this.emit("error", {
message,
code
});
};
this.onProblem = problem => {
this.emit("problem", problem);
};
this.onStartTyping = event => {
const conversation = this.activeConversations.get(event.conversationId);
if (conversation) {
conversation.addIncomingEvent("start-typing", event);
}
};
this.onStopTyping = event => {
const conversation = this.activeConversations.get(event.conversationId);
if (conversation) {
conversation.addIncomingEvent("stop-typing", event);
}
};
this.onMessage = event => {
const conversation = this.activeConversations.get(event.conversationId);
if (conversation) {
conversation.addIncomingEvent("message", event);
}
};
this.onEpisodeComplete = event => {
const conversation = this.activeConversations.get(event.conversationId);
if (conversation) {
conversation.addIncomingEvent("episode-complete", event);
}
};
this.token = token;
const {
playthrough_id: playthroughId
} = jwtDecode(this.token);
this.playthroughId = playthroughId;
this.baseUrl = baseUrl;
}
createConversation() {
return createConversation(this.token, {
baseUrl: this.baseUrl
});
}
createCharacterConversation(characterId) {
return createCharacterConversation(this.token, characterId, {
baseUrl: this.baseUrl
});
}
getMessageHistory(conversationId, minEventId) {
return getMessageHistory(this.token, conversationId, minEventId, {
baseUrl: this.baseUrl
});
}
getPlaythroughInfo() {
return getPlaythroughInfo(this.token, {
baseUrl: this.baseUrl
});
}
setMemory(memoryIdOrRecallValue, saveValue) {
return setMemory(this.token, memoryIdOrRecallValue, saveValue, {
baseUrl: this.baseUrl
});
}
restartFromEpisodeId(episodeId) {
return restartFromEpisodeId(this.token, episodeId, {
baseUrl: this.baseUrl
});
}
restartFromEpisodeIndex(episodeIndex) {
return restartFromEpisodeIndex(this.token, episodeIndex, {
baseUrl: this.baseUrl
});
}
restartFromEventId(eventId) {
return restartFromEventId(this.token, eventId, {
baseUrl: this.baseUrl
});
}
}
const SpeechRecognitionClass = typeof window !== "undefined" ? window.SpeechRecognition || window.webkitSpeechRecognition : undefined;
class Microphone extends EventEmitter {
constructor() {
super(...arguments);
this.recognition = SpeechRecognitionClass ? new SpeechRecognitionClass() : undefined;
this.isSupported = SpeechRecognitionClass !== undefined;
this.startListening = ({
continuous: _continuous = false,
interimResults: _interimResults = true,
lang: _lang = "en-GB",
timeout
} = {}) => {
if (!this.recognition) {
return;
}
if (this.timeoutId !== undefined) {
clearTimeout(this.timeoutId);
}
const {
recognition
} = this;
recognition.continuous = _continuous;
recognition.interimResults = _interimResults;
recognition.lang = _lang;
recognition.onresult = this.onRecognitionResult;
recognition.onstart = () => {
this.emit("start");
};
recognition.onend = () => {
this.emit("stop");
recognition.start();
};
recognition.onerror = event => {
this.emit("error", event.error);
};
try {
recognition.start();
} catch (err) {}
if (timeout !== undefined) {
this.timeoutId = window.setTimeout(this.onTimeout, timeout);
}
};
this.stopListening = ({
waitForLastResult: _waitForLastResult = false
} = {}) => {
if (this.timeoutId !== undefined) {
clearTimeout(this.timeoutId);
}
const {
recognition
} = this;
if (recognition) {
if (!_waitForLastResult) {
recognition.onresult = () => undefined;
}
recognition.onend = () => {
this.emit("stop");
};
try {
if (_waitForLastResult) {
recognition.stop();
} else {
recognition.abort();
}
} catch (err) {}
}
};
this.resetTimeout = timeout => {
if (this.timeoutId !== undefined) {
clearTimeout(this.timeoutId);
}
this.timeoutId = window.setTimeout(this.onTimeout, timeout);
};
this.onTimeout = () => {
this.timeoutId = undefined;
this.emit("timeout");
this.stopListening();
};
this.onRecognitionResult = event => {
if (event.results && event.results[0] && event.results[0][0]) {
const message = event.results[0][0].transcript.trim();
if (event.results[0].isFinal === false) {
this.emit("recognise-interim", message);
} else {
this.emit("recognise", message);
}
}
};
}
}
class Speaker extends EventEmitter {
constructor() {
var _this;
super(...arguments);
_this = this;
this.currentSources = [];
this.getAudioContext = () => {
if (this.audioContext) {
return this.audioContext;
}
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (!AudioContextClass) {
throw new Error("AudioContext isn't supported in this browser.");
}
const audioContext = new AudioContextClass();
this.audioContext = audioContext;
return audioContext;
};
this.play = async function (audio, options = {}) {
if (typeof options === "boolean") {
console.warn("Passing a boolean as the second parameter to `speaker.play()` is deprecated, and should be updated to use an `options` object.");
options = {
interrupt: options ? "all" : "none"
};
}
const {
interrupt = "none",
trackId
} = options;
const audioContext = _this.getAudioContext();
const source = audioContext.createBufferSource();
source.connect(audioContext.destination);
source.buffer = await new Promise((resolve, reject) => {
audioContext.decodeAudioData(audio, resolve, reject);
});
if (audioContext.state !== "running") {
return Promise.resolve();
}
return new Promise(resolve => {
source.onended = () => {
resolve();
_this.currentSources = _this.currentSources.filter(currentSource => currentSource.sourceNode !== source);
if (_this.currentSources.length === 0) {
_this.emit("stop");
}
};
if (_this.currentSources.length > 0 && interrupt !== "none") {
_this.currentSources.forEach(currentSource => {
if (interrupt === "all" || interrupt === "track" && currentSource.trackId === trackId) {
currentSource.sourceNode.stop();
}
});
}
if (_this.currentSources.length === 0) {
_this.emit("start");
}
_this.currentSources.push({
sourceNode: source,
trackId
});
source.start();
});
};
}
}
var ImageLayerResizeMode;
(function (ImageLayerResizeMode) {
ImageLayerResizeMode["Contain"] = "contain";
ImageLayerResizeMode["Cover"] = "cover";
})(ImageLayerResizeMode || (ImageLayerResizeMode = {}));
var AudioTrackBehaviour;
(function (AudioTrackBehaviour) {
AudioTrackBehaviour["Continue"] = "continue";
AudioTrackBehaviour["Restart"] = "restart";
})(AudioTrackBehaviour || (AudioTrackBehaviour = {}));
export { AudioTrackBehaviour, Conversation, ImageLayerResizeMode, Microphone, Playthrough, Speaker, api, createCharacterConversation, createConversation, createPlaythroughToken, getGlobalBaseUrl, getMessageHistory, getPlaythroughInfo, restartFromEpisodeId, restartFromEpisodeIndex, restartFromEventId, setGlobalBaseUrl, setMemory };
//# sourceMappingURL=index.modern.js.map