Skip to content

Commit ff17b6d

Browse files
authored
Merge pull request #5 from python-chile/feat-4-educational-python-errors
feat: add educational Python error help
2 parents bcc8a65 + db43267 commit ff17b6d

5 files changed

Lines changed: 292 additions & 13 deletions

File tree

‎src/components/station/editor/PythonEditor.astro‎

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const resolvedChallenge = challenge
3535
: undefined}
3636
>
3737
<div class="border-line shadow-editor overflow-hidden border bg-white">
38-
<div
38+
<header
3939
class="border-line-soft bg-panel flex items-center justify-between gap-4 border-b px-5 py-3.5"
4040
>
4141
<div class="flex items-center gap-3">
@@ -51,11 +51,11 @@ const resolvedChallenge = challenge
5151
role="status"
5252
aria-live="polite"
5353
>
54-
<span class="bg-brand size-1.5 rounded-full" aria-hidden="true"></span>
54+
<span class="bg-brand size-1.5 rounded-full" aria-hidden="true"> </span>
5555

56-
<span data-python-status-text>Preparando entorno…</span>
56+
<span data-python-status-text> Preparando entorno… </span>
5757
</div>
58-
</div>
58+
</header>
5959

6060
<div class="grid min-h-96 lg:h-128 lg:min-h-0" data-python-workspace>
6161
<div class="min-h-80 min-w-0 lg:h-full lg:min-h-0" data-python-code-panel>
@@ -68,7 +68,7 @@ const resolvedChallenge = challenge
6868
aria-label="Resultado de la ejecución"
6969
data-python-results
7070
>
71-
<div
71+
<header
7272
class="border-line-soft flex items-center gap-2 border-b bg-white px-5 py-3.5"
7373
>
7474
<span
@@ -81,7 +81,7 @@ const resolvedChallenge = challenge
8181
<span class="text-ink font-mono text-xs font-bold tracking-widest">
8282
SALIDA
8383
</span>
84-
</div>
84+
</header>
8585

8686
<div class="text-muted px-5 py-3 font-mono text-xs">
8787
$ python main.py
@@ -95,13 +95,13 @@ const resolvedChallenge = challenge
9595
</section>
9696
</div>
9797

98-
<div
98+
<footer
9999
class="border-line-soft bg-paper flex flex-wrap items-center gap-2 border-t px-4 py-4 sm:px-5"
100100
>
101101
<button
102102
type="button"
103103
disabled
104-
class="bg-accent text-ink shadow-action hover:bg-accent-dark inline-flex items-center gap-3 px-5 py-3 font-bold transition-colors disabled:cursor-not-allowed disabled:opacity-50"
104+
class="bg-accent text-ink shadow-action hover:bg-accent-dark focus-visible:outline-brand inline-flex items-center gap-3 px-5 py-3 font-bold transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
105105
data-python-run
106106
>
107107
<span aria-hidden="true">▶</span>
@@ -135,13 +135,76 @@ const resolvedChallenge = challenge
135135
</button>
136136
</div>
137137

138+
<label
139+
class="text-muted hover:bg-ink/5 hover:text-ink flex cursor-pointer items-center gap-2 px-3 py-2 text-sm font-medium transition-colors"
140+
title="Mostrar explicaciones sencillas para errores comunes de Python"
141+
>
142+
<input
143+
type="checkbox"
144+
class="accent-brand size-4"
145+
data-python-error-help-toggle
146+
/>
147+
148+
<span>Explicar errores</span>
149+
</label>
150+
138151
<a
139152
hidden
140153
class="text-brand decoration-accent hover:text-brand-dark ml-auto px-2 py-2 font-bold underline decoration-2 underline-offset-4 transition-colors"
141-
data-python-next></a>
142-
</div>
154+
data-python-next
155+
>
156+
</a>
157+
</footer>
143158
</div>
144159

160+
<aside
161+
hidden
162+
class="border-line-soft bg-panel border-l-brand mt-3 border-l-4 px-4 py-3"
163+
role="note"
164+
aria-live="polite"
165+
data-python-error-help
166+
>
167+
<div class="flex items-start gap-3">
168+
<span
169+
class="bg-brand/10 text-brand grid size-7 shrink-0 place-items-center rounded-full font-mono text-xs font-bold"
170+
aria-hidden="true"
171+
>
172+
?
173+
</span>
174+
175+
<div class="min-w-0 flex-1">
176+
<div class="flex flex-wrap items-center gap-x-2 gap-y-1">
177+
<span class="text-brand font-mono text-xs font-bold tracking-widest">
178+
AYUDA
179+
</span>
180+
181+
<span aria-hidden="true" class="text-muted"> · </span>
182+
183+
<span class="text-ink text-sm font-bold" data-python-error-help-name>
184+
</span>
185+
</div>
186+
187+
<div
188+
class="text-muted mt-1 text-sm leading-6"
189+
data-python-error-help-explanation
190+
>
191+
</div>
192+
193+
<div class="mt-2 flex items-start gap-2 text-sm leading-6">
194+
<span class="text-ink shrink-0 font-bold" aria-hidden="true">
195+
→
196+
</span>
197+
198+
<div>
199+
<span class="text-ink font-bold"> Sugerencia: </span>
200+
201+
<span class="text-muted" data-python-error-help-suggestion> </span>
202+
</div>
203+
</div>
204+
</div>
205+
</div>
206+
</aside>
207+
145208
<p
146209
hidden
147210
class="mt-5 border-l-4 px-4 py-3 text-sm font-medium"
@@ -176,7 +239,9 @@ const resolvedChallenge = challenge
176239
sandbox="allow-scripts"
177240
referrerpolicy="no-referrer"
178241
class="hidden"
179-
data-python-runner></iframe>
242+
data-python-runner
243+
>
244+
</iframe>
180245
</div>
181246

182247
<script>
@@ -188,7 +253,9 @@ const resolvedChallenge = challenge
188253
<style>
189254
@media (min-width: 64rem) {
190255
.python-workspace--with-results {
191-
grid-template-columns: minmax(0, 1.2fr) minmax(17rem, 0.8fr);
256+
grid-template-columns:
257+
minmax(0, 1.2fr)
258+
minmax(17rem, 0.8fr);
192259
}
193260
}
194261
</style>

‎src/components/station/editor/dom.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ export type PythonEditorElements = {
1313
chartContainer: HTMLElement;
1414
chart: HTMLImageElement;
1515
runner: HTMLIFrameElement;
16+
errorHelpToggle: HTMLInputElement;
17+
errorHelp: HTMLElement;
18+
errorHelpName: HTMLElement;
19+
errorHelpExplanation: HTMLElement;
20+
errorHelpSuggestion: HTMLElement;
1621
};
1722

1823
function queryRequiredElement<T extends Element>(
@@ -49,5 +54,22 @@ export function getPythonEditorElements(
4954
),
5055
chart: queryRequiredElement(editor, "[data-python-chart]"),
5156
runner: queryRequiredElement(editor, "[data-python-runner]"),
57+
errorHelpToggle: queryRequiredElement(
58+
editor,
59+
"[data-python-error-help-toggle]",
60+
),
61+
errorHelp: queryRequiredElement(editor, "[data-python-error-help]"),
62+
errorHelpName: queryRequiredElement(
63+
editor,
64+
"[data-python-error-help-name]",
65+
),
66+
errorHelpExplanation: queryRequiredElement(
67+
editor,
68+
"[data-python-error-help-explanation]",
69+
),
70+
errorHelpSuggestion: queryRequiredElement(
71+
editor,
72+
"[data-python-error-help-suggestion]",
73+
),
5274
};
5375
}

‎src/components/station/editor/initialize.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { createPythonCodeEditor } from "./code-editor";
22
import { getPythonEditorElements } from "./dom";
3+
import {
4+
getErrorHelpPreference,
5+
setErrorHelpPreference,
6+
} from "./python-error-help";
37
import { createPythonRunner } from "./runner";
48
import type { Challenge } from "./types";
59
import { createPythonEditorUi } from "./ui";
@@ -35,7 +39,20 @@ function initializePythonEditor(editor: HTMLElement) {
3539
moveHintIntoEditor(editor, elements.hintSlot);
3640

3741
const editorView = createPythonCodeEditor(elements.codeElement, starterCode);
42+
3843
const ui = createPythonEditorUi(elements);
44+
const errorHelpEnabled = getErrorHelpPreference();
45+
46+
elements.errorHelpToggle.checked = errorHelpEnabled;
47+
ui.setErrorHelpEnabled(errorHelpEnabled);
48+
49+
elements.errorHelpToggle.addEventListener("change", () => {
50+
const enabled = elements.errorHelpToggle.checked;
51+
52+
setErrorHelpPreference(enabled);
53+
ui.setErrorHelpEnabled(enabled);
54+
});
55+
3956
const runner = createPythonRunner({
4057
challenge,
4158
elements,
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
export type PythonErrorHelp = {
2+
errorName: string;
3+
explanation: string;
4+
suggestion: string;
5+
};
6+
7+
type PythonErrorRule = PythonErrorHelp & {
8+
pattern: RegExp;
9+
};
10+
11+
const ERROR_HELP_STORAGE_KEY = "pyschool-python-error-help";
12+
13+
const PYTHON_ERROR_RULES: PythonErrorRule[] = [
14+
{
15+
pattern: /\bIndentationError\b|\bTabError\b/,
16+
errorName: "Error de indentación",
17+
explanation:
18+
"Python encontró un bloque de código cuya sangría no es válida o no coincide con las líneas cercanas.",
19+
suggestion:
20+
"Revisa los espacios al comienzo de la línea. El código dentro de un if, for, while o función debe tener la misma indentación.",
21+
},
22+
{
23+
pattern: /\bSyntaxError\b/,
24+
errorName: "Error de sintaxis",
25+
explanation:
26+
"Python no pudo comprender una parte del código porque su estructura no es válida.",
27+
suggestion:
28+
"Revisa la línea indicada. Comprueba paréntesis, comillas, dos puntos y palabras reservadas.",
29+
},
30+
{
31+
pattern: /\bNameError\b/,
32+
errorName: "Nombre no definido",
33+
explanation:
34+
"Python intentó utilizar una variable o función que todavía no está definida.",
35+
suggestion:
36+
"Comprueba que el nombre exista, esté escrito correctamente y haya sido definido antes de utilizarlo.",
37+
},
38+
{
39+
pattern: /\bTypeError\b/,
40+
errorName: "Tipos incompatibles",
41+
explanation:
42+
"Python recibió un tipo de dato que no puede utilizar en esa operación o función.",
43+
suggestion:
44+
"Revisa los valores involucrados. Es posible que estés mezclando texto, números, listas u otros tipos incompatibles.",
45+
},
46+
{
47+
pattern: /\bIndexError\b/,
48+
errorName: "Índice fuera de rango",
49+
explanation:
50+
"Se intentó acceder a una posición que no existe dentro de una lista o secuencia.",
51+
suggestion:
52+
"Revisa la cantidad de elementos y recuerda que los índices de las listas comienzan en cero.",
53+
},
54+
{
55+
pattern: /\bKeyError\b/,
56+
errorName: "Clave inexistente",
57+
explanation:
58+
"Se intentó obtener una clave que no existe dentro de un diccionario.",
59+
suggestion:
60+
"Comprueba cómo está escrita la clave o verifica su existencia antes de acceder a ella.",
61+
},
62+
{
63+
pattern: /\bZeroDivisionError\b/,
64+
errorName: "División por cero",
65+
explanation: "Python intentó realizar una división cuyo divisor es cero.",
66+
suggestion:
67+
"Revisa el valor utilizado como divisor y valida que sea distinto de cero antes de dividir.",
68+
},
69+
];
70+
71+
export function getPythonErrorHelp(output: string): PythonErrorHelp | null {
72+
const rule = PYTHON_ERROR_RULES.find(({ pattern }) => pattern.test(output));
73+
74+
if (!rule) {
75+
return null;
76+
}
77+
78+
return {
79+
errorName: rule.errorName,
80+
explanation: rule.explanation,
81+
suggestion: rule.suggestion,
82+
};
83+
}
84+
85+
export function getErrorHelpPreference(): boolean {
86+
try {
87+
return window.localStorage.getItem(ERROR_HELP_STORAGE_KEY) === "true";
88+
} catch {
89+
return false;
90+
}
91+
}
92+
93+
export function setErrorHelpPreference(enabled: boolean): void {
94+
try {
95+
window.localStorage.setItem(ERROR_HELP_STORAGE_KEY, String(enabled));
96+
} catch {
97+
// La ayuda continúa funcionando durante la sesión
98+
// aunque el almacenamiento local no esté disponible.
99+
}
100+
}

0 commit comments

Comments
 (0)