{% extends "macros.html" %}
{% include "macros/browser-detect.html" %}

{% block stylesheets %}
    {{ super() }}
    {% include "theme.html" %}

    {% for x in css %}
        <style>
            {{ x }}
        </style>
    {% endfor %}

    {% for x in css_links %}
        <link rel="stylesheet" href={{ x }}>
    {% endfor %}
{% endblock %}

{% block body %}
    {% block header %}
        <div class="header">
            {% if config.show_progress_bar %}
                <div class="progress">
                    <div id="timeline-progress-bar" class="progress-bar" role="progressbar"
                         aria-valuenow="{{ progress_percentage }}" aria-valuemin="0" aria-valuemax="100"
                         style="width:{{ progress_percentage }}%">{{ progress_percentage }}%
                    </div>
                </div>
            {% endif %}
        </div>
    {% endblock %}


    <div id="main-body" class="my-5 container" style="clear: both;">
        {% block main_body %}
            {{ media.media_container() }}
            {{ progress.trial_progress_display(trial_progress_display_config) }}
        {% endblock %}

        <span id="psynet-event-listener"></span>
    </div>

    {% block modals %}
        {% block help_modal %}
            <div id="help-modal" class="modal">
                <div class="modal-content">
                    <span class="close">&times;</span>
                    <div style="margin: 20px">
                        <h3>{{ pgettext("timeline_problem", "In case of a problem, try the following:") }}</h3>
                        <ul>
                            <li>{{ pgettext("timeline_problem", "Reload the page.") }}</li>
                            <li>{{ pgettext("timeline_problem", "Check that your internet connection is working.") }}</li>
                            <li>
                                <a
                                        href="mailto:{{ contact_email_on_error }}?Subject=Problem%20with%20experiment%20%28App%20ID%3A%20{{ app_id }}%2C%20Worker%20ID%3A%20{{ worker_id }}%2C%20Participant%20ID%3A%20{{ participant.id }}%29"
                                        target="_top"
                                >
                                    {{ pgettext("timeline_problem", "If the above doesn't help, send us an email with the following information:") }}
                                </a>

                                <ol>
                                    <li>{{ pgettext("timeline_problem", "A link to the current web page;") }}</li>
                                    <li>{{ pgettext("timeline_problem", "Your Worker ID;") }}</li>
                                    <li>{{ pgettext("timeline_problem", "The problem you're experiencing.") }}</li>
                                </ol>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        {% endblock %}

        {% block alert_modal %}
            <div id="alert-modal" class="modal">
                <div class="modal-content">
                    <h3 id="alert-message"></h3>
                    <button type="button" style="max-width: 100px" class="btn btn-primary" id="alert-button">
                        {{ pgettext("alert_ok", "OK") }}
                    </button>
                </div>
            </div>
        {% endblock %}

        {% block resume_modal %}
            <div id="resume-modal" class="modal">
                <div class="modal-content">
                    <h3>{{ pgettext("timeline_resume", "Are you ready?") }}</h3>
                    <p>
                        {{ pgettext("timeline_resume", "Click the button when you're ready to continue the experiment.") }}
                    </p>
                    <button type="button" style="max-width: 100px" class="btn btn-primary" id="resume-button">
                        {{ pgettext("timeline_resume", "Resume") }}
                    </button>
                </div>
            </div>
        {% endblock %}
    {% endblock %}

    {% block footer %}
        <style>
            #footer {
                background-color: rgb(49, 124, 246);
                color: white;
                margin: 0px;
                padding: 0px;
                line-height: 33px;
                border-style: none;
                {% if config.show_footer == false %} display: none; {% endif %}
            }

            #media-download-progress-bar {
                width: {{ initial_download_progress }}%;
                height: 6px;
                background-color: black;
            }

            .footer-text {
                margin: 0px;
            }

        </style>

        <nav id="footer" class="navbar fixed-bottom">
            <div id="media-download-progress-bar"></div>
                <div class="container py-2">
                    {% if config.show_reward %}
                        <div class="footer-text">
                            {{ pgettext("timeline", "Reward") }}:
                            <strong>
                                <span id="reward-details" style="display: none">
                                    {{ config.currency }}<span id="time-reward">{{ time_reward }}</span> (time) +
                                    {{ config.currency }}<span id="performance-reward">{{ performance_reward }}</span> (performance) =
                                </span>
                                <span class="fw-bold">
                                    {{ config.currency }}<span id="total-reward">{{ total_reward }}</span>
                                </span>
                            </strong>
                        </div>
                    {% endif %}

                    <button type="button" class="btn btn-light"
                            id="help-button">{{ pgettext("timeline", "Help!") }}</button>

                    {% if show_termination_button or (experiment.with_lucid_recruitment() and show_termination_button is none) %}
                        <button id="terminate-button" class="btn btn-danger">
                            {{ pgettext("timeline_problem", "Terminate the experiment early") }}
                        </button>
                    {% endif %}
                </div>
        </nav>
        <script>
            // Allows the user to switch the language of the experiment if this setting is enabled.
            $('#iso-language').on('change', function (x) {
                var $_GET = [];
                window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (a, name, value) {
                    $_GET[name] = value;
                });
                $.get("/set_locale_participant/{{ participant.id }}?locale=" + $(this).val(), function (data) {
                    location.reload();
                });
                $('#iso-language').val()
            })
        </script>
    {% endblock %}
{% endblock %}

{% block head %}
    {{ super() }}

    <script src="/static/scripts/jquery-3.7.1.min.js"></script>
    <script src="/static/scripts/JSZip/jszip.min.js"></script>

    {% for x in js_links %}
        <script src="{{ x }}"></script>
    {% endfor %}

    <script>
        {{ init_js_vars }}
        var psynet = (function () {
                /**
                 * @namespace
                 * @alias psynet
                 */

                var psynet = {
                    media: {},
                    page: {
                        prompt: {},
                        control: {}
                    },
                    utils: {},
                    var: {{ js_vars | tojson }}
                };

                psynet.utils.shallowCopy = function (x) {
                    return Object.assign({}, x);
                }

                psynet.utils.deepCopy = function (x) {
                    return JSON.parse(JSON.stringify(x));
                }

                // check if it is a function
                psynet.utils.isFunction = function (functionToCheck) {
                    return functionToCheck !== null &&
                        functionToCheck &&
                        {}.toString.call(functionToCheck) === '[object Function]';
                };

                // Checks which value in the haystack is closest to the needle
                psynet.utils.closest = function (needle, haystack) {
                    var bestValue;
                    var bestDist;
                    var bestI;
                    for (var i = 0; i < haystack.length; i++) {
                        var proposal = haystack[i];
                        var dist = Math.abs(proposal - needle);
                        if (bestDist == undefined || dist < bestDist) {
                            bestValue = proposal;
                            bestDist = dist;
                            bestI = i;
                        }
                    }
                    return {
                        value: bestValue,
                        dist: bestDist,
                        index: bestI
                    }
                };

                // check if it is a dictionary
                psynet.utils.isDict = function (dictToCheck) {
                    return (
                        typeof dictToCheck === 'object' &&
                        dictToCheck !== null &&
                        !(dictToCheck instanceof Array) &&
                        !(dictToCheck instanceof Date)
                    );
                };

                // check if a key exists in an array
                psynet.utils.keyExistsInArray = function (key, arr) {
                    return (arr.indexOf(key) > -1);
                };

                // compute a mean; this is not build-in into Javascript XD
                psynet.utils.mean = function (numbers) {
                    var total = 0, i;
                    for (i = 0; i < numbers.length; i += 1) {
                        total += numbers[i];
                    }
                    return total / numbers.length;
                };

                psynet.removeBeforeUnloadEventListener = function () {
                    window.removeEventListener("beforeunload", beforeunloadFunction);
                };

                psynet.waitForEventListener = async function (target, type) {
                    await new Promise((resolve) => {
                        target.addEventListener(type, () => resolve(), {once: true});
                    });
                };

                class PsyNetError extends Error {
                    constructor(message) {
                        super(message);
                        this.name = "PsyNetError"; // (2)
                    }
                }

                psynet.log = {};

                psynet.log.max = 100;
                psynet.log.counter = 0;

                psynet.log.generic = function (msg, level) {
                    if (level !== "error" && level !== "warning" && level !== "info" && level !== "debug") {
                        throw new Error("Invalid log level: " + level);
                    }

                    let time = new Date();
                    console.log("LOG (" + level.toUpperCase() + ") at " + time.toLocaleTimeString() + ": " + msg);

                    if (level != "debug" && psynet.log.counter < psynet.log.max) {
                        psynet.log.counter += 1;
                        route = '/log/' + level + "/{{ unique_id }}";

                        dallinger.post(route, {message: msg});
                    }
                };

                psynet.log.error = function (msg) {
                    var msgWithBrowserInfo = msg + "\n    " + "Platform: " + platform.toString();
                    psynet.log.generic(msgWithBrowserInfo, "error");
                };

                psynet.log.warning = function (msg) {
                    psynet.log.generic(msg, "warning")
                };

                psynet.log.warn = psynet.log.warning

                psynet.log.info = function (msg) {
                    psynet.log.generic(msg, "info")
                };

                psynet.log.debug = function (msg) {
                    psynet.log.generic(msg, "debug")
                }

                let Trial = function () {
                    let trial = {
                        state: null,
                        events: {},
                        timers: [],
                        eventLog: [],
                        inProgress: false,
                        stopping: false
                    };

                    trial.reset = function () {
                        trial.state = null;
                        trial.inProgress = false;
                        trial.startTime = null;
                        Object.values(trial.events).forEach((e) => e.reset());
                    };

                    let Event = function (id, spec) {
                        let event = {
                            id: id,
                            toBeTriggered: [],
                            triggerCondition: spec.trigger_condition,
                            delay: spec.delay,
                            once: spec.once,
                            handlers: [],
                            happened: false,
                            message: spec.message,
                            messageColor: spec.message_color,
                            js: spec.js
                        };
                        event.isTriggeredBy = spec.is_triggered_by.map((t) => {
                            let trigger = {
                                delay: t.delay,
                                triggeringEvent: t.triggering_event,
                                fired: false,
                                triggeredEvent: event
                            };
                            trigger.fire = function (info) {
                                trigger.fired = true;
                                trigger.triggeredEvent.checkTriggers(info);
                            };
                            return trigger
                        });

                        event.checkTriggers = function (info) {
                            let allTriggersFired = event.isTriggeredBy.every((trigger) => trigger.fired);
                            if ((event.triggerCondition == "any" || allTriggersFired) && (!(event.once && event.happened))) {
                                trial.setTimer(() => trial.registerEvent(id, {info: info}), event.delay * 1000);
                            }
                        };

                        event.propagateTriggers = function () {
                            event.isTriggeredBy.forEach((trigger) => {
                                let triggeringEvent = trial.events[trigger.triggeringEvent];
                                if (!triggeringEvent) {
                                    triggeringEvent = genericEvent(trigger.triggeringEvent);
                                    trial.events[trigger.triggeringEvent] = triggeringEvent;
                                }

                                triggeringEvent.toBeTriggered.push(trigger);
                            });
                        };

                        event.resetTriggers = function () {
                            event.isTriggeredBy.forEach((trigger) => {
                                trigger.fired = false;
                            });
                        };

                        event.reset = function () {
                            event.happened = false;
                            event.resetTriggers();
                        };

                        event.showMessage = function () {
                            if (event.message !== null) {
                                psynet.trialProgress.setText(event.message, event.messageColor);
                            }
                        };

                        event.runJS = function (info) {
                            if (event.js !== null) {
                                Function("info", unescape(event.js))(info);
                            }
                        };

                        event.runHandlers = async function (info) {
                            let handlers = event.handlers;

                            // Sort in order of decreasing priority
                            handlers.sort((a, b) => -(a.priority - b.priority));

                            for (const handler of handlers) {
                                trial.pendingEventHandlers.add(id);
                                await handler.func(info);
                                trial.pendingEventHandlers.remove(id);
                            }
                        };

                        event.hitTriggers = function (info) {
                            for (const target of event.toBeTriggered) {
                                trial.setTimer(() => target.fire(info), target.delay * 1000);
                            }
                        };

                        return event;
                    };

                    let genericEvent = function (id) {
                        // We use snake_case here to mirror the specification format
                        // that comes from the Python back-end
                        return Event(id, {
                            is_triggered_by: [],
                            trigger_condition: "all",
                            delay: 0.0,
                            once: false
                        });
                    };

                    trial.initEvents = function () {
                        let rawEvents = {{ events | tojson }};
                        Object.entries(rawEvents).forEach(([id, spec]) => trial.events[id] = Event(id, spec));
                        Object.values(trial.events).forEach((event) => event.propagateTriggers());
                    };

                    trial.initEvents()

                    trial.setTimer = function (handler, timeout) {
                        trial.timers.push(setTimeout(handler, timeout));
                    };

                    trial.clearTimers = function () {
                        trial.timers.forEach(timer => clearTimeout(timer));
                    };

                    trial.pendingEventHandlers = (() => {
                        let data = {};
                        return {
                            data: data,

                            add: function (id) {
                                let time = new Date();
                                data[id] = {
                                    timeStarted: time
                                }
                            },

                            remove: function (id) {
                                delete data[id];
                            },

                            waitFor: async function (providedOptions) {
                                let options = {
                                    pollInterval: 0.005,
                                    timeOut: 10,
                                    ignore: []
                                };

                                Object.assign(options, providedOptions);

                                let timer = 0;

                                function queueIsEmpty() {
                                    let pendingHandlers = Object.keys(data);
                                    let filteredHandlers = pendingHandlers.filter((id) => !options.ignore.includes(id));
                                    return filteredHandlers.length == 0;
                                }

                                function checkQueue() {
                                    return new Promise((resolve, reject) => {
                                        let poller = setInterval(() => {
                                            if (queueIsEmpty()) {
                                                clearInterval(poller);
                                                resolve();
                                            } else {
                                                timer += options.pollInterval;
                                                if (timer >= options.timeOut) {
                                                    reject();
                                                    clearInterval(poller);
                                                    throw new Error("Timed out when waiting for event handlers to complete.");
                                                }
                                            }
                                        }, options.pollInterval);
                                    });
                                }

                                await checkQueue();
                            }
                        };
                    })();

                    trial.logEvent = function (id, info) {
                        let time = new Date();
                        trial.eventLog.push({
                            eventType: id,
                            localTime: time,
                            info: info
                        });
                        psynet.log.debug(id);
                    }

                    trial.registerEvent = async function (id, providedOptions) {
                        let options = {
                            info: null,
                            once: false
                        };
                        Object.assign(options, providedOptions);

                        let event = trial.events[id];
                        if (event !== undefined && event.happened && options.once) {
                            return;
                        }

                        trial.state = id;
                        trial.logEvent(id, options.info);

                        if (event !== undefined) {
                            event.happened = true;
                            event.showMessage();
                            event.runJS(options.info);
                            await event.runHandlers(options.info);
                            event.hitTriggers(options.info);
                        }
                    };

                    trial.onEvent = function (id, handler, providedOptions) {
                        // Higher priority values are executed first
                        let options = {
                            priority: 0.0
                        };
                        Object.assign(options, providedOptions);

                        let event = trial.events[id]
                        if (event === undefined) {
                            event = genericEvent(id);
                            trial.events[id] = event;
                        }
                        event.handlers.push({
                            func: handler,
                            priority: options.priority
                        });
                    };

                    trial.getGlobalTime = function () {
                        return psynet.media.audioContext.currentTime;
                    };

                    trial.listEvents = function () {
                        return Object.keys(trial.events);
                    };

                    trial.init = async function () {
                        // Should only be called once, on page load
                        await trial.registerEvent("trialConstruct");
                        $("#buttonStart").attr("disabled", false);
                    };

                    trial.stop = async function () {
                        /**
                         * Can be called manually to stop the trial.
                         * Is idempotent (you can call it multiple times
                         * with no bad side effects).
                         */
                        if (trial.inProgress && !trial.stopping) {
                            trial.stopping = true;
                            trial.clearTimers();
                            trial.inProgress = false;
                            await this.pendingEventHandlers.waitFor();
                            await trial.registerEvent("trialStop");
                            trial.reset();
                            trial.stopping = false;
                        }
                    };

                    trial.restart = async function (providedOptions) {
                        // Can be called manually at an arbitrary point to restart the trial
                        let options = {
                            from: "trialPrepare"
                        };
                        Object.assign(options, providedOptions);

                        await trial.stop();
                        trial.reset();
                        trial.inProgress = true;
                        await trial.registerEvent(options.from);
                    }

                    return trial;
                }

                psynet.trial = Trial();

                psynet.trial.onEvent("trialConstruct", async function () {
                    await psynet.media.init();
                    $(".wait-for-media-load").removeAttr("disabled");
                });

                psynet.trial.onEvent("trialPrepare", function () {
                    psynet.trial.inProgress = true;
                });

                psynet.trial.onEvent("trialFinished", function () {
                    psynet.trial.inProgress = false;
                });

                psynet.trial.onEvent("trialStopped", function () {
                    psynet.trial.inProgress = false;
                });

                psynet.submit = {
                    // .sd-navigation__complete-btn is the complete button in SurveyJS
                    enable: () => $(".submit, .sd-navigation__complete-btn").removeAttr("disabled"),
                    disable: () => $(".submit, .sd-navigation__complete-btn").attr("disabled", "disabled")
                };

                psynet.response = {
                    staged: {
                        rawAnswer: null,
                        metadata: {},
                        blobs: {}
                    },
                    enable: () => $(".response").removeAttr("disabled"),
                    disable: () => $(".response").attr("disabled", "disabled")
                };

                psynet.trial.onEvent("responseEnable", psynet.response.enable);
                psynet.trial.onEvent("submitEnable", psynet.submit.enable);

                psynet.media.types = ["audio", "image", "html", "video"];
                psynet.media.data = {};

                psynet.media.sounds = [];

                psynet.media.loaded = false;

                psynet.media.downloadProgress = {
                    byFile: {}
                };

                psynet.media.types.forEach(function (mediaType) {
                    psynet.media.downloadProgress.byFile[mediaType] = {};
                });

                psynet.media.downloadProgress.set = function (mediaType, fileId, value) {
                    psynet.media.downloadProgress.byFile[mediaType][fileId] = value;
                    psynet.media.downloadProgress.updateDisplay();
                };

                psynet.media.downloadProgress.getTotal = function () {
                    let res = [];
                    Object
                        .values(psynet.media.downloadProgress.byFile)
                        .forEach(function (processes) {
                            Object.values(processes).forEach(function (i) {
                                res.push(i)
                            });
                        });
                    return psynet.utils.mean(res);
                };

                psynet.media.downloadProgress.bar = function () {
                    return document.getElementById("media-download-progress-bar")
                };

                psynet.media.downloadProgress.updateDisplay = function () {
                    let bar = psynet.media.downloadProgress.bar();
                    if (bar !== null) {
                        var progress = psynet.media.downloadProgress.getTotal();
                        bar.style.width = Math.round(progress) + "%";
                        if (progress === 100) {
                            bar.classList.remove("colorfadeanim");
                            // You could do something here once loading is complete,
                            // e.g. delete progress bar text
                        }
                    }
                };

                // The last thing we expect of the user is that the resources, that need to be loaded, are dumped as a json:
                // For example here, we request a batch file that contains three files and we request a single file
                // As we can see each file has a ID and a url where the file is stored
                {{ define_media_requests }}

                // psynet.media.requests = {
                //     "audio": {
                //         "batch": {
                //             "url": "file_concatenated.mp3",
                //             "ids": ["funk_game_loop", "honey_bee", "there_it_is"],
                //             "type": "batch"
                //         },
                //         "bier": "bier.wav"
                //     }
                // };

                psynet.media.stopAllAudio = function () {
                    if (psynet.audio) {
                        Object.values(psynet.audio).forEach(function (x) {
                            x.stop();
                        });
                    }
                }

                psynet.media.getMicrophoneMetadataFromAudioStream = function (stream) {
                    var audioTracks = stream.getAudioTracks();
                    if (audioTracks.length === 0) {
                        psynet.log.error('No tracks found')
                        return {}
                    } else if (audioTracks.length > 1) {
                        psynet.log.warn(
                            'Expected exactly one audio track, but got ' + audioTracks.length + ' tracks. ' +
                            'Recording the first track.'
                        )
                    }

                    var audioTrack = audioTracks[0];
                    audioTrackSettings = audioTrack.getSettings();
                    return {
                        "microphone": {
                            label: audioTrack.label,
                            enabled: audioTrack.enabled,
                            muted: audioTrack.muted,
                            channelCount: audioTrackSettings.channelCount,
                            autoGainControl: audioTrackSettings.autoGainControl,
                            echoCancellation: audioTrackSettings.echoCancellation,
                            noiseSuppression: audioTrackSettings.noiseSuppression,
                            latency: audioTrackSettings.latency,
                            sampleRate: audioTrackSettings.sampleRate,
                            sampleSize: audioTrackSettings.sampleSize,
                        }
                    }
                }

                psynet.media.initAudioContext = function () {
                    let context = null;
                    if ("webkitAudioContext" in window) context = new webkitAudioContext();
                    if ("AudioContext" in window) context = new AudioContext();
                    if (!context) {
                        throw Error("ERROR: No AudioContext available. Try Chrome, Safari or Firefox Nightly.");
                    }
                    if (context.state == "suspended") {
                        context.resume();
                    }
                    psynet.media.audioContext = context;
                };

                let askUserToResume = function () {
                    return new Promise((resolve) => {
                        let resumeModal = $("#resume-modal");
                        let resumeButton = $("#resume-button");
                        resumeModal.css("display", "block");
                        resumeButton.click(async function () {
                            resumeModal.css("display", "none");
                            $("#js-psych").trigger("focus");
                            await psynet.media.init();
                            resolve();
                        });
                    });
                };

                psynet.media.init = async function () {
                    psynet.media.initAudioContext();

                    if (psynet.media.audioContext.state == "suspended") {
                        psynet.log.info(
                            "Audio context is suspended, and can only be resumed " +
                            "after a user interaction. Waiting for the participant to click 'Resume'."
                        );
                        await askUserToResume();
                        psynet.media.initAudioContext();
                    }
                    let requests = psynet.media.requests;
                    let mediaTypes = Object.keys(requests);
                    let bar = psynet.media.downloadProgress.bar();
                    bar.classList.add("colorfadeanim");
                    await Promise.all(
                        mediaTypes.map((x) => processRequests(x, requests[x]))
                    );
                    bar.classList.remove("colorfadeanim");
                };

                let initMediaType = function (mediaType) {
                    let x = {};
                    psynet.media.data[mediaType] = x;
                    psynet[mediaType] = x;
                };

                let initStimulus = function (id, mediaType) {
                    psynet.media.data[mediaType][id] = {
                        loaded: false
                    };
                };

                let processRequests = function (mediaType, requests) {
                    initMediaType(mediaType);
                    checkRequests(requests);

                    return Promise.all(
                        Object.keys(requests).map(function (id) {
                            let value = requests[id];
                            if (isBatch(value)) {
                                return preloadBatch(id, value, mediaType);
                            } else {
                                let url = value;
                                return preloadStimulus(id, url, mediaType);
                            }
                        })
                    );
                };

                let isBatch = function (x) {
                    return psynet.utils.isDict(x)
                };

                async function unzipBatch(url) {
                    console.log("Zip file: " + url);
                    let batchdata = (
                        fetch(url)
                        .then(response => response.arrayBuffer())
                        .then(data => JSZip.loadAsync(data))
                        .then(zip => { return zip.file("stim.batch").async("uint8array") })
                        .then(data => {
                           return data;
                        })
                    )
                    return batchdata
                }

                let preloadBatch = async function (batchId, batch, mediaType) {
                    let url = batch.url;

                    let args = {
                        batchId: batchId,
                        stimulusIds: batch.ids,
                        mediaType: mediaType,
                        fileId: batchId
                    };
                    args.stimulusIds.forEach(function (id) {
                        initStimulus(id, mediaType);
                    });

                    let unzip = batch.unzip;
                    if (unzip) {
                        let data = await unzipBatch(batch.url);
                        return processMediaBatch[mediaType](data.buffer, args);
                    }
                    else {
                        return createRequest(url, mediaType, processMediaBatch[mediaType], args);
                    }

                };

                let preloadStimulus = function (stimulusId, url, mediaType) {
                    let args = {
                        stimulusId: stimulusId,
                        mediaType: mediaType,
                        fileId: stimulusId
                    };
                    initStimulus(stimulusId, mediaType);
                    return createRequest(url, mediaType, createMediaFromBuffer[mediaType], args);
                };

                function reportRequestError(url, status) {
                    let msg;
                    if (status === 404) {
                        msg = "Failed to load media asset at " + url + " (404, file not found).";
                    } else {
                        msg = "Failed to load media asset at " + url + " (error code = " + status + ")."
                    }
                    throw Error(msg);
                }

                async function createRequest(url, mediaType, callbackFunction, args) {
                    let request = new XMLHttpRequest();
                    request.open("GET", url, true);

                    if (mediaType == "audio" || mediaType == "html") {
                        request.responseType = "arraybuffer";
                    } else if (mediaType == "image" || mediaType == "video") {
                        request.responseType = "blob";
                    } else {
                        throw Error("Unsupported media type: " + mediaType);
                    }

                    // Make sure all batches are processed as arraybuffers
                    if (args["batchId"]) {
                        request.responseType = "arraybuffer";
                    }

                    request.onprogress = function (e) {
                        psynet.media.downloadProgress.set(args["mediaType"], args["fileId"], e.loaded * 100 / e.total);
                    };

                    return new Promise((resolve) => {
                        request.onload = async function () {
                            if (request.status === 200) {
                                await callbackFunction(request.response, args);
                                resolve();
                            } else {
                                reportRequestError(url, request.status);
                            }
                        };

                        request.send();
                    });
                }

                checkRequests = function (requests) {
                    // - Checks for missing URLs in batches.
                    // - Checks for duplicated IDs
                    let ids = [];

                    let log = function (id) {
                        var value = requests[id];
                        if (psynet.utils.isDict(value)) {
                            logBatch(value);
                        } else {
                            logStimulus(id);
                        }
                    };

                    let logStimulus = function (id) {
                        if (psynet.utils.keyExistsInArray(id, ids)) {
                            throw Error("The ID you specified `" + id + "` already exists! IDs must be unique!");
                        }
                        ids.push(id);
                    };

                    let logBatch = function (batch) {
                        if (!("url" in batch)) {
                            throw Error('Each batch object must contain a "url" attribute.');
                        }
                        if (!("ids" in batch)) {
                            throw Error('Each batch object must contain an "ids" attribute specifying its constituent stimuli.');
                        }
                        let stimulusIds = batch["ids"];
                        stimulusIds.forEach(logStimulus);
                    };

                    Object.keys(requests).forEach(log);
                };

                let createAudioStimulus = function (data, stimulusId, handler) {
                    psynet.log.debug("Decoding sound " + stimulusId + "...");

                    return new Promise((resolve) => {
                        psynet.media.audioContext.decodeAudioData(data, function (buffer) {
                            psynet.log.debug("Sound " + stimulusId + " decoded.");

                            let out = psynet.media.data.audio[stimulusId];
                            out.buffer = buffer;

                            out.play = function (providedOptions) {
                                let options = {
                                    fadeIn: 0.0,
                                    fadeOut: 0.0,
                                    startDelay: 0.01,
                                    gain: 1,
                                    loop: false,
                                    start: null,
                                    end: null
                                };

                                Object.assign(options, providedOptions);

                                const start = (options.start === null) ? 0.0 : options.start;
                                const duration = (options.end === null) ? undefined : options.end - options.start;

                                let sound = {
                                    source: psynet.media.audioContext.createBufferSource(),
                                    gainNode: psynet.media.audioContext.createGain(),
                                    startTime: psynet.media.audioContext.currentTime + options.startDelay,
                                    options: options,
                                    manuallyStopped: false,
                                    duration: (duration === undefined) ? buffer.duration : duration,
                                    stimulusId: stimulusId,
                                    onEnd: null
                                };

                                sound.source.buffer = buffer;

                                sound.source.connect(sound.gainNode);
                                sound.gainNode.connect(psynet.media.audioContext.destination);

                                sound.gainNode.gain.setValueAtTime(
                                    0.001,
                                    sound.startTime
                                );

                                sound.source.start(sound.startTime, start, duration);

                                if (options.gain > 1e-10) {
                                    sound.gainNode.gain.exponentialRampToValueAtTime(
                                        options.gain,
                                        psynet.media.audioContext.currentTime + options.startDelay + options.fadeIn
                                    );
                                }

                                let stopTimer;

                                if (sound.options.fadeOut > 0.0) {
                                    stopTimer = setTimeout(
                                        () => sound.stop({fadeOut: options.fadeOut, manual: false}),
                                        1000 * (options.startDelay + sound.duration - options.fadeOut)
                                    );
                                }

                                sound.stop = function (providedOptions) {
                                    let options = {
                                        fadeOut: sound.options.fadeOut,
                                        manual: true
                                    };

                                    Object.assign(options, providedOptions);

                                    clearTimeout(stopTimer);

                                    psynet.log.debug("Stopping audio " + sound.stimulusId + ".");

                                    sound.manuallyStopped = options.manual;

                                    let gainNow = sound.gainNode.gain.value;
                                    let timeNow = psynet.media.audioContext.currentTime;

                                    if (sound.gainNode.gain.value > 0.001) {
                                        sound.gainNode.gain.setValueAtTime(gainNow, timeNow);
                                        sound.gainNode.gain.exponentialRampToValueAtTime(0.001, timeNow + options.fadeOut);
                                    }

                                    return new Promise((resolve) => {
                                        setTimeout(() => {
                                            sound.source.stop();
                                            resolve();
                                        }, options.fadeOut * 1000);
                                    });
                                }

                                sound.source.addEventListener("ended", function () {
                                    clearTimeout(stopTimer);
                                    psynet.log.debug("Finished sound with ID = " + sound.stimulusId);
                                    psynet.media.sounds = psynet.media.sounds.filter(s => s !== sound);
                                    psynet.trial.registerEvent("audioFinished: " + sound.stimulusId);
                                    if (sound.options.loop && !sound.manuallyStopped) {
                                        psynet.log.debug("Looping sound with ID = " + out.stimulusId);
                                        out.play(sound.options);
                                    }
                                });

                                psynet.media.sounds.push(sound);
                                return sound;
                            };

                            out.stop = function (options) {
                                psynet.media.sounds.forEach(function (s) {
                                    if (s.stimulusId == stimulusId) {
                                        s.stop(options);
                                    }
                                });
                                return this
                            };

                            out.loaded = true;

                            resolve();
                        })
                    });
                };

                psynet.media.blobToArrayBuffer = async function (blob) {
                    let buffer = await new Response(blob).arrayBuffer();
                    return buffer;
                }

                // Used to add extra stimuli that aren't loaded as part of the initial media load
                // (needs refactoring in a general cross-media way)
                psynet.media.addExtraAudioStimulus = async function (buffer, stimulusId) {
                    initStimulus(stimulusId, "audio");
                    await createAudioStimulus(buffer, stimulusId, () => Promise.resolve());
                }

                let createMediaFromBuffer = {};
                createMediaFromBuffer.audio = function (data, args) {
                    psynet.media.downloadProgress.set(args.mediaType, args.fileId, 100);
                    return createAudioStimulus(data, args.stimulusId);
                };

                let getImageMetadataFromURL = function (url) {
                    return new Promise((resolve, reject) => {
                        let img = new Image();
                        img.onload = () => resolve({
                            width: img.width,
                            height: img.height
                        });
                        img.onerror = () => reject();
                        img.src = url;
                    });
                };

                function getMimeTypeFromArrayBuffer(arrayBuffer) {
                    const uint8arr = new Uint8Array(arrayBuffer)
                    var dec = new TextDecoder()
                    var text = dec.decode(uint8arr).toLowerCase().replace(' ', '')

                    if (text.includes('<svg')) {
                        return 'image/svg+xml'
                    } else {
                        return null;
                    }
                }

                createMediaFromBuffer.html = async function (data, args) {
                    let stimulusId = args["stimulusId"];
                    let mediaType = args["mediaType"];
                    let fileId = args["fileId"];

                    let player = document.getElementById(stimulusId);
                    if (player != null) {
                        psynet.log.debug(
                            "Associating " + mediaType + " '" + stimulusId
                            + "' with the player of the same name.'"
                        );
                        const uint8arr = new Uint8Array(data)
                        var dec = new TextDecoder()
                        player.innerHTML = dec.decode(uint8arr);
                     }
                    let out = psynet.media.data[mediaType][stimulusId];
                    out.player = player;
                    out.innerHTML = player.innerHTML;
                    // let metadata = await getSvgMetadataFromData(player.innerHTML);
                    // {#out.url = url;#}
                    // {#out.width = metadata.width;#}
                    // {#out.height = metadata.height;#}
                    // {#out.aspectRatio = out.width / out.height;#}
                    out.loaded = true;

                    psynet.media.downloadProgress.set(mediaType, fileId, 100);
                    return true;
                }

                createMediaFromBuffer.image = async function (data, args) {
                    let stimulusId = args["stimulusId"];
                    let mediaType = args["mediaType"];
                    let fileId = args["fileId"];

                    let player = document.getElementById(stimulusId);

                    if (player) {
                        psynet.log.debug(
                            "Associating " + mediaType + " '" + stimulusId
                            + "' with the player of the same name.'"
                        );
                    } else {
                        player = document.createElement("div");
                    }

                    if (!data.type) {
                        let mimeType = getMimeTypeFromArrayBuffer(data);
                        data = new Blob([data], { type: mimeType })
                    }

                    player.src = URL.createObjectURL(data);

                    let out = psynet.media.data[mediaType][stimulusId];
                    out.player = player;

                    let url = player.src;
                    let metadata = await getImageMetadataFromURL(url);
                    out.url = url;
                    out.width = metadata.width;
                    out.height = metadata.height;
                    out.aspectRatio = out.width / out.height;
                    out.loaded = true;

                    psynet.media.downloadProgress.set(mediaType, fileId, 100);
                    return true;
                }

                createMediaFromBuffer.video = async function (data, args) {
                    let stimulusId = args["stimulusId"];
                    let mediaType = args["mediaType"];
                    let fileId = args["fileId"];

                    let player = document.getElementById(stimulusId);
                    if (player != null) {
                        psynet.log.debug(
                            "Associating " + mediaType + " '" + stimulusId
                            + "' with the player of the same name.'"
                        );
                        data = new Blob([data])  // convert to blob
                        player.src = URL.createObjectURL(data);
                        player.classList.remove("loader");
                        player.load();
                        await psynet.waitForEventListener(player, "canplaythrough");
                    }

                    let out = psynet.media.data[mediaType][stimulusId];
                    out.player = player;
                    out.loaded = true;

                    psynet.media.downloadProgress.set(mediaType, fileId, 100);
                    return true;
                };

                let processMediaBatch = {};

                processMediaBatch.extract_stimuli = function (data, args) {
                    let stimulusIds = args["stimulusIds"];
                    let mediaType = args["mediaType"];
                    let fileId = args["fileId"];

                    function extractBuffer(src, start, length) {
                        // This function is used to find the start and end of each file
                        let dstU8 = new Uint8Array(length);
                        let srcU8 = new Uint8Array(src, start, length);
                        dstU8.set(srcU8);
                        return dstU8;
                    }

                    let numFiles = 0;
                    psynet.log.debug('Unpacking the ' + mediaType + ' batch "' + fileId + '".');
                    let bb = new DataView(data);
                    let offset = 0;
                    let promises = [];

                    while (offset < bb.byteLength) {
                        let stimulusId = stimulusIds[numFiles];
                        let length = bb.getUint32(offset, true);
                        offset += 4;
                        let media = extractBuffer(data, offset, length);
                        offset += length;
                        numFiles++;

                        if (numFiles > stimulusIds.length) {
                            throw Error(
                                "Too many stimuli found in batch file (" +
                                "expected " + stimulusIds.length +
                                ", got at least" + numFiles + ")."
                            );
                        }

                        args = {
                            stimulusId: stimulusId,
                            mediaType: mediaType,
                            fileId: fileId
                        };

                        // In contrast to audio, video, and image are not loaded into the DOM, so we need to create a player
                        if (mediaType === 'html') {
                            $('#media-container').prepend('<div style="display: none" class="html" id="' + stimulusId + '"></div>')
                        }

                        if (mediaType === 'image') {
                            $('#media-container').prepend('<img style="display: none" class="image" id="' + stimulusId + '"></img>')
                        }

                        else if (mediaType === 'video') {
                            $('#media-container').prepend('<video style="display: none" class="video" id="' + stimulusId + '"></video>')
                        }

                        promises.push(createMediaFromBuffer[mediaType](media.buffer, args));
                    }
                    if (numFiles < stimulusIds.length) {
                        throw Error(
                            "Too few stimuli found in batch file (" +
                            "expected " + stimulusIds.length +
                            ", got " + numFiles + ")."
                        );
                    }
                    return Promise.all(promises);
                };

                const SUPPORTED_MODALITIES = ['audio', 'image', 'html', 'video'];
                SUPPORTED_MODALITIES.forEach(function (modality){
                    processMediaBatch[modality] = function (data, args) { //, ids,) {
                        return processMediaBatch.extract_stimuli(data, args);
                    };
                })

                let initModal = function () {
                    let modal = document.getElementById("help-modal");
                    let helpBtn = document.getElementById("help-button");
                    let span = document.getElementsByClassName("close")[0];

                    if (modal !== null) {
                        helpBtn.onclick = function () {
                            modal.style.display = "block";
                        };

                        span.onclick = function () {
                            modal.style.display = "none";
                        };

                        window.onclick = function (event) {
                            if (event.target === modal) {
                                modal.style.display = "none";
                            }
                        }
                    }
                };

                psynet.estimateDownloadSpeed = function () {
                    // Returns estimated download speed in megabits/second
                    // (at the time of writing, this is capped at 10).
                    let connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
                    if (connection) {
                        return connection.downlink
                    } else {
                        return null;
                    }

                }

                psynet.pageLoaded = false;

                let waitForPageLoad = function () {
                    return new Promise(resolve => {
                        window.addEventListener("load", () => resolve());
                    });
                };

                let waitForBrowserCheck = async function () {
                    return await browser.validate();
                };

                psynet.initPage = async function () {
                    console.log("Initialising page...");

                    psynet.registerErrorHandler();
                    initModal();

                    psynet.response.disable();
                    psynet.submit.disable();
                    $(".wait-for-media-load").attr("disabled", "disabled");

                    await waitForPageLoad();
                    let correctBrowser = await waitForBrowserCheck();

                    psynet.pageLoadTime = new Date();
                    psynet.pageLoaded = true;
                    psynet.assignmentId = "{{ participant.assignment_id }}";
                    psynet.participantId = {{ participant.id }};
                    psynet.uniqueId = "{{ participant.unique_id }}";

                    updateProgressAndReward();

                    if (correctBrowser) {
                        setTimeout(() => psynet.trial.init(), 25);
                    }
                };

                psynet.registerErrorHandler = function () {
                    window.onerror = function (msg, url, line, col, error) {
                        if (error) {
                            psynet.log.error(error.stack);
                        }
                    }
                };

                psynet.nextPagePending = false;

                psynet.submitResponse = async function (onRejection) {
                    if (psynet.stageResponse) {
                        psynet.stageResponse();
                    }
                    if (typeof retrieveResponse == "undefined") {
                        await psynet.nextPage();
                    } else {
                        let response = retrieveResponse();
                        await psynet.nextPage(
                            response.rawAnswer,
                            response.metadata,
                            response.blobs,
                            onRejection,
                        );
                    }
                }

                psynet.nextPage = async function (rawAnswer, metadata, blobs, onRejection) {
                    if (!psynet.pageLoaded) {
                        psynet.alert(
                            "{{ pgettext("timeline_problem", "Please wait for the page to load.") }} {{ pgettext("timeline_problem", "Try again in a couple of seconds.") }}"
                        )
                        return false;
                    }
                    if (psynet.nextPagePending) {
                        psynet.log.debug("Skipping nextPage request as nextPage is already pending.");
                        return false;
                    }
                    psynet.nextPagePending = true;
                    // rawAnswer, metadata, and blobs default to psynet.response.staged if they
                    // are not provided explicitly.
                    //
                    // Returns true if the answer passes validation checks.
                    if (rawAnswer === undefined) {
                        rawAnswer = psynet.response.staged.rawAnswer;
                    }
                    if (metadata === undefined) {
                        metadata = psynet.response.staged.metadata;
                    }
                    if (blobs === undefined) {
                        blobs = psynet.response.staged.blobs;
                    }
                    {% if experiment.with_lucid_recruitment() %}
                        psynet.removeBeforeUnloadEventListener();
                    {% endif %}
                    let passedValidation = await submitGenericResponse(rawAnswer, metadata, blobs, onSuccessResponse, onErrorResponse, onRejection);
                    if (!passedValidation) {
                        psynet.nextPagePending = false;
                    }
                    return passedValidation
                };

                psynet.alert = function (text) {
                    return new Promise((resolve) => {
                        let alertModal = $("#alert-modal");
                        let alertButton = $("#alert-button");
                        let alertText = $("#alert-message");
                        alertText.text(text);
                        alertModal.css("display", "block");
                        alertButton.click(async function () {
                            alertModal.css("display", "none");
                            resolve();
                        });
                    });
                };

                let onSuccessResponse = function (request, onRejection) {
                    let response = JSON.parse(request.response);
                    let passedValidation;
                    if (response.submission === "approved") {
                        psynet.log.debug("Response received successfully.");
                        let shouldRefreshPage = !(
                            response.page.attributes && psynet.page.attributes &&
                            response.page.attributes.session_id == psynet.page.attributes.session_id
                        );
                        if (shouldRefreshPage) {
                            window.location = "/timeline?unique_id=" + psynet.uniqueId;
                        } else {
                            psynet.page = response.page;
                            psynet.trial.registerEvent("pageUpdated");
                            psynet.nextPagePending = false;
                        }
                        passedValidation = true;
                    } else if (response.submission === "rejected") {
                        psynet.log.debug("Response rejected.");
                        psynet.alert(response.message);
                        psynet.response.enable();
                        psynet.submit.enable();
                        if (onRejection) {
                            onRejection(response);
                        }
                        passedValidation = false;
                    } else {
                        throw Error("Received a malformed response.");
                    }
                    return passedValidation
                };

                onPageUpdated = function (event) {
                    console.log("Dispatched 'onPageUpdated' event. Sending data to Unity:\nattributes:" + psynet.page.attributes + "\ncontents:" + psynet.page.contents);
                    unityInstance.SendMessage(
                        "PsynetObj",
                        "GetData",
                        JSON.stringify({
                            attributes: psynet.page.attributes,
                            contents: psynet.page.contents,
                        })
                    );
                };

                let onErrorResponse = function (request) {
                    dallinger.error({
                        "data": {
                            "participant_id": {{ participant.id }}
                        }
                    });
                };

                let addBlobs = function (formData, blobs) {
                    for (let [key, value] of Object.entries(blobs)) {
                        if (key === "json") {
                            throw Error("Blobs may not be named 'json'.");
                        }
                        formData.append(key, value);
                    }
                };

                let prepareJsonSubmission = function (rawAnswer, metadata) {
                    var currentTime = new Date();

                    var allMetadata = {
                        "time_taken": (currentTime - psynet.pageLoadTime) / 1000,
                        "platform": platform.toString(),
                        "download_speed_megabits_per_sec": psynet.estimateDownloadSpeed(),
                        "event_log": psynet.trial.eventLog
                    };

                    if (metadata !== undefined) {
                        for (var x in metadata) {
                            allMetadata[x] = metadata[x];
                        }
                    }

                    return JSON.stringify({
                        "participant_id": psynet.participantId,
                        "page_uuid": pageUuid,
                        "assignment_id": psynet.assignmentId,
                        "unique_id": psynet.uniqueId,
                        "raw_answer": rawAnswer,
                        "metadata": allMetadata
                    });
                };

                let submitGenericResponse = function (rawAnswer, metadata, blobs, onSuccessResponse, onErrorResponse, onRejection) {
                    // rawAnswer - an arbitrary Javascript object (not necessarily an Object) to be sent to JSON
                    // blobs - optional Object, each attribute should be a blob to upload.
                    //       - Note that 'json' is not a permitted name for an attribute
                    //
                    // Returns true if the answer passed validation checks, false otherwise.
                    $(" .response, .submit ").prop("disabled", true);

                    json = prepareJsonSubmission(rawAnswer, metadata);

                    var formData = new FormData();
                    formData.append("json", json);

                    if (blobs !== undefined) {
                        addBlobs(formData, blobs);
                    }

                    return new Promise((resolve) => {
                        let request = new XMLHttpRequest();
                        request.onreadystatechange = function () {
                            if (request.readyState === 4) {
                                let passedValidation;
                                if (request.status === 200) {
                                    psynet.log.debug("Response was successfully received.");
                                    passedValidation = onSuccessResponse(request, onRejection);
                                } else {
                                    psynet.log.debug("Something went wrong.");
                                    onErrorResponse(request);
                                    passedValidation = false;
                                }
                                resolve(passedValidation);
                            }
                        };
                        request.open("POST", "/response");
                        request.send(formData);
                    });
                };

                psynet.trialProgress = (() => {
                    let config = {{ trial_progress_display_config | tojson }};
                    let opacities = {light: 0.25, medium: 0.6, dark: 1.0};

                    let init = function () {
                        config.stages.forEach((stage) => stage.html = {});
                        config.stages.forEach((stage) => {
                            stage.html.past = addBarSegment(stage.color, opacities.dark);
                        });
                        config.stages.forEach((stage) => {
                            stage.html.future = addBarSegment(stage.color, opacities.light);
                        });
                        config.stages.forEach((stage) => {
                            stage.update = function (elapsed) {
                                let proportionOfStageComplete = bound((elapsed - stage.time[0]) / stage.duration, 0, 1);
                                let proportionOfBarComplete = (proportionOfStageComplete * stage.duration) / config.duration;
                                stage.html.past.setWidth(100 * proportionOfBarComplete);
                                let proportionOfStageIncomplete = 1 - proportionOfStageComplete;
                                let proportionOfBarIncomplete = (proportionOfStageIncomplete * stage.duration) / config.duration
                                stage.html.future.setWidth(100 * proportionOfBarIncomplete);
                                if (proportionOfStageComplete > 0) {
                                    if (stage.persistent || proportionOfStageComplete < 1) {
                                        activeStage = stage;
                                    } else {
                                        activeStage = null;
                                    }
                                }
                            };
                        });
                        setInterval(update, 5);
                    };

                    let bound = function (x, min, max) {
                        return Math.max(min, Math.min(x, max));
                    };

                    let addBarSegment = function (color, opacity) {
                        let bar = $("#trial-progress-bar");
                        let segment = $("<div/>", {
                            "class": "progress-bar trial-progress-bar-segment",
                            role: "progressbar",
                            style: "width: 0%; opacity: " + opacity + "; background-color: " + color + ";",
                            "aria-valuenow": "0",
                            "aria-valuemin": "0",
                            "aria-valuemax": "100"
                        });
                        bar.append(segment);
                        segment.setWidth = function (percent) {
                            segment.css("width", percent + "%");
                            segment.attr("aria-valuenow", percent);
                        };
                        return segment;
                    };

                    let activeStage = null;
                    let startTime = null;

                    let start = function () {
                        startTime = psynet.trial.getGlobalTime();
                    };

                    let stop = function () {
                        startTime = null;
                        activeStage = null;
                    };

                    let setText = function (content, color) {
                        if (content == "") {
                            resetText();
                        } else {
                            let text = $("#trial-progress-caption-contents");
                            text.text(content);
                            if (color) {
                                text.css("color", color);
                            }
                        }
                    };

                    let resetText = function () {
                        let text = $("#trial-progress-caption-contents");
                        text.html("&nbsp;");
                    };

                    let update = function () {
                        let elapsed = startTime !== null ? psynet.trial.getGlobalTime() - startTime : 0.0
                        config.stages.forEach((s) => s.update(elapsed));
                        if (config.stages.length > 0) {
                            if (activeStage) {
                                setText(activeStage.caption, activeStage.color);
                            } else {
                                resetText();
                            }
                        }
                    };

                    psynet.trial.onEvent("trialConstruct", init, {priority: -1000});
                    psynet.trial.onEvent(config.start, start);
                    psynet.trial.onEvent("trialStopped", stop);
                    psynet.trial.onEvent("pageUpdated", onPageUpdated);

                    return {
                        stages: config.stages,
                        setText: setText,
                        start: start,
                        stop: stop
                    };
                })();

                psynet.audio = psynet.media.data.audio;
                psynet.image = psynet.media.data.image;
                psynet.html = psynet.media.data.html;
                psynet.video = psynet.media.data.video;

                return psynet
            }()
        )

        let updateProgressAndReward = function () {
            if (psynet.participantId !== undefined) {
                $.get('/timeline/progress_and_reward', {'participantId': psynet.participantId})
                    .done(function (data) {
                        let progressPercentage = data['progressPercentage'];
                        let progressPercentageStr = progressPercentage + '%';
                        $('#timeline-progress-bar').text(progressPercentageStr);
                        $('#timeline-progress-bar').css("width", progressPercentageStr);
                        $('#timeline-progress-bar').attr("aria-valuenow", progressPercentage);

                        if (data['reward'] !== undefined) {
                            if (data['reward']['performance'].toFixed(2) > 0) {
                                $('#time-reward').text(data['reward']['time'].toFixed(2));
                                $('#performance-reward').text(data['reward']['performance'].toFixed(2));
                                $("#reward-details").show();
                            }
                            $('#total-reward').text(data['reward']['total'].toFixed(2));
                        }
                    });
            }
        }

        if (dynamicallyUpdateProgressBarAndReward) {
            setInterval(updateProgressAndReward, 1000);
        }

        function alertParticipantOpenedDevtools() {
            console.warn(
                'You have opened the developer tools. ' +
                'The experimenter is informed about this possible misconduct. ' +
                'You might be excluded from the experiment.'
            );
        }

        function writeToDbParticipantOpenedDevtools() {
            dallinger.post(
                "/participant_opened_devtools/{{ unique_id }}"
            );
        }

        function logParticipantOpenedDevtools() {
            psynet.log.warning('Participant ' + psynet.participantId + " opened the browser's developer tools.")
        }

        function isConsoleOpen() {
            let consoleOpen = false;
            let f = function () {
            }
            f.toString = function () {
                consoleOpen = true;
            }
            console.profile(f);
            console.profileEnd(f);
            return consoleOpen;
        }

        {% if config.check_participant_opened_devtools %}
            let warnedDevtools = false;
            setInterval(() => {
                if (!warnedDevtools && isConsoleOpen()) {
                    logParticipantOpenedDevtools();
                    writeToDbParticipantOpenedDevtools();
                    alertParticipantOpenedDevtools();
                    warnedDevtools = true;
                }
            }, 1000);
        {% endif %}
    </script>

{% endblock %}

{% block libs %}
    {{ super() }}
    <script src="{{ url_for('static', filename='scripts/platform.min.js') }}"></script>
    <script src="{{ url_for('static', filename='scripts/survey-jquery/survey-jquery.js') }}"></script>
{% endblock %}

{% block scripts %}
    {{ super() }}

    <script>
        psynet.initPage();
    </script>

    {% for x in scripts %}
        <script>
            {{ x }}
        </script>
    {% endfor %}

    {% if experiment.with_lucid_recruitment() %}
        <script>
            {
                // Terminate on 'page reload', 'window/tab close', and 'no focus' with timeouts and
                // polling frequency given in milliseconds.
                let NO_FOCUS_TIMEOUT = toMilliseconds({{ experiment.recruiter.no_focus_timeout_in_s }});
                let NO_FOCUS_TIMEOUT_REASON = "no-focus-timeout-";
                {% if aggressive_termination_on_no_focus %}
                    NO_FOCUS_TIMEOUT = toMilliseconds({{ experiment.recruiter.aggressive_no_focus_timeout_in_s }});
                    NO_FOCUS_TIMEOUT_REASON = "aggressive-" + NO_FOCUS_TIMEOUT_REASON;
                {% endif %}
                const OVERALL_TIMEOUT = {{ experiment.recruiter.termination_time_in_s }};
                const POLLING_INTERVAL = 1000;
                let checkTriedToLeaveIntervalID = setInterval(checkTriedToLeave, POLLING_INTERVAL);
                let clockIntervalID = setInterval(updateClocks, POLLING_INTERVAL);
                let triedToLeave = false;
                let noFocusSince = 0;
                let noActivitySince = 0;
                let secondsLeft = {{ experiment.recruiter.time_until_termination_in_s(participant.assignment_id) }};
                let assignmentId = "{{ worker_id }}";

                window.addEventListener("beforeunload", beforeunloadFunction);

                // Proceed to next page
                $(".btn, .sd-btn").click(function () {
                    psynet.removeBeforeUnloadEventListener();
                });

                $("#terminate-button").click(function () {
                    terminateParticipant('terminate-button');
                });

                function toMilliseconds(seconds) {
                    return seconds * 1000;
                }

                function beforeunloadFunction(event) {
                    event.returnValue = `Are you sure you want to leave?`;
                    triedToLeave = true;
                }

                function terminateParticipant(reason) {
                    psynet.removeBeforeUnloadEventListener();
                    clearInterval(checkTriedToLeaveIntervalID);
                    clearInterval(clockIntervalID);
                    return window.location.replace("/terminate_participant?participant_id=" + {{ participant.id }} + "&reason=" + reason)
                }

                function checkTriedToLeave() {
                    if (triedToLeave) {
                        terminateParticipant("user-tried-to-leave");
                    }
                }


                // Terminate due to inactivity
                function updateClocks() {
                    let msg = ''

                    // No focus
                    if (document.hasFocus()) {
                        noFocusSince = 0;
                    } else {
                        noFocusSince += POLLING_INTERVAL;
                        if (noFocusSince > NO_FOCUS_TIMEOUT) {
                            terminateParticipant(NO_FOCUS_TIMEOUT_REASON + NO_FOCUS_TIMEOUT / 1000 + "s");
                        } else {
                            msg = "No focus: " + noFocusSince / 1000 + "/" + NO_FOCUS_TIMEOUT / 1000 + "s";
                        }
                    }

                    // No activity
                    noActivitySince += POLLING_INTERVAL;
                    noActivityTimeout = {{ experiment.recruiter.inactivity_timeout_in_s }} * 1000
                    if (noActivitySince > noActivityTimeout) {
                        terminateParticipant("inactivity-timeout-" + {{ experiment.recruiter.inactivity_timeout_in_s }} + "s");
                    }
                    msg += " No activity: " + noActivitySince / 1000 + "/" + noActivityTimeout / 1000 + "s";

                    // Overall timeout
                    if (secondsLeft <= 0) {
                        psynet.removeBeforeUnloadEventListener();
                        terminateParticipant("overall-timeout-" + OVERALL_TIMEOUT + "s");
                    }
                    secondsLeft -= POLLING_INTERVAL / 1000;
                    msg += " Overall timeout: " + secondsLeft + "/" + OVERALL_TIMEOUT + "s";
                }

                // Events that reset the inactivity time
                const events = ["click", "keypress", "load", "mousedown", "mousemove", "touchstart"];
                for (let i = 0; i < events.length; i++) {
                    window.addEventListener(events[i], function (){
                        noActivitySince = 0;
                    });
                }
            }
        </script>
    {% endif %}
{% endblock %}
