From c7b1b7a1d74188c9779b3d8a43dfeffedba9d017 Mon Sep 17 00:00:00 2001 From: Cris Lucero Date: Wed, 30 Sep 2026 19:12:47 +0800 Subject: [PATCH] feat: add a Study Hub button to visualize and trace code Adds a 'Visualize and trace in Study Hub' link on the editor toolbar and on each visible test case. It opens the Study Hub tracer with the student's current code, and for a test case its input and expected output, so they can step through the run and see where it goes wrong. The data travels in the URL fragment, so it never reaches a server. Only what the student can already see is sent: hidden test cases get no link and solution code is never used. NEXT_PUBLIC_STUDYHUB_URL sets the Study Hub address (default https://studyhub.dcism.org). --- .env.example | 4 +- src/app/problem/[id]/page.tsx | 11 +++++ src/components/problem/code-editor.tsx | 39 ++++++++++++++--- src/components/problem/test-case-bar.tsx | 4 ++ src/components/problem/test-case.tsx | 54 +++++++++++++++++++++++- src/lib/studyhub.ts | 45 ++++++++++++++++++++ 6 files changed, 147 insertions(+), 10 deletions(-) create mode 100644 src/lib/studyhub.ts diff --git a/.env.example b/.env.example index c76051c..7bdc3ba 100644 --- a/.env.example +++ b/.env.example @@ -5,4 +5,6 @@ DB_PASSWORD= DB_NAME=ciscode_db PORT=3000 -JWT_SECRET=default_secret_key \ No newline at end of file +JWT_SECRET=default_secret_key +# Study Hub base URL (no trailing slash); defaults to https://studyhub.dcism.org +NEXT_PUBLIC_STUDYHUB_URL= diff --git a/src/app/problem/[id]/page.tsx b/src/app/problem/[id]/page.tsx index 2e55686..adeb9e3 100644 --- a/src/app/problem/[id]/page.tsx +++ b/src/app/problem/[id]/page.tsx @@ -15,6 +15,7 @@ import { TestCaseResponseType } from "@/dtos/testcase.dto"; import { runTestCase } from "@/lib/fetchers/code.fetchers"; import { fetchProblem } from "@/lib/fetchers/problem.fetchers"; import { submitCode } from "@/lib/fetchers/submission.fetchers"; +import { studyHubTraceUrl } from "@/lib/studyhub"; import { ProblemPageEnum } from "@/lib/types/enums/problempage.enum"; import SubmissionStatusEnum from "@/lib/types/enums/problemstatus.enum"; import TestCaseSubmissionStatusEnum from "@/lib/types/enums/submissionstatus.enum"; @@ -278,6 +279,11 @@ export default function Problem() { code={code} onCodeChange={handleCodeChange} handleReset={handleReset} + studyHubTraceUrl={studyHubTraceUrl({ + problemId: problem.id, + title: problem.title, + code, + })} /> } TestCases={ @@ -288,6 +294,11 @@ export default function Problem() { onCheckCode={handleCheckCode} submitted={submitted} sending={!code || sending} + studyHubTrace={{ + problemId: problem.id, + title: problem.title, + code, + }} /> } /> diff --git a/src/components/problem/code-editor.tsx b/src/components/problem/code-editor.tsx index 07ee454..09e40cd 100644 --- a/src/components/problem/code-editor.tsx +++ b/src/components/problem/code-editor.tsx @@ -5,7 +5,7 @@ import CodeMirror from "@uiw/react-codemirror"; import { useEditorSettings } from "@/hooks/use-editor-settings"; import ProblemCard from "./problem-card"; import EditorSettings from "./editor-settings"; -import { CodeXml, RefreshCcw } from "lucide-react"; +import { CodeXml, Footprints, RefreshCcw } from "lucide-react"; import { HoverCard, HoverCardContent, @@ -17,12 +17,14 @@ interface CodeEditorProps { code: string; onCodeChange?: (value: string | undefined) => void; handleReset?: () => void; + studyHubTraceUrl?: string; } const CodeEditor: React.FC = ({ code, onCodeChange, handleReset, + studyHubTraceUrl, }) => { const { settings, update_setting, get_extensions, reset_settings } = useEditorSettings(); @@ -41,12 +43,35 @@ const CodeEditor: React.FC = ({

main.c

- {/* editor settings */} - +
+ {studyHubTraceUrl && ( + + )} + + {/* editor settings */} + +
diff --git a/src/components/problem/test-case-bar.tsx b/src/components/problem/test-case-bar.tsx index af18d35..2209404 100644 --- a/src/components/problem/test-case-bar.tsx +++ b/src/components/problem/test-case-bar.tsx @@ -4,6 +4,7 @@ import { Button } from "../ui/button"; import TestCase from "./test-case"; import { TestCaseResponseType } from "@/dtos/testcase.dto"; import { CheckCodeResponseType } from "@/dtos/code.dto"; +import { StudyHubTraceContext } from "./test-case"; interface TestCaseBarProps { testCases: TestCaseResponseType[]; @@ -17,6 +18,7 @@ interface TestCaseBarProps { submitted?: boolean; sending: boolean; overrideHidden?: boolean; + studyHubTrace?: StudyHubTraceContext; } const TestCaseBar: React.FC = ({ @@ -27,6 +29,7 @@ const TestCaseBar: React.FC = ({ submitted, sending, overrideHidden = false, + studyHubTrace, }) => { return ( @@ -46,6 +49,7 @@ const TestCaseBar: React.FC = ({ submitted={submitted ?? false} sending={sending} overrideHidden={overrideHidden} + studyHubTrace={studyHubTrace} /> ))}
diff --git a/src/components/problem/test-case.tsx b/src/components/problem/test-case.tsx index f7669ad..222d362 100644 --- a/src/components/problem/test-case.tsx +++ b/src/components/problem/test-case.tsx @@ -2,7 +2,7 @@ import { CheckCodeResponseType } from "@/dtos/code.dto"; import { TestCaseResponseType } from "@/dtos/testcase.dto"; import TestCaseSubmissionStatusEnum from "@/lib/types/enums/submissionstatus.enum"; import { cn } from "@/lib/utils"; -import { Circle, CircleCheck, CircleX } from "lucide-react"; +import { Circle, CircleCheck, CircleX, Footprints } from "lucide-react"; import React, { useEffect, useState } from "react"; import { FaPlay } from "react-icons/fa"; import { ClipLoader } from "react-spinners"; @@ -11,6 +11,13 @@ import { Button } from "../ui/button"; import { getHighlightedDiff } from "../shared/diff"; import { DialogTitle } from "@radix-ui/react-dialog"; import Info from "../shared/info"; +import { studyHubTraceUrl } from "@/lib/studyhub"; + +export interface StudyHubTraceContext { + problemId: number; + title: string; + code: string; +} interface TestCaseProps { testCaseNumber: number; @@ -22,6 +29,7 @@ interface TestCaseProps { submitted: boolean; sending: boolean; overrideHidden?: boolean; + studyHubTrace?: StudyHubTraceContext; } const TestCase: React.FC = ({ @@ -32,6 +40,7 @@ const TestCase: React.FC = ({ submitted, sending, overrideHidden, + studyHubTrace, }) => { const [showDetails, setShowDetails] = useState(false); const [checking, setChecking] = useState(submitted); @@ -82,6 +91,17 @@ const TestCase: React.FC = ({ setChecking(false); }; + // Only visible test cases may be sent to the Study Hub, never hidden ones. + const traceUrl = + studyHubTrace && !testCase.hidden + ? studyHubTraceUrl({ + ...studyHubTrace, + input: testCase.input ?? "", + expected: testCase.output ?? "", + }) + : null; + const failed = testCase.status === TestCaseSubmissionStatusEnum.FAILED; + useEffect(() => { setChecking(submitted); }, [submitted]); @@ -101,6 +121,24 @@ const TestCase: React.FC = ({ + {traceUrl && ( + + + + )} + {checking ? ( = ({

)} - {testCase.status === TestCaseSubmissionStatusEnum.FAILED && ( + {failed && traceUrl && ( + + )} + {failed && (