From 243f38dbbc0968dabfad085b6ace3ae4b55fa3b6 Mon Sep 17 00:00:00 2001 From: Ccris Date: Mon, 10 Aug 2026 21:24:31 -0400 Subject: [PATCH 1/6] Added C++ library include tab --- v2/src/pages/MissionWorkspace.tsx | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/v2/src/pages/MissionWorkspace.tsx b/v2/src/pages/MissionWorkspace.tsx index 4940f61..734b955 100644 --- a/v2/src/pages/MissionWorkspace.tsx +++ b/v2/src/pages/MissionWorkspace.tsx @@ -70,6 +70,7 @@ const MIN_RESULTS_WIDTH = 290; const MIN_EDITOR_WIDTH = 420; const RESIZER_WIDTH = 7; const DEFAULT_RESULTS_WIDTH = 320; +let cppIncludeFoldingRegistered = false; function readResultsWidth() { const stored = Number(window.localStorage.getItem(RESULTS_WIDTH_KEY)); @@ -88,6 +89,44 @@ function executionFingerprint( } const configureMonaco: BeforeMount = (monaco) => { + if (!cppIncludeFoldingRegistered) { + monaco.languages.registerFoldingRangeProvider("cpp", { + provideFoldingRanges(model) { + let includeStart = 0; + let includeEnd = 0; + + for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber += 1) { + const line = model.getLineContent(lineNumber); + + if (/^\s*#include\b/.test(line)) { + includeStart ||= lineNumber; + includeEnd = lineNumber; + continue; + } + + if (includeStart && line.trim() === "") { + break; + } + + if (line.trim() !== "") { + break; + } + } + + return includeStart && includeEnd > includeStart + ? [ + { + start: includeStart, + end: includeEnd, + kind: monaco.languages.FoldingRangeKind.Imports, + }, + ] + : []; + }, + }); + cppIncludeFoldingRegistered = true; + } + monaco.editor.defineTheme("tomatin-terminal", { base: "vs-dark", inherit: true, @@ -1216,6 +1255,9 @@ export function Component() { tabSize: language === "python" ? 4 : 2, wordWrap: "off", scrollBeyondLastColumn: 5, + folding: true, + foldingImportsByDefault: language === "cpp", + showFoldingControls: "always", scrollbar: { horizontal: "auto", vertical: "auto", From 0f24dba2a8434eaa89b5e48ad6a2d72b102a9e78 Mon Sep 17 00:00:00 2001 From: Ccris Date: Tue, 11 Aug 2026 19:24:56 -0400 Subject: [PATCH 2/6] Corregida UI mobil MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reposiciona '.workbench-panes' en la página de misiones para que '.brief-pane' vuelva a obtener los gestos --- v2/e2e/mobile.spec.ts | 9 +++++++++ v2/src/styles.css | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/v2/e2e/mobile.spec.ts b/v2/e2e/mobile.spec.ts index 5ce1f31..d4a8a78 100644 --- a/v2/e2e/mobile.spec.ts +++ b/v2/e2e/mobile.spec.ts @@ -16,6 +16,15 @@ test("mobile navigation and workspace remain inside the viewport", async ({ .locator('.mission-card:has(h2:text-is("La once de Tomatin"))') .getByRole("link", { name: "Abrir workspace" }) .click(); + await page.getByRole("tab", { name: "Pistas" }).click(); + await expect(page.getByRole("heading", { name: "Pistas progresivas" })).toBeVisible(); + await page.getByRole("tab", { name: "Misión" }).click(); + await page.locator(".brief-scroll").evaluate((element) => { + element.scrollTop = element.scrollHeight; + }); + await expect + .poll(() => page.locator(".brief-scroll").evaluate((element) => element.scrollTop)) + .toBeGreaterThan(0); await page.getByRole("tab", { name: "Código" }).click(); await expect(page.getByRole("button", { name: "Ejecutar" })).toBeVisible(); await expect(page.getByRole("button", { name: "Entregar" })).toBeVisible(); diff --git a/v2/src/styles.css b/v2/src/styles.css index 6cd2562..038350a 100644 --- a/v2/src/styles.css +++ b/v2/src/styles.css @@ -5346,6 +5346,10 @@ fieldset legend { .code-pane.mobile-visible { display: grid; } + + .workspace-layout:has(> .brief-pane.mobile-visible) > .workbench-panes { + display: none; + } } @media (max-width: 760px) { From 25f222cccd4b2d67f8bbbd48c96b07242248109c Mon Sep 17 00:00:00 2001 From: Ccris Date: Tue, 11 Aug 2026 21:50:35 -0400 Subject: [PATCH 3/6] =?UTF-8?q?A=C3=B1adir=20ejecucion=20manual=20de=20est?= =?UTF-8?q?udiante?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Añade nuevo botón "Test manual" --- v2/src/data/missions-public.generated.ts | 6 +- v2/src/pages/MissionWorkspace.tsx | 161 ++++++++++++++++++++- v2/src/services/runner.ts | 120 +++++++++++++++ v2/src/styles.css | 132 +++++++++++++++++ v2/src/types.ts | 12 ++ v2/src/workers/javascript-runner.worker.ts | 25 +++- v2/src/workers/python-runner.worker.ts | 57 +++++--- 7 files changed, 485 insertions(+), 28 deletions(-) diff --git a/v2/src/data/missions-public.generated.ts b/v2/src/data/missions-public.generated.ts index 48f1919..2542333 100644 --- a/v2/src/data/missions-public.generated.ts +++ b/v2/src/data/missions-public.generated.ts @@ -1046,7 +1046,7 @@ export const publicMissions = [ "javascript": { "language": "javascript", "starterCode": "function stockSeguro(stock, retiro) {\n const retiroInvalido = false; // TODO: valida ambos casos inválidos.\n if (retiroInvalido) return -1;\n return stock; // TODO: aplica el retiro.\n}\n\nfunction necesitaReposicion(stock, minimo) {\n return false; // TODO: compara el stock con el mínimo.\n}\n\n// DATOS DE EJEMPLO (el evaluador usará también otros valores):\n// stockSeguro(12, 5)\n// Resultado esperado: 7\n", - "expectedSignature": "function stockSeguro(stock, retiro)", + "expectedSignature": "function stockSeguro(stock, retiro)\nfunction necesitaReposicion(stock, minimo)", "examples": [ { "id": "stock-update", @@ -1086,7 +1086,7 @@ export const publicMissions = [ "python": { "language": "python", "starterCode": "def stock_seguro(stock, retiro):\n retiro_invalido = False # TODO: valida ambos casos inválidos.\n if retiro_invalido:\n return -1\n return stock # TODO: aplica el retiro.\n\ndef necesita_reposicion(stock, minimo):\n return False # TODO: compara el stock con el mínimo.\n\n# DATOS DE EJEMPLO (el evaluador usará también otros valores):\n# stock_seguro(12, 5)\n# Resultado esperado: 7\n", - "expectedSignature": "def stock_seguro(stock, retiro)", + "expectedSignature": "def stock_seguro(stock, retiro)\ndef necesita_reposicion(stock, minimo)", "examples": [ { "id": "stock-update", @@ -1126,7 +1126,7 @@ export const publicMissions = [ "cpp": { "language": "cpp", "starterCode": "#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nusing namespace std;\n\nint stockSeguro(int stock, int retiro) {\n bool retiroInvalido = false; // TODO: valida ambos casos inválidos.\n if (retiroInvalido) return -1;\n return stock; // TODO: aplica el retiro.\n}\n\nbool necesitaReposicion(int stock, int minimo) {\n return false; // TODO: compara el stock con el mínimo.\n}\n\n// DATOS DE EJEMPLO (el evaluador usará también otros valores):\n// stockSeguro(12, 5) == 7\n// Resultado esperado: 7\n", - "expectedSignature": "int stockSeguro(int stock, int retiro)", + "expectedSignature": "int stockSeguro(int stock, int retiro)\nbool necesitaReposicion(int stock, int minimo)", "examples": [ { "id": "stock-update", diff --git a/v2/src/pages/MissionWorkspace.tsx b/v2/src/pages/MissionWorkspace.tsx index 734b955..4419b47 100644 --- a/v2/src/pages/MissionWorkspace.tsx +++ b/v2/src/pages/MissionWorkspace.tsx @@ -43,7 +43,7 @@ import { loadDraft, saveDraft, } from "@/services/draft-store"; -import { runMissionCode } from "@/services/runner"; +import { runManualCode, runMissionCode } from "@/services/runner"; import { runMissionAdmin, type MissionSolution, @@ -56,6 +56,7 @@ import { type Attempt, type AttemptKind, type Language, + type ManualRunResult, type RunResult, } from "@/types"; @@ -64,6 +65,7 @@ const MonacoEditor = lazy(() => import("@monaco-editor/react")); type MobilePane = "brief" | "code" | "results"; type BriefTab = "problem" | "hints" | "history" | "solution"; type SaveState = "loading" | "saving" | "synced" | "local" | "error"; +type ManualRunState = "idle" | "running"; const RESULTS_WIDTH_KEY = "tomatin.v3.workspace-results-width"; const MIN_RESULTS_WIDTH = 290; @@ -368,6 +370,10 @@ export function Component() { const [saveState, setSaveState] = useState("loading"); const [saveMessage, setSaveMessage] = useState(""); const [result, setResult] = useState(null); + const [manualOpen, setManualOpen] = useState(false); + const [manualExpression, setManualExpression] = useState(""); + const [manualResult, setManualResult] = useState(null); + const [manualRunning, setManualRunning] = useState("idle"); const [running, setRunning] = useState(null); const [briefTab, setBriefTab] = useState( linkedAttemptId || linkedReviewId ? "history" : "problem", @@ -675,6 +681,10 @@ export function Component() { testCase.actualExpression ?? testCase.expression, ]), ); + const manualSignatureHints = activeMission.variants[language].expectedSignature + .split("\n") + .map((entry) => entry.trim()) + .filter(Boolean); async function execute(kind: AttemptKind) { if ( @@ -730,6 +740,19 @@ export function Component() { setRunning(null); } + async function executeManualTest() { + if (language === "cpp" || manualRunning === "running") return; + setManualRunning("running"); + setManualResult(null); + const manual = await runManualCode({ + language, + code: currentCode, + expression: manualExpression, + }); + setManualResult(manual); + setManualRunning("idle"); + } + function clampResultsWidth(nextWidth: number) { const workbenchWidth = workbenchRef.current?.clientWidth ?? 0; const maxWidth = Math.max( @@ -1278,6 +1301,23 @@ export function Component() { ? "Pyodide Worker" : "Web Worker"} + + + + {manualSignatureHints.length > 0 ? ( +

+ Funciones evaluadas en este ejercicio:{" "} + {manualSignatureHints.map((signature, index) => ( + + {index > 0 ? ", " : ""} + {signature} + + ))} +

+ ) : null} +
+ + +
+ {manualResult ? ( +
+ + {manualResult.status === "passed" + ? "Resultado" + : "No se pudo ejecutar"} + +
+
+
Expresión
+
+ {manualResult.expression} +
+
+ {manualResult.value !== undefined ? ( +
+
Valor
+
+ {manualResult.value} +
+
+ ) : null} +
+ {manualResult.stdout || manualResult.stderr ? ( +
+                        {manualResult.stdout ? `$ ${manualResult.stdout}\n` : ""}
+                        {manualResult.stderr
+                          ? `[stderr]\n${manualResult.stderr}`
+                          : ""}
+                      
+ ) : null} +
+ ) : null} + + + ) : null} + - - - {manualSignatureHints.length > 0 ? ( -

- Funciones evaluadas en este ejercicio:{" "} - {manualSignatureHints.map((signature, index) => ( - - {index > 0 ? ", " : ""} - {signature} - - ))} -

- ) : null} -
- - -
- {manualResult ? ( -
- - {manualResult.status === "passed" - ? "Resultado" - : "No se pudo ejecutar"} - -
-
-
Expresión
-
- {manualResult.expression} -
-
- {manualResult.value !== undefined ? ( -
-
Valor
-
- {manualResult.value} -
-
- ) : null} -
- {manualResult.stdout || manualResult.stderr ? ( -
-                        {manualResult.stdout ? `$ ${manualResult.stdout}\n` : ""}
-                        {manualResult.stderr
-                          ? `[stderr]\n${manualResult.stderr}`
-                          : ""}
-                      
- ) : null} -
- ) : null} - - - ) : null} - + + + {manualSignatureHints.length > 0 ? ( +

+ Funciones evaluadas en este ejercicio:{" "} + {manualSignatureHints.map((signature, index) => ( + + {index > 0 ? ", " : ""} + {signature} + + ))} +

+ ) : null} +
+ + +
+ {manualResult ? ( +
+ + {manualResult.status === "passed" + ? "Resultado" + : "No se pudo ejecutar"} + +
+
+
Expresión
+
+ {manualResult.expression} +
+
+ {manualResult.value !== undefined ? ( +
+
Valor
+
+ {manualResult.value} +
+
+ ) : null} +
+ {manualResult.stdout || manualResult.stderr ? ( +
+                        {manualResult.stdout ? `$ ${manualResult.stdout}\n` : ""}
+                        {manualResult.stderr
+                          ? `[stderr]\n${manualResult.stderr}`
+                          : ""}
+                      
+ ) : null} +
+ ) : null} + + + ) : null} +