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/ExerciseSession.js b/src/exercises/ExerciseSession.js index 4898a98d9..e94fd538b 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; @@ -64,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); @@ -262,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) { @@ -425,18 +430,30 @@ export default function ExerciseSession({ articleID, backButtonAction, toSchedul
- {userDetails?.name === "Mircea" && ( -
+ + Reported +
+ ) : ( +
+ + )}
@@ -512,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/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..db8d77493 --- /dev/null +++ b/src/exercises/exerciseTypes/ReportExerciseDialog.js @@ -0,0 +1,174 @@ +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: "wrong_highlighting", label: "Wrong highlighting" }, + { 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: "wrong_highlighting", label: "Wrong highlighting" }, + { 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> + + Report a problem with the exercise + 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(); + } + }} + sx={{ + "& .MuiOutlinedInput-root": { + borderRadius: "8px", + }, + }} + /> + + + +
+ )} +
+
+ ); +}