All files / src/utils/components/submission-review SubmissionGraphAnswer.jsx

4.44% Statements 2/45
0% Branches 0/13
0% Functions 0/13
5.12% Lines 2/39

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129                                    8x                                                                                                                                                             8x                                                              
import React, { useCallback, useEffect, useMemo, useRef } from "react";
import { Box, Divider, Typography } from "@mui/material";
import { DrawingGraph } from "graph2d-react";
import {
    normalizeGraphAnswerForImport,
    parseGraphAxisBounds,
} from "@/utils/components/question-bank/question-display/graph/graphAnswerUtils";
import {
    GRAPH2D_QUESTION_BANK_CANVAS_SIZE,
    drawingGraphFrameSx,
} from "@/utils/components/question-bank/question-display/graph/graphConstants";
import { useSubmissionGraphSlot } from "./SubmissionGraphContext";
 
/**
 * Read-only Graph2D canvas for submission review. graphId + instance come from
 * SubmissionGraphProvider so Graph and Graph-Multiple-Select cards on the same
 * page do not share `embed-0`.
 */
const SubmissionGraphAnswer = ({ content }) => {
    const { graphInstance, graphId } = useSubmissionGraphSlot();
    const readyRef = useRef(false);
    const rafRef = useRef([]);
    const encoded = normalizeGraphAnswerForImport(content);
    const bounds = useMemo(() => parseGraphAxisBounds(content), [content]);
 
    const cancelRafs = useCallback(() => {
        rafRef.current.forEach((id) => cancelAnimationFrame(id));
        rafRef.current = [];
    }, []);
 
    const applyCanvas = useCallback(() => {
        if (!readyRef.current || !encoded || !graphInstance || graphId === undefined) return false;
        try {
            graphInstance.importAnswer(encoded, graphId);
            return true;
        } catch (err) {
            console.warn(`Failed to import graph review canvas for graphId ${graphId}:`, err);
            return false;
        }
    }, [encoded, graphId, graphInstance]);
 
    const importWhenReady = useCallback(() => {
        cancelRafs();
        applyCanvas();
        const outer = requestAnimationFrame(() => {
            applyCanvas();
            const inner = requestAnimationFrame(() => {
                applyCanvas();
            });
            rafRef.current.push(inner);
        });
        rafRef.current.push(outer);
    }, [applyCanvas, cancelRafs]);
 
    const handleGraphReady = useCallback(() => {
        readyRef.current = true;
        importWhenReady();
    }, [importWhenReady]);
 
    useEffect(() => {
        if (!readyRef.current) return undefined;
        importWhenReady();
        return cancelRafs;
    }, [bounds.xMax, bounds.xMin, bounds.yMax, bounds.yMin, cancelRafs, encoded, importWhenReady]);
 
    useEffect(() => () => cancelRafs(), [cancelRafs]);
 
    if (!encoded) {
        return (
            <Typography variant="body2" color="text.secondary" fontStyle="italic" sx={{ py: 1 }}>
                No graph answer submitted.
            </Typography>
        );
    }
 
    const size = GRAPH2D_QUESTION_BANK_CANVAS_SIZE;
 
    return (
        <Box sx={drawingGraphFrameSx}>
            <DrawingGraph
                key={`answer-drawing-${graphId}-${bounds.xMin}-${bounds.xMax}-${bounds.yMin}-${bounds.yMax}`}
                graphId={graphId}
                width={size}
                height={size}
                instance={graphInstance}
                steps={1}
                readOnly
                xMin={bounds.xMin}
                xMax={bounds.xMax}
                yMin={bounds.yMin}
                yMax={bounds.yMax}
                onGraphReady={handleGraphReady}
            />
        </Box>
    );
};
 
export const SubmissionGraphReview = ({ studentContent, correctContent }) => (
    <Box sx={{ mt: 1.5, display: "flex", flexDirection: "column", gap: 1.5 }}>
        <Box>
            <Typography
                variant="caption"
                color="text.secondary"
                sx={{ fontWeight: 600, display: "block", mb: 0.5 }}
            >
                Student&apos;s Answer
            </Typography>
            <SubmissionGraphAnswer content={studentContent} />
        </Box>
        {correctContent != null && correctContent !== "" && (
            <>
                <Divider />
                <Box>
                    <Typography
                        variant="caption"
                        color="text.secondary"
                        sx={{ fontWeight: 600, display: "block", mb: 0.5 }}
                    >
                        Correct Answer
                    </Typography>
                    <SubmissionGraphAnswer content={correctContent} />
                </Box>
            </>
        )}
    </Box>
);
 
export default SubmissionGraphAnswer;