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 */}
-
+
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 && (