Papercupsを使ってjsPsychにオンライン質問機能を実装する

実用性は微妙かもですが、ひとまず情報共有します


これはpsyJS Advent Calendar 2021の18日目の記事です。


きっかけ

oTree(集団実験や経済ゲーム実験をオンライン用に開発するためのライブラリおよびプラットフォーム)の資料を読んでいたところ、「参加者と実験者間のチャット」という記述を見つけました。oTreeの公式サイトでは、Papercupsというサービスを利用してチャットアプリを埋め込む例が示されています(リンク先の"chat_with_experimenter")。

Papercupsとは、オンラインチャットを実装するためのサービスです。百聞は一見に如かずで、まずはPapercupsのデモページをご覧ください。

どうでしょうか? ウェブ実験、とくに集団実験を実施する際、参加者とのチャット機能があれば便利そうな気がします。そこで、oTree公式に従ってPapercupsを試したところ、かなり簡単にチャット機能を実装することができました。

導入がかなり簡単だったので「jsPsychでも同じことできるのでは?」と思い至りました。そこで少しテストしたところ、一応jsPsychでもチャット機能を実装できました。そんなわけで、このたび情報を共有しようと思った次第です。

oTreeでの実装に関しては、公式ページで紹介されています。oTreeに関心のある方はそちらをご覧ください。


1. jsPsychのコードを書く

まず、普通のjsPsych(ver. 7.1)のコードを書きましょう。

公式サイトのチュートリアルの一番下にあるコードをコピーし、適当な名前のHTMLファイルを作ってください。

つぎに、青い円とオレンジの円の画像をimg/blue.pngimg/orange.pngとして保存してください。これらの画像がないとプログラムが動きません。

保存したHTMLファイルを開き、公式チュートリアルの実験課題が動くことを確認してください。


2. display_elementを指定する

さきほど作ったHTMLファイルのinitJsPsych()で、display_elementを指定しましょう。var jsPsych = ...から始まるコードを、下のように修正してください。

/* initialize jsPsych */
var jsPsych = initJsPsych({
  display_element: "my_awesome_task", //この行を追記
  on_finish: function() {
    jsPsych.data.displayData();
  }
});

つぎに、bodyタグの終わり(</body>)とscriptタグの先頭(<script>:jsPsychのコードを書いているタグ)の間に、以下のコードを追記してください。ちなみに、https://kywch.github.io/jsPsych-in-Qualtrics/hello-world/をかなり参考にしました。

<!-- 下記のようにstyleを指定しないと、呈示位置がおかしくなります -->
<style>
  #my_awesome_task {
    position: fixed;
    left: 1vw;
    top: 1vh;
    height: 98vh;
    width: 98vw;
    background-color: white;
    border-radius: 15px;
    z-index: 0;
    overflow-y: hidden;
    overflow-x: hidden;
  }
</style>
<div id='my_awesome_task'></div>

ここでまたHTMLファイルを開き、プログラムが動くことを確認してください。

なお、自分で試した範囲内だと、チャット機能を埋め込むにはdisplay_elementを指定しないといけないようです。もしかしたら、デフォルトの設定でもうまくやれる方法があるのかもしれません。ただ、とりあえず今回は「動くからOKでしょ」ということにします。


3. Papercupsの設定

チャット機能を実装するには、Paperucupsにサインアップする必要があります。手順は以下のとおりです。

  1. Papercupsのページでアカウントを作成

  2. Papercupsにログイン。細かい点は違うかもしれませんが、下のようなページが表示されるはず

ログイン後の画面

  1. <head></head>の間に下記のコードを挿入。なお、5行目のaccountIdでは、自分のPapercupsアカウントのトークンを指定する必要があります
<script>
  window.Papercups = {
    config: {
      // Papercupsのアカウントトークンをxの部分に貼り付ける
      accountId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx',
      title: 'Welcome to Papercups!',
      subtitle: 'Ask us anything in the chat window below 😊',
      newMessagePlaceholder: 'Start typing...',
      primaryColor: '#0693e3', // デフォルトの色はまぶしいので、ここだけ変えてます
      // Optionally pass in a default greeting
      greeting: 'Hi there! How can I help you?',
      // Optionally pass in metadata to identify the customer
      customer: {
        name: 'Test User',
        email: 'test@test.com',
        external_id: '123',
        metadata: {version: 1, plan: 'premium'}, // Custom fields go here
      },
      // Optionally specify the base URL
      baseUrl: 'https://app.papercups.io',
      // Add this if you want to require the customer to enter
      // their email before being able to send you a message
      requireEmailUpfront: true,
      // Add this if you want to indicate when you/your agents
      // are online or offline to your customers
      showAgentAvailability: true,
    },
  };
</script>
<script
  type="text/javascript"
  async
  defer
  src="https://app.papercups.io/widget.js"
></script>

今回の例だと、<link href="https://unpkg.com/jspsych@7.1.2/css/jspsych.css" rel="stylesheet" type="text/css" />の下に挿入することになります。オプションなどについては、Papercupsの公式資料をご覧ください。


最終的なコードは以下のようになります。

<!DOCTYPE html>
<html>
  <head>
    <title>My experiment</title>
    <script src="https://unpkg.com/jspsych@7.1.2"></script>
    <script src="https://unpkg.com/@jspsych/plugin-html-keyboard-response@1.1.0"></script>
    <script src="https://unpkg.com/@jspsych/plugin-image-keyboard-response@1.1.0"></script>
    <script src="https://unpkg.com/@jspsych/plugin-preload@1.1.0"></script>
    <link href="https://unpkg.com/jspsych@7.1.2/css/jspsych.css" rel="stylesheet" type="text/css" />
    <script>
      window.Papercups = {
        config: {
          // Papercupsのアカウントトークンを下に貼り付ける
          accountId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx',
          title: 'Welcome to Papercups!',
          subtitle: 'Ask us anything in the chat window below 😊',
          newMessagePlaceholder: 'Start typing...',
          primaryColor: '#0693e3', // デフォルトの色はまぶしいので、ここだけ変えてます
          // Optionally pass in a default greeting
          greeting: 'Hi there! How can I help you?',
          // Optionally pass in metadata to identify the customer
          customer: {
            name: 'Test User',
            email: 'test@test.com',
            external_id: '123',
            metadata: {version: 1, plan: 'premium'}, // Custom fields go here
          },
          // Optionally specify the base URL
          baseUrl: 'https://app.papercups.io',
          // Add this if you want to require the customer to enter
          // their email before being able to send you a message
          requireEmailUpfront: true,
          // Add this if you want to indicate when you/your agents
          // are online or offline to your customers
          showAgentAvailability: true,
        },
      };
    </script>
    <script
      type="text/javascript"
      async
      defer
      src="https://app.papercups.io/widget.js"
    ></script>
  </head>
  <body></body>
  <style>
    #my_awesome_task {
      position: fixed;
      left: 1vw;
      top: 1vh;
      height: 98vh;
      width: 98vw;
      background-color: white;
      border-radius: 15px;
      z-index: 0;
      overflow-y: hidden;
      overflow-x: hidden;
    }
  </style>
  <div id='my_awesome_task'></div>
  <script>

    /* initialize jsPsych */
    var jsPsych = initJsPsych({
      display_element: "my_awesome_task",
      on_finish: function() {
        jsPsych.data.displayData();
      }
    });

    /* create timeline */
    var timeline = [];

    /* preload images */
    var preload = {
      type: jsPsychPreload,
      images: ['img/blue.png', 'img/orange.png']
    };
    timeline.push(preload);

    /* define welcome message trial */
    var welcome = {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: "Welcome to the experiment. Press any key to begin."
    };
    timeline.push(welcome);

    /* define instructions trial */
    var instructions = {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: `
        <p>In this experiment, a circle will appear in the center
        of the screen.</p><p>If the circle is <strong>blue</strong>,
        press the letter F on the keyboard as fast as you can.</p>
        <p>If the circle is <strong>orange</strong>, press the letter J
        as fast as you can.</p>
        <div style='width: 700px;'>
        <div style='float: left;'><img src='img/blue.png'></img>
        <p class='small'><strong>Press the F key</strong></p></div>
        <div style='float: right;'><img src='img/orange.png'></img>
        <p class='small'><strong>Press the J key</strong></p></div>
        </div>
        <p>Press any key to begin.</p>
      `,
      post_trial_gap: 2000
    };
    timeline.push(instructions);

    /* define trial stimuli array for timeline variables */
    var test_stimuli = [
      { stimulus: "img/blue.png",  correct_response: 'f'},
      { stimulus: "img/orange.png",  correct_response: 'j'}
    ];

    /* define fixation and test trials */
    var fixation = {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: '<div style="font-size:60px;">+</div>',
      choices: "NO_KEYS",
      trial_duration: function(){
        return jsPsych.randomization.sampleWithoutReplacement([250, 500, 750, 1000, 1250, 1500, 1750, 2000], 1)[0];
      },
      data: {
        task: 'fixation'
      }
    };

    var test = {
      type: jsPsychImageKeyboardResponse,
      stimulus: jsPsych.timelineVariable('stimulus'),
      choices: ['f', 'j'],
      data: {
        task: 'response',
        correct_response: jsPsych.timelineVariable('correct_response')
      },
      on_finish: function(data){
        data.correct = jsPsych.pluginAPI.compareKeys(data.response, data.correct_response);
      }
    };

    /* define test procedure */
    var test_procedure = {
      timeline: [fixation, test],
      timeline_variables: test_stimuli,
      repetitions: 5,
      randomize_order: true
    };
    timeline.push(test_procedure);

    /* define debrief */
    var debrief_block = {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: function() {

        var trials = jsPsych.data.get().filter({task: 'response'});
        var correct_trials = trials.filter({correct: true});
        var accuracy = Math.round(correct_trials.count() / trials.count() * 100);
        var rt = Math.round(correct_trials.select('rt').mean());

        return `<p>You responded correctly on ${accuracy}% of the trials.</p>
          <p>Your average response time was ${rt}ms.</p>
          <p>Press any key to complete the experiment. Thank you!</p>`;

      }
    };
    timeline.push(debrief_block);

    /* start the experiment */
    jsPsych.run(timeline);

  </script>
</html>

HTMLファイルを開き、画面右下のチャット欄を開くと、下のような感じになるはずです。また、このチャット欄からメッセージを送信すると、自分のPapercupsアカウントに実際にメッセージが届きます。

完成

なお、今回はパスしましたが、Papercupsのオプション(external_id)に参加者のIDを指定すれば、各参加者ごとにチャットを管理できるようになります。


現時点で微妙かもしれないところ

なんとなく夢ありそうな感じがしたので、jsPsychにチャット機能を実装してみました。しかし、現時点で少し微妙かもしれない点があります。


1. 無料プランが実用に耐えるかは未知数

Papercupsは完全フリーのソフトウェアというわけではなく、あくまで一企業のサービスです。したがって、課金をしないと使えない機能もあります(有料プラン一覧)。

まだ実戦でPapercupsを使ったことがないので、無料プランが実用に耐えるかどうかは正直わかりません。一方で、有料と言っても3000〜10000円/月なので、実験を実施する月だけ課金するのが良いのかもしれません。ここらへんは追々見ていきたいです。


2. 実験中、チャットボタンが表示されっぱなしになる

oTreeでは、各フェーズに応じたHTMLファイルを作成します。そのため、「実験中のある特定のフェーズ(例:教示画面)ではチャットボタンを表示させ、課題中は非表示にする」ということができます(おそらく)。

一方jsPsychでは、チャットボタンが実験中ずっと表示されっぱなしになってしまいます。集中を要する知覚課題では、参加者の気が散って結果に悪影響が出る可能性もあります。

そもそも、教示や課題の内容が自明な実験では、チャット機能が不要なはずです。チャットを導入せずに済むなら、それに越したことはないでしょう。

逆に、課題やルールが複雑な実験(例:社会的意思決定を伴う課題)では、課題中でもチャット欄を表示させておいたほうが良いのかもしれません。いずれにせよ、チャット機能を導入することでメリット・デメリットが生じるはずなので、それらをよく考慮する必要がありそうです。

ちなみに、もしかしたらjsPsychでも「教示の間だけチャットボタンを表示させる」みたいなことが可能なのかもしれませんが、現段階では方法は不明です。external-htmlプラグインとか使えば良さそうな気がしますが、あまり自信はないです。ここは追々調べたいと思います。


まとめ

Papercupsというサービスを使うと、ウェブ実験(oTree、jsPsych、あとたぶんlab.js?)にチャット機能を導入することができます。実装自体はかなり簡単なので、使い勝手が気になる場合はぜひ試してみてください。

関連項目