From 78594696a40a24f14d1007d580932eb597e452b0 Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Thu, 8 Jan 2026 16:17:38 -0800 Subject: [PATCH 01/18] remove audience conditions --- src/types.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/types.ts b/src/types.ts index 85f3ca4..1052904 100644 --- a/src/types.ts +++ b/src/types.ts @@ -104,13 +104,6 @@ export interface Passage { text: string; } -// export const AudienceCondition = { -// NO_KNOWLEDGE: "NO_KNOWLEDGE", -// FULL_TRANSPARENCY: "FULL_TRANSPARENCY", -// } as const; -// export type AudienceCondition = -// (typeof AudienceCondition)[keyof typeof AudienceCondition]; - export type QuestionType = | "multipleChoice" | "openEnded" From 3945e43b63cd9b0e90676e07c7227aa6ad95fe8a Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Thu, 8 Jan 2026 16:21:21 -0800 Subject: [PATCH 02/18] create audience collections --- server/api/routes/firebaseAPI.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index c21fe19..7855557 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -3,10 +3,16 @@ import { db, FieldValue } from "../firebase/firebase"; const router = express.Router(); +// ARTIST COLLECTIONS const ARTIST_COLLECTION = "artist"; const ARTIST_SURVEY_COLLECTION = "artistSurvey"; const POEM_COLLECTION = "poem"; -const INCOMPLETE_SESSION_COLLECTION = "incompleteSession"; +const ARTIST_INCOMPLETE_SESSION_COLLECTION = "artistIncompleteSession"; + +// AUDIENCE COLLECTIONS +const AUDIENCE_COLLECTION = "audience"; +const AUDIENCE_SURVEY_COLLECTION = "audienceSurvey"; +const AUDIENCE_INCOMPLETE_SESSION_COLLECTION = "audienceIncompleteSession"; router.post("/autosave", async (req, res) => { try { @@ -32,7 +38,9 @@ router.post("/autosave", async (req, res) => { ? statusMap[data.data.timeStamps.length] || "started" : "started"; - const ref = db.collection(INCOMPLETE_SESSION_COLLECTION).doc(sessionId); + const ref = db + .collection(ARTIST_INCOMPLETE_SESSION_COLLECTION) + .doc(sessionId); const payload = { sessionId, role: data.role, @@ -75,7 +83,7 @@ router.post("/commit-session", async (req, res) => { const surveyRef = db.collection(ARTIST_SURVEY_COLLECTION).doc(); const poemRef = db.collection(POEM_COLLECTION).doc(); const incompleteRef = db - .collection(INCOMPLETE_SESSION_COLLECTION) + .collection(ARTIST_INCOMPLETE_SESSION_COLLECTION) .doc(sessionId); const artist = { From db5e525145a894fac4210977321f92df8f2f76b1 Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Thu, 8 Jan 2026 16:24:42 -0800 Subject: [PATCH 03/18] set correct path --- server/api/routes/firebaseAPI.ts | 7 +++++-- src/App.tsx | 2 +- src/pages/artist/PostSurvey.tsx | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index 7855557..afc3924 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -14,7 +14,8 @@ const AUDIENCE_COLLECTION = "audience"; const AUDIENCE_SURVEY_COLLECTION = "audienceSurvey"; const AUDIENCE_INCOMPLETE_SESSION_COLLECTION = "audienceIncompleteSession"; -router.post("/autosave", async (req, res) => { +// ARTIST ROUTES +router.post("/artist/autosave", async (req, res) => { try { const { sessionId, data } = req.body; @@ -57,7 +58,7 @@ router.post("/autosave", async (req, res) => { } }); -router.post("/commit-session", async (req, res) => { +router.post("/artist/commit-session", async (req, res) => { try { const { artistData, surveyData, poemData, sessionId } = req.body; @@ -107,4 +108,6 @@ router.post("/commit-session", async (req, res) => { } }); +// AUDIENCE ROUTES + export default router; diff --git a/src/App.tsx b/src/App.tsx index 27576ac..e5869a1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -86,7 +86,7 @@ function App() { if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current); saveTimerRef.current = window.setTimeout(async () => { - await fetch("/api/firebase/autosave", { + await fetch("/api/firebase/artist/autosave", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId, data }), diff --git a/src/pages/artist/PostSurvey.tsx b/src/pages/artist/PostSurvey.tsx index 48c858a..8649cd2 100644 --- a/src/pages/artist/PostSurvey.tsx +++ b/src/pages/artist/PostSurvey.tsx @@ -48,7 +48,7 @@ const ArtistPostSurvey = () => { // SEND IT RAHHHH try { - await fetch("/api/firebase/commit-session", { + await fetch("/api/firebase/artist/commit-session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ From fa796d2ca87ac995837811d3da763b04fc70a68a Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Thu, 8 Jan 2026 16:34:57 -0800 Subject: [PATCH 04/18] audience routes first draft --- server/api/routes/firebaseAPI.ts | 84 ++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index afc3924..2db1d85 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -109,5 +109,89 @@ router.post("/artist/commit-session", async (req, res) => { }); // AUDIENCE ROUTES +router.post("/audience/autosave", async (req, res) => { + try { + const { sessionId, data } = req.body; + + if (!sessionId || !data) { + return res + .status(400) + .json({ error: "Missing sessionId or data objects" }); + } + + const statusMap: Record = { + 1: "captcha", + 2: "consent", + 3: "pre-survey", + 4: "instructions", + 5: "passage", + 6: "poems", + 7: "post-survey", + }; + + const status = data.data?.timeStamps + ? statusMap[data.data.timeStamps.length] || "started" + : "started"; + + const ref = db + .collection(AUDIENCE_INCOMPLETE_SESSION_COLLECTION) + .doc(sessionId); + const payload = { + sessionId, + role: data.role, + partialData: data.data, + lastUpdated: FieldValue.serverTimestamp(), + completionStatus: status, + }; + + await ref.set(payload, { merge: true }); + res.json({ success: true }); + } catch (error) { + console.error(error); + res.status(500).json({ error: "Failed to autosave" }); + } +}); + +router.post("/audience/commit-session", async (req, res) => { + try { + const { audienceData, surveyData, sessionId } = req.body; + + if (!audienceData) { + return res.status(400).json({ error: "Missing audienceData" }); + } + + if (!surveyData) { + return res.status(400).json({ error: "Missing surveyData" }); + } + + if (!sessionId) { + return res.status(400).json({ error: "Missing sessionId" }); + } + + const batch = db.batch(); + + const audienceRef = db.collection(AUDIENCE_COLLECTION).doc(); + const surveyRef = db.collection(AUDIENCE_SURVEY_COLLECTION).doc(); + const incompleteRef = db + .collection(AUDIENCE_INCOMPLETE_SESSION_COLLECTION) + .doc(sessionId); + + const audience = { + surveyResponse: surveyRef, + timestamps: [...(audienceData.timeStamps ?? []), new Date()], + }; + + batch.set(audienceRef, audience); + batch.set(surveyRef, { audienceId: audienceRef.id, ...surveyData }); + batch.delete(incompleteRef); + + await batch.commit(); + + res.json({ success: true, audienceId: audienceRef.id }); + } catch (error) { + console.error(error); + res.status(500).json({ error: "Batch commit failed" }); + } +}); export default router; From 2703db7cc371a87ceb1083ad9ad2eab1e88b38da Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 11 Jan 2026 12:00:58 -0500 Subject: [PATCH 05/18] status updates --- server/api/routes/firebaseAPI.ts | 9 ++++++--- src/App.tsx | 26 +++++++++++++++++--------- src/pages/audience/step2/Step2.tsx | 6 +++--- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index 2db1d85..4e01532 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -124,9 +124,12 @@ router.post("/audience/autosave", async (req, res) => { 2: "consent", 3: "pre-survey", 4: "instructions", - 5: "passage", - 6: "poems", - 7: "post-survey", + 5: "readPassage", + 6: "poemEvaluation1", + 7: "poemEvaluation2", + 8: "poemEvaluation3", + 9: "poemEvaluation4", + 10: "post-survey", }; const status = data.data?.timeStamps diff --git a/src/App.tsx b/src/App.tsx index e5869a1..712e74e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -81,16 +81,24 @@ function App() { setSessionId(id); }, []); - const enqueueAutosave = (data: UserData | null) => { + const autoSave = (data: UserData | null) => { if (!data || !sessionId) return; if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current); saveTimerRef.current = window.setTimeout(async () => { - await fetch("/api/firebase/artist/autosave", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ sessionId, data }), - }); + if (data.role === "artist") { + await fetch("/api/firebase/artist/autosave", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessionId, data }), + }); + } else { + await fetch("/api/firebase/audience/autosave", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessionId, data }), + }); + } }, 500); }; @@ -128,7 +136,7 @@ function App() { ...updates, }, }; - enqueueAutosave(next as UserData); + autoSave(next as UserData); return next; }); }; @@ -158,7 +166,7 @@ function App() { }, }, }; - enqueueAutosave(next as UserData); + autoSave(next as UserData); return next; }); }; @@ -188,7 +196,7 @@ function App() { }, }, }; - enqueueAutosave(next as UserData); + autoSave(next as UserData); return next; }); }; diff --git a/src/pages/audience/step2/Step2.tsx b/src/pages/audience/step2/Step2.tsx index cf038d2..bcb9328 100644 --- a/src/pages/audience/step2/Step2.tsx +++ b/src/pages/audience/step2/Step2.tsx @@ -56,6 +56,9 @@ const AudiencePoems = () => { }, []); const handleSubmit = () => { + addRoleSpecificData({ + timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], + }); if (currPoem < poems.length - 1) { setCurrPoem(currPoem + 1); const container = document.querySelector( @@ -68,9 +71,6 @@ const AudiencePoems = () => { } return; } - addRoleSpecificData({ - timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], - }); navigate("/audience/passage"); }; From a90207043b1e888d8915338f1c5f4f6a2fa4bbaf Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 11 Jan 2026 12:23:48 -0500 Subject: [PATCH 06/18] add poem data --- src/App.tsx | 29 +++++++++++++++++++++++++++++ src/pages/audience/step1/Step1.tsx | 1 + src/pages/audience/step2/Step2.tsx | 8 ++++++-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 712e74e..4c582cb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -36,6 +36,7 @@ import type { Audience, ArtistSurvey, AudienceSurvey, + SurveyAnswers, } from "./types"; import { Provider } from "./components/ui/provider"; import { Toaster } from "./components/ui/toaster"; @@ -53,6 +54,7 @@ interface DataContextValue { addPostSurvey: ( updates: Partial | Partial ) => void; + addPoemEvaluation: (poemId: string, answers: SurveyAnswers) => void; sessionId: string | null; flushSaves: () => Promise; } @@ -201,6 +203,32 @@ function App() { }); }; + const addPoemEvaluation = (poemId: string, answers: SurveyAnswers) => { + setUserData((prev: any) => { + if (!prev || !prev.data) { + throw new Error( + "Tried to update poem evaluation when userData is null." + ); + } + + const poemAnswer = { poemId, ...answers }; + const existingPoemAnswers = prev.data.surveyResponse?.poemAnswers ?? []; + + const next = { + ...prev, + data: { + ...prev.data, + surveyResponse: { + ...prev.data.surveyResponse, + poemAnswers: [...existingPoemAnswers, poemAnswer], + }, + }, + }; + autoSave(next as UserData); + return next; + }); + }; + // Flush saves on tab hide/close useEffect(() => { const onVisibility = () => { @@ -231,6 +259,7 @@ function App() { addRoleSpecificData, addPostSurvey, addPreSurvey, + addPoemEvaluation, sessionId, flushSaves, }} diff --git a/src/pages/audience/step1/Step1.tsx b/src/pages/audience/step1/Step1.tsx index abb3e27..94b4778 100644 --- a/src/pages/audience/step1/Step1.tsx +++ b/src/pages/audience/step1/Step1.tsx @@ -20,6 +20,7 @@ const AudiencePassage = () => { const handleSubmit = () => { addRoleSpecificData({ + passageId: passageId, timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], }); navigate("/audience/poems"); diff --git a/src/pages/audience/step2/Step2.tsx b/src/pages/audience/step2/Step2.tsx index bcb9328..dae03c1 100644 --- a/src/pages/audience/step2/Step2.tsx +++ b/src/pages/audience/step2/Step2.tsx @@ -9,6 +9,7 @@ import { AudiencePoemQuestions } from "../../../consts/surveyQuestions"; import { Button } from "@chakra-ui/react"; import { LuEyeClosed } from "react-icons/lu"; import { HiOutlineDocumentText } from "react-icons/hi2"; +import type { SurveyAnswers } from "../../../types"; const AudiencePoems = () => { const [currPoem, setCurrPoem] = useState(0); @@ -22,7 +23,7 @@ const AudiencePoems = () => { throw new Error("Component must be used within a DataContext.Provider"); } - const { userData, addRoleSpecificData } = context; + const { userData, addRoleSpecificData, addPoemEvaluation } = context; const passageId = (userData as any)?.data?.passage || "1"; @@ -55,10 +56,13 @@ const AudiencePoems = () => { } }, []); - const handleSubmit = () => { + const handleSubmit = (answers: SurveyAnswers) => { + addPoemEvaluation(String(currPoem), answers); + addRoleSpecificData({ timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], }); + if (currPoem < poems.length - 1) { setCurrPoem(currPoem + 1); const container = document.querySelector( From 55be15b35a71eb2be6ee4f2abaf4a95923a30e5e Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 11 Jan 2026 12:33:26 -0500 Subject: [PATCH 07/18] poem fixes --- src/App.tsx | 15 ++++++++++++--- src/pages/audience/step2/Step2.tsx | 6 ++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4c582cb..35a0b73 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -54,7 +54,11 @@ interface DataContextValue { addPostSurvey: ( updates: Partial | Partial ) => void; - addPoemEvaluation: (poemId: string, answers: SurveyAnswers) => void; + addPoemEvaluation: ( + poemId: string, + answers: SurveyAnswers, + additionalData?: Partial + ) => void; sessionId: string | null; flushSaves: () => Promise; } @@ -203,7 +207,11 @@ function App() { }); }; - const addPoemEvaluation = (poemId: string, answers: SurveyAnswers) => { + const addPoemEvaluation = ( + poemId: string, + answers: SurveyAnswers, + additionalData?: Partial + ) => { setUserData((prev: any) => { if (!prev || !prev.data) { throw new Error( @@ -218,10 +226,11 @@ function App() { ...prev, data: { ...prev.data, + ...additionalData, surveyResponse: { ...prev.data.surveyResponse, - poemAnswers: [...existingPoemAnswers, poemAnswer], }, + poemAnswers: [...existingPoemAnswers, poemAnswer], }, }; autoSave(next as UserData); diff --git a/src/pages/audience/step2/Step2.tsx b/src/pages/audience/step2/Step2.tsx index dae03c1..0fa6f8f 100644 --- a/src/pages/audience/step2/Step2.tsx +++ b/src/pages/audience/step2/Step2.tsx @@ -23,7 +23,7 @@ const AudiencePoems = () => { throw new Error("Component must be used within a DataContext.Provider"); } - const { userData, addRoleSpecificData, addPoemEvaluation } = context; + const { userData, addPoemEvaluation } = context; const passageId = (userData as any)?.data?.passage || "1"; @@ -57,9 +57,7 @@ const AudiencePoems = () => { }, []); const handleSubmit = (answers: SurveyAnswers) => { - addPoemEvaluation(String(currPoem), answers); - - addRoleSpecificData({ + addPoemEvaluation(String(currPoem), answers, { timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], }); From 19f4aaa152a262875db9e5ceaabaee75e0adaa66 Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 11 Jan 2026 12:46:00 -0500 Subject: [PATCH 08/18] poem eval fix --- src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 35a0b73..33bf26d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -229,8 +229,8 @@ function App() { ...additionalData, surveyResponse: { ...prev.data.surveyResponse, + poemAnswers: [...existingPoemAnswers, poemAnswer], }, - poemAnswers: [...existingPoemAnswers, poemAnswer], }, }; autoSave(next as UserData); From a735d4b054ab68972218f7a41b9d7a88d292606f Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 11 Jan 2026 15:48:37 -0500 Subject: [PATCH 09/18] randomize passages --- src/pages/audience/AudienceCaptcha.tsx | 9 +++++++++ src/pages/audience/step1/Step1.tsx | 2 +- src/pages/audience/step2/Step2.tsx | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/pages/audience/AudienceCaptcha.tsx b/src/pages/audience/AudienceCaptcha.tsx index a5bacdf..a44a714 100644 --- a/src/pages/audience/AudienceCaptcha.tsx +++ b/src/pages/audience/AudienceCaptcha.tsx @@ -4,6 +4,7 @@ import HalfPageTemplate from "../../components/shared/pages/halfPage"; import { Button, Input } from "@chakra-ui/react"; import { toaster } from "../../components/ui/toaster"; import { DataContext } from "../../App"; +import { Passages } from "../../consts/passages"; const TEST_CAPTCHA = "*TEST"; const Captcha = () => { @@ -35,6 +36,12 @@ const Captcha = () => { setCaptchaMessage(captcha_text); }; + const getRandomPassage = () => { + const numPassages = Passages.length; + const randomIndex = Math.floor(Math.random() * numPassages); + return randomIndex.toString(); + }; + useEffect(() => { if (canvasRef.current) { const canvas = canvasRef.current; @@ -71,12 +78,14 @@ const Captcha = () => { if (inputCaptcha === captchaMessage) { addUserData({ role: "audience" }); addRoleSpecificData({ + passageId: getRandomPassage(), timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], }); navigate("/consent"); } else if (inputCaptcha == TEST_CAPTCHA) { addUserData({ role: "audience" }); addRoleSpecificData({ + passageId: getRandomPassage(), timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], }); navigate("/consent"); diff --git a/src/pages/audience/step1/Step1.tsx b/src/pages/audience/step1/Step1.tsx index 94b4778..c4bc142 100644 --- a/src/pages/audience/step1/Step1.tsx +++ b/src/pages/audience/step1/Step1.tsx @@ -14,7 +14,7 @@ const AudiencePassage = () => { const { userData, addRoleSpecificData } = context; - const passageId = (userData as any)?.data?.passage || "1"; + const passageId = (userData as any)?.data?.passageId || "1"; const passage = Passages.find((p) => p.id === passageId) || Passages[0]; diff --git a/src/pages/audience/step2/Step2.tsx b/src/pages/audience/step2/Step2.tsx index 0fa6f8f..0e3790e 100644 --- a/src/pages/audience/step2/Step2.tsx +++ b/src/pages/audience/step2/Step2.tsx @@ -25,7 +25,7 @@ const AudiencePoems = () => { const { userData, addPoemEvaluation } = context; - const passageId = (userData as any)?.data?.passage || "1"; + const passageId = (userData as any)?.data?.passageId || "1"; const passage = Passages.find((p) => p.id === passageId) || Passages[0]; const words = passage.text.split(" "); From afc4421e949a68ea38521e581735668485a129bf Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 11 Jan 2026 16:02:19 -0500 Subject: [PATCH 10/18] get poems endpoint --- server/api/routes/firebaseAPI.ts | 40 ++++++++++++++++++++++++++++++++ src/types.ts | 13 +---------- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index 4e01532..9b992f9 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -197,4 +197,44 @@ router.post("/audience/commit-session", async (req, res) => { } }); +router.get("/audience/poems", async (req, res) => { + try { + const { passageId } = req.query; + + if (!passageId || typeof passageId !== "string") { + return res.status(400).json({ error: "Missing or invalid passageId" }); + } + + // query all poems with the given passageId + const snapshot = await db + .collection(POEM_COLLECTION) + .where("passageId", "==", passageId) + .get(); + + if (snapshot.empty) { + return res.status(404).json({ error: "No poems found for this passage" }); + } + + // map to { poemId, text } format + const allPoems = snapshot.docs.map((doc) => ({ + poemId: doc.id, + text: doc.data().text as number[], + })); + + // Fisher-Yates shuffle for true randomness + for (let i = allPoems.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [allPoems[i], allPoems[j]] = [allPoems[j], allPoems[i]]; + } + + // Take first 4 (or fewer if not enough poems exist) + const randomPoems = allPoems.slice(0, 4); + + res.json({ poems: randomPoems }); + } catch (error) { + console.error(error); + res.status(500).json({ error: "Failed to get poems" }); + } +}); + export default router; diff --git a/src/types.ts b/src/types.ts index 1052904..246e8bf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,17 +14,6 @@ export interface ArtistSurvey { postAnswers: SurveyAnswers; } -export interface AudiencePoem { - id: string; - poemId: string; -} - -// export interface SurveyQuestion { -// id: string; -// q: string; -// answerType: -// } - export interface Poem { passageId: string; // passageId in Passage.id passage: Passage; @@ -75,7 +64,7 @@ export type Role = (typeof Role)[keyof typeof Role]; export interface Audience { passageId: string; surveyResponse: AudienceSurvey; - poemsViewed: AudiencePoem[]; + poemsViewed: string[]; timeStamps: Date[]; } From ff758911a6fff5ba2b0257f94c7eb1a03bd9de4f Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 11 Jan 2026 16:25:17 -0500 Subject: [PATCH 11/18] get randomized poems --- src/pages/audience/step2/Step2.tsx | 76 +++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/src/pages/audience/step2/Step2.tsx b/src/pages/audience/step2/Step2.tsx index 0e3790e..bb433d8 100644 --- a/src/pages/audience/step2/Step2.tsx +++ b/src/pages/audience/step2/Step2.tsx @@ -1,20 +1,28 @@ import PageTemplate from "../../../components/shared/pages/audiencePages/scrollFullPage"; import { useNavigate } from "react-router-dom"; -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import { DataContext } from "../../../App"; import { Passages } from "../../../consts/passages"; -import { Poems } from "../../../consts/poems"; import SurveyScroll from "../../../components/survey/surveyScroll"; import { AudiencePoemQuestions } from "../../../consts/surveyQuestions"; -import { Button } from "@chakra-ui/react"; +import { Button, Spinner } from "@chakra-ui/react"; import { LuEyeClosed } from "react-icons/lu"; import { HiOutlineDocumentText } from "react-icons/hi2"; import type { SurveyAnswers } from "../../../types"; +interface FetchedPoem { + poemId: string; + text: number[]; +} + const AudiencePoems = () => { const [currPoem, setCurrPoem] = useState(0); const [showScrollTop, setShowScrollTop] = useState(false); const [showPassage, setShowPassage] = useState(false); + const [poems, setPoems] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const hasFetched = useRef(false); const navigate = useNavigate(); const context = useContext(DataContext); @@ -23,14 +31,44 @@ const AudiencePoems = () => { throw new Error("Component must be used within a DataContext.Provider"); } - const { userData, addPoemEvaluation } = context; + const { userData, addPoemEvaluation, addRoleSpecificData } = context; const passageId = (userData as any)?.data?.passageId || "1"; const passage = Passages.find((p) => p.id === passageId) || Passages[0]; const words = passage.text.split(" "); - const poems = Poems; + // Fetch poems from API + useEffect(() => { + if (hasFetched.current) return; + hasFetched.current = true; + + const fetchPoems = async () => { + try { + setIsLoading(true); + const response = await fetch( + `/api/firebase/audience/poems?passageId=${encodeURIComponent( + passageId + )}` + ); + if (!response.ok) { + throw new Error("Failed to fetch poems"); + } + const data = await response.json(); + setPoems(data.poems); + + // Save the poem IDs to user data + const poemIds = data.poems.map((p: FetchedPoem) => p.poemId); + addRoleSpecificData({ poemsViewed: poemIds }); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load poems"); + } finally { + setIsLoading(false); + } + }; + + fetchPoems(); + }, []); useEffect(() => { const container = document.querySelector( @@ -57,7 +95,7 @@ const AudiencePoems = () => { }, []); const handleSubmit = (answers: SurveyAnswers) => { - addPoemEvaluation(String(currPoem), answers, { + addPoemEvaluation(poems[currPoem].poemId, answers, { timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], }); @@ -76,6 +114,32 @@ const AudiencePoems = () => { navigate("/audience/passage"); }; + if (isLoading) { + return ( + +
+ +
+
+ ); + } + + if (error || poems.length === 0) { + return ( + +
+ {error || "No poems available for this passage"} +
+
+ ); + } + return ( Date: Sun, 11 Jan 2026 16:41:02 -0500 Subject: [PATCH 12/18] get artist statements - consider refactoring db --- server/api/routes/firebaseAPI.ts | 75 ++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index 9b992f9..eaa6afc 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -237,4 +237,79 @@ router.get("/audience/poems", async (req, res) => { } }); +router.post("/audience/artist-statements", async (req, res) => { + try { + const { poemIds } = req.body; + + if (!poemIds || !Array.isArray(poemIds) || poemIds.length === 0) { + return res + .status(400) + .json({ error: "Missing or invalid poemIds array" }); + } + + // Get statements for the requested poem IDs + const poemStatements = await Promise.all( + poemIds.map((id: string) => getArtistStatement(id)) + ); + + // Get all poems to find 4 random other statements + const allPoemsSnapshot = await db.collection(POEM_COLLECTION).get(); + const requestedPoemIdSet = new Set(poemIds); + + // Filter out the requested poems + const otherPoemIds = allPoemsSnapshot.docs + .map((doc) => doc.id) + .filter((id) => !requestedPoemIdSet.has(id)); + + // Fisher-Yates shuffle for random selection + for (let i = otherPoemIds.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [otherPoemIds[i], otherPoemIds[j]] = [otherPoemIds[j], otherPoemIds[i]]; + } + + // Get statements for 4 random other poems + const randomPoemIds = otherPoemIds.slice(0, 4); + const randomStatementsResults = await Promise.all( + randomPoemIds.map((id) => getArtistStatement(id)) + ); + const randomStatements = randomStatementsResults + .filter((s): s is { poemId: string; statement: string } => s !== null) + .map((s) => s.statement); + + res.json({ + poemStatements, + randomStatements, + }); + } catch (error) { + console.error(error); + res.status(500).json({ error: "Failed to get artist statements" }); + } +}); + +const getArtistStatement = async ( + poemId: string +): Promise<{ poemId: string; statement: string } | null> => { + // 1. get artistId from poemId + const poemDoc = await db.collection(POEM_COLLECTION).doc(poemId).get(); + if (!poemDoc.exists) return null; + + const artistId = poemDoc.data()?.artistId; + if (!artistId) return null; + + // 2. query survey collection for matching artistId + const surveySnapshot = await db + .collection(ARTIST_SURVEY_COLLECTION) + .where("artistId", "==", artistId) + .limit(1) + .get(); + + if (surveySnapshot.empty) return null; + + // 3. extract q14 from postAnswers + const statement = surveySnapshot.docs[0].data()?.postSurveyAnswers.q14; + if (!statement) return null; + + return { poemId, statement }; +}; + export default router; From c1e7c31fdfaf85a1fcfd8c3f7aca3aa5d643934f Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 18 Jan 2026 16:32:37 -0500 Subject: [PATCH 13/18] add 1 to the index --- src/pages/audience/AudienceCaptcha.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/audience/AudienceCaptcha.tsx b/src/pages/audience/AudienceCaptcha.tsx index a44a714..dc0cea7 100644 --- a/src/pages/audience/AudienceCaptcha.tsx +++ b/src/pages/audience/AudienceCaptcha.tsx @@ -38,7 +38,7 @@ const Captcha = () => { const getRandomPassage = () => { const numPassages = Passages.length; - const randomIndex = Math.floor(Math.random() * numPassages); + const randomIndex = Math.floor(Math.random() * numPassages) + 1; return randomIndex.toString(); }; From f89cb7a2c65162c69ba9d9bb04c6d08b849f5fd6 Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 18 Jan 2026 17:13:07 -0500 Subject: [PATCH 14/18] grab artist statements yay --- server/api/routes/firebaseAPI.ts | 29 ++------ src/pages/audience/step2/Step2Rank.tsx | 98 ++++++++++++++++++++++---- 2 files changed, 89 insertions(+), 38 deletions(-) diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index eaa6afc..ba082a1 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -252,34 +252,13 @@ router.post("/audience/artist-statements", async (req, res) => { poemIds.map((id: string) => getArtistStatement(id)) ); - // Get all poems to find 4 random other statements - const allPoemsSnapshot = await db.collection(POEM_COLLECTION).get(); - const requestedPoemIdSet = new Set(poemIds); - - // Filter out the requested poems - const otherPoemIds = allPoemsSnapshot.docs - .map((doc) => doc.id) - .filter((id) => !requestedPoemIdSet.has(id)); - - // Fisher-Yates shuffle for random selection - for (let i = otherPoemIds.length - 1; i > 0; i--) { + // Fisher-Yates shuffle to randomize statement order + for (let i = poemStatements.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); - [otherPoemIds[i], otherPoemIds[j]] = [otherPoemIds[j], otherPoemIds[i]]; + [poemStatements[i], poemStatements[j]] = [poemStatements[j], poemStatements[i]]; } - // Get statements for 4 random other poems - const randomPoemIds = otherPoemIds.slice(0, 4); - const randomStatementsResults = await Promise.all( - randomPoemIds.map((id) => getArtistStatement(id)) - ); - const randomStatements = randomStatementsResults - .filter((s): s is { poemId: string; statement: string } => s !== null) - .map((s) => s.statement); - - res.json({ - poemStatements, - randomStatements, - }); + res.json({ poemStatements }); } catch (error) { console.error(error); res.status(500).json({ error: "Failed to get artist statements" }); diff --git a/src/pages/audience/step2/Step2Rank.tsx b/src/pages/audience/step2/Step2Rank.tsx index 4a69804..da0490d 100644 --- a/src/pages/audience/step2/Step2Rank.tsx +++ b/src/pages/audience/step2/Step2Rank.tsx @@ -8,29 +8,88 @@ import SurveyScroll from "../../../components/survey/surveyScroll"; import { AudienceRankingQuestions } from "../../../consts/surveyQuestions"; import type { SurveyDefinition, Section } from "../../../types"; +// TODO: Remove - Hard-coded fallback data for standalone rendering/testing +const defaultContextValue = { + userData: { + role: "audience" as const, + data: { + passage: "1", + timeStamps: [] as Date[], + poemsViewed: ["nvDp4FklkwSvxAMsyNqn", "5XWe4xHm6G1e9d43hKW7", "La33yHt4rC5vKg23fs7b", "FbCyvCErYRKrImwaksMZ"], // fallback poem IDs for testing + }, + }, + addRoleSpecificData: (_updates: any) => { + console.log("[Standalone Mode] addRoleSpecificData called:", _updates); + }, +}; + +// Fallback statements for standalone testing or when API fails +const fallbackStatements = [ + "Statement A", + "Statement B", + "Statement C", + "Statement D", +]; + const AudienceRanking = () => { const [showScrollTop, setShowScrollTop] = useState(false); + const [artistStatements, setArtistStatements] = useState([]); + const [isLoading, setIsLoading] = useState(true); const navigate = useNavigate(); const context = useContext(DataContext); - if (!context) { - throw new Error("Component must be used within a DataContext.Provider"); - } - - const { userData, addRoleSpecificData } = context; + // Use context if available, otherwise fall back to hard-coded defaults + const { userData, addRoleSpecificData } = context ?? defaultContextValue; const passageId = (userData as any)?.data?.passage || "1"; + // TODO: Remove hard coded values + const poemsViewed: string[] = (userData as any)?.data?.poemsViewed || ["nvDp4FklkwSvxAMsyNqn", "5XWe4xHm6G1e9d43hKW7", "La33yHt4rC5vKg23fs7b", "FbCyvCErYRKrImwaksMZ"]; const passage = Passages.find((p) => p.id === passageId) || Passages[0]; const poems = Poems; - const artistStatements = [ - "Statement A", - "Statement B", - "Statement C", - "Statement D", - "Unsure", - ]; + + // Fetch artist statements on mount + useEffect(() => { + const fetchArtistStatements = async () => { + if (poemsViewed.length === 0) { + setArtistStatements(fallbackStatements); + setIsLoading(false); + return; + } + + try { + const response = await fetch("/api/firebase/audience/artist-statements", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ poemIds: poemsViewed }), + }); + + if (!response.ok) { + throw new Error("Failed to fetch artist statements"); + } + + const data = await response.json(); + // Extract statements from the response, filter out nulls + const statements = data.poemStatements + .filter((s: { poemId: string; statement: string } | null) => s !== null) + .map((s: { poemId: string; statement: string }) => s.statement); + + // Use fetched statements or fallback if empty + setArtistStatements(statements.length > 0 ? statements : fallbackStatements); + } catch (error) { + console.error("Error fetching artist statements:", error); + setArtistStatements(fallbackStatements); + } finally { + setIsLoading(false); + } + }; + + fetchArtistStatements(); + }, []); + + // Options for multiple choice: statements + "Unsure" + const statementOptions = [...artistStatements, "Unsure"]; const surveyWithItems = (() => { const words = passage.text.split(" "); @@ -122,7 +181,7 @@ const AudienceRanking = () => { ), question: `Poem ${i + 1}`, - options: artistStatements, + options: statementOptions, required: true, }, { @@ -184,6 +243,19 @@ const AudienceRanking = () => { navigate("/audience/ai"); }; + if (isLoading) { + return ( + +
+

Loading...

+
+
+ ); + } + return ( Date: Tue, 20 Jan 2026 22:51:42 -0500 Subject: [PATCH 15/18] ranking --- src/App.tsx | 24 ++++++ src/pages/audience/step2/Step2Rank.tsx | 109 ++++++++++++++++++++----- src/types.ts | 22 +++++ 3 files changed, 133 insertions(+), 22 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index ca7484e..ac0bba7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -38,6 +38,7 @@ import type { ArtistSurvey, AudienceSurvey, SurveyAnswers, + RankingData, } from "./types"; import { Provider } from "./components/ui/provider"; import { Toaster } from "./components/ui/toaster"; @@ -64,6 +65,7 @@ interface DataContextValue { answers: SurveyAnswers, additionalData?: Partial ) => void; + addRankSurvey: (rankingData: RankingData) => void; sessionId: string | null; flushSaves: () => Promise; } @@ -243,6 +245,27 @@ function App() { }); }; + const addRankSurvey = (rankingData: RankingData) => { + setUserData((prev: any) => { + if (!prev || !prev.data) { + throw new Error("Tried to update rank survey when userData is null."); + } + + const next = { + ...prev, + data: { + ...prev.data, + surveyResponse: { + ...prev.data.surveyResponse, + rankingData, + }, + }, + }; + autoSave(next as UserData); + return next; + }); + }; + // Flush saves on tab hide/close useEffect(() => { const onVisibility = () => { @@ -274,6 +297,7 @@ function App() { addPostSurvey, addPreSurvey, addPoemEvaluation, + addRankSurvey, sessionId, flushSaves, }} diff --git a/src/pages/audience/step2/Step2Rank.tsx b/src/pages/audience/step2/Step2Rank.tsx index da0490d..21102a5 100644 --- a/src/pages/audience/step2/Step2Rank.tsx +++ b/src/pages/audience/step2/Step2Rank.tsx @@ -6,22 +6,25 @@ import { Passages } from "../../../consts/passages"; import { Poems } from "../../../consts/poems"; import SurveyScroll from "../../../components/survey/surveyScroll"; import { AudienceRankingQuestions } from "../../../consts/surveyQuestions"; -import type { SurveyDefinition, Section } from "../../../types"; +import type { SurveyDefinition, Section, SurveyAnswers, RankingData, StatementMatch, PoemRankings } from "../../../types"; // TODO: Remove - Hard-coded fallback data for standalone rendering/testing -const defaultContextValue = { - userData: { - role: "audience" as const, - data: { - passage: "1", - timeStamps: [] as Date[], - poemsViewed: ["nvDp4FklkwSvxAMsyNqn", "5XWe4xHm6G1e9d43hKW7", "La33yHt4rC5vKg23fs7b", "FbCyvCErYRKrImwaksMZ"], // fallback poem IDs for testing - }, - }, - addRoleSpecificData: (_updates: any) => { - console.log("[Standalone Mode] addRoleSpecificData called:", _updates); - }, -}; +// const defaultContextValue = { +// userData: { +// role: "audience" as const, +// data: { +// passage: "1", +// timeStamps: [] as Date[], +// poemsViewed: ["nvDp4FklkwSvxAMsyNqn", "5XWe4xHm6G1e9d43hKW7", "La33yHt4rC5vKg23fs7b", "FbCyvCErYRKrImwaksMZ"], // fallback poem IDs for testing +// }, +// }, +// addRoleSpecificData: (_updates: any) => { +// console.log("[Standalone Mode] addRoleSpecificData called:", _updates); +// }, +// addRankSurvey: (_rankingData: RankingData) => { +// console.log("[Standalone Mode] addRankSurvey called:", _rankingData); +// }, +// }; // Fallback statements for standalone testing or when API fails const fallbackStatements = [ @@ -31,16 +34,23 @@ const fallbackStatements = [ "Statement D", ]; +// Type for tracking which statement belongs to which poem +type PoemStatementMap = Record; // poemId -> correct statement + const AudienceRanking = () => { const [showScrollTop, setShowScrollTop] = useState(false); const [artistStatements, setArtistStatements] = useState([]); + const [correctStatements, setCorrectStatements] = useState({}); // Maps poemId to its correct statement const [isLoading, setIsLoading] = useState(true); const navigate = useNavigate(); const context = useContext(DataContext); - // Use context if available, otherwise fall back to hard-coded defaults - const { userData, addRoleSpecificData } = context ?? defaultContextValue; + if (!context) { + throw new Error("Component must be used within a DataContext.Provider"); + } + + const { userData, addRoleSpecificData, addRankSurvey } = context; const passageId = (userData as any)?.data?.passage || "1"; // TODO: Remove hard coded values @@ -70,12 +80,20 @@ const AudienceRanking = () => { } const data = await response.json(); - // Extract statements from the response, filter out nulls - const statements = data.poemStatements - .filter((s: { poemId: string; statement: string } | null) => s !== null) - .map((s: { poemId: string; statement: string }) => s.statement); + // Filter out nulls and build the correct statement mapping + const validStatements = data.poemStatements.filter( + (s: { poemId: string; statement: string } | null) => s !== null + ) as { poemId: string; statement: string }[]; - // Use fetched statements or fallback if empty + // Build poemId -> statement mapping for correctness checking + const statementMap: PoemStatementMap = {}; + validStatements.forEach((s) => { + statementMap[s.poemId] = s.statement; + }); + setCorrectStatements(statementMap); + + // Extract just the statement strings for display options (already shuffled by API) + const statements = validStatements.map((s) => s.statement); setArtistStatements(statements.length > 0 ? statements : fallbackStatements); } catch (error) { console.error("Error fetching artist statements:", error); @@ -236,7 +254,54 @@ const AudienceRanking = () => { } }, []); - const handleSubmit = () => { + const handleSubmit = (answers: SurveyAnswers) => { + // Helper to process a dragRank answer into ordered poemIds + const processRanking = (questionId: string): string[] => { + const rankingAnswer = answers[questionId] as string[] | undefined; + return rankingAnswer + ? rankingAnswer.map((itemId) => { + // Extract poem index from item ID (e.g., "q1-poem-2" -> 2) + const match = itemId.match(/poem-(\d+)$/); + const index = match ? parseInt(match[1], 10) : 0; + return poemsViewed[index] || itemId; + }) + : []; + }; + + // Process all three rankings + const poemRankings: PoemRankings = { + favourite: processRanking("q1"), + impact: processRanking("q2"), + creative: processRanking("q3"), + }; + + // Process statement matching answers + const statementMatches: StatementMatch[] = poems.map((_, i) => { + const poemId = poemsViewed[i] || `poem-${i}`; + const chosenStatement = (answers[`q4-poem-${i}`] as string) || ""; + const explanation = (answers[`q4-poem-${i}-unsure`] as string) || undefined; + + // Check if the chosen statement matches the correct one for this poem + const correctStatement = correctStatements[poemId]; + const isCorrect = chosenStatement !== "Unsure" && chosenStatement === correctStatement; + + return { + poemId, + isCorrect, + chosenStatement, + ...(chosenStatement === "Unsure" && explanation ? { explanation } : {}), + }; + }); + + // Save ranking data + const rankingData: RankingData = { + poemRankings, + statementMatches, + }; + console.log(rankingData); + addRankSurvey(rankingData); + + // Update timestamps and navigate addRoleSpecificData({ timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], }); diff --git a/src/types.ts b/src/types.ts index 070f70f..82da27d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -77,6 +77,9 @@ export interface AudienceSurvey { preAnswers: SurveyAnswers; poemSurvey: PoemSurveyDefinition[]; poemAnswers: PoemSurveyAnswers[]; + rankingData: RankingData; + postRankSurvey: SurveyDefinition; + postRankAnswers: SurveyAnswers; postSurvey: SurveyDefinition; postAnswers: SurveyAnswers; AISurvey: SurveyDefinition; @@ -177,6 +180,25 @@ export interface PoemSurveyAnswers extends SurveyAnswers { poemId: string; } +// Ranking survey data structures +export interface StatementMatch { + poemId: string; + isCorrect: boolean; + chosenStatement: string; + explanation?: string; +} + +export interface PoemRankings { + favourite: string[]; // most liked to least liked + impact: string[]; // most emotionally impactful to least + creative: string[]; // most creative to least +} + +export interface RankingData { + poemRankings: PoemRankings; + statementMatches: StatementMatch[]; +} + export type AnswerValue = string | string[] | number | null; export interface SurveyAnswers { From 77c2c79e77431486ab3c9e55f7ba45594f8b3646 Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Tue, 20 Jan 2026 23:09:36 -0500 Subject: [PATCH 16/18] all endpoints --- src/App.tsx | 47 ++++++++++++++++ src/pages/audience/step2/Step2AI.tsx | 55 ++++++++++++++++++- .../audience/step2/Step2AIDisclousure.tsx | 29 +++++++++- src/types.ts | 10 ++-- 4 files changed, 131 insertions(+), 10 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index ac0bba7..ee34f4d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,6 +39,7 @@ import type { AudienceSurvey, SurveyAnswers, RankingData, + ReRankingData, } from "./types"; import { Provider } from "./components/ui/provider"; import { Toaster } from "./components/ui/toaster"; @@ -66,6 +67,8 @@ interface DataContextValue { additionalData?: Partial ) => void; addRankSurvey: (rankingData: RankingData) => void; + addAISurvey: (answers: SurveyAnswers) => void; + addReRankSurvey: (reRankingData: ReRankingData) => void; sessionId: string | null; flushSaves: () => Promise; } @@ -266,6 +269,48 @@ function App() { }); }; + const addAISurvey = (answers: SurveyAnswers) => { + setUserData((prev: any) => { + if (!prev || !prev.data) { + throw new Error("Tried to update AI survey when userData is null."); + } + + const next = { + ...prev, + data: { + ...prev.data, + surveyResponse: { + ...prev.data.surveyResponse, + AIAnswers: answers, + }, + }, + }; + autoSave(next as UserData); + return next; + }); + }; + + const addReRankSurvey = (reRankingData: ReRankingData) => { + setUserData((prev: any) => { + if (!prev || !prev.data) { + throw new Error("Tried to update re-rank survey when userData is null."); + } + + const next = { + ...prev, + data: { + ...prev.data, + surveyResponse: { + ...prev.data.surveyResponse, + reRankingData, + }, + }, + }; + autoSave(next as UserData); + return next; + }); + }; + // Flush saves on tab hide/close useEffect(() => { const onVisibility = () => { @@ -298,6 +343,8 @@ function App() { addPreSurvey, addPoemEvaluation, addRankSurvey, + addAISurvey, + addReRankSurvey, sessionId, flushSaves, }} diff --git a/src/pages/audience/step2/Step2AI.tsx b/src/pages/audience/step2/Step2AI.tsx index 75dc237..7e49ddf 100644 --- a/src/pages/audience/step2/Step2AI.tsx +++ b/src/pages/audience/step2/Step2AI.tsx @@ -6,7 +6,25 @@ import { Passages } from "../../../consts/passages"; import { Poems } from "../../../consts/poems"; import SurveyScroll from "../../../components/survey/surveyScroll"; import { AudienceReRankingQuestions } from "../../../consts/surveyQuestions"; -import type { SurveyDefinition, Section } from "../../../types"; +import type { SurveyDefinition, Section, SurveyAnswers, PoemRankings, ReRankingData } from "../../../types"; + +// Dummy data for standalone rendering/testing +// const defaultContextValue = { +// userData: { +// role: "audience" as const, +// data: { +// passage: "1", +// timeStamps: [] as Date[], +// poemsViewed: ["poem1", "poem2", "poem3", "poem4"], +// }, +// }, +// addRoleSpecificData: (_updates: any) => { +// console.log("[Standalone Mode] addRoleSpecificData called:", _updates); +// }, +// addReRankSurvey: (_reRankingData: ReRankingData) => { +// console.log("[Standalone Mode] addReRankSurvey called:", _reRankingData); +// }, +// }; const AudienceReRanking = () => { const [showScrollTop, setShowScrollTop] = useState(false); @@ -18,9 +36,12 @@ const AudienceReRanking = () => { throw new Error("Component must be used within a DataContext.Provider"); } - const { userData, addRoleSpecificData } = context; + const { userData, addRoleSpecificData, addReRankSurvey } = context; + // const { userData, addRoleSpecificData, addReRankSurvey } = context ?? defaultContextValue; + const passageId = (userData as any)?.data?.passage || "1"; + const poemsViewed: string[] = (userData as any)?.data?.poemsViewed || ["poem1", "poem2", "poem3", "poem4"]; const passage = Passages.find((p) => p.id === passageId) || Passages[0]; const poems = Poems; @@ -116,7 +137,35 @@ const AudienceReRanking = () => { } }, []); - const handleSubmit = () => { + const handleSubmit = (answers: SurveyAnswers) => { + // Helper to process a dragRank answer into ordered poemIds + const processRanking = (questionId: string): string[] => { + const rankingAnswer = answers[questionId] as string[] | undefined; + return rankingAnswer + ? rankingAnswer.map((itemId) => { + // Extract poem index from item ID (e.g., "q1-poem-2" -> 2) + const match = itemId.match(/poem-(\d+)$/); + const index = match ? parseInt(match[1], 10) : 0; + return poemsViewed[index] || itemId; + }) + : []; + }; + + // Process all three re-rankings + const poemRankings: PoemRankings = { + favourite: processRanking("q1"), + impact: processRanking("q2"), + creative: processRanking("q3"), + }; + + // Save re-ranking data + const reRankingData: ReRankingData = { + poemRankings, + }; + console.log(reRankingData); + addReRankSurvey(reRankingData); + + // Update timestamps and navigate addRoleSpecificData({ timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], }); diff --git a/src/pages/audience/step2/Step2AIDisclousure.tsx b/src/pages/audience/step2/Step2AIDisclousure.tsx index 6c0bb57..4fc35b2 100644 --- a/src/pages/audience/step2/Step2AIDisclousure.tsx +++ b/src/pages/audience/step2/Step2AIDisclousure.tsx @@ -6,7 +6,24 @@ import { Passages } from "../../../consts/passages"; import { Poems } from "../../../consts/poems"; import SurveyScroll from "../../../components/survey/surveyScroll"; import { AudienceAIQuestionSurvey } from "../../../consts/surveyQuestions"; -import type { SurveyDefinition, Section } from "../../../types"; +import type { SurveyDefinition, Section, SurveyAnswers } from "../../../types"; + +// Dummy data for standalone rendering/testing +// const defaultContextValue = { +// userData: { +// role: "audience" as const, +// data: { +// passage: "1", +// timeStamps: [] as Date[], +// }, +// }, +// addRoleSpecificData: (_updates: any) => { +// console.log("[Standalone Mode] addRoleSpecificData called:", _updates); +// }, +// addAISurvey: (_answers: SurveyAnswers) => { +// console.log("[Standalone Mode] addAISurvey called:", _answers); +// }, +// }; const AudienceAI = () => { const [showScrollTop, setShowScrollTop] = useState(false); @@ -18,7 +35,8 @@ const AudienceAI = () => { throw new Error("Component must be used within a DataContext.Provider"); } - const { userData, addRoleSpecificData } = context; + const { userData, addRoleSpecificData, addAISurvey } = context; + // const { userData, addRoleSpecificData, addAISurvey } = context ?? defaultContextValue; const passageId = (userData as any)?.data?.passage || "1"; @@ -51,7 +69,12 @@ const AudienceAI = () => { } }, []); - const handleSubmit = () => { + const handleSubmit = (answers: SurveyAnswers) => { + // Save AI survey answers + console.log(answers); + addAISurvey(answers); + + // Update timestamps and navigate addRoleSpecificData({ timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], }); diff --git a/src/types.ts b/src/types.ts index 82da27d..f347923 100644 --- a/src/types.ts +++ b/src/types.ts @@ -78,12 +78,10 @@ export interface AudienceSurvey { poemSurvey: PoemSurveyDefinition[]; poemAnswers: PoemSurveyAnswers[]; rankingData: RankingData; - postRankSurvey: SurveyDefinition; - postRankAnswers: SurveyAnswers; + AIAnswers: SurveyAnswers; + reRankingData: ReRankingData; postSurvey: SurveyDefinition; postAnswers: SurveyAnswers; - AISurvey: SurveyDefinition; - AIAnswers: SurveyAnswers; } // TODO: Exact poem feedback fields tbd @@ -199,6 +197,10 @@ export interface RankingData { statementMatches: StatementMatch[]; } +export interface ReRankingData { + poemRankings: PoemRankings; +} + export type AnswerValue = string | string[] | number | null; export interface SurveyAnswers { From 09dc28f621f3f971179f78693d4ff14bd040e128 Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Tue, 20 Jan 2026 23:11:47 -0500 Subject: [PATCH 17/18] yeehaw --- server/api/routes/firebaseAPI.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index ba082a1..1ee75f1 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -179,13 +179,27 @@ router.post("/audience/commit-session", async (req, res) => { .collection(AUDIENCE_INCOMPLETE_SESSION_COLLECTION) .doc(sessionId); + // Main audience document with references and metadata const audience = { + passageId: audienceData.passageId, + poemsViewed: audienceData.poemsViewed ?? [], surveyResponse: surveyRef, timestamps: [...(audienceData.timeStamps ?? []), new Date()], }; + // Survey document with all survey responses + const survey = { + audienceId: audienceRef.id, + preAnswers: surveyData.preAnswers ?? {}, + poemAnswers: surveyData.poemAnswers ?? [], + rankingData: surveyData.rankingData ?? {}, + AIAnswers: surveyData.AIAnswers ?? {}, + reRankingData: surveyData.reRankingData ?? {}, + postAnswers: surveyData.postAnswers ?? {}, + }; + batch.set(audienceRef, audience); - batch.set(surveyRef, { audienceId: audienceRef.id, ...surveyData }); + batch.set(surveyRef, survey); batch.delete(incompleteRef); await batch.commit(); From edd808f77d6651ba6529bdf7b1847c12180ad9e6 Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Tue, 20 Jan 2026 23:30:16 -0500 Subject: [PATCH 18/18] yeehaw --- server/api/routes/firebaseAPI.ts | 6 +- src/pages/audience/PostSurvey.tsx | 69 ++++++++++++++++--- src/pages/audience/step2/Step2AI.tsx | 2 +- .../audience/step2/Step2AIDisclousure.tsx | 2 +- src/pages/audience/step2/Step2Rank.tsx | 2 +- 5 files changed, 67 insertions(+), 14 deletions(-) diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index 1ee75f1..5f8fda9 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -180,11 +180,15 @@ router.post("/audience/commit-session", async (req, res) => { .doc(sessionId); // Main audience document with references and metadata + const existingTimestamps = (audienceData.timeStamps ?? []).map( + (ts: string | Date) => new Date(ts) + ); const audience = { passageId: audienceData.passageId, poemsViewed: audienceData.poemsViewed ?? [], surveyResponse: surveyRef, - timestamps: [...(audienceData.timeStamps ?? []), new Date()], + timestamps: [...existingTimestamps, new Date()], + }; // Survey document with all survey responses diff --git a/src/pages/audience/PostSurvey.tsx b/src/pages/audience/PostSurvey.tsx index b81675c..dd062d8 100644 --- a/src/pages/audience/PostSurvey.tsx +++ b/src/pages/audience/PostSurvey.tsx @@ -4,6 +4,8 @@ import { DataContext } from "../../App"; import { AudiencePostSurveyQuestions } from "../../consts/surveyQuestions"; import SurveyScroll from "../../components/survey/surveyScroll"; import PageTemplate from "../../components/shared/pages/audiencePages/scrollFullPage"; +import type { Audience } from "../../types"; +import { toaster } from "../../components/ui/toaster"; const AudiencePostSurvey = () => { const navigate = useNavigate(); @@ -13,18 +15,65 @@ const AudiencePostSurvey = () => { throw new Error("Component must be used within a DataContext.Provider"); } - const { userData, addPreSurvey, addRoleSpecificData } = context; + const { userData, addPostSurvey, sessionId } = context; - const handleSubmit = (answers: any) => { - addRoleSpecificData({ - timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], - }); - navigate("/audience/thank-you"); - addPreSurvey({ - id: "audienceSurvey", - preSurvey: AudiencePostSurveyQuestions, - preAnswers: answers, + const submitDb = async (answers: any) => { + if (!userData || !userData.data) { + console.error("userData not loaded yet!"); + return; + } + + const audienceData = userData.data as Audience; + const survey = audienceData.surveyResponse; + + const surveyData = { + preAnswers: survey?.preAnswers ?? {}, + poemAnswers: survey?.poemAnswers ?? [], + rankingData: survey?.rankingData ?? {}, + AIAnswers: survey?.AIAnswers ?? {}, + reRankingData: survey?.reRankingData ?? {}, + postAnswers: answers, + }; + + try { + const response = await fetch("/api/firebase/audience/commit-session", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + audienceData, + surveyData, + sessionId, + }), + }); + + if (!response.ok) { + throw new Error("Failed to commit session"); + } + + toaster.create({ + description: "Survey successfully submitted!", + type: "success", + duration: 5000, + }); + navigate("/audience/thank-you"); + } catch (error) { + console.error("Error saving data:", error); + toaster.create({ + description: "There was an error submitting your survey. Please try again.", + type: "error", + duration: 5000, + }); + } + }; + + const handleSubmit = async (answers: any) => { + // Save post-survey answers locally + addPostSurvey({ + postAnswers: answers, }); + + // Commit to database + await submitDb(answers); }; return ( diff --git a/src/pages/audience/step2/Step2AI.tsx b/src/pages/audience/step2/Step2AI.tsx index 7e49ddf..a4859c6 100644 --- a/src/pages/audience/step2/Step2AI.tsx +++ b/src/pages/audience/step2/Step2AI.tsx @@ -40,7 +40,7 @@ const AudienceReRanking = () => { // const { userData, addRoleSpecificData, addReRankSurvey } = context ?? defaultContextValue; - const passageId = (userData as any)?.data?.passage || "1"; + const passageId = (userData as any)?.data?.passageId || "1"; const poemsViewed: string[] = (userData as any)?.data?.poemsViewed || ["poem1", "poem2", "poem3", "poem4"]; const passage = Passages.find((p) => p.id === passageId) || Passages[0]; diff --git a/src/pages/audience/step2/Step2AIDisclousure.tsx b/src/pages/audience/step2/Step2AIDisclousure.tsx index 4fc35b2..8da391b 100644 --- a/src/pages/audience/step2/Step2AIDisclousure.tsx +++ b/src/pages/audience/step2/Step2AIDisclousure.tsx @@ -38,7 +38,7 @@ const AudienceAI = () => { const { userData, addRoleSpecificData, addAISurvey } = context; // const { userData, addRoleSpecificData, addAISurvey } = context ?? defaultContextValue; - const passageId = (userData as any)?.data?.passage || "1"; + const passageId = (userData as any)?.data?.passageId || "1"; const passage = Passages.find((p) => p.id === passageId) || Passages[0]; const words = passage.text.split(" "); diff --git a/src/pages/audience/step2/Step2Rank.tsx b/src/pages/audience/step2/Step2Rank.tsx index 21102a5..f62402e 100644 --- a/src/pages/audience/step2/Step2Rank.tsx +++ b/src/pages/audience/step2/Step2Rank.tsx @@ -52,7 +52,7 @@ const AudienceRanking = () => { const { userData, addRoleSpecificData, addRankSurvey } = context; - const passageId = (userData as any)?.data?.passage || "1"; + const passageId = (userData as any)?.data?.passageId || "1"; // TODO: Remove hard coded values const poemsViewed: string[] = (userData as any)?.data?.poemsViewed || ["nvDp4FklkwSvxAMsyNqn", "5XWe4xHm6G1e9d43hKW7", "La33yHt4rC5vKg23fs7b", "FbCyvCErYRKrImwaksMZ"];