From 2f93ec3d360fda61f84c8cf9ebf08cb898bc8d86 Mon Sep 17 00:00:00 2001
From: Harsh Kumar
Date: Wed, 5 Aug 2026 12:17:06 -0400
Subject: [PATCH 1/3] feat: expand creator study materials
---
server/api/routes/firebaseAPI.ts | 66 ++++++++++++++--
src/components/chatbot/Chatbot.tsx | 5 +-
src/consts/passages.tsx | 105 +++++++++++++++++--------
src/consts/surveyQuestions.tsx | 21 +----
src/pages/Captcha.tsx | 43 +++++++---
src/pages/artist/PostSurvey.tsx | 3 +
src/pages/artist/step1/Step1.tsx | 6 +-
src/pages/artist/tutorial/Tutorial.tsx | 18 ++++-
src/types.ts | 4 +
9 files changed, 198 insertions(+), 73 deletions(-)
diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts
index 5fbc4aa..6f257ac 100644
--- a/server/api/routes/firebaseAPI.ts
+++ b/server/api/routes/firebaseAPI.ts
@@ -11,9 +11,23 @@ const ASSIGNMENT_COLLECTION = "artistAssignment";
router.post("/artist-assignment", async (req, res) => {
try {
- const { sessionId, passageId, prolificPid } = req.body;
- if (!sessionId || !passageId) {
- return res.status(400).json({ error: "Missing sessionId or passageId" });
+ const {
+ sessionId,
+ passageId,
+ tutorialPassageId,
+ passagePoolVersion,
+ prolificPid,
+ } = req.body;
+ if (
+ !sessionId ||
+ !passageId ||
+ !tutorialPassageId ||
+ !passagePoolVersion
+ ) {
+ return res.status(400).json({
+ error:
+ "Missing sessionId, passageId, tutorialPassageId, or passagePoolVersion",
+ });
}
const assignmentRef = db.collection(ASSIGNMENT_COLLECTION).doc(sessionId);
@@ -21,8 +35,40 @@ router.post("/artist-assignment", async (req, res) => {
const existingAssignment = await transaction.get(assignmentRef);
if (existingAssignment.exists) {
const existing = existingAssignment.data()!;
+ const taskPassageId = String(
+ existing.taskPassageId ?? existing.passageId,
+ );
+ const resolvedTutorialPassageId = String(
+ existing.tutorialPassageId ??
+ (tutorialPassageId === taskPassageId
+ ? passageId
+ : tutorialPassageId),
+ );
+ const resolvedPassagePoolVersion = String(
+ existing.passagePoolVersion ?? "legacy-creator-passages",
+ );
+
+ if (
+ !existing.taskPassageId ||
+ !existing.tutorialPassageId ||
+ !existing.passagePoolVersion
+ ) {
+ transaction.set(
+ assignmentRef,
+ {
+ taskPassageId,
+ tutorialPassageId: resolvedTutorialPassageId,
+ passagePoolVersion: resolvedPassagePoolVersion,
+ },
+ { merge: true },
+ );
+ }
+
return {
- passageId: existing.passageId as string,
+ passageId: taskPassageId,
+ taskPassageId,
+ tutorialPassageId: resolvedTutorialPassageId,
+ passagePoolVersion: resolvedPassagePoolVersion,
condition: existing.condition as "LLM" | "NO_AI",
strategy: existing.strategy as string,
};
@@ -36,12 +82,22 @@ router.post("/artist-assignment", async (req, res) => {
sessionId,
prolificPid: prolificPid || null,
passageId: String(passageId),
+ taskPassageId: String(passageId),
+ tutorialPassageId: String(tutorialPassageId),
+ passagePoolVersion: String(passagePoolVersion),
condition,
strategy,
assignedAt: FieldValue.serverTimestamp(),
});
- return { passageId: String(passageId), condition, strategy };
+ return {
+ passageId: String(passageId),
+ taskPassageId: String(passageId),
+ tutorialPassageId: String(tutorialPassageId),
+ passagePoolVersion: String(passagePoolVersion),
+ condition,
+ strategy,
+ };
});
res.json(assignment);
diff --git a/src/components/chatbot/Chatbot.tsx b/src/components/chatbot/Chatbot.tsx
index dc36fd5..6801005 100644
--- a/src/components/chatbot/Chatbot.tsx
+++ b/src/components/chatbot/Chatbot.tsx
@@ -36,7 +36,7 @@ interface ChatTabProps {
}
export const BLACKOUT_ASSISTANT_PROMPT_VERSION =
- "blackout-assistant-2026-08-04-v1";
+ "blackout-assistant-2026-08-05-v2";
/**
* Keep the locator-excerpt convention identical across both stages so users
@@ -50,9 +50,10 @@ Blackout poetry: the poet starts with an existing passage and creates a poem by
Grounding:
- Work only with the passage provided below. Never reference or substitute any other text.
-- When you point to a specific passage word, show it in a short excerpt containing two or three nearby passage words in total when available. Bold only the word you are pointing to. The unbolded words are only a locator to help the user find it; they are not part of the suggestion.
+- When you point to a specific passage word, show it in a short excerpt containing two or three nearby passage words in total when available. Bold only the word you are pointing to and italicize every surrounding locator word. For example: “*nights are* **clear**” or “*sharp,* **glittering** *sunshine*”. The italicized words are only locators to help the user find the bolded word; they are not part of the suggestion.
- Quote passage words exactly as written, keep multiple suggested words in passage order, and point to at most five words in a single response.
- Use bold only for passage words you are pointing to, never for general emphasis.
+- Use italics only for the surrounding locator words in these excerpts, never for general emphasis.
- Never suggest a word that does not appear in the passage.
Style and behavior:
diff --git a/src/consts/passages.tsx b/src/consts/passages.tsx
index 2d7cd6c..ebdcb03 100644
--- a/src/consts/passages.tsx
+++ b/src/consts/passages.tsx
@@ -1,46 +1,87 @@
import type { Passage } from "../types";
+export const CREATOR_PASSAGE_POOL_VERSION =
+ "creator-passages-2026-08-05-v1";
+
export const Passages: Passage[] = [
- // {
- // id: "1",
- // text: `The wind swept in soft big breaths down from the moor and was strange with a wild clear scented sweetness. Colin kept lifting his thin chest to draw it in, and his big eyes looked as if it were they which were listening - listening, instead of his ears. "There are so many sounds of singing and humming and calling out," he said. "What is that scent the puffs of wind bring?" "It's gorse on th' moor that's openin' out" answered Dickon. "Eh! th' bees are at it wonderful to-day." Not a human creature was to be caught sight of in the paths they took. In fact every gardener or gardener's lad had been witched away. But they wound in and out among the shrubbery and out and round the fountain beds, following their carefully planned route for the mere mysterious pleasure of it. But when at last they turned into the Long Walk by the ivied walls the excited sense of an approaching thrill made them, for some curious reason they could not have explained, begin to speak in whispers. "This is it" breathed Mary. "This is where I used to walk up and down and wonder and wonder".`,
- // author: "Frances Hodson Burnett",
- // title: "The Secret Garden",
- // },
- // {
- // id: "2",
- // text: "Two days later they arrived on the Common. The doctors had arranged to have Sylvia in the cottage, and had got permission from a farmer for the girls to camp in the field of a farm nearby. They had fixed that they should come every day for their middle-day meal at the cottage, for which Pauline was to pay a pound a week. For breakfast, tea, and supper they were to cook and cater for themselves. Perhaps it was because they were not expecting a holiday at all, or perhaps because it was Pauline’s holiday which she had paid for, but there was not one second which they did not find perfect. They took turns to cook, and it was lovely waking in the mornings to hear the cows mooing, and the cocks crowing, and to turn over and prod the day’s cook to get up and deal with breakfast. The palliasses stuffed with straw they found gloriously comfortable, and it was the height of luxury to lie on them sniffing the first smoke of the fire through the open flap of the tent before jumping up to race down to the stream in a bathing dress for a wash before breakfast. ",
- // author: "Noel Streatfeild",
- // title: "Ballet Shoes",
- // },
- // {
- // id: "3",
- // text: "The food varied a lot with who was cooking and catering. Eggs were the easiest things for breakfast, because they bought them from the farm; but for supper they had all sorts of things — sausages when Petrova was in charge, and two courses at least from Pauline. When Posy was catering, a great deal of cake got into the menu. The weather was not too good, but the farmer lent them a large barn for wet days, in which they practised every morning, wet or shine, as they had, of course, their ballet shoes with them; here, when it rained, they played a new and glorious kind of hide-and-seek. It consisted in the hiders burying themselves in the straw, which the nervous seeker had to prod. If the hider could grab any part of the seeker, she won; but if the seeker could see a movement in the hay, and lay a hand on the place, and say ‘One of you is here’, then she won. The game was such a thrilling one that when it was wet Sylvia and the two doctors usually came and played too.",
- // author: "Noel Streatfeild",
- // title: "Ballet Shoes",
- // },
+ {
+ id: "3",
+ text: "The wind swept in soft big breaths down from the moor and was strange with a wild clear scented sweetness. Colin kept lifting his thin chest to draw it in, and his big eyes looked as if it were they which were listening - listening, instead of his ears. \"There are so many sounds of singing and humming and calling out,\" he said. \"What is that scent the puffs of wind bring?\" \"It's gorse on th' moor that's openin' out\" answered Dickon. \"Eh! th' bees are at it wonderful to-day.\" Not a human creature was to be caught sight of in the paths they took. In fact every gardener or gardener's lad had been witched away. But they wound in and out among the shrubbery and out and round the fountain beds, following their carefully planned route for the mere mysterious pleasure of it. But when at last they turned into the Long Walk by the ivied walls the excited sense of an approaching thrill made them, for some curious reason they could not have explained, begin to speak in whispers.",
+ author: "Frances Hodson Burnett",
+ title: "The Secret Garden",
+ },
+ {
+ id: "4",
+ text: "Two days later they arrived on the Common. The doctors had arranged to have Sylvia in the cottage, and had got permission from a farmer for the girls to camp in the field of a farm nearby. They had fixed that they should come every day for their middle-day meal at the cottage, for which Pauline was to pay a pound a week. For breakfast, tea, and supper they were to cook and cater for themselves. Perhaps it was because they were not expecting a holiday at all, or perhaps because it was Pauline’s holiday which she had paid for, but there was not one second which they did not find perfect. They took turns to cook, and it was lovely waking in the mornings to hear the cows mooing, and the cocks crowing, and to turn over and prod the day’s cook to get up and deal with breakfast.",
+ author: "Noel Streatfeild",
+ title: "Ballet Shoes",
+ },
{
id: "2",
- text: `"Look at that arch of pale green sky over those houses and picture to yourself what it must look like over the purply-dark beech-woods back of Avonlea." "What are you going to wear for commencement, Jane?" asked Ruby practically. Jane and Josie both answered at once and the chatter drifted into a side eddy of fashions. But Anne, with her elbows on the window sill, her soft cheek laid against her clasped hands, and her eyes filled with visions, looked out unheedingly across city roof and spire to that glorious dome of sunset sky and wove her dreams of a possible future from the golden tissue of youth's own optimism.`,
+ text: "\"Next to trying and winning, the best thing is trying and failing. Girls, don't talk about exams! Look at that arch of pale green sky over those houses and picture to yourself what it must look like over the purply-dark beech-woods back of Avonlea.\" \"What are you going to wear for commencement, Jane?\" asked Ruby practically. Jane and Josie both answered at once and the chatter drifted into a side eddy of fashions. But Anne, with her elbows on the window sill, her soft cheek laid against her clasped hands, and her eyes filled with visions, looked out unheedingly across city roof and spire to that glorious dome of sunset sky and wove her dreams of a possible future from the golden tissue of youth's own optimism. All the Beyond was hers with its possibilities lurking rosily in the oncoming years — each year a rose of promise to be woven into an immortal chaplet.",
author: "Lucy Maud Montgomery",
title: "Anne of Green Gables",
},
- // {
- // id: "5",
- // text: `A woman who was so hard on herself and others could be sweet and smiling with Clara and at times, by extension, with Blanca. Only with Clara did she allow herself the luxury of giving in to her overwhelming desire to serve and be loved; with her, however slyly, she was able to express the secret, most delicate yearnings of her soul. e long years of solitude and unhappiness had distilled her emotions and purified her feelings down to a few terrible, magnificent passions, which possessed her totally. She had no gift for small perturbations, mean-spirited resentments, concealed envies, works of charity, faded endearments, ordinary friendly politeness, or day-to-day acts of kindness. She was one of those people who are born for the greatness of a single love, for exaggerated hatred, for apocalyptic vengeance, and for the most sublime forms of heroism, but she was unable to shape her fate to the dimensions of her amorous vocation, so it was lived out as something flat and gray trapped between her mother’s sickroom walls, wretched tenements, and the tortured confessions with which this large, opulent, hot-blooded woman—made for maternity, abundance, action, and ardor—was consuming herself.`,
- // author: "Isabel Allende",
- // title: "The House of Spirits",
- // },
{
id: "1",
- text: `The nights are clear, but suffused with sloth and sullen expectation. But by early June the southwest monsoon breaks and there are three months of wind and water with short spells of sharp, glittering sunshine that thrilled children snatch to play with. The countryside turns an immodest green. Boundaries blur as tapioca fences take root and bloom. Brick walls turn moss green. Pepper vines snake up electric poles. Wild creepers burst through laterite banks and spill across flooded roads. Boats ply in the bazaars. And small fish appear in the puddles that fill the PWD potholes on the highways. It was raining when Rahel came back to Ayemenem. Slanting silver ropes slammed into loose earth, plowing it up like gunfire. The old house on the hill wore its steep, gabled roof pulled over its ears like a low hat. The walls, streaked with moss, had grown soft, and bulged a little with dampness that seeped up from the ground. The wild, overgrown garden was full of the whisper and scurry of small lives. In the undergrowth a rat snake rubbed itself against a glistening stone. Hopeful yellow bullfrogs cruised the scummy pond for mates. A drenched mongoose flashed across the leaf-strewn driveway.`,
+ text: "The nights are clear, but suffused with sloth and sullen expectation. But by early June the southwest monsoon breaks and there are three months of wind and water with short spells of sharp, glittering sunshine that thrilled children snatch to play with. The countryside turns an immodest green. Boundaries blur as tapioca fences take root and bloom. Brick walls turn moss green. Pepper vines snake up electric poles. Wild creepers burst through laterite banks and spill across flooded roads. Boats ply in the bazaars. And small fish appear in the puddles that fill the PWD potholes on the highways. It was raining when Rahel came back to Ayemenem. Slanting silver ropes slammed into loose earth, plowing it up like gunfire. The old house on the hill wore its steep, gabled roof pulled over its ears like a low hat. The walls, streaked with moss, had grown soft, and bulged a little with dampness that seeped up from the ground.",
author: "Arundhati Roy",
title: "The God of Small Things",
},
- // {
- // id: "7",
- // text: `The house itself looked empty. The doors and windows were locked. The front verandah bare. Unfurnished. But the skyblue Plymouth with chrome tailfins was still parked outside, and inside, Baby Kochamma was still alive. She was Rahel’s baby grandaunt, her grandfather’s younger sister. Her name was really Navomi, Navomi Ipe, but everybody called her Baby. She became Baby Kochamma when she was old enough to be an aunt. Rahel hadn’t come to see her, though. Neither niece nor baby grandaunt labored under any illusions on that account. Rahel had come to see her brother, Estha. They were two-egg twins. “Dizygotic” doctors called them. Born from separate but simultaneously fertilized eggs. Estha—Esthappen was the older by eighteen minutes. They never did look much like each other, Estha and Rahel, and even when they were thin-armed children, flat-chested, wormridden and Elvis Presley-puffed, there was none of the usual “Who is who?” and “Which is which?” from oversmiling relatives or the Syrian Orthodox bishops who frequently visited the Ayemenem House for donations. The confusion lay in a deeper, more secret place.`,
- // author: "Arundhati Roy",
- // title: "The God of Small Things",
- // },
+ {
+ id: "5",
+ text: "Only with Clara did she allow herself the luxury of giving in to her overwhelming desire to serve and be loved; with her, however slyly, she was able to express the secret, most delicate yearnings of her soul. The long years of solitude and unhappiness had distilled her emotions and purified her feelings down to a few terrible, magnificent passions, which possessed her totally. She had no gift for small perturbations, mean-spirited resentments, concealed envies, works of charity, faded endearments, ordinary friendly politeness, or day-to-day acts of kindness. She was one of those people who are born for the greatness of a single love, for exaggerated hatred, for apocalyptic vengeance, and for the most sublime forms of heroism, but she was unable to shape her fate to the dimensions of her amorous vocation, so it was lived out as something flat and gray trapped between her mother’s sickroom walls, wretched tenements, and the tortured confessions with which this large, opulent, hot-blooded woman—made for maternity, abundance, action, and ardor—was consuming herself.",
+ author: "Isabel Allende",
+ title: "The House of Spirits",
+ },
+ {
+ id: "nyt-1",
+ text: "Zooming through a Stockholm intersection on his bicycle one morning, 18-year-old Max Tegmark never saw the truck that hit him. The blast of a horn, the screech of tires and a sickening thud followed in quick succession, extinguishing a young life filled with promise. It never happened, of course. As the adult Dr. Tegmark, now 46, relates in the opening pages of this provocative new book, young Max swerved at the last instant and continued on to school — and an academic career that eventually took him to the faculty of M.I.T. and the top ranks of the world’s cosmologists. But wait: That fatal accident did happen, Dr. Tegmark writes. Yes, he came through unscathed that day, but he was also killed, and he was severely injured, and slightly injured. He endured every possible outcome, happy and unhappy, that can befall a bicyclist who encounters a speeding truck.",
+ author: "Amir Alexander",
+ title: "If It's Possible, It Happened",
+ publication: "The New York Times",
+ },
+ {
+ id: "nyt-2",
+ text: "Thermal, Calif. — “People are getting more brave, which I love, just embracing it,” said Kelson Berkus, 30, a marketing executive in Los Angeles, nodding approvingly as he surveyed the Lacoste party, clad in aqua giraffe print swim trunks (source: “probably Fred Segal”). A few steps away, Ryan Greeley, 28, a former model who works in retail in Long Beach, Calif., stepped out of the pool and pulled what appeared to be a large piece of cheesecloth (it was actually vintage, from Buffalo Exchange) close to his chest. “I call it a shawl, but that’s just because it’s a cardigan that’s mesh,” Mr. Greeley said. It was the first weekend of the Coachella Valley Music and Arts Festival, and around him, hundreds of young men milled about, clad in sleeveless shirts and short shorts, hoisting up vodka sodas enthusiastically (“Let’s do this, bro!”), reliving the previous night’s excesses (“Ugh, the ceiling was spinning”) and puffing up their pecs as they strutted around the private estate here.",
+ author: "Sheila Marikar",
+ title: "At Coachella, It's A Guy Thing",
+ publication: "The New York Times",
+ },
+ {
+ id: "nyt-3",
+ text: "How do events from our childhood shape our adult preferences? I recently studied this question using a classic American example: baseball. I tested how a team’s performance at every age of our childhood affects which team we root for as adults. From Facebook’s publicly available advertising platform, I downloaded data on how many fans every baseball team has, broken out by gender and age. (Facebook estimates whether someone is a fan of a team based on both “likes” and posts.) This data is not perfect, but it does correlate pretty well with previous polling. According to Facebook, the most popular teams are the Yankees, Red Sox, Mets, Cardinals and Braves; there are 1.65 Yankees fans for every Mets fan. Facebook data can reveal patterns that polls generally do not have large enough sample sizes to detect. For example, Facebook tells us that among men, the ratio of Yankees fans to Mets fans fluctuates enormously depending on when those men were born.",
+ author: "Seth Stephens-Davidowitz",
+ title: "They Hook You When You're Young",
+ publication: "The New York Times",
+ },
+ {
+ id: "nyt-4",
+ text: "By what metric are we to determine whether a model is “super”? If it is name recognition, Forbes list position, magazines covered, campaigns fronted or some algorithm of them all, Kate Upton probably qualifies. In 2013 Ms. Upton, a 21-year-old from Melbourne, Fla., was named model of the year and appeared on the cover of the Sports Illustrated swimsuit issue for the second straight year. This year she was demoted to the inside cover, pictured wearing a bikini in zero gravity. As a model she floats off the page, but she’s also a performer, with videos of her dancing going crash the Internet viral. So, all aboard the Hollywood starship express. In “The Other Woman,” due Friday, Ms. Upton will appear alongside Cameron Diaz and Leslie Mann. It is Ms. Upton’s third and by far most substantial screen credit. In her debut, the surprisingly pleasant “Tower Heist,” she played a mistress in what was by definition no more than a cameo.",
+ author: "Christopher Wallace",
+ title: "Yet Another Pretty Face",
+ publication: "The New York Times",
+ },
];
+
+export interface PassagePair {
+ tutorialPassage: Passage;
+ taskPassage: Passage;
+}
+
+export const sampleDistinctPassages = (
+ random: () => number = Math.random,
+): PassagePair => {
+ if (Passages.length < 2) {
+ throw new Error("At least two passages are required for assignment");
+ }
+
+ const tutorialIndex = Math.floor(random() * Passages.length);
+ const taskOffset = Math.floor(random() * (Passages.length - 1));
+ const taskIndex = taskOffset >= tutorialIndex ? taskOffset + 1 : taskOffset;
+
+ return {
+ tutorialPassage: Passages[tutorialIndex],
+ taskPassage: Passages[taskIndex],
+ };
+};
diff --git a/src/consts/surveyQuestions.tsx b/src/consts/surveyQuestions.tsx
index f5abac4..43bc951 100644
--- a/src/consts/surveyQuestions.tsx
+++ b/src/consts/surveyQuestions.tsx
@@ -104,7 +104,7 @@ export const ArtistPreSurveyQuestions: SurveyDefinition = {
};
export const ArtistPostSurveyQuestions: SurveyDefinition = {
- id: "artist-post-survey-v2",
+ id: "artist-post-survey-v3",
title: "Creator post-task survey",
sections: [
{
@@ -260,25 +260,6 @@ export const ArtistPostSurveyQuestions: SurveyDefinition = {
},
],
},
- {
- id: "llm-attribution",
- title: "AI contribution",
- conditions: ["LLM"],
- questions: [
- {
- id: "llm_contribution_attribution",
- type: "multipleChoice",
- question: "During the blackout-poem process, I felt that…",
- options: [
- "I was creating the poem and AI was assisting me.",
- "AI was creating the poem and I was assisting it.",
- "AI and I contributed approximately equally.",
- "I did not use the AI tool.",
- ],
- required: true,
- },
- ],
- },
{
id: "outside-tools",
title: "Outside tools",
diff --git a/src/pages/Captcha.tsx b/src/pages/Captcha.tsx
index 2fe4257..fdfbacb 100644
--- a/src/pages/Captcha.tsx
+++ b/src/pages/Captcha.tsx
@@ -6,7 +6,12 @@ import { toaster } from "../components/ui/toaster";
import { DataContext } from "../App";
import { ArtistCondition } from "../types";
import type { ArtistAssignment, Poem } from "../types";
-import { Passages } from "../consts/passages";
+import {
+ CREATOR_PASSAGE_POOL_VERSION,
+ Passages,
+ sampleDistinctPassages,
+ type PassagePair,
+} from "../consts/passages";
import type { ChangeEvent, KeyboardEvent } from "react";
const TEST_CAPTCHA = "TEST";
@@ -99,16 +104,21 @@ const Captcha = () => {
const startArtist = (
condition: ArtistCondition,
- passageId = "1",
+ passages: PassagePair = sampleDistinctPassages(),
strategy: ArtistAssignment["strategy"] = "TEST_OVERRIDE",
+ passagePoolVersion = CREATOR_PASSAGE_POOL_VERSION,
) => {
+ const { tutorialPassage, taskPassage } = passages;
addUserData({ role: "artist" });
addRoleSpecificData({
condition,
- poem: makePoem(passageId),
+ poem: makePoem(taskPassage.id),
assignment: {
strategy,
- passageId,
+ passageId: taskPassage.id,
+ tutorialPassageId: tutorialPassage.id,
+ taskPassageId: taskPassage.id,
+ passagePoolVersion,
condition,
assignedAt: new Date(),
},
@@ -144,14 +154,15 @@ const Captcha = () => {
setIsAssigning(true);
try {
- const proposedPassage =
- Passages[Math.floor(Math.random() * Passages.length)];
+ const proposedPassages = sampleDistinctPassages();
const response = await fetch("/api/firebase/artist-assignment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sessionId,
- passageId: proposedPassage.id,
+ passageId: proposedPassages.taskPassage.id,
+ tutorialPassageId: proposedPassages.tutorialPassage.id,
+ passagePoolVersion: CREATOR_PASSAGE_POOL_VERSION,
prolificPid: prolific?.prolificPid ?? null,
}),
});
@@ -160,14 +171,27 @@ const Captcha = () => {
}
const assignment = (await response.json()) as {
passageId: string;
+ tutorialPassageId: string;
+ taskPassageId: string;
+ passagePoolVersion: string;
condition: ArtistCondition;
strategy: ArtistAssignment["strategy"];
};
const hasValidStrategy =
assignment.strategy === "INDEPENDENT_RANDOM_1_TO_1" ||
assignment.strategy === "PASSAGE_STRATIFIED_1_TO_1";
+ const tutorialPassage = Passages.find(
+ (passage) => passage.id === assignment.tutorialPassageId,
+ );
+ const taskPassage = Passages.find(
+ (passage) => passage.id === assignment.taskPassageId,
+ );
if (
- !Passages.some((passage) => passage.id === assignment.passageId) ||
+ !tutorialPassage ||
+ !taskPassage ||
+ tutorialPassage.id === taskPassage.id ||
+ assignment.passageId !== taskPassage.id ||
+ !assignment.passagePoolVersion ||
!Object.values(ArtistCondition).includes(assignment.condition) ||
!hasValidStrategy
) {
@@ -176,8 +200,9 @@ const Captcha = () => {
startArtist(
assignment.condition,
- assignment.passageId,
+ { tutorialPassage, taskPassage },
assignment.strategy,
+ assignment.passagePoolVersion,
);
navigate("/consent");
} catch (error) {
diff --git a/src/pages/artist/PostSurvey.tsx b/src/pages/artist/PostSurvey.tsx
index b073ca5..db683ae 100644
--- a/src/pages/artist/PostSurvey.tsx
+++ b/src/pages/artist/PostSurvey.tsx
@@ -48,6 +48,9 @@ const ArtistPostSurvey = () => {
const poemData = {
passageId: poem.passageId,
+ tutorialPassageId: artistData.assignment?.tutorialPassageId,
+ taskPassageId:
+ artistData.assignment?.taskPassageId ?? poem.passageId,
passage: poem.passage,
text: poem.text,
selectedWordIndexes: poem.text,
diff --git a/src/pages/artist/step1/Step1.tsx b/src/pages/artist/step1/Step1.tsx
index b0bd8b7..ab270cf 100644
--- a/src/pages/artist/step1/Step1.tsx
+++ b/src/pages/artist/step1/Step1.tsx
@@ -202,7 +202,11 @@ const ArtistStep1 = () => {
{'"' + passage.title + '"'}
- {", " + passage.author + " from The New York Times"}
+
+ {", " +
+ passage.author +
+ (passage.publication ? `, ${passage.publication}` : "")}
+
diff --git a/src/pages/artist/tutorial/Tutorial.tsx b/src/pages/artist/tutorial/Tutorial.tsx
index 4580eba..f91e6b4 100644
--- a/src/pages/artist/tutorial/Tutorial.tsx
+++ b/src/pages/artist/tutorial/Tutorial.tsx
@@ -4,7 +4,7 @@ import { DataContext } from "../../../App";
import FullPageTemplate from "../../../components/shared/pages/fullScrollPage";
import BlackoutPoetry from "../../../components/blackout/Blackout";
import { Button } from "@chakra-ui/react";
-import type { PoemSnapshot } from "../../../types";
+import type { Artist, PoemSnapshot } from "../../../types";
import { Passages } from "../../../consts/passages";
const STEPS = [
@@ -33,8 +33,14 @@ const ArtistTutorial = () => {
throw new Error("Component must be used within a DataContext.Provider");
}
const { userData, addRoleSpecificData } = context;
+ const artistData = userData?.data as Artist;
+ const passage = Passages.find(
+ (candidate) => candidate.id === artistData.assignment?.tutorialPassageId,
+ );
- const passage = Passages.find((p) => p.id === "2")!;
+ if (!passage) {
+ throw new Error("Tutorial passage assignment is missing or invalid");
+ }
const [selectedWordIndexes, setSelectedWordIndexes] = useState([]);
const [, setPoemSnapshots] = useState([]);
@@ -54,7 +60,7 @@ const ArtistTutorial = () => {
}
prevCountRef.current = count;
- }, [selectedWordIndexes]);
+ }, [selectedWordIndexes, step]);
const handleContinue = () => {
addRoleSpecificData({
@@ -102,7 +108,11 @@ const ArtistTutorial = () => {
{'"' + passage.title + '"'}
- {", " + passage.author}
+
+ {", " +
+ passage.author +
+ (passage.publication ? `, ${passage.publication}` : "")}
+
{/* Continue — only shown on final step */}
diff --git a/src/types.ts b/src/types.ts
index 37dbcdd..49fae80 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -13,6 +13,9 @@ export interface ArtistAssignment {
| "PASSAGE_STRATIFIED_1_TO_1"
| "TEST_OVERRIDE";
passageId: string;
+ tutorialPassageId: string;
+ taskPassageId: string;
+ passagePoolVersion: string;
condition: ArtistCondition;
assignedAt: Date;
}
@@ -100,6 +103,7 @@ export interface Passage {
text: string;
title: string;
author: string;
+ publication?: string;
}
export const Stage = {
From 57f355efe0a1ebdf81f4c187cc79b862ac03b1e3 Mon Sep 17 00:00:00 2001
From: Harsh Kumar
Date: Wed, 5 Aug 2026 12:26:10 -0400
Subject: [PATCH 2/3] feat: note that poems will be read
---
src/pages/artist/instructions/Instructions.tsx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/pages/artist/instructions/Instructions.tsx b/src/pages/artist/instructions/Instructions.tsx
index 3f2152d..638976e 100644
--- a/src/pages/artist/instructions/Instructions.tsx
+++ b/src/pages/artist/instructions/Instructions.tsx
@@ -38,6 +38,8 @@ const ArtistInstructions = () => {
existing passage of text.
+ Your final poem will be read by others.
+
Date: Wed, 5 Aug 2026 13:16:45 -0400
Subject: [PATCH 3/3] feat: expand artist interaction logging
---
README.md | 2 +
src/components/chatbot/Chatbot.tsx | 148 ++++++++++++++--
src/components/shared/pages/multiPage.tsx | 15 +-
src/consts/chatMessages.ts | 10 +-
src/consts/dataLogging.ts | 1 +
src/pages/Captcha.tsx | 8 +-
src/pages/artist/PostSurvey.tsx | 1 +
src/pages/artist/README.md | 205 ++++++++++++++++++++++
src/pages/artist/step1/Step1.tsx | 54 ++++--
src/pages/artist/step2/Step2.tsx | 61 ++++---
src/types.ts | 42 ++++-
src/utils/artistMetrics.ts | 158 ++++++++++++++++-
src/utils/llmUsage.ts | 12 ++
13 files changed, 649 insertions(+), 68 deletions(-)
create mode 100644 src/consts/dataLogging.ts
create mode 100644 src/pages/artist/README.md
create mode 100644 src/utils/llmUsage.ts
diff --git a/README.md b/README.md
index 0c1d28c..e24b3cb 100644
--- a/README.md
+++ b/README.md
@@ -11,5 +11,7 @@ Our experimental design involves two phases: one for artists and another for the
We plan to continue this research in a second part through a longitudinal field study.
+Developer documentation for the creator dataset is in [`src/pages/artist/README.md`](src/pages/artist/README.md).
+
1. Kwan, L. Y. -Y., Leung, A. K. -y., & Liou, S. (2018). Culture, creativity, and innovation. Journal of Cross-Cultural Psychology, 49(2), 165–170. https://doi.org/10.1177/0022022117753306s
2. Elisondo, R. (2016). Creativity is Always a Social Process. Creativity. Theories – Research - Applications, 3(2), 2016. 194-210. https://doi.org/10.1515/ctra-2016-0013
diff --git a/src/components/chatbot/Chatbot.tsx b/src/components/chatbot/Chatbot.tsx
index 6801005..5895ffe 100644
--- a/src/components/chatbot/Chatbot.tsx
+++ b/src/components/chatbot/Chatbot.tsx
@@ -10,18 +10,21 @@ import { FiSend } from "react-icons/fi";
import { Button, Textarea } from "@chakra-ui/react";
import { nanoid } from "nanoid";
import type {
- ChatOpening,
+ ChatAvailability,
+ ChatInputActivity,
+ ChatInputSource as ChatInputSourceType,
LlmRequestLog,
Message,
Stage,
} from "../../types";
-import { Role } from "../../types";
+import { ChatInputSource, MessageKind, Role } from "../../types";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { DataContext } from "../../App";
import {
createAssistantMessage,
IDLE_NUDGE_MESSAGES,
+ STAGE_OPENING_MESSAGES,
} from "../../consts/chatMessages";
interface ChatTabProps {
@@ -31,7 +34,9 @@ interface ChatTabProps {
selectedWordIndexes?: number[];
passage: string;
chatReady?: boolean;
- onChatOpened?: (opening: ChatOpening) => void;
+ initialInputActivity?: ChatInputActivity;
+ onChatAvailable?: (availability: ChatAvailability) => void;
+ onInputActivityUpdate?: (activity: ChatInputActivity) => void;
onRequestUpdate?: (request: LlmRequestLog) => void;
}
@@ -162,7 +167,9 @@ export default function ChatTab({
stage,
passage,
chatReady = true,
- onChatOpened,
+ initialInputActivity,
+ onChatAvailable,
+ onInputActivityUpdate,
onRequestUpdate,
}: ChatTabProps) {
const context = useContext(DataContext);
@@ -172,7 +179,18 @@ export default function ChatTab({
const chatContainerRef = useRef(null);
const timeoutRef = useRef | null>(null);
const stageStartMessageCountRef = useRef(messages.length);
- const hasLoggedOpeningRef = useRef(false);
+ const hasLoggedAvailabilityRef = useRef(false);
+ const inputRef = useRef("");
+ const hasDraftRef = useRef(false);
+ const inputActivityRef = useRef(
+ initialInputActivity ?? {
+ stage,
+ focusCount: 0,
+ draftStartCount: 0,
+ abandonedDraftCount: 0,
+ hasUnsentDraft: false,
+ },
+ );
const [isLLMLoading, setIsLLMLoading] = useState(false);
const [input, setInput] = useState("");
@@ -206,10 +224,29 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`,
}, [passage, selectedWordIndexes, stage]);
useEffect(() => {
- if (!chatReady || hasLoggedOpeningRef.current) return;
- hasLoggedOpeningRef.current = true;
- onChatOpened?.({ stage, timestamp: new Date() });
- }, [chatReady, onChatOpened, stage]);
+ if (!chatReady || hasLoggedAvailabilityRef.current) return;
+ hasLoggedAvailabilityRef.current = true;
+
+ const availableAt = new Date();
+ setMessages((previousMessages) => {
+ const alreadyHasOpening = previousMessages.some(
+ (message) =>
+ message.stage === stage &&
+ message.kind === MessageKind.STAGE_OPENING,
+ );
+ return alreadyHasOpening
+ ? previousMessages
+ : [
+ ...previousMessages,
+ createAssistantMessage(
+ STAGE_OPENING_MESSAGES[stage],
+ stage,
+ MessageKind.STAGE_OPENING,
+ ),
+ ];
+ });
+ onChatAvailable?.({ stage, availableAt });
+ }, [chatReady, onChatAvailable, setMessages, stage]);
useEffect(() => {
const element = chatContainerRef.current;
@@ -227,7 +264,11 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`,
timeoutRef.current = setTimeout(() => {
setMessages((previousMessages) => [
...previousMessages,
- createAssistantMessage(IDLE_NUDGE_MESSAGES[stage]),
+ createAssistantMessage(
+ IDLE_NUDGE_MESSAGES[stage],
+ stage,
+ MessageKind.IDLE_NUDGE,
+ ),
]);
setHasShownIdleNudge(true);
}, 40000);
@@ -246,8 +287,66 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`,
stage,
]);
- const sendMessage = async (messageContent?: string) => {
- const content = messageContent || input;
+ const publishInputActivity = (activity: ChatInputActivity) => {
+ inputActivityRef.current = activity;
+ onInputActivityUpdate?.(activity);
+ };
+
+ const handleInputFocus = () => {
+ const activity = inputActivityRef.current;
+ publishInputActivity({
+ ...activity,
+ firstFocusedAt: activity.firstFocusedAt ?? new Date(),
+ focusCount: activity.focusCount + 1,
+ });
+ };
+
+ const handleInputChange = (value: string) => {
+ const hadDraft = hasDraftRef.current;
+ const hasDraft = Boolean(value.trim());
+ inputRef.current = value;
+ hasDraftRef.current = hasDraft;
+ setInput(value);
+
+ if (!hadDraft && hasDraft) {
+ const activity = inputActivityRef.current;
+ publishInputActivity({
+ ...activity,
+ firstTypedAt: activity.firstTypedAt ?? new Date(),
+ draftStartCount: activity.draftStartCount + 1,
+ hasUnsentDraft: true,
+ });
+ } else if (hadDraft && !hasDraft) {
+ const activity = inputActivityRef.current;
+ publishInputActivity({
+ ...activity,
+ abandonedDraftCount: activity.abandonedDraftCount + 1,
+ hasUnsentDraft: false,
+ });
+ }
+ };
+
+ const markDraftSubmittedOrReplaced = (
+ inputSource: ChatInputSourceType,
+ ) => {
+ if (!hasDraftRef.current) return;
+
+ hasDraftRef.current = false;
+ const activity = inputActivityRef.current;
+ publishInputActivity({
+ ...activity,
+ abandonedDraftCount:
+ activity.abandonedDraftCount +
+ (inputSource === ChatInputSource.SUGGESTION ? 1 : 0),
+ hasUnsentDraft: false,
+ });
+ };
+
+ const sendMessage = async (
+ messageContent?: string,
+ inputSource: ChatInputSourceType = ChatInputSource.TYPED,
+ ) => {
+ const content = messageContent ?? inputRef.current;
if (!content.trim() || isLLMLoading) return;
if (timeoutRef.current) {
@@ -260,6 +359,9 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`,
role: Role.ARTIST,
content,
timestamp: new Date(),
+ stage,
+ kind: MessageKind.USER_MESSAGE,
+ inputSource,
};
const requestId = nanoid();
let requestLog: LlmRequestLog = {
@@ -269,6 +371,7 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`,
userMessageContent: content,
requestedAt: new Date(),
status: "STARTED",
+ inputSource,
systemPrompt: systemMessage.content,
promptVersion: BLACKOUT_ASSISTANT_PROMPT_VERSION,
};
@@ -282,6 +385,8 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`,
setMarkdownOutput("");
setSendError(null);
setMessages((prev) => [...prev, artistMessage]);
+ markDraftSubmittedOrReplaced(inputSource);
+ inputRef.current = "";
setInput("");
setIsLLMLoading(true);
@@ -321,6 +426,8 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`,
role: Role.LLM,
content: fullText,
timestamp: new Date(),
+ stage,
+ kind: MessageKind.LLM_RESPONSE,
};
requestLog = {
...requestLog,
@@ -348,9 +455,15 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`,
setMessages((previousMessages) =>
previousMessages.filter((message) => message.id !== artistMessage.id),
);
- setInput((currentInput) =>
- currentInput.trim() ? currentInput : content,
- );
+ if (!inputRef.current.trim()) {
+ inputRef.current = content;
+ hasDraftRef.current = true;
+ setInput(content);
+ publishInputActivity({
+ ...inputActivityRef.current,
+ hasUnsentDraft: true,
+ });
+ }
} finally {
setIsLLMLoading(false);
}
@@ -364,7 +477,7 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`,
};
const handlePromptSelection = (prompt: string) => {
- sendMessage(prompt);
+ sendMessage(prompt, ChatInputSource.SUGGESTION);
};
return (
@@ -448,7 +561,8 @@ CURRENT SELECTED WORDS (in passage order): ${selectedWords || "none yet"}`,
>