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 && (