Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/UX_AUDIT_AULA_3_3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Auditoría de interacción: Aula 3.3

## Objetivo

Hacer que el aula sea fácil de entender para estudiantes que están empezando y rápida de operar para un profesor con diez alumnos. Esta revisión no representa una certificación de Stanford, MIT o UDD: aplica principios públicos de diseño centrado en las personas y usabilidad publicados por esas instituciones.

## Referentes aplicados

- Stanford d.school: observar a las personas, definir el problema, probar y volver a iterar. [Design Thinking Bootleg](https://dschool.stanford.edu/tools/design-thinking-bootleg)
- MIT: priorizar facilidad de aprendizaje, eficiencia, prevención de errores y satisfacción. [Usability](https://ocw.mit.edu/courses/6-831-user-interface-design-and-implementation-spring-2011/pages/in-class-activities/usability/)
- MIT: mantener visibles el estado del sistema, el control del usuario y la protección de su trabajo. [Heuristic Evaluation](https://web.mit.edu/6.813/www/sp16/classes/20-heuristic-evaluation/)
- UDD: diseñar la interacción a partir del comportamiento humano y no solo desde la tecnología. [Diseño de Interacción Digital](https://www.udd.cl/internacional/catalogo-cursos-diseno-digital/)

## Recorrido del estudiante

### Problemas observados

- El inicio repetía dos accesos principales para continuar la misma tarea.
- Una frase prometía un catálogo de práctica que ya no está disponible para estudiantes.
- Las tareas se abrían mediante una flecha pequeña, aunque toda la fila parecía interactiva.
- El compilador usaba palabras como `tests`, `diagnósticos` y `provider`, poco útiles para un principiante.
- El resultado explicaba el estado técnico antes que el siguiente paso.

### Cambios aplicados

- Un solo botón principal lleva a la próxima tarea.
- Toda la fila de una tarea abre su workspace y funciona con teclado.
- El inicio explica que allí aparecen solamente las tareas asignadas.
- El compilador habla de pruebas, errores para corregir y próximos pasos.
- Los mensajes distinguen con palabras simples si todo salió bien, si hay resultados incorrectos o si el servicio no pudo ejecutar.

## Recorrido del profesor

### Problemas observados

- La navegación lateral se repetía en una segunda barra con las mismas ocho secciones.
- Ocho indicadores tenían la misma importancia y desplazaban la acción principal.
- El contador de revisiones podía mostrar estados sin una entrega realmente disponible.
- “Matriz del curso” y los criterios “Correctitud” o “Casos límite” requerían interpretación.
- La revisión podía generar un enlace inválido si la versión de una misión ya no estaba en el catálogo local.

### Cambios aplicados

- Se conserva una sola navegación.
- Las entregas por revisar aparecen primero; luego se muestran cuatro datos esenciales.
- Los indicadores secundarios quedan bajo “Ver más indicadores”.
- El contador usa exactamente la misma cola que la pantalla de revisiones.
- “Estado de las tareas” reemplaza “Matriz del curso”.
- La lista opcional de revisión usa tres preguntas directas y los botones indican claramente “Pedir cambios” o cuántos XP se aprobarán.
- “Ver misión” aparece únicamente cuando la versión existe.

## Validación pendiente con personas

Una evaluación heurística detecta problemas probables, pero no reemplaza observar a usuarios reales. Antes de agregar funciones grandes conviene hacer dos sesiones de 20 minutos: una con un estudiante resolviendo y entregando una misión, y otra con eeminionn creando una tarea y corrigiendo dos entregas. Registrar dónde dudan, qué texto leen y cuántas acciones necesitan.

## Funciones grandes no implementadas

Estas ideas necesitan una prueba breve de necesidad antes de construirlas:

1. Recorrido guiado para la primera sesión del estudiante y lista de preparación inicial para el profesor.
2. Plantillas de corrección configurables por tarea, con criterios definidos por eeminionn.
3. Corrección en lote para revisar o aprobar varias entregas similares.
4. Alertas pedagógicas que sugieran a quién ayudar según atrasos, errores repetidos y uso de pistas.
5. Panel histórico del curso con tendencias semanales y exportación de resultados.

La regla para una próxima iteración será sencilla: una función nueva debe reducir tiempo, evitar un error frecuente o responder una necesidad observada en las sesiones. Si no cumple una de esas condiciones, no entra al aula.
4 changes: 2 additions & 2 deletions v2/e2e/frontend-only.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@ test("frontend sandbox displays mentor controls but blocks mutations", async ({

await page.getByRole("link", { name: /^Revisiones/ }).click();
await expect(
page.getByRole("button", { name: "Solicitar cambios" }),
page.getByRole("button", { name: "Pedir cambios" }),
).toBeDisabled();
await expect(
page.getByRole("button", { name: "Aprobar y asignar XP" }),
page.getByRole("button", { name: /Aprobar \(\+\d+ XP\)/ }),
).toBeDisabled();
await page.getByRole("link", { name: "Premios", exact: true }).click();
await page.getByRole("button", { name: "Nuevo premio" }).click();
Expand Down
20 changes: 18 additions & 2 deletions v2/e2e/mentor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ test("mentor reviews a pending submission and awards XP once", async ({ page })
.getByLabel("Comentario en línea 2")
.fill("El acumulador debe actualizarse dentro del bucle.");
await page.getByRole("button", { name: "Agregar" }).click();
await page.getByLabel("Correctitud").check();
await page.getByRole("button", { name: "Aprobar y asignar XP" }).click();
await page.getByLabel("Da la respuesta correcta").check();
await page.getByRole("button", { name: /Aprobar \(\+\d+ XP\)/ }).click();
await expect(page.getByText("Cola al día")).toBeVisible();

await page.getByRole("button", { name: "Ver como estudiante" }).click();
Expand Down Expand Up @@ -56,6 +56,22 @@ test("mentor creates and edits a configurable invitation", async ({ page }) => {
await expect(invitation.getByRole("button", { name: "Copiar" })).toBeDisabled();
});

test("mentor overview prioritizes actions without duplicate navigation", async ({
page,
}) => {
await loginAsMentor(page);
await expect(
page.getByRole("button", { name: /entrega espera revisión/ }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Estado de las tareas" }),
).toBeVisible();
await expect(
page.getByRole("link", { name: "Resumen", exact: true }),
).toHaveCount(1);
await expect(page.getByText("Ver más indicadores")).toBeVisible();
});

test("mentor creates an assignment for selected students", async ({ page }) => {
await loginAsMentor(page);
await page.getByRole("link", { name: "Tareas", exact: true }).click();
Expand Down
8 changes: 6 additions & 2 deletions v2/e2e/student.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ test("opens an assigned mission in two actions and runs visible tests", async ({

await page.getByRole("button", { name: "Ejecutar" }).click();
await expect(
page.getByText("El código corre, pero aún no pasa todos los tests"),
page.getByText("Tu código funciona, pero hay resultados por corregir"),
).toBeVisible({ timeout: 15_000 });
await expect(page.getByText("Qué hacer ahora")).toBeVisible();
await expect(page.getByText("Compara un caso paso a paso")).toBeVisible();
Expand Down Expand Up @@ -68,6 +68,10 @@ test("prioritizes pending tasks and shows the mentor brief before the mission",
await expect(pending.locator(".assignment-row").first()).toContainText(
"Variables y acumuladores",
);
await expect(pending.locator(".assignment-row").first()).toHaveAttribute(
"href",
/mission\/la-once-de-tomatin/,
);
await approved.locator("summary").click();
await expect(approved).toContainText("Condiciones booleanas");

Expand Down Expand Up @@ -254,7 +258,7 @@ test("keeps student edits after running code opened from feedback", async ({
window.__TOMATIN_EDITOR__?.setValue(code);
}, revisedCode);
await page.getByRole("button", { name: "Ejecutar" }).click();
await expect(page.getByText("Todos los tests pasaron")).toBeVisible({
await expect(page.getByText("Todo salió bien")).toBeVisible({
timeout: 15_000,
});
await expect
Expand Down
19 changes: 7 additions & 12 deletions v2/src/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
useNavigate,
} from "react-router-dom";
import { ProfileAvatar } from "@/components/ProfileAvatar";
import { getPendingReviews } from "@/models/reviews";
import { useClassroom } from "@/state/classroom-context";

const studentNavigation = [
Expand Down Expand Up @@ -81,6 +82,7 @@ export function AppShell() {
const isMentor = isActorStaff && !isStudentPreview;
const students =
snapshot?.profiles.filter((entry) => entry.role === "student") ?? [];
const pendingReviews = snapshot ? getPendingReviews(snapshot).length : 0;

return (
<div className={`app-shell ${frontendOnly ? "frontend-only" : ""}`}>
Expand Down Expand Up @@ -109,7 +111,7 @@ export function AppShell() {
<nav className="sidebar-nav">
{isMentor ? (
<>
<p className="nav-label mentor-label">ADMINISTRACIÓN</p>
<p className="nav-label mentor-label">AULA</p>
{adminNavigation.map(({ to, label, icon: Icon, end }) => (
<NavLink
key={to}
Expand All @@ -122,15 +124,8 @@ export function AppShell() {
>
<Icon aria-hidden="true" />
<span>{label}</span>
{label === "Revisiones" &&
(snapshot?.progress.filter(
(entry) => entry.status === "awaiting_review",
).length ?? 0) > 0 ? (
<span className="nav-count">
{snapshot?.progress.filter(
(entry) => entry.status === "awaiting_review",
).length ?? 0}
</span>
{label === "Revisiones" && pendingReviews > 0 ? (
<span className="nav-count">{pendingReviews}</span>
) : null}
</NavLink>
))}
Expand Down Expand Up @@ -182,8 +177,8 @@ export function AppShell() {
{frontendOnly
? "Backend desactivado"
: backendMode === "supabase"
? "Supabase + Judge0"
: "Datos en este navegador"}
? "Todo funcionando"
: "Datos de ejemplo"}
</small>
</span>
</div>
Expand Down
44 changes: 44 additions & 0 deletions v2/src/models/reviews.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type {
Assignment,
Attempt,
ClassroomSnapshot,
Profile,
StudentProgress,
} from "@/types";

export interface PendingReview {
progress: StudentProgress;
attempt: Attempt;
student: Profile;
assignment: Assignment;
}

export function getPendingReviews(
snapshot: ClassroomSnapshot,
): PendingReview[] {
return snapshot.progress.flatMap((progress) => {
if (progress.status !== "awaiting_review") return [];

const attempt = snapshot.attempts
.filter(
(entry) =>
entry.userId === progress.userId &&
entry.assignmentId === progress.assignmentId &&
entry.kind === "submit",
)
.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)[0];
const student = snapshot.profiles.find(
(entry) => entry.id === progress.userId,
);
const assignment = snapshot.assignments.find(
(entry) => entry.id === progress.assignmentId,
);

return attempt && student && assignment
? [{ progress, attempt, student, assignment }]
: [];
});
}
42 changes: 21 additions & 21 deletions v2/src/pages/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
CircleDot,
ChevronDown,
Clock3,
Code2,
ExternalLink,
Github,
MessageSquareText,
Expand Down Expand Up @@ -110,8 +109,8 @@ export function Component() {
);
const status = state?.status ?? "not_started";
const overdue = isOverdue(assignment.dueAt, status);
return (
<article className="assignment-row" key={assignment.id}>
const content = (
<>
<div className="assignment-order">
{String(mission?.order ?? 0).padStart(2, "0")}
</div>
Expand All @@ -127,15 +126,24 @@ export function Component() {
</div>
<StatusBadge status={status} overdue={overdue} />
{mission ? (
<Link
className="icon-button"
to={`/mission/${mission.slug}?assignment=${assignment.id}`}
aria-label={`Trabajar en ${assignment.title}`}
title="Abrir tarea"
>
<span className="icon-button" aria-hidden="true">
<ArrowRight aria-hidden="true" />
</Link>
</span>
) : null}
</>
);
return mission ? (
<Link
className="assignment-row"
key={assignment.id}
to={`/mission/${mission.slug}?assignment=${assignment.id}`}
aria-label={`Abrir ${assignment.title}`}
>
{content}
</Link>
) : (
<article className="assignment-row" key={assignment.id}>
{content}
</article>
);
})}
Expand All @@ -157,22 +165,14 @@ export function Component() {
<p>
{isMentor
? "Revisa entregas, atrasos y actividad desde un solo lugar."
: "Tareas asignadas primero; el resto del catálogo queda disponible para practicar."}
: "Aquí aparecen solamente las tareas que te asignó tu profesor."}
</p>
</div>
{isMentor ? (
<Link className="button primary" to="/mentor">
<ShieldCheck aria-hidden="true" />
Abrir panel mentor
</Link>
) : nextMission ? (
<Link
className="button primary"
to={`/mission/${nextMission.slug}?assignment=${nextAssignment?.id}`}
>
<Code2 aria-hidden="true" />
Continuar
</Link>
) : null}
</header>

Expand Down Expand Up @@ -208,7 +208,7 @@ export function Component() {
</h2>
<p>
{nextAssignment
? `La próxima vence ${relativeDueDate(nextAssignment.dueAt)}: ${nextAssignment.title}.`
? `${nextAssignment.title} ${relativeDueDate(nextAssignment.dueAt).toLowerCase()}.`
: "No tienes entregas pendientes. Buen trabajo."}
</p>
</div>
Expand All @@ -217,7 +217,7 @@ export function Component() {
className="button primary"
to={`/mission/${nextMission.slug}?assignment=${nextAssignment.id}`}
>
Trabajar ahora <ArrowRight aria-hidden="true" />
Continuar <ArrowRight aria-hidden="true" />
</Link>
) : null}
</section>
Expand Down
Loading