From 13108471b6b7cecab85e0f37f3be20e92f18174f Mon Sep 17 00:00:00 2001 From: Mircea Filip Lungu Date: Sat, 14 Feb 2026 14:42:50 +0100 Subject: [PATCH 1/5] Add report exercise issue feature (frontend) - Add reportExerciseIssue API method - Create ReportExerciseDialog with contextual chips: - During exercise: Word not shown, Context confusing, Other - After exercise: Wrong translation, Context wrong, Other - Add subtle flag icon to SolutionFeedbackLinks (always visible) - Add ReportButton and ReportedBadge styled components Co-Authored-By: Claude Opus 4.5 --- src/api/exercises.js | 38 +++++ src/exercises/exerciseTypes/Exercise.sc.js | 24 +++ .../exerciseTypes/ReportExerciseDialog.js | 157 ++++++++++++++++++ .../exerciseTypes/SolutionFeedbackLinks.js | 41 +++++ 4 files changed, 260 insertions(+) create mode 100644 src/exercises/exerciseTypes/ReportExerciseDialog.js diff --git a/src/api/exercises.js b/src/api/exercises.js index 61901d9e7..f7458848f 100644 --- a/src/api/exercises.js +++ b/src/api/exercises.js @@ -143,3 +143,41 @@ Zeeguu_API.prototype.getSmallerContext = function (contextBookmark, wordBookmark }; return this._post(`/get_smaller_context`, qs.stringify(payload), callback); }; + +Zeeguu_API.prototype.reportExerciseIssue = function ( + bookmarkId, + exerciseSource, + reason, + comment, + contextUsed, + callback, + errorCallback +) { + const payload = JSON.stringify({ + bookmark_id: bookmarkId, + exercise_source: exerciseSource, + reason, + comment: comment || null, + context_used: contextUsed || null + }); + + fetch(this._appendSessionToUrl("report_exercise_issue"), { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: payload + }) + .then(response => response.json().then(data => ({ status: response.status, data }))) + .then(({ status, data }) => { + if (status >= 200 && status < 300 && data.success) { + if (callback) callback(data); + } else { + if (errorCallback) errorCallback(data); + } + }) + .catch(error => { + console.error("Error reporting exercise issue:", error); + if (errorCallback) errorCallback({ error: "Network error" }); + }); +}; diff --git a/src/exercises/exerciseTypes/Exercise.sc.js b/src/exercises/exerciseTypes/Exercise.sc.js index 8a92c0d16..2e7a9a83f 100644 --- a/src/exercises/exerciseTypes/Exercise.sc.js +++ b/src/exercises/exerciseTypes/Exercise.sc.js @@ -511,6 +511,28 @@ let MultipleChoiceContext = styled.div` } `; +let ReportButton = styled.button` + background: none; + border: none; + padding: 0.25rem; + cursor: pointer; + color: #ccc; + display: flex; + align-items: center; + + &:hover { + color: #999; + } +`; + +let ReportedBadge = styled.span` + color: #999; + font-size: 0.8rem; + display: flex; + align-items: center; + gap: 0.25rem; +`; + export { Exercise, FeedbackButton, @@ -540,6 +562,8 @@ export { EditSpeakButtonHolder, OrangeButtonMessage, MultipleChoiceContext, + ReportButton, + ReportedBadge, }; export default StyledButton; diff --git a/src/exercises/exerciseTypes/ReportExerciseDialog.js b/src/exercises/exerciseTypes/ReportExerciseDialog.js new file mode 100644 index 000000000..122efe97b --- /dev/null +++ b/src/exercises/exerciseTypes/ReportExerciseDialog.js @@ -0,0 +1,157 @@ +import { useState, useContext } from "react"; +import Dialog from "@mui/material/Dialog"; +import DialogContent from "@mui/material/DialogContent"; +import DialogTitle from "@mui/material/DialogTitle"; +import Chip from "@mui/material/Chip"; +import TextField from "@mui/material/TextField"; +import IconButton from "@mui/material/IconButton"; +import CloseIcon from "@mui/icons-material/Close"; +import SendIcon from "@mui/icons-material/Send"; +import { toast } from "react-toastify"; +import { APIContext } from "../../contexts/APIContext"; + +// Chips shown during exercise (before answering) +const DURING_EXERCISE_OPTIONS = [ + { value: "word_not_shown", label: "Word not shown" }, + { value: "context_confusing", label: "Context confusing" }, + { value: "other", label: "Other..." }, +]; + +// Chips shown after exercise (after answering) +const AFTER_EXERCISE_OPTIONS = [ + { value: "wrong_translation", label: "Wrong translation" }, + { value: "context_wrong", label: "Context wrong" }, + { value: "other", label: "Other..." }, +]; + +export default function ReportExerciseDialog({ + open, + onClose, + bookmarkId, + exerciseSource, + isExerciseOver, + contextUsed, +}) { + const api = useContext(APIContext); + const [selectedReason, setSelectedReason] = useState(null); + const [comment, setComment] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + const options = isExerciseOver ? AFTER_EXERCISE_OPTIONS : DURING_EXERCISE_OPTIONS; + + function handleChipClick(reason) { + if (reason === "other") { + setSelectedReason("other"); + } else { + // Submit immediately for non-other options + submitReport(reason, null); + } + } + + function handleSubmitOther() { + if (!comment.trim()) { + toast.error("Please describe the issue"); + return; + } + submitReport("other", comment.trim()); + } + + function submitReport(reason, commentText) { + setIsSubmitting(true); + + api.reportExerciseIssue( + bookmarkId, + exerciseSource, + reason, + commentText, + contextUsed, + (response) => { + setIsSubmitting(false); + if (response.already_reported) { + toast.info("You've already reported this exercise"); + } else { + toast.success("Thanks for the feedback!"); + } + handleClose(true); + }, + (error) => { + setIsSubmitting(false); + toast.error("Failed to submit report"); + } + ); + } + + function handleClose(reported = false) { + setSelectedReason(null); + setComment(""); + onClose(reported); + } + + return ( + handleClose(false)} maxWidth="xs" fullWidth> + + What's wrong? + handleClose(false)} + sx={{ + position: "absolute", + right: 8, + top: 8, + color: (theme) => theme.palette.grey[500], + }} + > + + + + + {selectedReason !== "other" ? ( +
+ {options.map((option) => ( + handleChipClick(option.value)} + variant="outlined" + clickable + disabled={isSubmitting} + sx={{ + borderColor: "#ccc", + "&:hover": { + borderColor: "#999", + backgroundColor: "rgba(0,0,0,0.04)", + }, + }} + /> + ))} +
+ ) : ( +
+ setComment(e.target.value)} + disabled={isSubmitting} + onKeyDown={(e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmitOther(); + } + }} + /> + + + +
+ )} +
+
+ ); +} diff --git a/src/exercises/exerciseTypes/SolutionFeedbackLinks.js b/src/exercises/exerciseTypes/SolutionFeedbackLinks.js index e936f3c9f..1f8976074 100644 --- a/src/exercises/exerciseTypes/SolutionFeedbackLinks.js +++ b/src/exercises/exerciseTypes/SolutionFeedbackLinks.js @@ -1,3 +1,4 @@ +import { useState } from "react"; import strings from "../../i18n/definitions"; import * as s from "./Exercise.sc"; import { toast } from "react-toastify"; @@ -5,6 +6,8 @@ import useScreenWidth from "../../hooks/useScreenWidth"; import DisableAudioSession from "./DisableAudioSession"; import { EXERCISE_TYPES } from "../ExerciseTypeConstants"; import SessionStorage from "../../assorted/SessionStorage"; +import FlagOutlinedIcon from "@mui/icons-material/FlagOutlined"; +import ReportExerciseDialog from "./ReportExerciseDialog"; export default function SolutionFeedbackLinks({ exerciseBookmarks, @@ -18,6 +21,8 @@ export default function SolutionFeedbackLinks({ onWordRemovedFromExercises, }) { const { isMobile } = useScreenWidth(); + const [reportDialogOpen, setReportDialogOpen] = useState(false); + const [isReported, setIsReported] = useState(false); function handleIKnowThisWord() { if (onWordRemovedFromExercises && exerciseBookmark) { @@ -26,6 +31,18 @@ export default function SolutionFeedbackLinks({ } } + function handleReportClose(reported) { + setReportDialogOpen(false); + if (reported) { + setIsReported(true); + } + } + + // Get context as string for the report + const contextUsed = exerciseBookmark?.context_tokenized + ? exerciseBookmark.context_tokenized.map(t => t.text || t).join("") + : exerciseBookmark?.context || ""; + return ( <> )} + + {/* Report button - always visible */} + {isReported ? ( + + + Reported + + ) : ( + setReportDialogOpen(true)} + title="Report issue with this exercise" + > + + + )} {isExerciseOver && ( @@ -73,6 +105,15 @@ export default function SolutionFeedbackLinks({ )} )} + + ); } From 190ce50af83a4946abdb108486ec18194263162e Mon Sep 17 00:00:00 2001 From: Mircea Filip Lungu Date: Sat, 14 Feb 2026 15:01:23 +0100 Subject: [PATCH 2/5] Fix ReportExerciseDialog styling for "Other" option - Use placeholder instead of floating label - Style send button with orange background when active - Better alignment and spacing Co-Authored-By: Claude Opus 4.5 --- src/exercises/ExerciseSession.js | 2 ++ .../exerciseTypes/ReportExerciseDialog.js | 21 ++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/exercises/ExerciseSession.js b/src/exercises/ExerciseSession.js index 4898a98d9..ae51e603c 100644 --- a/src/exercises/ExerciseSession.js +++ b/src/exercises/ExerciseSession.js @@ -27,6 +27,8 @@ import isEmptyDictionary from "../utils/misc/isEmptyDictionary"; import WordProgressBar from "./progressBars/WordProgressBar"; import { getExerciseTypeName } from "./exerciseTypes/exerciseTypeNames"; import { useFeedbackContext } from "../contexts/FeedbackContext"; +import FlagOutlinedIcon from "@mui/icons-material/FlagOutlined"; +import ReportExerciseDialog from "./exerciseTypes/ReportExerciseDialog"; const BOOKMARKS_DUE_REVIEW = false; diff --git a/src/exercises/exerciseTypes/ReportExerciseDialog.js b/src/exercises/exerciseTypes/ReportExerciseDialog.js index 122efe97b..3d6207e0c 100644 --- a/src/exercises/exerciseTypes/ReportExerciseDialog.js +++ b/src/exercises/exerciseTypes/ReportExerciseDialog.js @@ -126,12 +126,12 @@ export default function ReportExerciseDialog({ ))} ) : ( -
+
setComment(e.target.value)} disabled={isSubmitting} @@ -141,11 +141,26 @@ export default function ReportExerciseDialog({ handleSubmitOther(); } }} + sx={{ + "& .MuiOutlinedInput-root": { + borderRadius: "8px", + }, + }} /> From 69abd682f412601a9b5f9f0df12433d3b01fd648 Mon Sep 17 00:00:00 2001 From: Mircea Filip Lungu Date: Sat, 14 Feb 2026 15:04:53 +0100 Subject: [PATCH 3/5] Move report flag to top bar, make topbar thinner - Move flag icon from SolutionFeedbackLinks to ExerciseSession top bar - Replace exercise type name (was dev-only) with report flag for all users - Make top bar thinner with reduced padding - Update dialog title to "What's wrong with the exercise?" - Reset report state when moving to next exercise Co-Authored-By: Claude Opus 4.5 --- src/exercises/ExerciseSession.js | 45 ++++++++++++++++--- src/exercises/Exercises.sc.js | 2 + .../exerciseTypes/ReportExerciseDialog.js | 2 +- .../exerciseTypes/SolutionFeedbackLinks.js | 41 ----------------- 4 files changed, 41 insertions(+), 49 deletions(-) diff --git a/src/exercises/ExerciseSession.js b/src/exercises/ExerciseSession.js index ae51e603c..e94fd538b 100644 --- a/src/exercises/ExerciseSession.js +++ b/src/exercises/ExerciseSession.js @@ -66,6 +66,8 @@ export default function ExerciseSession({ articleID, backButtonAction, toSchedul const [totalPracticedBookmarksInSession, setTotalPracticedBookmarksInSession] = useState(0); const [exerciseMessageForAPI, setExerciseMessageForAPI] = useState({}); const [isSessionLoading, setIsSessionLoading] = useState(true); // New loading state + const [reportDialogOpen, setReportDialogOpen] = useState(false); + const [isReported, setIsReported] = useState(false); const currentIndexRef = useShadowRef(currentIndex); const hasKeptExercisingRef = useShadowRef(hasKeptExercising); @@ -264,6 +266,7 @@ export default function ExerciseSession({ articleID, backButtonAction, toSchedul setExerciseMessageForAPI({}); setIsCorrect(null); setShowFeedbackButtons(false); + setIsReported(false); // Reset report state for new exercise const newIndex = currentIndex + 1; if (newIndex === fullExerciseProgression.length) { @@ -427,18 +430,30 @@ export default function ExerciseSession({ articleID, backButtonAction, toSchedul
- {userDetails?.name === "Mircea" && ( -
+ + Reported +
+ ) : ( +
+ + )}
@@ -514,6 +529,22 @@ export default function ExerciseSession({ articleID, backButtonAction, toSchedul

)} + + { + setReportDialogOpen(false); + if (reported) setIsReported(true); + }} + bookmarkId={selectedExerciseBookmark?.id || currentBookmarksToStudy?.[0]?.id} + exerciseSource={getExerciseTypeName(currentExerciseType)} + isExerciseOver={isExerciseOver} + contextUsed={ + selectedExerciseBookmark?.context_tokenized + ? selectedExerciseBookmark.context_tokenized.map(t => t.text || t).join("") + : selectedExerciseBookmark?.context || currentBookmarksToStudy?.[0]?.context || "" + } + /> ); } diff --git a/src/exercises/Exercises.sc.js b/src/exercises/Exercises.sc.js index 2dc0ab483..5afcd5ad9 100644 --- a/src/exercises/Exercises.sc.js +++ b/src/exercises/Exercises.sc.js @@ -17,6 +17,7 @@ const ExercisesColumn = styled.div` #exerciseTopbar { width: 100%; + padding: 0.15rem 0; } #topbarRow { @@ -25,6 +26,7 @@ const ExercisesColumn = styled.div` flex-direction: row; justify-content: space-between; margin-bottom: 0; + min-height: 1.25rem; } `; diff --git a/src/exercises/exerciseTypes/ReportExerciseDialog.js b/src/exercises/exerciseTypes/ReportExerciseDialog.js index 3d6207e0c..c48c5be1b 100644 --- a/src/exercises/exerciseTypes/ReportExerciseDialog.js +++ b/src/exercises/exerciseTypes/ReportExerciseDialog.js @@ -90,7 +90,7 @@ export default function ReportExerciseDialog({ return ( handleClose(false)} maxWidth="xs" fullWidth> - What's wrong? + What's wrong with the exercise? handleClose(false)} diff --git a/src/exercises/exerciseTypes/SolutionFeedbackLinks.js b/src/exercises/exerciseTypes/SolutionFeedbackLinks.js index 1f8976074..e936f3c9f 100644 --- a/src/exercises/exerciseTypes/SolutionFeedbackLinks.js +++ b/src/exercises/exerciseTypes/SolutionFeedbackLinks.js @@ -1,4 +1,3 @@ -import { useState } from "react"; import strings from "../../i18n/definitions"; import * as s from "./Exercise.sc"; import { toast } from "react-toastify"; @@ -6,8 +5,6 @@ import useScreenWidth from "../../hooks/useScreenWidth"; import DisableAudioSession from "./DisableAudioSession"; import { EXERCISE_TYPES } from "../ExerciseTypeConstants"; import SessionStorage from "../../assorted/SessionStorage"; -import FlagOutlinedIcon from "@mui/icons-material/FlagOutlined"; -import ReportExerciseDialog from "./ReportExerciseDialog"; export default function SolutionFeedbackLinks({ exerciseBookmarks, @@ -21,8 +18,6 @@ export default function SolutionFeedbackLinks({ onWordRemovedFromExercises, }) { const { isMobile } = useScreenWidth(); - const [reportDialogOpen, setReportDialogOpen] = useState(false); - const [isReported, setIsReported] = useState(false); function handleIKnowThisWord() { if (onWordRemovedFromExercises && exerciseBookmark) { @@ -31,18 +26,6 @@ export default function SolutionFeedbackLinks({ } } - function handleReportClose(reported) { - setReportDialogOpen(false); - if (reported) { - setIsReported(true); - } - } - - // Get context as string for the report - const contextUsed = exerciseBookmark?.context_tokenized - ? exerciseBookmark.context_tokenized.map(t => t.text || t).join("") - : exerciseBookmark?.context || ""; - return ( <> )} - - {/* Report button - always visible */} - {isReported ? ( - - - Reported - - ) : ( - setReportDialogOpen(true)} - title="Report issue with this exercise" - > - - - )} {isExerciseOver && ( @@ -105,15 +73,6 @@ export default function SolutionFeedbackLinks({ )} )} - - ); } From 5feed3338fdb692de707a501e250a32c94760dee Mon Sep 17 00:00:00 2001 From: Mircea Filip Lungu Date: Sat, 14 Feb 2026 15:06:13 +0100 Subject: [PATCH 4/5] Change dialog title to statement form --- src/exercises/exerciseTypes/ReportExerciseDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exercises/exerciseTypes/ReportExerciseDialog.js b/src/exercises/exerciseTypes/ReportExerciseDialog.js index c48c5be1b..556d5720a 100644 --- a/src/exercises/exerciseTypes/ReportExerciseDialog.js +++ b/src/exercises/exerciseTypes/ReportExerciseDialog.js @@ -90,7 +90,7 @@ export default function ReportExerciseDialog({ return ( handleClose(false)} maxWidth="xs" fullWidth> - What's wrong with the exercise? + Report a problem with the exercise handleClose(false)} From 4d44b50944f296964aec5526acd21f1d45e6537e Mon Sep 17 00:00:00 2001 From: Mircea Filip Lungu Date: Sun, 15 Feb 2026 05:12:53 +0200 Subject: [PATCH 5/5] Add wrong_highlighting option to report dialog --- src/exercises/exerciseTypes/ReportExerciseDialog.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/exercises/exerciseTypes/ReportExerciseDialog.js b/src/exercises/exerciseTypes/ReportExerciseDialog.js index 556d5720a..db8d77493 100644 --- a/src/exercises/exerciseTypes/ReportExerciseDialog.js +++ b/src/exercises/exerciseTypes/ReportExerciseDialog.js @@ -13,6 +13,7 @@ import { APIContext } from "../../contexts/APIContext"; // Chips shown during exercise (before answering) const DURING_EXERCISE_OPTIONS = [ { value: "word_not_shown", label: "Word not shown" }, + { value: "wrong_highlighting", label: "Wrong highlighting" }, { value: "context_confusing", label: "Context confusing" }, { value: "other", label: "Other..." }, ]; @@ -20,6 +21,7 @@ const DURING_EXERCISE_OPTIONS = [ // Chips shown after exercise (after answering) const AFTER_EXERCISE_OPTIONS = [ { value: "wrong_translation", label: "Wrong translation" }, + { value: "wrong_highlighting", label: "Wrong highlighting" }, { value: "context_wrong", label: "Context wrong" }, { value: "other", label: "Other..." }, ];