diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index c21fe19..5f8fda9 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -3,12 +3,19 @@ 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"; -router.post("/autosave", async (req, res) => { +// AUDIENCE COLLECTIONS +const AUDIENCE_COLLECTION = "audience"; +const AUDIENCE_SURVEY_COLLECTION = "audienceSurvey"; +const AUDIENCE_INCOMPLETE_SESSION_COLLECTION = "audienceIncompleteSession"; + +// ARTIST ROUTES +router.post("/artist/autosave", async (req, res) => { try { const { sessionId, data } = req.body; @@ -32,7 +39,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, @@ -49,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; @@ -75,7 +84,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 = { @@ -99,4 +108,205 @@ router.post("/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: "readPassage", + 6: "poemEvaluation1", + 7: "poemEvaluation2", + 8: "poemEvaluation3", + 9: "poemEvaluation4", + 10: "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); + + // 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: [...existingTimestamps, 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, survey); + 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" }); + } +}); + +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" }); + } +}); + +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)) + ); + + // Fisher-Yates shuffle to randomize statement order + for (let i = poemStatements.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [poemStatements[i], poemStatements[j]] = [poemStatements[j], poemStatements[i]]; + } + + res.json({ poemStatements }); + } 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; diff --git a/src/App.tsx b/src/App.tsx index e3d874f..ee34f4d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -37,6 +37,9 @@ import type { Audience, ArtistSurvey, AudienceSurvey, + SurveyAnswers, + RankingData, + ReRankingData, } from "./types"; import { Provider } from "./components/ui/provider"; import { Toaster } from "./components/ui/toaster"; @@ -58,6 +61,14 @@ interface DataContextValue { addPostSurvey: ( updates: Partial | Partial ) => void; + addPoemEvaluation: ( + poemId: string, + answers: SurveyAnswers, + additionalData?: Partial + ) => void; + addRankSurvey: (rankingData: RankingData) => void; + addAISurvey: (answers: SurveyAnswers) => void; + addReRankSurvey: (reRankingData: ReRankingData) => void; sessionId: string | null; flushSaves: () => Promise; } @@ -86,16 +97,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/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); }; @@ -133,7 +152,7 @@ function App() { ...updates, }, }; - enqueueAutosave(next as UserData); + autoSave(next as UserData); return next; }); }; @@ -163,7 +182,7 @@ function App() { }, }, }; - enqueueAutosave(next as UserData); + autoSave(next as UserData); return next; }); }; @@ -193,7 +212,101 @@ function App() { }, }, }; - enqueueAutosave(next as UserData); + autoSave(next as UserData); + return next; + }); + }; + + const addPoemEvaluation = ( + poemId: string, + answers: SurveyAnswers, + additionalData?: Partial + ) => { + 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, + ...additionalData, + surveyResponse: { + ...prev.data.surveyResponse, + poemAnswers: [...existingPoemAnswers, poemAnswer], + }, + }, + }; + autoSave(next as UserData); + return next; + }); + }; + + 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; + }); + }; + + 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; }); }; @@ -228,6 +341,10 @@ function App() { addRoleSpecificData, addPostSurvey, addPreSurvey, + addPoemEvaluation, + addRankSurvey, + addAISurvey, + addReRankSurvey, sessionId, flushSaves, }} 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({ diff --git a/src/pages/audience/AudienceCaptcha.tsx b/src/pages/audience/AudienceCaptcha.tsx index a5bacdf..dc0cea7 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) + 1; + 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/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/step1/Step1.tsx b/src/pages/audience/step1/Step1.tsx index dbb3de1..29bf310 100644 --- a/src/pages/audience/step1/Step1.tsx +++ b/src/pages/audience/step1/Step1.tsx @@ -14,12 +14,13 @@ 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]; 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 c2de818..c7374a6 100644 --- a/src/pages/audience/step2/Step2.tsx +++ b/src/pages/audience/step2/Step2.tsx @@ -1,19 +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); @@ -22,14 +31,44 @@ const AudiencePoems = () => { throw new Error("Component must be used within a DataContext.Provider"); } - const { userData, addRoleSpecificData } = context; + const { userData, addPoemEvaluation, 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]; 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( @@ -55,7 +94,11 @@ const AudiencePoems = () => { } }, []); - const handleSubmit = () => { + const handleSubmit = (answers: SurveyAnswers) => { + addPoemEvaluation(poems[currPoem].poemId, answers, { + timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], + }); + if (currPoem < poems.length - 1) { setCurrPoem(currPoem + 1); const container = document.querySelector( @@ -74,6 +117,32 @@ const AudiencePoems = () => { navigate("/audience/ranking"); }; + if (isLoading) { + return ( + +
+ +
+
+ ); + } + + if (error || poems.length === 0) { + return ( + +
+ {error || "No poems available for this passage"} +
+
+ ); + } + return ( { +// 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 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]; 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..8da391b 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,9 +35,10 @@ 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"; + const passageId = (userData as any)?.data?.passageId || "1"; const passage = Passages.find((p) => p.id === passageId) || Passages[0]; const words = passage.text.split(" "); @@ -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/pages/audience/step2/Step2Rank.tsx b/src/pages/audience/step2/Step2Rank.tsx index 4a69804..f62402e 100644 --- a/src/pages/audience/step2/Step2Rank.tsx +++ b/src/pages/audience/step2/Step2Rank.tsx @@ -6,10 +6,42 @@ 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); +// }, +// addRankSurvey: (_rankingData: RankingData) => { +// console.log("[Standalone Mode] addRankSurvey called:", _rankingData); +// }, +// }; + +// Fallback statements for standalone testing or when API fails +const fallbackStatements = [ + "Statement A", + "Statement B", + "Statement C", + "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); @@ -18,19 +50,64 @@ const AudienceRanking = () => { throw new Error("Component must be used within a DataContext.Provider"); } - const { userData, addRoleSpecificData } = context; + 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"]; 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(); + // 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 }[]; + + // 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); + 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 +199,7 @@ const AudienceRanking = () => { ), question: `Poem ${i + 1}`, - options: artistStatements, + options: statementOptions, required: true, }, { @@ -177,13 +254,73 @@ 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()], }); navigate("/audience/ai"); }; + if (isLoading) { + return ( + +
+

Loading...

+
+
+ ); + } + return (