Encouragement extension for the Teneo Web Chat frontend

This is the code referred to in this article about the TWC encouragement extension.

((sName, bDebug) => {


var storage,
SESSION_ID, USER_INPUT_DISABLED,
GLOBAL_ENCOURAGEMENTS, NODE_ENCOURAGEMENTS, ENCOURAGEMENT_INDEX, ENCOURAGEMENT_THREAD_START,
sessionId = null,
encouragementThreadId, globalEncouragements;

const isSessionOpen = () => (sessionId && window.TeneoWebChat.get('chat_history').length !== 0),

getBooleanFromString = s => {
    if (s == null || (s = s.trim()).length === 0 || s == 0) return false;
    s = s.toLowerCase();
    return s!=='false' && s!=='null';
},


isValidEncouragementArray = xx => {
    if (!Array.isArray(xx)) return false;
    for (var i = 0; i !== xx.length; i++) {
        if (!(xx[i].delaySeconds > 0 && (xx[i].text || xx[i].command))) {
            console.warn(sName, 'Bad encouragement object', xx[i]);
            return false;
        }
    }
    return true;
},


toEncouragements = s => {
    try {
        const r = JSON.parse(s);
        if (isValidEncouragementArray(r)) return r;
        console.warn(sName, 'Skipping parsed encouragements value as it is either a non-array or has incorrect format', s);
    } catch (err) {
        console.error(sName, 'Bad encouragements value', s, err);
    }
    return null;
},


stopEncouragementThread = () => {
    if (encouragementThreadId == null) return;
    clearTimeout(encouragementThreadId);
    encouragementThreadId = null;
    if (bDebug) console.debug(sName, 'Stopping encouragements thread');
},


cancelAllEncouragements = () => {
    stopEncouragementThread();
    globalEncouragements = null;
    storage.removeItem(GLOBAL_ENCOURAGEMENTS);
    storage.removeItem(NODE_ENCOURAGEMENTS);
    storage.removeItem(ENCOURAGEMENT_INDEX);
    storage.removeItem(ENCOURAGEMENT_THREAD_START);
    if (bDebug) console.debug(sName, 'Deleting encouragements');
},


isProvidedWithInput = m => {
    if (m) {
        var x = m.type;
        if (x && (x === 'form' || x === 'buttons' || x === 'clickablelist' || x === 'quickreply' || x === 'form')) return true;
        if (Array.isArray(m)) {
            x = m.length;
            while (--x >= 0) {
                if (isProvidedWithInput(m[x])) return true;
            }
        } else if ('object' === typeof m) {
            for (x in m) {
                if (m.hasOwnProperty(x) && (x === 'postback' || x === 'parameters' || isProvidedWithInput(m[x]))) return true;
            }
        }
    }
    return false;
},


isLikelyCtaMessage = m => m != null && m.author !== 'user' && m.type !== 'system' && m.type !== 'text' && isProvidedWithInput(m),


/**
 * Handles encouragements.
 *
 * @param {number} [nEncInd] the index of the currently displayed encouragement in the encouragement array.
 * If its value is not provided or is null or undefined, then the encouragement count is resumed. Otherwise
 * it is (re)started. This value should be provided as 0 on engine response and as null or not provided
 * for page reloads.
 * @param {string} [sGlobalEncouragements] a stringified array of global encouragement objects. If it is not provided,
 * the existing array remains unchanged. If it provided as malformed, the existing array is deleted.
 * @param {string} [sNodeEncouragements] a stringified array of node encouragement objects.
 * @param {object} [nodeEncouragements] an array of a node encouragements objects. If sNodeEncouragements
 * is provided, it has priority over nodeEncouragements.
 */
doEncouragementThread = (nEncInd, sGlobalEncouragements, sNodeEncouragements, nodeEncouragements) => {
    if (nEncInd == null) {
        if (sGlobalEncouragements == null) sGlobalEncouragements = storage.getItem(GLOBAL_ENCOURAGEMENTS);
        if (sNodeEncouragements == null) sNodeEncouragements = storage.getItem(NODE_ENCOURAGEMENTS);
    }
    if (sGlobalEncouragements != null && (sGlobalEncouragements = sGlobalEncouragements.trim()).length !== 0) {
        globalEncouragements = toEncouragements(sGlobalEncouragements);
        if (globalEncouragements == null) storage.removeItem(GLOBAL_ENCOURAGEMENTS);
        else if (globalEncouragements.length === 0) {
            globalEncouragements = null;
            storage.removeItem(GLOBAL_ENCOURAGEMENTS);
        } else {
            storage.setItem(GLOBAL_ENCOURAGEMENTS, sGlobalEncouragements);
        }
    } else {
        if (globalEncouragements == null) storage.removeItem(GLOBAL_ENCOURAGEMENTS);
    }
    if (sNodeEncouragements != null && (sNodeEncouragements = sNodeEncouragements.trim()).length !== 0) {
        nodeEncouragements = toEncouragements(sNodeEncouragements);
        if (nodeEncouragements == null) storage.removeItem(NODE_ENCOURAGEMENTS);
        else storage.setItem(NODE_ENCOURAGEMENTS, sNodeEncouragements);
    } else {
        if (nodeEncouragements == null) storage.removeItem(NODE_ENCOURAGEMENTS);
    }
    const encs = nodeEncouragements || globalEncouragements;
    if (encs == null) {
        cancelAllEncouragements();
        return;
    }
    stopEncouragementThread();
    if (encs.length === 0) return;

    var x;
    if (nEncInd != null) storage.setItem(ENCOURAGEMENT_INDEX, nEncInd);
    else {
        // Page is refreshed

        x = storage.getItem(ENCOURAGEMENT_INDEX);
        if (x == null || (x = x.trim()).length === 0) nEncInd = 0;
        else if (Number.isNaN(nEncInd = Number.parseInt(x))) {
            console.warn(sName, 'Bad [' + ENCOURAGEMENT_INDEX + '] value:', x);
            return;
        }
        if (encs.length === nEncInd) {
            // Page is refreshed after all the encouragements have been displayed
            if (bDebug) console.debug(sName, 'encs.length==' + encs.length + ', nEncInd==' + nEncInd);
            return;
        }

        x = storage.getItem(ENCOURAGEMENT_THREAD_START);
        if (x != null) {
            if ((x = x.trim()).length === 0) x = null;
            else if (Number.isNaN(x = Number.parseInt(x))) {
                console.warn(sName, 'Bad [' + ENCOURAGEMENT_THREAD_START + '] value:', storage.getItem(ENCOURAGEMENT_THREAD_START));
                return;
            }
        }
    }
    // Here, x is the start time point for the countdown for the currently planned encouragement
    // or null (undefined) if it has not started yet or is irrelevant. nEncInd is the integer
    // index of the encouragement to be executed.

    const enc = encs[nEncInd], nDelay = (x == null) ? enc.delaySeconds * 1000 : (enc.delaySeconds * 1000 - (Date.now() - x));

    if (enc == null) {
        console.warn(sName, 'Bad nEncInd value', nEncInd, 'for encs', encs);
        return;
    }

    // If the encouragement countdown hasn't been set previously, set it to the current time point:
    if (x == null) storage.setItem(ENCOURAGEMENT_THREAD_START, Date.now());

    if (bDebug) console.debug(sName, 'Starting encouragement thread with calculated delay in milliseconds', nDelay, 'and value', enc);

    x = sessionId;
    encouragementThreadId = setTimeout(() => {
        encouragementThreadId = null;
        storage.removeItem(ENCOURAGEMENT_THREAD_START);
        if (!isSessionOpen()) {
            console.info(sName, 'Ignoring encouragements because the session closed');
            cancelAllEncouragements();
            return;
        }
        if (x !== sessionId) {
            console.info(sName, 'Ignoring encouragements because the session changed from', x, 'to', sessionId);
            cancelAllEncouragements();
            return;
        }
        if (bDebug) console.debug(sName, 'Printing encouragement', enc);
        if (enc.text) window.TeneoWebChat.call('add_message', { type: 'text', author: 'bot', data: { text: enc.text }});
        nEncInd++;

        if (enc.command) {
            switch (enc.command) {
                case 'reset':
                    if (nEncInd < encs.length) {
                        console.warn(sName, 'Command "reset" on a non-last item encouragements', encs);
                    }
                    window.TeneoWebChat.call('reset');
                    return;
                case 'endSession':
                    if (nEncInd < encs.length) {
                        console.warn(sName, 'Command "endSession" on a non-last item for encouragements', encs);
                    }
                    window.TeneoWebChat.call('end_session');
                    resetData();
                    return;
                case 'disableUserInput':
                    window.TeneoWebChat.call('disable_user_input');
                    storage.setItem(USER_INPUT_DISABLED, '1');
                    break;
                case 'enableUserInput':
                    window.TeneoWebChat.call('enable_user_input');
                    storage.removeItem(USER_INPUT_DISABLED);
                    break;
                default:
                    console.warn(sName, 'Unknown encouragement command', enc.command);
            }
        }
        if (enc.text) {
            // The enc text message has been added. So if the last output contained
            // CTAs, they have been relegated to the history and thus deactivated.
            // Get the last history item and repeat it if it is likely to contain CTAs:
            let h = window.TeneoWebChat.get('chat_history');
            h = h[h.length - 1];
            if (isLikelyCtaMessage(h)) {
                if (bDebug) console.debug(sName, 'Repeating a likely CTA message', h);
                window.TeneoWebChat.call('add_message', h);
            }
        }
        storage.setItem(ENCOURAGEMENT_INDEX, nEncInd);
        if (nEncInd < encs.length) doEncouragementThread(nEncInd, null, null, nodeEncouragements);
    }, nDelay > 0 ? nDelay : 0);
},


resetData = () => {
    cancelAllEncouragements();
    sessionId = null;
    storage.removeItem(SESSION_ID);
    storage.removeItem(USER_INPUT_DISABLED);
    if (bDebug) console.debug(sName, 'Data reset');
};



window.TeneoWebChat.on('ready',() => {

    storage = window.TeneoWebChat.get('storage');
    SESSION_ID = sName + '_sessionId';
    USER_INPUT_DISABLED = sName + '_userInputDisabled';
    GLOBAL_ENCOURAGEMENTS = sName + '_globalEncouragements';
    NODE_ENCOURAGEMENTS = sName + '_nodeEncouragements';
    ENCOURAGEMENT_INDEX = sName + '_encouragementIndex';
    ENCOURAGEMENT_THREAD_START = sName + '_encouragementThreadStart';
    sessionId = storage.getItem(SESSION_ID);

    if (isSessionOpen()) {
        if (bDebug) console.debug(sName,'Session is open on reload');
        doEncouragementThread();
    } else {
        cancelAllEncouragements();
        if (bDebug) console.debug(sName,'Session is not open on reload');
    }
    if (getBooleanFromString(storage.getItem(USER_INPUT_DISABLED))) setTimeout(() => window.TeneoWebChat.call('disable_user_input'));
});


window.TeneoWebChat.on('engine_request', stopEncouragementThread);


window.TeneoWebChat.on('engine_response', ({responseDetails}) => {
    storage.setItem(SESSION_ID, sessionId = responseDetails.sessionId);
    var x = responseDetails.output.parameters;
    if (x) doEncouragementThread(0, x.encouragements, x.nodeEncouragements);
    else doEncouragementThread(0);
});



window.TeneoWebChat.on('reset', resetData);


})('TWCEncouragement');