Add support for judging of equivalent answers
authorCarl Worth <cworth@cworth.org>
Thu, 11 Jun 2020 14:25:15 +0000 (07:25 -0700)
committerCarl Worth <cworth@cworth.org>
Thu, 11 Jun 2020 14:25:15 +0000 (07:25 -0700)
This involves receiving a list of unique words that were submitted and
then allowing the user to group them into words that should be scored
as if identical. The interface implemented here uses sequential
clicking, but maybe it would be more intuitive to do drag-and-drop
instead? I may have to experiement with that.

empathy/empathy.css
empathy/empathy.jsx

index ef88072179b4637b77da786fdc1d9e266dfc4a9b..7e9bf3015a22e8778da33ea56991c37127172e98 100644 (file)
         color: var(--text-fg-on-accent);
     }
 }
+
+.ambiguity-group {
+    width: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background-color: var(--accent-color);
+    margin-bottom: 0.25em;
+    padding: 0;
+    border-radius: 10px;
+}
+
+.ambiguity-button {
+    width: 100%;
+    border-radius: 10px;
+    margin: 0;
+}
+
+.ambiguity-button.selected {
+    background-color: var(--accent-color-bright);
+    color: var(--text-fg-on-accent-bright);
+}
index 6f051f484f3ca3dfc71a21835ac787360192f434..60540c03b8246d84fcab3b5c9dc7507f500c10b4 100644 (file)
@@ -60,6 +60,8 @@ events.addEventListener("game-state", event => {
   window.game.set_active_prompt(state.active_prompt);
 
   window.game.set_scores(state.scores);
+
+  window.game.set_ambiguities(state.ambiguities);
 });
 
 events.addEventListener("prompt", event => {
@@ -80,6 +82,18 @@ events.addEventListener("answered", event => {
   window.game.set_players_answered(players_answered);
 });
 
+events.addEventListener("ambiguities", event => {
+  const ambiguities = JSON.parse(event.data);
+
+  window.game.set_ambiguities(ambiguities);
+});
+
+events.addEventListener("judged", event => {
+  const players_judged = JSON.parse(event.data);
+
+  window.game.set_players_judged(players_judged);
+});
+
 events.addEventListener("scores", event => {
   const scores = JSON.parse(event.data);
 
@@ -305,6 +319,136 @@ const LetsPlay = React.memo(props => {
   );
 });
 
+class Ambiguities extends React.PureComponent {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      word_groups: props.words.map(word => [word]),
+      submitted: false,
+      selected: null
+    };
+  }
+
+  async handle_submit() {
+    const response = await fetch_post_json(`judging/${this.props.prompt.id}`,
+                                           this.state.word_groups);
+
+    if (response.status === 200) {
+      const result = await response.json();
+      if (! result.valid) {
+        add_message("danger", result.message);
+        return;
+      }
+    } else {
+      add_message("danger", "An error occurred submitting your answers");
+      return;
+    }
+
+    this.setState({
+      submitted: true
+    });
+  }
+
+  handle_click(word) {
+    if (this.state.selected == word) {
+      /* Second click on same word removes the word from the group. */
+      const new_groups = this.state.word_groups.filter(
+        group => (! group.includes(this.state.selected)) || (group.length > 1)).map(
+          group => {
+            return group.filter(w => w !== this.state.selected);
+          }
+        );
+      this.setState({
+        selected: null,
+        word_groups: [...new_groups, [word]]
+      });
+    } else if (this.state.selected) {
+      /* Click of a second word groups the two together. */
+      const new_groups = this.state.word_groups.filter(
+        group => (! group.includes(word)) || (group.length > 1)).map(
+          group => {
+            if (group.includes(this.state.selected)) {
+              if (! group.includes(word))
+                return [...group, word];
+              else
+                return group;
+            } else {
+              return group.filter(w => w !== word);
+            }
+          }
+        );
+      this.setState({
+        selected: null,
+        word_groups: new_groups
+      });
+    } else {
+      /* First click of a word selects it. */
+      this.setState({
+        selected: word
+      });
+    }
+  }
+
+  render() {
+    if (this.state.submitted)
+      return (
+        <div className="please-wait">
+          <h2>{this.props.players_judged}/
+            {this.props.players_total} players have responded</h2>
+          <p>
+            Please wait for the rest of the players to complete judging.
+          </p>
+        </div>
+      );
+
+    const btn_class = "ambiguity-button";
+    const btn_selected_class = btn_class + " selected";
+
+    return (
+      <div className="ambiguities">
+        <h2>Judging Answers</h2>
+        <p>
+          Click on each pair of answers that should be scored as equivalent,
+          (and click any word twice to split it out from a group). Remember,
+          what goes around comes around, so it's best to be generous when
+          judging.
+        </p>
+        {this.state.word_groups.map(word_group => {
+          return (
+            <div
+              className="ambiguity-group"
+              key={word_group[0]}
+            >
+            {word_group.map(word => {
+              return (
+                <button
+                className={this.state.selected === word ?
+                           btn_selected_class : btn_class }
+                key={word}
+                onClick={() => this.handle_click(word)}
+                  >
+                {word}
+                </button>
+              );
+            })}
+            </div>
+          );
+        })}
+        <p>
+          Click here when done judging:<br/>
+          <button
+            onClick={() => this.handle_submit()}
+          >
+            Send
+          </button>
+        </p>
+      </div>
+    );
+  }
+}
+
 class ActivePrompt extends React.PureComponent {
 
   constructor(props) {
@@ -407,7 +551,9 @@ class Game extends React.PureComponent {
       other_players: [],
       prompts: [],
       active_prompt: null,
-      players_answered: 0
+      players_answered: 0,
+      ambiguities: null,
+      players_judged: 0
     };
   }
 
@@ -467,6 +613,18 @@ class Game extends React.PureComponent {
     });
   }
 
+  set_ambiguities(ambiguities) {
+    this.setState({
+      ambiguities: ambiguities
+    });
+  }
+
+  set_players_judged(players_judged) {
+    this.setState({
+      players_judged: players_judged
+    });
+  }
+
   set_scores(scores) {
     this.setState({
       scores: scores
@@ -515,6 +673,15 @@ class Game extends React.PureComponent {
       );
     }
 
+    if (state.ambiguities){
+      return <Ambiguities
+               prompt={state.active_prompt}
+               words={state.ambiguities}
+               players_judged={state.players_judged}
+               players_total={players_total}
+             />;
+    }
+
     if (state.active_prompt) {
       return <ActivePrompt
                prompt={state.active_prompt}